mirror of
https://github.com/foomo/posh-providers.git
synced 2025-10-16 12:35:41 +00:00
593 lines
20 KiB
Go
593 lines
20 KiB
Go
package pulumi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/foomo/posh-providers/azure/az"
|
|
"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/suggests"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type (
|
|
Command struct {
|
|
l log.Logger
|
|
name string
|
|
az *az.AZ
|
|
op *onepassword.OnePassword
|
|
cfg Config
|
|
cache cache.Namespace
|
|
configKey string
|
|
commandTree tree.Root
|
|
}
|
|
NamespaceFn func(cluster, fleet, squadron string) string
|
|
CommandOption func(*Command)
|
|
)
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ~ Options
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
func CommandWithName(v string) CommandOption {
|
|
return func(o *Command) {
|
|
o.name = v
|
|
}
|
|
}
|
|
|
|
func CommandWithConfigKey(v string) CommandOption {
|
|
return func(o *Command) {
|
|
o.configKey = v
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ~ Constructor
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
func NewCommand(l log.Logger, az *az.AZ, op *onepassword.OnePassword, cache cache.Cache, opts ...CommandOption) (*Command, error) {
|
|
inst := &Command{
|
|
name: "pulumi",
|
|
configKey: "pulumi",
|
|
op: op,
|
|
az: az,
|
|
}
|
|
for _, opt := range opts {
|
|
if opt != nil {
|
|
opt(inst)
|
|
}
|
|
}
|
|
inst.l = l.Named(inst.name)
|
|
inst.cache = cache.Get(inst.name)
|
|
|
|
if err := viper.UnmarshalKey(inst.configKey, &inst.cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := os.Setenv("PULUMI_HOME", env.Path(inst.cfg.ConfigPath)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inst.commandTree = tree.New(&tree.Node{
|
|
Name: "pulumi",
|
|
Description: "Open the pulumi dashboard",
|
|
Nodes: tree.Nodes{
|
|
{
|
|
Name: "env",
|
|
Values: inst.completeEnvs,
|
|
Description: "Name of the environment",
|
|
Nodes: tree.Nodes{
|
|
{
|
|
Name: "backend",
|
|
Description: "Manage state backends",
|
|
Nodes: tree.Nodes{
|
|
{
|
|
Name: "create",
|
|
Description: "Create a new object storage backend",
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().String("tags", "", "Quoted string with space-separated tags")
|
|
fs.Default().Bool("debug", false, "Show full logs")
|
|
fs.Default().Bool("vebose", false, "Increase logging verbosity")
|
|
fs.Internal().String("group-args", "", "Additional group create args")
|
|
fs.Internal().String("storage-args", "", "Additional storaage create args")
|
|
return nil
|
|
},
|
|
Execute: func(ctx context.Context, r *readline.Readline) error {
|
|
backend, err := inst.cfg.Backend(r.Args().At(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fsi := r.FlagSets().Internal()
|
|
|
|
groupArgs, err := fsi.GetString("group-args")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
groupArgs = strings.Trim(strings.Trim(groupArgs, "\""), "'")
|
|
|
|
storageArgs, err := fsi.GetString("storage-args")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
storageArgs = strings.Trim(strings.Trim(storageArgs, "\""), "'")
|
|
|
|
// Create a new resource group
|
|
inst.l.Info("creating resource group:", backend.ResourceGroup)
|
|
if err := shell.New(ctx, inst.l, "az", "group", "create").
|
|
Args("--resource-group", backend.ResourceGroup).
|
|
Args("--subscription", backend.Subscription).
|
|
Args("--location", backend.Location).
|
|
Args(strings.Split(groupArgs, " ")...).
|
|
Args(r.FlagSets().Default().Visited().Args()...).
|
|
Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a new resource group
|
|
inst.l.Info("creating storage account:", backend.StorageAccount)
|
|
if err := shell.New(ctx, inst.l, "az", "storage", "account", "create").
|
|
Args("--name", backend.StorageAccount).
|
|
Args("--resource-group", backend.ResourceGroup).
|
|
Args("--subscription", backend.Subscription).
|
|
Args("--location", backend.Location).
|
|
Args(strings.Split(storageArgs, " ")...).
|
|
Args(r.FlagSets().Default().Visited().Args()...).
|
|
Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// retrieve storage key
|
|
storageAccountKey, err := inst.getStorageAccountKey(ctx, backend)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
inst.l.Info("creating storage container:", backend.Container)
|
|
return shell.New(ctx, inst.l, "az", "storage", "container", "create").
|
|
Args("--account-name", backend.StorageAccount).
|
|
Args("--account-key", storageAccountKey).
|
|
Args("--name", backend.Container).
|
|
Run()
|
|
},
|
|
},
|
|
{
|
|
Name: "login",
|
|
Description: "Log into your object storage backend",
|
|
Execute: func(ctx context.Context, r *readline.Readline) error {
|
|
backend, err := inst.cfg.Backend(r.Args().At(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
storageAccountKey, err := inst.getStorageAccountKey(ctx, backend)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return shell.New(ctx, inst.l, "pulumi", "login", fmt.Sprintf("azblob://%s", backend.Container)).
|
|
Env("AZURE_STORAGE_ACCOUNT=" + backend.StorageAccount).
|
|
Env("AZURE_STORAGE_KEY=" + storageAccountKey).
|
|
Env("ARM_SUBSCRIPTION_ID=" + backend.Subscription).
|
|
Debug().
|
|
Run()
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Description: "Manage stacks and view stack state",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
{
|
|
Name: "command",
|
|
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
|
|
return []goprompt.Suggest{
|
|
{Text: "init", Description: "Create an empty stack with the given name, ready for updates"},
|
|
{Text: "output", Description: "Show a stack's output properties"},
|
|
{Text: "history", Description: "Display history for a stack"},
|
|
}
|
|
},
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
fs.Default().Int("verbose", 3, "Enable verbose logging")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
{
|
|
Name: "up",
|
|
Description: "Create or update the resources in a stack",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("debug", false, "Print detailed debugging output during resource operations")
|
|
fs.Default().Bool("diff", false, "Display operation as a rich diff showing the overall change")
|
|
fs.Default().Bool("expect-no-changes", false, "Return an error if any changes occur during this update")
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
fs.Default().Bool("target-dependents", false, "Allows updating of dependent targets discovered but not specified in --target list")
|
|
fs.Default().Int("verbose", 3, "Enable verbose logging")
|
|
fs.Default().StringArray("target", nil, "Specify a single resource URN to update")
|
|
fs.Default().StringArray("target-replace", nil, "Specify a single resource URN to replace")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
{
|
|
Name: "destroy",
|
|
Description: "Destroy all existing resources in the stack",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("debug", false, "Print detailed debugging output during resource operations")
|
|
fs.Default().Bool("diff", false, "Display operation as a rich diff showing the overall change")
|
|
fs.Default().Bool("exclude-protected", false, "Do not destroy protected resources")
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
fs.Default().Bool("remove", false, "Remove the stack and its config file after all resources in the stack have been deleted")
|
|
fs.Default().Bool("target-dependents", false, "Allows updating of dependent targets discovered but not specified in --target list")
|
|
fs.Default().Int("verbose", 3, "Enable verbose logging")
|
|
fs.Default().StringArray("target", nil, "Specify a single resource URN to update")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
{
|
|
Name: "preview",
|
|
Description: "Show a preview of updates to a stack's resources",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("debug", false, "Print detailed debugging output during resource operations")
|
|
fs.Default().Bool("diff", false, "Display operation as a rich diff showing the overall change")
|
|
fs.Default().Bool("expect-no-changes", false, "Return an error if any changes occur during this update")
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
fs.Default().Bool("target-dependents", false, "Allows updating of dependent targets discovered but not specified in --target list")
|
|
fs.Default().Int("verbose", 3, "Enable verbose logging")
|
|
fs.Default().StringArray("target", nil, "Specify a single resource URN to update")
|
|
fs.Default().StringArray("target-replace", nil, "Specify a single resource URN to replace")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
{
|
|
Name: "cancel",
|
|
Description: "Cancel a stack's currently running update, if any",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
{
|
|
Name: "refresh",
|
|
Description: "Refresh the resources in a stack",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("clear-pending-creates", false, "Clear all pending creates, dropping them from the state")
|
|
fs.Default().Bool("debug", false, "Print detailed debugging output during resource operations")
|
|
fs.Default().Bool("diff", false, "Display operation as a rich diff showing the overall change")
|
|
fs.Default().Bool("expect-no-changes", false, "Return an error if any changes occur during this update")
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
fs.Default().Bool("show-replacement-steps", false, "Show detailed resource replacement creates and deletes instead of a single step")
|
|
fs.Default().Bool("show-sames", false, "Show resources that needn't be updated because they haven't changed, alongside those that d")
|
|
fs.Default().StringArray("import-pending-creates", nil, "A list of form [[URN ID]...] describing the provider IDs of pending creates")
|
|
fs.Default().StringArray("target", nil, "Specify a single resource URN to update")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
{
|
|
Name: "state",
|
|
Description: "Edit the current stack's state",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
{
|
|
Name: "command",
|
|
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
|
|
return []goprompt.Suggest{
|
|
{Text: "delete", Description: "Deletes a resource from a stack's state"},
|
|
{Text: "rename", Description: "Renames a resource from a stack's state"},
|
|
{Text: "unprotect", Description: "Unprotect resources in a stack's state"},
|
|
{Text: "upgrade", Description: "Migrates the current backend to the latest supported version"},
|
|
}
|
|
},
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
{
|
|
Name: "import",
|
|
Description: "Import resources into an existing stack",
|
|
Args: tree.Args{
|
|
{
|
|
Name: "project",
|
|
Suggest: inst.completeProjects,
|
|
},
|
|
{
|
|
Name: "stack",
|
|
Suggest: inst.completeStacks,
|
|
},
|
|
{
|
|
Name: "type",
|
|
},
|
|
{
|
|
Name: "name",
|
|
},
|
|
{
|
|
Name: "id",
|
|
},
|
|
},
|
|
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
|
|
fs.Default().Bool("debug", false, "Print detailed debugging output during resource operations")
|
|
fs.Default().Bool("diff", false, "Display operation as a rich diff showing the overall change")
|
|
fs.Default().Bool("help", false, "Show command help")
|
|
fs.Default().String("file", "", "The path to a JSON-encoded file containing a list of resources to import")
|
|
fs.Default().String("from", "", "Invoke a converter to import the resources")
|
|
fs.Default().String("out", "", "The path to the file that will contain the generated resource declarations")
|
|
fs.Default().String("parent", "", "The name and URN of the parent resource in the format name=urn")
|
|
fs.Default().StringArray("properties", nil, "The property names to use for the import in the format name1,name")
|
|
return nil
|
|
},
|
|
Execute: inst.executeStack,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
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) Help(ctx context.Context, r *readline.Readline) string {
|
|
return c.commandTree.Help(ctx, r)
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ~ Private methods
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
func (c *Command) completeEnvs(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
|
|
//nolint:forcetypeassert
|
|
return c.cache.Get("envs", func() any {
|
|
entries, err := os.ReadDir(c.cfg.Path)
|
|
if err != nil {
|
|
c.l.Debug(err.Error())
|
|
return []goprompt.Suggest{}
|
|
}
|
|
var ret []string
|
|
for _, e := range entries {
|
|
if e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
|
|
ret = append(ret, e.Name())
|
|
}
|
|
}
|
|
return suggests.List(ret)
|
|
}).([]goprompt.Suggest)
|
|
}
|
|
|
|
func (c *Command) configureStack(ctx context.Context, env, proj, stack string, be Backend, storageAccountKey, passphrase string) error {
|
|
filename := path.Join(c.cfg.Path, env, proj, fmt.Sprintf("Pulumi.%s.op", stack))
|
|
|
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
out, err := shell.New(ctx, c.l, "cat", filename, "|", "op", "inject").Output()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to inject onepassword")
|
|
}
|
|
|
|
var args []string
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" && !strings.HasPrefix(line, "#") && strings.Contains(line, "=") {
|
|
args = append(args, "--secret", line)
|
|
}
|
|
}
|
|
|
|
if len(args) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return shell.New(ctx, c.l, "pulumi", "config", "set-all").
|
|
Args(args...).
|
|
Args("--stack", stack).
|
|
Dir(path.Join(c.cfg.Path, env, proj)).
|
|
Env("PULUMI_BACKEND_URL=" + fmt.Sprintf("azblob://%s", be.Container)).
|
|
Env("PULUMI_CONFIG_PASSPHRASE=" + passphrase).
|
|
Env("AZURE_STORAGE_ACCOUNT=" + be.StorageAccount).
|
|
Env("AZURE_STORAGE_KEY=" + storageAccountKey).
|
|
Env("ARM_SUBSCRIPTION_ID=" + be.Subscription).
|
|
Args().
|
|
Run()
|
|
}
|
|
|
|
func (c *Command) executeStack(ctx context.Context, r *readline.Readline) error {
|
|
e := r.Args().At(0)
|
|
proj := r.Args().At(2)
|
|
stack := r.Args().At(3)
|
|
|
|
be, err := c.cfg.Backend(r.Args().At(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
storageAccountKey, err := c.getStorageAccountKey(ctx, be)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
passphrase, err := c.op.Get(ctx, be.Passphrase)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.configureStack(ctx, e, proj, stack, be, storageAccountKey, passphrase); err != nil {
|
|
return err
|
|
}
|
|
|
|
return shell.New(ctx, c.l, "pulumi", r.Args().At(1)).
|
|
Args("--stack", stack).
|
|
Args(r.Args().From(4)...).
|
|
Args(r.Flags()...).
|
|
Args(r.AdditionalArgs()...).
|
|
Args(r.AdditionalFlags()...).
|
|
Env("PULUMI_BACKEND_URL=" + fmt.Sprintf("azblob://%s", be.Container)).
|
|
Env("PULUMI_CONFIG_PASSPHRASE=" + passphrase).
|
|
Env("AZURE_STORAGE_ACCOUNT=" + be.StorageAccount).
|
|
Env("AZURE_STORAGE_KEY=" + storageAccountKey).
|
|
Env("ARM_SUBSCRIPTION_ID=" + be.Subscription).
|
|
Dir(path.Join(c.cfg.Path, e, proj)).
|
|
Run()
|
|
}
|
|
|
|
func (c *Command) completeProjects(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
|
|
e := r.Args().At(0)
|
|
//nolint:forcetypeassert
|
|
return c.cache.Get("projects-"+e, func() any {
|
|
entries, err := os.ReadDir(path.Join(c.cfg.Path, e))
|
|
if err != nil {
|
|
c.l.Debug(err.Error())
|
|
return []goprompt.Suggest{}
|
|
}
|
|
var ret []string
|
|
for _, e := range entries {
|
|
if e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
|
|
ret = append(ret, e.Name())
|
|
}
|
|
}
|
|
return suggests.List(ret)
|
|
}).([]goprompt.Suggest)
|
|
}
|
|
|
|
func (c *Command) getStorageAccountKey(ctx context.Context, be Backend) (string, error) {
|
|
// retrieve storage key
|
|
c.l.Info("retrieving storage key")
|
|
sk, err := shell.New(ctx, c.l, "az", "storage", "account", "keys", "list").
|
|
Args("--resource-group", be.ResourceGroup).
|
|
Args("--subscription", be.Subscription).
|
|
Args("--account-name", be.StorageAccount).
|
|
Args("-o", "tsv", "--query", "'[0].value'").
|
|
Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.ReplaceAll(strings.TrimSpace(string(sk)), "\n", ""), nil
|
|
}
|
|
|
|
func (c *Command) completeStacks(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
|
|
e := r.Args().At(0)
|
|
project := r.Args().At(2)
|
|
//nolint:forcetypeassert
|
|
return c.cache.Get("stacks-"+e+"-"+project, func() any {
|
|
entries, err := os.ReadDir(path.Join(c.cfg.Path, e, project))
|
|
if err != nil {
|
|
c.l.Debug(err.Error())
|
|
return []goprompt.Suggest{}
|
|
}
|
|
var ret []string
|
|
for _, e := range entries {
|
|
if !e.IsDir() && len(e.Name()) > 11 && strings.HasPrefix(e.Name(), "Pulumi.") && strings.HasSuffix(e.Name(), ".yaml") {
|
|
ret = append(ret, strings.TrimSuffix(strings.TrimPrefix(e.Name(), "Pulumi."), ".yaml"))
|
|
}
|
|
}
|
|
return suggests.List(ret)
|
|
}).([]goprompt.Suggest)
|
|
}
|