diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index 1ee2a12..3180a40 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -163,22 +163,25 @@ func FetchService(serviceName string, verb string, resourceName string, options hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", serviceName, envPrefix) // Configure gRPC connection - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts := []grpc.DialOption{ - grpc.WithTransportCredentials(creds), - grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB - grpc.MaxCallSendMsgSize(10*1024*1024), // 10MB - ), - } - - // Establish the connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", hostPort, err) + 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() @@ -271,7 +274,7 @@ func FetchService(serviceName string, verb string, resourceName string, options 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{}) @@ -353,7 +356,7 @@ func FetchService(serviceName string, verb string, resourceName string, options 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{}) @@ -449,32 +452,42 @@ func loadConfig() (*Config, error) { } func fetchJSONResponse(config *Config, serviceName string, verb string, resourceName string, options *FetchOptions) ([]byte, error) { - 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 opts []grpc.DialOption - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, + 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) + } } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - - opts = append(opts, grpc.WithDefaultCallOptions( - grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB - grpc.MaxCallSendMsgSize(10*1024*1024), // 10MB - )) - // Establish the connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", hostPort, err) - } defer conn.Close() ctx := metadata.AppendToOutgoingContext(context.Background(), "token", config.Environments[config.Environment].Token) @@ -991,4 +1004,3 @@ func formatCSVValue(val interface{}) string { return fmt.Sprintf("%v", v) } } - diff --git a/cmd/common/helpers.go b/cmd/common/helpers.go index 50f749b..33f5d3e 100644 --- a/cmd/common/helpers.go +++ b/cmd/common/helpers.go @@ -34,39 +34,42 @@ func BuildVerbResourceMap(serviceName string) (map[string][]string, error) { return nil, fmt.Errorf("failed to load config: %v", err) } - var envPrefix string - if strings.HasPrefix(config.Environment, "dev-") { - envPrefix = "dev" - } else if strings.HasPrefix(config.Environment, "stg-") { - envPrefix = "stg" + 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 { - return nil, fmt.Errorf("unsupported environment prefix") - } + 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") + } - // Convert service name to endpoint format - endpointServiceName := convertServiceNameToEndpoint(serviceName) - hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", endpointServiceName, envPrefix) + endpointServiceName := convertServiceNameToEndpoint(serviceName) + hostPort := fmt.Sprintf("%s.api.%s.spaceone.dev:443", endpointServiceName, envPrefix) - // Configure gRPC connection - var opts []grpc.DialOption - tlsConfig := &tls.Config{ - InsecureSkipVerify: false, - } - creds := credentials.NewTLS(tlsConfig) - opts = append(opts, grpc.WithTransportCredentials(creds)) - - // Establish the connection - conn, err := grpc.Dial(hostPort, opts...) - if err != nil { - return nil, fmt.Errorf("connection failed: unable to connect to %s: %v", hostPort, err) + 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)) + refClient = grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) defer refClient.Reset() - // List all services services, err := refClient.ListServices() if err != nil { return nil, fmt.Errorf("failed to list services: %v", err) @@ -88,7 +91,6 @@ func BuildVerbResourceMap(serviceName string) (map[string][]string, error) { continue } - // Extract the resource name from the service name parts := strings.Split(s, ".") resourceName := parts[len(parts)-1] @@ -101,10 +103,9 @@ func BuildVerbResourceMap(serviceName string) (map[string][]string, error) { } } - // Convert the map of resources to slices result := make(map[string][]string) for verb, resourcesSet := range verbResourceMap { - resources := []string{} + resources := make([]string, 0, len(resourcesSet)) for resource := range resourcesSet { resources = append(resources, resource) } diff --git a/cmd/other/setting.go b/cmd/other/setting.go index c72aec0..0bf19c5 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -121,13 +121,15 @@ var settingInitLocalCmd = &cobra.Command{ 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 local-cloudone --app - or - cfctl setting init local -n local-cloudone --user`, + Example: ` cfctl setting init local -n [domain] --app --dev + or + cfctl setting init local -n [domain] --user --stg`, 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") if localEnv == "" { pterm.Error.Println("The --name flag is required.") @@ -139,6 +141,11 @@ var settingInitLocalCmd = &cobra.Command{ cmd.Help() return } + if !devFlag && !stgFlag { + pterm.Error.Println("You must specify either --dev or --stg flag.") + cmd.Help() + return + } // Create setting directory if it doesn't exist settingDir := GetSettingDir() @@ -158,12 +165,23 @@ var settingInitLocalCmd = &cobra.Command{ } } + envPrefix := "" + if devFlag { + envPrefix = "dev" + } else if stgFlag { + envPrefix = "stg" + } + var envName string if appFlag { - envName = fmt.Sprintf("%s-app", localEnv) + 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 { - envName = fmt.Sprintf("%s-user", localEnv) updateLocalSetting(envName, "user", filepath.Join(settingDir, "cache", "setting.toml")) } @@ -226,11 +244,9 @@ var envCmd = &cobra.Command{ // Set paths for app and user configurations 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 { @@ -238,12 +254,6 @@ var envCmd = &cobra.Command{ return } - // Load user configuration - if err := loadSetting(userV, userSettingPath); err != nil { - pterm.Error.Println(err) - return - } - // Get current environment (from app setting only) currentEnv := getCurrentEnvironment(appV) @@ -255,7 +265,6 @@ var envCmd = &cobra.Command{ if switchEnv != "" { // Check environment in both app and user settings appEnvMap := appV.GetStringMap("environments") - userEnvMap := userV.GetStringMap("environments") if currentEnv == switchEnv { pterm.Info.Printf("Already in '%s' environment.\n", currentEnv) @@ -263,12 +272,10 @@ var envCmd = &cobra.Command{ } if _, existsApp := appEnvMap[switchEnv]; !existsApp { - if _, existsUser := userEnvMap[switchEnv]; !existsUser { - home, _ := os.UserHomeDir() - pterm.Error.Printf("Environment '%s' not found in %s/.cfctl/setting.toml", - switchEnv, home) - return - } + 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 @@ -290,14 +297,10 @@ var envCmd = &cobra.Command{ var targetViper *viper.Viper var targetSettingPath string envMapApp := appV.GetStringMap("environments") - envMapUser := userV.GetStringMap("environments") if _, exists := envMapApp[removeEnv]; exists { targetViper = appV targetSettingPath = appSettingPath - } else if _, exists := envMapUser[removeEnv]; exists { - targetViper = userV - targetSettingPath = userSettingPath } else { home, _ := os.UserHomeDir() pterm.Error.Printf("Environment '%s' not found in %s/.cfctl/setting.toml", @@ -348,7 +351,6 @@ var envCmd = &cobra.Command{ if listOnly { // Get environment maps from both app and user settings appEnvMap := appV.GetStringMap("environments") - userEnvMap := userV.GetStringMap("environments") // Map to store all unique environments allEnvs := make(map[string]bool) @@ -358,11 +360,6 @@ var envCmd = &cobra.Command{ allEnvs[envName] = true } - // Add user environments - for envName := range userEnvMap { - allEnvs[envName] = true - } - if len(allEnvs) == 0 { pterm.Println("No environments found in setting file") return @@ -377,8 +374,6 @@ var envCmd = &cobra.Command{ } else { if _, isApp := appEnvMap[envName]; isApp { pterm.Printf("%s\n", envName) - } else { - pterm.Printf("%s\n", envName) } } } @@ -874,7 +869,7 @@ func loadSetting(v *viper.Viper, settingPath string) error { // Initialize with default values if file doesn't exist defaultSettings := map[string]interface{}{ "environments": map[string]interface{}{}, - "environment": "", + "environment": "", } // Convert to TOML @@ -1118,6 +1113,8 @@ func init() { 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") envCmd.Flags().StringP("switch", "s", "", "Switch to a different environment") envCmd.Flags().StringP("remove", "r", "", "Remove an environment") diff --git a/cmd/root.go b/cmd/root.go index 98b970a..6ac3f90 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,15 +1,14 @@ package cmd import ( - "bytes" - "compress/gzip" + "context" "fmt" "os" "path/filepath" "strings" - "sync" "time" + "github.com/jhump/protoreflect/grpcreflect" "github.com/spf13/viper" "github.com/cloudforet-io/cfctl/cmd/common" @@ -18,6 +17,8 @@ import ( "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 @@ -156,15 +157,32 @@ func showInitializationGuide(originalErr error) { 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 { - envPrefix := parts[0] // dev, stg - serviceName := parts[1] // cloudone, spaceone, etc. - url := fmt.Sprintf("https://%s.console.%s.spaceone.dev", serviceName, envPrefix) + 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") { - // Show app token guide pterm.DefaultBox. WithTitle("Token Not Found"). WithTitleTopCenter(). @@ -215,43 +233,64 @@ func addDynamicServiceCommands() error { return nil } - // Try to load endpoints from file cache - endpoints, err := loadCachedEndpoints() - if err == nil { - cachedEndpointsMap = endpoints + // 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() - // 병렬로 커맨드 생성 - var wg sync.WaitGroup - cmdChan := make(chan *cobra.Command, len(endpoints)) + // Create reflection client + ctx := context.Background() + refClient := grpcreflect.NewClient(ctx, grpc_reflection_v1alpha.NewServerReflectionClient(conn)) + defer refClient.Reset() - for serviceName := range endpoints { - wg.Add(1) - go func(svc string) { - defer wg.Done() - cmd := createServiceCommand(svc) - cmdChan <- cmd - }(serviceName) + // List all services + services, err := refClient.ListServices() + if err != nil { + return fmt.Errorf("failed to list local services: %v", err) } - // 별도 고루틴에서 커맨드 추가 - go func() { - wg.Wait() - close(cmdChan) - }() + 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) + } - for cmd := range cmdChan { + // Create commands for each service + for serviceName := range endpointsMap { + cmd := createServiceCommand(serviceName) rootCmd.AddCommand(cmd) } return nil } - // If no cache available, fetch dynamically (this is slow path) - setting, err := loadConfig() - if err != nil { - return fmt.Errorf("failed to load setting: %v", err) - } - + // Continue with existing logic for non-local environments endpoint := setting.Endpoint if !strings.Contains(endpoint, "identity") { parts := strings.Split(endpoint, "://") @@ -269,13 +308,11 @@ func addDynamicServiceCommands() error { return fmt.Errorf("failed to fetch services: %v", err) } - // 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) @@ -330,7 +367,6 @@ func loadCachedEndpoints() (map[string]string, error) { // Create environment-specific cache directory envCacheDir := filepath.Join(home, ".cfctl", "cache", currentEnv) - // Read from environment-specific cache file cacheFile := filepath.Join(envCacheDir, "endpoints.toml") data, err := os.ReadFile(cacheFile) if err != nil { @@ -346,7 +382,6 @@ func loadCachedEndpoints() (map[string]string, error) { return nil, fmt.Errorf("cache expired") } - // Parse cached endpoints from TOML var endpoints map[string]string if err := toml.Unmarshal(data, &endpoints); err != nil { return nil, err @@ -380,20 +415,12 @@ func saveEndpointsCache(endpoints map[string]string) error { return err } - var buf bytes.Buffer - gw := gzip.NewWriter(&buf) - data, err := toml.Marshal(endpoints) if err != nil { return err } - if _, err := gw.Write(data); err != nil { - return err - } - gw.Close() - - return os.WriteFile(filepath.Join(envCacheDir, "endpoints.toml.gz"), buf.Bytes(), 0644) + return os.WriteFile(filepath.Join(envCacheDir, "endpoints.toml"), data, 0644) } // loadConfig loads configuration from both main and cache setting files