diff --git a/cmd/exec.go b/cmd/exec.go index c9cdbad..0383de6 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -1,342 +1,219 @@ +/* +Copyright © 2024 NAME HERE +*/ package cmd import ( "context" "crypto/tls" - "encoding/json" "fmt" "io/ioutil" + "log" + "os" "strings" - "time" - "google.golang.org/grpc/credentials/insecure" + "github.com/golang/protobuf/ptypes/empty" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - "github.com/golang/protobuf/jsonpb" - "github.com/jhump/protoreflect/desc" - "github.com/jhump/protoreflect/dynamic" - "github.com/jhump/protoreflect/dynamic/grpcdynamic" - "github.com/jhump/protoreflect/grpcreflect" - "github.com/pterm/pterm" "github.com/spf13/cobra" - "github.com/spf13/viper" "google.golang.org/grpc" "google.golang.org/grpc/metadata" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" "gopkg.in/yaml.v2" ) -var execCmd = &cobra.Command{ - Use: "exec [verb] [service.resource]", - Short: "Execute an operation on a resource", - Long: `Execute an operation on a specified service and resource. - -Command format: - cfctl exec [Verb] [Service].[Resource] - -Examples: - cfctl exec create identity.Role - cfctl exec list identity.User - cfctl exec get identity.User -p user_id=user-123 - cfctl exec update identity.Project -f params.yaml`, - Args: cobra.ExactArgs(2), - RunE: runExec, +// Config structure to parse environment files +type Config struct { + Token string `yaml:"token"` + Endpoints map[string]string `yaml:"endpoints"` } -func init() { - rootCmd.AddCommand(execCmd) - - execCmd.Flags().StringArrayP("parameter", "p", []string{}, "Input parameter (-p key=value)") - execCmd.Flags().StringP("json-parameter", "j", "", "JSON parameter") - execCmd.Flags().StringP("file-parameter", "f", "", "YAML file parameter") - execCmd.Flags().StringP("api-version", "v", "v1", "API version") - execCmd.Flags().StringP("output", "o", "yaml", "Output format (yaml/json)") -} - -func runExec(cmd *cobra.Command, args []string) error { - verb := args[0] - serviceResource := args[1] - - service, resource, err := parseServiceResource(serviceResource) +// Load environment configuration +func loadConfig(environment string) (*Config, error) { + configPath := fmt.Sprintf("%s/.spaceone/environments/%s.yml", os.Getenv("HOME"), environment) + data, err := ioutil.ReadFile(configPath) if err != nil { - return fmt.Errorf("failed to parse service.resource: %w", err) + return nil, fmt.Errorf("could not read config file: %w", err) } - parameters, _ := cmd.Flags().GetStringArray("parameter") - jsonParameter, _ := cmd.Flags().GetString("json-parameter") - fileParameter, _ := cmd.Flags().GetString("file-parameter") - apiVersion, _ := cmd.Flags().GetString("api-version") - output, _ := cmd.Flags().GetString("output") - - params := parseParameters(parameters, jsonParameter, fileParameter) - return executeAPI(service, resource, verb, params, apiVersion, output) -} - -func parseServiceResource(serviceResource string) (string, string, error) { - parts := strings.Split(serviceResource, ".") - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid resource format. It should be [service].[resource]") - } - return parts[0], parts[1], nil -} - -func parseParameters(parameters []string, jsonParameter string, fileParameter string) map[string]interface{} { - params := make(map[string]interface{}) - - // Handle key=value parameters - fmt.Println("Command line parameters:", parameters) - for _, p := range parameters { - parts := strings.SplitN(p, "=", 2) - if len(parts) == 2 { - params[parts[0]] = parts[1] - fmt.Printf("Added parameter: %s = %v\n", parts[0], parts[1]) - } + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("could not unmarshal config: %w", err) } - // Handle JSON parameter - if jsonParameter != "" { - fmt.Println("JSON parameter:", jsonParameter) - var jsonParams map[string]interface{} - if err := json.Unmarshal([]byte(jsonParameter), &jsonParams); err == nil { - for k, v := range jsonParams { - params[k] = v - fmt.Printf("Added JSON parameter: %s = %v\n", k, v) - } - } else { - fmt.Printf("JSON parsing error: %v\n", err) - } - } - - // Handle file parameter - if fileParameter != "" { - fmt.Printf("Reading file: %s\n", fileParameter) - fileContent, err := ioutil.ReadFile(fileParameter) - if err == nil { - fmt.Printf("File content: %s\n", string(fileContent)) - var fileParams map[string]interface{} - if err := yaml.Unmarshal(fileContent, &fileParams); err == nil { - for k, v := range fileParams { - params[k] = v - fmt.Printf("Added file parameter: %s = %v\n", k, v) - } - } else { - fmt.Printf("YAML parsing error: %v\n", err) - } - } else { - fmt.Printf("File reading error: %v\n", err) - } - } - - fmt.Printf("Final parameters: %+v\n", params) - return params + return &config, nil } -func getMethodDescriptor(ctx context.Context, conn *grpc.ClientConn, service, resource, method string) (*desc.MethodDescriptor, error) { - reflectClient := grpcreflect.NewClientV1Alpha(ctx, reflectpb.NewServerReflectionClient(conn)) +// Load current environment from environment.yml +func fetchCurrentEnvironment() (string, error) { + envPath := fmt.Sprintf("%s/.spaceone/environment.yml", os.Getenv("HOME")) - // Convert to SpaceONE service namespace format - // Example: spaceone.api.identity.v1.User - fullServiceName := fmt.Sprintf("%s.api.dev.spaceone.dev", service) - - fmt.Printf("Looking for service: %s\n", fullServiceName) - - svc, err := reflectClient.ResolveService(fullServiceName) + data, err := ioutil.ReadFile(envPath) if err != nil { - return nil, fmt.Errorf("service not found %s: %v", fullServiceName, err) - } - - // Capitalize the first letter of the method name - methodName := strings.Title(method) - methodDesc := svc.FindMethodByName(methodName) - if methodDesc == nil { - return nil, fmt.Errorf("method not found %s in %s", methodName, fullServiceName) + return "", fmt.Errorf("could not read environment file: %w", err) } - return methodDesc, nil -} - -func createDynamicMessage(methodDesc *desc.MethodDescriptor, params map[string]interface{}) (*dynamic.Message, error) { - msgDesc := methodDesc.GetInputType() - msg := dynamic.NewMessage(msgDesc) - - jsonData, err := json.Marshal(params) - if err != nil { - return nil, fmt.Errorf("failed to convert parameters: %v", err) + var envConfig struct { + Environment string `yaml:"environment"` } - if err := msg.UnmarshalJSON(jsonData); err != nil { - return nil, fmt.Errorf("failed to unmarshal message: %v", err) + if err := yaml.Unmarshal(data, &envConfig); err != nil { + return "", fmt.Errorf("could not unmarshal environment config: %w", err) } - return msg, nil + return envConfig.Environment, nil } -func callAPI(conn *grpc.ClientConn, service, resource, verb string, params map[string]interface{}) (interface{}, error) { - ctx := context.Background() - - // Set timeout - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - - // Read token from SpaceONE config file - cfgFile, err := getEnvironmentConfig() - if err != nil { - return nil, fmt.Errorf("failed to find environment config file: %v", err) - } - - viper.SetConfigFile(cfgFile) - if err := viper.ReadInConfig(); err != nil { - return nil, fmt.Errorf("failed to read config file: %v", err) - } - - // Get token value - if token := viper.GetString("token"); token != "" { - md := metadata.New(map[string]string{ - "authorization": "Bearer " + token, - }) - ctx = metadata.NewOutgoingContext(ctx, md) - } else { - return nil, fmt.Errorf("token is not set") - } - - // Get method descriptor using reflection - methodDesc, err := getMethodDescriptor(ctx, conn, service, resource, verb) - if err != nil { - return nil, err - } - - // Create dynamic message - msg, err := createDynamicMessage(methodDesc, params) - if err != nil { - return nil, err - } - - // Create dynamic gRPC client - stub := grpcdynamic.NewStub(conn) - - // Invoke API - resp, err := stub.InvokeRpc(ctx, methodDesc, msg) - if err != nil { - return nil, fmt.Errorf("API call failed: %v", err) - } +// execCmd represents the exec command +var execCmd = &cobra.Command{ + Use: "exec [verb] [service].[resource]", + Short: "Execute a gRPC request to a specified service and resource", + Long: `Executes a gRPC command to a given service and resource based on environment configuration. + For example: cfctl exec list identity.User`, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + // Verb and service resource extraction + serviceResource := args[1] + rpcVerb := args[0] - // Handle response - if dynamicMsg, ok := resp.(*dynamic.Message); ok { - jsonMarshaler := &jsonpb.Marshaler{ - EmitDefaults: true, - OrigName: true, - Indent: " ", - } - jsonStr, err := jsonMarshaler.MarshalToString(dynamicMsg) + // Load environment + environment, err := fetchCurrentEnvironment() if err != nil { - return nil, fmt.Errorf("failed to convert response: %v", err) + log.Fatalf("Failed to get current environment: %v", err) } - var result map[string]interface{} - if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { - return nil, fmt.Errorf("failed to parse JSON: %v", err) + config, err := loadConfig(environment) + if err != nil { + log.Fatalf("Failed to load config for environment %s: %v", environment, err) } - return result, nil - } - return resp, nil -} + // Extract service name + parts := strings.Split(serviceResource, ".") + if len(parts) != 2 { + log.Fatalf("Invalid service format. Use [service].[resource]") + } + serviceName := parts[0] + resourceName := parts[1] -func executeAPI(service, resource, verb string, params map[string]interface{}, apiVersion, output string) error { - spinner, _ := pterm.DefaultSpinner.Start("Executing API call...") + // Modify endpoint format + endpoint := config.Endpoints[serviceName] + endpoint = strings.Replace(strings.Replace(endpoint, "grpc+ssl://", "", 1), "/v1", "", 1) - // Read SpaceONE config file - cfgFile, err := getEnvironmentConfig() - if err != nil { - spinner.Fail(fmt.Sprintf("failed to find environment config file: %v", err)) - return err - } - - viper.SetConfigFile(cfgFile) - if err := viper.ReadInConfig(); err != nil { - spinner.Fail(fmt.Sprintf("failed to read config file: %v", err)) - return err - } + // Set up secure connection + conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) + if err != nil { + log.Fatalf("Failed to connect to gRPC server: %v", err) + } + defer conn.Close() - endpointsMap := viper.GetStringMapString("endpoints") - endpoint, ok := endpointsMap[service] - if !ok { - spinner.Fail(fmt.Sprintf("failed to find endpoint for service %s", service)) - return fmt.Errorf("endpoint not found for service: %s", service) - } + // Set up gRPC reflection client + refClient := grpc_reflection_v1alpha.NewServerReflectionClient(conn) + ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Token) - // Parse endpoint - parts := strings.Split(endpoint, "://") - if len(parts) != 2 { - return fmt.Errorf("invalid endpoint format: %s", endpoint) - } + stream, err := refClient.ServerReflectionInfo(ctx) + if err != nil { + log.Fatalf("Failed to create reflection stream: %v", err) + } - scheme := parts[0] - hostPort := strings.SplitN(parts[1], "/", 2)[0] + // Request service list + req := &grpc_reflection_v1alpha.ServerReflectionRequest{ + MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_ListServices{ + ListServices: "*", + }, + } - var opts []grpc.DialOption - if scheme == "grpc+ssl" { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, + if err := stream.Send(req); err != nil { + log.Fatalf("Failed to send reflection request: %v", err) } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - fmt.Printf("Connecting to endpoint: %s\n", hostPort) + // Receive and search for the specific service + resp, err := stream.Recv() + if err != nil { + log.Fatalf("Failed to receive reflection response: %v", err) + } - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - spinner.Fail(fmt.Sprintf("failed to connect to server: %v", err)) - return err - } - defer conn.Close() + serviceFound := false + for _, svc := range resp.GetListServicesResponse().Service { + if strings.Contains(svc.Name, resourceName) { + serviceFound = true - client := grpc_reflection_v1alpha.NewServerReflectionClient(conn) - stream, err := client.ServerReflectionInfo(context.Background()) - if err != nil { - return fmt.Errorf("failed to create reflection client: %v", err) - } + // Request file descriptor for the specific service + fileDescriptorReq := &grpc_reflection_v1alpha.ServerReflectionRequest{ + MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileContainingSymbol{ + FileContainingSymbol: svc.Name, + }, + } - // Construct service name - fullServiceName := fmt.Sprintf("spaceone.api.%s.v1.%s", service, strings.Title(resource)) + if err := stream.Send(fileDescriptorReq); err != nil { + log.Fatalf("Failed to send file descriptor request: %v", err) + } - // Request method information - req := &grpc_reflection_v1alpha.ServerReflectionRequest{ - MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileContainingSymbol{ - FileContainingSymbol: fullServiceName, - }, - } + fileResp, err := stream.Recv() + if err != nil { + log.Fatalf("Failed to receive file descriptor response: %v", err) + } - if err := stream.Send(req); err != nil { - return fmt.Errorf("failed to send reflection request: %v", err) - } + // Parse the file descriptor response + fd := fileResp.GetFileDescriptorResponse() + if fd == nil { + log.Fatalf("No file descriptor found for service %s", svc.Name) + } - response, err := stream.Recv() - if err != nil { - return fmt.Errorf("failed to receive reflection response: %v", err) - } + // Extract methods from the file descriptor + fmt.Printf("Available methods for service %s:\n", svc.Name) + methodFound := false + for _, b := range fd.FileDescriptorProto { + protoDescriptor := &descriptorpb.FileDescriptorProto{} + if err := proto.Unmarshal(b, protoDescriptor); err != nil { + log.Fatalf("Failed to unmarshal file descriptor proto: %v", err) + } + + for _, service := range protoDescriptor.Service { + if service.GetName() == resourceName { + for _, method := range service.Method { + fmt.Printf("- %s\n", method.GetName()) + if method.GetName() == rpcVerb { + methodFound = true + // Call the method if it matches + fmt.Printf("Calling method %s on service %s...\n", rpcVerb, svc.Name) + + // Assuming the list method has no parameters + // Prepare the request message (in this case, an empty request) + req := &empty.Empty{} + response := new(empty.Empty) // Create a response placeholder + + // Make the RPC call using the client connection + err = conn.Invoke(ctx, fmt.Sprintf("/%s/%s", svc.Name, method.GetName()), req, response) + if err != nil { + log.Fatalf("Failed to call method %s: %v", rpcVerb, err) + } + + // Print the response + fmt.Printf("Response: %+v\n", response) + } + } + } + } + } - spinner.Success("API call complete") + if !methodFound { + log.Fatalf("Method %s not found in service %s", rpcVerb, resourceName) + } - // Handle output format - var outputData []byte - if output == "yaml" { - outputData, err = yaml.Marshal(response) - } else if output == "json" { - outputData, err = json.MarshalIndent(response, "", " ") - } else { - return fmt.Errorf("unsupported output format: %s", output) - } + break + } + } - if err != nil { - return fmt.Errorf("failed to format output: %v", err) - } + if !serviceFound { + log.Fatalf("Service %s not found", resourceName) + } + }, +} - fmt.Println(string(outputData)) - return nil +func init() { + rootCmd.AddCommand(execCmd) }