diff --git a/.github/auto-release.yml b/.github/auto-release.yml index 87f08aca6..9976e1076 100644 --- a/.github/auto-release.yml +++ b/.github/auto-release.yml @@ -1,10 +1,6 @@ name-template: 'v$RESOLVED_VERSION' - -# https://golang.org/ref/mod#versions -tag-template: 'v$RESOLVED_VERSION' - +tag-template: '$RESOLVED_VERSION' version-template: '$MAJOR.$MINOR.$PATCH' - version-resolver: major: labels: diff --git a/internal/config/config.go b/internal/config/config.go index f982a9cf3..d206fddf6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,10 +59,10 @@ var ( ProcessedConfig ProcessedConfiguration ) -// InitConfig processes and merges configurations in the following order: system dir, home dir, current dir, ENV vars, command-line arguments +// InitConfig finds and merges CLI configurations in the following order: system dir, home dir, current dir, ENV vars, command-line arguments // https://dev.to/techschoolguru/load-config-from-file-environment-variables-in-golang-with-viper-2j2d // https://medium.com/@bnprashanth256/reading-configuration-files-and-environment-variables-in-go-golang-c2607f912b63 -func InitConfig(configAndStacksInfo ConfigAndStacksInfo) error { +func InitConfig() error { // Config is loaded from the following locations (from lower to higher priority): // system dir (`/usr/local/etc/atmos` on Linux, `%LOCALAPPDATA%/atmos` on Windows) // home dir (~/.atmos) @@ -148,8 +148,13 @@ func InitConfig(configAndStacksInfo ConfigAndStacksInfo) error { return err } + return nil +} + +// ProcessConfig processes and checks CLI configuration +func ProcessConfig(configAndStacksInfo ConfigAndStacksInfo) error { // Process ENV vars - err = processEnvVars() + err := processEnvVars() if err != nil { return err } diff --git a/internal/exec/describe.go b/internal/exec/describe.go index a92aa64cb..45a463fe2 100644 --- a/internal/exec/describe.go +++ b/internal/exec/describe.go @@ -27,7 +27,12 @@ func ExecuteDescribeComponent(cmd *cobra.Command, args []string) error { var configAndStacksInfo c.ConfigAndStacksInfo configAndStacksInfo.Stack = stack - err = c.InitConfig(configAndStacksInfo) + err = c.InitConfig() + if err != nil { + return err + } + + err = c.ProcessConfig(configAndStacksInfo) if err != nil { return err } diff --git a/internal/exec/terraform_generate.go b/internal/exec/terraform_generate.go index 47b05b83a..dffe4b2dd 100644 --- a/internal/exec/terraform_generate.go +++ b/internal/exec/terraform_generate.go @@ -27,7 +27,12 @@ func ExecuteTerraformGenerateBackend(cmd *cobra.Command, args []string) error { var configAndStacksInfo c.ConfigAndStacksInfo configAndStacksInfo.Stack = stack - err = c.InitConfig(configAndStacksInfo) + err = c.InitConfig() + if err != nil { + return err + } + + err = c.ProcessConfig(configAndStacksInfo) if err != nil { return err } diff --git a/internal/exec/utils.go b/internal/exec/utils.go index 01064a858..ccdb9cf5b 100644 --- a/internal/exec/utils.go +++ b/internal/exec/utils.go @@ -91,6 +91,15 @@ func findComponentConfig( var componentBackendSection map[interface{}]interface{} var ok bool + if len(stack) == 0 { + return nil, nil, nil, errors.New("stack must be provided and must not be empty") + } + if len(component) == 0 { + return nil, nil, nil, errors.New("component must be provided and must not be empty") + } + if len(componentType) == 0 { + return nil, nil, nil, errors.New("component type must be provided and must not be empty") + } if stackSection, ok = stacksMap[stack].(map[interface{}]interface{}); !ok { return nil, nil, nil, errors.New(fmt.Sprintf("Stack '%s' does not exist", stack)) } @@ -155,7 +164,12 @@ func processConfigAndStacks(componentType string, cmd *cobra.Command, args []str } // Process and merge CLI configurations - err = c.InitConfig(configAndStacksInfo) + err = c.InitConfig() + if err != nil { + return configAndStacksInfo, err + } + + err = c.ProcessConfig(configAndStacksInfo) if err != nil { return configAndStacksInfo, err } diff --git a/pkg/component/atmos.yaml b/pkg/component/atmos.yaml new file mode 100644 index 000000000..3082ddbac --- /dev/null +++ b/pkg/component/atmos.yaml @@ -0,0 +1,48 @@ +# CLI config is loaded from the following locations (from lowest to highest priority): +# system dir (`/usr/local/etc/atmos` on Linux, `%LOCALAPPDATA%/atmos` on Windows) +# home dir (~/.atmos) +# current directory +# ENV vars +# Command-line arguments +# +# It supports POSIX-style Globs for file names/paths (double-star `**` is supported) +# https://en.wikipedia.org/wiki/Glob_(programming) + +components: + terraform: + # Can also be set using `ATMOS_COMPONENTS_TERRAFORM_BASE_PATH` ENV var, or `--terraform-dir` command-line argument + # Supports both absolute and relative paths + base_path: "../../examples/complete/components/terraform" + # Can also be set using `ATMOS_COMPONENTS_TERRAFORM_APPLY_AUTO_APPROVE` ENV var + apply_auto_approve: false + # Can also be set using `ATMOS_COMPONENTS_TERRAFORM_DEPLOY_RUN_INIT` ENV var, or `--deploy-run-init` command-line argument + deploy_run_init: true + helmfile: + # Can also be set using `ATMOS_COMPONENTS_HELMFILE_BASE_PATH` ENV var, or `--helmfile-dir` command-line argument + # Supports both absolute and relative paths + base_path: "../../examples/complete/components/helmfile" + # Can also be set using `ATMOS_COMPONENTS_HELMFILE_KUBECONFIG_PATH` ENV var + kubeconfig_path: "/dev/shm" + # Can also be set using `ATMOS_COMPONENTS_HELMFILE_HELM_AWS_PROFILE_PATTERN` ENV var + helm_aws_profile_pattern: "{namespace}-{tenant}-gbl-{stage}-helm" + # Can also be set using `ATMOS_COMPONENTS_HELMFILE_CLUSTER_NAME_PATTERN` ENV var + cluster_name_pattern: "{namespace}-{tenant}-{environment}-{stage}-eks-cluster" + +stacks: + # Can also be set using `ATMOS_STACKS_BASE_PATH` ENV var, or `--config-dir` and `--stacks-dir` command-line arguments + # Supports both absolute and relative paths + base_path: "../../examples/complete/stacks" + # Can also be set using `ATMOS_STACKS_INCLUDED_PATHS` ENV var (comma-separated values string) + included_paths: + - "**/*" + # Can also be set using `ATMOS_STACKS_EXCLUDED_PATHS` ENV var (comma-separated values string) + excluded_paths: + - "globals/**/*" + - "catalog/**/*" + - "**/*globals*" + # Can also be set using `ATMOS_STACKS_NAME_PATTERN` ENV var + name_pattern: "{tenant}-{environment}-{stage}" + +logs: + verbose: false + colors: true diff --git a/pkg/component/component_processor.go b/pkg/component/component_processor.go new file mode 100644 index 000000000..e85eef823 --- /dev/null +++ b/pkg/component/component_processor.go @@ -0,0 +1,235 @@ +package component + +import ( + "fmt" + c "github.com/cloudposse/atmos/internal/config" + s "github.com/cloudposse/atmos/pkg/stack" + "github.com/pkg/errors" + "strings" +) + +// ProcessComponentInStack accepts a component and a stack name and returns the component configuration in the stack +func ProcessComponentInStack(component string, stack string) (map[string]interface{}, error) { + var configAndStacksInfo c.ConfigAndStacksInfo + configAndStacksInfo.Stack = stack + + err := c.InitConfig() + if err != nil { + return nil, err + } + + err = c.ProcessConfig(configAndStacksInfo) + if err != nil { + return nil, err + } + + _, stacksMap, err := s.ProcessYAMLConfigFiles( + c.ProcessedConfig.StacksBaseAbsolutePath, + c.ProcessedConfig.StackConfigFilesAbsolutePaths, + true, + true) + + if err != nil { + return nil, err + } + + var componentSection map[string]interface{} + var componentVarsSection map[interface{}]interface{} + + // Check and process stacks + if c.ProcessedConfig.StackType == "Directory" { + componentSection, componentVarsSection, _, err = findComponentConfig(stack, stacksMap, "terraform", component) + if err != nil { + componentSection, componentVarsSection, _, err = findComponentConfig(stack, stacksMap, "helmfile", component) + if err != nil { + return nil, err + } + } + } else { + if len(c.Config.Stacks.NamePattern) < 1 { + return nil, errors.New("stack name pattern must be provided in 'stacks.name_pattern' config or 'ATMOS_STACKS_NAME_PATTERN' ENV variable") + } + + stackParts := strings.Split(stack, "-") + stackNamePatternParts := strings.Split(c.Config.Stacks.NamePattern, "-") + + var tenant string + var environment string + var stage string + var tenantFound bool + var environmentFound bool + var stageFound bool + + for i, part := range stackNamePatternParts { + if part == "{tenant}" { + tenant = stackParts[i] + } else if part == "{environment}" { + environment = stackParts[i] + } else if part == "{stage}" { + stage = stackParts[i] + } + } + + for stackName := range stacksMap { + componentSection, componentVarsSection, _, err = findComponentConfig(stackName, stacksMap, "terraform", component) + if err != nil { + componentSection, componentVarsSection, _, err = findComponentConfig(stackName, stacksMap, "helmfile", component) + if err != nil { + continue + } + } + + tenantFound = true + environmentFound = true + stageFound = true + + // Search for tenant in stack + if len(tenant) > 0 { + if tenantInStack, ok := componentVarsSection["tenant"].(string); !ok || tenantInStack != tenant { + tenantFound = false + } + } + + // Search for environment in stack + if len(environment) > 0 { + if environmentInStack, ok := componentVarsSection["environment"].(string); !ok || environmentInStack != environment { + environmentFound = false + } + } + + // Search for stage in stack + if len(stage) > 0 { + if stageInStack, ok := componentVarsSection["stage"].(string); !ok || stageInStack != stage { + stageFound = false + } + } + + if tenantFound == true && environmentFound == true && stageFound == true { + stack = stackName + break + } + } + + if tenantFound == false || environmentFound == false || stageFound == false { + return nil, errors.New(fmt.Sprintf("\nCould not find config for the component '%s' in the stack '%s'.\n"+ + "Check that all attributes in the stack name pattern '%s' are defined in stack config files.\n"+ + "Are the component and stack names correct? Did you forget an import?", + component, + stack, + c.Config.Stacks.NamePattern, + )) + } + } + + baseComponentName := "" + if baseComponent, baseComponentExist := componentSection["component"]; baseComponentExist { + baseComponentName = baseComponent.(string) + } + + // workspace + var workspace string + if len(baseComponentName) == 0 { + workspace = stack + } else { + workspace = fmt.Sprintf("%s-%s", stack, component) + } + componentSection["workspace"] = strings.Replace(workspace, "/", "-", -1) + + return componentSection, nil +} + +// ProcessComponentFromContext accepts context (tenant, environment, stage) and returns the component configuration in the stack +func ProcessComponentFromContext(component string, tenant string, environment string, stage string) (map[string]interface{}, error) { + var stack string + + err := c.InitConfig() + if err != nil { + return nil, err + } + + if len(c.Config.Stacks.NamePattern) < 1 { + return nil, errors.New("stack name pattern must be provided in 'stacks.name_pattern' config or 'ATMOS_STACKS_NAME_PATTERN' ENV variable") + } + + stackNamePatternParts := strings.Split(c.Config.Stacks.NamePattern, "-") + + for _, part := range stackNamePatternParts { + if part == "{tenant}" { + if len(tenant) == 0 { + return nil, errors.New(fmt.Sprintf("stack name pattern '%s' includes '{tenant}', but tenant is not provided", c.Config.Stacks.NamePattern)) + } + if len(stack) == 0 { + stack = tenant + } else { + stack = fmt.Sprintf("%s-%s", stack, tenant) + } + } else if part == "{environment}" { + if len(environment) == 0 { + return nil, errors.New(fmt.Sprintf("stack name pattern '%s' includes '{environment}', but environment is not provided", c.Config.Stacks.NamePattern)) + } + if len(stack) == 0 { + stack = environment + } else { + stack = fmt.Sprintf("%s-%s", stack, environment) + } + } else if part == "{stage}" { + if len(stage) == 0 { + return nil, errors.New(fmt.Sprintf("stack name pattern '%s' includes '{stage}', but stage is not provided", c.Config.Stacks.NamePattern)) + } + if len(stack) == 0 { + stack = stage + } else { + stack = fmt.Sprintf("%s-%s", stack, stage) + } + } + } + + return ProcessComponentInStack(component, stack) +} + +// findComponentConfig finds component config sections +func findComponentConfig( + stack string, + stacksMap map[string]interface{}, + componentType string, + component string, +) (map[string]interface{}, map[interface{}]interface{}, map[interface{}]interface{}, error) { + + var stackSection map[interface{}]interface{} + var componentsSection map[string]interface{} + var componentTypeSection map[string]interface{} + var componentSection map[string]interface{} + var componentVarsSection map[interface{}]interface{} + var componentBackendSection map[interface{}]interface{} + var ok bool + + if len(stack) == 0 { + return nil, nil, nil, errors.New("Stack must be provided and must not be empty") + } + if len(component) == 0 { + return nil, nil, nil, errors.New("Component must be provided and must not be empty") + } + if len(componentType) == 0 { + return nil, nil, nil, errors.New("Component type must be provided and must not be empty") + } + if stackSection, ok = stacksMap[stack].(map[interface{}]interface{}); !ok { + return nil, nil, nil, errors.New(fmt.Sprintf("Stack '%s' does not exist", stack)) + } + if componentsSection, ok = stackSection["components"].(map[string]interface{}); !ok { + return nil, nil, nil, errors.New(fmt.Sprintf("'components' section is missing in the stack '%s'", stack)) + } + if componentTypeSection, ok = componentsSection[componentType].(map[string]interface{}); !ok { + return nil, nil, nil, errors.New(fmt.Sprintf("'components/%s' section is missing in the stack '%s'", componentType, stack)) + } + if componentSection, ok = componentTypeSection[component].(map[string]interface{}); !ok { + return nil, nil, nil, errors.New(fmt.Sprintf("Invalid or missing configuration for the component '%s' in the stack '%s'", component, stack)) + } + if componentVarsSection, ok = componentSection["vars"].(map[interface{}]interface{}); !ok { + return nil, nil, nil, errors.New(fmt.Sprintf("Missing 'vars' section for the component '%s' in the stack '%s'", component, stack)) + } + if componentBackendSection, ok = componentSection["backend"].(map[interface{}]interface{}); !ok { + componentBackendSection = nil + } + + return componentSection, componentVarsSection, componentBackendSection, nil +} diff --git a/pkg/component/component_processor_test.go b/pkg/component/component_processor_test.go new file mode 100644 index 000000000..d8d35e5e0 --- /dev/null +++ b/pkg/component/component_processor_test.go @@ -0,0 +1,94 @@ +package component + +import ( + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" + "testing" +) + +func TestComponentProcessor(t *testing.T) { + var err error + var component string + var stack string + var yamlConfig []byte + + var tenant1Ue2DevTestTestComponent map[string]interface{} + component = "test/test-component" + stack = "tenant1-ue2-dev" + tenant1Ue2DevTestTestComponent, err = ProcessComponentInStack(component, stack) + assert.Nil(t, err) + tenant1Ue2DevTestTestComponentBackend := tenant1Ue2DevTestTestComponent["backend"].(map[interface{}]interface{}) + tenant1Ue2DevTestTestComponentBaseComponent := tenant1Ue2DevTestTestComponent["component"] + tenant1Ue2DevTestTestComponentWorkspace := tenant1Ue2DevTestTestComponent["workspace"].(string) + tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix := tenant1Ue2DevTestTestComponentBackend["workspace_key_prefix"].(string) + tenant1Ue2DevTestTestComponentDeps := tenant1Ue2DevTestTestComponent["deps"].([]string) + assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix) + assert.Nil(t, tenant1Ue2DevTestTestComponentBaseComponent) + assert.Equal(t, "tenant1-ue2-dev", tenant1Ue2DevTestTestComponentWorkspace) + assert.Equal(t, 7, len(tenant1Ue2DevTestTestComponentDeps)) + assert.Equal(t, "catalog/terraform/services/service-1", tenant1Ue2DevTestTestComponentDeps[0]) + assert.Equal(t, "catalog/terraform/services/service-2", tenant1Ue2DevTestTestComponentDeps[1]) + assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentDeps[2]) + assert.Equal(t, "globals/globals", tenant1Ue2DevTestTestComponentDeps[3]) + assert.Equal(t, "globals/tenant1-globals", tenant1Ue2DevTestTestComponentDeps[4]) + assert.Equal(t, "globals/ue2-globals", tenant1Ue2DevTestTestComponentDeps[5]) + assert.Equal(t, "tenant1/ue2/dev", tenant1Ue2DevTestTestComponentDeps[6]) + + var tenant1Ue2DevTestTestComponent2 map[string]interface{} + component = "test/test-component" + tenant := "tenant1" + environment := "ue2" + stage := "dev" + tenant1Ue2DevTestTestComponent2, err = ProcessComponentFromContext(component, tenant, environment, stage) + assert.Nil(t, err) + tenant1Ue2DevTestTestComponentBackend2 := tenant1Ue2DevTestTestComponent2["backend"].(map[interface{}]interface{}) + tenant1Ue2DevTestTestComponentBaseComponent2 := tenant1Ue2DevTestTestComponent2["component"] + tenant1Ue2DevTestTestComponentWorkspace2 := tenant1Ue2DevTestTestComponent2["workspace"].(string) + tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix2 := tenant1Ue2DevTestTestComponentBackend2["workspace_key_prefix"].(string) + tenant1Ue2DevTestTestComponentDeps2 := tenant1Ue2DevTestTestComponent2["deps"].([]string) + assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentBackendWorkspaceKeyPrefix2) + assert.Nil(t, tenant1Ue2DevTestTestComponentBaseComponent2) + assert.Equal(t, "tenant1-ue2-dev", tenant1Ue2DevTestTestComponentWorkspace2) + assert.Equal(t, 7, len(tenant1Ue2DevTestTestComponentDeps2)) + assert.Equal(t, "catalog/terraform/services/service-1", tenant1Ue2DevTestTestComponentDeps2[0]) + assert.Equal(t, "catalog/terraform/services/service-2", tenant1Ue2DevTestTestComponentDeps2[1]) + assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentDeps2[2]) + assert.Equal(t, "globals/globals", tenant1Ue2DevTestTestComponentDeps2[3]) + assert.Equal(t, "globals/tenant1-globals", tenant1Ue2DevTestTestComponentDeps2[4]) + assert.Equal(t, "globals/ue2-globals", tenant1Ue2DevTestTestComponentDeps2[5]) + assert.Equal(t, "tenant1/ue2/dev", tenant1Ue2DevTestTestComponentDeps2[6]) + + yamlConfig, err = yaml.Marshal(tenant1Ue2DevTestTestComponent) + assert.Nil(t, err) + t.Log(string(yamlConfig)) + + var tenant1Ue2DevTestTestComponentOverrideComponent map[string]interface{} + component = "test/test-component-override" + stack = "tenant1-ue2-dev" + tenant1Ue2DevTestTestComponentOverrideComponent, err = ProcessComponentInStack(component, stack) + assert.Nil(t, err) + tenant1Ue2DevTestTestComponentOverrideComponentBackend := tenant1Ue2DevTestTestComponentOverrideComponent["backend"].(map[interface{}]interface{}) + tenant1Ue2DevTestTestComponentOverrideComponentBaseComponent := tenant1Ue2DevTestTestComponentOverrideComponent["component"].(string) + tenant1Ue2DevTestTestComponentOverrideComponentWorkspace := tenant1Ue2DevTestTestComponentOverrideComponent["workspace"].(string) + tenant1Ue2DevTestTestComponentOverrideComponentBackendWorkspaceKeyPrefix := tenant1Ue2DevTestTestComponentOverrideComponentBackend["workspace_key_prefix"].(string) + tenant1Ue2DevTestTestComponentOverrideComponentDeps := tenant1Ue2DevTestTestComponentOverrideComponent["deps"].([]string) + assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentOverrideComponentBackendWorkspaceKeyPrefix) + assert.Equal(t, "test/test-component", tenant1Ue2DevTestTestComponentOverrideComponentBaseComponent) + assert.Equal(t, "tenant1-ue2-dev-test-test-component-override", tenant1Ue2DevTestTestComponentOverrideComponentWorkspace) + assert.Equal(t, 11, len(tenant1Ue2DevTestTestComponentOverrideComponentDeps)) + assert.Equal(t, "catalog/terraform/services/service-1", tenant1Ue2DevTestTestComponentOverrideComponentDeps[0]) + assert.Equal(t, "catalog/terraform/services/service-1-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[1]) + assert.Equal(t, "catalog/terraform/services/service-2", tenant1Ue2DevTestTestComponentOverrideComponentDeps[2]) + assert.Equal(t, "catalog/terraform/services/service-2-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[3]) + assert.Equal(t, "catalog/terraform/tenant1-ue2-dev", tenant1Ue2DevTestTestComponentOverrideComponentDeps[4]) + assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentOverrideComponentDeps[5]) + assert.Equal(t, "catalog/terraform/test-component-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[6]) + assert.Equal(t, "globals/globals", tenant1Ue2DevTestTestComponentOverrideComponentDeps[7]) + assert.Equal(t, "globals/tenant1-globals", tenant1Ue2DevTestTestComponentOverrideComponentDeps[8]) + assert.Equal(t, "globals/ue2-globals", tenant1Ue2DevTestTestComponentOverrideComponentDeps[9]) + assert.Equal(t, "tenant1/ue2/dev", tenant1Ue2DevTestTestComponentOverrideComponentDeps[10]) + + yamlConfig, err = yaml.Marshal(tenant1Ue2DevTestTestComponentOverrideComponent) + assert.Nil(t, err) + t.Log(string(yamlConfig)) +} diff --git a/pkg/merge/merge.go b/pkg/merge/merge.go index e20d9b29b..1c7786210 100644 --- a/pkg/merge/merge.go +++ b/pkg/merge/merge.go @@ -5,8 +5,8 @@ import ( "gopkg.in/yaml.v2" ) -// Merge takes a list of maps of interface as input and returns a single map with the merged contents -func Merge(inputs []map[interface{}]interface{}) (map[interface{}]interface{}, error) { +// MergeWithOptions takes a list of maps of interface and options as input and returns a single map with the merged contents +func MergeWithOptions(inputs []map[interface{}]interface{}, appendSlice, sliceDeepCopy bool) (map[interface{}]interface{}, error) { merged := map[interface{}]interface{}{} for index := range inputs { @@ -27,10 +27,26 @@ func Merge(inputs []map[interface{}]interface{}) (map[interface{}]interface{}, e return nil, err } - if err = mergo.Merge(&merged, dataCurrent, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue, mergo.WithTypeCheck); err != nil { + var opts []func(*mergo.Config) + opts = append(opts, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue, mergo.WithTypeCheck) + + if appendSlice { + opts = append(opts, mergo.WithAppendSlice) + } + + if sliceDeepCopy { + opts = append(opts, mergo.WithSliceDeepCopy) + } + + if err = mergo.Merge(&merged, dataCurrent, opts...); err != nil { return nil, err } } return merged, nil } + +// Merge takes a list of maps of interface as input and returns a single map with the merged contents +func Merge(inputs []map[interface{}]interface{}) (map[interface{}]interface{}, error) { + return MergeWithOptions(inputs, false, false) +} diff --git a/pkg/spacelift/spacelift_stack_processor.go b/pkg/spacelift/spacelift_stack_processor.go index 41c7d6815..a42cf066e 100644 --- a/pkg/spacelift/spacelift_stack_processor.go +++ b/pkg/spacelift/spacelift_stack_processor.go @@ -159,7 +159,7 @@ func TransformStackConfigToSpaceliftStacks( } else { workspace = fmt.Sprintf("%s-%s", stackName, component) } - spaceliftConfig["workspace"] = workspace + spaceliftConfig["workspace"] = strings.Replace(workspace, "/", "-", -1) // labels labels := []string{} @@ -189,7 +189,12 @@ func TransformStackConfigToSpaceliftStacks( } labels = append(labels, fmt.Sprintf("folder:component/%s", component)) - labels = append(labels, fmt.Sprintf("folder:%s", stackName)) + + stackFolder := stackName + if !strings.Contains(stackName, "/") { + stackFolder = strings.Replace(stackName, "-", "/", -1) + } + labels = append(labels, fmt.Sprintf("folder:%s", stackFolder)) spaceliftConfig["labels"] = utils.UniqueStrings(labels) diff --git a/pkg/spacelift/spacelift_stack_processor_test.go b/pkg/spacelift/spacelift_stack_processor_test.go index 540b6e2ab..2aa324e30 100644 --- a/pkg/spacelift/spacelift_stack_processor_test.go +++ b/pkg/spacelift/spacelift_stack_processor_test.go @@ -27,6 +27,40 @@ func TestSpaceliftStackProcessor(t *testing.T) { var spaceliftStacks, err = CreateSpaceliftStacks(basePath, filePaths, processStackDeps, processComponentDeps, processImports, stackConfigPathTemplate) assert.Nil(t, err) + tenant1Ue2DevInfraVpcStack := spaceliftStacks["tenant1-ue2-dev-infra-vpc"].(map[string]interface{}) + tenant1Ue2DevInfraVpcStackBackend := tenant1Ue2DevInfraVpcStack["backend"].(map[interface{}]interface{}) + tenant1Ue2DevInfraVpcStackBackendWorkspaceKeyPrefix := tenant1Ue2DevInfraVpcStackBackend["workspace_key_prefix"].(string) + assert.Equal(t, "infra-vpc", tenant1Ue2DevInfraVpcStackBackendWorkspaceKeyPrefix) + + tenant1Ue2DevTestTestComponentOverrideComponent := spaceliftStacks["tenant1-ue2-dev-test-test-component-override"].(map[string]interface{}) + tenant1Ue2DevTestTestComponentOverrideComponentBackend := tenant1Ue2DevTestTestComponentOverrideComponent["backend"].(map[interface{}]interface{}) + tenant1Ue2DevTestTestComponentOverrideComponentBaseComponent := tenant1Ue2DevTestTestComponentOverrideComponent["base_component"].(string) + tenant1Ue2DevTestTestComponentOverrideComponentBackendWorkspaceKeyPrefix := tenant1Ue2DevTestTestComponentOverrideComponentBackend["workspace_key_prefix"].(string) + tenant1Ue2DevTestTestComponentOverrideComponentDeps := tenant1Ue2DevTestTestComponentOverrideComponent["deps"].([]string) + tenant1Ue2DevTestTestComponentOverrideComponentLabels := tenant1Ue2DevTestTestComponentOverrideComponent["labels"].([]string) + assert.Equal(t, "test-test-component", tenant1Ue2DevTestTestComponentOverrideComponentBackendWorkspaceKeyPrefix) + assert.Equal(t, "test/test-component", tenant1Ue2DevTestTestComponentOverrideComponentBaseComponent) + assert.Equal(t, 11, len(tenant1Ue2DevTestTestComponentOverrideComponentDeps)) + assert.Equal(t, "catalog/terraform/services/service-1", tenant1Ue2DevTestTestComponentOverrideComponentDeps[0]) + assert.Equal(t, "catalog/terraform/services/service-1-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[1]) + assert.Equal(t, "catalog/terraform/services/service-2", tenant1Ue2DevTestTestComponentOverrideComponentDeps[2]) + assert.Equal(t, "catalog/terraform/services/service-2-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[3]) + assert.Equal(t, "catalog/terraform/tenant1-ue2-dev", tenant1Ue2DevTestTestComponentOverrideComponentDeps[4]) + assert.Equal(t, "catalog/terraform/test-component", tenant1Ue2DevTestTestComponentOverrideComponentDeps[5]) + assert.Equal(t, "catalog/terraform/test-component-override", tenant1Ue2DevTestTestComponentOverrideComponentDeps[6]) + assert.Equal(t, "globals/globals", tenant1Ue2DevTestTestComponentOverrideComponentDeps[7]) + assert.Equal(t, "globals/tenant1-globals", tenant1Ue2DevTestTestComponentOverrideComponentDeps[8]) + assert.Equal(t, "globals/ue2-globals", tenant1Ue2DevTestTestComponentOverrideComponentDeps[9]) + assert.Equal(t, "tenant1/ue2/dev", tenant1Ue2DevTestTestComponentOverrideComponentDeps[10]) + assert.Equal(t, 32, len(tenant1Ue2DevTestTestComponentOverrideComponentLabels)) + assert.Equal(t, "deps:stacks/catalog/terraform/test-component-override.yaml", tenant1Ue2DevTestTestComponentOverrideComponentLabels[25]) + assert.Equal(t, "deps:stacks/globals/globals.yaml", tenant1Ue2DevTestTestComponentOverrideComponentLabels[26]) + assert.Equal(t, "deps:stacks/globals/tenant1-globals.yaml", tenant1Ue2DevTestTestComponentOverrideComponentLabels[27]) + assert.Equal(t, "deps:stacks/globals/ue2-globals.yaml", tenant1Ue2DevTestTestComponentOverrideComponentLabels[28]) + assert.Equal(t, "deps:stacks/tenant1/ue2/dev.yaml", tenant1Ue2DevTestTestComponentOverrideComponentLabels[29]) + assert.Equal(t, "folder:component/test/test-component-override", tenant1Ue2DevTestTestComponentOverrideComponentLabels[30]) + assert.Equal(t, "folder:tenant1/ue2/dev", tenant1Ue2DevTestTestComponentOverrideComponentLabels[31]) + yamlSpaceliftStacks, err := yaml.Marshal(spaceliftStacks) assert.Nil(t, err) t.Log(string(yamlSpaceliftStacks)) diff --git a/pkg/stack/stack_processor.go b/pkg/stack/stack_processor.go index 6490a8495..8457c693a 100644 --- a/pkg/stack/stack_processor.go +++ b/pkg/stack/stack_processor.go @@ -226,9 +226,6 @@ func ProcessConfig( helmfileSettings := map[interface{}]interface{}{} helmfileEnv := map[interface{}]interface{}{} - backendType := "s3" - backend := map[interface{}]interface{}{} - terraformComponents := map[string]interface{}{} helmfileComponents := map[string]interface{}{} allComponents := map[string]interface{}{} @@ -286,14 +283,14 @@ func ProcessConfig( return nil, err } + // Global backend + globalBackendType := "" + globalBackendSection := map[interface{}]interface{}{} if i, ok := globalTerraformSection["backend_type"]; ok { - backendType = i.(string) + globalBackendType = i.(string) } - if i, ok := globalTerraformSection["backend"]; ok { - if backendSection, backendSectionExist := i.(map[interface{}]interface{})[backendType]; backendSectionExist { - backend = backendSection.(map[interface{}]interface{}) - } + globalBackendSection = i.(map[interface{}]interface{}) } // Helmfile section @@ -347,9 +344,14 @@ func ProcessConfig( componentEnv = i.(map[interface{}]interface{}) } - componentBackend := map[interface{}]interface{}{} + // Component backend + componentBackendType := "" + componentBackendSection := map[interface{}]interface{}{} + if i, ok2 := componentMap["backend_type"]; ok2 { + componentBackendType = i.(string) + } if i, ok2 := componentMap["backend"]; ok2 { - componentBackend = i.(map[interface{}]interface{})[backendType].(map[interface{}]interface{}) + componentBackendSection = i.(map[interface{}]interface{}) } componentTerraformCommand := "terraform" @@ -360,9 +362,10 @@ func ProcessConfig( baseComponentVars := map[interface{}]interface{}{} baseComponentSettings := map[interface{}]interface{}{} baseComponentEnv := map[interface{}]interface{}{} - baseComponentBackend := map[interface{}]interface{}{} baseComponentName := "" baseComponentTerraformCommand := "" + baseComponentBackendType := "" + baseComponentBackendSection := map[interface{}]interface{}{} if baseComponent, baseComponentExist := componentMap["component"]; baseComponentExist { baseComponentName = baseComponent.(string) @@ -382,10 +385,12 @@ func ProcessConfig( baseComponentEnv = baseComponentEnvSection.(map[interface{}]interface{}) } - if baseComponentBackendSection, baseComponentBackendSectionExist := baseComponentMap["backend"]; baseComponentBackendSectionExist { - if backendTypeSection, backendTypeSectionExist := baseComponentBackendSection.(map[interface{}]interface{})[backendType]; backendTypeSectionExist { - baseComponentBackend = backendTypeSection.(map[interface{}]interface{}) - } + // Base component backend + if i, ok2 := baseComponentMap["backend_type"]; ok2 { + baseComponentBackendType = i.(string) + } + if i, ok2 := baseComponentMap["backend"]; ok2 { + baseComponentBackendSection = i.(map[interface{}]interface{}) } if baseComponentCommandSection, baseComponentCommandSectionExist := baseComponentMap["command"]; baseComponentCommandSectionExist { @@ -412,20 +417,34 @@ func ProcessConfig( return nil, err } - finalComponentBackend, err := m.Merge([]map[interface{}]interface{}{backend, baseComponentBackend, componentBackend}) + finalComponentBackendType := globalBackendType + if len(baseComponentBackendType) > 0 { + finalComponentBackendType = baseComponentBackendType + } + if len(componentBackendType) > 0 { + finalComponentBackendType = componentBackendType + } + + finalComponentBackendSection, err := m.Merge([]map[interface{}]interface{}{globalBackendSection, baseComponentBackendSection, componentBackendSection}) if err != nil { return nil, err } + finalComponentBackend := map[interface{}]interface{}{} + if i, ok2 := finalComponentBackendSection[finalComponentBackendType]; ok2 { + finalComponentBackend = i.(map[interface{}]interface{}) + } + finalComponentTerraformCommand := componentTerraformCommand if len(baseComponentTerraformCommand) > 0 { finalComponentTerraformCommand = baseComponentTerraformCommand } + comp := map[string]interface{}{} comp["vars"] = finalComponentVars comp["settings"] = finalComponentSettings comp["env"] = finalComponentEnv - comp["backend_type"] = backendType + comp["backend_type"] = finalComponentBackendType comp["backend"] = finalComponentBackend comp["command"] = finalComponentTerraformCommand diff --git a/pkg/stack/stack_processor_test.go b/pkg/stack/stack_processor_test.go index bb46dd498..7cbff5e82 100644 --- a/pkg/stack/stack_processor_test.go +++ b/pkg/stack/stack_processor_test.go @@ -3,10 +3,9 @@ package stack import ( c "github.com/cloudposse/atmos/pkg/convert" u "github.com/cloudposse/atmos/pkg/utils" - "testing" - "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" + "testing" ) func TestStackProcessor(t *testing.T) { @@ -57,6 +56,21 @@ func TestStackProcessor(t *testing.T) { assert.Equal(t, "globals/tenant1-globals", imports[14]) assert.Equal(t, "globals/ue2-globals", imports[15]) + components := mapConfig1["components"].(map[interface{}]interface{}) + terraformComponents := components["terraform"].(map[interface{}]interface{}) + + infraVpcComponent := terraformComponents["infra/vpc"].(map[interface{}]interface{}) + infraVpcComponentBackend := infraVpcComponent["backend"].(map[interface{}]interface{}) + infraVpcComponentBackendWorkspaceKeyPrefix := infraVpcComponentBackend["workspace_key_prefix"].(string) + assert.Equal(t, "infra-vpc", infraVpcComponentBackendWorkspaceKeyPrefix) + + testTestComponentOverrideComponent := terraformComponents["test/test-component-override"].(map[interface{}]interface{}) + testTestComponentOverrideComponentBackend := testTestComponentOverrideComponent["backend"].(map[interface{}]interface{}) + testTestComponentOverrideComponentBaseComponent := testTestComponentOverrideComponent["component"].(string) + testTestComponentOverrideComponentBackendWorkspaceKeyPrefix := testTestComponentOverrideComponentBackend["workspace_key_prefix"].(string) + assert.Equal(t, "test-test-component", testTestComponentOverrideComponentBackendWorkspaceKeyPrefix) + assert.Equal(t, "test/test-component", testTestComponentOverrideComponentBaseComponent) + yamlConfig, err := yaml.Marshal(mapConfig1) assert.Nil(t, err) t.Log(string(yamlConfig)) diff --git a/pkg/stack/stack_processor_utils.go b/pkg/stack/stack_processor_utils.go index 9c8dc65b0..04b51c147 100644 --- a/pkg/stack/stack_processor_utils.go +++ b/pkg/stack/stack_processor_utils.go @@ -155,7 +155,7 @@ func CreateComponentStackMap(basePath string, filePath string) (map[string]map[s if componentsConfig, componentsConfigExists := finalConfig["components"]; componentsConfigExists { componentsSection := componentsConfig.(map[string]interface{}) - stackName := strings.Replace(p, dir+"/", "", 1) + stackName := strings.Replace(p, basePath+"/", "", 1) if terraformConfig, terraformConfigExists := componentsSection["terraform"]; terraformConfigExists { terraformSection := terraformConfig.(map[string]interface{}) diff --git a/pkg/utils/string_utils.go b/pkg/utils/string_utils.go index 562e91b0b..8bc20dd04 100644 --- a/pkg/utils/string_utils.go +++ b/pkg/utils/string_utils.go @@ -1,10 +1,5 @@ package utils -import ( - "strconv" - "strings" -) - // UniqueStrings returns a unique subset of the string slice provided func UniqueStrings(input []string) []string { u := make([]string, 0, len(input)) @@ -19,9 +14,3 @@ func UniqueStrings(input []string) []string { return u } - -// UnquoteCodePoint converts a rune specified in the format \Uxxxxxxxx to the actual rune -func UnquoteCodePoint(s string) (string, error) { - r, err := strconv.ParseInt(strings.TrimPrefix(s, "\\U"), 16, 32) - return string(r), err -}