From 80a125fa32e83b3600f2c87aa1aa87a8712ac513 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 12 Dec 2024 17:15:23 +0900 Subject: [PATCH 1/3] feat: initialize cfctl CLI project Signed-off-by: Youngjin Jo --- .gitignore | 1 - cmd/common/apiResources.go | 256 ----- cmd/common/fetchApiResources.go | 20 - cmd/common/fetchService.go | 977 ------------------ cmd/common/fetchVerb.go | 348 ------- cmd/common/helpers.go | 234 ----- cmd/common/shortNameResolver.go | 43 - cmd/other/apiResources.go | 538 ---------- cmd/other/login.go | 1682 ------------------------------- cmd/other/setting.go | 1279 ----------------------- cmd/other/shortNames.go | 305 ------ cmd/root.go | 588 ----------- go.mod | 60 -- go.sum | 235 ----- main.go | 11 - 15 files changed, 6577 deletions(-) delete mode 100644 cmd/common/apiResources.go delete mode 100644 cmd/common/fetchApiResources.go delete mode 100644 cmd/common/fetchService.go delete mode 100644 cmd/common/fetchVerb.go delete mode 100644 cmd/common/helpers.go delete mode 100644 cmd/common/shortNameResolver.go delete mode 100644 cmd/other/apiResources.go delete mode 100644 cmd/other/login.go delete mode 100644 cmd/other/setting.go delete mode 100644 cmd/other/shortNames.go delete mode 100644 cmd/root.go delete mode 100644 main.go diff --git a/.gitignore b/.gitignore index 1d51fec..b717f86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ .idea - dist/ diff --git a/cmd/common/apiResources.go b/cmd/common/apiResources.go deleted file mode 100644 index 6f7ebb9..0000000 --- a/cmd/common/apiResources.go +++ /dev/null @@ -1,256 +0,0 @@ -// common/apiResources.go - -package common - -import ( - "context" - "crypto/tls" - "fmt" - "log" - "os" - "path/filepath" - "sort" - "strings" - - "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" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - "gopkg.in/yaml.v3" -) - -func loadShortNames() (map[string]string, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("unable to find home directory: %v", err) - } - shortNamesFile := filepath.Join(home, ".cfctl", "short_names.yaml") - shortNamesMap := make(map[string]string) - if _, err := os.Stat(shortNamesFile); err == nil { - file, err := os.Open(shortNamesFile) - if err != nil { - return nil, fmt.Errorf("failed to open short_names.yaml file: %v", err) - } - defer file.Close() - - err = yaml.NewDecoder(file).Decode(&shortNamesMap) - if err != nil { - return nil, fmt.Errorf("failed to decode short_names.yaml: %v", err) - } - } - return shortNamesMap, nil -} - -func ListAPIResources(serviceName string) error { - config, err := loadConfig() - if err != nil { - return fmt.Errorf("failed to load config: %v", err) - } - - endpoint, err := getServiceEndpoint(config, serviceName) - if err != nil { - return fmt.Errorf("failed to get endpoint for service %s: %v", serviceName, err) - } - - shortNamesMap, err := loadShortNames() - if err != nil { - return fmt.Errorf("failed to load short names: %v", err) - } - - data, err := fetchServiceResources(serviceName, endpoint, shortNamesMap, config) - if err != nil { - return fmt.Errorf("failed to fetch resources for service %s: %v", serviceName, err) - } - - sort.Slice(data, func(i, j int) bool { - return data[i][0] < data[j][0] - }) - - renderAPITable(data) - - return nil -} - -func getServiceEndpoint(config *Config, serviceName string) (string, error) { - var envPrefix string - if strings.HasPrefix(config.Environment, "dev-") { - envPrefix = "dev" - } else if strings.HasPrefix(config.Environment, "stg-") { - envPrefix = "stg" - } else { - return "", fmt.Errorf("unsupported environment prefix") - } - endpoint := fmt.Sprintf("grpc+ssl://%s.api.%s.spaceone.dev:443", serviceName, envPrefix) - return endpoint, nil -} - -func fetchServiceResources(serviceName, endpoint string, shortNamesMap map[string]string, config *Config) ([][]string, error) { - parts := strings.Split(endpoint, "://") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) - } - scheme := parts[0] - hostPort := parts[1] - - var opts []grpc.DialOption - if scheme == "grpc+ssl" { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - return nil, fmt.Errorf("unsupported scheme: %s", scheme) - } - - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", endpoint, err) - } - defer conn.Close() - - ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Environments[config.Environment].Token) - - refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - services, err := refClient.ListServices() - if err != nil { - 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 - } - if !strings.Contains(s, fmt.Sprintf(".%s.", serviceName)) { - continue - } - - serviceDesc, err := refClient.ResolveService(s) - if err != nil { - log.Printf("Failed to resolve service %s: %v", s, err) - continue - } - - resourceName := s[strings.LastIndex(s, ".")+1:] - verbs := []string{} - for _, method := range serviceDesc.GetMethods() { - verbs = append(verbs, method.GetName()) - } - - // 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}) - } - } - - // Then add remaining verbs - remainingVerbs := []string{} - for _, verb := range verbs { - if !usedVerbs[verb] { - remainingVerbs = append(remainingVerbs, verb) - } - } - - if len(remainingVerbs) > 0 { - resourceRows = append([][]string{{serviceName, strings.Join(remainingVerbs, ", "), resourceName, ""}}, resourceRows...) - } - - resourceData[resourceName] = resourceRows - } - - // Sort resources alphabetically - var resources []string - for resource := range resourceData { - resources = append(resources, resource) - } - sort.Strings(resources) - - // Build final data array - for _, resource := range resources { - data = append(data, resourceData[resource]...) - } - - return data, nil -} - -func renderAPITable(data [][]string) { - // Create table header - table := pterm.TableData{ - {"Service", "Verb", "Resource", "Short Names"}, - } - - // Add data rows - table = append(table, data...) - - // Render the table - pterm.DefaultTable.WithHasHeader().WithData(table).Render() -} - -// wordWrap function remains the same -func wordWrap(text string, width int) string { - var wrappedText string - var line string - words := strings.Fields(text) // Split on spaces only - - for _, word := range words { - if len(line)+len(word)+1 > width { - if wrappedText != "" { - wrappedText += "\n" - } - wrappedText += line - line = word - } else { - if line != "" { - line += " " - } - line += word - } - } - if line != "" { - if wrappedText != "" { - wrappedText += "\n" - } - wrappedText += line - } - - return wrappedText -} diff --git a/cmd/common/fetchApiResources.go b/cmd/common/fetchApiResources.go deleted file mode 100644 index 2489742..0000000 --- a/cmd/common/fetchApiResources.go +++ /dev/null @@ -1,20 +0,0 @@ -// fetchApiResources.go - -package common - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// FetchApiResourcesCmd provides api-resources command for the given service -func FetchApiResourcesCmd(serviceName string) *cobra.Command { - return &cobra.Command{ - Use: "api_resources", - Short: fmt.Sprintf("Displays supported API resources for the %s service", serviceName), - RunE: func(cmd *cobra.Command, args []string) error { - return ListAPIResources(serviceName) - }, - } -} diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go deleted file mode 100644 index a7cec2e..0000000 --- a/cmd/common/fetchService.go +++ /dev/null @@ -1,977 +0,0 @@ -package common - -import ( - "bytes" - "context" - "crypto/tls" - "encoding/csv" - "encoding/json" - "fmt" - "io" - "log" - "os" - "path/filepath" - "sort" - "strings" - - "google.golang.org/protobuf/types/known/structpb" - - "github.com/eiannone/keyboard" - "github.com/spf13/viper" - - "github.com/atotto/clipboard" - "github.com/pterm/pterm" - - "google.golang.org/grpc/metadata" - - "github.com/jhump/protoreflect/dynamic" - "github.com/jhump/protoreflect/grpcreflect" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - - "gopkg.in/yaml.v3" -) - -// Config structure to parse environment files -type Config struct { - Environment string `yaml:"environment"` - Environments map[string]Environment `yaml:"environments"` -} - -type Environment struct { - Endpoint string `yaml:"endpoint"` - Proxy string `yaml:"proxy"` - Token string `yaml:"token"` -} - -// FetchService handles the execution of gRPC commands for all services -func FetchService(serviceName string, verb string, resourceName string, options *FetchOptions) (map[string]interface{}, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get user home directory: %v", err) - } - - // Read configuration file - mainViper := viper.New() - 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") - } - - // Check current environment - currentEnv := mainViper.GetString("environment") - if currentEnv == "" { - return nil, fmt.Errorf("no environment set. Please run 'cfctl login' first") - } - - // Load configuration first - config, err := loadConfig() - if err != nil { - return nil, fmt.Errorf("failed to load config: %v", err) - } - - // Check token - token := config.Environments[config.Environment].Token - if token == "" { - pterm.Error.Println("No token found for authentication.") - - // Get current endpoint - endpoint := config.Environments[config.Environment].Endpoint - - if strings.HasSuffix(config.Environment, "-app") { - // App environment message - headerBox := pterm.DefaultBox.WithTitle("App Guide"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan)) - - appTokenExplain := "Please create a Domain Admin App in SpaceONE Console.\n" + - "This requires Domain Admin privilege.\n\n" + - "Or Please create a Workspace App in SpaceONE Console.\n" + - "This requires Workspace Owner privilege." - - pterm.Info.Printf("Using endpoint: %s\n", endpoint) - headerBox.Println(appTokenExplain) - fmt.Println() - - steps := []string{ - "1. Go to SpaceONE Console", - "2. Navigate to either 'Admin > App Page' or specific 'Workspace > App page'", - "3. Click 'Create' to create your App", - "4. Copy value of either 'client_secret' from Client ID or 'token' from Spacectl (CLI)", - } - - yamlExample := pterm.DefaultBox.WithTitle("Config Example"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - Sprint(fmt.Sprintf("environment: %s\nenvironments:\n %s:\n endpoint: %s\n proxy: true\n token: %s", - currentEnv, - currentEnv, - endpoint, - pterm.FgLightCyan.Sprint("YOUR_COPIED_TOKEN"))) - - instructionBox := pterm.DefaultBox.WithTitle("Required Steps"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4) - - allSteps := append(steps, - fmt.Sprintf("5. Add the token under the proxy in your config file:\n%s", yamlExample), - "6. Run 'cfctl login' again") - - instructionBox.Println(strings.Join(allSteps, "\n\n")) - } else { - // User environment message - headerBox := pterm.DefaultBox.WithTitle("Authentication Required"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan)) - - authExplain := "Please login to SpaceONE Console first.\n" + - "This requires your SpaceONE credentials." - - headerBox.Println(authExplain) - fmt.Println() - - steps := []string{ - "1. Run 'cfctl login'", - "2. Enter your credentials when prompted", - "3. Select your workspace", - "4. Try your command again", - } - - instructionBox := pterm.DefaultBox.WithTitle("Required Steps"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4) - - instructionBox.Println(strings.Join(steps, "\n\n")) - } - - 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 - var conn *grpc.ClientConn - if strings.HasPrefix(config.Environment, "local-") { - // For local environment, use insecure connection - conn, err = grpc.Dial("localhost:50051", grpc.WithInsecure()) - if err != nil { - pterm.Error.Printf("Cannot connect to local gRPC server (localhost:50051)\n") - pterm.Info.Println("Please check if your gRPC server is running") - return nil, fmt.Errorf("failed to connect to local server: %v", err) - } - } else { - // Existing SSL connection logic for non-local environments - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - conn, err = grpc.Dial(hostPort, grpc.WithTransportCredentials(creds)) - if err != nil { - return nil, fmt.Errorf("connection failed: %v", 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 { - // 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 != "" { - return nil, fmt.Errorf("missing required parameter: %s", paramName) - } - } - 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 != "" { - 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 - } - } - - // Apply limit if specified - if options.Limit > 0 && verb == "list" { - if results, ok := respMap["results"].([]interface{}); ok { - if len(results) > options.Limit { - respMap["results"] = results[:options.Limit] - } - } - } - - // Filter columns if specified - if options.Columns != "" && verb == "list" { - if results, ok := respMap["results"].([]interface{}); ok { - columns := strings.Split(options.Columns, ",") - filteredResults := make([]interface{}, len(results)) - - for i, result := range results { - if resultMap, ok := result.(map[string]interface{}); ok { - filteredMap := make(map[string]interface{}) - for _, col := range columns { - if val, exists := resultMap[strings.TrimSpace(col)]; exists { - filteredMap[strings.TrimSpace(col)] = val - } - } - filteredResults[i] = filteredMap - } - } - respMap["results"] = filteredResults - } - } - - printData(respMap, options, serviceName, resourceName, refClient) - } - - 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 { - return nil, fmt.Errorf("failed to get home directory: %v", err) - } - - // Load main configuration file - mainV := viper.New() - 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) - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - return nil, fmt.Errorf("no environment set in config") - } - - // Get environment config from main config file - envConfig := &Environment{ - Endpoint: mainV.GetString(fmt.Sprintf("environments.%s.endpoint", currentEnv)), - Proxy: mainV.GetString(fmt.Sprintf("environments.%s.proxy", 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 { - return nil, fmt.Errorf("environment '%s' not found in config files", currentEnv) - } - - return &Config{ - Environment: currentEnv, - Environments: map[string]Environment{ - currentEnv: *envConfig, - }, - }, nil -} - -func fetchJSONResponse(config *Config, serviceName string, verb string, resourceName string, options *FetchOptions) ([]byte, error) { - var conn *grpc.ClientConn - var err error - - if strings.HasPrefix(config.Environment, "local-") { - conn, err = grpc.Dial("localhost:50051", grpc.WithInsecure(), - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(10*1024*1024), - grpc.MaxCallSendMsgSize(10*1024*1024), - )) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to local server: %v", err) - } - } else { - 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) - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - - conn, err = grpc.Dial(hostPort, - grpc.WithTransportCredentials(creds), - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(10*1024*1024), - grpc.MaxCallSendMsgSize(10*1024*1024), - )) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", hostPort, err) - } - } - - defer conn.Close() - - ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Environments[config.Environment].Token) - refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - fullServiceName, err := discoverService(refClient, serviceName, resourceName) - if err != nil { - return nil, fmt.Errorf("failed to discover service: %v", err) - } - - serviceDesc, err := refClient.ResolveService(fullServiceName) - if err != nil { - return nil, fmt.Errorf("failed to resolve service %s: %v", fullServiceName, err) - } - - methodDesc := serviceDesc.FindMethodByName(verb) - if methodDesc == nil { - return nil, fmt.Errorf("method not found: %s", verb) - } - - // Create request and response messages - reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - - // Parse and set input parameters - inputParams, err := parseParameters(options) - if err != nil { - return nil, err - } - - for key, value := range inputParams { - if err := reqMsg.TrySetFieldByName(key, value); err != nil { - return nil, fmt.Errorf("failed to set field '%s': %v", key, err) - } - } - - fullMethod := fmt.Sprintf("/%s/%s", fullServiceName, verb) - - // Handle client streaming - if !methodDesc.IsClientStreaming() && methodDesc.IsServerStreaming() { - streamDesc := &grpc.StreamDesc{ - StreamName: verb, - ServerStreams: true, - ClientStreams: false, - } - - stream, err := conn.NewStream(ctx, streamDesc, fullMethod) - if err != nil { - return nil, fmt.Errorf("failed to create stream: %v", err) - } - - if err := stream.SendMsg(reqMsg); err != nil { - return nil, fmt.Errorf("failed to send request message: %v", err) - } - - if err := stream.CloseSend(); err != nil { - return nil, fmt.Errorf("failed to close send: %v", err) - } - - var allResponses []string - for { - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - err := stream.RecvMsg(respMsg) - if err == io.EOF { - break - } - if err != nil { - return nil, fmt.Errorf("failed to receive response: %v", err) - } - - jsonBytes, err := respMsg.MarshalJSON() - if err != nil { - return nil, fmt.Errorf("failed to marshal response: %v", err) - } - - allResponses = append(allResponses, string(jsonBytes)) - } - - if len(allResponses) == 1 { - return []byte(allResponses[0]), nil - } - - combinedJSON := fmt.Sprintf("{\"results\": [%s]}", strings.Join(allResponses, ",")) - return []byte(combinedJSON), nil - } - - // Regular unary call - err = conn.Invoke(ctx, fullMethod, reqMsg, respMsg) - if err != nil { - return nil, fmt.Errorf("failed to invoke method %s: %v", fullMethod, err) - } - - return respMsg.MarshalJSON() -} - -func parseParameters(options *FetchOptions) (map[string]interface{}, error) { - parsed := make(map[string]interface{}) - - // Load from file parameter if provided - if options.FileParameter != "" { - data, err := os.ReadFile(options.FileParameter) - if err != nil { - return nil, fmt.Errorf("failed to read file parameter: %v", err) - } - - var yamlData map[string]interface{} - if err := yaml.Unmarshal(data, &yamlData); err != nil { - return nil, fmt.Errorf("failed to unmarshal YAML file: %v", err) - } - - for key, value := range yamlData { - switch v := value.(type) { - case map[string]interface{}: - structValue, err := structpb.NewStruct(v) - if err != nil { - return nil, fmt.Errorf("failed to convert map to struct: %v", err) - } - parsed[key] = structValue - case []interface{}: - listValue, err := structpb.NewList(v) - if err != nil { - return nil, fmt.Errorf("failed to convert array to list: %v", err) - } - parsed[key] = listValue - default: - parsed[key] = value - } - } - } - - // Load from JSON parameter if provided - if options.JSONParameter != "" { - if err := json.Unmarshal([]byte(options.JSONParameter), &parsed); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON parameter: %v", err) - } - } - - // Parse key=value parameters - for _, param := range options.Parameters { - parts := strings.SplitN(param, "=", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid parameter format. Use key=value") - } - key := parts[0] - value := parts[1] - - // Attempt to parse value as JSON - var jsonValue interface{} - if err := json.Unmarshal([]byte(value), &jsonValue); err == nil { - parsed[key] = jsonValue - } else { - parsed[key] = value - } - } - - return parsed, nil -} - -func discoverService(refClient *grpcreflect.Client, serviceName string, resourceName string) (string, error) { - services, err := refClient.ListServices() - if err != nil { - return "", fmt.Errorf("failed to list services: %v", err) - } - - for _, service := range services { - if strings.Contains(service, fmt.Sprintf("spaceone.api.%s", serviceName)) && - strings.HasSuffix(service, resourceName) { - return service, nil - } - - if strings.Contains(service, serviceName) && - strings.HasSuffix(service, resourceName) { - return service, nil - } - } - - return "", fmt.Errorf("service not found for %s.%s", serviceName, resourceName) -} - -func printData(data map[string]interface{}, options *FetchOptions, serviceName, resourceName string, refClient *grpcreflect.Client) { - var output string - - switch options.OutputFormat { - case "json": - dataBytes, err := json.MarshalIndent(data, "", " ") - if err != nil { - log.Fatalf("Failed to marshal response to JSON: %v", err) - } - output = string(dataBytes) - fmt.Println(output) - - case "yaml": - if results, ok := data["results"].([]interface{}); ok && len(results) > 0 { - var sb strings.Builder - for i, item := range results { - if i > 0 { - sb.WriteString("---\n") - } - sb.WriteString(printYAMLDoc(item)) - } - output = sb.String() - fmt.Print(output) - } else { - output = printYAMLDoc(data) - fmt.Print(output) - } - - case "table": - output = printTable(data, options, serviceName, resourceName, refClient) - - case "csv": - output = printCSV(data) - - default: - output = printYAMLDoc(data) - fmt.Print(output) - } - - // Copy to clipboard if requested - if options.CopyToClipboard && output != "" { - if err := clipboard.WriteAll(output); err != nil { - log.Fatalf("Failed to copy to clipboard: %v", err) - } - pterm.Success.Println("The output has been copied to your clipboard.") - } -} - -func printYAMLDoc(v interface{}) string { - var buf bytes.Buffer - encoder := yaml.NewEncoder(&buf) - encoder.SetIndent(2) - if err := encoder.Encode(v); err != nil { - log.Fatalf("Failed to marshal response to YAML: %v", err) - } - return buf.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 - searchTerm := "" - filteredResults := results - - // Initialize keyboard - if err := keyboard.Open(); err != nil { - fmt.Println("Failed to initialize keyboard:", err) - return "" - } - defer keyboard.Close() - - // 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[key] = true - } - } - } - - // Convert headers map to sorted slice - headerSlice := make([]string, 0, len(headers)) - for key := range headers { - headerSlice = append(headerSlice, key) - } - 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) - } else { - filteredResults = results - } - - totalItems := len(filteredResults) - totalPages := (totalItems + pageSize - 1) / pageSize - - tableData := pterm.TableData{headerSlice} - - // Calculate current page items - startIdx := currentPage * pageSize - endIdx := startIdx + pageSize - if endIdx > totalItems { - endIdx = totalItems - } - - // Clear screen - fmt.Print("\033[H\033[2J") - - if searchTerm != "" { - fmt.Printf("Search: %s (Found: %d items)\n", searchTerm, totalItems) - } - - // 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(headerSlice)) - for i, key := range headerSlice { - rowData[i] = formatTableValue(row[key]) - } - tableData = append(tableData, rowData) - } - } - - // Clear screen - fmt.Print("\033[H\033[2J") - - // Print table - pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() - - fmt.Printf("\nPage %d of %d (Total items: %d)\n", currentPage+1, totalPages, totalItems) - fmt.Println("Navigation: [h]previous page, [l]next page, [/]search, [c]lear search, [q]uit") - - // Get keyboard input - char, _, err := keyboard.GetKey() - if err != nil { - fmt.Println("Error reading keyboard input:", err) - return "" - } - - switch char { - case 'l', 'L': - if currentPage < totalPages-1 { - currentPage++ - } else { - currentPage = 0 - } - case 'h', 'H': - if currentPage > 0 { - currentPage-- - } else { - currentPage = totalPages - 1 - } - case 'q', 'Q': - return "" - case 'c', 'C': - searchTerm = "" - currentPage = 0 - case '/': - fmt.Print("\nEnter search term: ") - keyboard.Close() - var input string - fmt.Scanln(&input) - searchTerm = input - currentPage = 0 - keyboard.Open() - } - } - } else { - headers := make([]string, 0) - for key := range data { - headers = append(headers, key) - } - sort.Strings(headers) - - tableData := pterm.TableData{ - {"Field", "Value"}, - } - - for _, header := range headers { - value := formatTableValue(data[header]) - tableData = append(tableData, []string{header, value}) - } - - pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() - } - return "" -} - -func filterResults(results []interface{}, searchTerm string) []interface{} { - var filtered []interface{} - searchTerm = strings.ToLower(searchTerm) - - 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) { - filtered = append(filtered, result) - break - } - } - } - } - return filtered -} - -func formatTableValue(val interface{}) string { - switch v := val.(type) { - case nil: - return "" - case string: - // Add colors for status values - switch strings.ToUpper(v) { - case "SUCCESS": - return pterm.FgGreen.Sprint(v) - case "FAILURE": - return pterm.FgRed.Sprint(v) - case "PENDING": - return pterm.FgYellow.Sprint(v) - case "RUNNING": - return pterm.FgBlue.Sprint(v) - default: - return v - } - case float64, float32, int, int32, int64, uint, uint32, uint64: - return fmt.Sprintf("%v", v) - case bool: - return fmt.Sprintf("%v", v) - case map[string]interface{}, []interface{}: - jsonBytes, err := json.Marshal(v) - if err != nil { - return fmt.Sprintf("%v", v) - } - return string(jsonBytes) - default: - return fmt.Sprintf("%v", v) - } -} - -func printCSV(data map[string]interface{}) string { - // CSV writer 생성 - writer := csv.NewWriter(os.Stdout) - defer writer.Flush() - - if results, ok := data["results"].([]interface{}); ok { - if len(results) == 0 { - return "" - } - - headers := make([]string, 0) - if firstRow, ok := results[0].(map[string]interface{}); ok { - for key := range firstRow { - headers = append(headers, key) - } - sort.Strings(headers) - writer.Write(headers) - } - - for _, result := range results { - if row, ok := result.(map[string]interface{}); ok { - rowData := make([]string, len(headers)) - for i, header := range headers { - rowData[i] = formatTableValue(row[header]) - } - writer.Write(rowData) - } - } - } else { - headers := []string{"Field", "Value"} - writer.Write(headers) - - fields := make([]string, 0) - for field := range data { - fields = append(fields, field) - } - sort.Strings(fields) - - for _, field := range fields { - row := []string{field, formatTableValue(data[field])} - writer.Write(row) - } - } - - return "" -} - -func formatCSVValue(val interface{}) string { - switch v := val.(type) { - case nil: - return "" - case string: - return v - case float64, float32, int, int32, int64, uint, uint32, uint64: - return fmt.Sprintf("%v", v) - case bool: - return fmt.Sprintf("%v", v) - case map[string]interface{}, []interface{}: - jsonBytes, err := json.Marshal(v) - if err != nil { - return fmt.Sprintf("%v", v) - } - return string(jsonBytes) - default: - return fmt.Sprintf("%v", v) - } -} diff --git a/cmd/common/fetchVerb.go b/cmd/common/fetchVerb.go deleted file mode 100644 index a7a4118..0000000 --- a/cmd/common/fetchVerb.go +++ /dev/null @@ -1,348 +0,0 @@ -// common/fetchVerb.go - -package common - -import ( - "fmt" - "os" - "os/signal" - "sort" - "strings" - "time" - - "github.com/pterm/pterm" - - "github.com/spf13/cobra" -) - -// FetchOptions holds the flag values for a command -type FetchOptions struct { - Parameters []string - JSONParameter string - FileParameter string - APIVersion string - OutputFormat string - CopyToClipboard bool - SortBy string - MinimalColumns bool - Columns string - Limit int -} - -// AddVerbCommands adds subcommands for each verb to the parent command -func AddVerbCommands(parentCmd *cobra.Command, serviceName string, groupID string) error { - // Build the verb-resource map - verbResourceMap, err := BuildVerbResourceMap(serviceName) - if err != nil { - return nil // Return nil to prevent Cobra from showing additional error messages - } - - // Get a sorted list of verbs - verbs := make([]string, 0, len(verbResourceMap)) - for verb := range verbResourceMap { - verbs = append(verbs, verb) - } - sort.Strings(verbs) - - for _, verb := range verbs { - currentVerb := verb - resources := verbResourceMap[currentVerb] - - // Prepare Short and Long descriptions - shortDesc := fmt.Sprintf("Supported %d resources", len(resources)) - - verbCmd := &cobra.Command{ - Use: currentVerb + " ", - Short: shortDesc, - Long: fmt.Sprintf(`Supported %d resources for %s command. - -%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))), - Args: cobra.ArbitraryArgs, // Allow any number of arguments - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - // Display the help message - cmd.Help() - return nil // Do not return an error to prevent additional error messages - } - resource := args[0] - - // Retrieve flag values - parameters, err := cmd.Flags().GetStringArray("parameter") - if err != nil { - return err - } - jsonParameter, err := cmd.Flags().GetString("json-parameter") - if err != nil { - return err - } - fileParameter, err := cmd.Flags().GetString("file-parameter") - if err != nil { - return err - } - apiVersion, err := cmd.Flags().GetString("api-version") - if err != nil { - return err - } - outputFormat, err := cmd.Flags().GetString("output") - if err != nil { - return err - } - copyToClipboard, err := cmd.Flags().GetBool("copy") - if err != nil { - return err - } - - sortBy := "" - columns := "" - limit := 0 - if currentVerb == "list" { - sortBy, _ = cmd.Flags().GetString("sort") - columns, _ = cmd.Flags().GetString("columns") - limit, _ = cmd.Flags().GetInt("limit") - } - - options := &FetchOptions{ - Parameters: parameters, - JSONParameter: jsonParameter, - FileParameter: fileParameter, - APIVersion: apiVersion, - OutputFormat: outputFormat, - CopyToClipboard: copyToClipboard, - SortBy: sortBy, - MinimalColumns: currentVerb == "list" && cmd.Flag("minimal") != nil && cmd.Flag("minimal").Changed, - Columns: columns, - Limit: limit, - } - - if currentVerb == "list" && !cmd.Flags().Changed("output") { - options.OutputFormat = "table" - } - - watch, _ := cmd.Flags().GetBool("watch") - if watch && currentVerb == "list" { - return watchResource(serviceName, currentVerb, resource, options) - } - - _, err = FetchService(serviceName, currentVerb, resource, options) - if err != nil { - // Use pterm to display the error message - pterm.Error.Println(err.Error()) - return nil // Return nil to prevent Cobra from displaying its own error message - } - return nil - }, - GroupID: groupID, - Annotations: map[string]string{ - "resources": strings.Join(resources, ", "), - }, - } - - if currentVerb == "list" { - verbCmd.Flags().BoolP("watch", "w", false, "Watch for changes") - verbCmd.Flags().StringP("sort", "s", "", "Sort by field (e.g. 'name', 'created_at')") - verbCmd.Flags().BoolP("minimal", "m", false, "Show minimal columns") - verbCmd.Flags().StringP("columns", "c", "", "Specific columns (-c id,name)") - verbCmd.Flags().IntP("limit", "l", 0, "Number of rows") - } - - // Define flags for verbCmd - verbCmd.Flags().StringArrayP("parameter", "p", []string{}, "Input Parameter (-p = -p ...)") - verbCmd.Flags().StringP("json-parameter", "j", "", "JSON type parameter") - verbCmd.Flags().StringP("file-parameter", "f", "", "YAML file parameter") - verbCmd.Flags().StringP("api-version", "v", "v1", "API Version") - verbCmd.Flags().StringP("output", "o", "yaml", "Output format (yaml, json, table, csv)") - verbCmd.Flags().BoolP("copy", "y", false, "Copy the output to the clipboard (copies any output format)") - - // Set custom help function - verbCmd.SetHelpFunc(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) - } - - return nil -} - -// watchResource monitors a resource for changes and prints updates -func watchResource(serviceName, verb, resource string, options *FetchOptions) error { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt) - - seenItems := make(map[string]bool) - - initialData, err := FetchService(serviceName, verb, resource, &FetchOptions{ - Parameters: options.Parameters, - JSONParameter: options.JSONParameter, - FileParameter: options.FileParameter, - APIVersion: options.APIVersion, - OutputFormat: "", - CopyToClipboard: false, - }) - if err != nil { - return err - } - - if results, ok := initialData["results"].([]interface{}); ok { - var recentItems []map[string]interface{} - - for _, item := range results { - if m, ok := item.(map[string]interface{}); ok { - identifier := generateIdentifier(m) - seenItems[identifier] = true - - recentItems = append(recentItems, m) - if len(recentItems) > 20 { - recentItems = recentItems[1:] - } - } - } - - if len(recentItems) > 0 { - fmt.Printf("Recent items:\n") - printNewItems(recentItems) - } - } - - fmt.Printf("\nWatching for changes... (Ctrl+C to quit)\n\n") - - for { - select { - case <-ticker.C: - newData, err := FetchService(serviceName, verb, resource, &FetchOptions{ - Parameters: options.Parameters, - JSONParameter: options.JSONParameter, - FileParameter: options.FileParameter, - APIVersion: options.APIVersion, - OutputFormat: "", - CopyToClipboard: false, - }) - if err != nil { - continue - } - - var newItems []map[string]interface{} - if results, ok := newData["results"].([]interface{}); ok { - for _, item := range results { - if m, ok := item.(map[string]interface{}); ok { - identifier := generateIdentifier(m) - if !seenItems[identifier] { - newItems = append(newItems, m) - seenItems[identifier] = true - } - } - } - } - - if len(newItems) > 0 { - fmt.Printf("Found %d new items at %s:\n", - len(newItems), - time.Now().Format("2006-01-02 15:04:05")) - - printNewItems(newItems) - fmt.Println() - } - - case <-sigChan: - fmt.Println("\nStopping watch...") - return nil - } - } -} - -func generateIdentifier(item map[string]interface{}) string { - if id, ok := item["job_task_id"]; ok { - return fmt.Sprintf("%v", id) - } - - var keys []string - for k := range item { - keys = append(keys, k) - } - sort.Strings(keys) - - var parts []string - for _, k := range keys { - parts = append(parts, fmt.Sprintf("%v=%v", k, item[k])) - } - return strings.Join(parts, ",") -} - -func printNewItems(items []map[string]interface{}) { - if len(items) == 0 { - return - } - - tableData := pterm.TableData{} - - headers := make([]string, 0) - for key := range items[0] { - headers = append(headers, key) - } - sort.Strings(headers) - tableData = append(tableData, headers) - - for _, item := range items { - row := make([]string, len(headers)) - for i, header := range headers { - if val, ok := item[header]; ok { - row[i] = formatTableValue(val) - } - } - tableData = append(tableData, row) - } - - pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() -} diff --git a/cmd/common/helpers.go b/cmd/common/helpers.go deleted file mode 100644 index 33f5d3e..0000000 --- a/cmd/common/helpers.go +++ /dev/null @@ -1,234 +0,0 @@ -// common/methods.go - -package common - -import ( - "context" - "crypto/tls" - "fmt" - "sort" - "strings" - - "github.com/spf13/cobra" - - "github.com/pterm/pterm" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - - "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() - if err != nil { - return nil, fmt.Errorf("failed to load config: %v", err) - } - - var conn *grpc.ClientConn - var refClient *grpcreflect.Client - - if strings.HasPrefix(config.Environment, "local-") { - conn, err = grpc.Dial("localhost:50051", grpc.WithInsecure()) - if err != nil { - return nil, fmt.Errorf("local connection failed: %v", err) - } - } else { - var envPrefix string - if strings.HasPrefix(config.Environment, "dev-") { - envPrefix = "dev" - } else if strings.HasPrefix(config.Environment, "stg-") { - envPrefix = "stg" - } else { - return nil, fmt.Errorf("unsupported environment prefix") - } - - endpointServiceName := convertServiceNameToEndpoint(serviceName) - hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", endpointServiceName, envPrefix) - - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - conn, err = grpc.Dial(hostPort, grpc.WithTransportCredentials(creds)) - if err != nil { - return nil, fmt.Errorf("connection failed: %v", err) - } - } - defer conn.Close() - - ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Environments[config.Environment].Token) - refClient = grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - services, err := refClient.ListServices() - if err != nil { - return nil, fmt.Errorf("failed to list services: %v", err) - } - - verbResourceMap := make(map[string]map[string]struct{}) - - for _, s := range services { - if strings.HasPrefix(s, "grpc.reflection.") { - continue - } - - if !strings.Contains(s, fmt.Sprintf(".%s.", serviceName)) { - continue - } - - serviceDesc, err := refClient.ResolveService(s) - if err != nil { - continue - } - - parts := strings.Split(s, ".") - resourceName := parts[len(parts)-1] - - for _, method := range serviceDesc.GetMethods() { - verb := method.GetName() - if verbResourceMap[verb] == nil { - verbResourceMap[verb] = make(map[string]struct{}) - } - verbResourceMap[verb][resourceName] = struct{}{} - } - } - - result := make(map[string][]string) - for verb, resourcesSet := range verbResourceMap { - resources := make([]string, 0, len(resourcesSet)) - for resource := range resourcesSet { - resources = append(resources, resource) - } - sort.Strings(resources) - result[verb] = resources - } - - return result, nil -} - -// CustomParentHelpFunc customizes the help output for the parent command -func CustomParentHelpFunc(cmd *cobra.Command, args []string) { - cmd.Printf("Usage:\n") - cmd.Printf(" %s\n", cmd.UseLine()) - cmd.Printf(" %s\n\n", getAlternativeUsage(cmd)) - - if cmd.Short != "" { - cmd.Println(cmd.Short) - cmd.Println() - } - - printSortedBulletList(cmd, "Verbs") - - cmd.Println("Flags:") - cmd.Print(cmd.Flags().FlagUsages()) - cmd.Println() - - if len(cmd.Commands()) > 0 { - cmd.Printf("Use \"%s --help\" for more information about a verb.\n", cmd.CommandPath()) - } -} - -// PrintAvailableVerbs prints the available verbs along with both usage patterns -func PrintAvailableVerbs(cmd *cobra.Command) { - cmd.Printf("Usage:\n") - cmd.Printf(" %s\n", cmd.UseLine()) - cmd.Printf(" %s\n\n", getAlternativeUsage(cmd)) - - if cmd.Short != "" { - cmd.Println(cmd.Short) - cmd.Println() - } - - verbs := []string{} - for _, subCmd := range cmd.Commands() { - if !subCmd.Hidden { - verbs = append(verbs, subCmd.Name()) - } - } - sort.Strings(verbs) - pterm.Printf("Supported %d verbs\n", len(verbs)) - - printSortedBulletList(cmd, "Verbs") - - cmd.Println("Flags:") - cmd.Print(cmd.Flags().FlagUsages()) - cmd.Println() - - if len(cmd.Commands()) > 0 { - cmd.Printf("Use \"%s --help\" for more information about a verb.\n", cmd.CommandPath()) - } -} - -// CustomVerbHelpFunc customizes the help output for verb commands -func CustomVerbHelpFunc(cmd *cobra.Command, args []string) { - cmd.Printf("Usage:\n %s\n\n", cmd.UseLine()) - - if cmd.Short != "" { - cmd.Println(cmd.Short) - cmd.Println() - } - - if resourcesStr, ok := cmd.Annotations["resources"]; ok && resourcesStr != "" { - resources := strings.Split(resourcesStr, ", ") - sort.Strings(resources) - pterm.DefaultSection.Println("Resources") - - listItems := []pterm.BulletListItem{} - for _, resource := range resources { - listItems = append(listItems, pterm.BulletListItem{Level: 2, Text: resource}) - } - pterm.DefaultBulletList.WithItems(listItems).Render() - cmd.Println() - } - - cmd.Println("Flags:") - cmd.Print(cmd.Flags().FlagUsages()) - cmd.Println() - - if len(cmd.Commands()) > 0 { - cmd.Printf("Use \"%s --help\" for more information about a resource.\n", cmd.CommandPath()) - } -} - -// getAlternativeUsage constructs the alternative usage string -func getAlternativeUsage(cmd *cobra.Command) string { - // Extract the base command without flags - baseCommand := cmd.CommandPath() - return fmt.Sprintf("%s [flags]", baseCommand) -} - -// printSortedBulletList prints a sorted bullet list under a given section title. -func printSortedBulletList(cmd *cobra.Command, sectionTitle string) { - if len(cmd.Commands()) == 0 { - return - } - - pterm.DefaultSection.Println(sectionTitle) - - items := []string{} - for _, subCmd := range cmd.Commands() { - if !subCmd.Hidden { - items = append(items, subCmd.Name()) - } - } - - sort.Strings(items) - - listItems := make([]pterm.BulletListItem, len(items)) - for i, item := range items { - listItems[i] = pterm.BulletListItem{Level: 2, Text: item} - } - - pterm.DefaultBulletList.WithItems(listItems).Render() - cmd.Println() -} diff --git a/cmd/common/shortNameResolver.go b/cmd/common/shortNameResolver.go deleted file mode 100644 index 0cc777d..0000000 --- a/cmd/common/shortNameResolver.go +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index 6794b8c..0000000 --- a/cmd/other/apiResources.go +++ /dev/null @@ -1,538 +0,0 @@ -// apiResources.go - -package other - -import ( - "context" - "crypto/tls" - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "sort" - "strings" - "sync" - - "github.com/BurntSushi/toml" - "github.com/jhump/protoreflect/dynamic" - "github.com/jhump/protoreflect/grpcreflect" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "gopkg.in/yaml.v2" - - "github.com/pterm/pterm" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/descriptorpb" -) - -var endpoints string - -func loadEndpointsFromCache(currentEnv string) (map[string]string, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("unable to find home directory: %v", err) - } - - // Read from environment-specific cache file - cacheFile := filepath.Join(home, ".cfctl", "cache", currentEnv, "endpoints.toml") - data, err := os.ReadFile(cacheFile) - if err != nil { - return nil, err - } - - var endpoints map[string]string - if err := toml.Unmarshal(data, &endpoints); err != nil { - return nil, err - } - - return endpoints, nil -} - -var ApiResourcesCmd = &cobra.Command{ - Use: "api_resources", - Short: "Displays supported API resources", - Run: func(cmd *cobra.Command, args []string) { - home, err := os.UserHomeDir() - if err != nil { - log.Fatalf("Unable to find home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.toml") - - // Read main setting file - mainV := viper.New() - mainV.SetConfigFile(settingPath) - mainV.SetConfigType("toml") - mainConfigErr := mainV.ReadInConfig() - - var currentEnv string - var envConfig map[string]interface{} - - if mainConfigErr == nil { - currentEnv = mainV.GetString("environment") - if currentEnv != "" { - envConfig = mainV.GetStringMap(fmt.Sprintf("environments.%s", currentEnv)) - } - } - - if envConfig == nil { - log.Fatalf("No configuration found for environment '%s'", currentEnv) - } - - // Try to load endpoints from cache first - endpointsMap, err := loadEndpointsFromCache(currentEnv) - if err != nil { - // If cache loading fails, fall back to fetching from identity service - endpoint, ok := envConfig["endpoint"].(string) - if !ok || endpoint == "" { - log.Fatalf("No endpoint found for environment '%s'", currentEnv) - } - - endpointsMap, err = FetchEndpointsMap(endpoint) - if err != nil { - log.Fatalf("Failed to fetch endpointsMap from '%s': %v", endpoint, err) - } - } - - // Load short names configuration - shortNamesFile := filepath.Join(home, ".cfctl", "short_names.yaml") - shortNamesMap := make(map[string]string) - if _, err := os.Stat(shortNamesFile); err == nil { - file, err := os.Open(shortNamesFile) - if err != nil { - log.Fatalf("Failed to open short_names.yaml file: %v", err) - } - defer file.Close() - - err = yaml.NewDecoder(file).Decode(&shortNamesMap) - if err != nil { - log.Fatalf("Failed to decode short_names.yaml: %v", err) - } - } - - // Process endpoints provided via flag - if endpoints != "" { - selectedEndpoints := strings.Split(endpoints, ",") - for i := range selectedEndpoints { - selectedEndpoints[i] = strings.TrimSpace(selectedEndpoints[i]) - } - var allData [][]string - - for _, endpointName := range selectedEndpoints { - serviceEndpoint, ok := endpointsMap[endpointName] - if !ok { - log.Printf("No endpoint found for %s", endpointName) - continue - } - - result, err := fetchServiceResources(endpointName, serviceEndpoint, shortNamesMap) - if err != nil { - log.Printf("Error processing service %s: %v", endpointName, err) - continue - } - - allData = append(allData, result...) - } - - sort.Slice(allData, func(i, j int) bool { - return allData[i][0] < allData[j][0] - }) - - renderTable(allData) - return - } - - // If no specific endpoints are provided, list all services - var wg sync.WaitGroup - dataChan := make(chan [][]string, len(endpointsMap)) - errorChan := make(chan error, len(endpointsMap)) - - for service, endpoint := range endpointsMap { - wg.Add(1) - go func(service, endpoint string) { - defer wg.Done() - result, err := fetchServiceResources(service, endpoint, shortNamesMap) - if err != nil { - errorChan <- fmt.Errorf("Error processing service %s: %v", service, err) - return - } - dataChan <- result - }(service, endpoint) - } - - wg.Wait() - close(dataChan) - close(errorChan) - - if len(errorChan) > 0 { - for err := range errorChan { - log.Println(err) - } - } - - var allData [][]string - for data := range dataChan { - allData = append(allData, data...) - } - - sort.Slice(allData, func(i, j int) bool { - return allData[i][0] < allData[j][0] - }) - - renderTable(allData) - }, -} - -func FetchEndpointsMap(endpoint string) (map[string]string, error) { - // Parse the endpoint - parts := strings.Split(endpoint, "://") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) - } - - scheme := parts[0] - hostPort := parts[1] - - // Configure gRPC connection based on scheme - var opts []grpc.DialOption - if scheme == "grpc+ssl" { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, // Enable server certificate verification - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - // Establish the connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", endpoint, err) - } - defer conn.Close() - - // Use Reflection to discover services - refClient := grpcreflect.NewClient(context.Background(), grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // Resolve the service and method - serviceName := "spaceone.api.identity.v2.Endpoint" - methodName := "list" - - serviceDesc, err := refClient.ResolveService(serviceName) - if err != nil { - return nil, fmt.Errorf("failed to resolve service %s: %v", serviceName, err) - } - - methodDesc := serviceDesc.FindMethodByName(methodName) - if methodDesc == nil { - return nil, fmt.Errorf("method not found: %s", methodName) - } - - // Dynamically create the request message - reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) - - // Set "query" field (optional) - queryField := methodDesc.GetInputType().FindFieldByName("query") - if queryField != nil && queryField.GetMessageType() != nil { - queryMsg := dynamic.NewMessage(queryField.GetMessageType()) - // Set additional query fields here if needed - reqMsg.SetFieldByName("query", queryMsg) - } - - // Prepare an empty response message - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - - // Full method name - fullMethod := fmt.Sprintf("/%s/%s", serviceName, methodName) - - // Invoke the gRPC method - err = conn.Invoke(context.Background(), fullMethod, reqMsg, respMsg) - if err != nil { - return nil, fmt.Errorf("failed to invoke method %s: %v", fullMethod, err) - } - - // Process the response to extract `service` and `endpoint` - endpointsMap := make(map[string]string) - resultsField := respMsg.FindFieldDescriptorByName("results") - if resultsField == nil { - return nil, fmt.Errorf("'results' field not found in response") - } - - results := respMsg.GetField(resultsField).([]interface{}) - for _, result := range results { - resultMsg := result.(*dynamic.Message) - serviceName := resultMsg.GetFieldByName("service").(string) - serviceEndpoint := resultMsg.GetFieldByName("endpoint").(string) - endpointsMap[serviceName] = serviceEndpoint - } - - return endpointsMap, nil -} - -func callGRPCMethod(hostPort, service, method string, requestPayload interface{}) ([]byte, error) { - // Configure gRPC connection - var opts []grpc.DialOption - if strings.HasPrefix(hostPort, "identity.api") { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, // Enable server certificate verification - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - // Connect to the gRPC server - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("failed to connect to gRPC server: %v", err) - } - defer conn.Close() - - // Full method name (e.g., "/spaceone.api.identity.v2.Endpoint/List") - fullMethod := fmt.Sprintf("/%s/%s", service, method) - - // Serialize the request payload to JSON - reqData, err := json.Marshal(requestPayload) - if err != nil { - return nil, fmt.Errorf("failed to marshal request payload: %v", err) - } - - // Prepare a generic response container - var respData json.RawMessage - - // Invoke the gRPC method - err = conn.Invoke(context.Background(), fullMethod, reqData, &respData) - if err != nil { - return nil, fmt.Errorf("failed to invoke method: %v", err) - } - - return respData, nil -} - -func fetchServiceResources(service, endpoint string, shortNamesMap map[string]string) ([][]string, error) { - // Configure gRPC connection based on TLS usage - parts := strings.Split(endpoint, "://") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) - } - - scheme := parts[0] - hostPort := strings.SplitN(parts[1], "/", 2)[0] - - var opts []grpc.DialOption - if scheme == "grpc+ssl" { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, // Enable server certificate verification - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", endpoint, err) - } - defer conn.Close() - - client := grpc_reflection_v1alpha.NewServerReflectionClient(conn) - stream, err := client.ServerReflectionInfo(context.Background()) - if err != nil { - return nil, fmt.Errorf("failed to create reflection client: %v", err) - } - - // List all services - req := &grpc_reflection_v1alpha.ServerReflectionRequest{ - MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_ListServices{ListServices: ""}, - } - - if err := stream.Send(req); err != nil { - return nil, fmt.Errorf("failed to send reflection request: %v", err) - } - - resp, err := stream.Recv() - if err != nil { - return nil, fmt.Errorf("failed to receive reflection response: %v", err) - } - - services := resp.GetListServicesResponse().Service - 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 all matching short names for this resource - verbsWithShortNames := make(map[string]string) - remainingVerbs := make([]string, 0) - - // Get service-specific short names - serviceShortNames := registeredShortNames[service] - if serviceMap, ok := serviceShortNames.(map[string]interface{}); ok { - for _, verb := range verbs { - hasShortName := false - for sn, cmd := range serviceMap { - if strings.Contains(cmd.(string), fmt.Sprintf("%s %s", verb, resourceName)) { - verbsWithShortNames[verb] = sn - hasShortName = true - break - } - } - if !hasShortName { - remainingVerbs = append(remainingVerbs, verb) - } - } - } else { - remainingVerbs = verbs - } - - // Add row for verbs without short names - if len(remainingVerbs) > 0 { - data = append(data, []string{service, resourceName, "", strings.Join(remainingVerbs, ", ")}) - } - - // 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 -} - -func getServiceMethods(client grpc_reflection_v1alpha.ServerReflectionClient, serviceName string) []string { - stream, err := client.ServerReflectionInfo(context.Background()) - if err != nil { - log.Fatalf("Failed to create reflection client: %v", err) - } - - req := &grpc_reflection_v1alpha.ServerReflectionRequest{ - MessageRequest: &grpc_reflection_v1alpha.ServerReflectionRequest_FileContainingSymbol{FileContainingSymbol: serviceName}, - } - - if err := stream.Send(req); err != nil { - log.Fatalf("Failed to send reflection request: %v", err) - } - - resp, err := stream.Recv() - if err != nil { - log.Fatalf("Failed to receive reflection response: %v", err) - } - - fileDescriptor := resp.GetFileDescriptorResponse() - if fileDescriptor == nil { - return []string{} - } - - methods := []string{} - for _, fdBytes := range fileDescriptor.FileDescriptorProto { - fd := &descriptorpb.FileDescriptorProto{} - if err := proto.Unmarshal(fdBytes, fd); err != nil { - log.Fatalf("Failed to unmarshal file descriptor: %v", err) - } - for _, service := range fd.GetService() { - if service.GetName() == serviceName[strings.LastIndex(serviceName, ".")+1:] { - for _, method := range service.GetMethod() { - methods = append(methods, method.GetName()) - } - } - } - } - - return methods -} - -func renderTable(data [][]string) { - // Calculate the dynamic width for the "Verb" column - terminalWidth := pterm.GetTerminalWidth() - usedWidth := 30 + 20 + 15 // Estimated widths for Service, Resource, and Short Names - verbColumnWidth := terminalWidth - usedWidth - if verbColumnWidth < 20 { - verbColumnWidth = 20 // Minimum width for Verb column - } - - // Use two distinct colors for alternating services - alternateColors := []pterm.Color{ - pterm.FgLightBlue, pterm.FgLightYellow, pterm.FgLightMagenta, pterm.FgGreen, pterm.FgLightRed, pterm.FgBlue, pterm.FgLightGreen, - } - - currentColorIndex := 0 - previousService := "" - - table := pterm.TableData{{"Service", "Verb", "Resource", "Short Names"}} // Column order updated - - for _, row := range data { - service := row[0] - - // Switch color if the service name changes - if service != previousService { - currentColorIndex = (currentColorIndex + 1) % len(alternateColors) - previousService = service - } - - // Apply the current color - color := alternateColors[currentColorIndex] - coloredStyle := pterm.NewStyle(color) - - // Color the entire row (Service, Resource, Short Names, Verb) - serviceColored := coloredStyle.Sprint(service) - resourceColored := coloredStyle.Sprint(row[1]) - shortNamesColored := coloredStyle.Sprint(row[2]) - - verbs := splitIntoLinesWithComma(row[3], verbColumnWidth) - for i, line := range verbs { - if i == 0 { - table = append(table, []string{serviceColored, coloredStyle.Sprint(line), resourceColored, shortNamesColored}) - } else { - table = append(table, []string{"", coloredStyle.Sprint(line), "", ""}) - } - } - } - - // Render the table using pterm - pterm.DefaultTable.WithHasHeader().WithData(table).Render() -} - -func splitIntoLinesWithComma(text string, maxWidth int) []string { - words := strings.Split(text, ", ") - var lines []string - var currentLine string - - for _, word := range words { - if len(currentLine)+len(word)+2 > maxWidth { // +2 accounts for the ", " separator - lines = append(lines, currentLine+",") - currentLine = word - } else { - if currentLine != "" { - currentLine += ", " - } - currentLine += word - } - } - if currentLine != "" { - lines = append(lines, currentLine) - } - - return lines -} - -func init() { - ApiResourcesCmd.Flags().StringVarP(&endpoints, "service", "s", "", "Specify the services to connect to, separated by commas (e.g., 'identity', 'identity,inventory')") -} diff --git a/cmd/other/login.go b/cmd/other/login.go deleted file mode 100644 index 0d9e2ac..0000000 --- a/cmd/other/login.go +++ /dev/null @@ -1,1682 +0,0 @@ -package other - -import ( - "context" - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "crypto/tls" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/AlecAivazis/survey/v2" - "github.com/eiannone/keyboard" - - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/structpb" - - "github.com/jhump/protoreflect/dynamic" - - "github.com/jhump/protoreflect/grpcreflect" - "github.com/pterm/pterm" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/zalando/go-keyring" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" -) - -//const encryptionKey = "spaceone-cfctl-encryption-key-32byte" - -const ( - keyringService = "cfctl-credentials" - keyringUser = "encryption-key" -) - -var providedUrl string - -// LoginCmd represents the login command -var LoginCmd = &cobra.Command{ - Use: "login", - Short: "Login to SpaceONE", - Long: `A command that allows you to login to SpaceONE. -It will prompt you for your User ID, Password, and fetch the Domain ID automatically, then fetch the token.`, - Run: executeLogin, -} - -// tokenAuth implements grpc.PerRPCCredentials for token-based authentication. -type tokenAuth struct { - token string -} - -func (t *tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { - return map[string]string{ - "token": t.token, // Use "token" key instead of "Authorization: Bearer" - }, nil -} - -func (t *tokenAuth) RequireTransportSecurity() bool { - return true -} - -func executeLogin(cmd *cobra.Command, args []string) { - homeDir, err := os.UserHomeDir() - if err != nil { - pterm.Error.Println("Failed to get user home directory:", err) - return - } - - configPath := filepath.Join(homeDir, ".cfctl", "setting.toml") - - // 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 setting init' to set up your configuration.") - pterm.Info.Println("After initialization, run 'cfctl login' to authenticate.") - return - } - - viper.SetConfigFile(configPath) - viper.SetConfigType("toml") - if err := viper.ReadInConfig(); err != nil { - pterm.Error.Printf("Failed to read config file: %v\n", err) - return - } - - currentEnv := viper.GetString("environment") - if currentEnv == "" { - pterm.Error.Println("No environment selected") - return - } - - // Check if it's an app environment - if strings.HasSuffix(currentEnv, "-app") { - pterm.DefaultBox.WithTitle("App Environment Detected"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - WithBoxStyle(pterm.NewStyle(pterm.FgYellow)). - Println("Login command is not available for app environments.\nPlease use the app token directly in your configuration file.") - return - } - - // Execute normal user login - executeUserLogin(currentEnv) -} - -type TokenInfo struct { - Token string `yaml:"token"` -} - -// promptToken prompts for token input -func promptToken() (string, error) { - prompt := &survey.Password{ - Message: "Enter your token:", - } - - var token string - err := survey.AskOne(prompt, &token, survey.WithValidator(survey.Required)) - if err != nil { - return "", err - } - - return token, nil -} - -// saveAppToken saves the token -func saveAppToken(currentEnv, token string) error { - homeDir, _ := os.UserHomeDir() - configPath := filepath.Join(homeDir, ".cfctl", "config.yaml") - - viper.SetConfigFile(configPath) - if err := viper.ReadInConfig(); err != nil && !os.IsNotExist(err) { - return err - } - - envPath := fmt.Sprintf("environments.%s", currentEnv) - envSettings := viper.GetStringMap(envPath) - if envSettings == nil { - envSettings = make(map[string]interface{}) - } - - // Initialize tokens array if it doesn't exist - var tokens []TokenInfo - if existingTokens, ok := envSettings["tokens"]; ok { - if tokenList, ok := existingTokens.([]interface{}); ok { - for _, t := range tokenList { - if tokenMap, ok := t.(map[string]interface{}); ok { - tokenInfo := TokenInfo{ - Token: tokenMap["token"].(string), - } - tokens = append(tokens, tokenInfo) - } - } - } - } - - // Add new token if it doesn't exist - tokenExists := false - for _, t := range tokens { - if t.Token == token { - tokenExists = true - break - } - } - - if !tokenExists { - newToken := TokenInfo{ - Token: token, - } - tokens = append(tokens, newToken) - } - - // Update environment settings - envSettings["tokens"] = tokens - - // Keep the existing endpoint and proxy settings - if endpoint, ok := envSettings["endpoint"]; ok { - envSettings["endpoint"] = endpoint - } - if proxy, ok := envSettings["proxy"]; ok { - envSettings["proxy"] = proxy - } - - viper.Set(envPath, envSettings) - return viper.WriteConfig() -} - -// promptTokenSelection shows available tokens and lets user select one -func promptTokenSelection(tokens []TokenInfo) (string, error) { - if len(tokens) == 0 { - return "", fmt.Errorf("no tokens available") - } - - if err := keyboard.Open(); err != nil { - return "", err - } - defer keyboard.Close() - - selectedIndex := 0 - for { - fmt.Print("\033[H\033[2J") // Clear screen - - pterm.DefaultHeader.WithFullWidth(). - WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)). - WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)). - Println("Select a token:") - - // Display available tokens - for i, token := range tokens { - maskedToken := maskToken(token.Token) - if i == selectedIndex { - pterm.Printf("→ %d: %s\n", i+1, maskedToken) - } else { - pterm.Printf(" %d: %s\n", i+1, maskedToken) - } - } - - pterm.DefaultBasicText.WithStyle(pterm.NewStyle(pterm.FgGray)). - Println("\nNavigation: [j]down [k]up [Enter]select [q]quit") - - char, key, err := keyboard.GetKey() - if err != nil { - return "", err - } - - switch key { - case keyboard.KeyEnter: - return tokens[selectedIndex].Token, nil - } - - switch char { - case 'j': - if selectedIndex < len(tokens)-1 { - selectedIndex++ - } - case 'k': - if selectedIndex > 0 { - selectedIndex-- - } - case 'q', 'Q': - return "", fmt.Errorf("selection cancelled") - } - } -} - -// maskToken returns a masked version of the token for display -func maskToken(token string) string { - if len(token) <= 10 { - return strings.Repeat("*", len(token)) - } - return token[:5] + "..." + token[len(token)-5:] -} - -// executeAppLogin handles login for app environments -func executeAppLogin(currentEnv string) error { - homeDir, _ := os.UserHomeDir() - configPath := filepath.Join(homeDir, ".cfctl", "config.yaml") - - viper.SetConfigFile(configPath) - if err := viper.ReadInConfig(); err != nil && !os.IsNotExist(err) { - return err - } - - envPath := fmt.Sprintf("environments.%s.tokens", currentEnv) - var tokens []TokenInfo - if tokensList := viper.Get(envPath); tokensList != nil { - if tokenList, ok := tokensList.([]interface{}); ok { - for _, t := range tokenList { - if tokenMap, ok := t.(map[string]interface{}); ok { - tokenInfo := TokenInfo{ - Token: tokenMap["token"].(string), - } - tokens = append(tokens, tokenInfo) - } - } - } - } - - if err := keyboard.Open(); err != nil { - return err - } - defer keyboard.Close() - - selectedIndex := 0 - options := []string{"Enter a new token"} - var validTokens []TokenInfo // New slice to store only valid tokens - - for _, tokenInfo := range tokens { - claims, err := validateAndDecodeToken(tokenInfo.Token) - if err != nil { - pterm.Warning.Printf("Invalid token found in config: %v\n", err) - continue - } - - displayName := getTokenDisplayName(claims) - options = append(options, displayName) - validTokens = append(validTokens, tokenInfo) - } - - if len(validTokens) == 0 && len(tokens) > 0 { - pterm.Warning.Println("All existing tokens are invalid. Please enter a new token.") - // Clear invalid tokens from config - if err := clearInvalidTokens(currentEnv); err != nil { - pterm.Warning.Printf("Failed to clear invalid tokens: %v\n", err) - } - } - - for { - fmt.Print("\033[H\033[2J") // Clear screen - - pterm.DefaultHeader.WithFullWidth(). - WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)). - WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)). - Println("Choose an option:") - - for i, option := range options { - if i == selectedIndex { - pterm.Printf("→ %d: %s\n", i, option) - } else { - pterm.Printf(" %d: %s\n", i, option) - } - } - - pterm.DefaultBasicText.WithStyle(pterm.NewStyle(pterm.FgGray)). - Println("\nNavigation: [j]down [k]up [Enter]select [q]uit") - - char, key, err := keyboard.GetKey() - if err != nil { - return err - } - - switch key { - case keyboard.KeyEnter: - if selectedIndex == 0 { - // Enter a new token - token, err := promptToken() - if err != nil { - return err - } - - // Validate new token before saving - if _, err := validateAndDecodeToken(token); err != nil { - return fmt.Errorf("invalid token: %v", err) - } - - // First save to tokens array - if err := saveAppToken(currentEnv, token); err != nil { - return err - } - // Then set as current token - if err := saveSelectedToken(currentEnv, token); err != nil { - return err - } - pterm.Success.Printf("Token successfully saved and selected\n") - return nil - } else { - // Use selected token from existing valid tokens - selectedToken := validTokens[selectedIndex-1].Token - if err := saveSelectedToken(currentEnv, selectedToken); err != nil { - return fmt.Errorf("failed to save selected token: %v", err) - } - pterm.Success.Printf("Token successfully selected\n") - return nil - } - } - - switch char { - case 'j': - if selectedIndex < len(options)-1 { - selectedIndex++ - } - case 'k': - if selectedIndex > 0 { - selectedIndex-- - } - case 'q', 'Q': - pterm.Error.Println("Selection cancelled.") - os.Exit(1) - } - } -} - -func getTokenDisplayName(claims map[string]interface{}) string { - role := claims["rol"].(string) - domainID := claims["did"].(string) - - if role == "WORKSPACE_OWNER" { - workspaceID := claims["wid"].(string) - return fmt.Sprintf("%s (%s, %s)", role, domainID, workspaceID) - } - - return fmt.Sprintf("%s (%s)", role, domainID) -} - -func executeUserLogin(currentEnv string) { - loadEnvironmentConfig() - - baseUrl := providedUrl - if baseUrl == "" { - pterm.Error.Println("No token endpoint specified in the configuration file.") - exitWithError() - } - - homeDir, _ := os.UserHomeDir() - // Get user_id from current environment - mainViper := viper.New() - settingPath := filepath.Join(homeDir, ".cfctl", "setting.toml") - mainViper.SetConfigFile(settingPath) - mainViper.SetConfigType("toml") - - if err := mainViper.ReadInConfig(); err != nil { - pterm.Error.Printf("Failed to read config file: %v\n", err) - exitWithError() - } - - // Extract domain name from environment - nameParts := strings.Split(currentEnv, "-") - if len(nameParts) < 3 { - pterm.Error.Println("Environment name format is invalid.") - exitWithError() - } - name := nameParts[1] - - // Fetch Domain ID - domainID, err := fetchDomainID(baseUrl, name) - if err != nil { - pterm.Error.Println("Failed to fetch Domain ID:", err) - exitWithError() - } - - // 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) { - // 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 - envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) - if err := os.MkdirAll(envCacheDir, 0700); err != nil { - pterm.Error.Printf("Failed to create cache directory: %v\n", err) - exitWithError() - } - - pterm.Info.Printf("Logged in as %s\n", tempUserID) - - // Use the tokens to fetch workspaces and role - workspaces, err := fetchWorkspaces(baseUrl, accessToken) - if err != nil { - pterm.Error.Println("Failed to fetch workspaces:", err) - exitWithError() - } - - domainID, roleType, err := fetchDomainIDAndRole(baseUrl, accessToken) - if err != nil { - pterm.Error.Println("Failed to fetch Domain ID and Role Type:", err) - exitWithError() - } - - // Determine scope and select workspace - scope := determineScope(roleType, len(workspaces)) - var workspaceID string - if roleType == "DOMAIN_ADMIN" { - workspaceID = selectScopeOrWorkspace(workspaces, roleType) - if workspaceID == "0" { - scope = "DOMAIN" - workspaceID = "" - } else { - scope = "WORKSPACE" - } - } else { - workspaceID = selectWorkspaceOnly(workspaces) - scope = "WORKSPACE" - } - - // Grant new token using the refresh token - newAccessToken, err = grantToken(baseUrl, refreshToken, scope, domainID, workspaceID) - if err != nil { - pterm.Error.Println("Failed to retrieve new access token:", err) - exitWithError() - } - - // Save all tokens - if err := os.WriteFile(filepath.Join(envCacheDir, "access_token"), []byte(accessToken), 0600); err != nil { - pterm.Error.Printf("Failed to save access token: %v\n", err) - exitWithError() - } - - if err := os.WriteFile(filepath.Join(envCacheDir, "refresh_token"), []byte(refreshToken), 0600); err != nil { - pterm.Error.Printf("Failed to save refresh token: %v\n", err) - exitWithError() - } - - if err := os.WriteFile(filepath.Join(envCacheDir, "grant_token"), []byte(newAccessToken), 0600); err != nil { - pterm.Error.Printf("Failed to save grant token: %v\n", err) - exitWithError() - } - - pterm.Success.Println("Successfully logged in and saved token.") -} - -// Prompt for password when token is expired -func promptPassword() string { - passwordInput := pterm.DefaultInteractiveTextInput.WithMask("*") - password, _ := passwordInput.Show("Enter your password") - return password -} - -// min returns the minimum of two integers -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func getEncryptionKey() ([]byte, error) { - key, err := keyring.Get(keyringService, keyringUser) - if err == keyring.ErrNotFound { - newKey := make([]byte, 32) - if _, err := rand.Read(newKey); err != nil { - return nil, fmt.Errorf("failed to generate new key: %v", err) - } - - encodedKey := base64.StdEncoding.EncodeToString(newKey) - if err := keyring.Set(keyringService, keyringUser, encodedKey); err != nil { - return nil, fmt.Errorf("failed to store key in keychain: %v", err) - } - - return newKey, nil - } - if err != nil { - return nil, fmt.Errorf("failed to access keychain: %v", err) - } - - return base64.StdEncoding.DecodeString(key) -} - -func encrypt(text string) (string, error) { - key, err := getEncryptionKey() - if err != nil { - return "", fmt.Errorf("failed to get encryption key: %v", err) - } - - plaintext := []byte(text) - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - - ciphertext := make([]byte, aes.BlockSize+len(plaintext)) - iv := ciphertext[:aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return "", err - } - - stream := cipher.NewCFBEncrypter(block, iv) - stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) - - return base64.URLEncoding.EncodeToString(ciphertext), nil -} - -func decrypt(cryptoText string) (string, error) { - key, err := getEncryptionKey() - if err != nil { - return "", fmt.Errorf("failed to get encryption key: %v", err) - } - - ciphertext, err := base64.URLEncoding.DecodeString(cryptoText) - if err != nil { - return "", err - } - - block, err := aes.NewCipher(key) - if err != nil { - return "", err - } - - if len(ciphertext) < aes.BlockSize { - return "", errors.New("ciphertext too short") - } - - iv := ciphertext[:aes.BlockSize] - ciphertext = ciphertext[aes.BlockSize:] - - stream := cipher.NewCFBDecrypter(block, iv) - stream.XORKeyStream(ciphertext, ciphertext) - - return string(ciphertext), nil -} - -// Define a struct for user credentials -type UserCredentials struct { - UserID string `yaml:"userid"` - Password string `yaml:"password"` - Token string `yaml:"token"` -} - -// saveCredentials saves the user's credentials to the configuration -func saveCredentials(currentEnv, userID, encryptedPassword, accessToken, refreshToken, grantToken string) { - homeDir, err := os.UserHomeDir() - if err != nil { - pterm.Error.Println("Failed to get home directory:", err) - exitWithError() - } - - // Update main settings file - settingPath := filepath.Join(homeDir, ".cfctl", "setting.toml") - mainViper := viper.New() - mainViper.SetConfigFile(settingPath) - mainViper.SetConfigType("toml") - - if err := mainViper.ReadInConfig(); err != nil { - pterm.Error.Printf("Failed to read config file: %v\n", err) - exitWithError() - } - - // Save user_id to environment settings - envPath := fmt.Sprintf("environments.%s.user_id", currentEnv) - mainViper.Set(envPath, userID) - - if err := mainViper.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to save config file: %v\n", err) - exitWithError() - } - - // Create cache directory - envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) - if err := os.MkdirAll(envCacheDir, 0700); err != nil { - pterm.Error.Printf("Failed to create cache directory: %v\n", err) - exitWithError() - } - - // Save tokens to cache - if err := os.WriteFile(filepath.Join(envCacheDir, "access_token"), []byte(accessToken), 0600); err != nil { - pterm.Error.Printf("Failed to save access token: %v\n", err) - exitWithError() - } - - if refreshToken != "" { - if err := os.WriteFile(filepath.Join(envCacheDir, "refresh_token"), []byte(refreshToken), 0600); err != nil { - pterm.Error.Printf("Failed to save refresh token: %v\n", err) - exitWithError() - } - } - - if grantToken != "" { - if err := os.WriteFile(filepath.Join(envCacheDir, "grant_token"), []byte(grantToken), 0600); err != nil { - pterm.Error.Printf("Failed to save grant token: %v\n", err) - exitWithError() - } - } -} - -func verifyAppToken(token string) (map[string]interface{}, bool) { - parts := strings.Split(token, ".") - if len(parts) != 3 { - pterm.Error.Println("Invalid token format") - return nil, false - } - - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - pterm.Error.Println("Failed to decode token payload:", err) - return nil, false - } - - var claims map[string]interface{} - if err := json.Unmarshal(payload, &claims); err != nil { - pterm.Error.Println("Failed to parse token payload:", err) - return nil, false - } - - exp, ok := claims["exp"].(float64) - if !ok { - pterm.Error.Println("Expiration time not found in token") - return nil, false - } - - if time.Now().After(time.Unix(int64(exp), 0)) { - pterm.DefaultBox.WithTitle("Expired App Token"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - WithBoxStyle(pterm.NewStyle(pterm.FgRed)). - Println("Your App token has expired.\nPlease generate a new App and update your config file.") - return nil, false - } - - role, ok := claims["rol"].(string) - if !ok { - pterm.Error.Println("Role not found in token") - return nil, false - } - - if role != "DOMAIN_ADMIN" && role != "WORKSPACE_OWNER" { - pterm.DefaultBox.WithTitle("Invalid App Token"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - WithBoxStyle(pterm.NewStyle(pterm.FgRed)). - Println("App token must have either DOMAIN_ADMIN or WORKSPACE_OWNER role.\nPlease generate a new App with appropriate permissions and update your config file.") - return nil, false - } - - return claims, true -} - -// Load environment-specific configuration based on the selected environment -func loadEnvironmentConfig() { - homeDir, err := os.UserHomeDir() - if err != nil { - pterm.Error.Println("Failed to get user home directory:", err) - exitWithError() - } - - settingPath := filepath.Join(homeDir, ".cfctl", "setting.toml") - viper.SetConfigFile(settingPath) - viper.SetConfigType("toml") - - if err := viper.ReadInConfig(); err != nil { - pterm.Error.Printf("Failed to read setting file: %v\n", err) - exitWithError() - } - - currentEnv := viper.GetString("environment") - if currentEnv == "" { - pterm.Error.Println("No environment selected") - exitWithError() - } - - v := viper.New() - v.SetConfigFile(settingPath) - if err := v.ReadInConfig(); err == nil { - endpointKey := fmt.Sprintf("environments.%s.endpoint", currentEnv) - tokenKey := fmt.Sprintf("environments.%s.token", currentEnv) - - if providedUrl == "" { - providedUrl = v.GetString(endpointKey) - } - - if token := v.GetString(tokenKey); token != "" { - viper.Set("token", token) - } - } - - isProxyEnabled := viper.GetBool(fmt.Sprintf("environments.%s.proxy", currentEnv)) - containsIdentity := strings.Contains(strings.ToLower(providedUrl), "identity") - - if !isProxyEnabled && !containsIdentity { - pterm.DefaultBox.WithTitle("Proxy Mode Required"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgYellow)). - Println("Current endpoint is not configured for identity service.\n" + - "Please enable proxy mode and set identity endpoint first.") - - pterm.DefaultBox.WithBoxStyle(pterm.NewStyle(pterm.FgCyan)). - Println("$ cfctl config endpoint -s identity\n" + - "$ cfctl login") - - exitWithError() - } -} - -func determineScope(roleType string, workspaceCount int) string { - switch roleType { - case "DOMAIN_ADMIN": - return "DOMAIN" - case "WORKSPACE_OWNER", "WORKSPACE_MEMBER", "USER": - return "WORKSPACE" - default: - pterm.Error.Println("Unknown role_type:", roleType) - exitWithError() - return "" // Unreachable - } -} - -// isTokenExpired checks if the token is expired -func isTokenExpired(token string) bool { - claims, err := decodeJWT(token) - if err != nil { - return true // 디코딩 실패 시 만료된 것으로 간주 - } - - if exp, ok := claims["exp"].(float64); ok { - return time.Now().Unix() > int64(exp) - } - return true // exp 필드가 없거나 잘못된 형식이면 만료된 것으로 간주 -} - -func verifyToken(token string) bool { - // This function should implement token verification logic, for example by making a request to an endpoint that requires authentication - // Returning true for simplicity in this example - return true -} - -func exitWithError() { - os.Exit(1) -} - -func fetchDomainID(baseUrl string, name string) (string, error) { - // Parse the endpoint - parts := strings.Split(baseUrl, "://") - if len(parts) != 2 { - return "", fmt.Errorf("invalid endpoint format: %s", baseUrl) - } - - hostPort := parts[1] - - // Configure gRPC connection - var opts []grpc.DialOption - if strings.HasPrefix(baseUrl, "grpc+ssl://") { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - // Establish connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return "", fmt.Errorf("failed to connect: %v", err) - } - defer conn.Close() - - // Create reflection client - refClient := grpcreflect.NewClient(context.Background(), grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // Resolve the service - serviceName := "spaceone.api.identity.v2.Domain" - serviceDesc, err := refClient.ResolveService(serviceName) - if err != nil { - return "", fmt.Errorf("failed to resolve service %s: %v", serviceName, err) - } - - // Find the method descriptor - methodDesc := serviceDesc.FindMethodByName("get_auth_info") - if methodDesc == nil { - return "", fmt.Errorf("method get_auth_info not found") - } - - // Create request message - reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) - reqMsg.SetFieldByName("name", name) - - // Make the gRPC call - fullMethod := fmt.Sprintf("/%s/%s", serviceName, "get_auth_info") - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - - err = conn.Invoke(context.Background(), fullMethod, reqMsg, respMsg) - if err != nil { - return "", fmt.Errorf("RPC failed: %v", err) - } - - // Extract domain_id from response - domainID, err := respMsg.TryGetFieldByName("domain_id") - if err != nil { - return "", fmt.Errorf("failed to get domain_id from response: %v", err) - } - - return domainID.(string), nil -} - -func issueToken(baseUrl, userID, password, domainID string) (string, string, error) { - // Parse the endpoint - parts := strings.Split(baseUrl, "://") - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid endpoint format: %s", baseUrl) - } - - hostPort := parts[1] - - // Configure gRPC connection - var opts []grpc.DialOption - if strings.HasPrefix(baseUrl, "grpc+ssl://") { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - // Establish connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return "", "", fmt.Errorf("failed to connect: %v", err) - } - defer conn.Close() - - // Create reflection client - refClient := grpcreflect.NewClient(context.Background(), grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // Resolve the service - serviceName := "spaceone.api.identity.v2.Token" - serviceDesc, err := refClient.ResolveService(serviceName) - if err != nil { - return "", "", fmt.Errorf("failed to resolve service %s: %v", serviceName, err) - } - - // Find the method descriptor - methodDesc := serviceDesc.FindMethodByName("issue") - if methodDesc == nil { - return "", "", fmt.Errorf("method issue not found") - } - - // Create request message - reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) - - // Create credentials struct using protobuf types - structpb := &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "user_id": { - Kind: &structpb.Value_StringValue{ - StringValue: userID, - }, - }, - "password": { - Kind: &structpb.Value_StringValue{ - StringValue: password, - }, - }, - }, - } - - // Set all fields in the request message - reqMsg.SetFieldByName("credentials", structpb) - reqMsg.SetFieldByName("auth_type", int32(1)) // LOCAL = 1 - reqMsg.SetFieldByName("timeout", int32(0)) - reqMsg.SetFieldByName("verify_code", "") - reqMsg.SetFieldByName("domain_id", domainID) - - // Make the gRPC call - fullMethod := fmt.Sprintf("/%s/%s", serviceName, "issue") - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - - err = conn.Invoke(context.Background(), fullMethod, reqMsg, respMsg) - if err != nil { - return "", "", fmt.Errorf("RPC failed: %v", err) - } - - // Extract tokens from response - accessToken, err := respMsg.TryGetFieldByName("access_token") - if err != nil { - return "", "", fmt.Errorf("failed to get access_token from response: %v", err) - } - - refreshToken, err := respMsg.TryGetFieldByName("refresh_token") - if err != nil { - return "", "", fmt.Errorf("failed to get refresh_token from response: %v", err) - } - - return accessToken.(string), refreshToken.(string), nil -} - -func fetchWorkspaces(baseUrl string, accessToken string) ([]map[string]interface{}, error) { - // Parse the endpoint - parts := strings.Split(baseUrl, "://") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid endpoint format: %s", baseUrl) - } - - hostPort := parts[1] - - // Configure gRPC connection - var opts []grpc.DialOption - if strings.HasPrefix(baseUrl, "grpc+ssl://") { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - // Add token credentials - creds := &tokenAuth{ - token: accessToken, - } - opts = append(opts, grpc.WithPerRPCCredentials(creds)) - - // Establish connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("failed to connect: %v", err) - } - defer conn.Close() - - // Create reflection client - refClient := grpcreflect.NewClient(context.Background(), grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // Resolve the service - serviceName := "spaceone.api.identity.v2.UserProfile" - serviceDesc, err := refClient.ResolveService(serviceName) - if err != nil { - return nil, fmt.Errorf("failed to resolve service %s: %v", serviceName, err) - } - - // Find the method descriptor - methodDesc := serviceDesc.FindMethodByName("get_workspaces") - if methodDesc == nil { - return nil, fmt.Errorf("method get_workspaces not found") - } - - // Create request message - reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) - - // Create metadata with token - md := metadata.New(map[string]string{ - "token": accessToken, - }) - ctx := metadata.NewOutgoingContext(context.Background(), md) - - // Make the gRPC call - fullMethod := "/spaceone.api.identity.v2.UserProfile/get_workspaces" - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - - err = conn.Invoke(ctx, fullMethod, reqMsg, respMsg) - if err != nil { - return nil, fmt.Errorf("RPC failed: %v", err) - } - - // Extract results from response - results, err := respMsg.TryGetFieldByName("results") - if err != nil { - return nil, fmt.Errorf("failed to get results from response: %v", err) - } - - workspaces, ok := results.([]interface{}) - if !ok || len(workspaces) == 0 { - pterm.Warning.Println("There are no accessible workspaces. Ask your administrators or workspace owners for access.") - exitWithError() - } - - var workspaceList []map[string]interface{} - for _, workspace := range workspaces { - workspaceMsg, ok := workspace.(*dynamic.Message) - if !ok { - return nil, fmt.Errorf("failed to parse workspace message") - } - - workspaceMap := make(map[string]interface{}) - fields := workspaceMsg.GetKnownFields() - - for _, field := range fields { - if value, err := workspaceMsg.TryGetFieldByName(field.GetName()); err == nil { - workspaceMap[field.GetName()] = value - } - } - - workspaceList = append(workspaceList, workspaceMap) - } - - return workspaceList, nil -} - -func fetchDomainIDAndRole(baseUrl string, accessToken string) (string, string, error) { - // Parse the endpoint - parts := strings.Split(baseUrl, "://") - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid endpoint format: %s", baseUrl) - } - - hostPort := parts[1] - - // Configure gRPC connection - var opts []grpc.DialOption - if strings.HasPrefix(baseUrl, "grpc+ssl://") { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - // Add token to metadata - opts = append(opts, grpc.WithPerRPCCredentials(&tokenAuth{token: accessToken})) - - // Establish connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return "", "", fmt.Errorf("failed to connect: %v", err) - } - defer conn.Close() - - // Create reflection client - refClient := grpcreflect.NewClient(context.Background(), grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // Resolve the service - serviceName := "spaceone.api.identity.v2.UserProfile" - serviceDesc, err := refClient.ResolveService(serviceName) - if err != nil { - return "", "", fmt.Errorf("failed to resolve service %s: %v", serviceName, err) - } - - // Find the method descriptor - methodDesc := serviceDesc.FindMethodByName("get") - if methodDesc == nil { - return "", "", fmt.Errorf("method get not found") - } - - // Create request message - reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) - - // Make the gRPC call - fullMethod := fmt.Sprintf("/%s/%s", serviceName, "get") - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - - err = conn.Invoke(context.Background(), fullMethod, reqMsg, respMsg) - if err != nil { - return "", "", fmt.Errorf("RPC failed: %v", err) - } - - // Extract domain_id and role_type from response - domainID, err := respMsg.TryGetFieldByName("domain_id") - if err != nil { - return "", "", fmt.Errorf("failed to get domain_id from response: %v", err) - } - - roleType, err := respMsg.TryGetFieldByName("role_type") - if err != nil { - return "", "", fmt.Errorf("failed to get role_type from response: %v", err) - } - - // Convert roleType to string based on enum value - var roleTypeStr string - switch v := roleType.(type) { - case int32: - switch v { - case 1: - roleTypeStr = "DOMAIN_ADMIN" - case 2: - roleTypeStr = "WORKSPACE_OWNER" - case 3: - roleTypeStr = "WORKSPACE_MEMBER" - default: - return "", "", fmt.Errorf("unknown role_type: %d", v) - } - case string: - roleTypeStr = v - default: - return "", "", fmt.Errorf("unexpected role_type type: %T", roleType) - } - - return domainID.(string), roleTypeStr, nil -} - -func grantToken(baseUrl, refreshToken, scope, domainID, workspaceID string) (string, error) { - // Parse the endpoint - parts := strings.Split(baseUrl, "://") - if len(parts) != 2 { - return "", fmt.Errorf("invalid endpoint format: %s", baseUrl) - } - - hostPort := parts[1] - - // Configure gRPC connection - var opts []grpc.DialOption - if strings.HasPrefix(baseUrl, "grpc+ssl://") { - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - // Establish connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return "", fmt.Errorf("failed to connect: %v", err) - } - defer conn.Close() - - // Create reflection client - refClient := grpcreflect.NewClient(context.Background(), grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // Resolve the service - serviceName := "spaceone.api.identity.v2.Token" - serviceDesc, err := refClient.ResolveService(serviceName) - if err != nil { - return "", fmt.Errorf("failed to resolve service %s: %v", serviceName, err) - } - - // Find the method descriptor - methodDesc := serviceDesc.FindMethodByName("grant") - if methodDesc == nil { - return "", fmt.Errorf("method grant not found") - } - - // Create request message - reqMsg := dynamic.NewMessage(methodDesc.GetInputType()) - - reqMsg.SetFieldByName("grant_type", int32(1)) - - var scopeEnum int32 - switch scope { - case "DOMAIN": - scopeEnum = 2 - case "WORKSPACE": - scopeEnum = 3 - case "USER": - scopeEnum = 5 - default: - return "", fmt.Errorf("unknown scope: %s", scope) - } - - reqMsg.SetFieldByName("scope", scopeEnum) - reqMsg.SetFieldByName("token", refreshToken) - reqMsg.SetFieldByName("timeout", int32(21600)) - reqMsg.SetFieldByName("domain_id", domainID) - if workspaceID != "" { - reqMsg.SetFieldByName("workspace_id", workspaceID) - } - - // Make the gRPC call - fullMethod := "/spaceone.api.identity.v2.Token/grant" - respMsg := dynamic.NewMessage(methodDesc.GetOutputType()) - - err = conn.Invoke(context.Background(), fullMethod, reqMsg, respMsg) - if err != nil { - return "", fmt.Errorf("RPC failed: %v", err) - } - - // Extract access_token from response - accessToken, err := respMsg.TryGetFieldByName("access_token") - if err != nil { - return "", fmt.Errorf("failed to get access_token from response: %v", err) - } - - return accessToken.(string), nil -} - -// saveSelectedToken saves the selected token as the current token for the environment -func saveSelectedToken(currentEnv, selectedToken string) error { - homeDir, _ := os.UserHomeDir() - configPath := filepath.Join(homeDir, ".cfctl", "config.yaml") - - viper.SetConfigFile(configPath) - if err := viper.ReadInConfig(); err != nil && !os.IsNotExist(err) { - return err - } - - envPath := fmt.Sprintf("environments.%s", currentEnv) - envSettings := viper.GetStringMap(envPath) - if envSettings == nil { - envSettings = make(map[string]interface{}) - } - - // Keep all existing settings - newEnvSettings := make(map[string]interface{}) - - // Keep endpoint and proxy settings - if endpoint, ok := envSettings["endpoint"]; ok { - newEnvSettings["endpoint"] = endpoint - } - if proxy, ok := envSettings["proxy"]; ok { - newEnvSettings["proxy"] = proxy - } - - // Keep tokens array - if tokens, ok := envSettings["tokens"]; ok { - newEnvSettings["tokens"] = tokens - } - - // Set the selected token as current token - newEnvSettings["token"] = selectedToken - - viper.Set(envPath, newEnvSettings) - return viper.WriteConfig() -} - -func selectScopeOrWorkspace(workspaces []map[string]interface{}, roleType string) string { - if err := keyboard.Open(); err != nil { - pterm.Error.Println("Failed to initialize keyboard:", err) - exitWithError() - } - defer keyboard.Close() - - if roleType != "DOMAIN_ADMIN" { - return selectWorkspaceOnly(workspaces) - } - - options := []string{"DOMAIN", "WORKSPACES"} - selectedIndex := 0 - - for { - fmt.Print("\033[H\033[2J") - - // Display scope selection - pterm.DefaultHeader.WithFullWidth(). - WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)). - WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)). - Println("Select Scope") - - for i, option := range options { - if i == selectedIndex { - pterm.Printf("→ %d: %s\n", i, option) - } else { - pterm.Printf(" %d: %s\n", i, option) - } - } - - // Show navigation help - pterm.DefaultBasicText.WithStyle(pterm.NewStyle(pterm.FgGray)). - Println("\nNavigation: [j]down [k]up, [Enter]select, [q]uit") - - // Get keyboard input - char, key, err := keyboard.GetKey() - if err != nil { - pterm.Error.Println("Error reading keyboard input:", err) - exitWithError() - } - - // Handle navigation and other commands - switch key { - case keyboard.KeyEnter: - if selectedIndex == 0 { - return "0" - } else { - return selectWorkspaceOnly(workspaces) - } - } - - switch char { - case 'j': // Down - if selectedIndex < len(options)-1 { - selectedIndex++ - } - case 'k': // Up - if selectedIndex > 0 { - selectedIndex-- - } - case 'q', 'Q': - pterm.Error.Println("Selection cancelled.") - os.Exit(1) - } - } -} - -// selectWorkspaceOnly handles workspace selection -func selectWorkspaceOnly(workspaces []map[string]interface{}) string { - const pageSize = 15 - currentPage := 0 - searchMode := false - searchTerm := "" - selectedIndex := 0 - inputBuffer := "" - filteredWorkspaces := workspaces - - if err := keyboard.Open(); err != nil { - pterm.Error.Println("Failed to initialize keyboard:", err) - exitWithError() - } - defer keyboard.Close() - - for { - // Clear screen - fmt.Print("\033[H\033[2J") - - // Apply search filter - if searchTerm != "" { - filteredWorkspaces = filterWorkspaces(workspaces, searchTerm) - if len(filteredWorkspaces) == 0 { - filteredWorkspaces = workspaces - } - } else { - filteredWorkspaces = workspaces - } - - // Calculate pagination - totalWorkspaces := len(filteredWorkspaces) - totalPages := (totalWorkspaces + pageSize - 1) / pageSize - startIndex := (currentPage % totalPages) * pageSize - endIndex := startIndex + pageSize - if endIndex > totalWorkspaces { - endIndex = totalWorkspaces - } - - // Display header with page information - pterm.DefaultHeader.WithFullWidth(). - WithBackgroundStyle(pterm.NewStyle(pterm.BgDarkGray)). - WithTextStyle(pterm.NewStyle(pterm.FgLightWhite)). - Printf("Accessible Workspaces (Page %d of %d)", currentPage+1, totalPages) - - // Show workspace list - for i := startIndex; i < endIndex; i++ { - name := filteredWorkspaces[i]["name"].(string) - if i-startIndex == selectedIndex { - pterm.Printf("→ %d: %s\n", i+1, name) - } else { - pterm.Printf(" %d: %s\n", i+1, name) - } - } - - // Show navigation help and search prompt - pterm.DefaultBasicText.WithStyle(pterm.NewStyle(pterm.FgGray)). - Println("\nNavigation: [h]prev-page [j]down [k]up [l]next-page [/]search [q]uit") - - // Show search or input prompt at the bottom - if searchMode { - fmt.Println() - pterm.Info.Printf("Search (ESC to cancel, Enter to confirm): %s", searchTerm) - } else { - fmt.Print("\nSelect a workspace above or input a number: ") - if inputBuffer != "" { - fmt.Print(inputBuffer) - } - } - - // Get keyboard input - char, key, err := keyboard.GetKey() - if err != nil { - pterm.Error.Println("Error reading keyboard input:", err) - exitWithError() - } - - // Handle search mode input - if searchMode { - switch key { - case keyboard.KeyEsc: - searchMode = false - searchTerm = "" - case keyboard.KeyBackspace, keyboard.KeyBackspace2: - if len(searchTerm) > 0 { - searchTerm = searchTerm[:len(searchTerm)-1] - } - case keyboard.KeyEnter: - searchMode = false - default: - if char != 0 { - searchTerm += string(char) - } - } - currentPage = 0 - selectedIndex = 0 - continue - } - - // Handle normal mode input - switch key { - case keyboard.KeyEnter: - if inputBuffer != "" { - index, err := strconv.Atoi(inputBuffer) - if err == nil && index >= 1 && index <= len(filteredWorkspaces) { - return filteredWorkspaces[index-1]["workspace_id"].(string) - } - inputBuffer = "" - } else { - adjustedIndex := startIndex + selectedIndex - if adjustedIndex >= 0 && adjustedIndex < len(filteredWorkspaces) { - return filteredWorkspaces[adjustedIndex]["workspace_id"].(string) - } - } - case keyboard.KeyBackspace, keyboard.KeyBackspace2: - if len(inputBuffer) > 0 { - inputBuffer = inputBuffer[:len(inputBuffer)-1] - } - } - - // Handle navigation and other commands - switch char { - case 'j': // Down - if selectedIndex < min(pageSize-1, endIndex-startIndex-1) { - selectedIndex++ - } - case 'k': // Up - if selectedIndex > 0 { - selectedIndex-- - } - case 'l': // Next page - currentPage = (currentPage + 1) % totalPages - selectedIndex = 0 - case 'h': // Previous page - currentPage = (currentPage - 1 + totalPages) % totalPages - selectedIndex = 0 - case 'q', 'Q': - fmt.Println() - pterm.Error.Println("Workspace selection cancelled.") - os.Exit(1) - case '/': - searchMode = true - searchTerm = "" - selectedIndex = 0 - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - if !searchMode { - inputBuffer += string(char) - } - } - } -} - -func filterWorkspaces(workspaces []map[string]interface{}, searchTerm string) []map[string]interface{} { - var filtered []map[string]interface{} - searchTerm = strings.ToLower(searchTerm) - - for _, workspace := range workspaces { - name := strings.ToLower(workspace["name"].(string)) - if strings.Contains(name, searchTerm) { - filtered = append(filtered, workspace) - } - } - return filtered -} - -func init() { - LoginCmd.Flags().StringVarP(&providedUrl, "url", "u", "", "The URL to use for login (e.g. cfctl login -u https://example.com)") -} - -// decodeJWT decodes a JWT token and returns the claims -func decodeJWT(token string) (map[string]interface{}, error) { - parts := strings.Split(token, ".") - if len(parts) != 3 { - return nil, fmt.Errorf("invalid token format") - } - - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return nil, err - } - - var claims map[string]interface{} - if err := json.Unmarshal(payload, &claims); err != nil { - return nil, err - } - - return claims, nil -} - -// validateAndDecodeToken decodes a JWT token and validates its expiration -func validateAndDecodeToken(token string) (map[string]interface{}, error) { - // Check if token has three parts (header.payload.signature) - parts := strings.Split(token, ".") - if len(parts) != 3 { - return nil, fmt.Errorf("invalid token format: token must have three parts") - } - - // Try to decode the payload - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return nil, fmt.Errorf("invalid token format: failed to decode payload: %v", err) - } - - var claims map[string]interface{} - if err := json.Unmarshal(payload, &claims); err != nil { - return nil, fmt.Errorf("invalid token format: failed to parse payload: %v", err) - } - - // Check required fields - requiredFields := []string{"exp", "did"} - for _, field := range requiredFields { - if _, ok := claims[field]; !ok { - return nil, fmt.Errorf("invalid token format: missing required field '%s'", field) - } - } - - // Check expiration - if isTokenExpired(token) { - return nil, fmt.Errorf("token has expired") - } - - return claims, nil -} - -// clearInvalidTokens removes invalid tokens from the config -func clearInvalidTokens(currentEnv string) error { - homeDir, _ := os.UserHomeDir() - configPath := filepath.Join(homeDir, ".cfctl", "config.yaml") - - viper.SetConfigFile(configPath) - if err := viper.ReadInConfig(); err != nil { - return err - } - - envPath := fmt.Sprintf("environments.%s", currentEnv) - envSettings := viper.GetStringMap(envPath) - if envSettings == nil { - return nil - } - - var validTokens []TokenInfo - if tokensList := viper.Get(fmt.Sprintf("%s.tokens", envPath)); tokensList != nil { - if tokenList, ok := tokensList.([]interface{}); ok { - for _, t := range tokenList { - if tokenMap, ok := t.(map[string]interface{}); ok { - token := tokenMap["token"].(string) - if _, err := validateAndDecodeToken(token); err == nil { - validTokens = append(validTokens, TokenInfo{Token: token}) - } - } - } - } - } - - // Update config with only valid tokens - envSettings["tokens"] = validTokens - viper.Set(envPath, envSettings) - return viper.WriteConfig() -} - -// readTokenFromFile reads a token from the specified file in the environment cache directory -func readTokenFromFile(envDir, tokenType string) (string, error) { - tokenPath := filepath.Join(envDir, tokenType) - data, err := os.ReadFile(tokenPath) - if err != nil { - return "", err - } - return string(data), nil -} - -// getValidTokens checks for existing valid tokens in the environment cache directory -func getValidTokens(currentEnv string) (accessToken, refreshToken, newAccessToken string, err error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", "", "", err - } - - envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) - - if refreshToken, err = readTokenFromFile(envCacheDir, "refresh_token"); err == nil { - claims, err := validateAndDecodeToken(refreshToken) - if err == nil { - if exp, ok := claims["exp"].(float64); ok { - if time.Now().Unix() < int64(exp) { - accessToken, _ = readTokenFromFile(envCacheDir, "access_token") - newAccessToken, _ = readTokenFromFile(envCacheDir, "grant_token") - return accessToken, refreshToken, newAccessToken, nil - } - } - } - } - - return "", "", "", fmt.Errorf("no valid tokens found") -} diff --git a/cmd/other/setting.go b/cmd/other/setting.go deleted file mode 100644 index bf1f494..0000000 --- a/cmd/other/setting.go +++ /dev/null @@ -1,1279 +0,0 @@ -package other - -import ( - "context" - "crypto/tls" - "encoding/json" - "fmt" - "log" - "net/url" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/jhump/protoreflect/dynamic" - - "github.com/jhump/protoreflect/grpcreflect" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" - - "github.com/pelletier/go-toml/v2" - "github.com/pterm/pterm" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// SettingCmd represents the setting command -var SettingCmd = &cobra.Command{ - Use: "setting", - Short: "Manage cfctl setting file", - Long: `Manage setting file for cfctl. You can initialize, -switch environments, and display the current configuration.`, -} - -// settingInitCmd initializes a new environment configuration -var settingInitCmd = &cobra.Command{ - Use: "init", - Short: "Initialize a new environment setting", - Long: `Initialize a new environment setting for cfctl by specifying either a URL or a local environment name.`, -} - -// settingInitURLCmd initializes configuration with a URL -var settingInitURLCmd = &cobra.Command{ - Use: "url", - Short: "Initialize configuration with a URL", - Long: `Specify a URL to initialize the environment configuration.`, - Args: cobra.NoArgs, - Example: ` cfctl setting init url -u https://example.com --app - or - cfctl setting init url -u https://example.com --user`, - Run: func(cmd *cobra.Command, args []string) { - urlStr, _ := cmd.Flags().GetString("url") - appFlag, _ := cmd.Flags().GetBool("app") - userFlag, _ := cmd.Flags().GetBool("user") - - if urlStr == "" { - pterm.Error.Println("The --url flag is required.") - cmd.Help() - return - } - if !appFlag && !userFlag { - pterm.Error.Println("You must specify either --app, --user, or --plugin flag.") - cmd.Help() - return - } - - envName, err := parseEnvNameFromURL(urlStr) - if err != nil { - pterm.Error.Println("Invalid URL:", err) - return - } - - // Create setting directory if it doesn't exist - settingDir := GetSettingDir() - if err := os.MkdirAll(settingDir, 0755); err != nil { - pterm.Error.Printf("Failed to create setting directory: %v\n", err) - return - } - - // Initialize setting.toml if it doesn't exist - mainSettingPath := filepath.Join(settingDir, "setting.toml") - - // Check if environment already exists - v := viper.New() - v.SetConfigFile(mainSettingPath) - v.SetConfigType("toml") - - if err := v.ReadInConfig(); err == nil { - // File exists and can be read - envSuffix := map[bool]string{true: "app", false: "user"}[appFlag] - fullEnvName := fmt.Sprintf("%s-%s", envName, envSuffix) - - if envConfig := v.GetStringMap(fmt.Sprintf("environments.%s", fullEnvName)); envConfig != nil { - // Environment exists, ask for confirmation - confirmBox := pterm.DefaultBox.WithTitle("Environment Already Exists"). - WithTitleTopCenter(). - WithRightPadding(4). - WithLeftPadding(4). - WithBoxStyle(pterm.NewStyle(pterm.FgYellow)) - - confirmBox.Println(fmt.Sprintf("Environment '%s' already exists.\nDo you want to overwrite it?", fullEnvName)) - - fmt.Print("\nEnter (y/N): ") - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response != "y" { - pterm.Info.Printf("Operation cancelled. Environment '%s' remains unchanged.\n", fullEnvName) - return - } - } - } - - // Update configuration in main setting file - updateSetting(envName, urlStr, map[bool]string{true: "app", false: "user"}[appFlag]) - - // Update the current environment - mainV := viper.New() - mainV.SetConfigFile(mainSettingPath) - mainV.SetConfigType("toml") - - if err := mainV.ReadInConfig(); err != nil && !os.IsNotExist(err) { - pterm.Error.Printf("Failed to read setting file: %v\n", err) - return - } - - // Set the environment name with app/user suffix - envName = fmt.Sprintf("%s-%s", envName, map[bool]string{true: "app", false: "user"}[appFlag]) - mainV.Set("environment", envName) - - if err := mainV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update current environment: %v\n", err) - return - } - - pterm.Success.Printf("Switched to '%s' environment.\n", envName) - }, -} - -// settingInitLocalCmd initializes configuration with a local environment -var settingInitLocalCmd = &cobra.Command{ - Use: "local", - Short: "Initialize configuration with a local environment", - Long: `Specify a local environment name to initialize the configuration.`, - Args: cobra.NoArgs, - Example: ` cfctl setting init local -n [domain] --app --dev - or - cfctl setting init local -n [domain] --user --stg, - or - cfctl setting init local -n [plugin_name] --plugin`, - Run: func(cmd *cobra.Command, args []string) { - localEnv, _ := cmd.Flags().GetString("name") - appFlag, _ := cmd.Flags().GetBool("app") - userFlag, _ := cmd.Flags().GetBool("user") - devFlag, _ := cmd.Flags().GetBool("dev") - stgFlag, _ := cmd.Flags().GetBool("stg") - pluginFlag, _ := cmd.Flags().GetBool("plugin") - - // Step 1: Check name flag - if localEnv == "" { - pterm.Error.Println("The --name flag is required.") - // Show only name flag in help - cmd.SetUsageFunc(func(cmd *cobra.Command) error { - fmt.Printf("\nUsage:\n cfctl setting init local [flags]\n\n") - fmt.Printf("Flags:\n") - fmt.Printf(" -n, --name string Local environment name for the environment\n") - fmt.Printf(" -h, --help help for local\n") - return nil - }) - cmd.Help() - return - } - - // Step 2: Check type flags (app/user/plugin) - if !appFlag && !userFlag && !pluginFlag { - pterm.Error.Println("You must specify either --app, --user, or --plugin flag.") - // Show only type flags in help - cmd.SetUsageFunc(func(cmd *cobra.Command) error { - fmt.Printf("\nUsage:\n cfctl setting init local --name %s [flags]\n\n", localEnv) - fmt.Printf("Flags:\n") - fmt.Printf(" --app Initialize as application configuration\n") - fmt.Printf(" --user Initialize as user-specific configuration\n") - fmt.Printf(" --plugin Initialize as plugin configuration\n") - return nil - }) - cmd.Help() - return - } - - // Step 3: For app/user configs, check environment type - if (appFlag || userFlag) && !devFlag && !stgFlag { - pterm.Error.Println("You must specify either --dev or --stg flag.") - // Show only environment flags in help - cmd.SetUsageFunc(func(cmd *cobra.Command) error { - fmt.Printf("\nUsage:\n cfctl setting init local --name %s --%s [flags]\n\n", - localEnv, map[bool]string{true: "app", false: "user"}[appFlag]) - fmt.Printf("Flags:\n") - fmt.Printf(" --dev Initialize as development environment\n") - fmt.Printf(" --stg Initialize as staging environment\n") - return nil - }) - cmd.Help() - return - } - - // Plugin flag takes precedence - if pluginFlag { - initializePluginSetting(localEnv) - return - } - - // Rest of the existing implementation... - settingDir := GetSettingDir() - if err := os.MkdirAll(settingDir, 0755); err != nil { - pterm.Error.Printf("Failed to create setting directory: %v\n", err) - return - } - - // Initialize setting.toml if it doesn't exist - mainSettingPath := filepath.Join(settingDir, "setting.toml") - if _, err := os.Stat(mainSettingPath); os.IsNotExist(err) { - // Initial TOML structure - initialSetting := []byte("environments = {}\n") - if err := os.WriteFile(mainSettingPath, initialSetting, 0644); err != nil { - pterm.Error.Printf("Failed to create setting file: %v\n", err) - return - } - } - - envPrefix := "" - if devFlag { - envPrefix = "dev" - } else if stgFlag { - envPrefix = "stg" - } - - var envName string - if appFlag { - envName = fmt.Sprintf("local-%s-%s-app", envPrefix, localEnv) - } else { - envName = fmt.Sprintf("local-%s-%s-user", envPrefix, localEnv) - } - - if appFlag { - updateLocalSetting(envName, "app", mainSettingPath) - } else { - updateLocalSetting(envName, "user", mainSettingPath) - } - - // Update the current environment in the main setting - mainV := viper.New() - mainV.SetConfigFile(mainSettingPath) - mainV.SetConfigType("toml") - - // Read the setting file - if err := mainV.ReadInConfig(); err != nil { - pterm.Error.Printf("Failed to read setting file: %v\n", err) - return - } - - // Set the new environment as current - mainV.Set("environment", envName) - - if err := mainV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update current environment: %v\n", err) - return - } - - pterm.Success.Printf("Switched to '%s' environment.\n", envName) - }, -} - -func initializePluginSetting(pluginName string) { - // Add 'local-' prefix to plugin name - envName := fmt.Sprintf("local-%s", pluginName) - - settingDir := GetSettingDir() - if err := os.MkdirAll(settingDir, 0755); err != nil { - pterm.Error.Printf("Failed to create setting directory: %v\n", err) - return - } - - mainSettingPath := filepath.Join(settingDir, "setting.toml") - if _, err := os.Stat(mainSettingPath); os.IsNotExist(err) { - initialSetting := []byte("environments = {}\n") - if err := os.WriteFile(mainSettingPath, initialSetting, 0644); err != nil { - pterm.Error.Printf("Failed to create setting file: %v\n", err) - return - } - } - - v := viper.New() - v.SetConfigFile(mainSettingPath) - v.SetConfigType("toml") - - if err := v.ReadInConfig(); err != nil && !os.IsNotExist(err) { - pterm.Error.Printf("Error reading setting: %v\n", err) - return - } - - // Set environment configuration using the prefixed name - v.Set(fmt.Sprintf("environments.%s.endpoint", envName), "grpc://localhost:50051") - v.Set(fmt.Sprintf("environments.%s.token", envName), "NO TOKEN") - v.Set("environment", envName) - - if err := v.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to write setting: %v\n", err) - return - } - - pterm.Success.Printf("Plugin environment '%s' successfully initialized.\n", envName) -} - -func updateLocalSetting(envName, settingType, settingPath string) { - v := viper.New() - v.SetConfigFile(settingPath) - - // Ensure directory exists - if err := os.MkdirAll(filepath.Dir(settingPath), 0755); err != nil { - pterm.Error.Printf("Failed to create directory: %v\n", err) - return - } - - // Read existing setting or create new - if err := v.ReadInConfig(); err != nil && !os.IsNotExist(err) { - pterm.Error.Printf("Error reading setting: %v\n", err) - return - } - - // Set environment configuration - v.Set(fmt.Sprintf("environments.%s.endpoint", envName), "grpc://localhost:50051") - if settingType == "app" { - v.Set(fmt.Sprintf("environments.%s.token", envName), "") - } - - // Write configuration - if err := v.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to write setting: %v\n", err) - return - } -} - -// envCmd manages environment switching and listing -var envCmd = &cobra.Command{ - Use: "environment", - Short: "List and manage environments", - Long: "List and manage environments", - Run: func(cmd *cobra.Command, args []string) { - // Set paths for app and user configurations - settingDir := GetSettingDir() - appSettingPath := filepath.Join(settingDir, "setting.toml") - - // Create separate Viper instances - appV := viper.New() - - // Load app configuration - if err := loadSetting(appV, appSettingPath); err != nil { - pterm.Error.Println(err) - return - } - - // Get current environment (from app setting only) - currentEnv := getCurrentEnvironment(appV) - - // Check if -s or -r flag is provided - switchEnv, _ := cmd.Flags().GetString("switch") - removeEnv, _ := cmd.Flags().GetString("remove") - - // Handle environment switching (app setting only) - if switchEnv != "" { - // Check environment in both app and user settings - appEnvMap := appV.GetStringMap("environments") - - if currentEnv == switchEnv { - pterm.Info.Printf("Already in '%s' environment.\n", currentEnv) - return - } - - if _, existsApp := appEnvMap[switchEnv]; !existsApp { - home, _ := os.UserHomeDir() - pterm.Error.Printf("Environment '%s' not found in %s/.cfctl/setting.toml", - switchEnv, home) - return - } - - // Update only the environment field in app setting - appV.Set("environment", switchEnv) - - if err := appV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update environment in setting.toml: %v", err) - return - } - - pterm.Success.Printf("Switched to '%s' environment.\n", switchEnv) - updateGlobalSetting() - return - } - - // Handle environment removal with confirmation - if removeEnv != "" { - // Determine which Viper instance contains the environment - var targetViper *viper.Viper - var targetSettingPath string - envMapApp := appV.GetStringMap("environments") - - if _, exists := envMapApp[removeEnv]; exists { - targetViper = appV - targetSettingPath = appSettingPath - } else { - home, _ := os.UserHomeDir() - pterm.Error.Printf("Environment '%s' not found in %s/.cfctl/setting.toml", - switchEnv, home) - return - } - - // Ask for confirmation before deletion - fmt.Printf("Are you sure you want to delete the environment '%s'? (Y/N): ", removeEnv) - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response == "y" { - // Remove the environment from the environments map - envMap := targetViper.GetStringMap("environments") - delete(envMap, removeEnv) - targetViper.Set("environments", envMap) - - // Write the updated configuration back to the respective setting file - if err := targetViper.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update setting file '%s': %v", targetSettingPath, err) - return - } - - // If the deleted environment was the current one, unset it - if currentEnv == removeEnv { - appV.Set("environment", "") - if err := appV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update environment in setting.toml: %v", err) - return - } - pterm.Info.WithShowLineNumber(false).Println("Cleared current environment in setting.toml") - } - - // Display success message - pterm.Success.Printf("Removed '%s' environment from %s.\n", removeEnv, targetSettingPath) - } else { - pterm.Info.Println("Environment deletion canceled.") - } - return - } - - // Check if the -l flag is provided - listOnly, _ := cmd.Flags().GetBool("list") - - // List environments if the -l flag is set - if listOnly { - // Get environment maps from both app and user settings - appEnvMap := appV.GetStringMap("environments") - - // Map to store all unique environments - allEnvs := make(map[string]bool) - - // Add app environments - for envName := range appEnvMap { - allEnvs[envName] = true - } - - if len(allEnvs) == 0 { - pterm.Println("No environments found in setting file") - return - } - - pterm.Println("Available Environments:") - - // Print environments with their source and current status - for envName := range allEnvs { - if envName == currentEnv { - pterm.FgGreen.Printf("%s (current)\n", envName) - } else { - if _, isApp := appEnvMap[envName]; isApp { - pterm.Printf("%s\n", envName) - } - } - } - return - } - - // If no flags are provided, show help by default - cmd.Help() - }, -} - -// showCmd displays the current cfctl configuration -var showCmd = &cobra.Command{ - Use: "show", - Short: "Display the current cfctl configuration", - Run: func(cmd *cobra.Command, args []string) { - settingDir := GetSettingDir() - appSettingPath := filepath.Join(settingDir, "setting.toml") - userSettingPath := filepath.Join(settingDir, "cache", "setting.toml") - - // Create separate Viper instances - appV := viper.New() - userV := viper.New() - - // Load app configuration - if err := loadSetting(appV, appSettingPath); err != nil { - pterm.Error.Println(err) - return - } - - currentEnv := getCurrentEnvironment(appV) - if currentEnv == "" { - pterm.Sprintf("No environment set in %s\n", appSettingPath) - return - } - - // Try to get the environment from appViper - envSetting := appV.GetStringMap(fmt.Sprintf("environments.%s", currentEnv)) - - // If not found in appViper, try userViper - if len(envSetting) == 0 { - envSetting = userV.GetStringMap(fmt.Sprintf("environments.%s", currentEnv)) - if len(envSetting) == 0 { - pterm.Error.Printf("Environment '%s' not found in %s or %s\n", currentEnv, appSettingPath, userSettingPath) - return - } - } - - output, _ := cmd.Flags().GetString("output") - - switch output { - case "json": - data, err := json.MarshalIndent(envSetting, "", " ") - if err != nil { - log.Fatalf("Error formatting output as JSON: %v", err) - } - fmt.Println(string(data)) - case "toml": - data, err := toml.Marshal(envSetting) - if err != nil { - log.Fatalf("Error formatting output as TOML: %v", err) - } - fmt.Println(string(data)) - default: - log.Fatalf("Unsupported output format: %v", output) - } - }, -} - -// settingEndpointCmd updates the endpoint for the current environment -var settingEndpointCmd = &cobra.Command{ - Use: "endpoint", - Short: "Set the endpoint for the current environment", - Long: `Update the endpoint for the current environment based on the specified service. -If the service is not 'identity', the proxy setting will be updated to false. - -Available Services are fetched dynamically from the backend.`, - Run: func(cmd *cobra.Command, args []string) { - service, _ := cmd.Flags().GetString("service") - if service == "" { - // Create a new Viper instance for app setting - appV := viper.New() - - // Load app configuration - settingPath := filepath.Join(GetSettingDir(), "setting.toml") - appV.SetConfigFile(settingPath) - appV.SetConfigType("toml") - - if err := loadSetting(appV, settingPath); err != nil { - pterm.Error.Println(err) - return - } - - token, err := getToken(appV) - if err != nil { - currentEnv := getCurrentEnvironment(appV) - if strings.HasSuffix(currentEnv, "-app") { - // Parse environment name to extract service name and environment - parts := strings.Split(currentEnv, "-") - if len(parts) >= 3 { - envPrefix := parts[0] // dev, stg - serviceName := parts[1] // cloudone, spaceone, etc. - url := fmt.Sprintf("https://%s.console.%s.spaceone.dev", serviceName, envPrefix) - settingPath := filepath.Join(GetSettingDir(), "setting.toml") - - // Create header for the error message - //pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgRed)).WithMargin(10).Println("Token Not Found") - pterm.DefaultBox. - WithTitle("Token Not Found"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgWhite)). - WithRightPadding(1). - WithLeftPadding(1). - WithTopPadding(0). - WithBottomPadding(0). - Println("Please follow the instructions below to obtain an App Token.") - - // Create a styled box with instructions - boxContent := fmt.Sprintf(`Please follow these steps to obtain an App Token: - -1. Visit %s -2. Go to Admin page or Workspace page -3. Navigate to the App page -4. Click [Create] button -5. Copy the generated App Token -6. Update your settings: - Path: %s - Environment: %s - Field: "token"`, - pterm.FgLightCyan.Sprint(url), - pterm.FgLightYellow.Sprint(settingPath), - pterm.FgLightGreen.Sprint(currentEnv)) - - // Print the box with instructions - pterm.DefaultBox. - WithTitle("Setup Instructions"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgLightBlue)). - // WithTextAlignment(pterm.TextAlignLeft). - Println(boxContent) - - // Print additional help message - pterm.Info.Println("After updating the token, please try your command again.") - - return - } - } else if strings.HasSuffix(currentEnv, "-user") { - pterm.Error.Printf("No token found for environment '%s'. Please run 'cfctl login' to authenticate.\n", currentEnv) - } else { - pterm.Error.Println("Error retrieving token:", err) - } - return - } - - pterm.Error.Println("Please specify a service using -s or --service.") - fmt.Println() - - // Fetch and display available services - baseURL, err := getBaseURL(appV) - if err != nil { - pterm.Error.Println("Error retrieving base URL:", err) - return - } - - services, err := fetchAvailableServices(baseURL, token) - if err != nil { - pterm.Error.Println("Error fetching available services:", err) - return - } - - if len(services) == 0 { - pterm.Println("No available services found.") - return - } - - var formattedServices []string - for _, service := range services { - if service == "identity" { - formattedServices = append(formattedServices, pterm.FgCyan.Sprintf("%s (proxy)", service)) - } else { - formattedServices = append(formattedServices, pterm.FgDefault.Sprint(service)) - } - } - - pterm.DefaultBox.WithTitle("Available Services"). - WithRightPadding(1). - WithLeftPadding(1). - WithTopPadding(0). - WithBottomPadding(0). - Println(strings.Join(formattedServices, "\n")) - return - } - - // Create Viper instances for both app and cache settings - appV := viper.New() - cacheV := viper.New() - - // Load app configuration (for getting current environment) - settingPath := filepath.Join(GetSettingDir(), "setting.toml") - appV.SetConfigFile(settingPath) - appV.SetConfigType("toml") - - // Load cache configuration - cachePath := filepath.Join(GetSettingDir(), "cache", "setting.toml") - cacheV.SetConfigFile(cachePath) - cacheV.SetConfigType("toml") - - if err := loadSetting(appV, settingPath); err != nil { - pterm.Error.Println(err) - return - } - - currentEnv := getCurrentEnvironment(appV) - if currentEnv == "" { - pterm.Error.Println("No environment is set. Please initialize or switch to an environment.") - return - } - - // Determine prefix from the current environment - var prefix string - if strings.HasPrefix(currentEnv, "dev-") { - prefix = "dev" - } else if strings.HasPrefix(currentEnv, "stg-") { - prefix = "stg" - } else { - pterm.Error.Printf("Unsupported environment prefix for '%s'.\n", currentEnv) - return - } - - // Construct new endpoint - newEndpoint := fmt.Sprintf("grpc+ssl://%s.api.%s.spaceone.dev:443", service, prefix) - - // Update the appropriate setting file based on environment type - if strings.HasSuffix(currentEnv, "-app") { - // Update endpoint in main setting for app environments - appV.Set(fmt.Sprintf("environments.%s.endpoint", currentEnv), newEndpoint) - if service != "identity" { - appV.Set(fmt.Sprintf("environments.%s.proxy", currentEnv), false) - } else { - appV.Set(fmt.Sprintf("environments.%s.proxy", currentEnv), true) - } - - if err := appV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update setting.toml: %v\n", err) - return - } - } else { - // Update endpoint in cache setting for user environments - cachePath := filepath.Join(GetSettingDir(), "cache", "setting.toml") - if err := loadSetting(cacheV, cachePath); err != nil { - pterm.Error.Println(err) - return - } - - cacheV.Set(fmt.Sprintf("environments.%s.endpoint", currentEnv), newEndpoint) - if service != "identity" { - cacheV.Set(fmt.Sprintf("environments.%s.proxy", currentEnv), false) - } else { - cacheV.Set(fmt.Sprintf("environments.%s.proxy", currentEnv), true) - } - - if err := cacheV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update cache/setting.toml: %v\n", err) - return - } - } - - pterm.Success.Printf("Updated endpoint for '%s' to '%s'.\n", currentEnv, newEndpoint) - }, -} - -// settingTokenCmd updates the token for the current environment -var settingTokenCmd = &cobra.Command{ - Use: "token [token_value]", - Short: "Set the token for the current environment", - Long: `Update the token for the current environment. -This command only works with app environments (-app suffix).`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - // Load current environment configuration file - settingDir := GetSettingDir() - settingPath := filepath.Join(settingDir, "setting.toml") - - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("toml") - - if err := v.ReadInConfig(); err != nil { - pterm.Error.Printf("Failed to read setting file: %v\n", err) - return - } - - // Get current environment - currentEnv := v.GetString("environment") - if currentEnv == "" { - pterm.Error.Println("No environment is currently selected.") - return - } - - // Check if it's an app environment - if !strings.HasSuffix(currentEnv, "-app") { - pterm.Error.Println("Token can only be set for app environments (-app suffix).") - return - } - - // Update token - tokenKey := fmt.Sprintf("environments.%s.token", currentEnv) - v.Set(tokenKey, args[0]) - - // Save configuration - if err := v.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update token: %v\n", err) - return - } - - pterm.Success.Printf("Token updated for '%s' environment.\n", currentEnv) - }, -} - -// fetchAvailableServices retrieves the list of services by calling the List method on the Endpoint service. -func fetchAvailableServices(endpoint, token string) ([]string, error) { - if !strings.Contains(endpoint, "identity.api") { - parts := strings.Split(endpoint, "://") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) - } - - hostParts := strings.Split(parts[1], ".") - if len(hostParts) < 4 { - return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) - } - env := hostParts[2] - - endpoint = fmt.Sprintf("grpc+ssl://identity.api.%s.spaceone.dev:443", env) - } - - parsedURL, err := url.Parse(endpoint) - if err != nil { - return nil, fmt.Errorf("failed to parse endpoint: %w", err) - } - - host := parsedURL.Hostname() - port := parsedURL.Port() - if port == "" { - port = "443" // Default gRPC port - } - - var opts []grpc.DialOption - - // Set up TLS credentials if the scheme is grpc+ssl:// - if strings.HasPrefix(endpoint, "grpc+ssl://") { - tlsSetting := &tls.Config{ - InsecureSkipVerify: false, // Set to true only if you want to skip TLS verification (not recommended) - } - creds := credentials.NewTLS(tlsSetting) - opts = append(opts, grpc.WithTransportCredentials(creds)) - } else { - return nil, fmt.Errorf("unsupported scheme in endpoint: %s", endpoint) - } - - // Add token-based authentication if a token is provided - if token != "" { - opts = append(opts, grpc.WithPerRPCCredentials(&tokenCreds{token})) - } - - // Establish a connection to the gRPC server - conn, err := grpc.Dial(fmt.Sprintf("%s:%s", host, port), opts...) - if err != nil { - return nil, fmt.Errorf("failed to dial gRPC endpoint: %w", err) - } - defer conn.Close() - - ctx := context.Background() - - // Create a reflection client to discover services and methods - refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // Resolve the service descriptor for "spaceone.api.identity.v2.Endpoint" - serviceName := "spaceone.api.identity.v2.Endpoint" - svcDesc, err := refClient.ResolveService(serviceName) - if err != nil { - return nil, fmt.Errorf("failed to resolve service %s: %w", serviceName, err) - } - - // Resolve the method descriptor for the "List" method - methodName := "list" - methodDesc := svcDesc.FindMethodByName(methodName) - if methodDesc == nil { - return nil, fmt.Errorf("method '%s' not found in service '%s'", methodName, serviceName) - } - - inputType := methodDesc.GetInputType() - if inputType == nil { - return nil, fmt.Errorf("input type not found for method '%s'", methodName) - } - - // Get the request and response message descriptors - reqDesc := methodDesc.GetInputType() - respDesc := methodDesc.GetOutputType() - - // Create a dynamic message for the request - reqMsg := dynamic.NewMessage(reqDesc) - // If ListRequest has required fields, set them here. For example: - // reqMsg.SetField("page_size", 100) - - // Create a dynamic message for the response - respMsg := dynamic.NewMessage(respDesc) - - // Invoke the RPC method - //err = grpc.Invoke(ctx, fmt.Sprintf("/%s/%s", serviceName, methodName), reqMsg, conn, respMsg) - err = conn.Invoke(ctx, fmt.Sprintf("/%s/%s", serviceName, methodName), reqMsg, respMsg) - if err != nil { - return nil, fmt.Errorf("failed to invoke RPC: %w", err) - } - - // Extract the 'results' field from the response message - resultsFieldDesc := respDesc.FindFieldByName("results") - if resultsFieldDesc == nil { - return nil, fmt.Errorf("'results' field not found in response message") - } - - resultsField, err := respMsg.TryGetField(resultsFieldDesc) - if err != nil { - return nil, fmt.Errorf("failed to get 'results' field: %w", err) - } - - // 'results' is expected to be a repeated field (list) of messages - resultsSlice, ok := resultsField.([]interface{}) - if !ok { - return nil, fmt.Errorf("'results' field is not a list") - } - - var availableServices []string - for _, res := range resultsSlice { - // Each item in 'results' should be a dynamic.Message - resMsg, ok := res.(*dynamic.Message) - if !ok { - continue - } - - // Extract the 'service' field from each result message - serviceFieldDesc := resMsg.GetMessageDescriptor().FindFieldByName("service") - if serviceFieldDesc == nil { - continue // Skip if 'service' field is not found - } - - serviceField, err := resMsg.TryGetField(serviceFieldDesc) - if err != nil { - continue // Skip if unable to get the 'service' field - } - - serviceStr, ok := serviceField.(string) - if !ok { - continue // Skip if 'service' field is not a string - } - - availableServices = append(availableServices, serviceStr) - } - - return availableServices, nil -} - -// tokenCreds implements grpc.PerRPCCredentials for token-based authentication. -type tokenCreds struct { - token string -} - -func (t *tokenCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { - return map[string]string{ - "authorization": fmt.Sprintf("Bearer %s", t.token), - }, nil -} - -func (t *tokenCreds) RequireTransportSecurity() bool { - return true -} - -// getBaseURL retrieves the base URL for the current environment from the given Viper instance. -func getBaseURL(v *viper.Viper) (string, error) { - currentEnv := getCurrentEnvironment(v) - if currentEnv == "" { - return "", fmt.Errorf("no environment is set") - } - - baseURL := v.GetString(fmt.Sprintf("environments.%s.endpoint", currentEnv)) - - if baseURL == "" { - return "", fmt.Errorf("no endpoint found for environment '%s' in setting.toml", currentEnv) - - } - - return baseURL, nil -} - -// getToken retrieves the token for the current environment. -func getToken(v *viper.Viper) (string, error) { - currentEnv := getCurrentEnvironment(v) - if currentEnv == "" { - return "", fmt.Errorf("no environment is set") - } - - token := v.GetString(fmt.Sprintf("environments.%s.token", currentEnv)) - if token == "" { - return "", fmt.Errorf("no token found for environment '%s'", currentEnv) - } - return token, nil -} - -// GetSettingDir returns the directory where setting file are stored -func GetSettingDir() string { - home, err := os.UserHomeDir() - if err != nil { - log.Fatalf("Unable to find home directory: %v", err) - } - return filepath.Join(home, ".cfctl") -} - -// loadSetting ensures that the setting directory and setting file exist. -// It initializes the setting file with default values if it does not exist. -func loadSetting(v *viper.Viper, settingPath string) error { - // Ensure the setting directory exists - settingDir := filepath.Dir(settingPath) - if err := os.MkdirAll(settingDir, 0755); err != nil { - return fmt.Errorf("failed to create setting directory '%s': %w", settingDir, err) - } - - // Set the setting file - v.SetConfigFile(settingPath) - v.SetConfigType("toml") - - // Read the setting file - if err := v.ReadInConfig(); err != nil { - if os.IsNotExist(err) { - // Initialize with default values if file doesn't exist - defaultSettings := map[string]interface{}{ - "environments": map[string]interface{}{}, - "environment": "", - } - - // Convert to TOML - data, err := toml.Marshal(defaultSettings) - if err != nil { - return fmt.Errorf("failed to marshal default settings: %w", err) - } - - // Write the default settings to file - if err := os.WriteFile(settingPath, data, 0644); err != nil { - return fmt.Errorf("failed to write default settings: %w", err) - } - - // Read the newly created file - if err := v.ReadInConfig(); err != nil { - return fmt.Errorf("failed to read newly created setting file: %w", err) - } - } else { - return fmt.Errorf("failed to read setting file: %w", err) - } - } - - return nil -} - -// getCurrentEnvironment reads the current environment from the given Viper instance -func getCurrentEnvironment(v *viper.Viper) string { - return v.GetString("environment") -} - -// updateGlobalSetting prints a success message for global setting update -func updateGlobalSetting() { - settingPath := filepath.Join(GetSettingDir(), "setting.toml") - v := viper.New() - - v.SetConfigFile(settingPath) - - if err := v.ReadInConfig(); err != nil { - if os.IsNotExist(err) { - pterm.Success.WithShowLineNumber(false).Printfln("Global setting updated with existing environments. (default: %s/setting.toml)", GetSettingDir()) - return - } - pterm.Warning.Printf("Warning: Could not read global setting: %v\n", err) - return - } - - pterm.Success.WithShowLineNumber(false).Printfln("Global setting updated with existing environments. (default: %s/setting.toml)", GetSettingDir()) -} - -// parseEnvNameFromURL parses environment name from the given URL and validates based on URL structure -func parseEnvNameFromURL(urlStr string) (string, error) { - if !strings.Contains(urlStr, "://") { - urlStr = "https://" + urlStr - } - parsedURL, err := url.Parse(urlStr) - if err != nil { - return "", err - } - hostname := parsedURL.Hostname() - - // Check for `prd` environment pattern - if strings.HasSuffix(hostname, "spaceone.megazone.io") { - re := regexp.MustCompile(`^(.*?)\.spaceone`) - matches := re.FindStringSubmatch(hostname) - if len(matches) == 2 { - return fmt.Sprintf("prd-%s", matches[1]), nil - } - } - - // Check for `dev` environment pattern - if strings.HasSuffix(hostname, "console.dev.spaceone.dev") { - re := regexp.MustCompile(`(.*)\.console\.dev\.spaceone\.dev`) - matches := re.FindStringSubmatch(hostname) - if len(matches) == 2 { - return fmt.Sprintf("dev-%s", matches[1]), nil - } - pterm.Error.WithShowLineNumber(false).Println("Invalid URL format for dev environment. Expected format: '.console.dev.spaceone.dev'") - return "", fmt.Errorf("invalid dev URL format") - } - - // Check for `stg` environment pattern - if strings.HasSuffix(hostname, "console.stg.spaceone.dev") { - re := regexp.MustCompile(`(.*)\.console\.stg\.spaceone\.dev`) - matches := re.FindStringSubmatch(hostname) - if len(matches) == 2 { - return fmt.Sprintf("stg-%s", matches[1]), nil - } - pterm.Error.WithShowLineNumber(false).Println("Invalid URL format for stg environment. Expected format: '.console.stg.spaceone.dev'") - return "", fmt.Errorf("invalid stg URL format") - } - - return "", fmt.Errorf("URL does not match any known environment patterns") -} - -// updateSetting updates the configuration files -func updateSetting(envName, urlStr, settingType string) { - settingDir := GetSettingDir() - mainSettingPath := filepath.Join(settingDir, "setting.toml") - - // Initialize main viper instance - mainV := viper.New() - mainV.SetConfigFile(mainSettingPath) - mainV.SetConfigType("toml") - - // Read existing configuration file - if err := mainV.ReadInConfig(); err != nil { - if !os.IsNotExist(err) { - pterm.Error.Printf("Error reading setting file: %v\n", err) - return - } - } - - // Initialize environments if not exists - if !mainV.IsSet("environments") { - mainV.Set("environments", make(map[string]interface{})) - } - - if urlStr != "" { - endpoint, err := constructEndpoint(urlStr) - if err != nil { - pterm.Error.Printf("Failed to construct endpoint: %v\n", err) - return - } - - // Append -app or -user to the environment name - envName = fmt.Sprintf("%s-%s", envName, settingType) - - // Get environments map - environments := mainV.GetStringMap("environments") - if environments == nil { - environments = make(map[string]interface{}) - } - - // Add new environment configuration - envConfig := map[string]interface{}{ - "endpoint": endpoint, - "proxy": true, - } - - // Only add token field for app configuration - if settingType == "app" { - envConfig["token"] = "" - } - - environments[envName] = envConfig - - // Update entire configuration - mainV.Set("environments", environments) - mainV.Set("environment", envName) - - // Save configuration file - if err := mainV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to write setting: %v\n", err) - return - } - } - - pterm.Success.Printf("Environment '%s' successfully initialized.\n", envName) -} - -// convertToStringMap converts map[interface{}]interface{} to map[string]interface{} -func convertToStringMap(m map[interface{}]interface{}) map[string]interface{} { - result := make(map[string]interface{}) - for k, v := range m { - switch v := v.(type) { - case map[interface{}]interface{}: - result[k.(string)] = convertToStringMap(v) - case []interface{}: - result[k.(string)] = convertToSlice(v) - default: - result[k.(string)] = v - } - } - return result -} - -// convertToSlice handles slice conversion if needed -func convertToSlice(s []interface{}) []interface{} { - result := make([]interface{}, len(s)) - for i, v := range s { - switch v := v.(type) { - case map[interface{}]interface{}: - result[i] = convertToStringMap(v) - case []interface{}: - result[i] = convertToSlice(v) - default: - result[i] = v - } - } - return result -} - -// constructEndpoint generates the gRPC endpoint string from baseURL -func constructEndpoint(baseURL string) (string, error) { - if !strings.Contains(baseURL, "://") { - baseURL = "https://" + baseURL - } - parsedURL, err := url.Parse(baseURL) - if err != nil { - return "", err - } - hostname := parsedURL.Hostname() - - prefix := "" - // Determine the prefix based on the hostname - switch { - case strings.Contains(hostname, ".dev.spaceone.dev"): - prefix = "dev" - case strings.Contains(hostname, ".stg.spaceone.dev"): - prefix = "stg" - // TODO: After set up production - default: - return "", fmt.Errorf("unknown environment prefix in URL: %s", hostname) - } - - // Extract the service from the hostname - service := strings.Split(hostname, ".")[0] - if service == "" { - return "", fmt.Errorf("unable to determine service from URL: %s", hostname) - } - - // Construct the endpoint dynamically based on the service - newEndpoint := fmt.Sprintf("grpc+ssl://identity.api.%s.spaceone.dev:443", prefix) - return newEndpoint, nil -} - -func init() { - SettingCmd.AddCommand(settingInitCmd) - SettingCmd.AddCommand(envCmd) - SettingCmd.AddCommand(showCmd) - SettingCmd.AddCommand(settingEndpointCmd) - SettingCmd.AddCommand(settingTokenCmd) - settingInitCmd.AddCommand(settingInitURLCmd) - settingInitCmd.AddCommand(settingInitLocalCmd) - - settingInitCmd.Flags().StringP("environment", "e", "", "Override environment name") - - settingInitURLCmd.Flags().StringP("url", "u", "", "URL for the environment") - settingInitURLCmd.Flags().Bool("app", false, "Initialize as application configuration") - settingInitURLCmd.Flags().Bool("user", false, "Initialize as user-specific configuration") - - settingInitLocalCmd.Flags().StringP("name", "n", "", "Local environment name for the environment") - settingInitLocalCmd.Flags().Bool("app", false, "Initialize as application configuration") - settingInitLocalCmd.Flags().Bool("user", false, "Initialize as user-specific configuration") - settingInitLocalCmd.Flags().Bool("dev", false, "Initialize as development environment") - settingInitLocalCmd.Flags().Bool("stg", false, "Initialize as staging environment") - settingInitLocalCmd.Flags().Bool("plugin", false, "Initialize as plugin configuration") - - envCmd.Flags().StringP("switch", "s", "", "Switch to a different environment") - envCmd.Flags().StringP("remove", "r", "", "Remove an environment") - envCmd.Flags().BoolP("list", "l", false, "List available environments") - - showCmd.Flags().StringP("output", "o", "toml", "Output format (toml/json)") - - settingEndpointCmd.Flags().StringP("service", "s", "", "Service to set the endpoint for") - - // No need to set global Viper setting type since we are using separate instances -} diff --git a/cmd/other/shortNames.go b/cmd/other/shortNames.go deleted file mode 100644 index cc0f4f6..0000000 --- a/cmd/other/shortNames.go +++ /dev/null @@ -1,305 +0,0 @@ -package other - -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", - Short: "Manage short names for commands", - Long: `Manage short names for frequently used commands.`, -} - -// validateServiceCommand checks if the given verb and resource are valid for the service -func validateServiceCommand(service, verb, resource string) error { - // Get current environment from main setting file - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - mainV := viper.New() - mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.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 -s inventory add -n job -c "list Job" - - Then use them as: - $ cfctl inventory job # This command is same as $ cfctl inventory list Job`, - Run: func(cmd *cobra.Command, args []string) { - // Show example if no flags are provided - if !cmd.Flags().Changed("name") || !cmd.Flags().Changed("command") || !cmd.Flags().Changed("service") { - pterm.DefaultBox. - WithTitle("Short Name Examples"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgLightBlue)). - Println(`Example: - $ cfctl short_name -s inventory add -n job -c "list Job" - -Then use them as: - $ cfctl inventory job # This command is same as $ cfctl inventory list Job`) - return - } - - shortName, _ := cmd.Flags().GetString("name") - command, _ := cmd.Flags().GetString("command") - service, _ := cmd.Flags().GetString("service") - - // Parse command to get verb and resource - parts := strings.Fields(command) - if len(parts) < 2 { - pterm.Error.Printf("Invalid command format. Expected ' ', got '%s'\n", command) - return - } - - verb := parts[0] - resource := parts[1] - - // Validate the command - if err := validateServiceCommand(service, verb, resource); err != nil { - pterm.Error.Printf("Invalid command: %v\n", err) - return - } - - if err := addShortName(service, shortName, command); err != nil { - pterm.Error.Printf("Failed to add short name: %v\n", err) - return - } - - pterm.Success.Printf("Successfully added short name '%s' for service '%s' command '%s'\n", shortName, service, command) - }, -} - -var removeShortNameCmd = &cobra.Command{ - Use: "remove", - Short: "Remove a short name", - Run: func(cmd *cobra.Command, args []string) { - shortName, err := cmd.Flags().GetString("name") - service, _ := cmd.Flags().GetString("service") - if err != nil || shortName == "" || service == "" { - pterm.Error.Println("The --name (-n) and --service (-s) flags are required") - cmd.Help() - return - } - - if err := removeShortName(service, shortName); err != nil { - pterm.Error.Printf("Failed to remove short name: %v\n", err) - return - } - - pterm.Success.Printf("Successfully removed short name '%s' from service '%s'\n", shortName, service) - }, -} - -var listShortNameCmd = &cobra.Command{ - Use: "list", - Short: "List all short names", - Run: func(cmd *cobra.Command, args []string) { - shortNames, err := listShortNames() - if err != nil { - pterm.Error.Printf("Failed to list short names: %v\n", err) - return - } - - if len(shortNames) == 0 { - pterm.Info.Println("No short names found") - return - } - - // Create table - table := pterm.TableData{ - {"Service", "Short Name", "Command"}, - } - - // Add short names to table - for service, serviceShortNames := range shortNames { - for name, command := range serviceShortNames.(map[string]interface{}) { - table = append(table, []string{service, name, command.(string)}) - } - } - - // Print table - pterm.DefaultTable.WithHasHeader().WithData(table).Render() - }, -} - -func addShortName(service, shortName, command string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.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.%s", service, shortName), command) - - if err := v.WriteConfig(); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -func removeShortName(service, shortName string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.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 service and short name exist - if !v.IsSet(fmt.Sprintf("short_names.%s.%s", service, shortName)) { - return fmt.Errorf("short name '%s' not found in service '%s'", shortName, service) - } - - // Get all short names for the service - serviceShortNames := v.GetStringMap(fmt.Sprintf("short_names.%s", service)) - delete(serviceShortNames, shortName) - - // Update config with removed short name - v.Set(fmt.Sprintf("short_names.%s", service), serviceShortNames) - - if err := v.WriteConfig(); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -func listShortNames() (map[string]interface{}, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.toml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("toml") - - if err := v.ReadInConfig(); err != nil { - if os.IsNotExist(err) { - return make(map[string]interface{}), nil - } - return nil, fmt.Errorf("failed to read config: %v", err) - } - - shortNames := v.GetStringMap("short_names") - if shortNames == nil { - return make(map[string]interface{}), nil - } - - return shortNames, nil -} - -func init() { - ShortNameCmd.AddCommand(addShortNameCmd) - ShortNameCmd.AddCommand(removeShortNameCmd) - ShortNameCmd.AddCommand(listShortNameCmd) - - ShortNameCmd.PersistentFlags().StringVarP(&service, "service", "s", "", "Service to manage short names for") - addShortNameCmd.MarkPersistentFlagRequired("service") - removeShortNameCmd.MarkPersistentFlagRequired("service") - - addShortNameCmd.Flags().StringP("name", "n", "", "Short name to add") - addShortNameCmd.Flags().StringP("command", "c", "", "Command to execute") - addShortNameCmd.MarkFlagRequired("name") - addShortNameCmd.MarkFlagRequired("command") - - removeShortNameCmd.Flags().StringP("name", "n", "", "Short name to remove") - removeShortNameCmd.MarkFlagRequired("name") -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index be74c50..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,588 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/jhump/protoreflect/grpcreflect" - "github.com/spf13/viper" - - "github.com/cloudforet-io/cfctl/cmd/common" - "github.com/cloudforet-io/cfctl/cmd/other" - - "github.com/BurntSushi/toml" - "github.com/pterm/pterm" - "github.com/spf13/cobra" - "google.golang.org/grpc" - "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" -) - -var cfgFile string -var cachedEndpointsMap map[string]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", - Short: "cfctl controls the SpaceONE services.", - Long: `cfctl controls the SpaceONE services. - Find more information at: - - https://github.com/cloudforet-io/cfctl - - https://docs.spaceone.megazone.io/developers/setup/cfctl (English) - - https://docs.spaceone.megazone.io/ko/developers/setup/cfctl (Korean)`, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - args := os.Args[1:] - - if len(args) > 1 { - // Check if the first argument is a service name and second is a short name - v := viper.New() - if home, err := os.UserHomeDir(); err == nil { - settingPath := filepath.Join(home, ".cfctl", "setting.toml") - v.SetConfigFile(settingPath) - v.SetConfigType("toml") - - if err := v.ReadInConfig(); err == nil { - serviceName := args[0] - shortName := args[1] - if command := v.GetString(fmt.Sprintf("short_names.%s.%s", serviceName, shortName)); command != "" { - // Replace the short name with the actual command - newArgs := append([]string{args[0]}, strings.Fields(command)...) - newArgs = append(newArgs, args[2:]...) - os.Args = append([]string{os.Args[0]}, newArgs...) - } - } - } - } - - if err := rootCmd.Execute(); err != nil { - os.Exit(1) - } -} - -func init() { - // Initialize available commands group - AvailableCommands := &cobra.Group{ - ID: "available", - Title: "Available Commands:", - } - rootCmd.AddGroup(AvailableCommands) - - go func() { - if _, err := loadCachedEndpoints(); err == nil { - return - } - }() - - if len(os.Args) > 1 && os.Args[1] == "__complete" { - pterm.DisableColor() - } - - // Determine if the current command is 'setting environment -l' - skipDynamicCommands := false - if len(os.Args) >= 3 && os.Args[1] == "setting" && os.Args[2] == "environment" { - for _, arg := range os.Args[3:] { - if arg == "-l" || arg == "--list" { - skipDynamicCommands = true - break - } - } - } - - if !skipDynamicCommands { - if err := addDynamicServiceCommands(); err != nil { - showInitializationGuide(err) - } - } - - // Initialize other commands group - OtherCommands := &cobra.Group{ - ID: "other", - Title: "Other Commands:", - } - rootCmd.AddGroup(OtherCommands) - 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() { - 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) { - // Skip showing guide for certain commands - if len(os.Args) >= 2 && (os.Args[1] == "setting" || - os.Args[1] == "login" || - os.Args[1] == "api-resources") { - return - } - - // Get current environment from setting file - home, err := os.UserHomeDir() - if err != nil { - pterm.Error.Printf("Unable to find home directory: %v\n", err) - return - } - - settingFile := filepath.Join(home, ".cfctl", "setting.toml") - mainV := viper.New() - mainV.SetConfigFile(settingFile) - mainV.SetConfigType("toml") - - if err := mainV.ReadInConfig(); err != nil { - pterm.Warning.Printf("No valid configuration found.\n") - pterm.Info.Println("Please run 'cfctl setting init' to set up your configuration.") - return - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - pterm.Warning.Printf("No environment selected.\n") - pterm.Info.Println("Please run 'cfctl setting init' to set up your configuration.") - return - } - - // Check if token exists for the current environment - envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) - if envConfig != nil && envConfig.GetString("token") != "" { - // Token exists, no need to show guide - return - } - - // Parse environment name to extract service name and environment - parts := strings.Split(currentEnv, "-") - if len(parts) >= 3 { - var url string - if parts[0] == "local" { - if len(parts) >= 4 { - envPrefix := parts[1] // dev - serviceName := parts[2] // cloudone - url = fmt.Sprintf("https://%s.console.%s.spaceone.dev\n"+ - " Note: If you're running a local console server,\n"+ - " you can also access it at http://localhost:8080", serviceName, envPrefix) - } - } else { - envPrefix := parts[0] // dev - serviceName := parts[1] // cloudone - url = fmt.Sprintf("https://%s.console.%s.spaceone.dev", serviceName, envPrefix) - } - - if strings.HasSuffix(currentEnv, "-app") { - pterm.DefaultBox. - WithTitle("Token Not Found"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgWhite)). - WithRightPadding(1). - WithLeftPadding(1). - WithTopPadding(0). - WithBottomPadding(0). - Println("Please follow the instructions below to obtain an App Token.") - - boxContent := fmt.Sprintf(`Please follow these steps to obtain an App Token: - -1. Visit %s -2. Go to Admin page or Workspace page -3. Navigate to the App page -4. Click [Create] button -5. Copy the generated App Token -6. Update your settings: - Path: %s - Environment: %s - Field: "token"`, - pterm.FgLightCyan.Sprint(url), - pterm.FgLightYellow.Sprint(settingFile), - pterm.FgLightGreen.Sprint(currentEnv)) - - pterm.DefaultBox. - WithTitle("Setup Instructions"). - WithTitleTopCenter(). - WithBoxStyle(pterm.NewStyle(pterm.FgLightBlue)). - Println(boxContent) - - pterm.Info.Println("After updating the token, please try your command again.") - } else { - pterm.Warning.Printf("Authentication required.\n") - pterm.Info.Println("To see Available Commands, please authenticate first:") - pterm.Info.Println("$ cfctl login") - } - } -} - -func addDynamicServiceCommands() error { - // If we already have in-memory cache, use it - if cachedEndpointsMap != nil { - for serviceName := range cachedEndpointsMap { - cmd := createServiceCommand(serviceName) - rootCmd.AddCommand(cmd) - } - return nil - } - - // Load configuration - setting, err := loadConfig() - if err != nil { - return fmt.Errorf("failed to load setting: %v", err) - } - - // Handle local environment - if strings.HasPrefix(setting.Environment, "local-") { - // Try connecting to local gRPC server - conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(2*time.Second)) - if err != nil { - pterm.Error.Printf("Cannot connect to local gRPC server (grpc://localhost:50051)\n") - pterm.Info.Println("Please check if your gRPC server is running") - return fmt.Errorf("local gRPC server connection failed: %v", err) - } - defer conn.Close() - - // Create reflection client - ctx := context.Background() - refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) - defer refClient.Reset() - - // List all services - services, err := refClient.ListServices() - if err != nil { - return fmt.Errorf("failed to list local services: %v", err) - } - - endpointsMap := make(map[string]string) - for _, svc := range services { - if strings.HasPrefix(svc, "spaceone.api.") { - parts := strings.Split(svc, ".") - if len(parts) >= 4 { - serviceName := parts[2] - // Skip core service - if serviceName != "core" { - endpointsMap[serviceName] = "grpc://localhost:50051" - } - } - } - } - - // Store in both memory and file cache - cachedEndpointsMap = endpointsMap - if err := saveEndpointsCache(endpointsMap); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to cache endpoints: %v\n", err) - } - - // Create commands for each service - for serviceName := range endpointsMap { - cmd := createServiceCommand(serviceName) - rootCmd.AddCommand(cmd) - } - - return nil - } - - // Continue with existing logic for non-local environments - endpoint := setting.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] - endpoint = fmt.Sprintf("grpc+ssl://identity.api.%s.spaceone.dev:443", env) - } - } - } - - endpointsMap, err := other.FetchEndpointsMap(endpoint) - if err != nil { - return fmt.Errorf("failed to fetch services: %v", err) - } - - cachedEndpointsMap = endpointsMap - if err := saveEndpointsCache(endpointsMap); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to cache endpoints: %v\n", err) - } - - for serviceName := range endpointsMap { - cmd := createServiceCommand(serviceName) - rootCmd.AddCommand(cmd) - } - - return nil -} - -func clearEndpointsCache() { - home, err := os.UserHomeDir() - if err != nil { - return - } - - mainV := viper.New() - mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.toml")) - mainV.SetConfigType("toml") - if err := mainV.ReadInConfig(); err != nil { - return - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - return - } - - // Remove environment-specific cache directory - envCacheDir := filepath.Join(home, ".cfctl", "cache", currentEnv) - os.RemoveAll(envCacheDir) - cachedEndpointsMap = nil -} - -func loadCachedEndpoints() (map[string]string, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, err - } - - // Get current environment from main setting file - mainV := viper.New() - mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.toml")) - mainV.SetConfigType("toml") - if err := mainV.ReadInConfig(); err != nil { - return nil, err - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - return nil, fmt.Errorf("no environment set") - } - - // Create environment-specific cache directory - envCacheDir := filepath.Join(home, ".cfctl", "cache", currentEnv) - - cacheFile := filepath.Join(envCacheDir, "endpoints.toml") - data, err := os.ReadFile(cacheFile) - if err != nil { - 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") - } - - var endpoints map[string]string - if err := toml.Unmarshal(data, &endpoints); err != nil { - return nil, err - } - - return endpoints, nil -} - -func saveEndpointsCache(endpoints map[string]string) error { - home, err := os.UserHomeDir() - if err != nil { - return err - } - - // Get current environment from main setting file - mainV := viper.New() - mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.toml")) - mainV.SetConfigType("toml") - if err := mainV.ReadInConfig(); err != nil { - return err - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - return fmt.Errorf("no environment set") - } - - // Create environment-specific cache directory - envCacheDir := filepath.Join(home, ".cfctl", "cache", currentEnv) - if err := os.MkdirAll(envCacheDir, 0755); err != nil { - return err - } - - data, err := toml.Marshal(endpoints) - if err != nil { - return err - } - - return os.WriteFile(filepath.Join(envCacheDir, "endpoints.toml"), data, 0644) -} - -// loadConfig loads configuration from both main and cache setting files -func loadConfig() (*Config, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("unable to find home directory: %v", err) - } - - settingFile := filepath.Join(home, ".cfctl", "setting.toml") - - // Read main setting file - mainV := viper.New() - mainV.SetConfigFile(settingFile) - mainV.SetConfigType("toml") - if err := mainV.ReadInConfig(); err != nil { - return nil, fmt.Errorf("failed to read setting file") - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - return nil, fmt.Errorf("no environment set") - } - - // 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") - } - - 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{ - 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. - -%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 - if len(args) == 0 { - common.PrintAvailableVerbs(cmd) - return nil - } - - // 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 - }, - } - - 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 -} diff --git a/go.mod b/go.mod index c192451..2707596 100644 --- a/go.mod +++ b/go.mod @@ -1,63 +1,3 @@ module github.com/cloudforet-io/cfctl go 1.23.1 - -require ( - github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/BurntSushi/toml v1.4.0 - github.com/atotto/clipboard v0.1.4 - github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 - github.com/jhump/protoreflect v1.17.0 - github.com/pelletier/go-toml/v2 v2.2.2 - github.com/pterm/pterm v0.12.79 - github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 - github.com/zalando/go-keyring v0.2.6 - google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.35.1 - gopkg.in/yaml.v2 v2.2.8 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - al.essio.dev/pkg/shellescape v1.5.1 // indirect - atomicgo.dev/cursor v0.2.0 // indirect - atomicgo.dev/keyboard v0.2.9 // indirect - atomicgo.dev/schedule v0.1.0 // indirect - github.com/bufbuild/protocompile v0.14.1 // indirect - github.com/containerd/console v1.0.3 // indirect - github.com/danieljoos/wincred v1.2.2 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/gookit/color v1.5.4 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/lithammer/fuzzysearch v1.1.8 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/ini.v1 v1.67.0 // indirect -) diff --git a/go.sum b/go.sum index 588c243..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,235 +0,0 @@ -al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= -al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= -atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= -atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= -atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= -atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= -atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= -atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= -atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= -github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= -github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= -github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= -github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= -github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= -github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= -github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= -github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= -github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= -github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= -github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= -github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= -github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= -github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= -github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= -github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= -github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= -github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= -github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= -github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= -github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= -github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= -github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= -github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go deleted file mode 100644 index 2e1fc98..0000000 --- a/main.go +++ /dev/null @@ -1,11 +0,0 @@ -/* -Copyright © 2024 NAME HERE - -*/ -package main - -import "github.com/cloudforet-io/cfctl/cmd" - -func main() { - cmd.Execute() -} From 930d05767c712fdbed33ae9cde82d95232d0c5ff Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 12 Dec 2024 17:24:19 +0900 Subject: [PATCH 2/3] feat: initialize cfctl using cobra-cli and viper Signed-off-by: Youngjin Jo --- LICENSE | 1 + cmd/root.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 28 +++++++++++++++++++ go.sum | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 22 +++++++++++++++ 5 files changed, 202 insertions(+) create mode 100644 cmd/root.go create mode 100644 main.go diff --git a/LICENSE b/LICENSE index 261eeb9..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..0ac826e --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,74 @@ +// Package cmd /* +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "cfctl", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// 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 { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cfctl.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".cfctl" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".cfctl") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/go.mod b/go.mod index 2707596..9fb1840 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,31 @@ module github.com/cloudforet-io/cfctl go 1.23.1 + +require ( + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index e69de29..aba9163 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,77 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a73f8e4 --- /dev/null +++ b/main.go @@ -0,0 +1,22 @@ +/* +Copyright © 2024 cloudforet-io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import "github.com/cloudforet-io/cfctl/cmd" + +func main() { + cmd.Execute() +} From e790c878339861683a16fef59018b04b89a9ca94 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 12 Dec 2024 18:13:49 +0900 Subject: [PATCH 3/3] feat: add setting subcommand Signed-off-by: Youngjin Jo --- cmd/other/setting.go | 27 +++++++++++++++++++++++++++ cmd/root.go | 31 +++++++++++++++++-------------- 2 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 cmd/other/setting.go diff --git a/cmd/other/setting.go b/cmd/other/setting.go new file mode 100644 index 0000000..dc6f9d6 --- /dev/null +++ b/cmd/other/setting.go @@ -0,0 +1,27 @@ +// Package other /* +package other + +import ( + "github.com/spf13/cobra" +) + +// SettingCmd represents the setting command +var SettingCmd = &cobra.Command{ + Use: "setting", + Short: "Manage cfctl setting file ($HOME/.cfctl/setting.yaml)", + Long: `Manage setting file for cfctl. You can initialize, +switch environments, and display the current configuration.`, + Run: func(cmd *cobra.Command, args []string) {}, +} + +func init() { + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // settingCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // settingCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/root.go b/cmd/root.go index 0ac826e..97ad29a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,22 +5,23 @@ import ( "fmt" "os" + "github.com/cloudforet-io/cfctl/cmd/other" + "github.com/spf13/cobra" "github.com/spf13/viper" ) var cfgFile string -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ Use: "cfctl", - Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, + Short: "cfctl controls the SpaceONE services.", + Long: `cfctl controls the SpaceONE services. + Find more information at: + - https://github.com/cloudforet-io/cfctl + - https://docs.spaceone.megazone.io/developers/setup/cfctl (English) + - https://docs.spaceone.megazone.io/ko/developers/setup/cfctl (Korean)`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, @@ -29,7 +30,7 @@ to quickly create a Cobra application.`, // 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() + err := RootCmd.Execute() if err != nil { os.Exit(1) } @@ -42,11 +43,10 @@ func init() { // Cobra supports persistent flags, which, if defined here, // will be global for your application. - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cfctl.yaml)") - // Cobra also supports local flags, which will only run // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + RootCmd.AddCommand(other.SettingCmd) } // initConfig reads in config file and ENV variables if set. @@ -69,6 +69,9 @@ func initConfig() { // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + _, err := fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + if err != nil { + return + } } }