From 710558332a7d5f8e66aeba6a3b0d3cc9fe57b832 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 16 Jan 2025 14:22:44 +0900 Subject: [PATCH 1/5] refactor: add case for internal flag Signed-off-by: Youngjin Jo --- cmd/other/setting.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/other/setting.go b/cmd/other/setting.go index 5b07bbc..01df07e 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -156,28 +156,32 @@ var settingInitProxyCmd = &cobra.Command{ Long: `Specify a proxy URL to initialize the environment configuration.`, Args: cobra.ExactArgs(1), Example: ` cfctl setting init proxy http[s]://example.com --app - cfctl setting init proxy http[s]://example.com --user`, + cfctl setting init proxy http[s]://example.com --user + cfctl setting init proxy http[s]://example.com --internal`, Run: func(cmd *cobra.Command, args []string) { endpointStr := args[0] appFlag, _ := cmd.Flags().GetBool("app") userFlag, _ := cmd.Flags().GetBool("user") internalFlag, _ := cmd.Flags().GetBool("internal") - if !appFlag && !userFlag { - pterm.Error.Println("You must specify either --app or --user flag.") + if internalFlag { + appFlag = true + } else if !appFlag && !userFlag { + pterm.Error.Println("You must specify either --app, --user, or --internal flag.") cmd.Help() return } - // Internal flag can only be used with --app flag - if internalFlag && userFlag { + if userFlag && internalFlag { pterm.DefaultBox.WithTitle("Internal Flag Not Allowed"). WithTitleTopCenter(). WithRightPadding(4). WithLeftPadding(4). WithBoxStyle(pterm.NewStyle(pterm.FgRed)). - Println("The --internal flag can only be used with the --app flag.\n" + + Println("The --internal flag can be used either alone or with the --app flag.\n" + "Example usage:\n" + + " $ cfctl setting init proxy --internal\n" + + " Or\n" + " $ cfctl setting init proxy --app --internal") return } From 7a7d5eca4e075f8a03045df4a3e805190e6b920f Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 16 Jan 2025 16:11:09 +0900 Subject: [PATCH 2/5] chore: remove println Signed-off-by: Youngjin Jo --- pkg/transport/service.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/transport/service.go b/pkg/transport/service.go index 057c3a8..8ee45cb 100644 --- a/pkg/transport/service.go +++ b/pkg/transport/service.go @@ -187,14 +187,12 @@ func FetchService(serviceName string, verb string, resourceName string, options hostPort = strings.TrimPrefix(config.Environments[config.Environment].Endpoint, "grpc://") } else { apiEndpoint, err = configs.GetAPIEndpoint(config.Environments[config.Environment].Endpoint) - fmt.Println(apiEndpoint) if err != nil { pterm.Error.Printf("Failed to get API endpoint: %v\n", err) os.Exit(1) } // Get identity service endpoint identityEndpoint, hasIdentityService, err = configs.GetIdentityEndpoint(apiEndpoint) - fmt.Println(identityEndpoint) if err != nil { pterm.Error.Printf("Failed to get identity endpoint: %v\n", err) os.Exit(1) From ddb64630c4ddb562828c2dc0da5fb11bb6bcb050 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 16 Jan 2025 16:26:11 +0900 Subject: [PATCH 3/5] refactor: remove auto ordering Signed-off-by: Youngjin Jo --- cmd/other/setting.go | 81 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/cmd/other/setting.go b/cmd/other/setting.go index 01df07e..7c88f29 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -317,8 +317,8 @@ var envCmd = &cobra.Command{ // Update only the environment field in app setting appV.Set("environment", switchEnv) - if err := appV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update environment in setting.yaml: %v", err) + if err := WriteConfigPreservingKeyOrder(appV, appSettingPath); err != nil { + pterm.Error.Printf("Failed to update environment in setting.yaml: %v\n", err) return } @@ -357,19 +357,18 @@ var envCmd = &cobra.Command{ targetViper.Set("environments", envMap) // Write the updated configuration back to the respective setting file - if err := targetViper.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update setting file '%s': %v", targetSettingPath, err) + if err := WriteConfigPreservingKeyOrder(targetViper, targetSettingPath); err != nil { + pterm.Error.Printf("Failed to update setting file '%s': %v\n", targetSettingPath, err) return } // If the deleted environment was the current one, unset it if currentEnv == removeEnv { appV.Set("environment", "") - if err := appV.WriteConfig(); err != nil { - pterm.Error.Printf("Failed to update environment in setting.yaml: %v", err) + if err := WriteConfigPreservingKeyOrder(appV, appSettingPath); err != nil { + pterm.Error.Printf("Failed to clear current environment: %v\n", err) return } - pterm.Info.WithShowLineNumber(false).Println("Cleared current environment in setting.yaml") } // Display success message @@ -1570,6 +1569,74 @@ func convertToSlice(s []interface{}) []interface{} { return result } +func WriteConfigPreservingKeyOrder(v *viper.Viper, path string) error { + allSettings := v.AllSettings() + + rawBytes, err := yaml.Marshal(allSettings) + if err != nil { + return fmt.Errorf("failed to marshal viper data: %w", err) + } + + var rootNode yaml.Node + if err := yaml.Unmarshal(rawBytes, &rootNode); err != nil { + return fmt.Errorf("failed to unmarshal into yaml.Node: %w", err) + } + + reorderRootNode(&rootNode) + + reorderedBytes, err := yaml.Marshal(&rootNode) + if err != nil { + return fmt.Errorf("failed to marshal reordered yaml.Node: %w", err) + } + + if err := os.WriteFile(path, reorderedBytes, 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} + +func reorderRootNode(doc *yaml.Node) { + if doc.Kind != yaml.DocumentNode || len(doc.Content) == 0 { + return + } + + rootMap := doc.Content[0] + if rootMap.Kind != yaml.MappingNode { + return + } + + // rootMap.Content 에는 [keyNode, valNode, keyNode, valNode, ...] 순 + var newContent []*yaml.Node + var aliasesKV []*yaml.Node + var environmentKV []*yaml.Node + var environmentsKV []*yaml.Node + var otherKVs []*yaml.Node + + for i := 0; i < len(rootMap.Content); i += 2 { + keyNode := rootMap.Content[i] + valNode := rootMap.Content[i+1] + + switch keyNode.Value { + case "aliases": + aliasesKV = append(aliasesKV, keyNode, valNode) + case "environment": + environmentKV = append(environmentKV, keyNode, valNode) + case "environments": + environmentsKV = append(environmentsKV, keyNode, valNode) + default: + otherKVs = append(otherKVs, keyNode, valNode) + } + } + + newContent = append(newContent, environmentKV...) + newContent = append(newContent, environmentsKV...) + newContent = append(newContent, otherKVs...) + newContent = append(newContent, aliasesKV...) + + rootMap.Content = newContent +} + func init() { SettingCmd.AddCommand(settingInitCmd) SettingCmd.AddCommand(settingEndpointCmd) From f25a2b287ffe16d4d63c66aaf7481843b9603321 Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 16 Jan 2025 16:47:23 +0900 Subject: [PATCH 4/5] feat: add chaning feature for apply command Signed-off-by: Youngjin Jo --- cmd/other/apply.go | 156 ++++++++++++++++++++++++++++++++------------- 1 file changed, 113 insertions(+), 43 deletions(-) diff --git a/cmd/other/apply.go b/cmd/other/apply.go index 123b8e7..6d790f6 100644 --- a/cmd/other/apply.go +++ b/cmd/other/apply.go @@ -1,12 +1,12 @@ -/* -Copyright © 2025 NAME HERE -*/ package other import ( + "bytes" "encoding/json" "fmt" + "io" "os" + "strings" "github.com/cloudforet-io/cfctl/pkg/transport" "github.com/pterm/pterm" @@ -21,76 +21,146 @@ type ResourceSpec struct { Spec map[string]interface{} `yaml:"spec"` } +func parseResourceSpecs(data []byte) ([]ResourceSpec, error) { + var resources []ResourceSpec + + // Split YAML documents + decoder := yaml.NewDecoder(bytes.NewReader(data)) + for { + var resource ResourceSpec + if err := decoder.Decode(&resource); err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to parse YAML: %v", err) + } + resources = append(resources, resource) + } + + return resources, nil +} + // ApplyCmd represents the apply command var ApplyCmd = &cobra.Command{ Use: "apply", Short: "Apply a configuration to a resource using a file", Long: `Apply the configuration in the YAML file to create or update a resource`, - Example: ` # Create test.yaml + Example: ` # 01. Create a test.yaml file with service-verb-resource-spec format service: identity verb: create - resource: user + resource: WorkspaceGroup + spec: + name: Test Workspace Group + --- + service: identity + verb: add_users + resource: WorkspaceGroup spec: - user_id: test-user - auth_type: LOCAL + workspace_group_id: wg-12345 + users: + - user_id: u-123 + role_id: role-123 + - user_id: u-456 + role_id: role-456 - # Apply the configuration in test.yaml - $ cfctl apply -f test.yaml`, + # 02. Apply the configuration + cfctl apply -f test.yaml`, RunE: func(cmd *cobra.Command, args []string) error { filename, _ := cmd.Flags().GetString("filename") if filename == "" { return fmt.Errorf("filename is required (-f flag)") } - // Read and parse YAML file + // Read YAML file data, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file: %v", err) } - var resource ResourceSpec - if err := yaml.Unmarshal(data, &resource); err != nil { - return fmt.Errorf("failed to parse YAML: %v", err) + // Parse all resource specs + resources, err := parseResourceSpecs(data) + if err != nil { + return err } - // Convert spec to parameters - var parameters []string - for key, value := range resource.Spec { - switch v := value.(type) { - case string: - parameters = append(parameters, fmt.Sprintf("%s=%s", key, v)) - case bool, int, float64: - parameters = append(parameters, fmt.Sprintf("%s=%v", key, v)) - case []interface{}, map[string]interface{}: - // For arrays and maps, convert to JSON string - jsonBytes, err := json.Marshal(v) - if err != nil { - return fmt.Errorf("failed to marshal parameter %s: %v", key, err) - } - parameters = append(parameters, fmt.Sprintf("%s=%s", key, string(jsonBytes))) - default: - // For other complex types, try JSON marshaling - jsonBytes, err := json.Marshal(v) - if err != nil { - return fmt.Errorf("failed to marshal parameter %s: %v", key, err) + // Process each resource sequentially + var lastResponse map[string]interface{} + for i, resource := range resources { + pterm.Info.Printf("Applying resource %d/%d: %s/%s\n", + i+1, len(resources), resource.Service, resource.Resource) + + // Convert spec to parameters + parameters := convertSpecToParameters(resource.Spec, lastResponse) + + options := &transport.FetchOptions{ + Parameters: parameters, + } + + response, err := transport.FetchService(resource.Service, resource.Verb, resource.Resource, options) + if err != nil { + pterm.Error.Printf("Failed to apply resource %d/%d: %v\n", i+1, len(resources), err) + return err + } + + lastResponse = response + pterm.Success.Printf("Resource %d/%d applied successfully\n", i+1, len(resources)) + } + + return nil + }, +} + +func convertSpecToParameters(spec map[string]interface{}, lastResponse map[string]interface{}) []string { + var parameters []string + + for key, value := range spec { + switch v := value.(type) { + case string: + // Check if value references previous response + if strings.HasPrefix(v, "${") && strings.HasSuffix(v, "}") { + refPath := strings.Trim(v, "${}") + if val := getValueFromPath(lastResponse, refPath); val != "" { + parameters = append(parameters, fmt.Sprintf("%s=%s", key, val)) } + } else { + parameters = append(parameters, fmt.Sprintf("%s=%s", key, v)) + } + case []interface{}, map[string]interface{}: + jsonBytes, err := json.Marshal(v) + if err == nil { parameters = append(parameters, fmt.Sprintf("%s=%s", key, string(jsonBytes))) } + default: + parameters = append(parameters, fmt.Sprintf("%s=%v", key, v)) } + } - options := &transport.FetchOptions{ - Parameters: parameters, - } + return parameters +} - _, err = transport.FetchService(resource.Service, resource.Verb, resource.Resource, options) - if err != nil { - pterm.Error.Println(err.Error()) - return nil +func getValueFromPath(data map[string]interface{}, path string) string { + parts := strings.Split(path, ".") + current := data + + for _, part := range parts { + if v, ok := current[part]; ok { + switch val := v.(type) { + case map[string]interface{}: + current = val + case string: + return val + default: + if str, err := json.Marshal(val); err == nil { + return string(str) + } + return fmt.Sprintf("%v", val) + } + } else { + return "" } + } - pterm.Success.Printf("Resource %s/%s applied successfully\n", resource.Service, resource.Resource) - return nil - }, + return "" } func init() { From 335be67380f0b3f914ff92d6fec909d939bda81c Mon Sep 17 00:00:00 2001 From: Youngjin Jo Date: Thu, 16 Jan 2025 16:48:25 +0900 Subject: [PATCH 5/5] chore: remove comment Signed-off-by: Youngjin Jo --- cmd/other/setting.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/other/setting.go b/cmd/other/setting.go index 7c88f29..d8acbd9 100644 --- a/cmd/other/setting.go +++ b/cmd/other/setting.go @@ -1606,7 +1606,6 @@ func reorderRootNode(doc *yaml.Node) { return } - // rootMap.Content 에는 [keyNode, valNode, keyNode, valNode, ...] 순 var newContent []*yaml.Node var aliasesKV []*yaml.Node var environmentKV []*yaml.Node