Skip to content

Commit

Permalink
Add Atmos CLI command aliases (#547)
Browse files Browse the repository at this point in the history
* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* updates

* Update website/docs/cli/configuration.mdx

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>

---------

Co-authored-by: Erik Osterman (CEO @ Cloud Posse) <[email protected]>
  • Loading branch information
aknysh and osterman committed Feb 28, 2024
1 parent 9c25fb0 commit d040264
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 109 deletions.
38 changes: 0 additions & 38 deletions atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ commands:
- 'echo Dependencies: "{{ .ComponentConfig.deps }}"'
- 'echo settings.config.is_prod: "{{ .ComponentConfig.settings.config.is_prod }}"'
- 'echo ATMOS_IS_PROD: "$ATMOS_IS_PROD"'

- name: list
description: Execute 'atmos list' commands
# subcommands
Expand Down Expand Up @@ -224,43 +223,6 @@ commands:
{{ end }}
{{ end }}
- name: set-eks-cluster
description: Download 'kubeconfig' and set EKS cluster
verbose: false # Set to `true` to see verbose outputs
arguments:
- name: component
description: Name of the component
flags:
- name: stack
shorthand: s
description: Name of the stack
required: true
- name: role
shorthand: r
description: IAM role to use
required: true
# If a custom command defines 'component_config' section with 'component' and 'stack',
# Atmos generates the config for the component in the stack
# and makes it available in {{ .ComponentConfig.xxx.yyy.zzz }} Go template variables,
# exposing all the component sections (which are also shown by 'atmos describe component' command)
component_config:
component: "{{ .Arguments.component }}"
stack: "{{ .Flags.stack }}"
env:
- key: KUBECONFIG
value: /dev/shm/kubecfg.{{ .Flags.stack }}-{{ .Flags.role }}
steps:
- >
aws
--profile {{ .ComponentConfig.vars.namespace }}-{{ .ComponentConfig.vars.tenant }}-gbl-{{ .ComponentConfig.vars.stage }}-{{ .Flags.role }}
--region {{ .ComponentConfig.vars.region }}
eks update-kubeconfig
--name={{ .ComponentConfig.vars.namespace }}-{{ .Flags.stack }}-eks-cluster
--kubeconfig="${KUBECONFIG}"
> /dev/null
- chmod 600 ${KUBECONFIG}
- echo ${KUBECONFIG}

# Integrations
integrations:

Expand Down
84 changes: 65 additions & 19 deletions cmd/cmd_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,6 @@ import (
u "github.com/cloudposse/atmos/pkg/utils"
)

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{
"atlantis": atlantisCmd,
"aws": awsCmd,
"completion": completionCmd,
"describe": describeCmd,
"docs": docsCmd,
"helmfile": helmfileCmd,
"terraform": terraformCmd,
"validate": validateCmd,
"vendor": vendorCmd,
"version": versionCmd,
"workflow": workflowCmd,
}
)

// processCustomCommands processes and executes custom commands
func processCustomCommands(
cliConfig schema.CliConfiguration,
Expand All @@ -44,6 +25,11 @@ func processCustomCommands(
topLevel bool,
) error {
var command *cobra.Command
existingTopLevelCommands := make(map[string]*cobra.Command)

if topLevel {
existingTopLevelCommands = getTopLevelCommands()
}

for _, commandCfg := range commands {
// Clone the 'commandCfg' struct into a local variable because of the automatic closure in the `Run` function of the Cobra command.
Expand Down Expand Up @@ -107,6 +93,55 @@ func processCustomCommands(
return nil
}

// processCommandAliases processes the command aliases
func processCommandAliases(
cliConfig schema.CliConfiguration,
aliases schema.CommandAliases,
parentCommand *cobra.Command,
topLevel bool,
) error {
existingTopLevelCommands := make(map[string]*cobra.Command)

if topLevel {
existingTopLevelCommands = getTopLevelCommands()
}

for k, v := range aliases {
alias := strings.TrimSpace(k)

if _, exist := existingTopLevelCommands[alias]; !exist && topLevel {
aliasCmd := strings.TrimSpace(v)
aliasFor := fmt.Sprintf("alias for '%s'", aliasCmd)

var aliasCommand = &cobra.Command{
Use: alias,
Short: aliasFor,
Long: aliasFor,
FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: true},
Run: func(cmd *cobra.Command, args []string) {
err := cmd.ParseFlags(args)
if err != nil {
u.LogErrorAndExit(err)
}

commandToRun := fmt.Sprintf("%s %s %s", os.Args[0], aliasCmd, strings.Join(args, " "))
err = e.ExecuteShell(cliConfig, commandToRun, commandToRun, ".", nil, false)
if err != nil {
u.LogErrorAndExit(err)
}
},
}

aliasCommand.DisableFlagParsing = true

// Add the alias to the parent command
parentCommand.AddCommand(aliasCommand)
}
}

return nil
}

// preCustomCommand is run before a custom command is executed
func preCustomCommand(
cmd *cobra.Command,
Expand All @@ -128,6 +163,17 @@ func preCustomCommand(
}
}

// getTopLevelCommands returns the top-level commands
func getTopLevelCommands() map[string]*cobra.Command {
existingTopLevelCommands := make(map[string]*cobra.Command)

for _, c := range RootCmd.Commands() {
existingTopLevelCommands[c.Name()] = c
}

return existingTopLevelCommands
}

// executeCustomCommand executes a custom command
func executeCustomCommand(
cliConfig schema.CliConfiguration,
Expand Down
31 changes: 18 additions & 13 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,6 @@ func Execute() error {
}
}

return RootCmd.Execute()
}

func init() {
RootCmd.PersistentFlags().String("redirect-stderr", "", "File descriptor to redirect 'stderr' to. "+
"Errors can be redirected to any file or any standard file descriptor (including '/dev/null'): atmos <command> --redirect-stderr /dev/stdout")

RootCmd.PersistentFlags().String("logs-level", "Info", "Logs level. Supported log levels are Trace, Debug, Info, Warning, Off. If the log level is set to Off, Atmos will not log any messages")
RootCmd.PersistentFlags().String("logs-file", "/dev/stdout", "The file to write Atmos logs to. Logs can be written to any file or any standard file descriptor, including '/dev/stdout', '/dev/stderr' and '/dev/null'")

cobra.OnInitialize(initConfig)

// InitCliConfig 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
Expand All @@ -84,13 +72,30 @@ func init() {
u.LogErrorAndExit(err)
}

// If CLI configuration was found, add its custom commands
// If CLI configuration was found, process its custom commands and command aliases
if err == nil {
err = processCustomCommands(cliConfig, cliConfig.Commands, RootCmd, true)
if err != nil {
u.LogErrorAndExit(err)
}

err = processCommandAliases(cliConfig, cliConfig.CommandAliases, RootCmd, true)
if err != nil {
u.LogErrorAndExit(err)
}
}

return RootCmd.Execute()
}

func init() {
RootCmd.PersistentFlags().String("redirect-stderr", "", "File descriptor to redirect 'stderr' to. "+
"Errors can be redirected to any file or any standard file descriptor (including '/dev/null'): atmos <command> --redirect-stderr /dev/stdout")

RootCmd.PersistentFlags().String("logs-level", "Info", "Logs level. Supported log levels are Trace, Debug, Info, Warning, Off. If the log level is set to Off, Atmos will not log any messages")
RootCmd.PersistentFlags().String("logs-file", "/dev/stdout", "The file to write Atmos logs to. Logs can be written to any file or any standard file descriptor, including '/dev/stdout', '/dev/stderr' and '/dev/null'")

cobra.OnInitialize(initConfig)
}

func initConfig() {
Expand Down
4 changes: 2 additions & 2 deletions examples/quick-start/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Geodesic: https://github.com/cloudposse/geodesic/
ARG GEODESIC_VERSION=2.9.1
ARG GEODESIC_VERSION=2.9.2
ARG GEODESIC_OS=debian

# Atmos
# https://atmos.tools/
# https://github.com/cloudposse/atmos
# https://github.com/cloudposse/atmos/releases
ARG ATMOS_VERSION=1.64.0
ARG ATMOS_VERSION=1.65.0

# Terraform: https://github.com/hashicorp/terraform/releases
ARG TF_VERSION=1.7.3
Expand Down
18 changes: 0 additions & 18 deletions examples/quick-start/rootfs/usr/local/etc/atmos/atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,24 +116,6 @@ commands:
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 Hello world
- name: ping
description: This command plays ping-pong
# If 'verbose' is set to 'true', atmos will output some info messages to the console before executing the command's steps
# If 'verbose' is not defined, it implicitly defaults to 'false'
verbose: true
steps:
- echo Playing ping-pong...
- echo pong
- name: show
description: Execute 'show' commands
# subcommands
Expand Down
13 changes: 13 additions & 0 deletions examples/tests/atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,16 @@ schemas:
# Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "../quick-start/stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"

# CLI command aliases
aliases:
# Aliases for Atmos native commands
tf: terraform
tp: terraform plan
up: terraform apply
down: terraform destroy
ds: describe stacks
dc: describe component
# Aliases for Atmos custom commands
ls: list stacks
lc: list components
13 changes: 13 additions & 0 deletions examples/tests/rootfs/usr/local/etc/atmos/atmos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,16 @@ schemas:
# Can also be set using 'ATMOS_SCHEMAS_ATMOS_MANIFEST' ENV var, or '--schemas-atmos-manifest' command-line arguments
# Supports both absolute and relative paths (relative to the `base_path` setting in `atmos.yaml`)
manifest: "stacks/schemas/atmos/atmos-manifest/1.0/atmos-manifest.json"

# CLI command aliases
aliases:
# Aliases for Atmos native commands
tf: terraform
tp: terraform plan
up: terraform apply
down: terraform destroy
ds: describe stacks
dc: describe component
# Aliases for Atmos custom commands
ls: list stacks
lc: list components
39 changes: 22 additions & 17 deletions pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ package schema

// CliConfiguration structure represents schema for `atmos.yaml` CLI config
type CliConfiguration struct {
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,omitempty" json:"workflows,omitempty" mapstructure:"workflows"`
Logs Logs `yaml:"logs,omitempty" json:"logs,omitempty" mapstructure:"logs"`
Commands []Command `yaml:"commands,omitempty" json:"commands,omitempty" mapstructure:"commands"`
Integrations Integrations `yaml:"integrations,omitempty" json:"integrations,omitempty" mapstructure:"integrations"`
Schemas Schemas `yaml:"schemas,omitempty" json:"schemas,omitempty" mapstructure:"schemas"`
Initialized bool `yaml:"initialized" json:"initialized" mapstructure:"initialized"`
StacksBaseAbsolutePath string `yaml:"stacksBaseAbsolutePath,omitempty" json:"stacksBaseAbsolutePath,omitempty" mapstructure:"stacksBaseAbsolutePath"`
IncludeStackAbsolutePaths []string `yaml:"includeStackAbsolutePaths,omitempty" json:"includeStackAbsolutePaths,omitempty" mapstructure:"includeStackAbsolutePaths"`
ExcludeStackAbsolutePaths []string `yaml:"excludeStackAbsolutePaths,omitempty" json:"excludeStackAbsolutePaths,omitempty" mapstructure:"excludeStackAbsolutePaths"`
TerraformDirAbsolutePath string `yaml:"terraformDirAbsolutePath,omitempty" json:"terraformDirAbsolutePath,omitempty" mapstructure:"terraformDirAbsolutePath"`
HelmfileDirAbsolutePath string `yaml:"helmfileDirAbsolutePath,omitempty" json:"helmfileDirAbsolutePath,omitempty" mapstructure:"helmfileDirAbsolutePath"`
StackConfigFilesRelativePaths []string `yaml:"stackConfigFilesRelativePaths,omitempty" json:"stackConfigFilesRelativePaths,omitempty" mapstructure:"stackConfigFilesRelativePaths"`
StackConfigFilesAbsolutePaths []string `yaml:"stackConfigFilesAbsolutePaths,omitempty" json:"stackConfigFilesAbsolutePaths,omitempty" mapstructure:"stackConfigFilesAbsolutePaths"`
StackType string `yaml:"stackType,omitempty" json:"StackType,omitempty" mapstructure:"stackType"`
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,omitempty" json:"workflows,omitempty" mapstructure:"workflows"`
Logs Logs `yaml:"logs,omitempty" json:"logs,omitempty" mapstructure:"logs"`
Commands []Command `yaml:"commands,omitempty" json:"commands,omitempty" mapstructure:"commands"`
CommandAliases CommandAliases `yaml:"aliases,omitempty" json:"aliases,omitempty" mapstructure:"aliases"`
Integrations Integrations `yaml:"integrations,omitempty" json:"integrations,omitempty" mapstructure:"integrations"`
Schemas Schemas `yaml:"schemas,omitempty" json:"schemas,omitempty" mapstructure:"schemas"`
Initialized bool `yaml:"initialized" json:"initialized" mapstructure:"initialized"`
StacksBaseAbsolutePath string `yaml:"stacksBaseAbsolutePath,omitempty" json:"stacksBaseAbsolutePath,omitempty" mapstructure:"stacksBaseAbsolutePath"`
IncludeStackAbsolutePaths []string `yaml:"includeStackAbsolutePaths,omitempty" json:"includeStackAbsolutePaths,omitempty" mapstructure:"includeStackAbsolutePaths"`
ExcludeStackAbsolutePaths []string `yaml:"excludeStackAbsolutePaths,omitempty" json:"excludeStackAbsolutePaths,omitempty" mapstructure:"excludeStackAbsolutePaths"`
TerraformDirAbsolutePath string `yaml:"terraformDirAbsolutePath,omitempty" json:"terraformDirAbsolutePath,omitempty" mapstructure:"terraformDirAbsolutePath"`
HelmfileDirAbsolutePath string `yaml:"helmfileDirAbsolutePath,omitempty" json:"helmfileDirAbsolutePath,omitempty" mapstructure:"helmfileDirAbsolutePath"`
StackConfigFilesRelativePaths []string `yaml:"stackConfigFilesRelativePaths,omitempty" json:"stackConfigFilesRelativePaths,omitempty" mapstructure:"stackConfigFilesRelativePaths"`
StackConfigFilesAbsolutePaths []string `yaml:"stackConfigFilesAbsolutePaths,omitempty" json:"stackConfigFilesAbsolutePaths,omitempty" mapstructure:"stackConfigFilesAbsolutePaths"`
StackType string `yaml:"stackType,omitempty" json:"StackType,omitempty" mapstructure:"stackType"`
}

type Terraform struct {
Expand Down Expand Up @@ -279,6 +280,10 @@ type CommandComponentConfig struct {
Stack string `yaml:"stack" json:"stack" mapstructure:"stack"`
}

// CLI command aliases

type CommandAliases map[string]string

// Integrations

type Integrations struct {
Expand Down
Loading

0 comments on commit d040264

Please sign in to comment.