Skip to content

Commit

Permalink
Update Spacelift deps. Update atmos validate stacks command. Update…
Browse files Browse the repository at this point in the history
… docs (#418)

* updates

* update docs

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates
  • Loading branch information
aknysh authored Aug 17, 2023
1 parent cfb94ba commit 39d33c7
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 48 deletions.
4 changes: 2 additions & 2 deletions examples/complete/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ ARG GEODESIC_VERSION=2.2.4
ARG GEODESIC_OS=debian

# atmos: https://github.com/cloudposse/atmos
ARG ATMOS_VERSION=1.42.0
ARG ATMOS_VERSION=1.44.0

# Terraform: https://github.com/hashicorp/terraform/releases
ARG TF_VERSION=1.5.3
ARG TF_VERSION=1.5.5

FROM cloudposse/geodesic:${GEODESIC_VERSION}-${GEODESIC_OS}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/hashicorp/go-getter v1.7.2
github.com/hashicorp/hcl v1.0.0
github.com/hashicorp/hcl/v2 v2.17.0
github.com/hashicorp/terraform-config-inspect v0.0.0-20230614215431-f32df32a01cd
github.com/hashicorp/terraform-config-inspect v0.0.0-20230808231734-f15f31bf62b3
github.com/imdario/mergo v0.3.13
github.com/json-iterator/go v1.1.12
github.com/mitchellh/go-homedir v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY=
github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4=
github.com/hashicorp/terraform-config-inspect v0.0.0-20230614215431-f32df32a01cd h1:1uPcotqoL4TjcGKlgIe7OFSRplf7BMVtUjekwmCrvuM=
github.com/hashicorp/terraform-config-inspect v0.0.0-20230614215431-f32df32a01cd/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg=
github.com/hashicorp/terraform-config-inspect v0.0.0-20230808231734-f15f31bf62b3 h1:1uP3RA50ayEcTrHJtHaqubpW66KkXKIYXHP1+79dbMc=
github.com/hashicorp/terraform-config-inspect v0.0.0-20230808231734-f15f31bf62b3/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/describe_dependents.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func ExecuteDescribeDependents(
return nil, err
}

// Skip if the stack component has an empty `settings.dependencies.depends_on` section
// Skip if the stack component has an empty `settings.depends_on` section
if reflect.ValueOf(stackComponentSettings).IsZero() ||
reflect.ValueOf(stackComponentSettings.DependsOn).IsZero() {
continue
Expand Down
10 changes: 6 additions & 4 deletions internal/exec/stack_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ func BuildDependentStackNameFromDependsOn(
) (string, error) {
var dependentStackName string

if u.SliceContainsString(allStackNames, dependsOn) {
dependentStackName = dependsOn
} else if u.SliceContainsString(componentNamesInCurrentStack, dependsOn) {
dependentStackName = fmt.Sprintf("%s-%s", currentStackName, dependsOn)
dep := strings.Replace(dependsOn, "/", "-", -1)

if u.SliceContainsString(allStackNames, dep) {
dependentStackName = dep
} else if u.SliceContainsString(componentNamesInCurrentStack, dep) {
dependentStackName = fmt.Sprintf("%s-%s", currentStackName, dep)
} else {
errorMessage := fmt.Errorf("the component '%[1]s' in the stack '%[2]s' specifies 'depends_on' dependency '%[3]s', "+
"but '%[3]s' is not a stack and not a component in the '%[2]s' stack",
Expand Down
75 changes: 67 additions & 8 deletions pkg/spacelift/spacelift_stack_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"

e "github.com/cloudposse/atmos/internal/exec"
Expand Down Expand Up @@ -63,7 +64,13 @@ func CreateSpaceliftStacks(
return nil, err
}

return TransformStackConfigToSpaceliftStacks(stacks, stackConfigPathTemplate, cliConfig.Stacks.NamePattern, processImports, rawStackConfigs)
return TransformStackConfigToSpaceliftStacks(
stacks,
stackConfigPathTemplate,
cliConfig.Stacks.NamePattern,
processImports,
rawStackConfigs,
)
}
}

Expand Down Expand Up @@ -129,11 +136,6 @@ func TransformStackConfigToSpaceliftStacks(
spaceliftExplicitLabels = i.([]any)
}

spaceliftDependsOn := []any{}
if i, ok2 := spaceliftSettings["depends_on"]; ok2 {
spaceliftDependsOn = i.([]any)
}

spaceliftConfig := map[string]any{}
spaceliftConfig["enabled"] = spaceliftWorkspaceEnabled

Expand Down Expand Up @@ -264,9 +266,15 @@ func TransformStackConfigToSpaceliftStacks(
terraformComponentNamesInCurrentStack = append(terraformComponentNamesInCurrentStack, strings.Replace(v, "/", "-", -1))
}

for _, v := range spaceliftDependsOn {
// Legacy/deprecated `settings.spacelift.depends_on`
spaceliftDependsOn := []any{}
if i, ok2 := spaceliftSettings["depends_on"]; ok2 {
spaceliftDependsOn = i.([]any)
}

for _, dep := range spaceliftDependsOn {
spaceliftStackNameDependsOn, err := e.BuildDependentStackNameFromDependsOn(
v.(string),
dep.(string),
allStackNames,
contextPrefix,
terraformComponentNamesInCurrentStack,
Expand All @@ -278,6 +286,57 @@ func TransformStackConfigToSpaceliftStacks(
labels = append(labels, fmt.Sprintf("depends-on:%s", spaceliftStackNameDependsOn))
}

// Recommended `settings.depends_on`
var stackComponentSettingsDependsOn schema.Settings
err = mapstructure.Decode(componentSettings, &stackComponentSettingsDependsOn)
if err != nil {
return nil, err
}

for _, stackComponentSettingsDependsOnContext := range stackComponentSettingsDependsOn.DependsOn {
if stackComponentSettingsDependsOnContext.Namespace == "" {
stackComponentSettingsDependsOnContext.Namespace = context.Namespace
}
if stackComponentSettingsDependsOnContext.Tenant == "" {
stackComponentSettingsDependsOnContext.Tenant = context.Tenant
}
if stackComponentSettingsDependsOnContext.Environment == "" {
stackComponentSettingsDependsOnContext.Environment = context.Environment
}
if stackComponentSettingsDependsOnContext.Stage == "" {
stackComponentSettingsDependsOnContext.Stage = context.Stage
}

var contextPrefixDependsOn string

if stackNamePattern != "" {
contextPrefixDependsOn, err = cfg.GetContextPrefix(
stackName,
stackComponentSettingsDependsOnContext,
stackNamePattern,
stackName,
)
if err != nil {
return nil, err
}
} else {
contextPrefixDependsOn = strings.Replace(stackName, "/", "-", -1)
}

spaceliftStackNameDependsOn, err := e.BuildDependentStackNameFromDependsOn(
stackComponentSettingsDependsOnContext.Component,
allStackNames,
contextPrefixDependsOn,
terraformComponentNamesInCurrentStack,
component)
if err != nil {
u.LogError(err)
return nil, err
}
labels = append(labels, fmt.Sprintf("depends-on:%s", spaceliftStackNameDependsOn))
}

// Add `component` and `folder` labels
labels = append(labels, fmt.Sprintf("folder:component/%s", component))
labels = append(labels, fmt.Sprintf("folder:%s", strings.Replace(contextPrefix, "-", "/", -1)))

Expand Down
8 changes: 7 additions & 1 deletion pkg/spacelift/spacelift_stack_processor_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package spacelift

import (
"gopkg.in/yaml.v2"
"testing"

"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)

func TestSpaceliftStackProcessor(t *testing.T) {
Expand Down Expand Up @@ -91,6 +91,12 @@ func TestSpaceliftStackProcessor(t *testing.T) {
newTenant1Ue2Test1TestTestComponentOverrideComponent2InfrastructureStackName := newTenant1Ue2Test1TestTestComponentOverrideComponent2["stack"].(string)
assert.Equal(t, "tenant1-ue2-test-1", newTenant1Ue2Test1TestTestComponentOverrideComponent2InfrastructureStackName)

// Test `settings.depends_on`
tenant1Ue2ProdTopLevelComponent1 := spaceliftStacks["tenant1-ue2-prod-top-level-component1"].(map[string]any)
tenant1Ue2ProdTopLevelComponent1Labels := tenant1Ue2ProdTopLevelComponent1["labels"].([]string)
assert.Equal(t, "depends-on:tenant1-ue2-prod-test-test-component-override", tenant1Ue2ProdTopLevelComponent1Labels[35])
assert.Equal(t, "depends-on:tenant1-ue2-dev-test-test-component", tenant1Ue2ProdTopLevelComponent1Labels[36])

yamlSpaceliftStacks, err := yaml.Marshal(spaceliftStacks)
assert.Nil(t, err)
t.Log(string(yamlSpaceliftStacks))
Expand Down
48 changes: 37 additions & 11 deletions pkg/stack/stack_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"sort"
"strings"
"sync"
"text/template"
"text/template/parse"

"github.com/pkg/errors"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -211,18 +213,42 @@ func ProcessYAMLConfigFile(
if err != nil || len(importMatches) == 0 {
// Retry (b/c we are using `doublestar` library and it sometimes has issues reading many files in a Docker container)
// TODO: review `doublestar` library

importMatches, err = u.GetGlobMatches(impWithExtPath)
if err != nil {
errorMessage := fmt.Sprintf("no matches found for the import '%s' in the file '%s'\nError: %s",
imp,
relativeFilePath,
err)
return nil, nil, nil, errors.New(errorMessage)
} else if importMatches == nil {
errorMessage := fmt.Sprintf("invalid import in the file '%s'\nNo matches found for the import '%s'",
relativeFilePath,
imp)
return nil, nil, nil, errors.New(errorMessage)
if err != nil || len(importMatches) == 0 {
// The import was not found -> check if the import is a Go template; if not, return the error
t, err2 := template.New(imp).Parse(imp)
if err2 != nil {
return nil, nil, nil, err2
}

isGoTemplate := false

// Iterate over all nodes in the template and check if any of them is of type `NodeAction` (field evaluation)
for _, node := range t.Root.Nodes {
if node.Type() == parse.NodeAction {
isGoTemplate = true
break
}
}

// If the import is not a Go template, return the error
if !isGoTemplate {
if err != nil {
errorMessage := fmt.Sprintf("no matches found for the import '%s' in the file '%s'\nError: %s",
imp,
relativeFilePath,
err,
)
return nil, nil, nil, errors.New(errorMessage)
} else if importMatches == nil {
errorMessage := fmt.Sprintf("no matches found for the import '%s' in the file '%s'",
imp,
relativeFilePath,
)
return nil, nil, nil, errors.New(errorMessage)
}
}
}
}

Expand Down
78 changes: 72 additions & 6 deletions website/docs/core-concepts/stacks/imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ Use [mixins](/core-concepts/stacks/mixins) for reusable snippets of configuratio

## Imports Schema

The `import` section supports the following two formats (note that only one format is supported in a stack config file, but different stack
configs can use one format or the other):
The `import` section supports the following two formats:

- a list of paths to the imported files, for example:

Expand Down Expand Up @@ -82,18 +81,43 @@ where:
- `context` - an optional freeform map of context variables that are applied as template variables to the imported file (if the imported file is
a [Go template](https://pkg.go.dev/text/template))

<br/>

:::note

In a stack config file, you can use only one of the supported formats. Using the two formats at the same time in a single stack config file is not
currently supported. If you are importing a template file and providing a `context` to it, you have to use the same schema with the `path` to the
files to import all other configs even if they don't use templates and don't require a `context`.

:::

For example:

```yaml
import:
- path: catalog/terraform/eks_cluster_tmpl
context:
flavor: "blue"
enabled: true
service_1_name: "blue-service-1"
service_2_name: "blue-service-2"
- path: catalog/terraform/test-component
- path: catalog/terraform/vpc
- path: catalog/helmfile/echo-server
```

## `Go` Templates in Imports

Atmos supports all the functionality of the [Go templates](https://pkg.go.dev/text/template) in imported stack configurations, including
[functions](https://pkg.go.dev/text/template#hdr-Functions) and [Sprig functions](http://masterminds.github.io/sprig/).
Atmos supports all the functionality of [Go templates](https://pkg.go.dev/text/template) in imported stack configurations, including
[functions](https://pkg.go.dev/text/template#hdr-Functions) and [Sprig functions](http://masterminds.github.io/sprig/).

Stack configurations can be templatized and then reused with different settings provided via the import `context` section.

For example, we can define the following configuration for EKS Atmos components in the `catalog/terraform/eks_cluster_tmpl.yaml` template file:

```yaml title=stacks/catalog/terraform/eks_cluster_tmpl.yaml
# Imports can also be parameterized using `Go` templates
import: []
import: [ ]

components:
terraform:
Expand Down Expand Up @@ -293,7 +317,7 @@ atmos terraform apply eks-blue/cluster -s tenant1-uw1-test-1
atmos terraform apply eks-green/cluster -s tenant1-uw1-test-1
```

All the parameterized variables get their values from the all the hierarchical `context` settings:
All the parameterized variables get their values from the hierarchical `context` settings:

```yaml title="atmos describe component eks-blue/cluster -s tenant1-uw1-test-1"
vars:
Expand All @@ -310,6 +334,48 @@ vars:
tenant: tenant1
```

<br/>

## Advanced Examples of Templates in Atmos Configurations

Atmos supports all the functionality of [Go templates](https://pkg.go.dev/text/template), including [functions](https://pkg.go.dev/text/template#hdr-Functions) and [Sprig functions](http://masterminds.github.io/sprig/).
The Sprig library provides over 70 template functions for `Go's` template language.

The following example shows how to dynamically include a variable in the Atmos component configuration by using the `hasKey` Sprig function.
The hasKey function returns `true` if the given dictionary contains the given key.

```yaml
components:
terraform:
eks/iam-role/{{ .app_name }}/{{ .service_environment }}:
metadata:
component: eks/iam-role
settings:
spacelift:
workspace_enabled: true
vars:
enabled: {{ .enabled }}
tags:
Service: {{ .app_name }}
service_account_name: {{ .app_name }}
service_account_namespace: {{ .service_account_namespace }}
{{ if hasKey . "iam_managed_policy_arns" }}
iam_managed_policy_arns:
{{ range $i, $iam_managed_policy_arn := .iam_managed_policy_arns }}
- '{{ $iam_managed_policy_arn }}'
{{ end }}
{{- end }}
{{ if hasKey . "iam_source_policy_documents" }}
iam_source_policy_documents:
{{ range $i, $iam_source_policy_document := .iam_source_policy_documents }}
- '{{ $iam_source_policy_document }}'
{{ end }}
{{- end }}
```

The `iam_managed_policy_arns` and `iam_source_policy_documents` variables will be included in the component configuration only if the
provided `context` object has the `iam_managed_policy_arns` and `iam_source_policy_documents` fields.

## Summary

Using imports with context (and hierarchical imports with context) with parameterized config files will help you make the configurations
Expand Down
Loading

0 comments on commit 39d33c7

Please sign in to comment.