From a8cb9de51417f5f0e11d3688c17eb5395b774bdb Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 18 Nov 2024 23:41:07 +0900 Subject: [PATCH] feat: implement identity command, except UTF Encoding Signed-off-by: Youngjin Jo --- cmd/available/identity.go | 7 +- cmd/common/execute.go | 315 ++++++++++++++++++++++++++++++++++++++ cmd/other/exec.go | 54 +++---- 3 files changed, 347 insertions(+), 29 deletions(-) create mode 100644 cmd/common/execute.go diff --git a/cmd/available/identity.go b/cmd/available/identity.go index 39ea3a5..36c4731 100644 --- a/cmd/available/identity.go +++ b/cmd/available/identity.go @@ -1,17 +1,20 @@ package available import ( + "github.com/cloudforet-io/cfctl/cmd/common" "github.com/spf13/cobra" ) var IdentityCmd = &cobra.Command{ Use: "identity ", Short: "Interact with the Identity service", - Long: `Use this command to interact with the Identity service. Available verbs: list, get, create, update, delete`, + Long: `Use this command to interact with the Identity service. Available verbs: list, get, create, update, delete, ...`, Args: cobra.ExactArgs(2), GroupID: "available", RunE: func(cmd *cobra.Command, args []string) error { - return nil + verb := args[0] + resource := args[1] + return common.ExecuteCommand("identity", verb, resource) }, } diff --git a/cmd/common/execute.go b/cmd/common/execute.go new file mode 100644 index 0000000..ac3e550 --- /dev/null +++ b/cmd/common/execute.go @@ -0,0 +1,315 @@ +package common + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/csv" + "encoding/json" + "fmt" + "log" + "os" + "strings" + + "github.com/atotto/clipboard" + "github.com/pterm/pterm" + + "google.golang.org/grpc/metadata" + + "github.com/jhump/protoreflect/dynamic" + "github.com/jhump/protoreflect/grpcreflect" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + + "gopkg.in/yaml.v3" +) + +var parameters []string +var jsonParameter string +var fileParameter string +var apiVersion string +var outputFormat string +var copyToClipboard bool + +// Config structure to parse environment files +type Config struct { + Environment string `yaml:"environment"` + Environments map[string]Environment `yaml:"environments"` +} + +type Environment struct { + Token string `yaml:"token"` +} + +// ExecuteCommand handles the execution of gRPC commands for all services +func ExecuteCommand(serviceName, verb, resourceName string) error { + config, err := loadConfig() + if err != nil { + return fmt.Errorf("failed to load config: %v", err) + } + + respMap, err := fetchEndpointsMap(config, serviceName, verb, resourceName) + if err != nil { + return fmt.Errorf("failed to fetch endpoints map: %v", err) + } + + printData(respMap, outputFormat) + + return nil +} + +func loadConfig() (*Config, error) { + configPath := fmt.Sprintf("%s/.cfctl/config.yaml", os.Getenv("HOME")) + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("could not read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("could not unmarshal config: %w", err) + } + + return &config, nil +} + +func fetchCurrentEnvironment(config *Config) (*Environment, error) { + currentEnv, ok := config.Environments[config.Environment] + if !ok { + return nil, fmt.Errorf("current environment '%s' not found in config", config.Environment) + } + return ¤tEnv, nil +} + +func fetchEndpointsMap(config *Config, serviceName, verb, resourceName string) (map[string]interface{}, error) { + var envPrefix string + if strings.HasPrefix(config.Environment, "dev-") { + envPrefix = "dev" + } else if strings.HasPrefix(config.Environment, "stg-") { + envPrefix = "stg" + } + hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", serviceName, envPrefix) + + // Configure gRPC connection + var opts []grpc.DialOption + tlsConfig := &tls.Config{ + InsecureSkipVerify: false, + } + creds := credentials.NewTLS(tlsConfig) + opts = append(opts, grpc.WithTransportCredentials(creds)) + + // Establish the connection + conn, err := grpc.Dial(hostPort, opts...) + if err != nil { + return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", hostPort, err) + } + defer conn.Close() + + ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Environments[config.Environment].Token) + refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) + defer refClient.Reset() + + fullServiceName, err := discoverService(refClient, serviceName, resourceName) + if err != nil { + return nil, fmt.Errorf("failed to discover service: %v", err) + } + + serviceDesc, err := refClient.ResolveService(fullServiceName) + if err != nil { + return nil, fmt.Errorf("failed to resolve service %s: %v", fullServiceName, err) + } + + methodDesc := serviceDesc.FindMethodByName(verb) + if methodDesc == nil { + return nil, fmt.Errorf("method not found: %s", verb) + } + + reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) + respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) + + fullMethod := fmt.Sprintf("/%s/%s", fullServiceName, verb) + + err = conn.Invoke(ctx, fullMethod, reqMsg, respMsg) + if err != nil { + return nil, fmt.Errorf("failed to invoke method %s: %v", fullMethod, err) + } + + respMap, err := messageToMap(respMsg) + if err != nil { + return nil, fmt.Errorf("failed to convert response message to map: %v", err) + } + + return respMap, nil +} + +func discoverService(refClient *grpcreflect.Client, serviceName, resourceName string) (string, error) { + possibleVersions := []string{"v1", "v2"} + + for _, version := range possibleVersions { + fullServiceName := fmt.Sprintf("spaceone.api.%s.%s.%s", serviceName, version, resourceName) + if _, err := refClient.ResolveService(fullServiceName); err == nil { + return fullServiceName, nil + } + } + + return "", fmt.Errorf("service not found for %s.%s", serviceName, resourceName) +} + +func messageToMap(msg *dynamic.Message) (map[string]interface{}, error) { + result := make(map[string]interface{}) + fields := msg.GetKnownFields() + for _, fd := range fields { + val := msg.GetField(fd) + switch v := val.(type) { + case *dynamic.Message: + subMap, err := messageToMap(v) + if err != nil { + return nil, err + } + result[fd.GetName()] = subMap + case []*dynamic.Message: + var subList []map[string]interface{} + for _, subMsg := range v { + subMap, err := messageToMap(subMsg) + if err != nil { + return nil, err + } + subList = append(subList, subMap) + } + result[fd.GetName()] = subList + default: + result[fd.GetName()] = v + } + } + return result, nil +} + +func printData(data map[string]interface{}, format string) { + var output string + + switch format { + case "json": + dataBytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + log.Fatalf("Failed to marshal response to JSON: %v", err) + } + output = string(dataBytes) + fmt.Println(output) + + case "yaml": + var buf bytes.Buffer + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + err := encoder.Encode(data) + if err != nil { + log.Fatalf("Failed to marshal response to YAML: %v", err) + } + output = buf.String() + fmt.Printf("---\n%s\n", output) + + case "table": + output = printTable(data) + + case "csv": + output = printCSV(data) + + default: + var buf bytes.Buffer + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + err := encoder.Encode(data) + if err != nil { + log.Fatalf("Failed to marshal response to YAML: %v", err) + } + output = buf.String() + fmt.Printf("---\n%s\n", output) + } + + // Copy to clipboard if requested + if copyToClipboard && output != "" { + if err := clipboard.WriteAll(output); err != nil { + log.Fatalf("Failed to copy to clipboard: %v", err) + } + pterm.Success.Println("The output has been copied to your clipboard.") + } +} + +func printTable(data map[string]interface{}) string { + var output string + if results, ok := data["results"].([]interface{}); ok { + tableData := pterm.TableData{} + + // Extract headers + headers := []string{} + if len(results) > 0 { + if row, ok := results[0].(map[string]interface{}); ok { + for key := range row { + headers = append(headers, key) + } + } + } + + // Append headers to table data + tableData = append(tableData, headers) + + // Extract rows + for _, result := range results { + if row, ok := result.(map[string]interface{}); ok { + rowData := []string{} + for _, key := range headers { + rowData = append(rowData, fmt.Sprintf("%v", row[key])) + } + tableData = append(tableData, rowData) + } + } + + // Disable styling only for the table output + pterm.DisableStyling() + renderedOutput, err := pterm.DefaultTable.WithHasHeader(true).WithData(tableData).Srender() + pterm.EnableStyling() // Re-enable styling for other outputs + if err != nil { + log.Fatalf("Failed to render table: %v", err) + } + output = renderedOutput + fmt.Println(output) // Print to console + } + return output +} + +func printCSV(data map[string]interface{}) string { + var buf bytes.Buffer + if results, ok := data["results"].([]interface{}); ok { + writer := csv.NewWriter(&buf) + var headers []string + + // Extract headers + for _, result := range results { + if row, ok := result.(map[string]interface{}); ok { + if headers == nil { + for key := range row { + headers = append(headers, key) + } + writer.Write(headers) + } + + // Extract row values + var rowValues []string + for _, key := range headers { + if val, ok := row[key]; ok { + rowValues = append(rowValues, fmt.Sprintf("%v", val)) + } else { + rowValues = append(rowValues, "") + } + } + writer.Write(rowValues) + } + } + + writer.Flush() + output := buf.String() + fmt.Print(output) // Print to console + return output + } + return "" +} diff --git a/cmd/other/exec.go b/cmd/other/exec.go index ba79aaf..16f798a 100644 --- a/cmd/other/exec.go +++ b/cmd/other/exec.go @@ -12,17 +12,17 @@ import ( "os" "strings" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" "github.com/atotto/clipboard" "github.com/jhump/protoreflect/dynamic" "github.com/jhump/protoreflect/grpcreflect" "github.com/pterm/pterm" "github.com/spf13/cobra" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" "gopkg.in/yaml.v3" ) @@ -54,29 +54,6 @@ var ExecCmd = &cobra.Command{ Run: runExecCommand, } -func loadConfig() (*Config, error) { - configPath := fmt.Sprintf("%s/.cfctl/config.yaml", os.Getenv("HOME")) - data, err := os.ReadFile(configPath) - if err != nil { - return nil, fmt.Errorf("could not read config file: %w", err) - } - - var config Config - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("could not unmarshal config: %w", err) - } - - return &config, nil -} - -func fetchCurrentEnvironment(config *Config) (*Environment, error) { - currentEnv, ok := config.Environments[config.Environment] - if !ok { - return nil, fmt.Errorf("current environment '%s' not found in config", config.Environment) - } - return ¤tEnv, nil -} - func runExecCommand(cmd *cobra.Command, args []string) { config, err := loadConfig() if err != nil { @@ -195,6 +172,29 @@ func runExecCommand(cmd *cobra.Command, args []string) { printData(prettyMap, outputFormat) } +func loadConfig() (*Config, error) { + configPath := fmt.Sprintf("%s/.cfctl/config.yaml", os.Getenv("HOME")) + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("could not read config file: %w", err) + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("could not unmarshal config: %w", err) + } + + return &config, nil +} + +func fetchCurrentEnvironment(config *Config) (*Environment, error) { + currentEnv, ok := config.Environments[config.Environment] + if !ok { + return nil, fmt.Errorf("current environment '%s' not found in config", config.Environment) + } + return ¤tEnv, nil +} + func discoverService(refClient *grpcreflect.Client, serviceName, resourceName string) (string, error) { possibleVersions := []string{"v1", "v2"}