posh-providers/gruntwork-io/terragrunt/command.go
2024-09-19 14:30:01 +02:00

311 lines
9.9 KiB
Go

package terragrunt
import (
"context"
"errors"
"os"
"os/exec"
"path"
"strings"
"github.com/foomo/posh-providers/onepassword"
"github.com/foomo/posh/pkg/cache"
"github.com/foomo/posh/pkg/command/tree"
"github.com/foomo/posh/pkg/env"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/prompt/goprompt"
"github.com/foomo/posh/pkg/readline"
"github.com/foomo/posh/pkg/shell"
"github.com/foomo/posh/pkg/util/files"
"github.com/foomo/posh/pkg/util/suggests"
"github.com/spf13/viper"
"go.uber.org/zap"
)
type (
Command struct {
l log.Logger
op *onepassword.OnePassword
cfg Config
name string
cache cache.Namespace
configKey string
commandTree tree.Root
}
StackNameProvider func(path string) string
CommandOption func(*Command)
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func CommandWithName(v string) CommandOption {
return func(o *Command) {
o.name = v
}
}
func WithConfigKey(v string) CommandOption {
return func(o *Command) {
o.configKey = v
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewCommand(l log.Logger, op *onepassword.OnePassword, cache cache.Cache, opts ...CommandOption) (*Command, error) {
inst := &Command{
l: l.Named("terragrunt"),
op: op,
name: "terragrunt",
cache: cache.Get("terragrunt"),
configKey: "terragrunt",
}
for _, opt := range opts {
if opt != nil {
opt(inst)
}
}
if err := viper.UnmarshalKey(inst.configKey, &inst.cfg); err != nil {
return nil, err
}
if err := os.Setenv("TERRAGRUNT_DOWNLOAD", env.Path(inst.cfg.CachePath)); err != nil {
return nil, err
}
stackArgs := tree.Args{
{
Name: "stacks",
Description: "Stacks to run",
Repeat: true,
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
return inst.getStacks(ctx, r)
},
},
}
inst.commandTree = tree.New(&tree.Node{
Name: inst.name,
Description: "Run terragrunt commands",
Nodes: tree.Nodes{
{
Name: "env",
Values: inst.getEnvs,
Description: "Environment to provision",
Nodes: tree.Nodes{
{
Name: "site",
Values: inst.getSites,
Description: "Site to provision",
Nodes: tree.Nodes{
{
Name: "secrets",
Description: "Render secret templates",
Execute: inst.secrets,
},
// terraform: main commands
{
Name: "init",
Description: "Prepare your working directory for other commands",
Args: stackArgs,
Execute: inst.execute,
},
{
Name: "validate",
Description: "Check whether the configuration is valid",
Args: stackArgs,
Execute: inst.execute,
},
{
Name: "plan",
Description: "Show changes required by the current configuration",
Args: stackArgs,
Execute: inst.execute,
},
{
Name: "apply",
Description: "Create or update infrastructure",
Args: stackArgs,
Execute: inst.execute,
},
{
Name: "destroy",
Description: "Destroy previously-created infrastructure",
Args: stackArgs,
Execute: inst.execute,
},
{
Name: "refresh",
Description: "Update the state to match remote systems",
Args: stackArgs,
Execute: inst.execute,
},
{
Name: "output",
Description: "Show output values from your root module",
Args: stackArgs,
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
fs.Default().String("raw", "", "Print the raw string directly")
return nil
},
Execute: inst.execute,
},
// {
// Name: "command",
// Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
// return []prompt.Suggest{
// // terraform: main commands
// {Text: "init", Description: "Prepare your working directory for other commands"},
// {Text: "validate", Description: "Check whether the configuration is valid"},
// {Text: "plan", Description: "Show changes required by the current configuration"},
// {Text: "apply", Description: "Create or update infrastructure"},
// {Text: "destroy", Description: "Destroy previously-created infrastructure"},
// // terraform: other commands
// {Text: "hclfmt", Description: "Reformat your configuration in the standard style"},
// // {Text: "console", Description: "Try Terragrunt expressions at an interactive command prompt"},
// {Text: "force-unlock", Description: "Unlock a stuck lock on the current workspace"},
// {Text: "get", Description: "Install or upgrade remote Terragrunt modules"},
// {Text: "graph", Description: "Generate a Graphviz graph of the steps in an operation"},
// {Text: "import", Description: "Associate existing infrastructure with a Terragrunt resource"},
// // {Text: "login", Description: "Obtain and save credentials for a remote host"},
// // {Text: "logout", Description: "Remove locally-stored credentials for a remote host"},
// {Text: "output", Description: "Show output values from your root module"},
// {Text: "providers:lock", Description: "Write out dependency locks for the configured providers"},
// {Text: "refresh", Description: "Update the state to match remote systems"},
// {Text: "show", Description: "Show the current state or a saved plan"},
// {Text: "state", Description: "Advanced state management"},
// // {"taint", "Mark a resource instance as not fully functional"},
// // {"untaint", "Remove the 'tainted' state from a resource instance"},
// // {"version", "Show the current Terragrunt version"},
// // {"workspace", "Workspace management"},
// }
// },
// Description: "Terragrunt command to execute",
// Args: tree.Args{
// {
// Name: "stacks",
// Description: "Stacks to run",
// Repeat: true,
// Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
// return inst.getStacks(ctx, r)
// },
// },
// },
// Execute: inst.execute,
// },
},
Execute: inst.execute,
},
},
},
},
})
return inst, nil
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (c *Command) Name() string {
return c.commandTree.Node().Name
}
func (c *Command) Description() string {
return c.commandTree.Node().Description
}
func (c *Command) Complete(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
return c.commandTree.Complete(ctx, r)
}
func (c *Command) Execute(ctx context.Context, r *readline.Readline) error {
return c.commandTree.Execute(ctx, r)
}
func (c *Command) Validate(ctx context.Context, r *readline.Readline) error {
if _, err := exec.LookPath("terragrunt"); err != nil {
c.l.Print()
return errors.New("missing terragrunt executable")
}
return nil
}
func (c *Command) Help(ctx context.Context, r *readline.Readline) string {
return c.commandTree.Help(ctx, r)
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (c *Command) secrets(ctx context.Context, r *readline.Readline) error {
envName := r.Args().At(0)
siteName := r.Args().At(1)
// validate stack & change dir
values, err := files.Find(ctx, c.cfg.StacksPath(envName, siteName), "secrets.tpl.yaml", files.FindWithIsFile(true))
if err != nil {
return err
}
c.l.Info("Rendering secret templates...")
for _, value := range values {
c.l.Info("└ " + value)
if err := c.op.RenderFileTo(ctx, value, strings.Replace(value, ".tpl.yaml", ".yaml", 1)); err != nil {
return err
}
}
return nil
}
func (c *Command) execute(ctx context.Context, r *readline.Readline) error {
envName := r.Args().At(0)
siteName := r.Args().At(1)
command := r.Args().At(2)
stacks := r.Args().From(3)
c.l.Info("Running terragrunt...")
for _, stack := range stacks {
c.l.Info("└ " + stack)
if err := shell.New(ctx, c.l, "terragrunt", command).
Args(r.AdditionalFlags()...).
Dir(path.Join(c.cfg.StacksPath(envName, siteName), stack)).
Run(); err != nil {
return err
}
}
return nil
}
func (c *Command) getEnvs(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
return c.cache.GetSuggests("envs", func() any {
return suggests.List(c.cfg.EnvNames())
})
}
func (c *Command) getSites(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
envName := r.Args().At(0)
return c.cache.GetSuggests("sites-"+envName, func() any {
return suggests.List(c.cfg.SiteNames(envName))
})
}
func (c *Command) getStacks(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
envName := r.Args().At(0)
site := r.Args().At(1)
return c.cache.GetSuggests("stacks-"+envName+"-"+site, func() any {
stacks, err := c.cfg.StackNames(ctx, envName, site)
if err != nil {
c.l.Debug("failed to retrieve stacks", zap.Error(err))
}
return suggests.List(stacks)
})
}