diff --git a/atmos.yaml b/atmos.yaml index 61b0876b8..73785f4fc 100644 --- a/atmos.yaml +++ b/atmos.yaml @@ -128,3 +128,66 @@ commands: steps: - echo Playing ping-pong... - echo pong + +# Integrations +integrations: + + # Atlantis integration + # https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html + atlantis: + # Path and name of the Atlantis config file `atlantis.yaml` + # Supports absolute and relative paths + # All the intermediate folders will be created automatically (e.g. `path: /config/atlantis/atlantis.yaml`) + # Can be overridden on the command line by using `--output-path` command-line argument in `atmos atlantis generate repo-config` command + # If not specified (set to an empty string/omitted here, and set to an empty string on the command line), the content of the file will be dumped to `stdout` + # On Linux/macOS, you can also use `--output-path=/dev/stdout` to dump the content to `stdout` without setting it to an empty string in `atlantis.path` + path: "atlantis.yaml" + + # Config templates + # Select a template by using the `--config-template ` command-line argument in `atmos atlantis generate repo-config` command + config_templates: + config-1: + version: 3 + automerge: true + delete_source_branch_on_merge: true + parallel_plan: true + parallel_apply: true + allowed_regexp_prefixes: + - dev/ + - staging/ + - prod/ + + # Project templates + # Select a template by using the `--project-template ` command-line argument in `atmos atlantis generate repo-config` command + project_templates: + project-1: + # generate a project entry for each component in every stack + name: "{tenant}-{environment}-{stage}-{component}" + workspace: "{workspace}" + dir: "{component-path}" + terraform_version: v1.2 + delete_source_branch_on_merge: true + autoplan: + enabled: true + when_modified: + - "**/*.tf" + - "varfiles/$PROJECT_NAME.tfvars.json" + apply_requirements: + - "approved" + + # Workflow templates + # https://www.runatlantis.io/docs/custom-workflows.html#custom-init-plan-apply-commands + # https://www.runatlantis.io/docs/custom-workflows.html#custom-run-command + # Select a template by using the `--workflow-template ` command-line argument in `atmos atlantis generate repo-config` command + workflow_templates: + workflow-1: + plan: + steps: + - run: terraform init -input=false + # When using workspaces, you need to select the workspace using the $WORKSPACE environment variable + - run: terraform workspace select $WORKSPACE + # You must output the plan using `-out $PLANFILE` because Atlantis expects plans to be in a specific location + - run: terraform plan -input=false -refresh -out $PLANFILE -var-file varfiles/$PROJECT_NAME.tfvars.json + apply: + steps: + - run: terraform apply $PLANFILE diff --git a/cmd/atlantis.go b/cmd/atlantis.go new file mode 100644 index 000000000..cefefa8b5 --- /dev/null +++ b/cmd/atlantis.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// atlantisCmd executes Atlantis commands +var atlantisCmd = &cobra.Command{ + Use: "atlantis", + Short: "Execute 'atlantis' commands", + Long: `This command executes Atlantis integration commands`, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, +} + +func init() { + RootCmd.AddCommand(atlantisCmd) +} diff --git a/cmd/atlantis_generate.go b/cmd/atlantis_generate.go new file mode 100644 index 000000000..5fe98114f --- /dev/null +++ b/cmd/atlantis_generate.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// atlantisGenerateCmd generates various Atlantis configurations +var atlantisGenerateCmd = &cobra.Command{ + Use: "generate", + Short: "Execute 'atlantis generate' commands", + Long: "This command generates various Atlantis configurations", + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, +} + +func init() { + atlantisCmd.AddCommand(atlantisGenerateCmd) +} diff --git a/cmd/atlantis_generate_repo_config.go b/cmd/atlantis_generate_repo_config.go new file mode 100644 index 000000000..1cc88a43b --- /dev/null +++ b/cmd/atlantis_generate_repo_config.go @@ -0,0 +1,61 @@ +package cmd + +import ( + e "github.com/cloudposse/atmos/internal/exec" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/spf13/cobra" +) + +// atlantisGenerateRepoConfigCmd generates repository configuration for Atlantis +var atlantisGenerateRepoConfigCmd = &cobra.Command{ + Use: "repo-config", + Short: "Execute 'atlantis generate repo-config`", + Long: "This command generates repository configuration for Atlantis", + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true}, + Run: func(cmd *cobra.Command, args []string) { + err := e.ExecuteAtlantisGenerateRepoConfigCmd(cmd, args) + if err != nil { + u.PrintErrorToStdErrorAndExit(err) + } + }, +} + +func init() { + atlantisGenerateRepoConfigCmd.DisableFlagParsing = false + + atlantisGenerateRepoConfigCmd.PersistentFlags().String("output-path", "", "atmos atlantis generate repo-config --output-path ./atmos.yaml --config-template config-1 --project-template project-1 --workflow-template workflow-1") + atlantisGenerateRepoConfigCmd.PersistentFlags().String("config-template", "", "atmos atlantis generate repo-config --config-template config-1 --project-template project-1 --workflow-template workflow-1") + atlantisGenerateRepoConfigCmd.PersistentFlags().String("project-template", "", "atmos atlantis generate repo-config --config-template config-1 --project-template project-1 --workflow-template workflow-1") + atlantisGenerateRepoConfigCmd.PersistentFlags().String("workflow-template", "", "atmos atlantis generate repo-config --config-template config-1 --project-template project-1 --workflow-template workflow-1") + + atlantisGenerateRepoConfigCmd.PersistentFlags().String("stacks", "", + "Only generate config for the specified stacks (comma-separated values).\n"+ + "atmos atlantis generate repo-config --config-template --project-template --stacks ,\n"+ + "The filter can contain the names of the top-level stack config files and the logical stack names (derived from the context vars)\n"+ + "atmos atlantis generate repo-config --config-template --project-template --workflow-template --stacks orgs/cp/tenant1/staging/us-east-2,orgs/cp/tenant2/dev/us-east-2\n"+ + "atmos atlantis generate repo-config --config-template --project-template --workflow-template --stacks tenant1-ue2-staging,tenant1-ue2-prod\n"+ + "atmos atlantis generate repo-config --config-template --project-template --workflow-template --stacks orgs/cp/tenant1/staging/us-east-2,tenant1-ue2-prod", + ) + + atlantisGenerateRepoConfigCmd.PersistentFlags().String("components", "", + "Only generate config for the specified components (comma-separated values).\n"+ + "atmos atlantis generate repo-config --config-template --project-template --workflow-template --components ,", + ) + + err := atlantisGenerateRepoConfigCmd.MarkPersistentFlagRequired("config-template") + if err != nil { + u.PrintErrorToStdErrorAndExit(err) + } + + err = atlantisGenerateRepoConfigCmd.MarkPersistentFlagRequired("project-template") + if err != nil { + u.PrintErrorToStdErrorAndExit(err) + } + + err = atlantisGenerateRepoConfigCmd.MarkPersistentFlagRequired("workflow-template") + if err != nil { + u.PrintErrorToStdErrorAndExit(err) + } + + atlantisGenerateCmd.AddCommand(atlantisGenerateRepoConfigCmd) +} diff --git a/cmd/terraform_generate_varfiles.go b/cmd/terraform_generate_varfiles.go new file mode 100644 index 000000000..bc2820622 --- /dev/null +++ b/cmd/terraform_generate_varfiles.go @@ -0,0 +1,59 @@ +package cmd + +import ( + e "github.com/cloudposse/atmos/internal/exec" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/spf13/cobra" +) + +// terraformGenerateVarfilesCmd generates varfiles for all terraform components in all stacks +var terraformGenerateVarfilesCmd = &cobra.Command{ + Use: "varfiles", + Short: "Execute 'terraform generate varfiles' command", + Long: `This command generates varfiles for all terraform components in all stacks`, + FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, + Run: func(cmd *cobra.Command, args []string) { + err := e.ExecuteTerraformGenerateVarfilesCmd(cmd, args) + if err != nil { + u.PrintErrorToStdErrorAndExit(err) + } + }, +} + +func init() { + terraformGenerateVarfilesCmd.DisableFlagParsing = false + + terraformGenerateVarfilesCmd.PersistentFlags().String("file-template", "", + "Varfile template (the file path, file name, and file extension).\n"+ + "Supports absolute and relative paths.\n"+ + "Supports context tokens: {namespace}, {tenant}, {environment}, {region}, {stage}, {component}, {component-path}.\n"+ + "atmos terraform generate varfiles --file-template {component-path}/{environment}-{stage}.tfvars.json\n"+ + "atmos terraform generate varfiles --file-template /configs/{tenant}/{environment}/{stage}/{component}.json\n"+ + "atmos terraform generate varfiles --file-template /{tenant}/{stage}/{region}/{component}.yaml\n"+ + "All subfolders in the path will be created automatically.", + ) + + terraformGenerateVarfilesCmd.PersistentFlags().String("stacks", "", + "Only process the specified stacks (comma-separated values).\n"+ + "atmos terraform generate varfiles --file-template --stacks ,\n"+ + "The filter can contain the names of the top-level stack config files and the logical stack names (derived from the context vars)\n"+ + "atmos terraform generate varfiles --stacks orgs/cp/tenant1/staging/us-east-2,orgs/cp/tenant2/dev/us-east-2\n"+ + "atmos terraform generate varfiles --stacks tenant1-ue2-staging,tenant1-ue2-prod\n"+ + "atmos terraform generate varfiles --stacks orgs/cp/tenant1/staging/us-east-2,tenant1-ue2-prod", + ) + + terraformGenerateVarfilesCmd.PersistentFlags().String("components", "", + "Only process the specified components (comma-separated values).\n"+ + "atmos terraform generate varfiles --file-template --components ,", + ) + + terraformGenerateVarfilesCmd.PersistentFlags().String("format", "json", "Output format.\n"+ + "atmos terraform generate varfiles --file-template --format=json/yaml ('json' is default)") + + err := terraformGenerateVarfilesCmd.MarkPersistentFlagRequired("file-template") + if err != nil { + u.PrintErrorToStdErrorAndExit(err) + } + + terraformGenerateCmd.AddCommand(terraformGenerateVarfilesCmd) +} diff --git a/examples/complete/Dockerfile b/examples/complete/Dockerfile index e1f8d7f9a..c2f514f2b 100644 --- a/examples/complete/Dockerfile +++ b/examples/complete/Dockerfile @@ -2,7 +2,7 @@ ARG GEODESIC_VERSION=1.2.3 ARG GEODESIC_OS=debian # atmos: https://github.com/cloudposse/atmos -ARG ATMOS_VERSION=1.4.28 +ARG ATMOS_VERSION=1.5.0 # Terraform: https://github.com/hashicorp/terraform/releases ARG TF_VERSION=1.2.8 diff --git a/examples/complete/atmos.yaml b/examples/complete/atmos.yaml index 038f361dc..f3ff82101 100644 --- a/examples/complete/atmos.yaml +++ b/examples/complete/atmos.yaml @@ -128,3 +128,66 @@ commands: steps: - echo Playing ping-pong... - echo pong + +# Integrations +integrations: + + # Atlantis integration + # https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html + atlantis: + # Path and name of the Atlantis config file `atlantis.yaml` + # Supports absolute and relative paths + # All the intermediate folders will be created automatically (e.g. `path: /config/atlantis/atlantis.yaml`) + # Can be overridden on the command line by using `--output-path` command-line argument in `atmos atlantis generate repo-config` command + # If not specified (set to an empty string/omitted here, and set to an empty string on the command line), the content of the file will be dumped to `stdout` + # On Linux/macOS, you can also use `--output-path=/dev/stdout` to dump the content to `stdout` without setting it to an empty string in `atlantis.path` + path: "atlantis.yaml" + + # Config templates + # Select a template by using the `--config-template ` command-line argument in `atmos atlantis generate repo-config` command + config_templates: + config-1: + version: 3 + automerge: true + delete_source_branch_on_merge: true + parallel_plan: true + parallel_apply: true + allowed_regexp_prefixes: + - dev/ + - staging/ + - prod/ + + # Project templates + # Select a template by using the `--project-template ` command-line argument in `atmos atlantis generate repo-config` command + project_templates: + project-1: + # generate a project entry for each component in every stack + name: "{tenant}-{environment}-{stage}-{component}" + workspace: "{workspace}" + dir: "{component-path}" + terraform_version: v1.2 + delete_source_branch_on_merge: true + autoplan: + enabled: true + when_modified: + - "**/*.tf" + - "varfiles/$PROJECT_NAME.tfvars.json" + apply_requirements: + - "approved" + + # Workflow templates + # https://www.runatlantis.io/docs/custom-workflows.html#custom-init-plan-apply-commands + # https://www.runatlantis.io/docs/custom-workflows.html#custom-run-command + # Select a template by using the `--workflow-template ` command-line argument in `atmos atlantis generate repo-config` command + workflow_templates: + workflow-1: + plan: + steps: + - run: terraform init -input=false + # When using workspaces, you need to select the workspace using the $WORKSPACE environment variable + - run: terraform workspace select $WORKSPACE + # You must output the plan using `-out $PLANFILE` because Atlantis expects plans to be in a specific location + - run: terraform plan -input=false -refresh -out $PLANFILE -var-file varfiles/$PROJECT_NAME.tfvars.json + apply: + steps: + - run: terraform apply $PLANFILE diff --git a/examples/complete/stacks/mixins/region/us-east-1.yaml b/examples/complete/stacks/mixins/region/us-east-1.yaml index 5370a543c..ffa506d17 100644 --- a/examples/complete/stacks/mixins/region/us-east-1.yaml +++ b/examples/complete/stacks/mixins/region/us-east-1.yaml @@ -4,7 +4,7 @@ vars: components: terraform: - vpc: + infra/vpc: vars: availability_zones: - us-east-1a diff --git a/examples/complete/stacks/mixins/region/us-east-2.yaml b/examples/complete/stacks/mixins/region/us-east-2.yaml index f4c131d92..713032875 100644 --- a/examples/complete/stacks/mixins/region/us-east-2.yaml +++ b/examples/complete/stacks/mixins/region/us-east-2.yaml @@ -4,7 +4,7 @@ vars: components: terraform: - vpc: + infra/vpc: vars: availability_zones: - us-east-2a diff --git a/go.mod b/go.mod index 2b03d9922..381c90ad1 100644 --- a/go.mod +++ b/go.mod @@ -12,23 +12,27 @@ require ( github.com/otiai10/copy v1.7.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.5.0 - github.com/spf13/viper v1.12.0 + github.com/spf13/viper v1.13.0 github.com/stretchr/testify v1.8.0 gopkg.in/yaml.v2 v2.4.0 ) require ( - cloud.google.com/go v0.100.2 // indirect - cloud.google.com/go/compute v1.6.1 // indirect - cloud.google.com/go/iam v0.3.0 // indirect - cloud.google.com/go/storage v1.14.0 // indirect + cloud.google.com/go v0.102.1 // indirect + cloud.google.com/go/compute v1.7.0 // indirect + cloud.google.com/go/iam v0.4.0 // indirect + cloud.google.com/go/storage v1.22.1 // indirect github.com/aws/aws-sdk-go v1.15.78 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect github.com/googleapis/gax-go/v2 v2.4.0 // indirect + github.com/googleapis/go-type-adapters v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.1.0 // indirect @@ -44,25 +48,25 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.3.0 // indirect + github.com/subosito/gotenv v1.4.1 // indirect github.com/ulikunitz/xz v0.5.8 // indirect go.opencensus.io v0.23.0 // indirect - golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect - golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect + golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect + golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect - google.golang.org/api v0.81.0 // indirect + golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/api v0.93.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect - google.golang.org/grpc v1.46.2 // indirect - google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/ini.v1 v1.66.4 // indirect + google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959 // indirect + google.golang.org/grpc v1.48.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a66553e60..b9a28c015 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,10 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1 h1:vpK6iQWv/2uUeFJth4/cBHsQAGjn1iIE6AAlxipRaA0= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -40,12 +42,14 @@ cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTB cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.4.0 h1:YBYU00SCDzZJdHqVc4I5d6lsklcYIjQZa1YmEz4jlSE= +cloud.google.com/go/iam v0.4.0/go.mod h1:cbaZxyScUhxl7ZAkNWiALgihfP75wS/fUsVNaa1r3vA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -55,8 +59,9 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -184,6 +189,11 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -192,6 +202,8 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -259,8 +271,8 @@ github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= @@ -282,8 +294,8 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= +github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -295,8 +307,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= -github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -396,8 +408,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= -golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -416,8 +429,10 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -429,7 +444,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -489,9 +504,12 @@ golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -564,8 +582,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -603,8 +622,10 @@ google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8= -google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.93.0 h1:T2xt9gi0gHdxdnRkVQhT8mIvPaXKNsDNWz+L696M66M= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -654,6 +675,7 @@ google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -688,8 +710,14 @@ google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I= -google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959 h1:hw4Y42zL1VyVKxPgRHHh191fpVBGV8sNVmcow5Z8VXY= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -719,8 +747,10 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -735,15 +765,16 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/exec/atlantis_generate_repo_config.go b/internal/exec/atlantis_generate_repo_config.go new file mode 100644 index 000000000..daa866bff --- /dev/null +++ b/internal/exec/atlantis_generate_repo_config.go @@ -0,0 +1,273 @@ +package exec + +import ( + "fmt" + c "github.com/cloudposse/atmos/pkg/config" + s "github.com/cloudposse/atmos/pkg/stack" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "path" + "path/filepath" + "strings" +) + +// ExecuteAtlantisGenerateRepoConfigCmd executes `atlantis generate repo-config` command +func ExecuteAtlantisGenerateRepoConfigCmd(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + outputPath, err := flags.GetString("output-path") + if err != nil { + return err + } + + configTemplateName, err := flags.GetString("config-template") + if err != nil { + return err + } + + projectTemplateName, err := flags.GetString("project-template") + if err != nil { + return err + } + + workflowTemplateName, err := flags.GetString("workflow-template") + if err != nil { + return err + } + + stacksCsv, err := flags.GetString("stacks") + if err != nil { + return err + } + var stacks []string + if stacksCsv != "" { + stacks = strings.Split(stacksCsv, ",") + } + + componentsCsv, err := flags.GetString("components") + if err != nil { + return err + } + var components []string + if componentsCsv != "" { + components = strings.Split(componentsCsv, ",") + } + + return ExecuteAtlantisGenerateRepoConfig(outputPath, configTemplateName, projectTemplateName, workflowTemplateName, stacks, components) +} + +// ExecuteAtlantisGenerateRepoConfig generates repository configuration for Atlantis +func ExecuteAtlantisGenerateRepoConfig( + outputPath string, + configTemplateName string, + projectTemplateName string, + workflowTemplateName string, + stacks []string, + components []string) error { + + var configAndStacksInfo c.ConfigAndStacksInfo + stacksMap, err := FindStacksMap(configAndStacksInfo, false) + if err != nil { + return err + } + + var configTemplate c.AtlantisRepoConfig + var projectTemplate c.AtlantisProjectConfig + var workflowTemplate any + var ok bool + + if configTemplate, ok = c.Config.Integrations.Atlantis.ConfigTemplates[configTemplateName]; !ok { + return errors.Errorf("atlantis config template '%s' is not defined in 'integrations.atlantis.config_templates' in atmos.yaml", configTemplateName) + } + + if projectTemplate, ok = c.Config.Integrations.Atlantis.ProjectTemplates[projectTemplateName]; !ok { + return errors.Errorf("atlantis project template '%s' is not defined in 'integrations.atlantis.project_templates' in atmos.yaml", projectTemplateName) + } + + if workflowTemplate, ok = c.Config.Integrations.Atlantis.WorkflowTemplates[workflowTemplateName]; !ok { + return errors.Errorf("atlantis workflow template '%s' is not defined in 'integrations.atlantis.workflow_templates' in atmos.yaml", workflowTemplateName) + } + + var atlantisProjects []c.AtlantisProjectConfig + var componentsSection map[string]any + var terraformSection map[string]any + var componentSection map[string]any + var varsSection map[any]any + + // Iterate over all components in all stacks and generate atlantis projects + for stackConfigFileName, stackSection := range stacksMap { + if componentsSection, ok = stackSection.(map[any]any)["components"].(map[string]any); !ok { + continue + } + + if terraformSection, ok = componentsSection["terraform"].(map[string]any); !ok { + continue + } + + for componentName, compSection := range terraformSection { + if componentSection, ok = compSection.(map[string]any); !ok { + continue + } + + // Find all derived components of the provided components + derivedComponents, err := s.FindComponentsDerivedFromBaseComponents(stackConfigFileName, terraformSection, components) + if err != nil { + return err + } + + // Check if `components` filter is provided + if len(components) == 0 || + u.SliceContainsString(components, componentName) || + u.SliceContainsString(derivedComponents, componentName) { + + // Component vars + if varsSection, ok = componentSection["vars"].(map[any]any); !ok { + continue + } + + // Component metadata + metadataSection := map[any]any{} + if metadataSection, ok = componentSection["metadata"].(map[any]any); ok { + if componentType, ok := metadataSection["type"].(string); ok { + // Don't include abstract components + if componentType == "abstract" { + continue + } + } + } + + // Find the terraform component + // If `component` attribute is present, it's the terraform component + // Otherwise, the YAML component name is the terraform component (by default) + terraformComponent := componentName + if componentAttribute, ok := componentSection["component"].(string); ok { + terraformComponent = componentAttribute + } + + // Absolute path to the terraform component + terraformComponentPath := path.Join( + c.Config.BasePath, + c.Config.Components.Terraform.BasePath, + terraformComponent, + ) + + // Context + context := c.GetContextFromVars(varsSection) + context.Component = strings.Replace(componentName, "/", "-", -1) + context.ComponentPath = terraformComponentPath + contextPrefix, err := c.GetContextPrefix(stackConfigFileName, context, c.Config.Stacks.NamePattern, stackConfigFileName) + if err != nil { + return err + } + + // Terraform workspace + workspace, err := BuildTerraformWorkspace( + stackConfigFileName, + c.Config.Stacks.NamePattern, + metadataSection, + context, + ) + if err != nil { + return err + } + + context.Workspace = workspace + + // Check if `stacks` filter is provided + if len(stacks) == 0 || + // `stacks` filter can contain the names of the top-level stack config files: + // atmos terraform generate varfiles --stacks=orgs/cp/tenant1/staging/us-east-2,orgs/cp/tenant2/dev/us-east-2 + u.SliceContainsString(stacks, stackConfigFileName) || + // `stacks` filter can also contain the logical stack names (derived from the context vars): + // atmos terraform generate varfiles --stacks=tenant1-ue2-staging,tenant1-ue2-prod + u.SliceContainsString(stacks, contextPrefix) { + + // Generate an atlantis project for the component in the stack + // Replace the context tokens + var whenModified []string + + for _, item := range projectTemplate.Autoplan.WhenModified { + processedItem := c.ReplaceContextTokens(context, item) + whenModified = append(whenModified, processedItem) + } + + atlantisProjectAutoplanConfig := c.AtlantisProjectAutoplanConfig{ + Enabled: projectTemplate.Autoplan.Enabled, + ApplyRequirements: projectTemplate.Autoplan.ApplyRequirements, + WhenModified: whenModified, + } + + atlantisProjectName := c.ReplaceContextTokens(context, projectTemplate.Name) + + atlantisProject := c.AtlantisProjectConfig{ + Name: atlantisProjectName, + Workspace: c.ReplaceContextTokens(context, projectTemplate.Workspace), + Workflow: workflowTemplateName, + Dir: c.ReplaceContextTokens(context, projectTemplate.Dir), + TerraformVersion: projectTemplate.TerraformVersion, + DeleteSourceBranchOnMerge: projectTemplate.DeleteSourceBranchOnMerge, + Autoplan: atlantisProjectAutoplanConfig, + } + + atlantisProjects = append(atlantisProjects, atlantisProject) + } + } + } + } + + // Workflows + atlantisWorkflows := map[string]any{ + workflowTemplateName: workflowTemplate, + } + + // Final atlantis config + atlantisYaml := c.AtlantisConfigOutput{} + atlantisYaml.Version = configTemplate.Version + atlantisYaml.Automerge = configTemplate.Automerge + atlantisYaml.DeleteSourceBranchOnMerge = configTemplate.DeleteSourceBranchOnMerge + atlantisYaml.ParallelPlan = configTemplate.ParallelPlan + atlantisYaml.ParallelApply = configTemplate.ParallelApply + atlantisYaml.AllowedRegexpPrefixes = configTemplate.AllowedRegexpPrefixes + atlantisYaml.Projects = atlantisProjects + atlantisYaml.Workflows = atlantisWorkflows + + // Write the atlantis config to a file at the specified path + // Check the command line argument `--output-path` first + // Then check the `atlantis.path` setting in `atmos.yaml` + fileName := outputPath + if fileName == "" { + fileName = c.Config.Integrations.Atlantis.Path + u.PrintInfo(fmt.Sprintf("Using 'atlantis.path: %s' from atmos.yaml", fileName)) + } else { + u.PrintInfo(fmt.Sprintf("Using '--output-path %s' command-line argument", fileName)) + } + + // If the path is empty, dump to `stdout` + if fileName != "" { + u.PrintInfo(fmt.Sprintf("Writing atlantis repo config file to '%s'", fileName)) + + fileAbsolutePath, err := filepath.Abs(fileName) + if err != nil { + return err + } + + // Create all the intermediate subdirectories + err = u.EnsureDir(fileAbsolutePath) + if err != nil { + return err + } + + err = u.WriteToFileAsYAML(fileAbsolutePath, atlantisYaml, 0644) + if err != nil { + return err + } + } else { + err = u.PrintAsYAML(atlantisYaml) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/exec/terraform_generate_varfiles.go b/internal/exec/terraform_generate_varfiles.go new file mode 100644 index 000000000..1fd599c33 --- /dev/null +++ b/internal/exec/terraform_generate_varfiles.go @@ -0,0 +1,173 @@ +package exec + +import ( + "fmt" + c "github.com/cloudposse/atmos/pkg/config" + s "github.com/cloudposse/atmos/pkg/stack" + u "github.com/cloudposse/atmos/pkg/utils" + "github.com/spf13/cobra" + "path" + "path/filepath" + "strings" +) + +// ExecuteTerraformGenerateVarfilesCmd executes `terraform generate varfiles` command +func ExecuteTerraformGenerateVarfilesCmd(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + fileTemplate, err := flags.GetString("file-template") + if err != nil { + return err + } + + stacksCsv, err := flags.GetString("stacks") + if err != nil { + return err + } + var stacks []string + if stacksCsv != "" { + stacks = strings.Split(stacksCsv, ",") + } + + componentsCsv, err := flags.GetString("components") + if err != nil { + return err + } + var components []string + if componentsCsv != "" { + components = strings.Split(componentsCsv, ",") + } + + format, err := flags.GetString("format") + if err != nil { + return err + } + if format != "" && format != "yaml" && format != "json" { + return fmt.Errorf("invalid '--format' flag '%s'. Valid values are 'json' (default) and 'yaml'", format) + } + if format == "" { + format = "json" + } + + return ExecuteTerraformGenerateVarfiles(fileTemplate, format, stacks, components) +} + +// ExecuteTerraformGenerateVarfiles generates varfiles for all terraform components in all stacks +func ExecuteTerraformGenerateVarfiles(fileTemplate string, format string, stacks []string, components []string) error { + var configAndStacksInfo c.ConfigAndStacksInfo + stacksMap, err := FindStacksMap(configAndStacksInfo, false) + if err != nil { + return err + } + + fmt.Println() + + var ok bool + var componentsSection map[string]any + var terraformSection map[string]any + var componentSection map[string]any + var varsSection map[any]any + + for stackConfigFileName, stackSection := range stacksMap { + if componentsSection, ok = stackSection.(map[any]any)["components"].(map[string]any); !ok { + continue + } + + if terraformSection, ok = componentsSection["terraform"].(map[string]any); !ok { + continue + } + + for componentName, compSection := range terraformSection { + if componentSection, ok = compSection.(map[string]any); !ok { + continue + } + + // Find all derived components of the provided components + derivedComponents, err := s.FindComponentsDerivedFromBaseComponents(stackConfigFileName, terraformSection, components) + if err != nil { + return err + } + + // Check if `components` filter is provided + if len(components) == 0 || + u.SliceContainsString(components, componentName) || + u.SliceContainsString(derivedComponents, componentName) { + + // Component vars + if varsSection, ok = componentSection["vars"].(map[any]any); !ok { + continue + } + + // Find terraform component. + // If `component` attribute is present, it's the terraform component. + // Otherwise, the YAML component name is the terraform component. + terraformComponent := componentName + if componentAttribute, ok := componentSection["component"].(string); ok { + terraformComponent = componentAttribute + } + + // Absolute path to the terraform component + terraformComponentPath := path.Join( + c.Config.BasePath, + c.Config.Components.Terraform.BasePath, + terraformComponent, + ) + + // Context + context := c.GetContextFromVars(varsSection) + context.Component = strings.Replace(componentName, "/", "-", -1) + context.ComponentPath = terraformComponentPath + contextPrefix, err := c.GetContextPrefix(stackConfigFileName, context, c.Config.Stacks.NamePattern, stackConfigFileName) + if err != nil { + return err + } + + // Check if `stacks` filter is provided + if len(stacks) == 0 || + // `stacks` filter can contain the names of the top-level stack config files: + // atmos terraform generate varfiles --stacks=orgs/cp/tenant1/staging/us-east-2,orgs/cp/tenant2/dev/us-east-2 + u.SliceContainsString(stacks, stackConfigFileName) || + // `stacks` filter can also contain the logical stack names (derived from the context vars): + // atmos terraform generate varfiles --stacks=tenant1-ue2-staging,tenant1-ue2-prod + u.SliceContainsString(stacks, contextPrefix) { + + // Replace the tokens in the file template + // Supported context tokens: {namespace}, {tenant}, {environment}, {region}, {stage}, {component}, {component-path} + fileName := c.ReplaceContextTokens(context, fileTemplate) + fileAbsolutePath, err := filepath.Abs(fileName) + if err != nil { + return err + } + + // Create all the intermediate subdirectories + err = u.EnsureDir(fileAbsolutePath) + if err != nil { + return err + } + + // Write the varfile + if format == "yaml" { + err = u.WriteToFileAsYAML(fileAbsolutePath, varsSection, 0644) + if err != nil { + return err + } + } else if format == "json" { + err = u.WriteToFileAsJSON(fileAbsolutePath, varsSection, 0644) + if err != nil { + return err + } + } + + u.PrintInfo(fmt.Sprintf("Varfile: %s", fileName)) + u.PrintMessage(fmt.Sprintf("Terraform component: %s", terraformComponent)) + u.PrintMessage(fmt.Sprintf("YAML component: %s", componentName)) + u.PrintMessage(fmt.Sprintf("Stack: %s", contextPrefix)) + u.PrintMessage(fmt.Sprintf("Stack config file: %s", stackConfigFileName)) + fmt.Println() + } + } + } + } + + return nil +} diff --git a/pkg/atlantis/README.md b/pkg/atlantis/README.md new file mode 100644 index 000000000..9c4e7502a --- /dev/null +++ b/pkg/atlantis/README.md @@ -0,0 +1,166 @@ +# `atlantis` integration + +## Atlantis Repo Config Generation + +`atmos` supports generating [Repo Level atlantis.yaml Config](https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html) for `atmos` components +and stacks. + +The following `atmos` commands will first generate the varfiles for all components in all stacks, +then generate the `atlantis.yaml` repo config file: + +```bash +atmos terraform generate varfiles --file-template=varfiles/{tenant}-{environment}-{stage}-{component}.tfvars.json +atmos atlantis generate repo-config --config-template config-1 --project-template project-1 --workflow-template workflow-1 +``` + +__NOTE:__ All paths, `--file-template` in the `atmos terraform generate varfiles` command, and in the `atlantis` config in `atmos.yaml`, +should be relative to the root of the repo. + +Supported context tokens: `{namespace}`, `{tenant}`, `{environment}`, `{region}`, `{stage}`, `{component}`, `{component-path}`. + +You can run these commands manually and commit the generated varfiles and `atlantis.yaml` repo config. + +If you want to generate `atlantis.yaml` on the server dynamically, +you can add the following run commands to [pre_workflow_hooks](https://www.runatlantis.io/docs/pre-workflow-hooks.html#pre-workflow-hooks). +The `atlantis.yaml` repo config file will be generated right before Atlantis parses it. + +```yaml +repos: + - id: /.*/ + pre_workflow_hooks: + - run: | + atmos terraform generate varfiles --file-template=varfiles/{tenant}-{environment}-{stage}-{component}.tfvars.json + atmos atlantis generate repo-config --config-template config-1 --project-template project-1 --workflow-template workflow-1 +``` + +Note that the `-file-template` parameter in the `atmos terraform generate varfiles` command must match the following two settings in `atmos.yaml`: + +- `when_modified` must use the same template with the context tokens - this will allow Atlantis to check if any of the generated variables were + modified +- workflow `extra_args` must use the same template with the context tokens - this will allow Atlantis to run Terraform commands with the + correct `-var-file` parameters + +```yaml +# atmos.yaml CLI config + +# Integrations +integrations: + + # Atlantis integration + # https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html + atlantis: + # Path and name of the Atlantis config file `atlantis.yaml` + # Supports absolute and relative paths + # All the intermediate folders will be created automatically (e.g. `path: /config/atlantis/atlantis.yaml`) + # Can be overridden on the command line by using `--output-path` command-line argument in `atmos atlantis generate repo-config` command + # If not specified (set to an empty string/omitted here, and set to an empty string on the command line), the content of the file will be dumped to `stdout` + # On Linux/macOS, you can also use `--output-path=/dev/stdout` to dump the content to `stdout` without setting it to an empty string in `atlantis.path` + path: "atlantis.yaml" + + # Config templates + # Select a template by using the `--config-template ` command-line argument in `atmos atlantis generate repo-config` command + config_templates: + config-1: + version: 3 + automerge: true + delete_source_branch_on_merge: true + parallel_plan: true + parallel_apply: true + allowed_regexp_prefixes: + - dev/ + - staging/ + - prod/ + + # Project templates + # Select a template by using the `--project-template ` command-line argument in `atmos atlantis generate repo-config` command + project_templates: + project-1: + # generate a project entry for each component in every stack + name: "{tenant}-{environment}-{stage}-{component}" + workspace: "{workspace}" + dir: "{component-path}" + terraform_version: v1.2 + delete_source_branch_on_merge: true + autoplan: + enabled: true + when_modified: + - "**/*.tf" + - "varfiles/$PROJECT_NAME.tfvars.json" + apply_requirements: + - "approved" + + # Workflow templates + # https://www.runatlantis.io/docs/custom-workflows.html#custom-init-plan-apply-commands + # https://www.runatlantis.io/docs/custom-workflows.html#custom-run-command + # Select a template by using the `--workflow-template ` command-line argument in `atmos atlantis generate repo-config` command + workflow_templates: + workflow-1: + plan: + steps: + - run: terraform init -input=false + # When using workspaces, you need to select the workspace using the $WORKSPACE environment variable + - run: terraform workspace select $WORKSPACE + # You must output the plan using `-out $PLANFILE` because Atlantis expects plans to be in a specific location + - run: terraform plan -input=false -refresh -out $PLANFILE -var-file varfiles/$PROJECT_NAME.tfvars.json + apply: + steps: + - run: terraform apply $PLANFILE +``` + +Using the config, project and workflow templates, `atmos` generates a separate `atlantis` project for each `atmos` component in every stack: + +```yaml +version: 3 +automerge: true +delete_source_branch_on_merge: true +parallel_plan: true +parallel_apply: true +allowed_regexp_prefixes: + - dev/ + - staging/ + - prod/ +projects: + - name: tenant1-ue2-staging-test-test-component-override-3 + workspace: test-component-override-3-workspace + workflow: workflow-1 + dir: examples/complete/components/terraform/test/test-component + terraform_version: v1.2 + delete_source_branch_on_merge: true + autoplan: + enabled: true + when_modified: + - '**/*.tf' + - varfiles/tenant1-ue2-staging-test-test-component-override-3.tfvars.json + apply_requirements: + - approved + - name: tenant1-ue2-staging-infra-vpc + workspace: tenant1-ue2-staging + workflow: workflow-1 + dir: examples/complete/components/terraform/infra/vpc + terraform_version: v1.2 + delete_source_branch_on_merge: true + autoplan: + enabled: true + when_modified: + - '**/*.tf' + - varfiles/tenant1-ue2-staging-infra-vpc.tfvars.json + apply_requirements: + - approved +workflows: + workflow-1: + apply: + steps: + - run: terraform apply $PLANFILE + plan: + steps: + - run: terraform init -input=false + - run: terraform workspace select $WORKSPACE + - run: terraform plan -input=false -refresh -out $PLANFILE -var-file varfiles/$PROJECT_NAME.tfvars.json +``` + +## References + +- [Repo Level atlantis.yaml Config](https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html) +- [Pre-workflow Hooks](https://www.runatlantis.io/docs/pre-workflow-hooks.html#pre-workflow-hooks) +- [Dynamic Repo Config Generation](https://www.runatlantis.io/docs/pre-workflow-hooks.html#dynamic-repo-config-generation) +- [Custom init/plan/apply Commands](https://www.runatlantis.io/docs/custom-workflows.html#custom-init-plan-apply-commands) diff --git a/pkg/atlantis/atlantis_generate_repo_config_test.go b/pkg/atlantis/atlantis_generate_repo_config_test.go new file mode 100644 index 000000000..598ef3637 --- /dev/null +++ b/pkg/atlantis/atlantis_generate_repo_config_test.go @@ -0,0 +1,38 @@ +package atlantis + +import ( + c "github.com/cloudposse/atmos/pkg/config" + "github.com/cloudposse/atmos/pkg/utils" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAtlantisGenerateRepoConfig(t *testing.T) { + err := c.InitConfig() + assert.Nil(t, err) + + err = utils.PrintAsYAML(c.Config) + assert.Nil(t, err) + + atlantisConfig := c.Config.Integrations.Atlantis + configTemplateName := "config-1" + configTemplate := atlantisConfig.ConfigTemplates[configTemplateName] + projectTemplateName := "project-1" + projectTemplate := atlantisConfig.ProjectTemplates[projectTemplateName] + workflowTemplateName := "workflow-1" + workflowTemplate := atlantisConfig.ProjectTemplates[workflowTemplateName] + projectTemplate.Workflow = workflowTemplateName + + atlantisYaml := c.AtlantisConfigOutput{} + atlantisYaml.Version = configTemplate.Version + atlantisYaml.Automerge = configTemplate.Automerge + atlantisYaml.DeleteSourceBranchOnMerge = configTemplate.DeleteSourceBranchOnMerge + atlantisYaml.ParallelPlan = configTemplate.ParallelPlan + atlantisYaml.ParallelApply = configTemplate.ParallelApply + atlantisYaml.Workflows = map[string]any{workflowTemplateName: workflowTemplate} + atlantisYaml.AllowedRegexpPrefixes = configTemplate.AllowedRegexpPrefixes + atlantisYaml.Projects = []c.AtlantisProjectConfig{projectTemplate} + + err = utils.PrintAsYAML(atlantisYaml) + assert.Nil(t, err) +} diff --git a/pkg/atlantis/atmos.yaml b/pkg/atlantis/atmos.yaml new file mode 100644 index 000000000..774fd8e54 --- /dev/null +++ b/pkg/atlantis/atmos.yaml @@ -0,0 +1,127 @@ +# 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) + +# Base path for components, stacks and workflows configurations. +# Can also be set using `ATMOS_BASE_PATH` ENV var, or `--base-path` command-line argument. +# Supports both absolute and relative paths. +# If not provided or is an empty string, `components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path` and `workflows.base_path` +# are independent settings (supporting both absolute and relative paths). +# If `base_path` is provided, `components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path` and `workflows.base_path` +# are considered paths relative to `base_path`. +base_path: "../../examples/complete" + +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: "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 + # Can also be set using `ATMOS_COMPONENTS_TERRAFORM_INIT_RUN_RECONFIGURE` ENV var, or `--init-run-reconfigure` command-line argument + init_run_reconfigure: true + # Can also be set using `ATMOS_COMPONENTS_TERRAFORM_AUTO_GENERATE_BACKEND_FILE` ENV var, or `--auto-generate-backend-file` command-line argument + auto_generate_backend_file: false + 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: "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: "stacks" + # Can also be set using `ATMOS_STACKS_INCLUDED_PATHS` ENV var (comma-separated values string) + included_paths: + - "orgs/**/*" + # Can also be set using `ATMOS_STACKS_EXCLUDED_PATHS` ENV var (comma-separated values string) + excluded_paths: + - "**/_defaults.yaml" + # Can also be set using `ATMOS_STACKS_NAME_PATTERN` ENV var + name_pattern: "{tenant}-{environment}-{stage}" + +workflows: + # Can also be set using `ATMOS_WORKFLOWS_BASE_PATH` ENV var, or `--workflows-dir` command-line arguments + # Supports both absolute and relative paths + base_path: "stacks/workflows" + +logs: + verbose: false + colors: true + +# Integrations +integrations: + + # Atlantis integration + # https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html + atlantis: + # Path and name of the Atlantis config file `atlantis.yaml` + # Supports absolute and relative paths + # All the intermediate folders will be created automatically (e.g. `path: /config/atlantis/atlantis.yaml`) + # Can be overridden on the command line by using `--output-path` command-line argument in `atmos atlantis generate repo-config` command + # If not specified (set to an empty string/omitted here, and set to an empty string on the command line), the content of the file will be dumped to `stdout` + # On Linux/macOS, you can also use `--output-path=/dev/stdout` to dump the content to `stdout` without setting it to an empty string in `atlantis.path` + path: "atlantis.yaml" + + # Config templates + # Select a template by using the `--config-template ` command-line argument in `atmos atlantis generate repo-config` command + config_templates: + config-1: + version: 3 + automerge: true + delete_source_branch_on_merge: true + parallel_plan: true + parallel_apply: true + allowed_regexp_prefixes: + - dev/ + - staging/ + - prod/ + + # Project templates + # Select a template by using the `--project-template ` command-line argument in `atmos atlantis generate repo-config` command + project_templates: + project-1: + # generate a project entry for each component in every stack + name: "{tenant}-{environment}-{stage}-{component}" + workspace: "{workspace}" + dir: "{component-path}" + terraform_version: v1.2 + delete_source_branch_on_merge: true + autoplan: + enabled: true + when_modified: + - "**/*.tf" + - "varfiles/$PROJECT_NAME.tfvars.json" + apply_requirements: + - "approved" + + # Workflow templates + # https://www.runatlantis.io/docs/custom-workflows.html#custom-init-plan-apply-commands + # https://www.runatlantis.io/docs/custom-workflows.html#custom-run-command + # Select a template by using the `--workflow-template ` command-line argument in `atmos atlantis generate repo-config` command + workflow_templates: + workflow-1: + plan: + steps: + - run: terraform init -input=false + # When using workspaces, you need to select the workspace using the $WORKSPACE environment variable + - run: terraform workspace select $WORKSPACE + # You must output the plan using `-out $PLANFILE` because Atlantis expects plans to be in a specific location + - run: terraform plan -input=false -refresh -out $PLANFILE -var-file varfiles/$PROJECT_NAME.tfvars.json + apply: + steps: + - run: terraform apply $PLANFILE diff --git a/pkg/config/schema.go b/pkg/config/schema.go index 8c544c9d1..705964e53 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -16,8 +16,8 @@ type Helmfile struct { } type Components struct { - Terraform Terraform - Helmfile Helmfile + Terraform Terraform `yaml:"terraform" json:"terraform" mapstructure:"terraform"` + Helmfile Helmfile `yaml:"helmfile" json:"helmfile" mapstructure:"helmfile"` } type Stacks struct { @@ -36,44 +36,15 @@ type Logs struct { Colors bool `yaml:"colors" json:"colors" mapstructure:"colors"` } -type Command struct { - Name string `yaml:"name" json:"name" mapstructure:"name"` - Description string `yaml:"description" json:"description" mapstructure:"description"` - Env []CommandEnv `yaml:"env" json:"env" mapstructure:"env"` - Arguments []CommandArgument `yaml:"arguments" json:"arguments" mapstructure:"arguments"` - Flags []CommandFlag `yaml:"flags" json:"flags" mapstructure:"flags"` - Steps []string `yaml:"steps" json:"steps" mapstructure:"steps"` - Commands []Command `yaml:"commands" json:"commands" mapstructure:"commands"` -} - -type CommandArgument struct { - Name string `yaml:"name" json:"name" mapstructure:"name"` - Description string `yaml:"description" json:"description" mapstructure:"description"` -} - -type CommandFlag struct { - Name string `yaml:"name" json:"name" mapstructure:"name"` - Shorthand string `yaml:"shorthand" json:"shorthand" mapstructure:"shorthand"` - Type string `yaml:"type" json:"type" mapstructure:"type"` - Description string `yaml:"description" json:"description" mapstructure:"description"` - Usage string `yaml:"usage" json:"usage" mapstructure:"usage"` - Required bool `yaml:"required" json:"required" mapstructure:"required"` -} - -type CommandEnv struct { - Key string `yaml:"key" json:"key" mapstructure:"key"` - Value string `yaml:"value" json:"value" mapstructure:"value"` - ValueCommand string `yaml:"valueCommand" json:"valueCommand" mapstructure:"valueCommand"` -} - type Configuration struct { - BasePath string `yaml:"base_path" json:"base_path" mapstructure:"base_path"` - Components Components - Stacks Stacks - Workflows Workflows - Logs Logs - Commands []Command - Initialized bool + BasePath string `yaml:"base_path" json:"base_path" mapstructure:"base_path"` + Components Components `yaml:"components" json:"components" mapstructure:"components"` + Stacks Stacks `yaml:"stacks" json:"stacks" mapstructure:"stacks"` + Workflows Workflows `yaml:"workflows" json:"workflows" mapstructure:"workflows"` + Logs Logs `yaml:"logs" json:"logs" mapstructure:"logs"` + Commands []Command `yaml:"commands" json:"commands" mapstructure:"commands"` + Integrations Integrations `yaml:"integrations" json:"integrations" mapstructure:"integrations"` + Initialized bool } type ProcessedConfiguration struct { @@ -95,6 +66,8 @@ type Context struct { Region string Component string BaseComponent string + ComponentPath string + Workspace string Attributes []string } @@ -224,3 +197,86 @@ type VendorComponentConfig struct { Metadata VendorComponentMetadata Spec VendorComponentSpec `yaml:"spec" json:"spec" mapstructure:"spec"` } + +// Custom CLI commands + +type Command struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + Description string `yaml:"description" json:"description" mapstructure:"description"` + Env []CommandEnv `yaml:"env" json:"env" mapstructure:"env"` + Arguments []CommandArgument `yaml:"arguments" json:"arguments" mapstructure:"arguments"` + Flags []CommandFlag `yaml:"flags" json:"flags" mapstructure:"flags"` + Steps []string `yaml:"steps" json:"steps" mapstructure:"steps"` + Commands []Command `yaml:"commands" json:"commands" mapstructure:"commands"` +} + +type CommandArgument struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + Description string `yaml:"description" json:"description" mapstructure:"description"` +} + +type CommandFlag struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + Shorthand string `yaml:"shorthand" json:"shorthand" mapstructure:"shorthand"` + Type string `yaml:"type" json:"type" mapstructure:"type"` + Description string `yaml:"description" json:"description" mapstructure:"description"` + Usage string `yaml:"usage" json:"usage" mapstructure:"usage"` + Required bool `yaml:"required" json:"required" mapstructure:"required"` +} + +type CommandEnv struct { + Key string `yaml:"key" json:"key" mapstructure:"key"` + Value string `yaml:"value" json:"value" mapstructure:"value"` + ValueCommand string `yaml:"valueCommand" json:"valueCommand" mapstructure:"valueCommand"` +} + +// Integrations + +type Integrations struct { + Atlantis Atlantis `yaml:"atlantis" json:"atlantis" mapstructure:"atlantis"` +} + +// Atlantis integration + +type Atlantis struct { + Path string `yaml:"path" json:"path" mapstructure:"path"` + ConfigTemplates map[string]AtlantisRepoConfig `yaml:"config_templates" json:"config_templates" mapstructure:"config_templates"` + ProjectTemplates map[string]AtlantisProjectConfig `yaml:"project_templates" json:"project_templates" mapstructure:"project_templates"` + WorkflowTemplates map[string]any `yaml:"workflow_templates" json:"workflow_templates" mapstructure:"workflow_templates"` +} + +type AtlantisRepoConfig struct { + Version int `yaml:"version" json:"version" mapstructure:"version"` + Automerge bool `yaml:"automerge" json:"automerge" mapstructure:"automerge"` + DeleteSourceBranchOnMerge bool `yaml:"delete_source_branch_on_merge" json:"delete_source_branch_on_merge" mapstructure:"delete_source_branch_on_merge"` + ParallelPlan bool `yaml:"parallel_plan" json:"parallel_plan" mapstructure:"parallel_plan"` + ParallelApply bool `yaml:"parallel_apply" json:"parallel_apply" mapstructure:"parallel_apply"` + AllowedRegexpPrefixes []string `yaml:"allowed_regexp_prefixes" json:"allowed_regexp_prefixes" mapstructure:"allowed_regexp_prefixes"` +} + +type AtlantisProjectConfig struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + Workspace string `yaml:"workspace" json:"workspace" mapstructure:"workspace"` + Workflow string `yaml:"workflow" json:"workflow" mapstructure:"workflow"` + Dir string `yaml:"dir" json:"dir" mapstructure:"dir"` + TerraformVersion string `yaml:"terraform_version" json:"terraform_version" mapstructure:"terraform_version"` + DeleteSourceBranchOnMerge bool `yaml:"delete_source_branch_on_merge" json:"delete_source_branch_on_merge" mapstructure:"delete_source_branch_on_merge"` + Autoplan AtlantisProjectAutoplanConfig `yaml:"autoplan" json:"autoplan" mapstructure:"autoplan"` +} + +type AtlantisProjectAutoplanConfig struct { + Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"` + WhenModified []string `yaml:"when_modified" json:"when_modified" mapstructure:"when_modified"` + ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements" mapstructure:"apply_requirements"` +} + +type AtlantisConfigOutput struct { + Version int `yaml:"version" json:"version" mapstructure:"version"` + Automerge bool `yaml:"automerge" json:"automerge" mapstructure:"automerge"` + DeleteSourceBranchOnMerge bool `yaml:"delete_source_branch_on_merge" json:"delete_source_branch_on_merge" mapstructure:"delete_source_branch_on_merge"` + ParallelPlan bool `yaml:"parallel_plan" json:"parallel_plan" mapstructure:"parallel_plan"` + ParallelApply bool `yaml:"parallel_apply" json:"parallel_apply" mapstructure:"parallel_apply"` + AllowedRegexpPrefixes []string `yaml:"allowed_regexp_prefixes" json:"allowed_regexp_prefixes" mapstructure:"allowed_regexp_prefixes"` + Projects []AtlantisProjectConfig `yaml:"projects" json:"projects" mapstructure:"projects"` + Workflows map[string]any `yaml:"workflows" json:"workflows" mapstructure:"workflows"` +} diff --git a/pkg/config/utils.go b/pkg/config/utils.go index 3b355b045..5168084e4 100644 --- a/pkg/config/utils.go +++ b/pkg/config/utils.go @@ -445,10 +445,13 @@ func ReplaceContextTokens(context Context, pattern string) string { r := strings.NewReplacer( "{base-component}", context.BaseComponent, "{component}", context.Component, + "{component-path}", context.ComponentPath, "{namespace}", context.Namespace, "{environment}", context.Environment, + "{region}", context.Region, "{tenant}", context.Tenant, "{stage}", context.Stage, + "{workspace}", context.Workspace, "{attributes}", strings.Join(context.Attributes, "-"), ) return r.Replace(pattern) diff --git a/pkg/generate/atmos.yaml b/pkg/generate/atmos.yaml new file mode 100644 index 000000000..5c1851dd0 --- /dev/null +++ b/pkg/generate/atmos.yaml @@ -0,0 +1,64 @@ +# 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) + +# Base path for components, stacks and workflows configurations. +# Can also be set using `ATMOS_BASE_PATH` ENV var, or `--base-path` command-line argument. +# Supports both absolute and relative paths. +# If not provided or is an empty string, `components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path` and `workflows.base_path` +# are independent settings (supporting both absolute and relative paths). +# If `base_path` is provided, `components.terraform.base_path`, `components.helmfile.base_path`, `stacks.base_path` and `workflows.base_path` +# are considered paths relative to `base_path`. +base_path: "../../examples/complete" + +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: "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 + # Can also be set using `ATMOS_COMPONENTS_TERRAFORM_INIT_RUN_RECONFIGURE` ENV var, or `--init-run-reconfigure` command-line argument + init_run_reconfigure: true + # Can also be set using `ATMOS_COMPONENTS_TERRAFORM_AUTO_GENERATE_BACKEND_FILE` ENV var, or `--auto-generate-backend-file` command-line argument + auto_generate_backend_file: false + 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: "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: "stacks" + # Can also be set using `ATMOS_STACKS_INCLUDED_PATHS` ENV var (comma-separated values string) + included_paths: + - "orgs/**/*" + # Can also be set using `ATMOS_STACKS_EXCLUDED_PATHS` ENV var (comma-separated values string) + excluded_paths: + - "**/_defaults.yaml" + # Can also be set using `ATMOS_STACKS_NAME_PATTERN` ENV var + name_pattern: "{tenant}-{environment}-{stage}" + +workflows: + # Can also be set using `ATMOS_WORKFLOWS_BASE_PATH` ENV var, or `--workflows-dir` command-line arguments + # Supports both absolute and relative paths + base_path: "stacks/workflows" + +logs: + verbose: false + colors: true diff --git a/pkg/generate/terraform_generate_varfiles_test.go b/pkg/generate/terraform_generate_varfiles_test.go new file mode 100644 index 000000000..22c2cd112 --- /dev/null +++ b/pkg/generate/terraform_generate_varfiles_test.go @@ -0,0 +1,33 @@ +package vender + +import ( + e "github.com/cloudposse/atmos/internal/exec" + c "github.com/cloudposse/atmos/pkg/config" + "github.com/stretchr/testify/assert" + "os" + "path" + "strconv" + "testing" + "time" +) + +func TestTerraformGenerateVarfiles(t *testing.T) { + err := c.InitConfig() + assert.Nil(t, err) + + tempDir, err := os.MkdirTemp("", strconv.FormatInt(time.Now().Unix(), 10)) + assert.Nil(t, err) + + defer func(path string) { + err := os.RemoveAll(path) + assert.Nil(t, err) + }(tempDir) + + var stacks []string + var components []string + filePattern := path.Join(tempDir, "varfiles/{tenant}-{environment}-{stage}-{component}.tfvars.json") + format := "json" + + err = e.ExecuteTerraformGenerateVarfiles(filePattern, format, stacks, components) + assert.Nil(t, err) +} diff --git a/pkg/spacelift/spacelift_stack_processor_test.go b/pkg/spacelift/spacelift_stack_processor_test.go index 4f8aa747d..f843926b6 100644 --- a/pkg/spacelift/spacelift_stack_processor_test.go +++ b/pkg/spacelift/spacelift_stack_processor_test.go @@ -25,7 +25,7 @@ func TestSpaceliftStackProcessor(t *testing.T) { ) assert.Nil(t, err) - assert.Equal(t, 42, len(spaceliftStacks)) + assert.Equal(t, 35, len(spaceliftStacks)) tenant1Ue2DevInfraVpcStack := spaceliftStacks["tenant1-ue2-dev-infra-vpc"].(map[string]any) tenant1Ue2DevInfraVpcStackInfrastructureStackName := tenant1Ue2DevInfraVpcStack["stack"].(string) @@ -132,7 +132,7 @@ func TestLegacySpaceliftStackProcessor(t *testing.T) { ) assert.Nil(t, err) - assert.Equal(t, 42, len(spaceliftStacks)) + assert.Equal(t, 35, len(spaceliftStacks)) tenant1Ue2DevInfraVpcStack := spaceliftStacks["orgs-cp-tenant1-dev-us-east-2-infra-vpc"].(map[string]any) tenant1Ue2DevInfraVpcStackBackend := tenant1Ue2DevInfraVpcStack["backend"].(map[any]any) diff --git a/pkg/utils/file_utils.go b/pkg/utils/file_utils.go index 42c71deae..978d30d2e 100644 --- a/pkg/utils/file_utils.go +++ b/pkg/utils/file_utils.go @@ -97,3 +97,15 @@ func JoinAbsolutePathWithPath(basePath string, providedPath string) (string, err return absPath, nil } + +// EnsureDir accepts a file path and creates all the intermediate subdirectories +func EnsureDir(fileName string) error { + dirName := filepath.Dir(fileName) + if _, err := os.Stat(dirName); err != nil { + err := os.MkdirAll(dirName, os.ModePerm) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/utils/json_utils.go b/pkg/utils/json_utils.go index 97e2d48b5..8e1059398 100644 --- a/pkg/utils/json_utils.go +++ b/pkg/utils/json_utils.go @@ -3,14 +3,14 @@ package utils import ( "fmt" jsoniter "github.com/json-iterator/go" - "io/ioutil" "os" "strings" ) // PrintAsJSON prints the provided value as YAML document to the console func PrintAsJSON(data any) error { - var json = jsoniter.ConfigDefault + // ConfigCompatibleWithStandardLibrary will sort the map keys in the alphabetical order + var json = jsoniter.ConfigCompatibleWithStandardLibrary j, err := json.MarshalIndent(data, "", strings.Repeat(" ", 2)) if err != nil { return err @@ -21,12 +21,13 @@ func PrintAsJSON(data any) error { // WriteToFileAsJSON converts the provided value to YAML and writes it to the provided file func WriteToFileAsJSON(filePath string, data any, fileMode os.FileMode) error { - var json = jsoniter.ConfigDefault + // ConfigCompatibleWithStandardLibrary will sort the map keys in the alphabetical order + var json = jsoniter.ConfigCompatibleWithStandardLibrary j, err := json.MarshalIndent(data, "", strings.Repeat(" ", 2)) if err != nil { return err } - err = ioutil.WriteFile(filePath, j, fileMode) + err = os.WriteFile(filePath, j, fileMode) if err != nil { return err } diff --git a/pkg/utils/os_utils.go b/pkg/utils/os_utils.go index 0b2f5e19d..7b02a9c0f 100644 --- a/pkg/utils/os_utils.go +++ b/pkg/utils/os_utils.go @@ -43,14 +43,26 @@ func PrintErrorVerbose(err error) { } } -// PrintInfo prints the provided message +// PrintInfo prints the provided info message func PrintInfo(message string) { color.Cyan("%s", message) } -// PrintInfoVerbose checks the log level and prints the provided message +// PrintInfoVerbose checks the log level and prints the provided info message func PrintInfoVerbose(message string) { if g.LogVerbose { PrintInfo(message) } } + +// PrintMessage prints the provided message to the console +func PrintMessage(message string) { + fmt.Println(message) +} + +// PrintMessageVerbose checks the log level and prints the provided message to the console +func PrintMessageVerbose(message string) { + if g.LogVerbose { + PrintMessage(message) + } +} diff --git a/pkg/utils/yaml_utils.go b/pkg/utils/yaml_utils.go index 414429e56..8515bf3db 100644 --- a/pkg/utils/yaml_utils.go +++ b/pkg/utils/yaml_utils.go @@ -3,7 +3,6 @@ package utils import ( "fmt" "gopkg.in/yaml.v2" - "io/ioutil" "os" ) @@ -23,7 +22,7 @@ func WriteToFileAsYAML(filePath string, data any, fileMode os.FileMode) error { if err != nil { return err } - err = ioutil.WriteFile(filePath, y, fileMode) + err = os.WriteFile(filePath, y, fileMode) if err != nil { return err }