Skip to content

Remove auto ordering and add chaining feature for apply command #124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading