diff --git a/azure/az/README.md b/azure/az/README.md index 09f4fe0..7331841 100644 --- a/azure/az/README.md +++ b/azure/az/README.md @@ -61,6 +61,7 @@ az: prod: name: aks-my-prod ``` + ### Ownbrew To install binary locally, add: diff --git a/azure/az/az.go b/azure/az/az.go index 22c2c07..590ebe3 100644 --- a/azure/az/az.go +++ b/azure/az/az.go @@ -64,3 +64,11 @@ func New(l log.Logger, cache cache.Cache, opts ...Option) (*AZ, error) { return inst, nil } + +// ------------------------------------------------------------------------------------------------ +// ~ Getter +// ------------------------------------------------------------------------------------------------ + +func (a *AZ) Config() Config { + return a.cfg +} diff --git a/go.mod b/go.mod index e1fd503..6d26e6b 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/slack-go/slack v0.12.3 github.com/spf13/viper v1.18.2 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - golang.org/x/oauth2 v0.15.0 + golang.org/x/oauth2 v0.16.0 golang.org/x/sync v0.6.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -64,10 +64,10 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index accb1cc..cccf092 100644 --- a/go.sum +++ b/go.sum @@ -195,8 +195,8 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -206,10 +206,10 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -234,15 +234,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/onepassword/README.md b/onepassword/README.md index fe4b0fe..4f8e0f7 100644 --- a/onepassword/README.md +++ b/onepassword/README.md @@ -24,32 +24,33 @@ Available commands: package plugin type Plugin struct { - l log.Logger - c cache.Cache - commands command.Commands + l log.Logger + oo *onepassword.OnePassword + cacche cache.Cache + commands command.Commands } func New(l log.Logger) (plugin.Plugin, error) { - inst := &Plugin{ - l: l, - c: cache.MemoryCache{}, - commands: command.Commands{}, - } - - // ... - - // 1Password - if onePassword, err := onepassword.New(l, inst.c)); err != nil { - return nil, err - } else if cmd, err := onepassword.NewCommand(l, onePassword); err != nil { - return nil, err - } else { - inst.commands.Add(cmd) - } - - // ... - - return inst, nil + inst := &Plugin{ + l: l, + cache: cache.MemoryCache{}, + commands: command.Commands{}, + } + + // ... + + inst.op, err := onepassword.New(l, inst.cache)); + if err != nil { + return nil, errors.Wrap(err, "failed to create onepassword") + } + + // ... + + inst.commands.MustAdd(onepassword.NewCommand(l, onePassword)) + + // ... + + return inst, nil } ``` diff --git a/pulumi/pulumi/azure/README.md b/pulumi/pulumi/azure/README.md new file mode 100644 index 0000000..eb2a2b1 --- /dev/null +++ b/pulumi/pulumi/azure/README.md @@ -0,0 +1,69 @@ +# POSH pulumi (azure) provider + +## Usage + +```go +package plugin + +type Plugin struct { + l log.Logger + az *az.AZ + cache cache.Cache + kubectl *kubectl.Kubectl + commands command.Commands +} + +func New(l log.Logger) (plugin.Plugin, error) { + inst := &Plugin{ + l: l, + cache: cache.MemoryCache{}, + commands: command.Commands{}, + } + + // ... + + inst.op, err := onepassword.New(l, inst.cache)); + if err != nil { + return nil, errors.Wrap(err, "failed to create onepassword") + } + + inst.kubectl, err = kubectl.New(l, inst.cache) + if err != nil { + return nil, errors.Wrap(err, "failed to create kubectl") + } + + inst.az, err = az.New(l, inst.cache) + if err != nil { + return nil, errors.Wrap(err, "failed to create az") + } + + // ... + + inst.commands.MustAdd(pulumi.NewCommand(l, inst.op, inst.az, inst.cache)) + + // ... + + return inst, nil +} +``` + +### Config + +```yaml +## az +pulumi: + path: .posh/pulumi + configPath: .posh/config/pulumi + backends: + prod: + location: Germany West Central + container: pulumi-state + subscription: xxx + resourceGroup: rg-my-name + storageAccount: sa-my-name + passphrase: + account: xxxx + vault: xxxx + itemId: xxxx + field: password +``` diff --git a/pulumi/pulumi/azure/backend.go b/pulumi/pulumi/azure/backend.go new file mode 100644 index 0000000..a5afb97 --- /dev/null +++ b/pulumi/pulumi/azure/backend.go @@ -0,0 +1,14 @@ +package pulumi + +import ( + "github.com/foomo/posh-providers/onepassword" +) + +type Backend struct { + Location string `json:"location" yaml:"location"` + Container string `json:"container" yaml:"container"` + Subscription string `json:"subscription" yaml:"subscription"` + ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"` + StorageAccount string `json:"storageAccount" yaml:"storageAccount"` + Passphrase onepassword.Secret `json:"passphrase" yaml:"passphrase"` +} diff --git a/pulumi/pulumi/azure/command.go b/pulumi/pulumi/azure/command.go new file mode 100644 index 0000000..4a2d6c4 --- /dev/null +++ b/pulumi/pulumi/azure/command.go @@ -0,0 +1,553 @@ +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("debug", "", "Show full logs") + fs.Default().String("tags", "", "Quoted string with space-separated tags") + fs.Default().String("vebose", "", "Increase logging verbosity") + return nil + }, + Execute: func(ctx context.Context, r *readline.Readline) error { + be, err := inst.cfg.Backend(r.Args().At(0)) + if err != nil { + return err + } + + // Create a new resource group + inst.l.Info("creating resource group:", be.ResourceGroup) + if err := shell.New(ctx, inst.l, "az", "group", "create"). + Args("--resource-group", be.ResourceGroup). + Args("--subscription", be.Subscription). + Args("--location", be.Location). + Args(r.Flags()...). + Run(); err != nil { + return err + } + + // Create a new resource group + inst.l.Info("creating storage account:", be.StorageAccount) + if err := shell.New(ctx, inst.l, "az", "storage", "account", "create"). + Args("--name", be.StorageAccount). + Args("--resource-group", be.ResourceGroup). + Args("--subscription", be.Subscription). + Args("--location", be.Location). + Args("--sku", "Standard_LRS"). + Args(r.Flags()...). + Run(); err != nil { + return err + } + + // retrieve storage key + inst.l.Info("retrieving storage key") + sk, err := shell.New(ctx, inst.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 + } + + inst.l.Info("creating storage container:", be.Container) + return shell.New(ctx, inst.l, "az", "storage", "container", "create"). + Args("--account-name", be.StorageAccount). + Args("--account-key", string(sk)). + Args("--name", be.Container). + Run() + }, + }, + { + Name: "login", + Description: "Log into your object storage backend", + Execute: func(ctx context.Context, r *readline.Readline) error { + be, err := inst.cfg.Backend(r.Args().At(0)) + if err != nil { + return err + } + + // retrieve storage key + inst.l.Info("retrieving storage key") + sk, err := shell.New(ctx, inst.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 shell.New(ctx, inst.l, "pulumi", "login", fmt.Sprintf("azblob://%s", be.Container)). + Env("AZURE_STORAGE_ACCOUNT=" + be.StorageAccount). + Env("AZURE_STORAGE_KEY=" + string(sk)). + Run() + }, + }, + }, + }, + { + Name: "stack", + Description: "Opens the current stack in the Pulumi Console", + 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: "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) 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) + } + } + + return shell.New(ctx, c.l, "pulumi", "config", "set-all"). + Args(args...). + Args("--stack", stack). + Dir(path.Join(c.cfg.Path, env, proj)). + 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(e) + if err != nil { + return err + } + + if err := c.configureStack(ctx, e, proj, stack); err != nil { + return err + } + + passphrase, err := c.op.Get(ctx, be.Passphrase) + if 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_CONFIG_PASSPHRASE=" + passphrase). + Env("PULUMI_BACKEND_URL=" + fmt.Sprintf("azblob://%s", be.Container)). + 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) 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) +} diff --git a/pulumi/pulumi/azure/config.go b/pulumi/pulumi/azure/config.go new file mode 100644 index 0000000..d4f915c --- /dev/null +++ b/pulumi/pulumi/azure/config.go @@ -0,0 +1,24 @@ +package pulumi + +import ( + "github.com/pkg/errors" + "github.com/samber/lo" +) + +type Config struct { + Path string `json:"path" yaml:"path"` + ConfigPath string `json:"configPath" yaml:"configPath"` + Backends map[string]Backend `json:"backends" yaml:"backends"` +} + +func (p Config) Backend(name string) (Backend, error) { + value, ok := p.Backends[name] + if !ok { + return Backend{}, errors.Errorf("backend not found: %s", name) + } + return value, nil +} + +func (p Config) Azure() []string { + return lo.Keys(p.Backends) +} diff --git a/pulumi/pulumi/azure/storageaccount.go b/pulumi/pulumi/azure/storageaccount.go new file mode 100644 index 0000000..6bc2083 --- /dev/null +++ b/pulumi/pulumi/azure/storageaccount.go @@ -0,0 +1,5 @@ +package pulumi + +type StorageAccount struct { + Name string `json:"name" yaml:"name"` +}