Skip to content

Commit

Permalink
Merge pull request #124 from yjinjo/master
Browse files Browse the repository at this point in the history
Remove auto ordering
  • Loading branch information
yjinjo authored Jan 16, 2025
2 parents f126456 + 335be67 commit a6e1090
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 58 deletions.
156 changes: 113 additions & 43 deletions cmd/other/apply.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
package other

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"

"github.com/cloudforet-io/cfctl/pkg/transport"
"github.com/pterm/pterm"
Expand All @@ -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() {
Expand Down
96 changes: 83 additions & 13 deletions cmd/other/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <URL> --internal\n" +
" Or\n" +
" $ cfctl setting init proxy <URL> --app --internal")
return
}
Expand Down Expand Up @@ -313,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
}

Expand Down Expand Up @@ -353,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
Expand Down Expand Up @@ -1566,6 +1569,73 @@ 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
}

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)
Expand Down
2 changes: 0 additions & 2 deletions pkg/transport/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit a6e1090

Please sign in to comment.