diff --git a/cmd/available/identity.go b/cmd/available/identity.go deleted file mode 100644 index 280033d..0000000 --- a/cmd/available/identity.go +++ /dev/null @@ -1,50 +0,0 @@ -// identity.go - -package available - -import ( - "fmt" - "os" - - "github.com/cloudforet-io/cfctl/cmd/common" - "github.com/spf13/cobra" -) - -var IdentityCmd = &cobra.Command{ - Use: "identity", - Short: "Interact with the Identity service", - Long: `Use this command to interact with the Identity service.`, - GroupID: "available", - Run: func(cmd *cobra.Command, args []string) { - // If no arguments are provided, display the available verbs - if len(args) == 0 { - common.PrintAvailableVerbs(cmd) - return - } - - // If arguments are provided, proceed normally - cmd.Help() - }, -} - -func init() { - IdentityCmd.AddGroup(&cobra.Group{ - ID: "available", - Title: "Available Commands:", - }, &cobra.Group{ - ID: "other", - Title: "Other Commands:", - }) - - // Set custom help function using common.CustomHelpFunc - IdentityCmd.SetHelpFunc(common.CustomParentHelpFunc) - - apiResourcesCmd := common.FetchApiResourcesCmd("identity") - apiResourcesCmd.GroupID = "available" - IdentityCmd.AddCommand(apiResourcesCmd) - - err := common.AddVerbCommands(IdentityCmd, "identity", "other") - if err != nil { - fmt.Fprintf(os.Stderr, "Error adding verb commands: %v\n", err) - } -} diff --git a/cmd/available/inventory.go b/cmd/available/inventory.go deleted file mode 100644 index 5a9f74a..0000000 --- a/cmd/available/inventory.go +++ /dev/null @@ -1,50 +0,0 @@ -// inventory.go - -package available - -import ( - "fmt" - "os" - - "github.com/cloudforet-io/cfctl/cmd/common" - "github.com/spf13/cobra" -) - -var InventoryCmd = &cobra.Command{ - Use: "inventory", - Short: "Interact with the Inventory service", - Long: `Use this command to interact with the Inventory service.`, - GroupID: "available", - Run: func(cmd *cobra.Command, args []string) { - // If no arguments are provided, display the available verbs - if len(args) == 0 { - common.PrintAvailableVerbs(cmd) - return - } - - // If arguments are provided, proceed normally - cmd.Help() - }, -} - -func init() { - InventoryCmd.AddGroup(&cobra.Group{ - ID: "available", - Title: "Available Commands:", - }, &cobra.Group{ - ID: "other", - Title: "Other Commands:", - }) - - // Set custom help function using common.CustomParentHelpFunc - InventoryCmd.SetHelpFunc(common.CustomParentHelpFunc) - - apiResourcesCmd := common.FetchApiResourcesCmd("inventory") - apiResourcesCmd.GroupID = "available" - InventoryCmd.AddCommand(apiResourcesCmd) - - err := common.AddVerbCommands(InventoryCmd, "inventory", "available") - if err != nil { - fmt.Fprintf(os.Stderr, "Error adding verb commands: %v\n", err) - } -} diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index ac6c338..a8d950b 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -62,14 +62,21 @@ func FetchService(serviceName string, verb string, resourceName string, options return nil, fmt.Errorf("no environment set. Please run 'cfctl login' first") } - token := mainViper.GetString(fmt.Sprintf("environments.%s.token", currentEnv)) + // Load configuration first (including cache) + config, err := loadConfig() + if err != nil { + return nil, fmt.Errorf("failed to load config: %v", err) + } + + // Check token from both main config and cache + token := config.Environments[config.Environment].Token if token == "" { pterm.Error.Println("No token found for authentication.") // Get current endpoint - endpoint := mainViper.GetString(fmt.Sprintf("environments.%s.endpoint", currentEnv)) + endpoint := config.Environments[config.Environment].Endpoint - if strings.HasSuffix(currentEnv, "-app") { + if strings.HasSuffix(config.Environment, "-app") { // App environment message headerBox := pterm.DefaultBox.WithTitle("App Guide"). WithTitleTopCenter(). @@ -145,11 +152,6 @@ func FetchService(serviceName string, verb string, resourceName string, options return nil, nil } - config, err := loadConfig() - if err != nil { - return nil, fmt.Errorf("failed to load config: %v", err) - } - jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options) if err != nil { return nil, err @@ -175,7 +177,7 @@ func loadConfig() (*Config, error) { return nil, fmt.Errorf("failed to get home directory: %v", err) } - // Load main config + // Load main configuration file mainV := viper.New() mainConfigPath := filepath.Join(home, ".cfctl", "config.yaml") mainV.SetConfigFile(mainConfigPath) @@ -188,33 +190,20 @@ func loadConfig() (*Config, error) { return nil, fmt.Errorf("no environment set in config") } - // Try to get environment config from main config first - var envConfig *Environment - if mainEnvConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)); mainEnvConfig != nil { - envConfig = &Environment{ - Endpoint: mainEnvConfig.GetString("endpoint"), - Token: mainEnvConfig.GetString("token"), - Proxy: mainEnvConfig.GetString("proxy"), - } + // First try to 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 not found in main config or token is empty, try cache config - if envConfig == nil || envConfig.Token == "" { + // 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 { - if cacheEnvConfig := cacheV.Sub(fmt.Sprintf("environments.%s", currentEnv)); cacheEnvConfig != nil { - if envConfig == nil { - envConfig = &Environment{ - Endpoint: cacheEnvConfig.GetString("endpoint"), - Token: cacheEnvConfig.GetString("token"), - Proxy: cacheEnvConfig.GetString("proxy"), - } - } else if envConfig.Token == "" { - envConfig.Token = cacheEnvConfig.GetString("token") - } - } + envConfig.Token = cacheV.GetString(fmt.Sprintf("environments.%s.token", currentEnv)) } } @@ -222,7 +211,6 @@ func loadConfig() (*Config, error) { return nil, fmt.Errorf("environment '%s' not found in config files", currentEnv) } - // Convert Environment to Config return &Config{ Environment: currentEnv, Environments: map[string]Environment{ @@ -398,7 +386,16 @@ func printData(data map[string]interface{}, options *FetchOptions) { fmt.Printf("---\n%s\n", output) case "table": - output = printTable(data) + // Check if data has 'results' key + if _, ok := data["results"].([]interface{}); ok { + output = printTable(data) + } else { + // If no 'results' key, treat the entire data as results + wrappedData := map[string]interface{}{ + "results": []interface{}{data}, + } + output = printTable(wrappedData) + } case "csv": output = printCSV(data) @@ -493,7 +490,7 @@ func printTable(data map[string]interface{}) string { pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() fmt.Printf("\nPage %d of %d (Total items: %d)\n", currentPage+1, totalPages, totalItems) - fmt.Println("Navigation: [p]revious page, [n]ext page, [/]search, [c]lear search, [q]uit") + fmt.Println("Navigation: [h]previous page, [l]next page, [/]search, [c]lear search, [q]uit") // Get keyboard input char, _, err := keyboard.GetKey() @@ -503,13 +500,13 @@ func printTable(data map[string]interface{}) string { } switch char { - case 'n', 'N': + case 'l', 'L': if currentPage < totalPages-1 { currentPage++ } else { currentPage = 0 } - case 'p', 'P': + case 'h', 'H': if currentPage > 0 { currentPage-- } else { diff --git a/cmd/other/apiResources.go b/cmd/other/apiResources.go index 86c3d79..9bc92af 100644 --- a/cmd/other/apiResources.go +++ b/cmd/other/apiResources.go @@ -125,7 +125,7 @@ var ApiResourcesCmd = &cobra.Command{ } // Fetch endpointsMap dynamically - endpointsMap, err := fetchEndpointsMap(endpoint) + endpointsMap, err := FetchEndpointsMap(endpoint) if err != nil { log.Fatalf("Failed to fetch endpointsMap from '%s': %v", endpoint, err) } @@ -219,7 +219,7 @@ var ApiResourcesCmd = &cobra.Command{ }, } -func fetchEndpointsMap(endpoint string) (map[string]string, error) { +func FetchEndpointsMap(endpoint string) (map[string]string, error) { // Parse the endpoint parts := strings.Split(endpoint, "://") if len(parts) != 2 { diff --git a/cmd/other/config.go b/cmd/other/config.go index c54de3f..569f967 100644 --- a/cmd/other/config.go +++ b/cmd/other/config.go @@ -104,8 +104,8 @@ var configInitURLCmd = &cobra.Command{ mainV := viper.New() mainV.SetConfigFile(mainConfigPath) - // Read the config file - if err := mainV.ReadInConfig(); err != nil { + // Create empty config if it doesn't exist + if err := mainV.ReadInConfig(); err != nil && !os.IsNotExist(err) { pterm.Error.Printf("Failed to read config file: %v\n", err) return } diff --git a/cmd/other/login.go b/cmd/other/login.go index e46d46d..3cb1a4c 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -77,8 +77,16 @@ func executeLogin(cmd *cobra.Command, args []string) { } configPath := filepath.Join(homeDir, ".cfctl", "config.yaml") - viper.SetConfigFile(configPath) + // Check if config file exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + pterm.Error.Println("No valid configuration found.") + pterm.Info.Println("Please run 'cfctl config init' to set up your configuration.") + pterm.Info.Println("After initialization, run 'cfctl login' to authenticate.") + return + } + + viper.SetConfigFile(configPath) if err := viper.ReadInConfig(); err != nil { pterm.Error.Printf("Failed to read config file: %v\n", err) return diff --git a/cmd/root.go b/cmd/root.go index 8fc8c3b..55213ac 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,19 +1,29 @@ -/* -Copyright © 2024 NAME HERE -*/ package cmd import ( + "fmt" "os" + "path/filepath" + "strings" - "github.com/cloudforet-io/cfctl/cmd/available" + "github.com/spf13/viper" + + "github.com/cloudforet-io/cfctl/cmd/common" "github.com/cloudforet-io/cfctl/cmd/other" + "github.com/pterm/pterm" "github.com/spf13/cobra" ) var cfgFile string +// Config represents the configuration structure +type Config struct { + Environment string + Endpoint string + Token string +} + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "cfctl", @@ -38,14 +48,24 @@ func Execute() { } func init() { + // Initialize available commands group AvailableCommands := &cobra.Group{ ID: "available", Title: "Available Commands:", } rootCmd.AddGroup(AvailableCommands) - rootCmd.AddCommand(available.IdentityCmd) - rootCmd.AddCommand(available.InventoryCmd) + // Skip dynamic service loading for config init commands + if len(os.Args) >= 3 && os.Args[1] == "config" && os.Args[2] == "init" { + // Skip dynamic service loading for initialization + } else { + // Try to add dynamic service commands + if err := addDynamicServiceCommands(); err != nil { + showInitializationGuide(err) + } + } + + // Initialize other commands group OtherCommands := &cobra.Group{ ID: "other", Title: "Other Commands:", @@ -55,9 +75,187 @@ func init() { rootCmd.AddCommand(other.ConfigCmd) rootCmd.AddCommand(other.LoginCmd) + // Set default group for commands without a group for _, cmd := range rootCmd.Commands() { if cmd.Name() != "help" && cmd.Name() != "completion" && cmd.GroupID == "" { cmd.GroupID = "other" } } } + +// showInitializationGuide displays a helpful message when configuration is missing +func showInitializationGuide(originalErr error) { + // Only show error message for commands that require configuration + if len(os.Args) >= 2 && (os.Args[1] == "config" || + os.Args[1] == "login" || + os.Args[1] == "api-resources") { // Add api-resources to skip list + return + } + + pterm.Error.Printf("No valid configuration found.\n") + pterm.Info.Println("Please run 'cfctl config init' to set up your configuration.") + pterm.Info.Println("After initialization, run 'cfctl login' to authenticate.") +} + +func addDynamicServiceCommands() error { + // Load configuration + config, err := loadConfig() + if err != nil { + return fmt.Errorf("failed to load config: %v", err) + } + + // Convert endpoint to identity endpoint if necessary + endpoint := config.Endpoint + if !strings.Contains(endpoint, "identity") { + parts := strings.Split(endpoint, "://") + if len(parts) == 2 { + hostParts := strings.Split(parts[1], ".") + if len(hostParts) >= 4 { + env := hostParts[2] // dev or stg + endpoint = fmt.Sprintf("grpc+ssl://identity.api.%s.spaceone.dev:443", env) + } + } + } + + // Fetch available microservices + endpointsMap, err := other.FetchEndpointsMap(endpoint) + if err != nil { + return fmt.Errorf("failed to fetch services: %v", err) + } + + // Create and register commands for each service + for serviceName := range endpointsMap { + cmd := createServiceCommand(serviceName) + rootCmd.AddCommand(cmd) + } + + return nil +} + +// loadConfig loads configuration from both main and cache config files +func loadConfig() (*Config, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("unable to find home directory: %v", err) + } + + configFile := filepath.Join(home, ".cfctl", "config.yaml") + cacheConfigFile := filepath.Join(home, ".cfctl", "cache", "config.yaml") + + var currentEnv string + var endpoint string + var token string + + // Try to read main config first + mainV := viper.New() + mainV.SetConfigFile(configFile) + mainConfigErr := mainV.ReadInConfig() + + if mainConfigErr == nil { + // Main config 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") + } + } + } + + // If main config doesn't have what we need, try cache config + if endpoint == "" || token == "" { + cacheV := viper.New() + cacheV.SetConfigFile(cacheConfigFile) + if err := cacheV.ReadInConfig(); err == nil { + // If no current environment set, try to get it from cache config + if currentEnv == "" { + currentEnv = cacheV.GetString("environment") + } + + // Try to get environment config 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 + } + } + } + } + } + } + + if endpoint == "" { + return nil, fmt.Errorf("no endpoint found in configuration") + } + + if token == "" { + return nil, fmt.Errorf("no token found in configuration") + } + + return &Config{ + Environment: currentEnv, + Endpoint: endpoint, + Token: token, + }, nil +} + +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), + GroupID: "available", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + common.PrintAvailableVerbs(cmd) + return + } + cmd.Help() + }, + } + + cmd.AddGroup(&cobra.Group{ + ID: "available", + Title: "Available Commands:", + }, &cobra.Group{ + ID: "other", + Title: "Other Commands:", + }) + + cmd.SetHelpFunc(common.CustomParentHelpFunc) + + apiResourcesCmd := common.FetchApiResourcesCmd(serviceName) + apiResourcesCmd.GroupID = "available" + cmd.AddCommand(apiResourcesCmd) + + err := common.AddVerbCommands(cmd, serviceName, "other") + if err != nil { + fmt.Fprintf(os.Stderr, "Error adding verb commands for %s: %v\n", serviceName, err) + } + + return cmd +}