From 1dd537ecd82ea6f8b9bc47dd0bcfe59e4c78a63e Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 2 Jan 2025 12:46:34 +0900 Subject: [PATCH 1/3] refactor: merge service endpoint to local Signed-off-by: Youngjin Jo --- cmd/other/setting.go | 51 ++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/cmd/other/setting.go b/cmd/other/setting.go index 8875230..1aa7b63 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -6,7 +6,6 @@ import ( "crypto/tls" "encoding/json" "fmt" - "google.golang.org/grpc/credentials/insecure" "log" "net/http" "net/url" @@ -15,6 +14,8 @@ import ( "regexp" "strings" + "google.golang.org/grpc/credentials/insecure" + "gopkg.in/yaml.v3" "github.com/jhump/protoreflect/dynamic" @@ -53,16 +54,21 @@ var settingInitCmd = &cobra.Command{ cfctl setting init endpoint http://localhost:8080 --app cfctl setting init endpoint http://localhost:8080 --user or - cfctl setting init local`, + cfctl setting init static grpc://localhost:50051 + cfctl setting init static grpc+ssl://inventory.-`, } -// settingInitLocalCmd represents the setting init local command -var settingInitLocalCmd = &cobra.Command{ - Use: "local", - Short: "Initialize local environment setting", - Long: `Initialize a local environment setting with default configuration.`, - Args: cobra.NoArgs, +// settingInitStaticCmd represents the setting init direct command +var settingInitStaticCmd = &cobra.Command{ + Use: "static [endpoint]", + Short: "Initialize static connection to a local or service endpoint", + Long: `Initialize configuration with a static service endpoint. +This is useful for development or when connecting directly to specific service endpoints.`, + Example: ` cfctl setting init static grpc://localhost:50051 + cfctl setting init static grpc+ssl://inventory-`, + Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { + endpoint := args[0] settingDir := GetSettingDir() if err := os.MkdirAll(settingDir, 0755); err != nil { pterm.Error.Printf("Failed to create setting directory: %v\n", err) @@ -74,14 +80,20 @@ var settingInitLocalCmd = &cobra.Command{ v.SetConfigFile(mainSettingPath) v.SetConfigType("yaml") - // Check if local environment already exists + envName, err := parseEnvNameFromURL(endpoint) + if err != nil { + pterm.Error.Printf("Failed to parse environment name: %v\n", err) + return + } + + // Check if environment already exists if err := v.ReadInConfig(); err == nil { environments := v.GetStringMap("environments") - if existingEnv, exists := environments["local"]; exists { + if existingEnv, exists := environments[envName]; exists { currentConfig, _ := yaml.Marshal(map[string]interface{}{ - "environment": "local", + "environment": envName, "environments": map[string]interface{}{ - "local": existingEnv, + envName: existingEnv, }, }) @@ -91,7 +103,7 @@ var settingInitLocalCmd = &cobra.Command{ WithLeftPadding(4). WithBoxStyle(pterm.NewStyle(pterm.FgYellow)) - confirmBox.Println("Environment 'local' already exists.\nDo you want to overwrite it?") + confirmBox.Println(fmt.Sprintf("Environment '%s' already exists.\nDo you want to overwrite it?", envName)) pterm.Info.Println("Current configuration:") fmt.Println(string(currentConfig)) @@ -102,13 +114,14 @@ var settingInitLocalCmd = &cobra.Command{ response = strings.ToLower(strings.TrimSpace(response)) if response != "y" { - pterm.Info.Println("Operation cancelled. Environment 'local' remains unchanged.") + pterm.Info.Printf("Operation cancelled. Environment '%s' remains unchanged.\n", envName) return } } } - updateSetting("local", "grpc://localhost:50051", "") + updateSetting(envName, endpoint, "") + pterm.Success.Printf("Successfully initialized direct connection to %s\n", endpoint) }, } @@ -1201,11 +1214,17 @@ func updateGlobalSetting() { } func parseEnvNameFromURL(urlStr string) (string, error) { + isGRPC := strings.HasPrefix(urlStr, "grpc://") || strings.HasPrefix(urlStr, "grpc+ssl://") + urlStr = strings.TrimPrefix(urlStr, "https://") urlStr = strings.TrimPrefix(urlStr, "http://") urlStr = strings.TrimPrefix(urlStr, "grpc://") urlStr = strings.TrimPrefix(urlStr, "grpc+ssl://") + if isGRPC { + return "local", nil + } + if strings.Contains(urlStr, "localhost") { return "local", nil } @@ -1378,7 +1397,7 @@ func init() { SettingCmd.AddCommand(settingEndpointCmd) SettingCmd.AddCommand(showCmd) settingInitCmd.AddCommand(settingInitEndpointCmd) - settingInitCmd.AddCommand(settingInitLocalCmd) + settingInitCmd.AddCommand(settingInitStaticCmd) settingInitEndpointCmd.Flags().Bool("app", false, "Initialize as application configuration") settingInitEndpointCmd.Flags().Bool("user", false, "Initialize as user-specific configuration") From 12c2ee2a70b24d61d23290ee211c3b3a42cfb0be Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 2 Jan 2025 14:41:52 +0900 Subject: [PATCH 2/3] refactor: handle grpc connection for local and static mode Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 46 ++++++++++--- cmd/other/apiResources.go | 33 +++++++++ cmd/other/login.go | 31 ++++++++- cmd/other/setting.go | 24 +++++++ cmd/root.go | 138 +++++++++++++++++++++++++------------ 5 files changed, 215 insertions(+), 57 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index 6722882..af29b55 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -236,6 +236,12 @@ func FetchService(serviceName string, verb string, resourceName string, options defer refClient.Reset() // Call the service + fmt.Println("serviceName:", serviceName) + fmt.Println("verb:", verb) + fmt.Println("resourceName:", resourceName) + fmt.Println("apiEndpoint:", apiEndpoint) + fmt.Println("identityEndpoint:", identityEndpoint) + fmt.Println("hasIdentityService:", hasIdentityService) jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options, apiEndpoint, identityEndpoint, hasIdentityService) if err != nil { // Check if the error is about missing required parameters @@ -427,19 +433,39 @@ func fetchJSONResponse(config *Config, serviceName string, verb string, resource } } else { if !hasIdentityService { - urlParts := strings.Split(apiEndpoint, "//") - if len(urlParts) != 2 { - return nil, fmt.Errorf("invalid API endpoint format: %s", apiEndpoint) - } + // Handle gRPC+SSL protocol directly + if strings.HasPrefix(config.Environments[config.Environment].Endpoint, "grpc+ssl://") { + endpoint := config.Environments[config.Environment].Endpoint + parts := strings.Split(endpoint, "/") + endpoint = strings.Join(parts[:len(parts)-1], "/") + parts = strings.Split(endpoint, "://") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) + } - domainParts := strings.Split(urlParts[1], ".") - if len(domainParts) < 4 { - return nil, fmt.Errorf("invalid domain format in API endpoint: %s", apiEndpoint) - } + hostParts := strings.Split(parts[1], ".") + if len(hostParts) < 4 { + return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) + } - domainParts[0] = convertServiceNameToEndpoint(serviceName) + // Replace service name + hostParts[0] = convertServiceNameToEndpoint(serviceName) + hostPort = strings.Join(hostParts, ".") + } else { + // Original HTTP/HTTPS handling + urlParts := strings.Split(apiEndpoint, "//") + if len(urlParts) != 2 { + return nil, fmt.Errorf("invalid API endpoint format: %s", apiEndpoint) + } - hostPort = strings.Join(domainParts, ".") + ":443" + domainParts := strings.Split(urlParts[1], ".") + if len(domainParts) < 4 { + return nil, fmt.Errorf("invalid domain format in API endpoint: %s", apiEndpoint) + } + + domainParts[0] = convertServiceNameToEndpoint(serviceName) + hostPort = strings.Join(domainParts, ".") + ":443" + } } else { trimmedEndpoint := strings.TrimPrefix(identityEndpoint, "grpc+ssl://") parts := strings.Split(trimmedEndpoint, ".") diff --git a/cmd/other/apiResources.go b/cmd/other/apiResources.go index 4ad28a7..2c21bcc 100644 --- a/cmd/other/apiResources.go +++ b/cmd/other/apiResources.go @@ -201,6 +201,39 @@ func FetchEndpointsMap(endpoint string) (map[string]string, error) { } if !hasIdentityService { + // Handle gRPC+SSL protocol directly + if strings.HasPrefix(endpoint, "grpc+ssl://") { + // Parse the endpoint + parts := strings.Split(endpoint, "/") + endpoint = strings.Join(parts[:len(parts)-1], "/") + parts = strings.Split(endpoint, "://") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid endpoint format: %s", endpoint) + } + + hostParts := strings.Split(parts[1], ".") + svc := hostParts[0] + baseDomain := strings.Join(hostParts[1:], ".") + + // Configure TLS + tlsConfig := &tls.Config{ + InsecureSkipVerify: false, + } + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + } + + //If current service is not identity, modify hostPort to use identity service + if svc != "identity" { + hostPort := fmt.Sprintf("identity.%s", baseDomain) + endpoints, err := invokeGRPCEndpointList(hostPort, opts) + if err != nil { + return nil, fmt.Errorf("failed to get endpoints from gRPC: %v", err) + } + return endpoints, nil + } + } + payload := map[string]string{} jsonPayload, err := json.Marshal(payload) if err != nil { diff --git a/cmd/other/login.go b/cmd/other/login.go index 16a3a82..69d94f3 100644 --- a/cmd/other/login.go +++ b/cmd/other/login.go @@ -697,6 +697,13 @@ func executeUserLogin(currentEnv string) { scope = "WORKSPACE" } + // Grant new token using the refresh token + newAccessToken, err := grantToken("", identityEndpoint, hasIdentityService, refreshToken, scope, domainID, workspaceID) + if err != nil { + pterm.Error.Println("Failed to retrieve new access token:", err) + exitWithError() + } + // Create cache directory envCacheDir := filepath.Join(homeDir, ".cfctl", "cache", currentEnv) if err := os.MkdirAll(envCacheDir, 0700); err != nil { @@ -710,7 +717,7 @@ func executeUserLogin(currentEnv string) { exitWithError() } - if err := os.WriteFile(filepath.Join(envCacheDir, "access_token"), []byte(accessToken), 0600); err != nil { + if err := os.WriteFile(filepath.Join(envCacheDir, "access_token"), []byte(newAccessToken), 0600); err != nil { pterm.Error.Printf("Failed to save access token: %v\n", err) exitWithError() } @@ -721,6 +728,12 @@ func executeUserLogin(currentEnv string) { // GetAPIEndpoint fetches the actual API endpoint from the config endpoint func GetAPIEndpoint(endpoint string) (string, error) { + // Handle gRPC+SSL protocol + if strings.HasPrefix(endpoint, "grpc+ssl://") { + // For gRPC+SSL endpoints, return as is since it's already in the correct format + return endpoint, nil + } + // Remove protocol prefix if exists endpoint = strings.TrimPrefix(endpoint, "https://") endpoint = strings.TrimPrefix(endpoint, "http://") @@ -759,6 +772,20 @@ func GetAPIEndpoint(endpoint string) (string, error) { // GetIdentityEndpoint fetches the identity service endpoint from the API endpoint func GetIdentityEndpoint(apiEndpoint string) (string, bool, error) { + // If the endpoint is already gRPC+SSL + if strings.HasPrefix(apiEndpoint, "grpc+ssl://") { + // Check if it contains 'identity' + containsIdentity := strings.Contains(apiEndpoint, "identity") + + // Remove /v1 suffix if present + if idx := strings.Index(apiEndpoint, "/v"); idx != -1 { + apiEndpoint = apiEndpoint[:idx] + } + + return apiEndpoint, containsIdentity, nil + } + + // Original HTTP/HTTPS handling logic endpointListURL := fmt.Sprintf("%s/identity/endpoint/list", apiEndpoint) payload := map[string]string{} @@ -1062,7 +1089,7 @@ func loadEnvironmentConfig() { "Please enable proxy mode and set identity endpoint first.") pterm.DefaultBox.WithBoxStyle(pterm.NewStyle(pterm.FgCyan)). - Println("$ cfctl config endpoint -s identity\n" + + Println("$ cfctl setting endpoint -s identity\n" + "$ cfctl login") exitWithError() diff --git a/cmd/other/setting.go b/cmd/other/setting.go index 1aa7b63..ec64e10 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -493,6 +493,17 @@ You can either specify a new endpoint URL directly or use the service-based endp if listFlag { token, err := getToken(appV) if err != nil { + if strings.HasSuffix(currentEnv, "-user") { + pterm.DefaultBox.WithTitle("Authentication Required"). + WithTitleTopCenter(). + WithBoxStyle(pterm.NewStyle(pterm.FgLightCyan)). + WithRightPadding(4). + WithLeftPadding(4). + Println("Please login to SpaceONE Console first.\n" + + "Run the following command to authenticate:\n\n" + + "$ cfctl login") + return + } pterm.Error.Println("Error retrieving token:", err) return } @@ -1290,6 +1301,19 @@ func updateSetting(envName, endpoint string, envSuffix string) { envKey := fmt.Sprintf("environments.%s.endpoint", fullEnvName) v.Set(envKey, endpoint) + // Set proxy based on endpoint type + proxyKey := fmt.Sprintf("environments.%s.proxy", fullEnvName) + if strings.HasPrefix(endpoint, "grpc://") || strings.HasPrefix(endpoint, "grpc+ssl://") { + // Check if endpoint contains 'identity' + if strings.Contains(strings.ToLower(endpoint), "identity") { + v.Set(proxyKey, true) + } else { + v.Set(proxyKey, false) + } + } else { + v.Set(proxyKey, true) + } + // Set additional configurations based on environment type if envName == "local" { // Local environment settings (only for pure 'local', not 'local-user' or 'local-app') diff --git a/cmd/root.go b/cmd/root.go index 183b150..9d4e059 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -233,6 +233,20 @@ func showInitializationGuide() { pterm.Info.Println("After updating the token, please try your command again.") } } else if strings.HasSuffix(currentEnv, "-user") { + // Get endpoint from environment config + envConfig := mainV.Sub(fmt.Sprintf("environments.%s", currentEnv)) + if envConfig == nil { + pterm.Warning.Printf("No environment configuration found.\n") + return + } + + endpoint := envConfig.GetString("endpoint") + + // Skip authentication warning for gRPC+SSL endpoints + if strings.HasPrefix(endpoint, "grpc+ssl://") { + return + } + pterm.Warning.Printf("Authentication required.\n") pterm.Info.Println("To see Available Commands, please authenticate first:") pterm.Info.Println("$ cfctl login") @@ -318,70 +332,104 @@ func addDynamicServiceCommands() error { return nil } - // For non-local environments, continue with existing logic... + // For non-local environments + endpoint := 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 = other.GetAPIEndpoint(endpoint) + if err != nil { + return fmt.Errorf("failed to get API endpoint: %v", err) + } + } + + // Try to use cached endpoints first if cachedEndpointsMap != nil { + currentService := "" + if strings.HasPrefix(endpoint, "grpc+ssl://") { + parts := strings.Split(endpoint, "://") + if len(parts) == 2 { + hostParts := strings.Split(parts[1], ".") + if len(hostParts) > 0 { + currentService = hostParts[0] + } + } + } + + if currentService != "identity" && currentService != "" { + if cmd := createServiceCommand(currentService); cmd != nil { + cmd.GroupID = "available" + rootCmd.AddCommand(cmd) + } + return nil + } + + // If identity service or no specific service, add all available commands for serviceName := range cachedEndpointsMap { cmd := createServiceCommand(serviceName) + cmd.GroupID = "available" rootCmd.AddCommand(cmd) } return nil } - // Only show progress bar when actually fetching services - if len(os.Args) == 1 || (len(os.Args) > 1 && - os.Args[1] != "setting" && - os.Args[1] != "login" && - os.Args[1] != "api_resources" && - os.Args[1] != "short_name") { - // Create progress bar - progressbar, _ := pterm.DefaultProgressbar. - WithTotal(4). - WithTitle("Initializing services"). - Start() - - progressbar.UpdateTitle("Preparing endpoint configuration") - endpoint := config.Endpoint - - var apiEndpoint string - if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") { - apiEndpoint, err = other.GetAPIEndpoint(endpoint) - if err != nil { - pterm.Error.Printf("Failed to get API endpoint: %v\n", err) - os.Exit(1) - } - } + // If no cached endpoints, fetch them + progressbar, _ := pterm.DefaultProgressbar. + WithTotal(4). + WithTitle("Initializing services"). + Start() - progressbar.Increment() - time.Sleep(time.Millisecond * 300) + progressbar.UpdateTitle("Fetching available services") + endpointsMap, err := other.FetchEndpointsMap(apiEndpoint) + if err != nil { + return fmt.Errorf("failed to fetch services: %v", err) + } - progressbar.UpdateTitle("Fetching available services") - endpointsMap, err := other.FetchEndpointsMap(apiEndpoint) - if err != nil { - return fmt.Errorf("failed to fetch services: %v", err) - } - progressbar.Increment() - time.Sleep(time.Millisecond * 300) - - progressbar.UpdateTitle("Creating cache for faster subsequent runs") - cachedEndpointsMap = endpointsMap - if err := saveEndpointsCache(endpointsMap); err != nil { - _, err2 := fmt.Fprintf(os.Stderr, "Warning: Failed to cache endpoints: %v\n", err) - if err2 != nil { - return err2 + progressbar.Increment() + time.Sleep(time.Millisecond * 300) + + progressbar.UpdateTitle("Creating cache for faster subsequent runs") + cachedEndpointsMap = endpointsMap + if err := saveEndpointsCache(endpointsMap); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to cache endpoints: %v\n", err) + } + + progressbar.Increment() + time.Sleep(time.Millisecond * 300) + + //progressbar.UpdateTitle("Finalizing") + progressbar.Increment() + time.Sleep(time.Millisecond * 300) + + fmt.Println() + // Add commands based on the current service + currentService := "" + if strings.HasPrefix(endpoint, "grpc+ssl://") { + parts := strings.Split(endpoint, "://") + if len(parts) == 2 { + hostParts := strings.Split(parts[1], ".") + if len(hostParts) > 0 { + currentService = hostParts[0] } } - progressbar.Increment() - time.Sleep(time.Millisecond * 300) + } - progressbar.UpdateTitle("Registering verbs and resources commands to the cache") + if currentService != "identity" && currentService != "" { + if cmd := createServiceCommand(currentService); cmd != nil { + cmd.GroupID = "available" + rootCmd.AddCommand(cmd) + } + } else { for serviceName := range endpointsMap { cmd := createServiceCommand(serviceName) + cmd.GroupID = "available" rootCmd.AddCommand(cmd) } - progressbar.Increment() - time.Sleep(time.Millisecond * 300) } + progressbar.Increment() return nil } From 5d771044605ebd08c60cf3d4980fbb5b26353ffe Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 2 Jan 2025 14:43:04 +0900 Subject: [PATCH 3/3] chore: remove print line Signed-off-by: Youngjin Jo --- cmd/common/fetchService.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cmd/common/fetchService.go b/cmd/common/fetchService.go index af29b55..7e3a2ba 100644 --- a/cmd/common/fetchService.go +++ b/cmd/common/fetchService.go @@ -236,12 +236,6 @@ func FetchService(serviceName string, verb string, resourceName string, options defer refClient.Reset() // Call the service - fmt.Println("serviceName:", serviceName) - fmt.Println("verb:", verb) - fmt.Println("resourceName:", resourceName) - fmt.Println("apiEndpoint:", apiEndpoint) - fmt.Println("identityEndpoint:", identityEndpoint) - fmt.Println("hasIdentityService:", hasIdentityService) jsonBytes, err := fetchJSONResponse(config, serviceName, verb, resourceName, options, apiEndpoint, identityEndpoint, hasIdentityService) if err != nil { // Check if the error is about missing required parameters