diff --git a/cmd/commands/api_resources.go b/cmd/common/api_resources.go similarity index 95% rename from cmd/commands/api_resources.go rename to cmd/common/api_resources.go index 6375f53..9cb5ce4 100644 --- a/cmd/commands/api_resources.go +++ b/cmd/common/api_resources.go @@ -1,4 +1,4 @@ -package commands +package common import ( "context" @@ -12,7 +12,6 @@ import ( "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/cloudforet-io/cfctl/pkg/format" - "github.com/cloudforet-io/cfctl/pkg/transport" "github.com/jhump/protoreflect/grpcreflect" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -41,7 +40,7 @@ func ListAPIResources(serviceName string) error { } //endpoint, err := getServiceEndpoint(setting, serviceName) - endpoint, err := transport.GetServiceEndpoint(setting, serviceName) + endpoint, err := configs.GetServiceEndpoint(setting, serviceName) if err != nil { return fmt.Errorf("failed to get endpoint for service %s: %v", serviceName, err) } @@ -51,7 +50,7 @@ func ListAPIResources(serviceName string) error { return fmt.Errorf("failed to load short names: %v", err) } - data, err := fetchServiceResources(serviceName, endpoint, shortNamesMap, setting) + data, err := FetchServiceResources(serviceName, endpoint, shortNamesMap, setting) if err != nil { return fmt.Errorf("failed to fetch resources for service %s: %v", serviceName, err) } @@ -87,7 +86,7 @@ func loadShortNames() (map[string]string, error) { return shortNamesMap, nil } -func fetchServiceResources(serviceName, endpoint string, shortNamesMap map[string]string, config *configs.Setting) ([][]string, error) { +func FetchServiceResources(serviceName, endpoint string, shortNamesMap map[string]string, config *configs.Setting) ([][]string, error) { parts := strings.Split(endpoint, "://") if len(parts) != 2 { return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) diff --git a/cmd/other/alias.go b/cmd/other/alias.go index 2a83174..1e80215 100644 --- a/cmd/other/alias.go +++ b/cmd/other/alias.go @@ -1,20 +1,14 @@ package other import ( - "fmt" - "os" - "path/filepath" "strings" - "github.com/cloudforet-io/cfctl/pkg/transport" + "github.com/cloudforet-io/cfctl/pkg/configs" + "github.com/cloudforet-io/cfctl/pkg/format" "github.com/pterm/pterm" "github.com/spf13/cobra" - "github.com/spf13/viper" - "gopkg.in/yaml.v3" ) -var service string - // AliasCmd represents the alias command var AliasCmd = &cobra.Command{ Use: "alias", @@ -25,57 +19,54 @@ var AliasCmd = &cobra.Command{ var addAliasCmd = &cobra.Command{ Use: "add", Short: "Add a new alias", - Example: ` $ cfctl alias add -k user -v "identity list User" + Example: ` $ cfctl alias add -s identity -k user -v "list User" Then use it as: - $ cfctl user # This command is same as $ cfctl identity list User`, + $ cfctl identity user # This command is same as $ cfctl identity list User`, Run: func(cmd *cobra.Command, args []string) { + service, _ := cmd.Flags().GetString("service") key, _ := cmd.Flags().GetString("key") value, _ := cmd.Flags().GetString("value") // Parse command to validate parts := strings.Fields(value) - if len(parts) < 3 { - pterm.Error.Printf("Invalid command format. Expected ' ', got '%s'\n", value) + if len(parts) < 2 { + pterm.Error.Printf("Invalid command format. Expected ' ', got '%s'\n", value) return } - service := parts[0] - verb := parts[1] - resource := parts[2] + verb := parts[0] + resource := parts[1] - if err := validateServiceCommand(service, verb, resource); err != nil { + if err := format.ValidateServiceCommand(service, verb, resource); err != nil { pterm.Error.Printf("Invalid command: %v\n", err) return } - if err := addAlias(key, value); err != nil { + if err := configs.AddAlias(service, key, value); err != nil { pterm.Error.Printf("Failed to add alias: %v\n", err) return } - pterm.Success.Printf("Successfully added alias '%s' for command '%s'\n", key, value) + pterm.Success.Printf("Successfully added alias '%s' for command '%s' in service '%s'\n", key, value, service) }, } var removeAliasCmd = &cobra.Command{ - Use: "remove", - Short: "Remove an alias", - Example: ` $ cfctl alias remove -k user`, + Use: "remove", + Short: "Remove an alias", + Example: ` # Remove an alias from a specific service + $ cfctl alias remove -s identity -k user`, Run: func(cmd *cobra.Command, args []string) { + service, _ := cmd.Flags().GetString("service") key, _ := cmd.Flags().GetString("key") - if key == "" { - pterm.Error.Println("The --key (-k) flag is required") - cmd.Help() - return - } - if err := removeAlias(key); err != nil { + if err := configs.RemoveAlias(service, key); err != nil { pterm.Error.Printf("Failed to remove alias: %v\n", err) return } - pterm.Success.Printf("Successfully removed alias '%s'\n", key) + pterm.Success.Printf("Successfully removed alias '%s' from service '%s'\n", key, service) }, } @@ -83,7 +74,7 @@ var listAliasCmd = &cobra.Command{ Use: "list", Short: "List all aliases", Run: func(cmd *cobra.Command, args []string) { - aliases, err := ListAliases() + aliases, err := configs.ListAliases() if err != nil { pterm.Error.Printf("Failed to list aliases: %v\n", err) return @@ -96,12 +87,18 @@ var listAliasCmd = &cobra.Command{ // Create table table := pterm.TableData{ - {"Alias", "Command"}, + {"Service", "Alias", "Command"}, } // Add aliases to table - for alias, command := range aliases { - table = append(table, []string{alias, command}) + for service, serviceAliases := range aliases { + if serviceMap, ok := serviceAliases.(map[string]interface{}); ok { + for alias, command := range serviceMap { + if cmdStr, ok := command.(string); ok { + table = append(table, []string{service, alias, cmdStr}) + } + } + } } // Print table @@ -109,216 +106,20 @@ var listAliasCmd = &cobra.Command{ }, } -// validateServiceCommand checks if the given verb and resource are valid for the service -func validateServiceCommand(service, verb, resource string) error { - // Get current environment from main setting file - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - mainV := viper.New() - mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.yaml")) - mainV.SetConfigType("yaml") - if err := mainV.ReadInConfig(); err != nil { - return fmt.Errorf("failed to read config: %v", err) - } - - currentEnv := mainV.GetString("environment") - if currentEnv == "" { - return fmt.Errorf("no environment set") - } - - // Get environment config - envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) - if envConfig == nil { - return fmt.Errorf("environment %s not found", currentEnv) - } - - endpoint := envConfig.GetString("endpoint") - if endpoint == "" { - return fmt.Errorf("no endpoint found in configuration") - } - - endpoint, _ = transport.GetAPIEndpoint(endpoint) - - // Fetch endpoints map - endpointsMap, err := transport.FetchEndpointsMap(endpoint) - if err != nil { - return fmt.Errorf("failed to fetch endpoints: %v", err) - } - - // Check if service exists - serviceEndpoint, ok := endpointsMap[service] - if !ok { - return fmt.Errorf("service '%s' not found", service) - } - - // Fetch service resources - resources, err := fetchServiceResources(service, serviceEndpoint, nil) - if err != nil { - return fmt.Errorf("failed to fetch service resources: %v", err) - } - - // Find the resource and check if the verb is valid - resourceFound := false - verbFound := false - - for _, row := range resources { - if row[2] == resource { - resourceFound = true - verbs := strings.Split(row[1], ", ") - 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 -} - -func addAlias(key, value string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - - data, err := os.ReadFile(settingPath) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read config: %v", err) - } - - var config map[string]interface{} - if err := yaml.Unmarshal(data, &config); err != nil { - return fmt.Errorf("failed to parse config: %v", err) - } - - aliases, ok := config["aliases"].(map[string]interface{}) - if !ok { - aliases = make(map[string]interface{}) - } - - delete(config, "aliases") - aliases[key] = value - - newData, err := yaml.Marshal(config) - if err != nil { - return fmt.Errorf("failed to encode config: %v", err) - } - - aliasData, err := yaml.Marshal(map[string]interface{}{"aliases": aliases}) - if err != nil { - return fmt.Errorf("failed to encode aliases: %v", err) - } - - finalData := append(newData, aliasData...) - - if err := os.WriteFile(settingPath, finalData, 0644); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -// Function to remove an alias -func removeAlias(key string) error { - home, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - - data, err := os.ReadFile(settingPath) - if err != nil { - return fmt.Errorf("failed to read config: %v", err) - } - - var config map[string]interface{} - if err := yaml.Unmarshal(data, &config); err != nil { - return fmt.Errorf("failed to parse config: %v", err) - } - - aliases, ok := config["aliases"].(map[string]interface{}) - if !ok || aliases[key] == nil { - return fmt.Errorf("alias '%s' not found", key) - } - - delete(aliases, key) - delete(config, "aliases") - - // YAML 인코딩 - newData, err := yaml.Marshal(config) - if err != nil { - return fmt.Errorf("failed to encode config: %v", err) - } - - if len(aliases) > 0 { - aliasData, err := yaml.Marshal(map[string]interface{}{"aliases": aliases}) - if err != nil { - return fmt.Errorf("failed to encode aliases: %v", err) - } - newData = append(newData, aliasData...) - } - - if err := os.WriteFile(settingPath, newData, 0644); err != nil { - return fmt.Errorf("failed to write config: %v", err) - } - - return nil -} - -func ListAliases() (map[string]string, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil { - if os.IsNotExist(err) { - return make(map[string]string), nil - } - return nil, fmt.Errorf("failed to read config: %v", err) - } - - aliases := v.GetStringMapString("aliases") - if aliases == nil { - return make(map[string]string), nil - } - - return aliases, nil -} - func init() { AliasCmd.AddCommand(addAliasCmd) AliasCmd.AddCommand(removeAliasCmd) AliasCmd.AddCommand(listAliasCmd) - // Remove service flag as it's no longer needed + addAliasCmd.Flags().StringP("service", "s", "", "Service to add alias for") addAliasCmd.Flags().StringP("key", "k", "", "Alias key to add") - addAliasCmd.Flags().StringP("value", "v", "", "Command to execute (e.g., \"identity list User\")") + addAliasCmd.Flags().StringP("value", "v", "", "Command to execute (e.g., \"list User\")") + addAliasCmd.MarkFlagRequired("service") addAliasCmd.MarkFlagRequired("key") addAliasCmd.MarkFlagRequired("value") + removeAliasCmd.Flags().StringP("service", "s", "", "Service to remove alias from") removeAliasCmd.Flags().StringP("key", "k", "", "Alias key to remove") + removeAliasCmd.MarkFlagRequired("service") removeAliasCmd.MarkFlagRequired("key") } diff --git a/cmd/other/api_resources.go b/cmd/other/api_resources.go index 14c5c8f..d7487af 100644 --- a/cmd/other/api_resources.go +++ b/cmd/other/api_resources.go @@ -3,8 +3,6 @@ package other import ( - "context" - "crypto/tls" "fmt" "log" "os" @@ -13,18 +11,13 @@ import ( "strings" "sync" - "github.com/cloudforet-io/cfctl/pkg/transport" + "github.com/cloudforet-io/cfctl/pkg/configs" + "github.com/cloudforet-io/cfctl/pkg/format" "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 @@ -93,14 +86,14 @@ var ApiResourcesCmd = &cobra.Command{ 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 == "" { + endpointName, ok := envConfig["endpoint"].(string) + if !ok || endpointName == "" { return } - endpointsMap, err = transport.FetchEndpointsMap(endpoint) + endpointsMap, err = configs.FetchEndpointsMap(endpointName) if err != nil { - log.Fatalf("Failed to fetch endpointsMap from '%s': %v", endpoint, err) + log.Fatalf("Failed to fetch endpointsMap from '%s': %v", endpointName, err) } } @@ -135,7 +128,7 @@ var ApiResourcesCmd = &cobra.Command{ continue } - result, err := fetchServiceResources(endpointName, serviceEndpoint, shortNamesMap) + result, err := format.FetchServiceResources(endpointName, serviceEndpoint, shortNamesMap) if err != nil { log.Printf("Error processing service %s: %v", endpointName, err) continue @@ -161,7 +154,7 @@ var ApiResourcesCmd = &cobra.Command{ wg.Add(1) go func(service, endpoint string) { defer wg.Done() - result, err := fetchServiceResources(service, endpoint, shortNamesMap) + result, err := format.FetchServiceResources(service, endpoint, shortNamesMap) if err != nil { errorChan <- fmt.Errorf("Error processing service %s: %v", service, err) return @@ -193,171 +186,6 @@ var ApiResourcesCmd = &cobra.Command{ }, } -func loadAliases() (map[string]string, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("unable to find home directory: %v", err) - } - - settingPath := filepath.Join(home, ".cfctl", "setting.yaml") - v := viper.New() - v.SetConfigFile(settingPath) - v.SetConfigType("yaml") - - if err := v.ReadInConfig(); err != nil { - if os.IsNotExist(err) { - return make(map[string]string), nil - } - return nil, fmt.Errorf("failed to read config: %v", err) - } - - aliases := v.GetStringMapString("aliases") - if aliases == nil { - return make(map[string]string), nil - } - - return aliases, nil -} - -func fetchServiceResources(service, endpoint string, shortNamesMap map[string]string) ([][]string, error) { - 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, - } - 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) - } - - 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 - - // Load aliases - aliases, err := loadAliases() - if err != nil { - return nil, fmt.Errorf("failed to load aliases: %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) - - // Group verbs by alias - verbsWithAlias := make(map[string]string) - remainingVerbs := make([]string, 0) - - for _, verb := range verbs { - hasAlias := false - for alias, cmd := range aliases { - cmdParts := strings.Fields(cmd) - if len(cmdParts) >= 3 && - cmdParts[0] == service && - cmdParts[1] == verb && - cmdParts[2] == resourceName { - verbsWithAlias[verb] = alias - hasAlias = true - break - } - } - if !hasAlias { - remainingVerbs = append(remainingVerbs, verb) - } - } - - // Add row for verbs without aliases - if len(remainingVerbs) > 0 { - data = append(data, []string{service, strings.Join(remainingVerbs, ", "), resourceName, ""}) - } - - // Add separate rows for each verb with an alias - for verb, alias := range verbsWithAlias { - data = append(data, []string{service, verb, resourceName, alias}) - } - } - - 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() diff --git a/cmd/other/login.go b/cmd/other/login.go index aafefdb..c6efe8d 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -20,7 +20,7 @@ import ( "time" "github.com/AlecAivazis/survey/v2" - "github.com/cloudforet-io/cfctl/pkg/transport" + "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/eiannone/keyboard" "google.golang.org/grpc/metadata" @@ -426,7 +426,7 @@ func executeUserLogin(currentEnv string) { } // Get console API endpoint - apiEndpoint, err := transport.GetAPIEndpoint(baseUrl) + apiEndpoint, err := configs.GetAPIEndpoint(baseUrl) if err != nil { pterm.Error.Printf("Failed to get API endpoint: %v\n", err) exitWithError() @@ -434,7 +434,7 @@ func executeUserLogin(currentEnv string) { restIdentityEndpoint := apiEndpoint + "/identity" // Get identity service endpoint - identityEndpoint, hasIdentityService, err := transport.GetIdentityEndpoint(apiEndpoint) + identityEndpoint, hasIdentityService, err := configs.GetIdentityEndpoint(apiEndpoint) if err != nil { pterm.Error.Printf("Failed to get identity endpoint: %v\n", err) exitWithError() diff --git a/cmd/other/setting.go b/cmd/other/setting.go index 09f93db..f38c16d 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -15,6 +15,7 @@ import ( "sort" "strings" + "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/cloudforet-io/cfctl/pkg/transport" "gopkg.in/yaml.v3" @@ -543,7 +544,7 @@ You can either specify a new endpoint URL directly or use the service-based endp return } - endpoint, err := getEndpoint(appV) + endpointName, err := getEndpoint(appV) if err != nil { pterm.Error.Printf("Failed to get endpoint: %v\n", err) return @@ -586,14 +587,14 @@ You can either specify a new endpoint URL directly or use the service-based endp var identityEndpoint, restIdentityEndpoint string var hasIdentityService bool - if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { - apiEndpoint, err := transport.GetAPIEndpoint(endpoint) + if strings.HasPrefix(endpointName, "http://") || strings.HasPrefix(endpointName, "https://") { + apiEndpoint, err := configs.GetAPIEndpoint(endpointName) if err != nil { pterm.Error.Printf("Failed to get API endpoint: %v\n", err) return } - identityEndpoint, hasIdentityService, err = transport.GetIdentityEndpoint(apiEndpoint) + identityEndpoint, hasIdentityService, err = configs.GetIdentityEndpoint(apiEndpoint) if err != nil { pterm.Error.Printf("Failed to get identity endpoint: %v\n", err) return @@ -628,7 +629,7 @@ You can either specify a new endpoint URL directly or use the service-based endp isProxy := appV.GetBool(fmt.Sprintf("environments.%s.proxy", currentEnv)) - if strings.HasPrefix(endpoint, "grpc://") || strings.HasPrefix(endpoint, "grpc+ssl://") { + if strings.HasPrefix(endpointName, "grpc://") || strings.HasPrefix(endpointName, "grpc+ssl://") { if !isProxy { pterm.Error.Println("Service listing is only available when proxy is enabled.") pterm.DefaultBox.WithTitle("Available Options"). @@ -645,11 +646,11 @@ You can either specify a new endpoint URL directly or use the service-based endp } var endpoints map[string]string - parts := strings.Split(endpoint, "/") - endpoint = strings.Join(parts[:len(parts)-1], "/") - parts = strings.Split(endpoint, "://") + parts := strings.Split(endpointName, "/") + endpointName = strings.Join(parts[:len(parts)-1], "/") + parts = strings.Split(endpointName, "://") if len(parts) != 2 { - fmt.Errorf("invalid endpoint format: %s", endpoint) + fmt.Errorf("invalid endpoint format: %s", endpointName) } scheme := parts[0] @@ -670,7 +671,7 @@ You can either specify a new endpoint URL directly or use the service-based endp // Establish the connection conn, err := grpc.Dial(hostPort, opts...) if err != nil { - fmt.Errorf("connection failed: unable to connect to %s: %v", endpoint, err) + fmt.Errorf("connection failed: unable to connect to %s: %v", endpointName, err) } defer conn.Close() @@ -770,7 +771,7 @@ You can either specify a new endpoint URL directly or use the service-based endp WithData(tableData). WithBoxed(true). Render() - } else if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { + } else if strings.HasPrefix(endpointName, "http://") || strings.HasPrefix(endpointName, "https://") { var formattedServices []string endpoints, err := fetchAvailableServices(identityEndpoint, restIdentityEndpoint, hasIdentityService, token) if err != nil { diff --git a/cmd/root.go b/cmd/root.go index 1ed7162..8210f27 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,7 +8,8 @@ import ( "strings" "time" - "github.com/cloudforet-io/cfctl/cmd/commands" + "github.com/cloudforet-io/cfctl/cmd/common" + "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/cloudforet-io/cfctl/pkg/transport" "gopkg.in/yaml.v3" @@ -232,10 +233,10 @@ func showInitializationGuide() { return } - endpoint := envConfig.GetString("endpoint") + endpointName := envConfig.GetString("endpoint") // Skip authentication warning for gRPC+SSL endpoints - if strings.HasPrefix(endpoint, "grpc+ssl://") { + if strings.HasPrefix(endpointName, "grpc+ssl://") { return } @@ -252,13 +253,13 @@ func addDynamicServiceCommands() error { } // For non-local environments - endpoint := config.Endpoint + endpointName := config.Endpoint var apiEndpoint string - if strings.HasPrefix(endpoint, "grpc+ssl://") { - apiEndpoint = endpoint - } else if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { - apiEndpoint, err = transport.GetAPIEndpoint(endpoint) + if strings.HasPrefix(endpointName, "grpc+ssl://") { + apiEndpoint = endpointName + } else if strings.HasPrefix(endpointName, "http://") || strings.HasPrefix(endpointName, "https://") { + apiEndpoint, err = configs.GetAPIEndpoint(endpointName) if err != nil { return fmt.Errorf("failed to get API endpoint: %v", err) } @@ -267,8 +268,8 @@ func addDynamicServiceCommands() error { // Try to use cached endpoints first if cachedEndpointsMap != nil { currentService := "" - if strings.HasPrefix(endpoint, "grpc+ssl://") { - parts := strings.Split(endpoint, "://") + if strings.HasPrefix(endpointName, "grpc+ssl://") { + parts := strings.Split(endpointName, "://") if len(parts) == 2 { hostParts := strings.Split(parts[1], ".") if len(hostParts) > 0 { @@ -301,7 +302,7 @@ func addDynamicServiceCommands() error { Start() progressbar.UpdateTitle("Fetching available service endpoints from the API server") - endpointsMap, err := transport.FetchEndpointsMap(apiEndpoint) + endpointsMap, err := configs.FetchEndpointsMap(apiEndpoint) if err != nil { return fmt.Errorf("failed to fetch services: %v", err) } @@ -317,8 +318,8 @@ func addDynamicServiceCommands() error { progressbar.UpdateTitle("Registering available service commands") // Add commands based on the current service currentService := "" - if strings.HasPrefix(endpoint, "grpc+ssl://") { - parts := strings.Split(endpoint, "://") + if strings.HasPrefix(endpointName, "grpc+ssl://") { + parts := strings.Split(endpointName, "://") if len(parts) == 2 { hostParts := strings.Split(parts[1], ".") if len(hostParts) > 0 { @@ -457,14 +458,14 @@ func loadConfig() (*Config, error) { return nil, fmt.Errorf("environment %s not found", currentEnv) } - endpoint := envConfig.GetString("endpoint") - if endpoint == "" { + endpointName := envConfig.GetString("endpoint") + if endpointName == "" { return nil, fmt.Errorf("no endpoint found in configuration") } config := &Config{ Environment: currentEnv, - Endpoint: endpoint, + Endpoint: endpointName, } if strings.HasSuffix(currentEnv, "-app") { @@ -499,7 +500,7 @@ func createServiceCommand(serviceName string) *cobra.Command { } if verb == "api_resources" { - return commands.ListAPIResources(serviceName) + return common.ListAPIResources(serviceName) } parameters, _ := cmd.Flags().GetStringArray("parameter") @@ -521,16 +522,17 @@ func createServiceCommand(serviceName string) *cobra.Command { } options := &transport.FetchOptions{ - Parameters: parameters, - JSONParameter: jsonParameter, - FileParameter: fileParameter, - OutputFormat: outputFormat, - CopyToClipboard: copyToClipboard, - SortBy: sortBy, - MinimalColumns: verb == "list" && cmd.Flag("minimal") != nil && cmd.Flag("minimal").Changed, - Columns: columns, - Limit: limit, - PageSize: pageSize, + Parameters: parameters, + JSONParameter: jsonParameter, + FileParameter: fileParameter, + OutputFormat: outputFormat, + OutputFormatExplicit: cmd.Flags().Changed("output"), + CopyToClipboard: copyToClipboard, + SortBy: sortBy, + MinimalColumns: verb == "list" && cmd.Flag("minimal") != nil && cmd.Flag("minimal").Changed, + Columns: columns, + Limit: limit, + PageSize: pageSize, } if verb == "list" && !cmd.Flags().Changed("output") { @@ -552,7 +554,7 @@ func createServiceCommand(serviceName string) *cobra.Command { } // Add api_resources subcommand - cmd.AddCommand(commands.FetchApiResourcesCmd(serviceName)) + cmd.AddCommand(common.FetchApiResourcesCmd(serviceName)) // Add list-specific flags cmd.Flags().BoolP("watch", "w", false, "Watch for changes") diff --git a/pkg/configs/alias.go b/pkg/configs/alias.go new file mode 100644 index 0000000..17f9703 --- /dev/null +++ b/pkg/configs/alias.go @@ -0,0 +1,179 @@ +package configs + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +func AddAlias(service, key, value string) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + + data, err := os.ReadFile(settingPath) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to read config: %v", err) + } + + var config map[string]interface{} + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse config: %v", err) + } + + aliases, ok := config["aliases"].(map[string]interface{}) + if !ok { + aliases = make(map[string]interface{}) + } + + serviceAliases, ok := aliases[service].(map[string]interface{}) + if !ok { + serviceAliases = make(map[string]interface{}) + } + + serviceAliases[key] = value + aliases[service] = serviceAliases + + delete(config, "aliases") + + newData, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to encode config: %v", err) + } + + aliasData, err := yaml.Marshal(map[string]interface{}{ + "aliases": aliases, + }) + if err != nil { + return fmt.Errorf("failed to encode aliases: %v", err) + } + + finalData := append(newData, aliasData...) + + if err := os.WriteFile(settingPath, finalData, 0644); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +func RemoveAlias(service, key string) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + + data, err := os.ReadFile(settingPath) + if err != nil { + return fmt.Errorf("failed to read config: %v", err) + } + + var config map[string]interface{} + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse config: %v", err) + } + + aliases, ok := config["aliases"].(map[string]interface{}) + if !ok { + return fmt.Errorf("no aliases found") + } + + serviceAliases, ok := aliases[service].(map[string]interface{}) + if !ok { + return fmt.Errorf("no aliases found for service '%s'", service) + } + + if _, exists := serviceAliases[key]; !exists { + return fmt.Errorf("alias '%s' not found in service '%s'", key, service) + } + + delete(serviceAliases, key) + if len(serviceAliases) == 0 { + delete(aliases, service) + } else { + aliases[service] = serviceAliases + } + + config["aliases"] = aliases + + newData, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to encode config: %v", err) + } + + if err := os.WriteFile(settingPath, newData, 0644); err != nil { + return fmt.Errorf("failed to write config: %v", err) + } + + return nil +} + +func ListAliases() (map[string]interface{}, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + if os.IsNotExist(err) { + return make(map[string]interface{}), nil + } + return nil, fmt.Errorf("failed to read config: %v", err) + } + + aliases := v.Get("aliases") + if aliases == nil { + return make(map[string]interface{}), nil + } + + aliasesMap, ok := aliases.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid aliases format") + } + + return aliasesMap, nil +} + +func LoadAliases() (map[string]interface{}, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("unable to find home directory: %v", err) + } + + settingPath := filepath.Join(home, ".cfctl", "setting.yaml") + v := viper.New() + v.SetConfigFile(settingPath) + v.SetConfigType("yaml") + + if err := v.ReadInConfig(); err != nil { + if os.IsNotExist(err) { + return make(map[string]interface{}), nil + } + return nil, fmt.Errorf("failed to read config: %v", err) + } + + aliases := v.Get("aliases") + if aliases == nil { + return make(map[string]interface{}), nil + } + + aliasesMap, ok := aliases.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid aliases format") + } + + return aliasesMap, nil +} diff --git a/pkg/transport/endpoint.go b/pkg/configs/endpoint.go similarity index 98% rename from pkg/transport/endpoint.go rename to pkg/configs/endpoint.go index 8d788d8..d65c960 100644 --- a/pkg/transport/endpoint.go +++ b/pkg/configs/endpoint.go @@ -1,4 +1,4 @@ -package transport +package configs import ( "bytes" @@ -10,7 +10,6 @@ import ( "os" "strings" - "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/jhump/protoreflect/dynamic" "github.com/jhump/protoreflect/grpcreflect" "github.com/pterm/pterm" @@ -127,7 +126,7 @@ func GetIdentityEndpoint(apiEndpoint string) (string, bool, error) { return "", false, nil } -func GetServiceEndpoint(config *configs.Setting, serviceName string) (string, error) { +func GetServiceEndpoint(config *Setting, serviceName string) (string, error) { envConfig := config.Environments[config.Environment] if envConfig.Endpoint == "" { return "", fmt.Errorf("endpoint not found in environment config") diff --git a/pkg/format/validator.go b/pkg/format/validator.go new file mode 100644 index 0000000..b269d80 --- /dev/null +++ b/pkg/format/validator.go @@ -0,0 +1,242 @@ +package format + +import ( + "context" + "crypto/tls" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/cloudforet-io/cfctl/pkg/configs" + "github.com/spf13/viper" + "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" +) + +// ValidateServiceCommand checks if the given verb and resource are valid for the service +func ValidateServiceCommand(service, verb, resourceName string) error { + // Get current environment from main setting file + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + + mainV := viper.New() + mainV.SetConfigFile(filepath.Join(home, ".cfctl", "setting.yaml")) + mainV.SetConfigType("yaml") + if err := mainV.ReadInConfig(); err != nil { + return fmt.Errorf("failed to read config: %v", err) + } + + currentEnv := mainV.GetString("environment") + if currentEnv == "" { + return fmt.Errorf("no environment set") + } + + // Get environment config + envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) + if envConfig == nil { + return fmt.Errorf("environment %s not found", currentEnv) + } + + endpointName := envConfig.GetString("endpoint") + if endpointName == "" { + return fmt.Errorf("no endpoint found in configuration") + } + + endpointName, _ = configs.GetAPIEndpoint(endpointName) + + // Fetch endpoints map + endpointsMap, err := configs.FetchEndpointsMap(endpointName) + 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[2] == resourceName { + resourceFound = true + verbs := strings.Split(row[1], ", ") + for _, v := range verbs { + if v == verb { + verbFound = true + break + } + } + break + } + } + + if !resourceFound { + return fmt.Errorf("resource '%s' not found in service '%s'", resourceName, service) + } + + if !verbFound { + return fmt.Errorf("verb '%s' not found for resource '%s' in service '%s'", verb, resourceName, service) + } + + return nil +} + +func FetchServiceResources(service, endpoint string, shortNamesMap map[string]string) ([][]string, error) { + 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, + } + 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) + } + + 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 + + // Load aliases + aliases, err := configs.LoadAliases() + if err != nil { + return nil, fmt.Errorf("failed to load aliases: %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) + + // Group verbs by alias + verbsWithAlias := make(map[string]string) + remainingVerbs := make([]string, 0) + + for _, verb := range verbs { + hasAlias := false + if serviceAliases, ok := aliases[service].(map[string]interface{}); ok { + for alias, cmd := range serviceAliases { + if cmdStr, ok := cmd.(string); ok { + cmdParts := strings.Fields(cmdStr) + if len(cmdParts) >= 2 && + cmdParts[0] == verb && + cmdParts[1] == resourceName { + verbsWithAlias[verb] = alias + hasAlias = true + break + } + } + } + } + if !hasAlias { + remainingVerbs = append(remainingVerbs, verb) + } + } + + // Add row for verbs without aliases + if len(remainingVerbs) > 0 { + data = append(data, []string{service, strings.Join(remainingVerbs, ", "), resourceName, ""}) + } + + // Add separate rows for each verb with an alias + for verb, alias := range verbsWithAlias { + data = append(data, []string{service, verb, resourceName, alias}) + } + } + + 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 +} diff --git a/pkg/transport/service.go b/pkg/transport/service.go index de245be..6f6c467 100644 --- a/pkg/transport/service.go +++ b/pkg/transport/service.go @@ -16,12 +16,12 @@ import ( "strings" "time" + "github.com/atotto/clipboard" + "github.com/cloudforet-io/cfctl/pkg/configs" "github.com/cloudforet-io/cfctl/pkg/format" "github.com/eiannone/keyboard" - "github.com/spf13/viper" - - "github.com/atotto/clipboard" "github.com/pterm/pterm" + "github.com/spf13/viper" "google.golang.org/grpc/metadata" @@ -48,18 +48,19 @@ type Config struct { // FetchOptions holds the flag values for a command type FetchOptions struct { - Parameters []string - JSONParameter string - FileParameter string - APIVersion string - OutputFormat string - CopyToClipboard bool - SortBy string - MinimalColumns bool - Columns string - Limit int - Page int - PageSize int + Parameters []string + JSONParameter string + FileParameter string + APIVersion string + OutputFormat string + OutputFormatExplicit bool + CopyToClipboard bool + SortBy string + MinimalColumns bool + Columns string + Limit int + Page int + PageSize int } // FetchService handles the execution of gRPC commands for all services @@ -185,13 +186,13 @@ func FetchService(serviceName string, verb string, resourceName string, options if config.Environment == "local" { hostPort = strings.TrimPrefix(config.Environments[config.Environment].Endpoint, "grpc://") } else { - apiEndpoint, err = GetAPIEndpoint(config.Environments[config.Environment].Endpoint) + apiEndpoint, err = configs.GetAPIEndpoint(config.Environments[config.Environment].Endpoint) if err != nil { pterm.Error.Printf("Failed to get API endpoint: %v\n", err) os.Exit(1) } // Get identity service endpoint - identityEndpoint, hasIdentityService, err = GetIdentityEndpoint(apiEndpoint) + identityEndpoint, hasIdentityService, err = configs.GetIdentityEndpoint(apiEndpoint) if err != nil { pterm.Error.Printf("Failed to get identity endpoint: %v\n", err) os.Exit(1) @@ -251,6 +252,46 @@ func FetchService(serviceName string, verb string, resourceName string, options refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) defer refClient.Reset() + // Check for alias + aliases, err := configs.ListAliases() + if err != nil { + return nil, fmt.Errorf("failed to load aliases: %v", err) + } + + // Check if the verb is an alias + if serviceAliases, ok := aliases[serviceName].(map[string]interface{}); ok { + if cmd, ok := serviceAliases[verb].(string); ok { + // Split the alias command + parts := strings.Fields(cmd) + if len(parts) >= 2 { + verb = parts[0] + resourceName = parts[1] + + // If the command from alias is 'list' + if verb == "list" { + if !options.OutputFormatExplicit { + options.OutputFormat = "table" + } + + // Create new options for list command + newOptions := &FetchOptions{ + Parameters: options.Parameters, + JSONParameter: options.JSONParameter, + FileParameter: options.FileParameter, + APIVersion: options.APIVersion, + OutputFormat: options.OutputFormat, + OutputFormatExplicit: options.OutputFormatExplicit, + CopyToClipboard: options.CopyToClipboard, + MinimalColumns: false, // Always show all columns for alias + PageSize: 15, // Default page size + } + + options = newOptions + } + } + } + } + // Call the service jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options, apiEndpoint, identityEndpoint, hasIdentityService) if err != nil { @@ -604,6 +645,36 @@ func fetchJSONResponse(config *Config, serviceName string, verb string, resource // Regular unary call err = conn.Invoke(ctx, fullMethod, reqMsg, respMsg) if err != nil { + if strings.Contains(err.Error(), "ERROR_AUTHENTICATE_FAILURE") || + strings.Contains(err.Error(), "Token is invalid or expired") { + // Create a styled error message box + headerBox := pterm.DefaultBox.WithTitle("Authentication Error"). + WithTitleTopCenter(). + WithRightPadding(4). + WithLeftPadding(4). + WithBoxStyle(pterm.NewStyle(pterm.FgLightRed)) + + errorExplain := "Your authentication token has expired or is invalid.\n" + + "Please login again to refresh your credentials." + + headerBox.Println(errorExplain) + fmt.Println() + + steps := []string{ + "1. Run 'cfctl login'", + "2. Enter your credentials when prompted", + "3. Try your command again", + } + + instructionBox := pterm.DefaultBox.WithTitle("Required Steps"). + WithTitleTopCenter(). + WithRightPadding(4). + WithLeftPadding(4) + + instructionBox.Println(strings.Join(steps, "\n\n")) + + return nil, fmt.Errorf("authentication required") + } return nil, fmt.Errorf("failed to invoke method %s: %v", fullMethod, err) }