From 22f185935ec9e4cf6aa187c5bfb902478296fd9f Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 14:31:06 +0900 Subject: [PATCH 01/14] refactor: show available commands only when a user successfully logged in Signed-off-by: Youngjin Jo --- cmd/other/login.go | 53 +++++++++++++----------- cmd/root.go | 100 +++++++++++++++------------------------------ 2 files changed, 62 insertions(+), 91 deletions(-) diff --git a/cmd/other/login.go b/cmd/other/login.go index d692226..2b8132d 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -423,24 +423,6 @@ func executeUserLogin(currentEnv string) { exitWithError() } - userID := mainViper.GetString(fmt.Sprintf("environments.%s.user_id", currentEnv)) - if userID == "" { - userIDInput := pterm.DefaultInteractiveTextInput - userID, _ = userIDInput.Show("Enter your User ID") - - mainViper.Set(fmt.Sprintf("environments.%s.user_id", currentEnv), userID) - if err := mainViper.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to save user ID to config: %v\n", err) - exitWithError() - } - } else { - pterm.Info.Printf("Logging in as: %s\n", userID) - } - - // Check for valid tokens first - accessToken, refreshToken, newAccessToken, err := getValidTokens(currentEnv) - var password string - // Extract domain name from environment nameParts := strings.Split(currentEnv, "-") if len(nameParts) < 3 { @@ -456,14 +438,39 @@ func executeUserLogin(currentEnv string) { exitWithError() } - // If refresh token is not valid, get new tokens with password - if refreshToken == "" || isTokenExpired(refreshToken) { - password = promptPassword() - accessToken, refreshToken, err = issueToken(baseUrl, userID, password, domainID) + // Check for existing user_id in config + userID := mainViper.GetString(fmt.Sprintf("environments.%s.user_id", currentEnv)) + var tempUserID string + + if userID == "" { + userIDInput := pterm.DefaultInteractiveTextInput + tempUserID, _ = userIDInput.Show("Enter your User ID") + } else { + tempUserID = userID + pterm.Info.Printf("Logging in as: %s\n", userID) + } + + accessToken, refreshToken, newAccessToken, err := getValidTokens(currentEnv) + if err == nil && refreshToken != "" && !isTokenExpired(refreshToken) { + // Use existing valid refresh token + accessToken = newAccessToken + } else { + // Get new tokens with password + password := promptPassword() + accessToken, refreshToken, err = issueToken(baseUrl, tempUserID, password, domainID) if err != nil { pterm.Error.Printf("Failed to issue token: %v\n", err) exitWithError() } + + // Only save user_id after successful token issue + if userID == "" { + mainViper.Set(fmt.Sprintf("environments.%s.user_id", currentEnv), tempUserID) + if err := mainViper.WriteConfig(); err != nil { + pterm.Error.Printf("Failed to save user ID to config: %v\n", err) + exitWithError() + } + } } // Create cache directory and save tokens @@ -473,7 +480,7 @@ func executeUserLogin(currentEnv string) { exitWithError() } - pterm.Info.Printf("Logged in as %s\n", userID) + pterm.Info.Printf("Logged in as %s\n", tempUserID) // Use the tokens to fetch workspaces and role workspaces, err := fetchWorkspaces(baseUrl, accessToken) diff --git a/cmd/root.go b/cmd/root.go index 2fe4111..2ea5513 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -165,9 +165,9 @@ func showInitializationGuide(originalErr error) { pterm.Info.Println("After updating the token, please try your command again.") } else { - // Show user login guide pterm.Warning.Printf("Authentication required.\n") - pterm.Info.Println("Please run 'cfctl login' to authenticate.") + pterm.Info.Println("To see Available Commands, please authenticate first:") + pterm.Info.Println("$ cfctl login") } } } @@ -338,87 +338,51 @@ func loadConfig() (*Config, error) { return nil, fmt.Errorf("unable to find home directory: %v", err) } - // Change file extension from .yaml to .toml settingFile := filepath.Join(home, ".cfctl", "setting.toml") - cacheConfigFile := filepath.Join(home, ".cfctl", "cache", "setting.toml") - // Try to read main setting first + // Read main setting file mainV := viper.New() mainV.SetConfigFile(settingFile) - mainV.SetConfigType("toml") // Explicitly set config type to TOML - mainConfigErr := mainV.ReadInConfig() - - if mainConfigErr != nil { + mainV.SetConfigType("toml") + if err := mainV.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read setting file") } - var currentEnv string - var endpoint string - var token string - - // Main setting exists, try to get environment - currentEnv = mainV.GetString("environment") - if currentEnv != "" { - envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) - if envConfig != nil { - endpoint = envConfig.GetString("endpoint") - token = envConfig.GetString("token") - } + currentEnv := mainV.GetString("environment") + if currentEnv == "" { + return nil, fmt.Errorf("no environment set") } - // If main setting doesn't have what we need, try cache setting - if endpoint == "" || token == "" { - cacheV := viper.New() - cacheV.SetConfigFile(cacheConfigFile) - cacheV.SetConfigType("toml") // Explicitly set config type to TOML - - if err := cacheV.ReadInConfig(); err == nil { - // If no current environment set, try to get it from cache setting - if currentEnv == "" { - currentEnv = cacheV.GetString("environment") - } - - // Try to get environment setting from cache - if currentEnv != "" { - envConfig := cacheV.Sub(fmt.Sprintf("environments.%s", currentEnv)) - if envConfig != nil { - if endpoint == "" { - endpoint = envConfig.GetString("endpoint") - } - if token == "" { - token = envConfig.GetString("token") - } - } - } - - // If still no environment, try to find first user environment - if currentEnv == "" { - envs := cacheV.GetStringMap("environments") - for env := range envs { - if strings.HasSuffix(env, "-user") { - currentEnv = env - envConfig := cacheV.Sub(fmt.Sprintf("environments.%s", currentEnv)) - if envConfig != nil { - if endpoint == "" { - endpoint = envConfig.GetString("endpoint") - } - if token == "" { - token = envConfig.GetString("token") - } - break - } - } - } - } - } + // Get environment config + envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) + if envConfig == nil { + return nil, fmt.Errorf("environment %s not found", currentEnv) } + endpoint := envConfig.GetString("endpoint") if endpoint == "" { return nil, fmt.Errorf("no endpoint found in configuration") } - if token == "" { - return nil, fmt.Errorf("no token found in configuration") + var token string + // Check environment suffix + if strings.HasSuffix(currentEnv, "-user") { + // For user environments, read from cache directory + envCacheDir := filepath.Join(home, ".cfctl", "cache", currentEnv) + grantTokenPath := filepath.Join(envCacheDir, "grant_token") + data, err := os.ReadFile(grantTokenPath) + if err != nil { + return nil, fmt.Errorf("no valid token found in cache") + } + token = string(data) + } else if strings.HasSuffix(currentEnv, "-app") { + // For app environments, read from setting.toml + token = envConfig.GetString("token") + if token == "" { + return nil, fmt.Errorf("no token found in configuration") + } + } else { + return nil, fmt.Errorf("invalid environment suffix: must end with -user or -app") } return &Config{ From beea846bfdfb166953b6d4b9e65322fa3cd975c8 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 14:38:36 +0900 Subject: [PATCH 02/14] refactor: change config path from config.yaml to setting.toml Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index a8d950b..08cf8f2 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -51,7 +51,8 @@ func FetchService(serviceName string, verb string, resourceName string, options // Read configuration file mainViper := viper.New() - mainViper.SetConfigFile(filepath.Join(homeDir, ".cfctl", "config.yaml")) + mainViper.SetConfigFile(filepath.Join(homeDir, ".cfctl", "setting.toml")) + mainViper.SetConfigType("toml") if err := mainViper.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read configuration file. Please run 'cfctl login' first") } @@ -62,13 +63,13 @@ func FetchService(serviceName string, verb string, resourceName string, options return nil, fmt.Errorf("no environment set. Please run 'cfctl login' first") } - // Load configuration first (including cache) + // Load configuration first config, err := loadConfig() if err != nil { return nil, fmt.Errorf("failed to load config: %v", err) } - // Check token from both main config and cache + // Check token token := config.Environments[config.Environment].Token if token == "" { pterm.Error.Println("No token found for authentication.") @@ -179,8 +180,9 @@ func loadConfig() (*Config, error) { // Load main configuration file mainV := viper.New() - mainConfigPath := filepath.Join(home, ".cfctl", "config.yaml") + mainConfigPath := filepath.Join(home, ".cfctl", "setting.toml") mainV.SetConfigFile(mainConfigPath) + mainV.SetConfigType("toml") if err := mainV.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read config file: %v", err) } @@ -190,21 +192,23 @@ func loadConfig() (*Config, error) { return nil, fmt.Errorf("no environment set in config") } - // First try to get environment config from main config file + // Get environment config from main config file envConfig := &Environment{ Endpoint: mainV.GetString(fmt.Sprintf("environments.%s.endpoint", currentEnv)), - Token: mainV.GetString(fmt.Sprintf("environments.%s.token", currentEnv)), Proxy: mainV.GetString(fmt.Sprintf("environments.%s.proxy", currentEnv)), } - // If it's a -user environment and token is empty, try to get token from cache config - if strings.HasSuffix(currentEnv, "-user") && envConfig.Token == "" { - cacheV := viper.New() - cacheConfigPath := filepath.Join(home, ".cfctl", "cache", "config.yaml") - cacheV.SetConfigFile(cacheConfigPath) - if err := cacheV.ReadInConfig(); err == nil { - envConfig.Token = cacheV.GetString(fmt.Sprintf("environments.%s.token", currentEnv)) + // Handle token based on environment type + if strings.HasSuffix(currentEnv, "-user") { + // For user environments, read from grant_token file + grantTokenPath := filepath.Join(home, ".cfctl", "cache", currentEnv, "grant_token") + tokenBytes, err := os.ReadFile(grantTokenPath) + if err == nil { + envConfig.Token = strings.TrimSpace(string(tokenBytes)) } + } else if strings.HasSuffix(currentEnv, "-app") { + // For app environments, get token from main config + envConfig.Token = mainV.GetString(fmt.Sprintf("environments.%s.token", currentEnv)) } if envConfig == nil { From 4850cb6d7823c24183a370bec54055f2ee34d431 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 14:58:37 +0900 Subject: [PATCH 03/14] feat: add search feature Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index 08cf8f2..88afd1e 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -476,8 +476,8 @@ func printTable(data map[string]interface{}) string { fmt.Printf("Search: %s (Found: %d items)\n", searchTerm, totalItems) } - // Add rows for current page - for _, result := range results[startIdx:endIdx] { + // Add rows for current page using filteredResults + for _, result := range filteredResults[startIdx:endIdx] { if row, ok := result.(map[string]interface{}); ok { rowData := make([]string, len(headers)) for i, key := range headers { @@ -532,7 +532,6 @@ func printTable(data map[string]interface{}) string { } } } else { - // 단일 객체인 경우 (get 명령어) headers := make([]string, 0) for key := range data { headers = append(headers, key) @@ -559,7 +558,6 @@ func filterResults(results []interface{}, searchTerm string) []interface{} { for _, result := range results { if row, ok := result.(map[string]interface{}); ok { - // 모든 필드에서 검색 for _, value := range row { strValue := strings.ToLower(fmt.Sprintf("%v", value)) if strings.Contains(strValue, searchTerm) { From c8c41fe675b4ef762518a1bb8530096201f93af3 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 16:22:22 +0900 Subject: [PATCH 04/14] refactor: speed up when getting services dynamically Signed-off-by: Youngjin Jo --- cmd/root.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 2ea5513..d8323c8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,10 +1,14 @@ package cmd import ( + "bytes" + "compress/gzip" "fmt" "os" "path/filepath" "strings" + "sync" + "time" "github.com/spf13/viper" @@ -57,6 +61,12 @@ func init() { } rootCmd.AddGroup(AvailableCommands) + go func() { + if _, err := loadCachedEndpoints(); err == nil { + return + } + }() + if len(os.Args) > 1 && os.Args[1] == "__complete" { pterm.DisableColor() } @@ -185,14 +195,31 @@ func addDynamicServiceCommands() error { // Try to load endpoints from file cache endpoints, err := loadCachedEndpoints() if err == nil { - // Store in memory for subsequent calls cachedEndpointsMap = endpoints - // Create commands using cached endpoints + // 병렬로 커맨드 생성 + var wg sync.WaitGroup + cmdChan := make(chan *cobra.Command, len(endpoints)) + for serviceName := range endpoints { - cmd := createServiceCommand(serviceName) + wg.Add(1) + go func(svc string) { + defer wg.Done() + cmd := createServiceCommand(svc) + cmdChan <- cmd + }(serviceName) + } + + // 별도 고루틴에서 커맨드 추가 + go func() { + wg.Wait() + close(cmdChan) + }() + + for cmd := range cmdChan { rootCmd.AddCommand(cmd) } + return nil } @@ -287,6 +314,15 @@ func loadCachedEndpoints() (map[string]string, error) { return nil, err } + cacheInfo, err := os.Stat(cacheFile) + if err != nil { + return nil, err + } + + if time.Since(cacheInfo.ModTime()) > 24*time.Hour { + return nil, fmt.Errorf("cache expired") + } + // Parse cached endpoints from TOML var endpoints map[string]string if err := toml.Unmarshal(data, &endpoints); err != nil { @@ -321,14 +357,20 @@ func saveEndpointsCache(endpoints map[string]string) error { return err } - // Marshal endpoints to TOML format + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + data, err := toml.Marshal(endpoints) if err != nil { return err } - // Write to environment-specific cache file - return os.WriteFile(filepath.Join(envCacheDir, "endpoints.toml"), data, 0644) + if _, err := gw.Write(data); err != nil { + return err + } + gw.Close() + + return os.WriteFile(filepath.Join(envCacheDir, "endpoints.toml.gz"), buf.Bytes(), 0644) } // loadConfig loads configuration from both main and cache setting files From 687e044b4b8d87323ac58b671a8f90708890bf12 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 17:21:20 +0900 Subject: [PATCH 05/14] feat: add short names feature Signed-off-by: Youngjin Jo --- cmd/other/apiResources.go | 42 +++++--- cmd/other/shortNames.go | 198 ++++++++++++++++++++++++++++++++++++++ cmd/root.go | 24 ++++- 3 files changed, 248 insertions(+), 16 deletions(-) create mode 100644 cmd/other/shortNames.go diff --git a/cmd/other/apiResources.go b/cmd/other/apiResources.go index 1a8efdc..0b357bc 100644 --- a/cmd/other/apiResources.go +++ b/cmd/other/apiResources.go @@ -55,7 +55,7 @@ func loadEndpointsFromCache(currentEnv string) (map[string]string, error) { } var ApiResourcesCmd = &cobra.Command{ - Use: "api-resources", + Use: "api_resources", Short: "Displays supported API resources", Run: func(cmd *cobra.Command, args []string) { home, err := os.UserHomeDir() @@ -64,7 +64,7 @@ var ApiResourcesCmd = &cobra.Command{ } settingPath := filepath.Join(home, ".cfctl", "setting.toml") - + // Read main setting file mainV := viper.New() mainV.SetConfigFile(settingPath) @@ -366,18 +366,32 @@ func fetchServiceResources(service, endpoint string, shortNamesMap map[string]st } services := resp.GetListServicesResponse().Service - data := [][]string{} - for _, s := range services { - if strings.HasPrefix(s.Name, "grpc.reflection.v1alpha.") { - continue - } - resourceName := s.Name[strings.LastIndex(s.Name, ".")+1:] - verbs := getServiceMethods(client, s.Name) - shortName := shortNamesMap[fmt.Sprintf("%s.%s", service, resourceName)] - data = append(data, []string{service, resourceName, shortName, strings.Join(verbs, ", ")}) - } - - return data, nil + registeredShortNames, err := listShortNames() + if err != nil { + return nil, fmt.Errorf("failed to load short names: %v", err) + } + + data := [][]string{} + for _, s := range services { + if strings.HasPrefix(s.Name, "grpc.reflection.v1alpha.") { + continue + } + resourceName := s.Name[strings.LastIndex(s.Name, ".")+1:] + verbs := getServiceMethods(client, s.Name) + + // Find matching short name + var shortName string + for sn, cmd := range registeredShortNames { + if strings.Contains(cmd, fmt.Sprintf("%s list %s", service, resourceName)) { + shortName = sn + break + } + } + + data = append(data, []string{service, resourceName, shortName, strings.Join(verbs, ", ")}) + } + + return data, nil } func getServiceMethods(client grpc_reflection_v1alpha.ServerReflectionClient, serviceName string) []string { diff --git a/cmd/other/shortNames.go b/cmd/other/shortNames.go new file mode 100644 index 0000000..7121013 --- /dev/null +++ b/cmd/other/shortNames.go @@ -0,0 +1,198 @@ +package other + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// 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.`, +} + +var addShortNameCmd = &cobra.Command{ + Use: "add", + Short: "Add a new short name", + Example: ` $ cfctl short_name add -n user -c "identity list User" + + Then use them as: + $ cfctl user # This command is same as $ cfctl identity list User`, + Run: func(cmd *cobra.Command, args []string) { + // Show example if no flags are provided + if !cmd.Flags().Changed("name") || !cmd.Flags().Changed("command") { + pterm.DefaultBox. + WithTitle("Short Name Examples"). + WithTitleTopCenter(). + WithBoxStyle(pterm.NewStyle(pterm.FgLightBlue)). + Println(`Example: + $ cfctl short_name add -n user -c "identity list User" + +Then use them as: + $ cfctl user # This command is same as $ cfctl identity list User`) + return + } + + shortName, _ := cmd.Flags().GetString("name") + command, _ := cmd.Flags().GetString("command") + + if err := addShortName(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 command '%s'\n", shortName, 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") + if err != nil || shortName == "" { + pterm.Error.Println("The --name (-n) flag is required") + cmd.Help() + return + } + + if err := removeShortName(shortName); err != nil { + pterm.Error.Printf("Failed to remove short name: %v\n", err) + return + } + + pterm.Success.Printf("Successfully removed short name '%s'\n", shortName) + }, +} + +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{ + {"Short Name", "Command"}, + } + + // Add short names to table + for name, command := range shortNames { + table = append(table, []string{name, command}) + } + + // Print table + pterm.DefaultTable.WithHasHeader().WithData(table).Render() + }, +} + +func addShortName(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.toml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("toml") + + 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", shortName), command) + + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +func removeShortName(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.toml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("toml") + + if err := v.ReadInConfig(); err != nil { + return fmt.Errorf("failed to read config: %v", err) + } + + // Check if short name exists + if !v.IsSet(fmt.Sprintf("short_names.%s", shortName)) { + return fmt.Errorf("short name '%s' not found", shortName) + } + + // Get all short names + shortNames := v.GetStringMap("short_names") + delete(shortNames, shortName) + + // Update config with removed short name + v.Set("short_names", shortNames) + + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +func listShortNames() (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.toml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("toml") + + 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) + } + + shortNames := v.GetStringMapString("short_names") + if shortNames == nil { + return make(map[string]string), nil + } + + return shortNames, nil +} + +func init() { + ShortNameCmd.AddCommand(addShortNameCmd) + ShortNameCmd.AddCommand(removeShortNameCmd) + ShortNameCmd.AddCommand(listShortNameCmd) + 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 d8323c8..07cc96b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -47,8 +47,27 @@ 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() { - err := rootCmd.Execute() - if err != nil { + args := os.Args[1:] + + if len(args) > 0 { + // Check if the first argument is a short name + v := viper.New() + if home, err := os.UserHomeDir(); err == nil { + settingPath := filepath.Join(home, ".cfctl", "setting.toml") + v.SetConfigFile(settingPath) + v.SetConfigType("toml") + + if err := v.ReadInConfig(); err == nil { + if command := v.GetString(fmt.Sprintf("short_names.%s", args[0])); command != "" { + // Replace the short name with the actual command + newArgs := append(strings.Fields(command), args[1:]...) + os.Args = append([]string{os.Args[0]}, newArgs...) + } + } + } + } + + if err := rootCmd.Execute(); err != nil { os.Exit(1) } } @@ -90,6 +109,7 @@ func init() { rootCmd.AddCommand(other.ApiResourcesCmd) rootCmd.AddCommand(other.SettingCmd) rootCmd.AddCommand(other.LoginCmd) + rootCmd.AddCommand(other.ShortNameCmd) // Set default group for commands without a group for _, cmd := range rootCmd.Commands() { From a1e109d76796a4f2d59f401974c1977b5fd868bd Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 17:46:11 +0900 Subject: [PATCH 06/14] refactor: add short_names dynamically for matching specific verb Signed-off-by: Youngjin Jo --- cmd/other/apiResources.go | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/cmd/other/apiResources.go b/cmd/other/apiResources.go index 0b357bc..58c8dae 100644 --- a/cmd/other/apiResources.go +++ b/cmd/other/apiResources.go @@ -379,16 +379,33 @@ func fetchServiceResources(service, endpoint string, shortNamesMap map[string]st resourceName := s.Name[strings.LastIndex(s.Name, ".")+1:] verbs := getServiceMethods(client, s.Name) - // Find matching short name - var shortName string - for sn, cmd := range registeredShortNames { - if strings.Contains(cmd, fmt.Sprintf("%s list %s", service, resourceName)) { - shortName = sn - break + // Find all matching short names for this resource + verbsWithShortNames := make(map[string]string) + remainingVerbs := make([]string, 0) + + for _, verb := range verbs { + hasShortName := false + for sn, cmd := range registeredShortNames { + if strings.Contains(cmd, fmt.Sprintf("%s %s %s", service, verb, resourceName)) { + verbsWithShortNames[verb] = sn + hasShortName = true + break + } } + if !hasShortName { + remainingVerbs = append(remainingVerbs, verb) + } + } + + // Add row for verbs without short names + if len(remainingVerbs) > 0 { + data = append(data, []string{service, resourceName, "", strings.Join(remainingVerbs, ", ")}) } - data = append(data, []string{service, resourceName, shortName, strings.Join(verbs, ", ")}) + // Add separate rows for each verb with a short name + for verb, shortName := range verbsWithShortNames { + data = append(data, []string{service, resourceName, shortName, verb}) + } } return data, nil From d4b46af464376f554b0e7ac68918ecd8921ef443 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 18:19:48 +0900 Subject: [PATCH 07/14] feat: complete short_name command Signed-off-by: Youngjin Jo --- cmd/common/serviceCommand.go | 69 ++++++++++++++ cmd/common/shortNameResolver.go | 43 +++++++++ cmd/other/apiResources.go | 26 +++-- cmd/other/shortNames.go | 163 ++++++++++++++++++++++++++------ cmd/root.go | 55 +++++++++-- 5 files changed, 311 insertions(+), 45 deletions(-) create mode 100644 cmd/common/serviceCommand.go create mode 100644 cmd/common/shortNameResolver.go diff --git a/cmd/common/serviceCommand.go b/cmd/common/serviceCommand.go new file mode 100644 index 0000000..a3a9c69 --- /dev/null +++ b/cmd/common/serviceCommand.go @@ -0,0 +1,69 @@ +package common + +import ( + "fmt" + "strings" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// CreateServiceCommand creates a new cobra command for a service +func CreateServiceCommand(serviceName string) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s [flags]\n %s [flags]", serviceName, serviceName), + Short: fmt.Sprintf("Interact with the %s service", serviceName), + RunE: func(cmd *cobra.Command, args []string) error { + // If no args provided, show help + if len(args) == 0 { + return cmd.Help() + } + + // Check if the first argument is a short name + if actualVerb, actualResource, isShortName := ResolveShortName(serviceName, args[0]); isShortName { + // Replace the short name with actual verb and resource + args = append([]string{actualVerb, actualResource}, args[1:]...) + } + + // After resolving short name, proceed with normal command processing + if len(args) < 2 { + return cmd.Help() + } + + verb := args[0] + resource := args[1] + + // Create options from remaining args + options := &FetchOptions{ + Parameters: make([]string, 0), + } + + // Process remaining args as parameters + for i := 2; i < len(args); i++ { + if strings.HasPrefix(args[i], "--") { + paramName := strings.TrimPrefix(args[i], "--") + if i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") { + options.Parameters = append(options.Parameters, fmt.Sprintf("%s=%s", paramName, args[i+1])) + i++ + } + } + } + + // Call FetchService with the processed arguments + result, err := FetchService(serviceName, verb, resource, options) + if err != nil { + pterm.Error.Printf("Failed to execute command: %v\n", err) + return err + } + + if result != nil { + // The result will be printed by FetchService if needed + return nil + } + + return nil + }, + } + + return cmd +} diff --git a/cmd/common/shortNameResolver.go b/cmd/common/shortNameResolver.go new file mode 100644 index 0000000..0cc777d --- /dev/null +++ b/cmd/common/shortNameResolver.go @@ -0,0 +1,43 @@ +package common + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/viper" +) + +// ResolveShortName checks if the given verb is a short name and returns the actual command if it is +func ResolveShortName(service, verb string) (string, string, bool) { + home, err := os.UserHomeDir() + if err != nil { + return "", "", false + } + + settingPath := filepath.Join(home, ".cfctl", "setting.toml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("toml") + + if err := v.ReadInConfig(); err != nil { + return "", "", false + } + + // Check if there are short names for this service + serviceShortNames := v.GetStringMapString(fmt.Sprintf("short_names.%s", service)) + if serviceShortNames == nil { + return "", "", false + } + + // Check if the verb is a short name + if command, exists := serviceShortNames[verb]; exists { + parts := strings.SplitN(command, " ", 2) + if len(parts) == 2 { + return parts[0], parts[1], true + } + } + + return "", "", false +} diff --git a/cmd/other/apiResources.go b/cmd/other/apiResources.go index 58c8dae..6794b8c 100644 --- a/cmd/other/apiResources.go +++ b/cmd/other/apiResources.go @@ -383,18 +383,24 @@ func fetchServiceResources(service, endpoint string, shortNamesMap map[string]st verbsWithShortNames := make(map[string]string) remainingVerbs := make([]string, 0) - for _, verb := range verbs { - hasShortName := false - for sn, cmd := range registeredShortNames { - if strings.Contains(cmd, fmt.Sprintf("%s %s %s", service, verb, resourceName)) { - verbsWithShortNames[verb] = sn - hasShortName = true - break + // 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) } } - if !hasShortName { - remainingVerbs = append(remainingVerbs, verb) - } + } else { + remainingVerbs = verbs } // Add row for verbs without short names diff --git a/cmd/other/shortNames.go b/cmd/other/shortNames.go index 7121013..cc0f4f6 100644 --- a/cmd/other/shortNames.go +++ b/cmd/other/shortNames.go @@ -4,12 +4,15 @@ import ( "fmt" "os" "path/filepath" + "strings" "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", @@ -17,37 +20,132 @@ var ShortNameCmd = &cobra.Command{ 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.toml")) + mainV.SetConfigType("toml") + 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 := 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 add -n user -c "identity list User" + Example: ` $ cfctl short_name -s inventory add -n job -c "list Job" Then use them as: - $ cfctl user # This command is same as $ cfctl identity list User`, + $ 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") { + 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 add -n user -c "identity list User" + $ cfctl short_name -s inventory add -n job -c "list Job" Then use them as: - $ cfctl user # This command is same as $ cfctl identity list User`) + $ 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(shortName, command); err != nil { + 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 command '%s'\n", shortName, command) + pterm.Success.Printf("Successfully added short name '%s' for service '%s' command '%s'\n", shortName, service, command) }, } @@ -56,18 +154,19 @@ var removeShortNameCmd = &cobra.Command{ Short: "Remove a short name", Run: func(cmd *cobra.Command, args []string) { shortName, err := cmd.Flags().GetString("name") - if err != nil || shortName == "" { - pterm.Error.Println("The --name (-n) flag is required") + 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(shortName); err != nil { + 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'\n", shortName) + pterm.Success.Printf("Successfully removed short name '%s' from service '%s'\n", shortName, service) }, } @@ -88,12 +187,14 @@ var listShortNameCmd = &cobra.Command{ // Create table table := pterm.TableData{ - {"Short Name", "Command"}, + {"Service", "Short Name", "Command"}, } // Add short names to table - for name, command := range shortNames { - table = append(table, []string{name, command}) + for service, serviceShortNames := range shortNames { + for name, command := range serviceShortNames.(map[string]interface{}) { + table = append(table, []string{service, name, command.(string)}) + } } // Print table @@ -101,7 +202,7 @@ var listShortNameCmd = &cobra.Command{ }, } -func addShortName(shortName, command string) error { +func addShortName(service, shortName, command string) error { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %v", err) @@ -116,7 +217,7 @@ func addShortName(shortName, command string) error { return fmt.Errorf("failed to read config: %v", err) } - v.Set(fmt.Sprintf("short_names.%s", shortName), command) + 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) @@ -125,7 +226,7 @@ func addShortName(shortName, command string) error { return nil } -func removeShortName(shortName string) error { +func removeShortName(service, shortName string) error { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %v", err) @@ -140,17 +241,17 @@ func removeShortName(shortName string) error { return fmt.Errorf("failed to read config: %v", err) } - // Check if short name exists - if !v.IsSet(fmt.Sprintf("short_names.%s", shortName)) { - return fmt.Errorf("short name '%s' not found", shortName) + // 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 - shortNames := v.GetStringMap("short_names") - delete(shortNames, shortName) + // 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("short_names", shortNames) + v.Set(fmt.Sprintf("short_names.%s", service), serviceShortNames) if err := v.WriteConfig(); err != nil { return fmt.Errorf("failed to write config: %v", err) @@ -159,7 +260,7 @@ func removeShortName(shortName string) error { return nil } -func listShortNames() (map[string]string, error) { +func listShortNames() (map[string]interface{}, error) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("failed to get home directory: %v", err) @@ -172,14 +273,14 @@ func listShortNames() (map[string]string, error) { if err := v.ReadInConfig(); err != nil { if os.IsNotExist(err) { - return make(map[string]string), nil + return make(map[string]interface{}), nil } return nil, fmt.Errorf("failed to read config: %v", err) } - shortNames := v.GetStringMapString("short_names") + shortNames := v.GetStringMap("short_names") if shortNames == nil { - return make(map[string]string), nil + return make(map[string]interface{}), nil } return shortNames, nil @@ -189,10 +290,16 @@ 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 07cc96b..62767f7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -49,8 +49,8 @@ var rootCmd = &cobra.Command{ func Execute() { args := os.Args[1:] - if len(args) > 0 { - // Check if the first argument is a short name + 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.toml") @@ -58,9 +58,12 @@ func Execute() { v.SetConfigType("toml") if err := v.ReadInConfig(); err == nil { - if command := v.GetString(fmt.Sprintf("short_names.%s", args[0])); command != "" { + 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(strings.Fields(command), args[1:]...) + newArgs := append([]string{args[0]}, strings.Fields(command)...) + newArgs = append(newArgs, args[2:]...) os.Args = append([]string{os.Args[0]}, newArgs...) } } @@ -460,12 +463,50 @@ func createServiceCommand(serviceName string) *cobra.Command { Short: fmt.Sprintf("Interact with the %s service", serviceName), Long: fmt.Sprintf(`Use this command to interact with the %s service.`, serviceName), GroupID: "available", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { + // If no args provided, show available verbs if len(args) == 0 { common.PrintAvailableVerbs(cmd) - return + return nil } - cmd.Help() + + // Process command arguments + if len(args) < 2 { + return cmd.Help() + } + + verb := args[0] + resource := args[1] + + // Create options from remaining args + options := &common.FetchOptions{ + Parameters: make([]string, 0), + } + + // Process remaining args as parameters + for i := 2; i < len(args); i++ { + if strings.HasPrefix(args[i], "--") { + paramName := strings.TrimPrefix(args[i], "--") + if i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") { + options.Parameters = append(options.Parameters, fmt.Sprintf("%s=%s", paramName, args[i+1])) + i++ + } + } + } + + // Call FetchService with the processed arguments + result, err := common.FetchService(serviceName, verb, resource, options) + if err != nil { + pterm.Error.Printf("Failed to execute command: %v\n", err) + return err + } + + if result != nil { + // The result will be printed by FetchService if needed + return nil + } + + return nil }, } From f9157c7c794cfeb3f84ca2c4ead1077953313d6e Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 18:31:09 +0900 Subject: [PATCH 08/14] feat: add api_resources subcommand for all microservices Signed-off-by: Youngjin Jo --- cmd/common/apiResources.go | 112 ++++++++++++++++++++------------ cmd/common/fetchApiResources.go | 2 +- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/cmd/common/apiResources.go b/cmd/common/apiResources.go index 9112480..6f7ebb9 100644 --- a/cmd/common/apiResources.go +++ b/cmd/common/apiResources.go @@ -14,6 +14,7 @@ import ( "github.com/jhump/protoreflect/grpcreflect" "github.com/pterm/pterm" + "github.com/spf13/viper" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" @@ -121,7 +122,31 @@ func fetchServiceResources(serviceName, endpoint string, shortNamesMap map[strin return nil, fmt.Errorf("failed to list services: %v", err) } + // Load short names from setting.toml + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.toml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("toml") + + serviceShortNames := make(map[string]string) + if err := v.ReadInConfig(); err == nil { + // Get short names for this service + shortNamesSection := v.GetStringMap(fmt.Sprintf("short_names.%s", serviceName)) + for shortName, cmd := range shortNamesSection { + if cmdStr, ok := cmd.(string); ok { + serviceShortNames[shortName] = cmdStr + } + } + } + data := [][]string{} + resourceData := make(map[string][][]string) + for _, s := range services { if strings.HasPrefix(s, "grpc.reflection.") { continue @@ -137,64 +162,67 @@ func fetchServiceResources(serviceName, endpoint string, shortNamesMap map[strin } resourceName := s[strings.LastIndex(s, ".")+1:] - shortName := shortNamesMap[fmt.Sprintf("%s.%s", serviceName, resourceName)] - verbs := []string{} for _, method := range serviceDesc.GetMethods() { verbs = append(verbs, method.GetName()) } - data = append(data, []string{strings.Join(verbs, ", "), resourceName, shortName}) - } + // Create a map to track which verbs have been used in short names + usedVerbs := make(map[string]bool) + resourceRows := [][]string{} + + // First, check for verbs with short names + for shortName, cmdStr := range serviceShortNames { + parts := strings.Fields(cmdStr) + if len(parts) == 2 && parts[1] == resourceName { + verb := parts[0] + usedVerbs[verb] = true + // Add a row for the verb with short name + resourceRows = append(resourceRows, []string{serviceName, verb, resourceName, shortName}) + } + } - return data, nil -} + // Then add remaining verbs + remainingVerbs := []string{} + for _, verb := range verbs { + if !usedVerbs[verb] { + remainingVerbs = append(remainingVerbs, verb) + } + } -func renderAPITable(data [][]string) { - // Sort the data by the 'Resource' column alphabetically - sort.Slice(data, func(i, j int) bool { - return data[i][1] < data[j][1] - }) + if len(remainingVerbs) > 0 { + resourceRows = append([][]string{{serviceName, strings.Join(remainingVerbs, ", "), resourceName, ""}}, resourceRows...) + } - // Calculate the terminal width - terminalWidth, _, err := pterm.GetTerminalSize() - if err != nil { - terminalWidth = 80 // Default width if unable to get terminal size + resourceData[resourceName] = resourceRows } - // Define the minimum widths for the columns - minResourceWidth := 15 - minShortNameWidth := 15 - padding := 5 // Padding between columns and borders - - // Calculate the available width for the Verb column - verbColumnWidth := terminalWidth - (minResourceWidth + minShortNameWidth + padding) - if verbColumnWidth < 20 { - verbColumnWidth = 20 // Minimum width for the Verb column + // Sort resources alphabetically + var resources []string + for resource := range resourceData { + resources = append(resources, resource) } + sort.Strings(resources) - // Prepare the table data with headers - table := pterm.TableData{{"Verb", "Resource", "Short Names"}} - - for _, row := range data { - verbs := row[0] - resource := row[1] - shortName := row[2] + // Build final data array + for _, resource := range resources { + data = append(data, resourceData[resource]...) + } - // Wrap the verbs text based on the calculated column width - wrappedVerbs := wordWrap(verbs, verbColumnWidth) + return data, nil +} - // Build the table row - table = append(table, []string{wrappedVerbs, resource, shortName}) +func renderAPITable(data [][]string) { + // Create table header + table := pterm.TableData{ + {"Service", "Verb", "Resource", "Short Names"}, } - // Render the table using pterm with separators - pterm.DefaultTable.WithHasHeader(). - WithRowSeparator("-"). - WithHeaderRowSeparator("-"). - WithLeftAlignment(). - WithData(table). - Render() + // Add data rows + table = append(table, data...) + + // Render the table + pterm.DefaultTable.WithHasHeader().WithData(table).Render() } // wordWrap function remains the same diff --git a/cmd/common/fetchApiResources.go b/cmd/common/fetchApiResources.go index 32b6b8b..2489742 100644 --- a/cmd/common/fetchApiResources.go +++ b/cmd/common/fetchApiResources.go @@ -11,7 +11,7 @@ import ( // FetchApiResourcesCmd provides api-resources command for the given service func FetchApiResourcesCmd(serviceName string) *cobra.Command { return &cobra.Command{ - Use: "api-resources", + 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) From c5efcd00c8f812f3a97bb11a3e469bbf2f7999fc Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 18:54:21 +0900 Subject: [PATCH 09/14] feat: add interactive mode Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 61 +++++++++++++++++++++++++++++++ cmd/common/serviceCommand.go | 69 ------------------------------------ 2 files changed, 61 insertions(+), 69 deletions(-) delete mode 100644 cmd/common/serviceCommand.go diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index 88afd1e..e856a6d 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -153,8 +153,47 @@ func FetchService(serviceName string, verb string, resourceName string, options return nil, nil } + // Call the service jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options) if err != nil { + // Check if the error is about missing required parameters + if strings.Contains(err.Error(), "ERROR_REQUIRED_PARAMETER") { + // Extract parameter name from error message + paramName := extractParameterName(err.Error()) + if paramName != "" { + // Ask user for the missing parameter + pterm.Info.Printf("Required parameter '%s' is missing.\n", paramName) + value, err := promptForParameter(paramName) + if err != nil { + return nil, err + } + + // Add the parameter to options + if options.Parameters == nil { + options.Parameters = make([]string, 0) + } + options.Parameters = append(options.Parameters, fmt.Sprintf("%s=%s", paramName, value)) + + // Retry the call with the new parameter + jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options) + if err != nil { + return nil, err + } + + // Unmarshal JSON bytes to a map + var respMap map[string]interface{} + if err := json.Unmarshal(jsonBytes, &respMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) + } + + // Print the data if not in watch mode + if options.OutputFormat != "" { + printData(respMap, options) + } + + return respMap, nil + } + } return nil, err } @@ -172,6 +211,28 @@ func FetchService(serviceName string, verb string, resourceName string, options return respMap, nil } +// extractParameterName extracts the parameter name from the error message +func extractParameterName(errMsg string) string { + if strings.Contains(errMsg, "Required parameter. (key = ") { + start := strings.Index(errMsg, "key = ") + 6 + end := strings.Index(errMsg[start:], ")") + if end != -1 { + return errMsg[start : start+end] + } + } + return "" +} + +// promptForParameter prompts the user to enter a value for the given parameter +func promptForParameter(paramName string) (string, error) { + prompt := fmt.Sprintf("Please enter value for '%s'", paramName) + result, err := pterm.DefaultInteractiveTextInput.WithDefaultText("").Show(prompt) + if err != nil { + return "", fmt.Errorf("failed to read input: %v", err) + } + return result, nil +} + func loadConfig() (*Config, error) { home, err := os.UserHomeDir() if err != nil { diff --git a/cmd/common/serviceCommand.go b/cmd/common/serviceCommand.go deleted file mode 100644 index a3a9c69..0000000 --- a/cmd/common/serviceCommand.go +++ /dev/null @@ -1,69 +0,0 @@ -package common - -import ( - "fmt" - "strings" - - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -// CreateServiceCommand creates a new cobra command for a service -func CreateServiceCommand(serviceName string) *cobra.Command { - cmd := &cobra.Command{ - Use: fmt.Sprintf("%s [flags]\n %s [flags]", serviceName, serviceName), - Short: fmt.Sprintf("Interact with the %s service", serviceName), - RunE: func(cmd *cobra.Command, args []string) error { - // If no args provided, show help - if len(args) == 0 { - return cmd.Help() - } - - // Check if the first argument is a short name - if actualVerb, actualResource, isShortName := ResolveShortName(serviceName, args[0]); isShortName { - // Replace the short name with actual verb and resource - args = append([]string{actualVerb, actualResource}, args[1:]...) - } - - // After resolving short name, proceed with normal command processing - if len(args) < 2 { - return cmd.Help() - } - - verb := args[0] - resource := args[1] - - // Create options from remaining args - options := &FetchOptions{ - Parameters: make([]string, 0), - } - - // Process remaining args as parameters - for i := 2; i < len(args); i++ { - if strings.HasPrefix(args[i], "--") { - paramName := strings.TrimPrefix(args[i], "--") - if i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") { - options.Parameters = append(options.Parameters, fmt.Sprintf("%s=%s", paramName, args[i+1])) - i++ - } - } - } - - // Call FetchService with the processed arguments - result, err := FetchService(serviceName, verb, resource, options) - if err != nil { - pterm.Error.Printf("Failed to execute command: %v\n", err) - return err - } - - if result != nil { - // The result will be printed by FetchService if needed - return nil - } - - return nil - }, - } - - return cmd -} From c65520cec253d89971d5b4ed83a6cd82a98e4d49 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 19:10:51 +0900 Subject: [PATCH 10/14] feat: complete interactive mode Signed-off-by: Youngjin Jo --- cmd/common/fetchVerb.go | 25 ++++++++++++++++++++++++- cmd/common/helpers.go | 10 ++++++++++ cmd/root.go | 27 ++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/cmd/common/fetchVerb.go b/cmd/common/fetchVerb.go index 687ce6b..5400032 100644 --- a/cmd/common/fetchVerb.go +++ b/cmd/common/fetchVerb.go @@ -50,7 +50,30 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin verbCmd := &cobra.Command{ Use: currentVerb + " ", Short: shortDesc, - Args: cobra.ArbitraryArgs, // Allow any number of arguments + Long: fmt.Sprintf(`Supported %d resources for %s command. + +%s + +%s`, + len(resources), + currentVerb, + pterm.DefaultBox.WithTitle("Interactive Mode").WithTitleTopCenter().Sprint( + func() string { + str, _ := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{ + {Level: 0, Text: "Required parameters will be prompted if not provided"}, + {Level: 0, Text: "Missing parameters will be requested interactively"}, + {Level: 0, Text: "Just follow the prompts to fill in the required fields"}, + }).Srender() + return str + }()), + pterm.DefaultBox.WithTitle("Example").WithTitleTopCenter().Sprint( + fmt.Sprintf("Instead of:\n"+ + " $ cfctl %s %s -p key=value\n\n"+ + "You can simply run:\n"+ + " $ cfctl %s %s \n\n"+ + "The tool will interactively prompt for the required parameters.", + serviceName, currentVerb, serviceName, 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 diff --git a/cmd/common/helpers.go b/cmd/common/helpers.go index e449c88..c9f10a0 100644 --- a/cmd/common/helpers.go +++ b/cmd/common/helpers.go @@ -117,6 +117,11 @@ func CustomParentHelpFunc(cmd *cobra.Command, args []string) { cmd.Println() } + if cmd.Long != "" { + cmd.Println(cmd.Long) + cmd.Println() + } + printSortedBulletList(cmd, "Verbs") cmd.Println("Flags:") @@ -168,6 +173,11 @@ func CustomVerbHelpFunc(cmd *cobra.Command, args []string) { cmd.Println() } + if cmd.Long != "" { + cmd.Println(cmd.Long) + cmd.Println() + } + if resourcesStr, ok := cmd.Annotations["resources"]; ok && resourcesStr != "" { resources := strings.Split(resourcesStr, ", ") sort.Strings(resources) diff --git a/cmd/root.go b/cmd/root.go index 62767f7..98b970a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -459,9 +459,30 @@ func loadConfig() (*Config, error) { func createServiceCommand(serviceName string) *cobra.Command { cmd := &cobra.Command{ - Use: serviceName, - Short: fmt.Sprintf("Interact with the %s service", serviceName), - Long: fmt.Sprintf(`Use this command to interact with the %s service.`, serviceName), + Use: serviceName, + Short: fmt.Sprintf("Interact with the %s service", serviceName), + Long: fmt.Sprintf(`Use this command to interact with the %s service. + +%s + +%s`, + serviceName, + pterm.DefaultBox.WithTitle("Interactive Mode").WithTitleTopCenter().Sprint( + func() string { + str, _ := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{ + {Level: 0, Text: "Required parameters will be prompted if not provided"}, + {Level: 0, Text: "Missing parameters will be requested interactively"}, + {Level: 0, Text: "Just follow the prompts to fill in the required fields"}, + }).Srender() + return str + }()), + pterm.DefaultBox.WithTitle("Example").WithTitleTopCenter().Sprint( + fmt.Sprintf("Instead of:\n"+ + " $ cfctl %s -p key=value\n\n"+ + "You can simply run:\n"+ + " $ cfctl %s \n\n"+ + "The tool will interactively prompt for the required parameters.", + serviceName, serviceName))), GroupID: "available", RunE: func(cmd *cobra.Command, args []string) error { // If no args provided, show available verbs From 9a399a67b1dde7d60cbe76695734e538fef8f7d0 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 19:31:45 +0900 Subject: [PATCH 11/14] feat: add sort feature Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 68 ++++++++++++++++++++++++++++++++++++++ cmd/common/fetchVerb.go | 51 ++++++++++++++++++++++++---- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index e856a6d..6e4dfd8 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -188,6 +188,40 @@ func FetchService(serviceName string, verb string, resourceName string, options // Print the data if not in watch mode if options.OutputFormat != "" { + if options.SortBy != "" && verb == "list" { + if results, ok := respMap["results"].([]interface{}); ok { + // Sort the results by the specified field + sort.Slice(results, func(i, j int) bool { + iMap := results[i].(map[string]interface{}) + jMap := results[j].(map[string]interface{}) + + iVal, iOk := iMap[options.SortBy] + jVal, jOk := jMap[options.SortBy] + + // Handle cases where the field doesn't exist + if !iOk && !jOk { + return false + } else if !iOk { + return false + } else if !jOk { + return true + } + + // Compare based on type + switch v := iVal.(type) { + case string: + return v < jVal.(string) + case float64: + return v < jVal.(float64) + case bool: + return !v && jVal.(bool) + default: + return false + } + }) + respMap["results"] = results + } + } printData(respMap, options) } @@ -205,6 +239,40 @@ func FetchService(serviceName string, verb string, resourceName string, options // Print the data if not in watch mode if options.OutputFormat != "" { + if options.SortBy != "" && verb == "list" { + if results, ok := respMap["results"].([]interface{}); ok { + // Sort the results by the specified field + sort.Slice(results, func(i, j int) bool { + iMap := results[i].(map[string]interface{}) + jMap := results[j].(map[string]interface{}) + + iVal, iOk := iMap[options.SortBy] + jVal, jOk := jMap[options.SortBy] + + // Handle cases where the field doesn't exist + if !iOk && !jOk { + return false + } else if !iOk { + return false + } else if !jOk { + return true + } + + // Compare based on type + switch v := iVal.(type) { + case string: + return v < jVal.(string) + case float64: + return v < jVal.(float64) + case bool: + return !v && jVal.(bool) + default: + return false + } + }) + respMap["results"] = results + } + } printData(respMap, options) } diff --git a/cmd/common/fetchVerb.go b/cmd/common/fetchVerb.go index 5400032..b0c74d5 100644 --- a/cmd/common/fetchVerb.go +++ b/cmd/common/fetchVerb.go @@ -23,6 +23,7 @@ type FetchOptions struct { APIVersion string OutputFormat string CopyToClipboard bool + SortBy string } // AddVerbCommands adds subcommands for each verb to the parent command @@ -67,12 +68,14 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin return str }()), pterm.DefaultBox.WithTitle("Example").WithTitleTopCenter().Sprint( - fmt.Sprintf("Instead of:\n"+ - " $ cfctl %s %s -p key=value\n\n"+ - "You can simply run:\n"+ - " $ cfctl %s %s \n\n"+ - "The tool will interactively prompt for the required parameters.", - serviceName, currentVerb, serviceName, currentVerb))), + fmt.Sprintf("List resources:\n"+ + " $ cfctl %s list \n\n"+ + "List and sort by field:\n"+ + " $ cfctl %s list -s name\n"+ + " $ cfctl %s list -s created_at\n\n"+ + "Watch for changes:\n"+ + " $ cfctl %s list -w", + serviceName, serviceName, serviceName, serviceName))), Args: cobra.ArbitraryArgs, // Allow any number of arguments RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { @@ -108,6 +111,11 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin return err } + sortBy := "" + if currentVerb == "list" { + sortBy, _ = cmd.Flags().GetString("sort") + } + options := &FetchOptions{ Parameters: parameters, JSONParameter: jsonParameter, @@ -115,6 +123,7 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin APIVersion: apiVersion, OutputFormat: outputFormat, CopyToClipboard: copyToClipboard, + SortBy: sortBy, } if currentVerb == "list" && !cmd.Flags().Changed("output") { @@ -142,6 +151,7 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin 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')") } // Define flags for verbCmd @@ -155,6 +165,35 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin // Set custom help function verbCmd.SetHelpFunc(CustomVerbHelpFunc) + // Update example for list command + if currentVerb == "list" { + verbCmd.Long = fmt.Sprintf(`Supported %d resources for %s command. + +%s + +%s`, + len(resources), + currentVerb, + pterm.DefaultBox.WithTitle("Interactive Mode").WithTitleTopCenter().Sprint( + func() string { + str, _ := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{ + {Level: 0, Text: "Required parameters will be prompted if not provided"}, + {Level: 0, Text: "Missing parameters will be requested interactively"}, + {Level: 0, Text: "Just follow the prompts to fill in the required fields"}, + }).Srender() + return str + }()), + pterm.DefaultBox.WithTitle("Example").WithTitleTopCenter().Sprint( + fmt.Sprintf("List resources:\n"+ + " $ cfctl %s list \n\n"+ + "List and sort by field:\n"+ + " $ cfctl %s list -s name\n"+ + " $ cfctl %s list -s created_at\n\n"+ + "Watch for changes:\n"+ + " $ cfctl %s list -w", + serviceName, serviceName, serviceName, serviceName))) + } + parentCmd.AddCommand(verbCmd) } From ac2312ac75091cae41ac230030bdd8eb13c21561 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 19:37:08 +0900 Subject: [PATCH 12/14] feat: view all fields of list Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index 6e4dfd8..c025225 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -568,17 +568,23 @@ func printTable(data map[string]interface{}) string { } defer keyboard.Close() - // Extract headers - headers := []string{} - if len(results) > 0 { - if row, ok := results[0].(map[string]interface{}); ok { + // Extract headers from all results to ensure we get all possible fields + headers := make(map[string]bool) + for _, result := range results { + if row, ok := result.(map[string]interface{}); ok { for key := range row { - headers = append(headers, key) + headers[key] = true } - sort.Strings(headers) } } + // Convert headers map to sorted slice + headerSlice := make([]string, 0, len(headers)) + for key := range headers { + headerSlice = append(headerSlice, key) + } + sort.Strings(headerSlice) + for { if searchTerm != "" { filteredResults = filterResults(results, searchTerm) @@ -589,7 +595,7 @@ func printTable(data map[string]interface{}) string { totalItems := len(filteredResults) totalPages := (totalItems + pageSize - 1) / pageSize - tableData := pterm.TableData{headers} + tableData := pterm.TableData{headerSlice} // Calculate current page items startIdx := currentPage * pageSize @@ -608,8 +614,8 @@ func printTable(data map[string]interface{}) string { // Add rows for current page using filteredResults for _, result := range filteredResults[startIdx:endIdx] { if row, ok := result.(map[string]interface{}); ok { - rowData := make([]string, len(headers)) - for i, key := range headers { + rowData := make([]string, len(headerSlice)) + for i, key := range headerSlice { rowData[i] = formatTableValue(row[key]) } tableData = append(tableData, rowData) @@ -799,3 +805,4 @@ func formatCSVValue(val interface{}) string { return fmt.Sprintf("%v", v) } } + From bd1d93059b6bfff322faaa6e0a64b8cf3b6a6dac Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 19:56:15 +0900 Subject: [PATCH 13/14] feat: add minimal flags Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 142 ++++++++++++++++++++++++++++++++++--- cmd/common/fetchVerb.go | 3 + 2 files changed, 136 insertions(+), 9 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index c025225..a2248e2 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -153,6 +153,40 @@ func FetchService(serviceName string, verb string, resourceName string, options return nil, nil } + // Get hostPort based on environment prefix + var envPrefix string + if strings.HasPrefix(config.Environment, "dev-") { + envPrefix = "dev" + } else if strings.HasPrefix(config.Environment, "stg-") { + envPrefix = "stg" + } + hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", serviceName, envPrefix) + + // Configure gRPC connection + tlsConfig := &tls.Config{ + InsecureSkipVerify: false, + } + creds := credentials.NewTLS(tlsConfig) + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(creds), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB + grpc.MaxCallSendMsgSize(10*1024*1024), // 10MB + ), + } + + // Establish the connection + conn, err := grpc.Dial(hostPort, opts...) + if err != nil { + return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", hostPort, err) + } + defer conn.Close() + + // Create reflection client for both service calls and minimal fields detection + ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Environments[config.Environment].Token) + refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) + defer refClient.Reset() + // Call the service jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options) if err != nil { @@ -175,14 +209,14 @@ func FetchService(serviceName string, verb string, resourceName string, options options.Parameters = append(options.Parameters, fmt.Sprintf("%s=%s", paramName, value)) // Retry the call with the new parameter - jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options) + jsonBytes, err = fetchJSONResponse(config, serviceName, verb, resourceName, options) if err != nil { return nil, err } // Unmarshal JSON bytes to a map var respMap map[string]interface{} - if err := json.Unmarshal(jsonBytes, &respMap); err != nil { + if err = json.Unmarshal(jsonBytes, &respMap); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) } @@ -222,7 +256,7 @@ func FetchService(serviceName string, verb string, resourceName string, options respMap["results"] = results } } - printData(respMap, options) + printData(respMap, options, serviceName, resourceName, refClient) } return respMap, nil @@ -233,7 +267,7 @@ func FetchService(serviceName string, verb string, resourceName string, options // Unmarshal JSON bytes to a map var respMap map[string]interface{} - if err := json.Unmarshal(jsonBytes, &respMap); err != nil { + if err = json.Unmarshal(jsonBytes, &respMap); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON: %v", err) } @@ -273,7 +307,7 @@ func FetchService(serviceName string, verb string, resourceName string, options respMap["results"] = results } } - printData(respMap, options) + printData(respMap, options, serviceName, resourceName, refClient) } return respMap, nil @@ -495,7 +529,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) { +func printData(data map[string]interface{}, options *FetchOptions, serviceName, resourceName string, refClient *grpcreflect.Client) { var output string switch options.OutputFormat { @@ -521,13 +555,13 @@ func printData(data map[string]interface{}, options *FetchOptions) { case "table": // Check if data has 'results' key if _, ok := data["results"].([]interface{}); ok { - output = printTable(data) + output = printTable(data, options, serviceName, resourceName, refClient) } else { // If no 'results' key, treat the entire data as results wrappedData := map[string]interface{}{ "results": []interface{}{data}, } - output = printTable(wrappedData) + output = printTable(wrappedData, options, serviceName, resourceName, refClient) } case "csv": @@ -554,7 +588,83 @@ func printData(data map[string]interface{}, options *FetchOptions) { } } -func printTable(data map[string]interface{}) string { +func getMinimalFields(serviceName, resourceName string, refClient *grpcreflect.Client) []string { + // Default minimal fields that should always be included if they exist + defaultFields := []string{"name", "created_at"} + + // Try to get message descriptor for the resource + fullServiceName := fmt.Sprintf("spaceone.api.%s.v1.%s", serviceName, resourceName) + serviceDesc, err := refClient.ResolveService(fullServiceName) + if err != nil { + // Try v2 if v1 fails + fullServiceName = fmt.Sprintf("spaceone.api.%s.v2.%s", serviceName, resourceName) + serviceDesc, err = refClient.ResolveService(fullServiceName) + if err != nil { + return defaultFields + } + } + + // Get list method descriptor + listMethod := serviceDesc.FindMethodByName("list") + if listMethod == nil { + return defaultFields + } + + // Get response message descriptor + respDesc := listMethod.GetOutputType() + if respDesc == nil { + return defaultFields + } + + // Find the 'results' field which should be repeated message type + resultsField := respDesc.FindFieldByName("results") + if resultsField == nil { + return defaultFields + } + + // Get the message type of items in the results + itemMsgDesc := resultsField.GetMessageType() + if itemMsgDesc == nil { + return defaultFields + } + + // Collect required fields and important fields + minimalFields := make([]string, 0) + fields := itemMsgDesc.GetFields() + for _, field := range fields { + // Add ID fields + if strings.HasSuffix(field.GetName(), "_id") { + minimalFields = append(minimalFields, field.GetName()) + continue + } + + // Add status/state fields + if field.GetName() == "status" || field.GetName() == "state" { + minimalFields = append(minimalFields, field.GetName()) + continue + } + + // Add timestamp fields + if field.GetName() == "created_at" || field.GetName() == "finished_at" { + minimalFields = append(minimalFields, field.GetName()) + continue + } + + // Add name field + if field.GetName() == "name" { + minimalFields = append(minimalFields, field.GetName()) + continue + } + } + + if len(minimalFields) == 0 { + return defaultFields + } + + return minimalFields +} + +func printTable(data map[string]interface{}, options *FetchOptions, serviceName, resourceName string, refClient *grpcreflect.Client) string { if results, ok := data["results"].([]interface{}); ok { pageSize := 10 currentPage := 0 @@ -585,6 +695,20 @@ func printTable(data map[string]interface{}) string { } sort.Strings(headerSlice) + // If minimal columns are requested, only show essential fields + if options.MinimalColumns { + minimalFields := getMinimalFields(serviceName, resourceName, refClient) + var minimalHeaderSlice []string + for _, field := range minimalFields { + if headers[field] { + minimalHeaderSlice = append(minimalHeaderSlice, field) + } + } + if len(minimalHeaderSlice) > 0 { + headerSlice = minimalHeaderSlice + } + } + for { if searchTerm != "" { filteredResults = filterResults(results, searchTerm) diff --git a/cmd/common/fetchVerb.go b/cmd/common/fetchVerb.go index b0c74d5..ca04c42 100644 --- a/cmd/common/fetchVerb.go +++ b/cmd/common/fetchVerb.go @@ -24,6 +24,7 @@ type FetchOptions struct { OutputFormat string CopyToClipboard bool SortBy string + MinimalColumns bool } // AddVerbCommands adds subcommands for each verb to the parent command @@ -124,6 +125,7 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin OutputFormat: outputFormat, CopyToClipboard: copyToClipboard, SortBy: sortBy, + MinimalColumns: cmd.Flag("minimal").Changed, } if currentVerb == "list" && !cmd.Flags().Changed("output") { @@ -152,6 +154,7 @@ func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID strin 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") } // Define flags for verbCmd From 53ece853e142ccf61aa99571a4962d145d18b6d5 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 5 Dec 2024 20:51:33 +0900 Subject: [PATCH 14/14] fix: modify both _ and - cases Signed-off-by: Youngjin Jo --- cmd/common/helpers.go | 21 ++++++++++----------- cmd/root.go | 1 + 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/common/helpers.go b/cmd/common/helpers.go index c9f10a0..50f749b 100644 --- a/cmd/common/helpers.go +++ b/cmd/common/helpers.go @@ -21,6 +21,12 @@ import ( "github.com/jhump/protoreflect/grpcreflect" ) +func convertServiceNameToEndpoint(serviceName string) string { + // cost_analysis -> cost-analysis + // file_manager -> file-manager + return strings.ReplaceAll(serviceName, "_", "-") +} + // BuildVerbResourceMap builds a mapping from verbs to resources for a given service func BuildVerbResourceMap(serviceName string) (map[string][]string, error) { config, err := loadConfig() @@ -36,7 +42,10 @@ func BuildVerbResourceMap(serviceName string) (map[string][]string, error) { } else { return nil, fmt.Errorf("unsupported environment prefix") } - hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", serviceName, envPrefix) + + // Convert service name to endpoint format + endpointServiceName := convertServiceNameToEndpoint(serviceName) + hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", endpointServiceName, envPrefix) // Configure gRPC connection var opts []grpc.DialOption @@ -117,11 +126,6 @@ func CustomParentHelpFunc(cmd *cobra.Command, args []string) { cmd.Println() } - if cmd.Long != "" { - cmd.Println(cmd.Long) - cmd.Println() - } - printSortedBulletList(cmd, "Verbs") cmd.Println("Flags:") @@ -173,11 +177,6 @@ func CustomVerbHelpFunc(cmd *cobra.Command, args []string) { cmd.Println() } - if cmd.Long != "" { - cmd.Println(cmd.Long) - cmd.Println() - } - if resourcesStr, ok := cmd.Annotations["resources"]; ok && resourcesStr != "" { resources := strings.Split(resourcesStr, ", ") sort.Strings(resources) diff --git a/cmd/root.go b/cmd/root.go index 98b970a..7d853e6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,6 +113,7 @@ func init() { rootCmd.AddCommand(other.SettingCmd) rootCmd.AddCommand(other.LoginCmd) rootCmd.AddCommand(other.ShortNameCmd) + rootCmd.AddCommand(other.AnalyzeCmd) // Set default group for commands without a group for _, cmd := range rootCmd.Commands() {