Skip to content

Commit

Permalink
Add ATMOS_CLI_CONFIG_PATH ENV var. Add functionality to define `atm…
Browse files Browse the repository at this point in the history
…os` custom CLI commands (cloudposse#168)

* Update CLI docs

* Update CLI docs

* Update CLI docs

* Add custom commands

* Add custom commands

* Add `ATMOS_CLI_CONFIG_PATH` ENV var

* Add `ATMOS_CLI_CONFIG_PATH` ENV var

* Improve invalid stack config detection and error messages

* Improve invalid stack config detection and error messages

* Add custom commands

* Add custom commands

* Add custom commands

* Add custom commands

* Add custom commands

* Add custom commands

* Add custom commands

* Add custom commands

* Add custom commands

* Add custom commands

* Update cmd/cmd_utils.go

Co-authored-by: nitrocode <[email protected]>

* Update internal/exec/shell_utils.go

Co-authored-by: nitrocode <[email protected]>

* Add custom commands

Co-authored-by: nitrocode <[email protected]>
  • Loading branch information
aknysh and nitrocode authored Jun 27, 2022
1 parent 8a3ebcc commit 0dcb91d
Show file tree
Hide file tree
Showing 15 changed files with 585 additions and 52 deletions.
66 changes: 66 additions & 0 deletions atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,69 @@ workflows:
logs:
verbose: false
colors: true

# Custom CLI commands
commands:
- name: tf
description: Execute terraform commands
# subcommands
commands:
- name: plan
description: This command plans terraform components
arguments:
- name: component
description: Name of the component
flags:
- name: stack
shorthand: s
description: Name of the stack
required: true
env:
- key: ENV_VAR_1
value: ENV_VAR_1_value
- key: ENV_VAR_2
# `valueCommand` is an external command to execute to get the value for the ENV var
# Either 'value' or 'valueCommand' can be specified for the ENV var, but not both
valueCommand: echo ENV_VAR_2_value
# steps support Go templates
steps:
- atmos terraform plan {{ .Arguments.component }} -s {{ .Flags.stack }}
- name: terraform
description: Execute terraform commands
# subcommands
commands:
- name: provision
description: This command provisions terraform components
arguments:
- name: component
description: Name of the component
flags:
- name: stack
shorthand: s
description: Name of the stack
required: true
# ENV var values support Go templates
env:
- key: ATMOS_COMPONENT
value: "{{ .Arguments.component }}"
- key: ATMOS_STACK
value: "{{ .Flags.stack }}"
steps:
- atmos terraform plan $ATMOS_COMPONENT -s $ATMOS_STACK
- atmos terraform apply $ATMOS_COMPONENT -s $ATMOS_STACK
- name: play
description: This command plays games
steps:
- echo Playing...
# subcommands
commands:
- name: hello
description: This command says Hello world
steps:
- echo Saying Hello world...
- echo Hello world
- name: ping
description: This command plays ping-pong
steps:
- echo Playing ping-pong...
- echo pong
198 changes: 198 additions & 0 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package cmd

import (
"bytes"
"fmt"
e "github.com/cloudposse/atmos/internal/exec"
c "github.com/cloudposse/atmos/pkg/config"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/spf13/cobra"
"os"
"strings"
"text/template"
)

var (
// This map contains the existing atmos top-level commands
// All custom top-level commands will be checked against this map in order to not override `atmos` top-level commands,
// but just add subcommands to them
existingTopLevelCommands = map[string]*cobra.Command{
"terraform": terraformCmd,
"helmfile": helmfileCmd,
"describe": describeCmd,
"aws": awsCmd,
"validate": validateCmd,
"vendor": vendorCmd,
"workflow": workflowCmd,
"version": versionCmd,
}
)

func processCustomCommands(commands []c.Command, parentCommand *cobra.Command, topLevel bool) error {
var command *cobra.Command

for _, commandConfig := range commands {
if _, exist := existingTopLevelCommands[commandConfig.Name]; exist && topLevel {
command = existingTopLevelCommands[commandConfig.Name]
} else {
// Deep-copy the slices because of the automatic closure in the `Run` function of the command.
// https://www.calhoun.io/gotchas-and-common-mistakes-with-closures-in-go/
// It will make a closure on the new local variables which are different in each iteration.
customCommandSteps := make([]string, len(commandConfig.Steps))
copy(customCommandSteps, commandConfig.Steps)
customCommandArguments := make([]c.CommandArgument, len(commandConfig.Arguments))
copy(customCommandArguments, commandConfig.Arguments)
customCommandFlags := make([]c.CommandFlag, len(commandConfig.Flags))
copy(customCommandFlags, commandConfig.Flags)
customEnvVars := make([]c.CommandEnv, len(commandConfig.Env))
copy(customEnvVars, commandConfig.Env)

var customCommand = &cobra.Command{
Use: commandConfig.Name,
Short: commandConfig.Description,
Long: commandConfig.Description,
Run: func(cmd *cobra.Command, args []string) {
var err error
var t *template.Template

if len(args) != len(customCommandArguments) {
err = fmt.Errorf("invalid number of arguments, %d argument(s) required", len(customCommandArguments))
u.PrintErrorToStdErrorAndExit(err)
}

// Execute custom command's steps
for i, step := range customCommandSteps {
// Prepare template data for arguments
argumentsData := map[string]string{}
for ix, arg := range customCommandArguments {
argumentsData[arg.Name] = args[ix]
}

// Prepare template data for flags
flags := cmd.Flags()
flagsData := map[string]string{}
for _, fl := range customCommandFlags {
if fl.Type == "" || fl.Type == "string" {
providedFlag, err := flags.GetString(fl.Name)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
flagsData[fl.Name] = providedFlag
}
}

// Prepare full template data
var data = map[string]map[string]string{
"Arguments": argumentsData,
"Flags": flagsData,
}

// Parse and execute Go templates in the command's steps
t, err = template.New(fmt.Sprintf("step-%d", i)).Parse(step)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
var tpl bytes.Buffer
err = t.Execute(&tpl, data)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}

// Prepare ENV vars
var envVarsList []string
for _, v := range customEnvVars {
key := v.Key
value := v.Value
valCommand := v.ValueCommand

if value != "" && valCommand != "" {
err = fmt.Errorf("either 'value' or 'valueCommand' can be specified for the ENV var, but not both.\n"+
"Custom command '%s %s' defines 'value=%s' and 'valueCommand=%s' for the ENV var '%s'",
parentCommand.Name(), commandConfig.Name, value, valCommand, key)
u.PrintErrorToStdErrorAndExit(err)
}

// If the command to get the value for the ENV var is provided, execute it
if valCommand != "" {
valCommandArgs := strings.Fields(valCommand)
res, err := e.ExecuteShellCommandAndReturnOutput(valCommandArgs[0], valCommandArgs[1:], ".", nil, false)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
value = res
} else {
// Parse and execute Go templates in the values of the command's ENV vars
t, err = template.New(fmt.Sprintf("env-var-%d", i)).Parse(value)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
var tplEnvVarValue bytes.Buffer
err = t.Execute(&tplEnvVarValue, data)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
value = tplEnvVarValue.String()
}

envVarsList = append(envVarsList, fmt.Sprintf("%s=%s", key, value))
err = os.Setenv(key, value)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
}

if len(envVarsList) > 0 {
u.PrintInfo("\nUsing ENV vars:")
for _, v := range envVarsList {
fmt.Println(v)
}
}

commandToRun := os.ExpandEnv(tpl.String())

// Execute the command step
stepArgs := strings.Fields(commandToRun)
err = e.ExecuteShellCommand(stepArgs[0], stepArgs[1:], ".", envVarsList, false)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
}
},
}

// Add customCommandFlags
for _, flag := range customCommandFlags {
if flag.Type == "bool" {
if flag.Shorthand != "" {
customCommand.PersistentFlags().BoolP(flag.Name, flag.Shorthand, false, flag.Usage)
} else {
customCommand.PersistentFlags().Bool(flag.Name, false, flag.Usage)
}
} else {
if flag.Shorthand != "" {
customCommand.PersistentFlags().StringP(flag.Name, flag.Shorthand, "", flag.Usage)
} else {
customCommand.PersistentFlags().String(flag.Name, "", flag.Usage)
}
}

if flag.Required {
err := customCommand.MarkPersistentFlagRequired(flag.Name)
if err != nil {
return err
}
}
}

parentCommand.AddCommand(customCommand)
command = customCommand
}

err := processCustomCommands(commandConfig.Commands, command, false)
if err != nil {
return err
}
}

return nil
}
18 changes: 18 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
c "github.com/cloudposse/atmos/pkg/config"
u "github.com/cloudposse/atmos/pkg/utils"
"github.com/spf13/cobra"
)

Expand All @@ -19,11 +21,27 @@ func Execute() error {

func init() {
cobra.OnInitialize(initConfig)

// InitConfig finds and merges CLI configurations in the following order:
// system dir, home dir, current dir, ENV vars, command-line arguments
// Here we need the custom commands from the config
err := c.InitConfig()
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}

// Add custom commands from the CLI config
err = processCustomCommands(c.Config.Commands, RootCmd, true)
if err != nil {
u.PrintErrorToStdErrorAndExit(err)
}
}

func initConfig() {
}

// https://www.sobyte.net/post/2021-12/create-cli-app-with-cobra/
// https://github.com/spf13/cobra/blob/master/user_guide.md
// https://blog.knoldus.com/create-kubectl-like-cli-with-go-and-cobra/
// https://pkg.go.dev/github.com/c-bata/go-prompt
// https://pkg.go.dev/github.com/spf13/cobra
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ components:
- "mixin/test-1"
- "mixin/test-2"
# Override Terraform workspace
# Note that by default, Terraform workspace is generated from the context, e.g. `<environment>-<stage>`
# Note that by default, Terraform workspace is generated from the context, e.g. `<tenant>-<environment>-<stage>`
terraform_workspace: test-component-override-3-workspace
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ go 1.18
require (
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/fatih/color v1.13.0
github.com/hashicorp/go-getter v1.6.1
github.com/hashicorp/go-getter v1.6.2
github.com/imdario/mergo v0.3.13
github.com/json-iterator/go v1.1.12
github.com/mitchellh/go-homedir v1.1.0
github.com/otiai10/copy v1.7.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.4.0
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.2
github.com/stretchr/testify v1.7.5
gopkg.in/yaml.v2 v2.4.0
)

Expand Down
15 changes: 8 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -196,8 +196,8 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-getter v1.6.1 h1:NASsgP4q6tL94WH6nJxKWj8As2H/2kop/bB1d8JMyRY=
github.com/hashicorp/go-getter v1.6.1/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA=
github.com/hashicorp/go-getter v1.6.2 h1:7jX7xcB+uVCliddZgeKyNxv0xoT7qL5KDtH7rU4IqIk=
github.com/hashicorp/go-getter v1.6.2/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA=
github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
Expand Down Expand Up @@ -276,24 +276,25 @@ github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
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/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=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/stretchr/testify v1.7.5/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/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
Expand Down
3 changes: 2 additions & 1 deletion internal/exec/aws_eks_update_kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,13 @@ func ExecuteAwsEksUpdateKubeconfig(kubeconfigContext c.AwsEksUpdateKubeconfigCon
kubeconfigContext.Stack = stack
}

var err error
var configAndStacksInfo c.ConfigAndStacksInfo
configAndStacksInfo.ComponentFromArg = kubeconfigContext.Component
configAndStacksInfo.Stack = kubeconfigContext.Stack

configAndStacksInfo.ComponentType = "terraform"
configAndStacksInfo, err := ProcessStacks(configAndStacksInfo, true)
configAndStacksInfo, err = ProcessStacks(configAndStacksInfo, true)
shellCommandWorkingDir = path.Join(c.ProcessedConfig.TerraformDirAbsolutePath, configAndStacksInfo.ComponentFolderPrefix, configAndStacksInfo.FinalComponent)
if err != nil {
configAndStacksInfo.ComponentType = "helmfile"
Expand Down
Loading

0 comments on commit 0dcb91d

Please sign in to comment.