From 1d367fe8c96f613619e2fa442fc22aa016372a99 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 10:24:07 +0900 Subject: [PATCH 01/10] refactor: remove verb and resource mappings Signed-off-by: Youngjin Jo --- cmd/root.go | 167 ++++++++++++++++++---------------------------------- 1 file changed, 56 insertions(+), 111 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index e43ee60..4d14b51 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "fmt" "log" "os" @@ -10,13 +9,8 @@ import ( "time" "github.com/cloudforet-io/cfctl/cmd/commands" - "github.com/cloudforet-io/cfctl/pkg/format" pkggrpc "github.com/cloudforet-io/cfctl/pkg/grpc" "github.com/cloudforet-io/cfctl/pkg/rest" - "github.com/jhump/protoreflect/grpcreflect" - "google.golang.org/grpc" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - "gopkg.in/yaml.v3" "github.com/spf13/viper" @@ -262,79 +256,6 @@ func addDynamicServiceCommands() error { return err } - // For local environment - if config.Environment == "local" { - endpoint := strings.TrimPrefix(config.Endpoint, "grpc://") - - conn, err := grpc.Dial(endpoint, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(time.Second)) - if err != nil { - pterm.DefaultBox.WithTitle("Local gRPC Server Not Found"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgYellow)). - Printfln("Unable to connect to local gRPC server.\nPlease make sure your gRPC server is running on %s", config.Endpoint) - return nil - } - defer func(conn *grpc.ClientConn) { - err := conn.Close() - if err != nil { - - } - }(conn) - - ctx := context.Background() - refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - services, err := refClient.ListServices() - if err != nil { - return err - } - - // Check if plugin service exists - hasPlugin := false - microservices := make(map[string]bool) - - for _, service := range services { - // Skip grpc reflection and health check services - if strings.HasPrefix(service, "grpc.") { - continue - } - - // Handle plugin service - if strings.Contains(service, ".plugin.") { - hasPlugin = true - continue - } - - // Handle SpaceONE microservices - if strings.Contains(service, "spaceone.api.") { - parts := strings.Split(service, ".") - if len(parts) >= 4 { - serviceName := parts[2] - // Skip core service and version prefixes - if serviceName != "core" && !strings.HasPrefix(serviceName, "v") { - microservices[serviceName] = true - } - } - } - } - - if hasPlugin { - cmd := createServiceCommand("plugin") - cmd.GroupID = "available" - rootCmd.AddCommand(cmd) - } - - // Add commands for other microservices - for serviceName := range microservices { - cmd := createServiceCommand(serviceName) - cmd.GroupID = "available" - rootCmd.AddCommand(cmd) - } - - return nil - } - // For non-local environments endpoint := config.Endpoint var apiEndpoint string @@ -378,35 +299,27 @@ func addDynamicServiceCommands() error { return nil } - // If no cached endpoints, fetch them + // If no cached endpoints, show progress with detailed messages progressbar, _ := pterm.DefaultProgressbar. WithTotal(4). - WithTitle("Initializing services"). + WithTitle(fmt.Sprintf("Setting up %s environment", config.Environment)). Start() - progressbar.UpdateTitle("Fetching available services") + progressbar.UpdateTitle("Fetching available service endpoints from the API server") endpointsMap, err := rest.FetchEndpointsMap(apiEndpoint) if err != nil { return fmt.Errorf("failed to fetch services: %v", err) } - progressbar.Increment() - time.Sleep(time.Millisecond * 300) - progressbar.UpdateTitle("Creating cache for faster subsequent runs") + progressbar.UpdateTitle(fmt.Sprintf("Caching endpoints to %s/.cfctl/cache for faster access", os.Getenv("HOME"))) cachedEndpointsMap = endpointsMap if err := saveEndpointsCache(endpointsMap); err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to cache endpoints: %v\n", err) } - - progressbar.Increment() - time.Sleep(time.Millisecond * 300) - - //progressbar.UpdateTitle("Finalizing") progressbar.Increment() - time.Sleep(time.Millisecond * 300) - fmt.Println() + progressbar.UpdateTitle("Registering available service commands") // Add commands based on the current service currentService := "" if strings.HasPrefix(endpoint, "grpc+ssl://") { @@ -431,8 +344,12 @@ func addDynamicServiceCommands() error { rootCmd.AddCommand(cmd) } } + progressbar.Increment() + progressbar.UpdateTitle("Setup completed successfully!") progressbar.Increment() + + fmt.Println() // Add newline after progress bar return nil } @@ -563,33 +480,61 @@ func loadConfig() (*Config, error) { func createServiceCommand(serviceName string) *cobra.Command { cmd := &cobra.Command{ - Use: serviceName, + Use: serviceName + " [verb] [resource] [flags]", Short: fmt.Sprintf("Interact with the %s service", serviceName), Long: fmt.Sprintf("Use this command to interact with the %s service.", serviceName), GroupID: "available", - } + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } - cmd.AddGroup(&cobra.Group{ - ID: "available", - Title: "Available Commands:", - }, &cobra.Group{ - ID: "other", - Title: "Other Commands:", - }) + verb := args[0] + resource := "" + if len(args) > 1 { + resource = args[1] + } - cmd.SetHelpFunc(format.SetParentHelp) + if verb == "api_resources" { + return commands.ListAPIResources(serviceName) + } - apiResourcesCmd := commands.FetchApiResourcesCmd(serviceName) - apiResourcesCmd.GroupID = "available" - cmd.AddCommand(apiResourcesCmd) + parameters, _ := cmd.Flags().GetStringArray("parameter") + jsonParameter, _ := cmd.Flags().GetString("json-parameter") + fileParameter, _ := cmd.Flags().GetString("file-parameter") + outputFormat, _ := cmd.Flags().GetString("output") + copyToClipboard, _ := cmd.Flags().GetBool("copy") + + options := &pkggrpc.FetchOptions{ + Parameters: parameters, + JSONParameter: jsonParameter, + FileParameter: fileParameter, + OutputFormat: outputFormat, + CopyToClipboard: copyToClipboard, + } - err := pkggrpc.AddVerbCommands(cmd, serviceName, "other") - if err != nil { - _, err2 := fmt.Fprintf(os.Stderr, "Error adding verb commands for %s: %v\n", serviceName, err) - if err2 != nil { + _, err := pkggrpc.FetchService(serviceName, verb, resource, options) + if err != nil { + pterm.Error.Println(err.Error()) + return nil + } return nil - } - } + }, + Example: fmt.Sprintf(` # List available API resources + cfctl %[1]s api_resources + + # List resources + cfctl %[1]s list User + + # Create a resource + cfctl %[1]s create Project -p name=test`, serviceName), + } + + cmd.Flags().StringArrayP("parameter", "p", []string{}, "Input Parameter (-p = -p ...)") + cmd.Flags().StringP("json-parameter", "j", "", "JSON type parameter") + cmd.Flags().StringP("file-parameter", "f", "", "YAML file parameter") + cmd.Flags().StringP("output", "o", "yaml", "Output format (yaml, json, table, csv)") + cmd.Flags().BoolP("copy", "y", false, "Copy the output to the clipboard") return cmd } From c91afbb7b83962c6da79ff2c7a1d3ab571b56ac9 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 10:54:16 +0900 Subject: [PATCH 02/10] refactor: modify services' api_resources Signed-off-by: Youngjin Jo --- cmd/commands/api_resources.go | 41 +++++++------------ cmd/root.go | 20 +++++----- pkg/format/output.go | 75 +++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 37 deletions(-) diff --git a/cmd/commands/api_resources.go b/cmd/commands/api_resources.go index d947c54..7ba05f1 100644 --- a/cmd/commands/api_resources.go +++ b/cmd/commands/api_resources.go @@ -11,8 +11,8 @@ import ( "strings" "github.com/cloudforet-io/cfctl/pkg/configs" + "github.com/cloudforet-io/cfctl/pkg/format" "github.com/jhump/protoreflect/grpcreflect" - "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" "google.golang.org/grpc" @@ -28,12 +28,12 @@ func FetchApiResourcesCmd(serviceName string) *cobra.Command { Use: "api_resources", Short: fmt.Sprintf("Displays supported API resources for the %s service", serviceName), RunE: func(cmd *cobra.Command, args []string) error { - return listAPIResources(serviceName) + return ListAPIResources(serviceName) }, } } -func listAPIResources(serviceName string) error { +func ListAPIResources(serviceName string) error { setting, err := configs.LoadSetting() if err != nil { return fmt.Errorf("failed to load setting: %v", err) @@ -58,33 +58,33 @@ func listAPIResources(serviceName string) error { return data[i][0] < data[j][0] }) - renderAPITable(data) + format.RenderTable(data) return nil } func getServiceEndpoint(config *configs.Setting, serviceName string) (string, error) { envConfig := config.Environments[config.Environment] - if envConfig.URL == "" { - return "", fmt.Errorf("URL not found in environment config") + if envConfig.Endpoint == "" { + return "", fmt.Errorf("endpoint not found in environment config") } - // Parse URL to get environment - urlParts := strings.Split(envConfig.URL, ".") - if len(urlParts) < 4 { - return "", fmt.Errorf("invalid URL format: %s", envConfig.URL) + // Parse endpoint to get environment + endpointParts := strings.Split(envConfig.Endpoint, ".") + if len(endpointParts) < 4 { + return "", fmt.Errorf("invalid endpoint format: %s", envConfig.Endpoint) } var envPrefix string - for i, part := range urlParts { - if part == "console" && i+1 < len(urlParts) { - envPrefix = urlParts[i+1] // Get the part after "console" (dev or stg) + for i, part := range endpointParts { + if part == "console" && i+1 < len(endpointParts) { + envPrefix = endpointParts[i+1] // Get the part after "console" (dev or stg) break } } if envPrefix == "" { - return "", fmt.Errorf("environment prefix not found in URL: %s", envConfig.URL) + return "", fmt.Errorf("environment prefix not found in endpoint: %s", envConfig.Endpoint) } endpoint := fmt.Sprintf("grpc+ssl://%s.api.%s.spaceone.dev:443", serviceName, envPrefix) @@ -237,16 +237,3 @@ func fetchServiceResources(serviceName, endpoint string, shortNamesMap map[strin return data, nil } - -func renderAPITable(data [][]string) { - // Create table header - table := pterm.TableData{ - {"Service", "Verb", "Resource", "Short Names"}, - } - - // Add data rows - table = append(table, data...) - - // Render the table - pterm.DefaultTable.WithHasHeader().WithData(table).Render() -} diff --git a/cmd/root.go b/cmd/root.go index 4d14b51..f75f5b0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -480,13 +480,17 @@ func loadConfig() (*Config, error) { func createServiceCommand(serviceName string) *cobra.Command { cmd := &cobra.Command{ - Use: serviceName + " [verb] [resource] [flags]", + Use: serviceName + " [verb] [resource]", Short: fmt.Sprintf("Interact with the %s service", serviceName), Long: fmt.Sprintf("Use this command to interact with the %s service.", serviceName), GroupID: "available", RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return cmd.Help() + pterm.Info.Println("To see available API resources, run:") + pterm.Info.Printf(" cfctl %s api_resources\n", serviceName) + cmd.Help() + fmt.Println() // Add newline + return nil } verb := args[0] @@ -520,16 +524,12 @@ func createServiceCommand(serviceName string) *cobra.Command { } return nil }, - Example: fmt.Sprintf(` # List available API resources - cfctl %[1]s api_resources - - # List resources - cfctl %[1]s list User - - # Create a resource - cfctl %[1]s create Project -p name=test`, serviceName), } + // Add api_resources subcommand + cmd.AddCommand(commands.FetchApiResourcesCmd(serviceName)) + + // Add existing flags cmd.Flags().StringArrayP("parameter", "p", []string{}, "Input Parameter (-p = -p ...)") cmd.Flags().StringP("json-parameter", "j", "", "JSON type parameter") cmd.Flags().StringP("file-parameter", "f", "", "YAML file parameter") diff --git a/pkg/format/output.go b/pkg/format/output.go index 28ea998..6f6696f 100644 --- a/pkg/format/output.go +++ b/pkg/format/output.go @@ -105,3 +105,78 @@ func printSortedBulletList(cmd *cobra.Command, sectionTitle string) { pterm.DefaultBulletList.WithItems(listItems).Render() cmd.Println() } + +func RenderTable(data [][]string) { + // Get terminal width + terminalWidth := pterm.GetTerminalWidth() + + // Calculate verb column width based on terminal width + verbColumnWidth := terminalWidth / 2 + + // Define alternating colors for better readability + alternateColors := []pterm.Color{ + pterm.FgDefault, + pterm.FgYellow, + } + currentColorIndex := 0 + previousService := "" + + // Create table with headers + table := pterm.TableData{{"Service", "Verb", "Resource", "Short Names"}} + + for _, row := range data { + service := row[0] + + // Switch color if the service name changes + if service != previousService { + currentColorIndex = (currentColorIndex + 1) % len(alternateColors) + previousService = service + } + + // Apply the current color + color := alternateColors[currentColorIndex] + coloredStyle := pterm.NewStyle(color) + + // Color the entire row + serviceColored := coloredStyle.Sprint(service) + resourceColored := coloredStyle.Sprint(row[2]) + shortNamesColored := coloredStyle.Sprint(row[3]) + + // Split verbs into multiple lines if needed + verbs := splitIntoLinesWithComma(row[1], verbColumnWidth) + for i, line := range verbs { + if i == 0 { + table = append(table, []string{serviceColored, coloredStyle.Sprint(line), resourceColored, shortNamesColored}) + } else { + table = append(table, []string{"", coloredStyle.Sprint(line), "", ""}) + } + } + } + + // Render the table + pterm.DefaultTable.WithHasHeader().WithData(table).Render() +} + +func splitIntoLinesWithComma(text string, maxWidth int) []string { + words := strings.Split(text, ", ") + var lines []string + var currentLine string + + for _, word := range words { + if len(currentLine)+len(word)+2 > maxWidth && currentLine != "" { + lines = append(lines, strings.TrimSuffix(currentLine, ", ")) + currentLine = word + } else { + if currentLine != "" { + currentLine += ", " + } + currentLine += word + } + } + + if currentLine != "" { + lines = append(lines, strings.TrimSuffix(currentLine, ", ")) + } + + return lines +} From 4df8188a78ca62e39a746f929ed86f38ee61fcde Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 11:21:10 +0900 Subject: [PATCH 03/10] chore: add transport directory to merge grpc and rest Signed-off-by: Youngjin Jo --- cmd/other/api_resources.go | 4 ++-- cmd/other/login.go | 6 +++--- cmd/other/setting.go | 9 ++++----- cmd/other/short_names.go | 4 ++-- cmd/root.go | 11 +++++------ pkg/{rest => transport}/endpoint.go | 2 +- pkg/{grpc => transport}/reflection.go | 2 +- pkg/{grpc => transport}/service.go | 7 +++---- pkg/{grpc => transport}/verb.go | 9 ++++----- 9 files changed, 25 insertions(+), 29 deletions(-) rename pkg/{rest => transport}/endpoint.go (99%) rename pkg/{grpc => transport}/reflection.go (99%) rename pkg/{grpc => transport}/service.go (99%) rename pkg/{grpc => transport}/verb.go (98%) diff --git a/cmd/other/api_resources.go b/cmd/other/api_resources.go index ecceb83..3d4be70 100644 --- a/cmd/other/api_resources.go +++ b/cmd/other/api_resources.go @@ -13,7 +13,7 @@ import ( "strings" "sync" - "github.com/cloudforet-io/cfctl/pkg/rest" + "github.com/cloudforet-io/cfctl/pkg/transport" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v2" @@ -90,7 +90,7 @@ var ApiResourcesCmd = &cobra.Command{ return } - endpointsMap, err = rest.FetchEndpointsMap(endpoint) + endpointsMap, err = transport.FetchEndpointsMap(endpoint) if err != nil { log.Fatalf("Failed to fetch endpointsMap from '%s': %v", endpoint, err) } diff --git a/cmd/other/login.go b/cmd/other/login.go index 73ab1ba..aafefdb 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -20,7 +20,7 @@ import ( "time" "github.com/AlecAivazis/survey/v2" - "github.com/cloudforet-io/cfctl/pkg/rest" + "github.com/cloudforet-io/cfctl/pkg/transport" "github.com/eiannone/keyboard" "google.golang.org/grpc/metadata" @@ -426,7 +426,7 @@ func executeUserLogin(currentEnv string) { } // Get console API endpoint - apiEndpoint, err := rest.GetAPIEndpoint(baseUrl) + apiEndpoint, err := transport.GetAPIEndpoint(baseUrl) if err != nil { pterm.Error.Printf("Failed to get API endpoint: %v\n", err) exitWithError() @@ -434,7 +434,7 @@ func executeUserLogin(currentEnv string) { restIdentityEndpoint := apiEndpoint + "/identity" // Get identity service endpoint - identityEndpoint, hasIdentityService, err := rest.GetIdentityEndpoint(apiEndpoint) + identityEndpoint, hasIdentityService, err := transport.GetIdentityEndpoint(apiEndpoint) if err != nil { pterm.Error.Printf("Failed to get identity endpoint: %v\n", err) exitWithError() diff --git a/cmd/other/setting.go b/cmd/other/setting.go index c3c0ddf..09f93db 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -15,7 +15,7 @@ import ( "sort" "strings" - "github.com/cloudforet-io/cfctl/pkg/rest" + "github.com/cloudforet-io/cfctl/pkg/transport" "gopkg.in/yaml.v3" "google.golang.org/grpc" @@ -23,7 +23,6 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - pkggrpc "github.com/cloudforet-io/cfctl/pkg/grpc" "github.com/jhump/protoreflect/dynamic" "github.com/jhump/protoreflect/grpcreflect" "github.com/pterm/pterm" @@ -588,13 +587,13 @@ You can either specify a new endpoint URL directly or use the service-based endp var identityEndpoint, restIdentityEndpoint string var hasIdentityService bool if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { - apiEndpoint, err := rest.GetAPIEndpoint(endpoint) + apiEndpoint, err := transport.GetAPIEndpoint(endpoint) if err != nil { pterm.Error.Printf("Failed to get API endpoint: %v\n", err) return } - identityEndpoint, hasIdentityService, err = rest.GetIdentityEndpoint(apiEndpoint) + identityEndpoint, hasIdentityService, err = transport.GetIdentityEndpoint(apiEndpoint) if err != nil { pterm.Error.Printf("Failed to get identity endpoint: %v\n", err) return @@ -1373,7 +1372,7 @@ func updateSetting(envName, endpoint, envSuffix string) { proxyKey := fmt.Sprintf("environments.%s.proxy", envName) if strings.HasPrefix(endpoint, "grpc://") || strings.HasPrefix(endpoint, "grpc+ssl://") { - isProxy, err := pkggrpc.CheckIdentityProxyAvailable(endpoint) + isProxy, err := transport.CheckIdentityProxyAvailable(endpoint) if err != nil { pterm.Warning.Printf("Failed to check gRPC endpoint: %v\n", err) v.Set(proxyKey, true) diff --git a/cmd/other/short_names.go b/cmd/other/short_names.go index 6059c10..2cd945e 100644 --- a/cmd/other/short_names.go +++ b/cmd/other/short_names.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "github.com/cloudforet-io/cfctl/pkg/rest" + "github.com/cloudforet-io/cfctl/pkg/transport" "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -53,7 +53,7 @@ func validateServiceCommand(service, verb, resource string) error { } // Fetch endpoints map - endpointsMap, err := rest.FetchEndpointsMap(endpoint) + endpointsMap, err := transport.FetchEndpointsMap(endpoint) if err != nil { return fmt.Errorf("failed to fetch endpoints: %v", err) } diff --git a/cmd/root.go b/cmd/root.go index f75f5b0..2468fdf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,8 +9,7 @@ import ( "time" "github.com/cloudforet-io/cfctl/cmd/commands" - pkggrpc "github.com/cloudforet-io/cfctl/pkg/grpc" - "github.com/cloudforet-io/cfctl/pkg/rest" + "github.com/cloudforet-io/cfctl/pkg/transport" "gopkg.in/yaml.v3" "github.com/spf13/viper" @@ -263,7 +262,7 @@ func addDynamicServiceCommands() error { if strings.HasPrefix(endpoint, "grpc+ssl://") { apiEndpoint = endpoint } else if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { - apiEndpoint, err = rest.GetAPIEndpoint(endpoint) + apiEndpoint, err = transport.GetAPIEndpoint(endpoint) if err != nil { return fmt.Errorf("failed to get API endpoint: %v", err) } @@ -306,7 +305,7 @@ func addDynamicServiceCommands() error { Start() progressbar.UpdateTitle("Fetching available service endpoints from the API server") - endpointsMap, err := rest.FetchEndpointsMap(apiEndpoint) + endpointsMap, err := transport.FetchEndpointsMap(apiEndpoint) if err != nil { return fmt.Errorf("failed to fetch services: %v", err) } @@ -509,7 +508,7 @@ func createServiceCommand(serviceName string) *cobra.Command { outputFormat, _ := cmd.Flags().GetString("output") copyToClipboard, _ := cmd.Flags().GetBool("copy") - options := &pkggrpc.FetchOptions{ + options := &transport.FetchOptions{ Parameters: parameters, JSONParameter: jsonParameter, FileParameter: fileParameter, @@ -517,7 +516,7 @@ func createServiceCommand(serviceName string) *cobra.Command { CopyToClipboard: copyToClipboard, } - _, err := pkggrpc.FetchService(serviceName, verb, resource, options) + _, err := transport.FetchService(serviceName, verb, resource, options) if err != nil { pterm.Error.Println(err.Error()) return nil diff --git a/pkg/rest/endpoint.go b/pkg/transport/endpoint.go similarity index 99% rename from pkg/rest/endpoint.go rename to pkg/transport/endpoint.go index b1ff4b5..48af3e3 100644 --- a/pkg/rest/endpoint.go +++ b/pkg/transport/endpoint.go @@ -1,4 +1,4 @@ -package rest +package transport import ( "bytes" diff --git a/pkg/grpc/reflection.go b/pkg/transport/reflection.go similarity index 99% rename from pkg/grpc/reflection.go rename to pkg/transport/reflection.go index daae7c8..00719ea 100644 --- a/pkg/grpc/reflection.go +++ b/pkg/transport/reflection.go @@ -1,4 +1,4 @@ -package grpc +package transport import ( "context" diff --git a/pkg/grpc/service.go b/pkg/transport/service.go similarity index 99% rename from pkg/grpc/service.go rename to pkg/transport/service.go index 2e16767..b91a8de 100644 --- a/pkg/grpc/service.go +++ b/pkg/transport/service.go @@ -1,4 +1,4 @@ -package grpc +package transport import ( "bytes" @@ -15,7 +15,6 @@ import ( "strings" "github.com/cloudforet-io/cfctl/pkg/format" - "github.com/cloudforet-io/cfctl/pkg/rest" "github.com/eiannone/keyboard" "github.com/spf13/viper" @@ -168,13 +167,13 @@ func FetchService(serviceName string, verb string, resourceName string, options if config.Environment == "local" { hostPort = strings.TrimPrefix(config.Environments[config.Environment].Endpoint, "grpc://") } else { - apiEndpoint, err = rest.GetAPIEndpoint(config.Environments[config.Environment].Endpoint) + apiEndpoint, err = GetAPIEndpoint(config.Environments[config.Environment].Endpoint) if err != nil { pterm.Error.Printf("Failed to get API endpoint: %v\n", err) os.Exit(1) } // Get identity service endpoint - identityEndpoint, hasIdentityService, err = rest.GetIdentityEndpoint(apiEndpoint) + identityEndpoint, hasIdentityService, err = GetIdentityEndpoint(apiEndpoint) if err != nil { pterm.Error.Printf("Failed to get identity endpoint: %v\n", err) os.Exit(1) diff --git a/pkg/grpc/verb.go b/pkg/transport/verb.go similarity index 98% rename from pkg/grpc/verb.go rename to pkg/transport/verb.go index 5063148..5b259ba 100644 --- a/pkg/grpc/verb.go +++ b/pkg/transport/verb.go @@ -1,6 +1,6 @@ // common/fetchVerb.go -package grpc +package transport import ( "context" @@ -15,7 +15,6 @@ import ( "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/cloudforet-io/cfctl/pkg/format" - "github.com/cloudforet-io/cfctl/pkg/rest" "github.com/jhump/protoreflect/grpcreflect" "github.com/pterm/pterm" "google.golang.org/grpc" @@ -323,12 +322,12 @@ func fetchVerbResourceMap(serviceName string, config *configs.Setting) (map[stri tlsConfig := &tls.Config{ InsecureSkipVerify: false, } - apiEndpoint, _ := rest.GetAPIEndpoint(envConfig.Endpoint) - identityEndpoint, hasIdentityService, err := rest.GetIdentityEndpoint(apiEndpoint) + apiEndpoint, _ := GetAPIEndpoint(envConfig.Endpoint) + identityEndpoint, hasIdentityService, err := GetIdentityEndpoint(apiEndpoint) if !hasIdentityService { // Get endpoints map first - endpointsMap, err := rest.FetchEndpointsMap(apiEndpoint) + endpointsMap, err := FetchEndpointsMap(apiEndpoint) if err != nil { return nil, fmt.Errorf("failed to fetch endpoints map: %v", err) } From 456e1a25d6b97a01268c266a63d4cd9f4bf842fb Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 11:39:48 +0900 Subject: [PATCH 04/10] refactor: get api_resources for grpc and rest protocol Signed-off-by: Youngjin Jo --- cmd/commands/api_resources.go | 32 +++--------------------------- pkg/transport/endpoint.go | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/cmd/commands/api_resources.go b/cmd/commands/api_resources.go index 7ba05f1..6375f53 100644 --- a/cmd/commands/api_resources.go +++ b/cmd/commands/api_resources.go @@ -12,6 +12,7 @@ import ( "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/cloudforet-io/cfctl/pkg/format" + "github.com/cloudforet-io/cfctl/pkg/transport" "github.com/jhump/protoreflect/grpcreflect" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -39,7 +40,8 @@ func ListAPIResources(serviceName string) error { return fmt.Errorf("failed to load setting: %v", err) } - endpoint, err := getServiceEndpoint(setting, serviceName) + //endpoint, err := getServiceEndpoint(setting, serviceName) + endpoint, err := transport.GetServiceEndpoint(setting, serviceName) if err != nil { return fmt.Errorf("failed to get endpoint for service %s: %v", serviceName, err) } @@ -63,34 +65,6 @@ func ListAPIResources(serviceName string) error { return nil } -func getServiceEndpoint(config *configs.Setting, serviceName string) (string, error) { - envConfig := config.Environments[config.Environment] - if envConfig.Endpoint == "" { - return "", fmt.Errorf("endpoint not found in environment config") - } - - // Parse endpoint to get environment - endpointParts := strings.Split(envConfig.Endpoint, ".") - if len(endpointParts) < 4 { - return "", fmt.Errorf("invalid endpoint format: %s", envConfig.Endpoint) - } - - var envPrefix string - for i, part := range endpointParts { - if part == "console" && i+1 < len(endpointParts) { - envPrefix = endpointParts[i+1] // Get the part after "console" (dev or stg) - break - } - } - - if envPrefix == "" { - return "", fmt.Errorf("environment prefix not found in endpoint: %s", envConfig.Endpoint) - } - - endpoint := fmt.Sprintf("grpc+ssl://%s.api.%s.spaceone.dev:443", serviceName, envPrefix) - return endpoint, nil -} - func loadShortNames() (map[string]string, error) { home, err := os.UserHomeDir() if err != nil { diff --git a/pkg/transport/endpoint.go b/pkg/transport/endpoint.go index 48af3e3..8d788d8 100644 --- a/pkg/transport/endpoint.go +++ b/pkg/transport/endpoint.go @@ -10,6 +10,7 @@ import ( "os" "strings" + "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/jhump/protoreflect/dynamic" "github.com/jhump/protoreflect/grpcreflect" "github.com/pterm/pterm" @@ -126,6 +127,42 @@ func GetIdentityEndpoint(apiEndpoint string) (string, bool, error) { return "", false, nil } +func GetServiceEndpoint(config *configs.Setting, serviceName string) (string, error) { + envConfig := config.Environments[config.Environment] + if envConfig.Endpoint == "" { + return "", fmt.Errorf("endpoint not found in environment config") + } + + // Get console API endpoint + apiEndpoint, err := GetAPIEndpoint(envConfig.Endpoint) + if err != nil { + pterm.Error.Printf("Failed to get API endpoint: %v\n", err) + os.Exit(1) + } + + // Get identity endpoint + identityEndpoint, _, err := GetIdentityEndpoint(apiEndpoint) + + // Fetch endpoints map + endpointsMap, err := FetchEndpointsMap(identityEndpoint) + if err != nil { + return "", fmt.Errorf("failed to fetch endpoints map: %v", err) + } + + // Get endpoint for the requested service + endpoint, exists := endpointsMap[serviceName] + if !exists { + return "", fmt.Errorf("no endpoint found for service: %s", serviceName) + } + + // Remove /v1 suffix if present + if idx := strings.Index(endpoint, "/v"); idx != -1 { + endpoint = endpoint[:idx] + } + + return endpoint, nil +} + func FetchEndpointsMap(endpoint string) (map[string]string, error) { // Get identity service endpoint identityEndpoint, hasIdentityService, err := GetIdentityEndpoint(endpoint) From 9c01900e445ae78e3b2aac3d58201846ee3c24fb Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 13:21:58 +0900 Subject: [PATCH 05/10] refactor: add default table format Signed-off-by: Youngjin Jo --- pkg/transport/service.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pkg/transport/service.go b/pkg/transport/service.go index b91a8de..4176c7a 100644 --- a/pkg/transport/service.go +++ b/pkg/transport/service.go @@ -202,7 +202,6 @@ func FetchService(serviceName string, verb string, resourceName string, options // Replace 'identity' with the converted service name parts[0] = format.ConvertServiceName(serviceName) hostPort = strings.Join(parts, ".") - fmt.Println(hostPort) } } @@ -321,7 +320,7 @@ func FetchService(serviceName string, verb string, resourceName string, options } } - printData(respMap, options, serviceName, resourceName, refClient) + printData(respMap, options, serviceName, verb, resourceName, refClient) } return respMap, nil @@ -671,7 +670,7 @@ func discoverService(refClient *grpcreflect.Client, serviceName string, resource return "", fmt.Errorf("service not found for %s.%s", serviceName, resourceName) } -func printData(data map[string]interface{}, options *FetchOptions, serviceName, resourceName string, refClient *grpcreflect.Client) { +func printData(data map[string]interface{}, options *FetchOptions, serviceName, verbName, resourceName string, refClient *grpcreflect.Client) { var output string switch options.OutputFormat { @@ -686,21 +685,26 @@ func printData(data map[string]interface{}, options *FetchOptions, serviceName, case "yaml": if results, ok := data["results"].([]interface{}); ok && len(results) > 0 { var sb strings.Builder - for i, item := range results { - if i > 0 { - sb.WriteString("---\n") + + if verbName == "list" { + output = printTable(data, options, serviceName, verbName, resourceName, refClient) + } else { + for i, item := range results { + if i > 0 { + sb.WriteString("---\n") + } + sb.WriteString(printYAMLDoc(item)) } - sb.WriteString(printYAMLDoc(item)) + output = sb.String() + fmt.Print(output) } - output = sb.String() - fmt.Print(output) } else { output = printYAMLDoc(data) fmt.Print(output) } case "table": - output = printTable(data, options, serviceName, resourceName, refClient) + output = printTable(data, options, serviceName, verbName, resourceName, refClient) case "csv": output = printCSV(data) @@ -805,7 +809,7 @@ func getMinimalFields(serviceName, resourceName string, refClient *grpcreflect.C return minimalFields } -func printTable(data map[string]interface{}, options *FetchOptions, serviceName, resourceName string, refClient *grpcreflect.Client) string { +func printTable(data map[string]interface{}, options *FetchOptions, serviceName, verbName, resourceName string, refClient *grpcreflect.Client) string { if results, ok := data["results"].([]interface{}); ok { // Set default page size if not specified if options.PageSize == 0 { From 013fbe92f276acb4ad18622f59d93bd14c1c979c Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 13:46:33 +0900 Subject: [PATCH 06/10] refactor: list for circular table Signed-off-by: Youngjin Jo --- pkg/transport/service.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/transport/service.go b/pkg/transport/service.go index 4176c7a..6c04d3b 100644 --- a/pkg/transport/service.go +++ b/pkg/transport/service.go @@ -813,7 +813,7 @@ func printTable(data map[string]interface{}, options *FetchOptions, serviceName, if results, ok := data["results"].([]interface{}); ok { // Set default page size if not specified if options.PageSize == 0 { - options.PageSize = 100 + options.PageSize = 10 } // Initialize keyboard @@ -911,13 +911,9 @@ func printTable(data map[string]interface{}, options *FetchOptions, serviceName, switch char { case 'l', 'L': - if currentPage < totalPages-1 { - currentPage++ - } + currentPage = (currentPage + 1) % totalPages case 'h', 'H': - if currentPage > 0 { - currentPage-- - } + currentPage = (currentPage - 1 + totalPages) % totalPages case 'q', 'Q': return "" case 'c', 'C': From 6088271f8c500099ac6bfa2ab33e2884962cba2c Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 14:07:15 +0900 Subject: [PATCH 07/10] refactor: add list flags Signed-off-by: Youngjin Jo --- cmd/root.go | 39 ++- pkg/format/output.go | 79 ++++++ pkg/transport/service.go | 107 ++++++++ pkg/transport/verb.go | 541 --------------------------------------- 4 files changed, 224 insertions(+), 542 deletions(-) delete mode 100644 pkg/transport/verb.go diff --git a/cmd/root.go b/cmd/root.go index 2468fdf..4bdffd8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -487,7 +487,10 @@ func createServiceCommand(serviceName string) *cobra.Command { if len(args) == 0 { pterm.Info.Println("To see available API resources, run:") pterm.Info.Printf(" cfctl %s api_resources\n", serviceName) - cmd.Help() + err := cmd.Help() + if err != nil { + return err + } fmt.Println() // Add newline return nil } @@ -508,12 +511,38 @@ func createServiceCommand(serviceName string) *cobra.Command { outputFormat, _ := cmd.Flags().GetString("output") copyToClipboard, _ := cmd.Flags().GetBool("copy") + sortBy := "" + columns := "" + limit := 0 + pageSize := 100 // 기본 페이지 크기 + + if verb == "list" { + sortBy, _ = cmd.Flags().GetString("sort") + columns, _ = cmd.Flags().GetString("columns") + limit, _ = cmd.Flags().GetInt("limit") + pageSize, _ = cmd.Flags().GetInt("page-size") + } + options := &transport.FetchOptions{ Parameters: parameters, JSONParameter: jsonParameter, FileParameter: fileParameter, OutputFormat: outputFormat, CopyToClipboard: copyToClipboard, + SortBy: sortBy, + MinimalColumns: verb == "list" && cmd.Flag("minimal") != nil && cmd.Flag("minimal").Changed, + Columns: columns, + Limit: limit, + PageSize: pageSize, + } + + if verb == "list" && !cmd.Flags().Changed("output") { + options.OutputFormat = "table" + } + + watch, _ := cmd.Flags().GetBool("watch") + if watch && verb == "list" { + return transport.WatchResource(serviceName, verb, resource, options) } _, err := transport.FetchService(serviceName, verb, resource, options) @@ -528,6 +557,14 @@ func createServiceCommand(serviceName string) *cobra.Command { // Add api_resources subcommand cmd.AddCommand(commands.FetchApiResourcesCmd(serviceName)) + // Add list-specific flags + cmd.Flags().BoolP("watch", "w", false, "Watch for changes") + cmd.Flags().StringP("sort", "s", "", "Sort by field (e.g. 'name', 'created_at')") + cmd.Flags().BoolP("minimal", "m", false, "Show minimal columns") + cmd.Flags().StringP("columns", "c", "", "Specific columns (-c id,name)") + cmd.Flags().IntP("limit", "l", 0, "Number of rows") + cmd.Flags().IntP("page-size", "n", 15, "Number of items per page") + // Add existing flags cmd.Flags().StringArrayP("parameter", "p", []string{}, "Input Parameter (-p = -p ...)") cmd.Flags().StringP("json-parameter", "j", "", "JSON type parameter") diff --git a/pkg/format/output.go b/pkg/format/output.go index 6f6696f..f6d1b2f 100644 --- a/pkg/format/output.go +++ b/pkg/format/output.go @@ -3,6 +3,7 @@ package format import ( + "encoding/json" "fmt" "sort" "strings" @@ -180,3 +181,81 @@ func splitIntoLinesWithComma(text string, maxWidth int) []string { return lines } + +func GenerateIdentifier(item map[string]interface{}) string { + if id, ok := item["job_task_id"]; ok { + return fmt.Sprintf("%v", id) + } + + var keys []string + for k := range item { + keys = append(keys, k) + } + sort.Strings(keys) + + var parts []string + for _, k := range keys { + parts = append(parts, fmt.Sprintf("%v=%v", k, item[k])) + } + return strings.Join(parts, ",") +} + +func PrintNewItems(items []map[string]interface{}) { + if len(items) == 0 { + return + } + + tableData := pterm.TableData{} + + headers := make([]string, 0) + for key := range items[0] { + headers = append(headers, key) + } + sort.Strings(headers) + tableData = append(tableData, headers) + + for _, item := range items { + row := make([]string, len(headers)) + for i, header := range headers { + if val, ok := item[header]; ok { + row[i] = formatTableValue(val) + } + } + tableData = append(tableData, row) + } + + pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() +} + +func formatTableValue(val interface{}) string { + switch v := val.(type) { + case nil: + return "" + case string: + // Add colors for status values + switch strings.ToUpper(v) { + case "SUCCESS": + return pterm.FgGreen.Sprint(v) + case "FAILURE": + return pterm.FgRed.Sprint(v) + case "PENDING": + return pterm.FgYellow.Sprint(v) + case "RUNNING": + return pterm.FgBlue.Sprint(v) + default: + return v + } + case float64, float32, int, int32, int64, uint, uint32, uint64: + return fmt.Sprintf("%v", v) + case bool: + return fmt.Sprintf("%v", v) + case map[string]interface{}, []interface{}: + jsonBytes, err := json.Marshal(v) + if err != nil { + return fmt.Sprintf("%v", v) + } + return string(jsonBytes) + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/pkg/transport/service.go b/pkg/transport/service.go index 6c04d3b..d77e4ed 100644 --- a/pkg/transport/service.go +++ b/pkg/transport/service.go @@ -10,9 +10,11 @@ import ( "io" "log" "os" + "os/signal" "path/filepath" "sort" "strings" + "time" "github.com/cloudforet-io/cfctl/pkg/format" "github.com/eiannone/keyboard" @@ -44,6 +46,22 @@ type Config struct { Environments map[string]Environment `yaml:"environments"` } +// FetchOptions holds the flag values for a command +type FetchOptions struct { + Parameters []string + JSONParameter string + FileParameter string + APIVersion string + OutputFormat string + CopyToClipboard bool + SortBy string + MinimalColumns bool + Columns string + Limit int + Page int + PageSize int +} + // FetchService handles the execution of gRPC commands for all services func FetchService(serviceName string, verb string, resourceName string, options *FetchOptions) (map[string]interface{}, error) { homeDir, err := os.UserHomeDir() @@ -670,6 +688,95 @@ func discoverService(refClient *grpcreflect.Client, serviceName string, resource return "", fmt.Errorf("service not found for %s.%s", serviceName, resourceName) } +// WatchResource monitors a resource for changes and prints updates +func WatchResource(serviceName, verb, resource string, options *FetchOptions) error { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt) + + seenItems := make(map[string]bool) + + initialData, err := FetchService(serviceName, verb, resource, &FetchOptions{ + Parameters: options.Parameters, + JSONParameter: options.JSONParameter, + FileParameter: options.FileParameter, + APIVersion: options.APIVersion, + OutputFormat: "", + CopyToClipboard: false, + }) + if err != nil { + return err + } + + if results, ok := initialData["results"].([]interface{}); ok { + var recentItems []map[string]interface{} + + for _, item := range results { + if m, ok := item.(map[string]interface{}); ok { + identifier := format.GenerateIdentifier(m) + seenItems[identifier] = true + + recentItems = append(recentItems, m) + if len(recentItems) > 20 { + recentItems = recentItems[1:] + } + } + } + + if len(recentItems) > 0 { + fmt.Printf("Recent items:\n") + format.PrintNewItems(recentItems) + } + } + + fmt.Printf("\nWatching for changes... (Ctrl+C to quit)\n\n") + + for { + select { + case <-ticker.C: + newData, err := FetchService(serviceName, verb, resource, &FetchOptions{ + Parameters: options.Parameters, + JSONParameter: options.JSONParameter, + FileParameter: options.FileParameter, + APIVersion: options.APIVersion, + OutputFormat: "", + CopyToClipboard: false, + }) + if err != nil { + continue + } + + var newItems []map[string]interface{} + if results, ok := newData["results"].([]interface{}); ok { + for _, item := range results { + if m, ok := item.(map[string]interface{}); ok { + identifier := format.GenerateIdentifier(m) + if !seenItems[identifier] { + newItems = append(newItems, m) + seenItems[identifier] = true + } + } + } + } + + if len(newItems) > 0 { + fmt.Printf("Found %d new items at %s:\n", + len(newItems), + time.Now().Format("2006-01-02 15:04:05")) + + format.PrintNewItems(newItems) + fmt.Println() + } + + case <-sigChan: + fmt.Println("\nStopping watch...") + return nil + } + } +} + func printData(data map[string]interface{}, options *FetchOptions, serviceName, verbName, resourceName string, refClient *grpcreflect.Client) { var output string diff --git a/pkg/transport/verb.go b/pkg/transport/verb.go deleted file mode 100644 index 5b259ba..0000000 --- a/pkg/transport/verb.go +++ /dev/null @@ -1,541 +0,0 @@ -// common/fetchVerb.go - -package transport - -import ( - "context" - "crypto/tls" - "fmt" - "os" - "os/signal" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/cloudforet-io/cfctl/pkg/configs" - "github.com/cloudforet-io/cfctl/pkg/format" - "github.com/jhump/protoreflect/grpcreflect" - "github.com/pterm/pterm" - "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" - - "github.com/spf13/cobra" -) - -// FetchOptions holds the flag values for a command -type FetchOptions struct { - Parameters []string - JSONParameter string - FileParameter string - APIVersion string - OutputFormat string - CopyToClipboard bool - SortBy string - MinimalColumns bool - Columns string - Limit int - Page int - PageSize int -} - -// AddVerbCommands adds subcommands for each verb to the parent command -func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID string) error { - // Build the verb-resource map - verbResourceMap, err := buildVerbResourceMap(serviceName) - if err != nil { - return nil // Return nil to prevent Cobra from showing additional error messages - } - - // Get a sorted list of verbs - verbs := make([]string, 0, len(verbResourceMap)) - for verb := range verbResourceMap { - verbs = append(verbs, verb) - } - sort.Strings(verbs) - - for _, verb := range verbs { - currentVerb := verb - resources := verbResourceMap[currentVerb] - - // Prepare Short and Long descriptions - shortDesc := fmt.Sprintf("Supported %d resources", len(resources)) - - verbCmd := &cobra.Command{ - Use: currentVerb + " ", - Short: shortDesc, - Long: fmt.Sprintf("Supported %d resources for %s command.", len(resources), currentVerb), - Args: cobra.ArbitraryArgs, // Allow any number of arguments - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - // Display the help message - cmd.Help() - return nil // Do not return an error to prevent additional error messages - } - resource := args[0] - - // Retrieve flag values - parameters, err := cmd.Flags().GetStringArray("parameter") - if err != nil { - return err - } - jsonParameter, err := cmd.Flags().GetString("json-parameter") - if err != nil { - return err - } - fileParameter, err := cmd.Flags().GetString("file-parameter") - if err != nil { - return err - } - outputFormat, err := cmd.Flags().GetString("output") - if err != nil { - return err - } - copyToClipboard, err := cmd.Flags().GetBool("copy") - if err != nil { - return err - } - - sortBy := "" - columns := "" - limit := 0 - pageSize := 100 // Default page size - - if currentVerb == "list" { - sortBy, _ = cmd.Flags().GetString("sort") - columns, _ = cmd.Flags().GetString("columns") - limit, _ = cmd.Flags().GetInt("limit") - pageSize, _ = cmd.Flags().GetInt("page-size") - } - - options := &FetchOptions{ - Parameters: parameters, - JSONParameter: jsonParameter, - FileParameter: fileParameter, - OutputFormat: outputFormat, - CopyToClipboard: copyToClipboard, - SortBy: sortBy, - MinimalColumns: currentVerb == "list" && cmd.Flag("minimal") != nil && cmd.Flag("minimal").Changed, - Columns: columns, - Limit: limit, - PageSize: pageSize, - } - - if currentVerb == "list" && !cmd.Flags().Changed("output") { - options.OutputFormat = "table" - } - - watch, _ := cmd.Flags().GetBool("watch") - if watch && currentVerb == "list" { - return watchResource(serviceName, currentVerb, resource, options) - } - - _, err = FetchService(serviceName, currentVerb, resource, options) - if err != nil { - // Use pterm to display the error message - pterm.Error.Println(err.Error()) - return nil // Return nil to prevent Cobra from displaying its own error message - } - return nil - }, - GroupID: groupID, - Annotations: map[string]string{ - "resources": strings.Join(resources, ", "), - }, - } - - if currentVerb == "list" { - verbCmd.Flags().BoolP("watch", "w", false, "Watch for changes") - verbCmd.Flags().StringP("sort", "s", "", "Sort by field (e.g. 'name', 'created_at')") - verbCmd.Flags().BoolP("minimal", "m", false, "Show minimal columns") - verbCmd.Flags().StringP("columns", "c", "", "Specific columns (-c id,name)") - verbCmd.Flags().IntP("limit", "l", 0, "Number of rows") - verbCmd.Flags().IntP("page-size", "n", 15, "Number of items per page") - } - - // Define flags for verbCmd - verbCmd.Flags().StringArrayP("parameter", "p", []string{}, "Input Parameter (-p = -p ...)") - verbCmd.Flags().StringP("json-parameter", "j", "", "JSON type parameter") - verbCmd.Flags().StringP("file-parameter", "f", "", "YAML file parameter") - verbCmd.Flags().StringP("output", "o", "yaml", "Output format (yaml, json, table, csv)") - verbCmd.Flags().BoolP("copy", "y", false, "Copy the output to the clipboard (copies any output format)") - - // Set custom help function - verbCmd.SetHelpFunc(format.SetVerbHelp) - - // Update example for list command - if currentVerb == "list" { - verbCmd.Long = fmt.Sprintf("Supported %d resources for %s command.", len(resources), currentVerb) - } - - parentCmd.AddCommand(verbCmd) - } - - return nil -} - -func buildVerbResourceMap(serviceName string) (map[string][]string, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %v", err) - } - - config, err := configs.LoadSetting() - if err != nil { - return nil, fmt.Errorf("failed to load config: %v", err) - } - - if config.Environment == "local" { - return handleLocalEnvironment(serviceName) - } - - cacheDir := filepath.Join(home, ".cfctl", "cache", config.Environment) - cacheFile := filepath.Join(cacheDir, "verb_resources.yaml") - - if info, err := os.Stat(cacheFile); err == nil { - if time.Since(info.ModTime()) < time.Hour { - data, err := os.ReadFile(cacheFile) - if err == nil { - var allServices map[string]map[string][]string - if err := yaml.Unmarshal(data, &allServices); err == nil { - if verbMap, exists := allServices[serviceName]; exists { - return verbMap, nil - } - } - } - } - } - - verbResourceMap, err := fetchVerbResourceMap(serviceName, config) - if err != nil { - return nil, err - } - - var allServices map[string]map[string][]string - if data, err := os.ReadFile(cacheFile); err == nil { - yaml.Unmarshal(data, &allServices) - } - if allServices == nil { - allServices = make(map[string]map[string][]string) - } - - allServices[serviceName] = verbResourceMap - - if err := os.MkdirAll(cacheDir, 0755); err == nil { - data, err := yaml.Marshal(allServices) - if err == nil { - os.WriteFile(cacheFile, data, 0644) - } - } - - return verbResourceMap, nil -} - -func handleLocalEnvironment(serviceName string) (map[string][]string, error) { - // TODO: check services - //if serviceName != "plugin" { - // return nil, fmt.Errorf("only plugin service is supported in local environment") - //} - - conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) - if err != nil { - return nil, fmt.Errorf("failed to connect to local plugin service: %v", err) - } - defer conn.Close() - - ctx := context.Background() - refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - services, err := refClient.ListServices() - if err != nil { - return nil, fmt.Errorf("failed to list local services: %v", err) - } - - verbResourceMap := make(map[string][]string) - for _, s := range services { - // Skip grpc reflection services - if strings.HasPrefix(s, "grpc.") { - continue - } - - // Handle plugin service - if serviceName == "plugin" && strings.Contains(s, ".plugin.") { - serviceDesc, err := refClient.ResolveService(s) - if err != nil { - continue - } - - resourceName := s[strings.LastIndex(s, ".")+1:] - for _, method := range serviceDesc.GetMethods() { - verb := method.GetName() - if resources, ok := verbResourceMap[verb]; ok { - verbResourceMap[verb] = append(resources, resourceName) - } else { - verbResourceMap[verb] = []string{resourceName} - } - } - continue - } - - // Handle other microservices - if strings.Contains(s, fmt.Sprintf("spaceone.api.%s.", serviceName)) { - serviceDesc, err := refClient.ResolveService(s) - if err != nil { - continue - } - - resourceName := s[strings.LastIndex(s, ".")+1:] - for _, method := range serviceDesc.GetMethods() { - verb := method.GetName() - if resources, ok := verbResourceMap[verb]; ok { - verbResourceMap[verb] = append(resources, resourceName) - } else { - verbResourceMap[verb] = []string{resourceName} - } - } - } - } - - return verbResourceMap, nil -} - -func fetchVerbResourceMap(serviceName string, config *configs.Setting) (map[string][]string, error) { - envConfig := config.Environments[config.Environment] - if envConfig.Endpoint == "" { - return nil, fmt.Errorf("endpoint not found in environment config") - } - - var conn *grpc.ClientConn - var err error - - if config.Environment == "local" { - endpoint := strings.TrimPrefix(envConfig.Endpoint, "grpc://") - conn, err = grpc.Dial(endpoint, grpc.WithInsecure()) - if err != nil { - return nil, fmt.Errorf("connection failed: %v", err) - } - } else { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - apiEndpoint, _ := GetAPIEndpoint(envConfig.Endpoint) - identityEndpoint, hasIdentityService, err := GetIdentityEndpoint(apiEndpoint) - - if !hasIdentityService { - // Get endpoints map first - endpointsMap, err := FetchEndpointsMap(apiEndpoint) - if err != nil { - return nil, fmt.Errorf("failed to fetch endpoints map: %v", err) - } - - // Find the endpoint for the current service - endpoint, exists := endpointsMap[serviceName] - if !exists { - return nil, fmt.Errorf("endpoint not found for service: %s", serviceName) - } - - // Parse the endpoint - parts := strings.Split(endpoint, "://") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) - } - - // Extract hostPort (remove the /v1 suffix if present) - hostPort := strings.Split(parts[1], "/")[0] - - creds := credentials.NewTLS(tlsConfig) - conn, err = grpc.Dial(hostPort, grpc.WithTransportCredentials(creds)) - if err != nil { - return nil, fmt.Errorf("connection failed: %v", err) - } - } else { - trimmedEndpoint := strings.TrimPrefix(identityEndpoint, "grpc+ssl://") - parts := strings.Split(trimmedEndpoint, ".") - if len(parts) < 4 { - return nil, fmt.Errorf("invalid endpoint format: %s", trimmedEndpoint) - } - - // Replace 'identity' with the converted service name - parts[0] = format.ConvertServiceName(serviceName) - serviceEndpoint := strings.Join(parts, ".") - - creds := credentials.NewTLS(tlsConfig) - conn, err = grpc.Dial(serviceEndpoint, grpc.WithTransportCredentials(creds)) - if err != nil { - return nil, fmt.Errorf("connection failed: %v", err) - } - } - } - defer conn.Close() - - ctx := metadata.AppendToOutgoingContext(context.Background(), "token", envConfig.Token) - refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - services, err := refClient.ListServices() - if err != nil { - return nil, fmt.Errorf("failed to list services: %v", err) - } - - verbResourceMap := make(map[string][]string) - for _, s := range services { - if !strings.Contains(s, fmt.Sprintf(".%s.", serviceName)) { - continue - } - - serviceDesc, err := refClient.ResolveService(s) - if err != nil { - continue - } - - resourceName := s[strings.LastIndex(s, ".")+1:] - for _, method := range serviceDesc.GetMethods() { - verb := method.GetName() - if resources, ok := verbResourceMap[verb]; ok { - verbResourceMap[verb] = append(resources, resourceName) - } else { - verbResourceMap[verb] = []string{resourceName} - } - } - } - - return verbResourceMap, nil -} - -// watchResource monitors a resource for changes and prints updates -func watchResource(serviceName, verb, resource string, options *FetchOptions) error { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt) - - seenItems := make(map[string]bool) - - initialData, err := FetchService(serviceName, verb, resource, &FetchOptions{ - Parameters: options.Parameters, - JSONParameter: options.JSONParameter, - FileParameter: options.FileParameter, - APIVersion: options.APIVersion, - OutputFormat: "", - CopyToClipboard: false, - }) - if err != nil { - return err - } - - if results, ok := initialData["results"].([]interface{}); ok { - var recentItems []map[string]interface{} - - for _, item := range results { - if m, ok := item.(map[string]interface{}); ok { - identifier := generateIdentifier(m) - seenItems[identifier] = true - - recentItems = append(recentItems, m) - if len(recentItems) > 20 { - recentItems = recentItems[1:] - } - } - } - - if len(recentItems) > 0 { - fmt.Printf("Recent items:\n") - printNewItems(recentItems) - } - } - - fmt.Printf("\nWatching for changes... (Ctrl+C to quit)\n\n") - - for { - select { - case <-ticker.C: - newData, err := FetchService(serviceName, verb, resource, &FetchOptions{ - Parameters: options.Parameters, - JSONParameter: options.JSONParameter, - FileParameter: options.FileParameter, - APIVersion: options.APIVersion, - OutputFormat: "", - CopyToClipboard: false, - }) - if err != nil { - continue - } - - var newItems []map[string]interface{} - if results, ok := newData["results"].([]interface{}); ok { - for _, item := range results { - if m, ok := item.(map[string]interface{}); ok { - identifier := generateIdentifier(m) - if !seenItems[identifier] { - newItems = append(newItems, m) - seenItems[identifier] = true - } - } - } - } - - if len(newItems) > 0 { - fmt.Printf("Found %d new items at %s:\n", - len(newItems), - time.Now().Format("2006-01-02 15:04:05")) - - printNewItems(newItems) - fmt.Println() - } - - case <-sigChan: - fmt.Println("\nStopping watch...") - return nil - } - } -} - -func generateIdentifier(item map[string]interface{}) string { - if id, ok := item["job_task_id"]; ok { - return fmt.Sprintf("%v", id) - } - - var keys []string - for k := range item { - keys = append(keys, k) - } - sort.Strings(keys) - - var parts []string - for _, k := range keys { - parts = append(parts, fmt.Sprintf("%v=%v", k, item[k])) - } - return strings.Join(parts, ",") -} - -func printNewItems(items []map[string]interface{}) { - if len(items) == 0 { - return - } - - tableData := pterm.TableData{} - - headers := make([]string, 0) - for key := range items[0] { - headers = append(headers, key) - } - sort.Strings(headers) - tableData = append(tableData, headers) - - for _, item := range items { - row := make([]string, len(headers)) - for i, header := range headers { - if val, ok := item[header]; ok { - row[i] = FormatTableValue(val) - } - } - tableData = append(tableData, row) - } - - pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() -} From 33db8f7a7d1c0f008a6d47f18748f48b33ca55ee Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 14:43:42 +0900 Subject: [PATCH 08/10] refactor: modify code for output format Signed-off-by: Youngjin Jo --- pkg/transport/service.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pkg/transport/service.go b/pkg/transport/service.go index d77e4ed..de245be 100644 --- a/pkg/transport/service.go +++ b/pkg/transport/service.go @@ -793,18 +793,14 @@ func printData(data map[string]interface{}, options *FetchOptions, serviceName, if results, ok := data["results"].([]interface{}); ok && len(results) > 0 { var sb strings.Builder - if verbName == "list" { - output = printTable(data, options, serviceName, verbName, resourceName, refClient) - } else { - for i, item := range results { - if i > 0 { - sb.WriteString("---\n") - } - sb.WriteString(printYAMLDoc(item)) + for i, item := range results { + if i > 0 { + sb.WriteString("---\n") } - output = sb.String() - fmt.Print(output) + sb.WriteString(printYAMLDoc(item)) } + output = sb.String() + fmt.Print(output) } else { output = printYAMLDoc(data) fmt.Print(output) From 60ab5fae2a770c23aa07982cc413b2efdabc15d2 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 16:26:12 +0900 Subject: [PATCH 09/10] refactor: modify command from short_name to alias Signed-off-by: Youngjin Jo --- cmd/other/alias.go | 513 +++++++++++++++++++++++++++++++++++++++ cmd/other/short_names.go | 306 ----------------------- cmd/root.go | 38 ++- 3 files changed, 530 insertions(+), 327 deletions(-) create mode 100644 cmd/other/alias.go delete mode 100644 cmd/other/short_names.go diff --git a/cmd/other/alias.go b/cmd/other/alias.go new file mode 100644 index 0000000..8e650ec --- /dev/null +++ b/cmd/other/alias.go @@ -0,0 +1,513 @@ +package other + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/cloudforet-io/cfctl/pkg/transport" + "github.com/pterm/pterm" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +var service string + +// AliasCmd represents the alias command +var AliasCmd = &cobra.Command{ + Use: "alias", + Short: "Manage command aliases", + Long: `Manage aliases for frequently used commands.`, +} + +var addAliasCmd = &cobra.Command{ + Use: "add", + Short: "Add a new alias", + Example: ` $ cfctl alias add -k user -v "identity list User" + +Then use it as: + $ cfctl user # This command is same as $ cfctl identity list User`, + Run: func(cmd *cobra.Command, args []string) { + key, _ := cmd.Flags().GetString("key") + value, _ := cmd.Flags().GetString("value") + + // Parse command to validate + parts := strings.Fields(value) + if len(parts) < 3 { + pterm.Error.Printf("Invalid command format. Expected ' ', got '%s'\n", value) + return + } + + service := parts[0] + verb := parts[1] + resource := parts[2] + + if err := validateServiceCommand(service, verb, resource); err != nil { + pterm.Error.Printf("Invalid command: %v\n", err) + return + } + + if err := addAlias(key, value); err != nil { + pterm.Error.Printf("Failed to add alias: %v\n", err) + return + } + + pterm.Success.Printf("Successfully added alias '%s' for command '%s'\n", key, value) + }, +} + +var removeAliasCmd = &cobra.Command{ + Use: "remove", + Short: "Remove an alias", + Example: ` $ cfctl alias remove -k user`, + Run: func(cmd *cobra.Command, args []string) { + key, _ := cmd.Flags().GetString("key") + if key == "" { + pterm.Error.Println("The --key (-k) flag is required") + cmd.Help() + return + } + + if err := removeAlias(key); err != nil { + pterm.Error.Printf("Failed to remove alias: %v\n", err) + return + } + + pterm.Success.Printf("Successfully removed alias '%s'\n", key) + }, +} + +var listAliasCmd = &cobra.Command{ + Use: "list", + Short: "List all aliases", + Run: func(cmd *cobra.Command, args []string) { + aliases, err := listAliases() + if err != nil { + pterm.Error.Printf("Failed to list aliases: %v\n", err) + return + } + + if len(aliases) == 0 { + pterm.Info.Println("No aliases found") + return + } + + // Create table + table := pterm.TableData{ + {"Alias", "Command"}, + } + + // Add aliases to table + for alias, command := range aliases { + table = append(table, []string{alias, command}) + } + + // Print table + pterm.DefaultTable.WithHasHeader().WithData(table).Render() + }, +} + +// validateServiceCommand checks if the given verb and resource are valid for the service +func validateServiceCommand(service, verb, resource string) error { + // Get current environment from main setting file + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + mainV := viper.New() + mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.yaml")) + mainV.SetConfigType("yaml") + if err := mainV.ReadInConfig(); err != nil { + return fmt.Errorf("failed to read config: %v", err) + } + + currentEnv := mainV.GetString("environment") + if currentEnv == "" { + return fmt.Errorf("no environment set") + } + + // Get environment config + envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) + if envConfig == nil { + return fmt.Errorf("environment %s not found", currentEnv) + } + + endpoint := envConfig.GetString("endpoint") + if endpoint == "" { + return fmt.Errorf("no endpoint found in configuration") + } + + endpoint, _ = transport.GetAPIEndpoint(endpoint) + + // Fetch endpoints map + endpointsMap, err := transport.FetchEndpointsMap(endpoint) + if err != nil { + return fmt.Errorf("failed to fetch endpoints: %v", err) + } + + // Check if service exists + serviceEndpoint, ok := endpointsMap[service] + if !ok { + return fmt.Errorf("service '%s' not found", service) + } + + // Fetch service resources + resources, err := fetchServiceResources(service, serviceEndpoint, nil) + if err != nil { + return fmt.Errorf("failed to fetch service resources: %v", err) + } + + // Find the resource and check if the verb is valid + resourceFound := false + verbFound := false + + for _, row := range resources { + if row[1] == resource { // row[1] is the resource name + resourceFound = true + verbs := strings.Split(row[3], ", ") // row[3] contains the verbs + for _, v := range verbs { + if v == verb { + verbFound = true + break + } + } + break + } + } + + if !resourceFound { + return fmt.Errorf("resource '%s' not found in service '%s'", resource, service) + } + + if !verbFound { + return fmt.Errorf("verb '%s' not found for resource '%s' in service '%s'", verb, resource, service) + } + + return nil +} + +//var addShortNameCmd = &cobra.Command{ +// Use: "add", +// Short: "Add a new short name", +// Example: ` $ cfctl short_name -s inventory add -n job -c "list Job" +// +// Then use them as: +// $ cfctl inventory job # This command is same as $ cfctl inventory list Job`, +// Run: func(cmd *cobra.Command, args []string) { +// // Show example if no flags are provided +// if !cmd.Flags().Changed("name") || !cmd.Flags().Changed("command") || !cmd.Flags().Changed("service") { +// pterm.DefaultBox. +// WithTitle("Short Name Examples"). +// WithTitleTopCenter(). +// WithBoxStyle(pterm.NewStyle(pterm.FgLightBlue)). +// Println(`Example: +// $ cfctl short_name -s inventory add -n job -c "list Job" +// +//Then use them as: +// $ cfctl inventory job # This command is same as $ cfctl inventory list Job`) +// return +// } +// +// shortName, _ := cmd.Flags().GetString("name") +// command, _ := cmd.Flags().GetString("command") +// service, _ := cmd.Flags().GetString("service") +// +// // Parse command to get verb and resource +// parts := strings.Fields(command) +// if len(parts) < 2 { +// pterm.Error.Printf("Invalid command format. Expected ' ', got '%s'\n", command) +// return +// } +// +// verb := parts[0] +// resource := parts[1] +// +// // Validate the command +// if err := validateServiceCommand(service, verb, resource); err != nil { +// pterm.Error.Printf("Invalid command: %v\n", err) +// return +// } +// +// if err := addShortName(service, shortName, command); err != nil { +// pterm.Error.Printf("Failed to add short name: %v\n", err) +// return +// } +// +// pterm.Success.Printf("Successfully added short name '%s' for service '%s' command '%s'\n", shortName, service, command) +// }, +//} + +var removeShortNameCmd = &cobra.Command{ + Use: "remove", + Short: "Remove a short name", + Run: func(cmd *cobra.Command, args []string) { + shortName, err := cmd.Flags().GetString("name") + service, _ := cmd.Flags().GetString("service") + if err != nil || shortName == "" || service == "" { + pterm.Error.Println("The --name (-n) and --service (-s) flags are required") + cmd.Help() + return + } + + if err := removeShortName(service, shortName); err != nil { + pterm.Error.Printf("Failed to remove short name: %v\n", err) + return + } + + pterm.Success.Printf("Successfully removed short name '%s' from service '%s'\n", shortName, service) + }, +} + +var listShortNameCmd = &cobra.Command{ + Use: "list", + Short: "List all short names", + Run: func(cmd *cobra.Command, args []string) { + shortNames, err := listShortNames() + if err != nil { + pterm.Error.Printf("Failed to list short names: %v\n", err) + return + } + + if len(shortNames) == 0 { + pterm.Info.Println("No short names found") + return + } + + // Create table + table := pterm.TableData{ + {"Service", "Short Name", "Command"}, + } + + // Add short names to table + for service, serviceShortNames := range shortNames { + for name, command := range serviceShortNames.(map[string]interface{}) { + table = append(table, []string{service, name, command.(string)}) + } + } + + // Print table + pterm.DefaultTable.WithHasHeader().WithData(table).Render() + }, +} + +func addShortName(service, shortName, command string) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to read config: %v", err) + } + + v.Set(fmt.Sprintf("short_names.%s.%s", service, shortName), command) + + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +func removeShortName(service, shortName string) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + return fmt.Errorf("failed to read config: %v", err) + } + + // Check if service and short name exist + if !v.IsSet(fmt.Sprintf("short_names.%s.%s", service, shortName)) { + return fmt.Errorf("short name '%s' not found in service '%s'", shortName, service) + } + + // Get all short names for the service + serviceShortNames := v.GetStringMap(fmt.Sprintf("short_names.%s", service)) + delete(serviceShortNames, shortName) + + // Update config with removed short name + v.Set(fmt.Sprintf("short_names.%s", service), serviceShortNames) + + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +func listShortNames() (map[string]interface{}, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + if os.IsNotExist(err) { + return make(map[string]interface{}), nil + } + return nil, fmt.Errorf("failed to read config: %v", err) + } + + shortNames := v.GetStringMap("short_names") + if shortNames == nil { + return make(map[string]interface{}), nil + } + + return shortNames, nil +} + +func addAlias(key, value string) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + + data, err := os.ReadFile(settingPath) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to read config: %v", err) + } + + var config map[string]interface{} + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse config: %v", err) + } + + aliases, ok := config["aliases"].(map[string]interface{}) + if !ok { + aliases = make(map[string]interface{}) + } + + delete(config, "aliases") + aliases[key] = value + + newData, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to encode config: %v", err) + } + + aliasData, err := yaml.Marshal(map[string]interface{}{"aliases": aliases}) + if err != nil { + return fmt.Errorf("failed to encode aliases: %v", err) + } + + finalData := append(newData, aliasData...) + + if err := os.WriteFile(settingPath, finalData, 0644); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +// Function to remove an alias +func removeAlias(key string) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + + data, err := os.ReadFile(settingPath) + if err != nil { + return fmt.Errorf("failed to read config: %v", err) + } + + var config map[string]interface{} + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse config: %v", err) + } + + aliases, ok := config["aliases"].(map[string]interface{}) + if !ok || aliases[key] == nil { + return fmt.Errorf("alias '%s' not found", key) + } + + delete(aliases, key) + delete(config, "aliases") + + // YAML 인코딩 + newData, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to encode config: %v", err) + } + + if len(aliases) > 0 { + aliasData, err := yaml.Marshal(map[string]interface{}{"aliases": aliases}) + if err != nil { + return fmt.Errorf("failed to encode aliases: %v", err) + } + newData = append(newData, aliasData...) + } + + if err := os.WriteFile(settingPath, newData, 0644); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +// Function to list all aliases +func listAliases() (map[string]string, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + if os.IsNotExist(err) { + return make(map[string]string), nil + } + return nil, fmt.Errorf("failed to read config: %v", err) + } + + aliases := v.GetStringMapString("aliases") + if aliases == nil { + return make(map[string]string), nil + } + + return aliases, nil +} + +func init() { + AliasCmd.AddCommand(addAliasCmd) + AliasCmd.AddCommand(removeAliasCmd) + AliasCmd.AddCommand(listAliasCmd) + + // Remove service flag as it's no longer needed + addAliasCmd.Flags().StringP("key", "k", "", "Alias key to add") + addAliasCmd.Flags().StringP("value", "v", "", "Command to execute (e.g., \"identity list User\")") + addAliasCmd.MarkFlagRequired("key") + addAliasCmd.MarkFlagRequired("value") + + removeAliasCmd.Flags().StringP("key", "k", "", "Alias key to remove") + removeAliasCmd.MarkFlagRequired("key") +} diff --git a/cmd/other/short_names.go b/cmd/other/short_names.go deleted file mode 100644 index 2cd945e..0000000 --- a/cmd/other/short_names.go +++ /dev/null @@ -1,306 +0,0 @@ -package other - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/cloudforet-io/cfctl/pkg/transport" - "github.com/pterm/pterm" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var service string - -// ShortNameCmd represents the shortName command -var ShortNameCmd = &cobra.Command{ - Use: "short_name", - Short: "Manage short names for commands", - Long: `Manage short names for frequently used commands.`, -} - -// validateServiceCommand checks if the given verb and resource are valid for the service -func validateServiceCommand(service, verb, resource string) error { - // Get current environment from main setting file - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - mainV := viper.New() - mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.yaml")) - mainV.SetConfigType("yaml") - if err := mainV.ReadInConfig(); err != nil { - return fmt.Errorf("failed to read config: %v", err) - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - return fmt.Errorf("no environment set") - } - - // Get environment config - envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) - if envConfig == nil { - return fmt.Errorf("environment %s not found", currentEnv) - } - - endpoint := envConfig.GetString("endpoint") - if endpoint == "" { - return fmt.Errorf("no endpoint found in configuration") - } - - // Fetch endpoints map - endpointsMap, err := transport.FetchEndpointsMap(endpoint) - if err != nil { - return fmt.Errorf("failed to fetch endpoints: %v", err) - } - - // Check if service exists - serviceEndpoint, ok := endpointsMap[service] - if !ok { - return fmt.Errorf("service '%s' not found", service) - } - - // Fetch service resources - resources, err := fetchServiceResources(service, serviceEndpoint, nil) - if err != nil { - return fmt.Errorf("failed to fetch service resources: %v", err) - } - - // Find the resource and check if the verb is valid - resourceFound := false - verbFound := false - - for _, row := range resources { - if row[1] == resource { // row[1] is the resource name - resourceFound = true - verbs := strings.Split(row[3], ", ") // row[3] contains the verbs - for _, v := range verbs { - if v == verb { - verbFound = true - break - } - } - break - } - } - - if !resourceFound { - return fmt.Errorf("resource '%s' not found in service '%s'", resource, service) - } - - if !verbFound { - return fmt.Errorf("verb '%s' not found for resource '%s' in service '%s'", verb, resource, service) - } - - return nil -} - -var addShortNameCmd = &cobra.Command{ - Use: "add", - Short: "Add a new short name", - Example: ` $ cfctl short_name -s inventory add -n job -c "list Job" - - Then use them as: - $ cfctl inventory job # This command is same as $ cfctl inventory list Job`, - Run: func(cmd *cobra.Command, args []string) { - // Show example if no flags are provided - if !cmd.Flags().Changed("name") || !cmd.Flags().Changed("command") || !cmd.Flags().Changed("service") { - pterm.DefaultBox. - WithTitle("Short Name Examples"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgLightBlue)). - Println(`Example: - $ cfctl short_name -s inventory add -n job -c "list Job" - -Then use them as: - $ cfctl inventory job # This command is same as $ cfctl inventory list Job`) - return - } - - shortName, _ := cmd.Flags().GetString("name") - command, _ := cmd.Flags().GetString("command") - service, _ := cmd.Flags().GetString("service") - - // Parse command to get verb and resource - parts := strings.Fields(command) - if len(parts) < 2 { - pterm.Error.Printf("Invalid command format. Expected ' ', got '%s'\n", command) - return - } - - verb := parts[0] - resource := parts[1] - - // Validate the command - if err := validateServiceCommand(service, verb, resource); err != nil { - pterm.Error.Printf("Invalid command: %v\n", err) - return - } - - if err := addShortName(service, shortName, command); err != nil { - pterm.Error.Printf("Failed to add short name: %v\n", err) - return - } - - pterm.Success.Printf("Successfully added short name '%s' for service '%s' command '%s'\n", shortName, service, command) - }, -} - -var removeShortNameCmd = &cobra.Command{ - Use: "remove", - Short: "Remove a short name", - Run: func(cmd *cobra.Command, args []string) { - shortName, err := cmd.Flags().GetString("name") - service, _ := cmd.Flags().GetString("service") - if err != nil || shortName == "" || service == "" { - pterm.Error.Println("The --name (-n) and --service (-s) flags are required") - cmd.Help() - return - } - - if err := removeShortName(service, shortName); err != nil { - pterm.Error.Printf("Failed to remove short name: %v\n", err) - return - } - - pterm.Success.Printf("Successfully removed short name '%s' from service '%s'\n", shortName, service) - }, -} - -var listShortNameCmd = &cobra.Command{ - Use: "list", - Short: "List all short names", - Run: func(cmd *cobra.Command, args []string) { - shortNames, err := listShortNames() - if err != nil { - pterm.Error.Printf("Failed to list short names: %v\n", err) - return - } - - if len(shortNames) == 0 { - pterm.Info.Println("No short names found") - return - } - - // Create table - table := pterm.TableData{ - {"Service", "Short Name", "Command"}, - } - - // Add short names to table - for service, serviceShortNames := range shortNames { - for name, command := range serviceShortNames.(map[string]interface{}) { - table = append(table, []string{service, name, command.(string)}) - } - } - - // Print table - pterm.DefaultTable.WithHasHeader().WithData(table).Render() - }, -} - -func addShortName(service, shortName, command string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read config: %v", err) - } - - v.Set(fmt.Sprintf("short_names.%s.%s", service, shortName), command) - - if err := v.WriteConfig(); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -func removeShortName(service, shortName string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil { - return fmt.Errorf("failed to read config: %v", err) - } - - // Check if service and short name exist - if !v.IsSet(fmt.Sprintf("short_names.%s.%s", service, shortName)) { - return fmt.Errorf("short name '%s' not found in service '%s'", shortName, service) - } - - // Get all short names for the service - serviceShortNames := v.GetStringMap(fmt.Sprintf("short_names.%s", service)) - delete(serviceShortNames, shortName) - - // Update config with removed short name - v.Set(fmt.Sprintf("short_names.%s", service), serviceShortNames) - - if err := v.WriteConfig(); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -func listShortNames() (map[string]interface{}, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil { - if os.IsNotExist(err) { - return make(map[string]interface{}), nil - } - return nil, fmt.Errorf("failed to read config: %v", err) - } - - shortNames := v.GetStringMap("short_names") - if shortNames == nil { - return make(map[string]interface{}), nil - } - - return shortNames, nil -} - -func init() { - ShortNameCmd.AddCommand(addShortNameCmd) - ShortNameCmd.AddCommand(removeShortNameCmd) - ShortNameCmd.AddCommand(listShortNameCmd) - - ShortNameCmd.PersistentFlags().StringVarP(&service, "service", "s", "", "Service to manage short names for") - addShortNameCmd.MarkPersistentFlagRequired("service") - removeShortNameCmd.MarkPersistentFlagRequired("service") - - addShortNameCmd.Flags().StringP("name", "n", "", "Short name to add") - addShortNameCmd.Flags().StringP("command", "c", "", "Command to execute") - addShortNameCmd.MarkFlagRequired("name") - addShortNameCmd.MarkFlagRequired("command") - - removeShortNameCmd.Flags().StringP("name", "n", "", "Short name to remove") - removeShortNameCmd.MarkFlagRequired("name") -} diff --git a/cmd/root.go b/cmd/root.go index 4bdffd8..3992ff0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -46,26 +46,10 @@ var rootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - args := os.Args[1:] - - if len(args) > 1 { - // Check if the first argument is a service name and second is a short name - v := viper.New() - if home, err := os.UserHomeDir(); err == nil { - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err == nil { - serviceName := args[0] - shortName := args[1] - if command := v.GetString(fmt.Sprintf("short_names.%s.%s", serviceName, shortName)); command != "" { - // Replace the short name with the actual command - newArgs := append([]string{args[0]}, strings.Fields(command)...) - newArgs = append(newArgs, args[2:]...) - os.Args = append([]string{os.Args[0]}, newArgs...) - } - } + if len(os.Args) == 2 { + alias := os.Args[1] + if cmd := getAliasCommand(alias); cmd != "" { + os.Args = append([]string{os.Args[0]}, strings.Fields(cmd)...) } } @@ -74,6 +58,18 @@ func Execute() { } } +func getAliasCommand(alias string) string { + v := viper.New() + home, _ := os.UserHomeDir() + v.SetConfigFile(filepath.Join(home, ".cfctl", "setting.yaml")) + + if err := v.ReadInConfig(); err != nil { + return "" + } + + return v.GetString(fmt.Sprintf("aliases.%s", alias)) +} + func init() { // Initialize available commands group AvailableCommands := &cobra.Group{ @@ -125,7 +121,7 @@ func init() { rootCmd.AddCommand(other.ApiResourcesCmd) rootCmd.AddCommand(other.SettingCmd) rootCmd.AddCommand(other.LoginCmd) - rootCmd.AddCommand(other.ShortNameCmd) + rootCmd.AddCommand(other.AliasCmd) // Set default group for commands without a group for _, cmd := range rootCmd.Commands() { From 409cc7c91afb636777a05fb5ab2c5ca72c884199 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Mon, 13 Jan 2025 17:22:58 +0900 Subject: [PATCH 10/10] refactor: add alias for api_resources Signed-off-by: Youngjin Jo --- cmd/other/alias.go | 197 +------------------------------------ cmd/other/api_resources.go | 122 +++++++++++++++-------- cmd/root.go | 1 + 3 files changed, 86 insertions(+), 234 deletions(-) diff --git a/cmd/other/alias.go b/cmd/other/alias.go index 8e650ec..2a83174 100644 --- a/cmd/other/alias.go +++ b/cmd/other/alias.go @@ -83,7 +83,7 @@ var listAliasCmd = &cobra.Command{ Use: "list", Short: "List all aliases", Run: func(cmd *cobra.Command, args []string) { - aliases, err := listAliases() + aliases, err := ListAliases() if err != nil { pterm.Error.Printf("Failed to list aliases: %v\n", err) return @@ -165,9 +165,9 @@ func validateServiceCommand(service, verb, resource string) error { verbFound := false for _, row := range resources { - if row[1] == resource { // row[1] is the resource name + if row[2] == resource { resourceFound = true - verbs := strings.Split(row[3], ", ") // row[3] contains the verbs + verbs := strings.Split(row[1], ", ") for _, v := range verbs { if v == verb { verbFound = true @@ -189,194 +189,6 @@ func validateServiceCommand(service, verb, resource string) error { return nil } -//var addShortNameCmd = &cobra.Command{ -// Use: "add", -// Short: "Add a new short name", -// Example: ` $ cfctl short_name -s inventory add -n job -c "list Job" -// -// Then use them as: -// $ cfctl inventory job # This command is same as $ cfctl inventory list Job`, -// Run: func(cmd *cobra.Command, args []string) { -// // Show example if no flags are provided -// if !cmd.Flags().Changed("name") || !cmd.Flags().Changed("command") || !cmd.Flags().Changed("service") { -// pterm.DefaultBox. -// WithTitle("Short Name Examples"). -// WithTitleTopCenter(). -// WithBoxStyle(pterm.NewStyle(pterm.FgLightBlue)). -// Println(`Example: -// $ cfctl short_name -s inventory add -n job -c "list Job" -// -//Then use them as: -// $ cfctl inventory job # This command is same as $ cfctl inventory list Job`) -// return -// } -// -// shortName, _ := cmd.Flags().GetString("name") -// command, _ := cmd.Flags().GetString("command") -// service, _ := cmd.Flags().GetString("service") -// -// // Parse command to get verb and resource -// parts := strings.Fields(command) -// if len(parts) < 2 { -// pterm.Error.Printf("Invalid command format. Expected ' ', got '%s'\n", command) -// return -// } -// -// verb := parts[0] -// resource := parts[1] -// -// // Validate the command -// if err := validateServiceCommand(service, verb, resource); err != nil { -// pterm.Error.Printf("Invalid command: %v\n", err) -// return -// } -// -// if err := addShortName(service, shortName, command); err != nil { -// pterm.Error.Printf("Failed to add short name: %v\n", err) -// return -// } -// -// pterm.Success.Printf("Successfully added short name '%s' for service '%s' command '%s'\n", shortName, service, command) -// }, -//} - -var removeShortNameCmd = &cobra.Command{ - Use: "remove", - Short: "Remove a short name", - Run: func(cmd *cobra.Command, args []string) { - shortName, err := cmd.Flags().GetString("name") - service, _ := cmd.Flags().GetString("service") - if err != nil || shortName == "" || service == "" { - pterm.Error.Println("The --name (-n) and --service (-s) flags are required") - cmd.Help() - return - } - - if err := removeShortName(service, shortName); err != nil { - pterm.Error.Printf("Failed to remove short name: %v\n", err) - return - } - - pterm.Success.Printf("Successfully removed short name '%s' from service '%s'\n", shortName, service) - }, -} - -var listShortNameCmd = &cobra.Command{ - Use: "list", - Short: "List all short names", - Run: func(cmd *cobra.Command, args []string) { - shortNames, err := listShortNames() - if err != nil { - pterm.Error.Printf("Failed to list short names: %v\n", err) - return - } - - if len(shortNames) == 0 { - pterm.Info.Println("No short names found") - return - } - - // Create table - table := pterm.TableData{ - {"Service", "Short Name", "Command"}, - } - - // Add short names to table - for service, serviceShortNames := range shortNames { - for name, command := range serviceShortNames.(map[string]interface{}) { - table = append(table, []string{service, name, command.(string)}) - } - } - - // Print table - pterm.DefaultTable.WithHasHeader().WithData(table).Render() - }, -} - -func addShortName(service, shortName, command string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read config: %v", err) - } - - v.Set(fmt.Sprintf("short_names.%s.%s", service, shortName), command) - - if err := v.WriteConfig(); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -func removeShortName(service, shortName string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil { - return fmt.Errorf("failed to read config: %v", err) - } - - // Check if service and short name exist - if !v.IsSet(fmt.Sprintf("short_names.%s.%s", service, shortName)) { - return fmt.Errorf("short name '%s' not found in service '%s'", shortName, service) - } - - // Get all short names for the service - serviceShortNames := v.GetStringMap(fmt.Sprintf("short_names.%s", service)) - delete(serviceShortNames, shortName) - - // Update config with removed short name - v.Set(fmt.Sprintf("short_names.%s", service), serviceShortNames) - - if err := v.WriteConfig(); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -func listShortNames() (map[string]interface{}, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil { - if os.IsNotExist(err) { - return make(map[string]interface{}), nil - } - return nil, fmt.Errorf("failed to read config: %v", err) - } - - shortNames := v.GetStringMap("short_names") - if shortNames == nil { - return make(map[string]interface{}), nil - } - - return shortNames, nil -} - func addAlias(key, value string) error { home, err := os.UserHomeDir() if err != nil { @@ -470,8 +282,7 @@ func removeAlias(key string) error { return nil } -// Function to list all aliases -func listAliases() (map[string]string, error) { +func ListAliases() (map[string]string, error) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("failed to get home directory: %v", err) diff --git a/cmd/other/api_resources.go b/cmd/other/api_resources.go index 3d4be70..14c5c8f 100644 --- a/cmd/other/api_resources.go +++ b/cmd/other/api_resources.go @@ -53,6 +53,14 @@ func loadEndpointsFromCache(currentEnv string) (map[string]string, error) { var ApiResourcesCmd = &cobra.Command{ Use: "api_resources", Short: "Displays supported API resources", + Example: ` # List all API resources for all services + $ cfctl api_resources + + # List API resources for a specific service + $ cfctl api_resources -s identity + + # List API resources for multiple services + $ cfctl api_resources -s identity,inventory,repository`, Run: func(cmd *cobra.Command, args []string) { home, err := os.UserHomeDir() if err != nil { @@ -185,8 +193,33 @@ var ApiResourcesCmd = &cobra.Command{ }, } +func loadAliases() (map[string]string, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("unable to find home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + if os.IsNotExist(err) { + return make(map[string]string), nil + } + return nil, fmt.Errorf("failed to read config: %v", err) + } + + aliases := v.GetStringMapString("aliases") + if aliases == nil { + return make(map[string]string), nil + } + + return aliases, nil +} + func fetchServiceResources(service, endpoint string, shortNamesMap map[string]string) ([][]string, error) { - // Configure gRPC connection based on TLS usage parts := strings.Split(endpoint, "://") if len(parts) != 2 { return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) @@ -198,7 +231,7 @@ func fetchServiceResources(service, endpoint string, shortNamesMap map[string]st var opts []grpc.DialOption if scheme == "grpc+ssl" { tlsConfig := &tls.Config{ - InsecureSkipVerify: false, // Enable server certificate verification + InsecureSkipVerify: false, } creds := credentials.NewTLS(tlsConfig) opts = append(opts, grpc.WithTransportCredentials(creds)) @@ -218,7 +251,6 @@ func fetchServiceResources(service, endpoint string, shortNamesMap map[string]st return nil, fmt.Errorf("failed to create reflection client: %v", err) } - // List all services req := &grpc_reflection_v1alpha.ServerReflectionRequest{ MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_ListServices{ListServices: ""}, } @@ -233,9 +265,11 @@ func fetchServiceResources(service, endpoint string, shortNamesMap map[string]st } services := resp.GetListServicesResponse().Service - registeredShortNames, err := listShortNames() + + // Load aliases + aliases, err := loadAliases() if err != nil { - return nil, fmt.Errorf("failed to load short names: %v", err) + return nil, fmt.Errorf("failed to load aliases: %v", err) } data := [][]string{} @@ -246,38 +280,36 @@ func fetchServiceResources(service, endpoint string, shortNamesMap map[string]st resourceName := s.Name[strings.LastIndex(s.Name, ".")+1:] verbs := getServiceMethods(client, s.Name) - // Find all matching short names for this resource - verbsWithShortNames := make(map[string]string) + // Group verbs by alias + verbsWithAlias := make(map[string]string) remainingVerbs := make([]string, 0) - // Get service-specific short names - serviceShortNames := registeredShortNames[service] - if serviceMap, ok := serviceShortNames.(map[string]interface{}); ok { - for _, verb := range verbs { - hasShortName := false - for sn, cmd := range serviceMap { - if strings.Contains(cmd.(string), fmt.Sprintf("%s %s", verb, resourceName)) { - verbsWithShortNames[verb] = sn - hasShortName = true - break - } - } - if !hasShortName { - remainingVerbs = append(remainingVerbs, verb) + for _, verb := range verbs { + hasAlias := false + for alias, cmd := range aliases { + cmdParts := strings.Fields(cmd) + if len(cmdParts) >= 3 && + cmdParts[0] == service && + cmdParts[1] == verb && + cmdParts[2] == resourceName { + verbsWithAlias[verb] = alias + hasAlias = true + break } } - } else { - remainingVerbs = verbs + if !hasAlias { + remainingVerbs = append(remainingVerbs, verb) + } } - // Add row for verbs without short names + // Add row for verbs without aliases if len(remainingVerbs) > 0 { - data = append(data, []string{service, resourceName, "", strings.Join(remainingVerbs, ", ")}) + data = append(data, []string{service, strings.Join(remainingVerbs, ", "), resourceName, ""}) } - // Add separate rows for each verb with a short name - for verb, shortName := range verbsWithShortNames { - data = append(data, []string{service, resourceName, shortName, verb}) + // Add separate rows for each verb with an alias + for verb, alias := range verbsWithAlias { + data = append(data, []string{service, verb, resourceName, alias}) } } @@ -329,7 +361,7 @@ func getServiceMethods(client grpc_reflection_v1alpha.ServerReflectionClient, se func renderTable(data [][]string) { // Calculate the dynamic width for the "Verb" column terminalWidth := pterm.GetTerminalWidth() - usedWidth := 30 + 20 + 15 // Estimated widths for Service, Resource, and Short Names + usedWidth := 30 + 20 + 15 // Estimated widths for Service, Resource, and Alias columns verbColumnWidth := terminalWidth - usedWidth if verbColumnWidth < 20 { verbColumnWidth = 20 // Minimum width for Verb column @@ -337,43 +369,51 @@ func renderTable(data [][]string) { // Use two distinct colors for alternating services alternateColors := []pterm.Color{ - pterm.FgLightBlue, pterm.FgLightYellow, pterm.FgLightMagenta, pterm.FgGreen, pterm.FgLightRed, pterm.FgBlue, pterm.FgLightGreen, + pterm.FgLightBlue, pterm.FgLightYellow, pterm.FgLightMagenta, + pterm.FgGreen, pterm.FgLightRed, pterm.FgBlue, pterm.FgLightGreen, } currentColorIndex := 0 previousService := "" - table := pterm.TableData{{"Service", "Verb", "Resource", "Short Names"}} // Column order updated + table := pterm.TableData{{"Service", "Verb", "Resource", "Alias"}} for _, row := range data { service := row[0] - // Switch color if the service name changes if service != previousService { currentColorIndex = (currentColorIndex + 1) % len(alternateColors) previousService = service } - // Apply the current color color := alternateColors[currentColorIndex] coloredStyle := pterm.NewStyle(color) - // Color the entire row (Service, Resource, Short Names, Verb) - serviceColored := coloredStyle.Sprint(service) - resourceColored := coloredStyle.Sprint(row[1]) - shortNamesColored := coloredStyle.Sprint(row[2]) + serviceColored := coloredStyle.Sprint(row[0]) + resourceColored := coloredStyle.Sprint(row[2]) + aliasColored := coloredStyle.Sprint(row[3]) - verbs := splitIntoLinesWithComma(row[3], verbColumnWidth) + // Split verbs into multiple lines if needed + verbs := splitIntoLinesWithComma(row[1], verbColumnWidth) for i, line := range verbs { if i == 0 { - table = append(table, []string{serviceColored, coloredStyle.Sprint(line), resourceColored, shortNamesColored}) + table = append(table, []string{ + serviceColored, + coloredStyle.Sprint(line), + resourceColored, + aliasColored, + }) } else { - table = append(table, []string{"", coloredStyle.Sprint(line), "", ""}) + table = append(table, []string{ + "", + coloredStyle.Sprint(line), + "", + "", + }) } } } - // Render the table using pterm pterm.DefaultTable.WithHasHeader().WithData(table).Render() } diff --git a/cmd/root.go b/cmd/root.go index 3992ff0..1ed7162 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -345,6 +345,7 @@ func addDynamicServiceCommands() error { progressbar.Increment() fmt.Println() // Add newline after progress bar + return nil }