From 73bc6e15fc8d5d17b5acf890f4aed9a676c31345 Mon Sep 17 00:00:00 2001 From: Youngjin Jo <youngjinjo@megazone.com> Date: Thu, 14 Nov 2024 11:46:20 +0900 Subject: [PATCH 1/5] feat: add sync subcommand, which sync config.yaml and environments Signed-off-by: Youngjin Jo <youngjinjo@megazone.com> --- cmd/config.go | 78 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 9afff34..541493b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -49,8 +49,8 @@ var configInitCmd = &cobra.Command{ // Determine environment name var envName string if localEnv != "" { - // Use local environment name directly - envName = localEnv + // Use local environment name directly with '-user' suffix + envName = fmt.Sprintf("%s-user", localEnv) if urlStr == "" { urlStr = "http://localhost:8080" } @@ -151,9 +151,11 @@ var envCmd = &cobra.Command{ // Handle environment switching if switchEnv != "" { - configPath := filepath.Join(getConfigDir(), "environments", switchEnv+".yaml") - if _, err := os.Stat(configPath); os.IsNotExist(err) { - log.Fatalf("Environment '%s' not found.", switchEnv) + // Check for both .yaml and .yml extensions + if _, err := os.Stat(filepath.Join(getConfigDir(), "environments", switchEnv+".yaml")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(getConfigDir(), "environments", switchEnv+".yml")); os.IsNotExist(err) { + log.Fatalf("Environment '%s' not found.", switchEnv) + } } // Update the environment in ~/.spaceone/config.yaml @@ -183,9 +185,11 @@ var envCmd = &cobra.Command{ // Handle environment removal with confirmation if removeEnv != "" { - configPath := filepath.Join(getConfigDir(), "environments", removeEnv+".yaml") - if _, err := os.Stat(configPath); os.IsNotExist(err) { - log.Fatalf("Environment '%s' not found.", removeEnv) + // Check for both .yaml and .yml extensions + if _, err := os.Stat(filepath.Join(getConfigDir(), "environments", removeEnv+".yaml")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(getConfigDir(), "environments", removeEnv+".yml")); os.IsNotExist(err) { + log.Fatalf("Environment '%s' not found.", removeEnv) + } } // Ask for confirmation before deletion @@ -194,11 +198,10 @@ var envCmd = &cobra.Command{ fmt.Scanln(&response) response = strings.ToLower(strings.TrimSpace(response)) - if response == "Y" || response == "y" { + if response == "y" { // Remove the environment file - if err := os.Remove(configPath); err != nil { - log.Fatalf("Failed to remove environment '%s': %v", removeEnv, err) - } + os.Remove(filepath.Join(getConfigDir(), "environments", removeEnv+".yaml")) + os.Remove(filepath.Join(getConfigDir(), "environments", removeEnv+".yml")) // Check if this environment is set in config.yaml and clear it if so configFilePath := filepath.Join(getConfigDir(), "config.yaml") @@ -208,7 +211,7 @@ var envCmd = &cobra.Command{ // Update environment to "no-env" if the deleted environment was the current one if viper.GetString("environment") == removeEnv { viper.Set("environment", "no-env") - pterm.Info.WithShowLineNumber(false).Printfln("Cleared current environment(default: %s/config.yaml)", getConfigDir()) + pterm.Info.WithShowLineNumber(false).Printfln("Cleared current environment (default: %s/config.yaml)", getConfigDir()) } // Remove the environment from the environments map if it exists @@ -249,7 +252,7 @@ var envCmd = &cobra.Command{ pterm.Println("Available Environments:") for _, entry := range entries { name := entry.Name() - name = name[:len(name)-len(filepath.Ext(name))] // Remove ".yaml" extension + name = strings.TrimSuffix(name, filepath.Ext(name)) // Remove ".yaml" or ".yml" extension if name == currentEnv { pterm.FgGreen.Printf(" > %s (current)\n", name) } else { @@ -315,6 +318,46 @@ var showCmd = &cobra.Command{ }, } +// syncCmd syncs the environments in ~/.spaceone/environments with ~/.spaceone/config.yaml +var syncCmd = &cobra.Command{ + Use: "sync", + Short: "Sync environments from the environments directory to config.yaml", + Long: "Sync all environment files from the ~/.spaceone/environments directory to ~/.spaceone/config.yaml", + Run: func(cmd *cobra.Command, args []string) { + // Define paths + envDir := filepath.Join(getConfigDir(), "environments") + configPath := filepath.Join(getConfigDir(), "config.yaml") + + // Ensure the config file is loaded + viper.SetConfigFile(configPath) + _ = viper.ReadInConfig() + + // Iterate over each .yaml file in the environments directory + entries, err := os.ReadDir(envDir) + if err != nil { + log.Fatalf("Unable to read environments directory: %v", err) + } + + for _, entry := range entries { + if !entry.IsDir() && (filepath.Ext(entry.Name()) == ".yaml" || filepath.Ext(entry.Name()) == ".yml") { + envName := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())) + + // Check if the environment already has a URL; if not, set it to an empty string + if viper.GetString(fmt.Sprintf("environments.%s.url", envName)) == "" { + viper.Set(fmt.Sprintf("environments.%s.url", envName), "") + } + } + } + + // Save updated config to config.yaml + if err := viper.WriteConfig(); err != nil { + log.Fatalf("Failed to write updated config.yaml: %v", err) + } + + pterm.Success.Println("Successfully synced environments from environments directory to config.yaml.") + }, +} + // getConfigDir returns the directory where config files are stored func getConfigDir() string { home, err := os.UserHomeDir() @@ -395,7 +438,7 @@ func parseEnvNameFromURL(urlStr string) (string, error) { re := regexp.MustCompile(`^(.*?)\.spaceone`) matches := re.FindStringSubmatch(hostname) if len(matches) == 2 { - return fmt.Sprintf("prd-%s", matches[1]), nil + return fmt.Sprintf("prd-%s-user", matches[1]), nil } } @@ -404,7 +447,7 @@ func parseEnvNameFromURL(urlStr string) (string, error) { re := regexp.MustCompile(`(.*)\.console\.dev\.spaceone\.dev`) matches := re.FindStringSubmatch(hostname) if len(matches) == 2 { - return fmt.Sprintf("dev-%s", matches[1]), nil + return fmt.Sprintf("dev-%s-user", matches[1]), nil } pterm.Error.WithShowLineNumber(false).Println("Invalid URL format for dev environment. Expected format: '<prefix>.console.dev.spaceone.dev'") return "", fmt.Errorf("invalid dev URL format") @@ -415,7 +458,7 @@ func parseEnvNameFromURL(urlStr string) (string, error) { re := regexp.MustCompile(`(.*)\.console\.stg\.spaceone\.dev`) matches := re.FindStringSubmatch(hostname) if len(matches) == 2 { - return fmt.Sprintf("stg-%s", matches[1]), nil + return fmt.Sprintf("stg-%s-user", matches[1]), nil } pterm.Error.WithShowLineNumber(false).Println("Invalid URL format for stg environment. Expected format: '<prefix>.console.stg.spaceone.dev'") return "", fmt.Errorf("invalid stg URL format") @@ -431,6 +474,7 @@ func init() { configCmd.AddCommand(configInitCmd) configCmd.AddCommand(envCmd) configCmd.AddCommand(showCmd) + configCmd.AddCommand(syncCmd) // Defining flags for configInitCmd configInitCmd.Flags().StringP("environment", "e", "", "Override environment name") From 1967c154b9afd89692334977423f852dc2601a98 Mon Sep 17 00:00:00 2001 From: Youngjin Jo <youngjinjo@megazone.com> Date: Thu, 14 Nov 2024 11:46:57 +0900 Subject: [PATCH 2/5] feat: read all .yaml and .yml Signed-off-by: Youngjin Jo <youngjinjo@megazone.com> --- cmd/exec.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/exec.go b/cmd/exec.go index 2759ba1..df61f30 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -57,7 +57,7 @@ func init() { } func loadConfig(environment string) (*Config, error) { - configPath := fmt.Sprintf("%s/.spaceone/environments/%s.yml", os.Getenv("HOME"), environment) + configPath := fmt.Sprintf("%s/.spaceone/environments/%s.yaml", os.Getenv("HOME"), environment) data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("could not read config file: %w", err) @@ -72,7 +72,7 @@ func loadConfig(environment string) (*Config, error) { } func fetchCurrentEnvironment() (string, error) { - envPath := fmt.Sprintf("%s/.spaceone/environment.yml", os.Getenv("HOME")) + envPath := fmt.Sprintf("%s/.spaceone/config.yaml", os.Getenv("HOME")) data, err := os.ReadFile(envPath) if err != nil { return "", fmt.Errorf("could not read environment file: %w", err) From 377898133e01030464523589869889b5ad90a744 Mon Sep 17 00:00:00 2001 From: Youngjin Jo <youngjinjo@megazone.com> Date: Thu, 14 Nov 2024 14:14:35 +0900 Subject: [PATCH 3/5] feat: update config when user set up Signed-off-by: Youngjin Jo <youngjinjo@megazone.com> --- cmd/config.go | 86 ++++++++++++++++++++------------------------------- config.yaml | 0 2 files changed, 34 insertions(+), 52 deletions(-) delete mode 100644 config.yaml diff --git a/cmd/config.go b/cmd/config.go index 541493b..4457f6e 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -40,26 +40,21 @@ var configInitCmd = &cobra.Command{ urlStr, _ := cmd.Flags().GetString("url") localEnv, _ := cmd.Flags().GetString("local") - // If both url and local flags are empty, show help and return if urlStr == "" && localEnv == "" { cmd.Help() return } - // Determine environment name var envName string if localEnv != "" { - // Use local environment name directly with '-user' suffix envName = fmt.Sprintf("%s-user", localEnv) if urlStr == "" { urlStr = "http://localhost:8080" } } else { - // Ensure URL has a scheme; default to "https" if missing if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") { urlStr = "https://" + urlStr } - // Parse environment name from URL parsedEnvName, err := parseEnvNameFromURL(urlStr) if err != nil { pterm.Error.WithShowLineNumber(false).Println("Invalid URL format:", err) @@ -69,73 +64,60 @@ var configInitCmd = &cobra.Command{ envName = parsedEnvName } - // Override the parsed name if an explicit environment is provided if environment != "" { envName = environment } // Ensure environments directory exists - configPath := filepath.Join(getConfigDir(), "config.yaml") + configDir := filepath.Join(getConfigDir(), "environments") + if err := os.MkdirAll(configDir, 0755); err != nil { + pterm.Error.WithShowLineNumber(false).Println("Failed to create environments directory:", err) + return + } + envFilePath := filepath.Join(configDir, envName+".yaml") + + // Create an empty environment file if it doesn't already exist + if _, err := os.Stat(envFilePath); os.IsNotExist(err) { + file, err := os.Create(envFilePath) + if err != nil { + pterm.Error.WithShowLineNumber(false).Println("Failed to create environment file:", err) + return + } + file.Close() + } - // Load existing config if it exists + // Set configuration in config.yaml + configPath := filepath.Join(getConfigDir(), "config.yaml") viper.SetConfigFile(configPath) _ = viper.ReadInConfig() - // Add or update the environment entry in viper - if urlStr != "" { + // Add or update the environment entry in config.yaml + if !viper.IsSet(fmt.Sprintf("environments.%s", envName)) { viper.Set(fmt.Sprintf("environments.%s.url", envName), urlStr) - } else { - viper.Set(fmt.Sprintf("environments.%s", envName), "local") } - // Set the default environment to the new envName - viper.Set("environment", envName) - - // Serialize config data with 2-space indentation - configData := viper.AllSettings() - yamlData, err := yaml.Marshal(configData) - if err != nil { - pterm.Error.WithShowLineNumber(false).Println("Failed to encode YAML data:", err) - return + var baseURL string + if strings.HasPrefix(envName, "dev") { + baseURL = "grpc+ssl://identity.api.dev.spaceone.dev:443/v1" + } else if strings.HasPrefix(envName, "stg") { + baseURL = "grpc+ssl://identity.api.stg.spaceone.dev:443/v1" } - // Write the serialized YAML to file with 2-space indentation - file, err := os.Create(configPath) - if err != nil { - pterm.Error.WithShowLineNumber(false).Println("Failed to write to config.yaml:", err) - return + if baseURL != "" { + viper.Set(fmt.Sprintf("environments.%s.endpoint", envName), baseURL) } - defer file.Close() - if _, err := file.Write(yamlData); err != nil { - pterm.Error.WithShowLineNumber(false).Println("Failed to write YAML data to file:", err) + // Set the current environment + viper.Set("environment", envName) + + // Write the updated configuration to config.yaml + if err := viper.WriteConfig(); err != nil { + pterm.Error.WithShowLineNumber(false).Println("Failed to write updated config.yaml:", err) return } pterm.Success.WithShowLineNumber(false). - Printfln("Environment '%s' successfully initialized and set as the current environment in '%s/config.yaml'", envName, getConfigDir()) - - // After successfully writing to config.yaml, create the environment-specific YAML file - envFilePath := filepath.Join(getConfigDir(), "environments", fmt.Sprintf("%s.yaml", envName)) - - // Ensure the environments directory exists - environmentsDir := filepath.Dir(envFilePath) - if _, err := os.Stat(environmentsDir); os.IsNotExist(err) { - os.MkdirAll(environmentsDir, os.ModePerm) - } - - // Create a blank environment-specific file if it doesn't exist - if _, err := os.Stat(envFilePath); os.IsNotExist(err) { - file, err := os.Create(envFilePath) - if err != nil { - pterm.Error.WithShowLineNumber(false).Println("Failed to create environment file:", err) - return - } - defer file.Close() - pterm.Success.WithShowLineNumber(false).Printfln("Created environment-specific file: %s", envFilePath) - } else { - pterm.Info.WithShowLineNumber(false).Printfln("Environment file already exists: %s", envFilePath) - } + Printfln("Environment '%s' successfully initialized with configuration in '%s/config.yaml'", envName, getConfigDir()) }, } diff --git a/config.yaml b/config.yaml deleted file mode 100644 index e69de29..0000000 From 10c0428f59e6d3f8a1c9da3e206392e1005040f0 Mon Sep 17 00:00:00 2001 From: Youngjin Jo <youngjinjo@megazone.com> Date: Thu, 14 Nov 2024 14:24:20 +0900 Subject: [PATCH 4/5] feat: modify login code, which is getting from config.yaml Signed-off-by: Youngjin Jo <youngjinjo@megazone.com> --- cmd/login.go | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index f55a8c0..c0502f1 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -32,6 +32,7 @@ It will prompt you for your User ID, Password, and fetch the Domain ID automatic } func executeLogin(cmd *cobra.Command, args []string) { + // Load the environment-specific configuration loadEnvironmentConfig() token := viper.GetString("token") @@ -44,12 +45,6 @@ func executeLogin(cmd *cobra.Command, args []string) { pterm.Warning.Println("Saved token is invalid, proceeding with login.") } - // If no URL is provided, check if this is a first login - if providedUrl == "" { - pterm.Error.Println("URL must be provided with the -u flag for initial login or a valid token must be provided.") - exitWithError() - } - userID, password := promptCredentials() re := regexp.MustCompile(`https://(.*?)\.`) @@ -125,7 +120,7 @@ func loadEnvironmentConfig() { } // Load the main environment file to get the current environment - viper.SetConfigFile(filepath.Join(homeDir, ".spaceone", "environment.yml")) + viper.SetConfigFile(filepath.Join(homeDir, ".spaceone", "config.yaml")) if err := viper.ReadInConfig(); err != nil { pterm.Error.Println("Failed to read environment file:", err) exitWithError() @@ -133,16 +128,24 @@ func loadEnvironmentConfig() { currentEnvironment := viper.GetString("environment") if currentEnvironment == "" { - pterm.Error.Println("No environment specified in environment.yml") + pterm.Error.Println("No environment specified in config.yaml") exitWithError() } - // Load the environment-specific configuration file - viper.SetConfigFile(filepath.Join(homeDir, ".spaceone", "environments", currentEnvironment+".yml")) - if err := viper.MergeInConfig(); err != nil { - pterm.Error.Println("Failed to read environment-specific configuration file:", err) + // Load the environment-specific file to get the endpoint + envConfig := viper.Sub(fmt.Sprintf("environments.%s", currentEnvironment)) + if envConfig == nil { + pterm.Error.Printf("No configuration found for environment '%s' in config.yaml\n", currentEnvironment) exitWithError() } + + providedUrl = envConfig.GetString("endpoint") + if providedUrl == "" { + pterm.Error.Printf("No endpoint found for the current environment '%s' in config.yaml\n", currentEnvironment) + exitWithError() + } + + pterm.Info.Printf("Using endpoint: %s\n", providedUrl) } func determineScope(roleType string, workspaceCount int) string { @@ -426,19 +429,19 @@ func saveToken(newToken string) { } // Load the main environment file to get the current environment - viper.SetConfigFile(filepath.Join(homeDir, ".spaceone", "environment.yml")) + viper.SetConfigFile(filepath.Join(homeDir, ".spaceone", "config.yaml")) if err := viper.ReadInConfig(); err != nil { pterm.Error.Println("Failed to read environment file:", err) exitWithError() } currentEnvironment := viper.GetString("environment") if currentEnvironment == "" { - pterm.Error.Println("No environment specified in environment.yml") + pterm.Error.Println("No environment specified in environment.yaml") exitWithError() } // Path to the environment-specific file - envFilePath := filepath.Join(homeDir, ".spaceone", "environments", currentEnvironment+".yml") + envFilePath := filepath.Join(homeDir, ".spaceone", "environments", currentEnvironment+".yaml") // Read the file line by line, replacing or adding the token line if needed file, err := os.Open(envFilePath) From 6f5a7d520de7791e99a11ac6d869c8e967a8eab9 Mon Sep 17 00:00:00 2001 From: Youngjin Jo <youngjinjo@megazone.com> Date: Thu, 14 Nov 2024 14:56:15 +0900 Subject: [PATCH 5/5] feat: modify login process Signed-off-by: Youngjin Jo <youngjinjo@megazone.com> --- cmd/login.go | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index c0502f1..0c02eae 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -11,7 +11,6 @@ import ( "net/http" "os" "path/filepath" - "regexp" "strings" "time" @@ -35,6 +34,13 @@ func executeLogin(cmd *cobra.Command, args []string) { // Load the environment-specific configuration loadEnvironmentConfig() + // Set baseUrl directly from providedUrl loaded in loadEnvironmentConfig + baseUrl := providedUrl + if baseUrl == "" { + pterm.Error.Println("No token endpoint specified in the configuration file.") + exitWithError() + } + token := viper.GetString("token") if token != "" && !isTokenExpired(token) { pterm.Info.Println("Existing token found and it is still valid. Attempting to authenticate with saved credentials.") @@ -47,44 +53,44 @@ func executeLogin(cmd *cobra.Command, args []string) { userID, password := promptCredentials() - re := regexp.MustCompile(`https://(.*?)\.`) - matches := re.FindStringSubmatch(providedUrl) - if len(matches) < 2 { - pterm.Error.Println("Invalid URL format.") - exitWithError() - } - name := matches[1] - - baseUrl := viper.GetString("base_url") - if baseUrl == "" { - pterm.Error.Println("No token endpoint specified in the configuration file.") + // Extract the middle part of the environment name for `name` + currentEnvironment := viper.GetString("environment") + nameParts := strings.Split(currentEnvironment, "-") + if len(nameParts) < 3 { + pterm.Error.Println("Environment name format is invalid.") exitWithError() } + name := nameParts[1] // Extract the middle part, e.g., "cloudone" from "dev-cloudone-user" + // Fetch Domain ID using the base URL and domain name domainID, err := fetchDomainID(baseUrl, name) if err != nil { pterm.Error.Println("Failed to fetch Domain ID:", err) exitWithError() } + // Issue tokens (access token and refresh token) using user credentials accessToken, refreshToken, err := issueToken(baseUrl, userID, password, domainID) if err != nil { pterm.Error.Println("Failed to retrieve token:", err) exitWithError() } + // Fetch workspaces available to the user workspaces, err := fetchWorkspaces(baseUrl, accessToken) if err != nil { pterm.Error.Println("Failed to fetch workspaces:", err) exitWithError() } + // Fetch Domain ID and Role Type using the access token domainID, roleType, err := fetchDomainIDAndRole(baseUrl, accessToken) if err != nil { pterm.Error.Println("Failed to fetch Domain ID and Role Type:", err) exitWithError() } + // Determine the appropriate scope and workspace ID based on the role type scope := determineScope(roleType, len(workspaces)) var workspaceID string if roleType == "DOMAIN_ADMIN" { @@ -100,12 +106,14 @@ func executeLogin(cmd *cobra.Command, args []string) { scope = "WORKSPACE" } + // Grant a new access token using the refresh token and selected scope newAccessToken, err := grantToken(baseUrl, refreshToken, scope, domainID, workspaceID) if err != nil { pterm.Error.Println("Failed to retrieve new access token:", err) exitWithError() } + // Save the new access token saveToken(newAccessToken) pterm.Success.Println("Successfully logged in and saved token.") } @@ -122,24 +130,20 @@ func loadEnvironmentConfig() { // Load the main environment file to get the current environment viper.SetConfigFile(filepath.Join(homeDir, ".spaceone", "config.yaml")) if err := viper.ReadInConfig(); err != nil { - pterm.Error.Println("Failed to read environment file:", err) + pterm.Error.Println("Failed to read config.yaml:", err) exitWithError() } + // Get the currently selected environment currentEnvironment := viper.GetString("environment") if currentEnvironment == "" { pterm.Error.Println("No environment specified in config.yaml") exitWithError() } - // Load the environment-specific file to get the endpoint - envConfig := viper.Sub(fmt.Sprintf("environments.%s", currentEnvironment)) - if envConfig == nil { - pterm.Error.Printf("No configuration found for environment '%s' in config.yaml\n", currentEnvironment) - exitWithError() - } - - providedUrl = envConfig.GetString("endpoint") + // Retrieve the endpoint for the current environment + endpointKey := fmt.Sprintf("environments.%s.endpoint", currentEnvironment) + providedUrl = viper.GetString(endpointKey) if providedUrl == "" { pterm.Error.Printf("No endpoint found for the current environment '%s' in config.yaml\n", currentEnvironment) exitWithError()