diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index af68cec9d..151f76423 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -26,6 +26,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/commands/scopescmd" "github.com/hashicorp/boundary/internal/cmd/commands/server" "github.com/hashicorp/boundary/internal/cmd/commands/sessionscmd" + "github.com/hashicorp/boundary/internal/cmd/commands/storagebucketscmd" "github.com/hashicorp/boundary/internal/cmd/commands/targetscmd" "github.com/hashicorp/boundary/internal/cmd/commands/userscmd" "github.com/hashicorp/boundary/internal/cmd/commands/version" @@ -1023,6 +1024,42 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { }, nil }, + "storage-buckets": func() (cli.Command, error) { + return &storagebucketscmd.Command{ + Command: base.NewCommand(ui), + }, nil + }, + "storage-buckets read": func() (cli.Command, error) { + return &storagebucketscmd.Command{ + Command: base.NewCommand(ui), + Func: "read", + }, nil + }, + "storage-buckets delete": func() (cli.Command, error) { + return &storagebucketscmd.Command{ + Command: base.NewCommand(ui), + Func: "delete", + }, nil + }, + "storage-buckets list": func() (cli.Command, error) { + return &storagebucketscmd.Command{ + Command: base.NewCommand(ui), + Func: "list", + }, nil + }, + "storage-buckets create": func() (cli.Command, error) { + return &storagebucketscmd.Command{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, + "storage-buckets update": func() (cli.Command, error) { + return &storagebucketscmd.Command{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, + "targets": func() (cli.Command, error) { return &targetscmd.Command{ Command: base.NewCommand(ui), diff --git a/internal/cmd/commands/storagebucketscmd/funcs.go b/internal/cmd/commands/storagebucketscmd/funcs.go new file mode 100644 index 000000000..8ddbfd997 --- /dev/null +++ b/internal/cmd/commands/storagebucketscmd/funcs.go @@ -0,0 +1,332 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package storagebucketscmd + +import ( + "fmt" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/storagebuckets" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-bexpr" +) + +func init() { + extraFlagsFunc = extraPluginFlagsFuncImpl + extraActionsFlagsMapFunc = extraPluginActionsFlagsMapFuncImpl + extraFlagsHandlingFunc = extraPluginFlagHandlingFuncImpl +} + +const ( + bucketNameFlagName = "bucket-name" + bucketPrefixFlagName = "bucket-prefix" + workerFilterFlagName = "worker-filter" +) + +type extraCmdVars struct { + flagBucketName string + flagBucketPrefix string + flagWorkerFilter string +} + +func extraPluginActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + bucketNameFlagName, + bucketPrefixFlagName, + workerFilterFlagName, + }, + "update": { + workerFilterFlagName, + }, + } + return flags +} + +func extraPluginFlagsFuncImpl(c *Command, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Storage Bucket Options") + + for _, name := range flagsMap[c.Func] { + switch name { + case bucketNameFlagName: + f.StringVar(&base.StringVar{ + Name: bucketNameFlagName, + Target: &c.flagBucketName, + Usage: "The bucket name within the external object store.", + }) + case bucketPrefixFlagName: + f.StringVar(&base.StringVar{ + Name: bucketPrefixFlagName, + Target: &c.flagBucketPrefix, + Usage: "The optional bucket prefix to use.", + }) + + case workerFilterFlagName: + f.StringVar(&base.StringVar{ + Name: workerFilterFlagName, + Target: &c.flagWorkerFilter, + Usage: `A boolean expression to filter which workers can handle communication to this storage bucket.`, + }) + } + } +} + +func extraPluginFlagHandlingFuncImpl(c *Command, f *base.FlagSets, opts *[]storagebuckets.Option) bool { + switch c.flagBucketName { + case "": + default: + *opts = append(*opts, storagebuckets.WithBucketName(c.flagBucketName)) + } + switch c.flagBucketPrefix { + case "": + default: + *opts = append(*opts, storagebuckets.WithBucketPrefix(c.flagBucketPrefix)) + } + + switch c.flagWorkerFilter { + case "": + case "null": + *opts = append(*opts, storagebuckets.DefaultWorkerFilter()) + default: + if _, err := bexpr.CreateEvaluator(c.flagWorkerFilter); err != nil { + c.UI.Error(fmt.Sprintf("Unable to successfully parse filter expression: %s", err)) + return false + } + *opts = append(*opts, storagebuckets.WithWorkerFilter(c.flagWorkerFilter)) + } + + return true +} + +func (c *Command) extraPluginHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary storage-buckets create [options] [args]", + "", + " Create a storage bucket. Example:", + "", + ` $ boundary storage-buckets create -scope-id global -bucket-name test -name prodops -description "Storage bucket for ProdOps"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary storage-buckets update [options] [args]", + "", + " Update a storage bucket given its ID. Example:", + "", + ` $ boundary storage-buckets update -id sb_1234567890 -name "devops" -description "Storage bucket for DevOps"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "": + return base.WrapForHelpText([]string{ + "Usage: boundary storage-buckets [sub command] [options] [args]", + "", + " This command allows operations on Boundary storage bucket resources. Example:", + "", + " Read a storage bucket:", + "", + ` $ boundary storage-buckets read -id sb_1234567890`, + "", + " Please see the storage-buckets subcommand help for detailed usage information.", + }) + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary storage-buckets create [sub command] [options] [args]", + "", + " This command allows create operations on Boundary storage bucket resources. Example:", + "", + " Create a storage bucket:", + "", + ` $ boundary storage-buckets create plugin -bucket-name prod_bucket`, + "", + " Please see the subcommand help for detailed usage information.", + }) + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary storage-buckets update [sub command] [options] [args]", + "", + " This command allows update operations on Boundary storage bucket resources. Example:", + "", + " Update a storage bucket:", + "", + ` $ boundary storage-buckets update plugin -id sb_1234567890 -name devops -description "For DevOps usage"`, + "", + " Please see the subcommand help for detailed usage information.", + }) + default: + helpStr = helpMap["base"]() + } + return helpStr + c.Flags().Help() +} + +func (c *Command) printListTable(items []*storagebuckets.StorageBucket) string { + if len(items) == 0 { + return "No storage buckets found" + } + + var output []string + output = []string{ + "", + "Storage Bucket information:", + } + for i, m := range items { + if i > 0 { + output = append(output, "") + } + if m.Id != "" { + output = append(output, + fmt.Sprintf(" ID: %s", m.Id), + ) + } else { + output = append(output, + fmt.Sprintf(" ID: %s", "(not available)"), + ) + } + if m.BucketName != "" { + output = append(output, + fmt.Sprintf(" Bucket Name: %s", m.BucketName), + ) + } + if m.BucketPrefix != "" { + output = append(output, + fmt.Sprintf(" Bucket Prefix: %s", m.BucketPrefix), + ) + } + if m.WorkerFilter != "" { + output = append(output, + fmt.Sprintf(" Worker Filter: %s", m.WorkerFilter), + ) + } + if c.FlagRecursive && m.ScopeId != "" { + output = append(output, + fmt.Sprintf(" Scope ID: %s", m.ScopeId), + ) + } + if m.PluginId != "" { + output = append(output, + fmt.Sprintf(" Plugin ID: %s", m.PluginId), + ) + } + if m.Version > 0 { + output = append(output, + fmt.Sprintf(" Version: %d", m.Version), + ) + } + if m.Name != "" { + output = append(output, + fmt.Sprintf(" Name: %s", m.Name), + ) + } + if m.Description != "" { + output = append(output, + fmt.Sprintf(" Description: %s", m.Description), + ) + } + if len(m.AuthorizedActions) > 0 { + output = append(output, + " Authorized Actions:", + base.WrapSlice(6, m.AuthorizedActions), + ) + } + } + + return base.WrapForHelpText(output) +} + +func printItemTable(item *storagebuckets.StorageBucket, resp *api.Response) string { + nonAttributeMap := map[string]any{} + if item.Id != "" { + nonAttributeMap["ID"] = item.Id + } + if item.BucketName != "" { + nonAttributeMap["Bucket Name"] = item.BucketName + } + if item.BucketPrefix != "" { + nonAttributeMap["Bucket Prefix"] = item.BucketPrefix + } + if item.WorkerFilter != "" { + nonAttributeMap["Worker Filter"] = item.WorkerFilter + } + if item.Version != 0 { + nonAttributeMap["Version"] = item.Version + } + if !item.CreatedTime.IsZero() { + nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) + } + if !item.UpdatedTime.IsZero() { + nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) + } + if item.Name != "" { + nonAttributeMap["Name"] = item.Name + } + if item.Description != "" { + nonAttributeMap["Description"] = item.Description + } + if item.PluginId != "" { + nonAttributeMap["Plugin ID"] = item.PluginId + } + if item.SecretsHmac != "" { + nonAttributeMap["Secrets HMAC"] = item.SecretsHmac + } + + maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) + + ret := []string{ + "", + "Storage Bucket information:", + base.WrapMap(2, maxLength+2, nonAttributeMap), + } + + if item.Scope != nil { + ret = append(ret, + "", + " Scope:", + base.ScopeInfoForOutput(item.Scope, maxLength), + ) + } + + if item.Plugin != nil { + ret = append(ret, + "", + " Plugin:", + base.PluginInfoForOutput(item.Plugin, maxLength), + ) + } + + if len(item.Attributes) > 0 { + ret = append(ret, + "", + " Attributes:", + base.WrapMap(4, maxLength, item.Attributes), + ) + } + + if len(item.AuthorizedActions) > 0 { + ret = append(ret, + "", + " Authorized Actions:", + base.WrapSlice(4, item.AuthorizedActions), + ) + } + + return base.WrapForHelpText(ret) +} + +var keySubstMap = map[string]string{ + "region": "Region", + "endpoint": "Endpoint", +} diff --git a/internal/cmd/commands/storagebucketscmd/storagebuckets.gen.go b/internal/cmd/commands/storagebucketscmd/storagebuckets.gen.go new file mode 100644 index 000000000..0cb14dcbc --- /dev/null +++ b/internal/cmd/commands/storagebucketscmd/storagebuckets.gen.go @@ -0,0 +1,423 @@ +// Code generated by "make cli"; DO NOT EDIT. +package storagebucketscmd + +import ( + "errors" + "fmt" + "sync" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/storagebuckets" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initFlags() { + flagsOnce.Do(func() { + extraFlags := extraActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsMap[k] = append(flagsMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*Command)(nil) + _ cli.CommandAutocomplete = (*Command)(nil) +) + +type Command struct { + *base.Command + + Func string + + plural string + + extraCmdVars +} + +func (c *Command) AutocompleteArgs() complete.Predictor { + initFlags() + return complete.PredictAnything +} + +func (c *Command) AutocompleteFlags() complete.Flags { + initFlags() + return c.Flags().Completions() +} + +func (c *Command) Synopsis() string { + if extra := extraSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "storage bucket" + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *Command) Help() string { + initFlags() + + var helpStr string + helpMap := common.HelpMap("storage bucket") + + switch c.Func { + + default: + + helpStr = c.extraHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsMap = map[string][]string{ + + "create": {"scope-id", "name", "description", "plugin-id", "plugin-name", "attributes", "attr", "string-attr", "bool-attr", "num-attr", "secrets", "secret", "string-secret", "bool-secret", "num-secret"}, + + "update": {"id", "name", "description", "version", "attributes", "attr", "string-attr", "bool-attr", "num-attr", "secrets", "secret", "string-secret", "bool-secret", "num-secret"}, + + "read": {"id"}, + + "delete": {"id"}, + + "list": {"scope-id", "filter", "recursive"}, +} + +func (c *Command) Flags() *base.FlagSets { + if len(flagsMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "storage bucket", flagsMap, c.Func) + + f = set.NewFlagSet("Attribute Options") + attrsInput := common.CombinedSliceFlagValuePopulationInput{ + FlagSet: f, + FlagNames: flagsMap[c.Func], + FullPopulationFlag: &c.FlagAttributes, + FullPopulationInputName: "attributes", + PiecewisePopulationFlag: &c.FlagAttrs, + PiecewisePopulationInputBaseName: "attr", + } + common.PopulateCombinedSliceFlagValue(attrsInput) + + f = set.NewFlagSet("Secrets Options") + scrtsInput := common.CombinedSliceFlagValuePopulationInput{ + FlagSet: f, + FlagNames: flagsMap[c.Func], + FullPopulationFlag: &c.FlagSecrets, + FullPopulationInputName: "secrets", + PiecewisePopulationFlag: &c.FlagScrts, + PiecewisePopulationInputBaseName: "secret", + } + common.PopulateCombinedSliceFlagValue(scrtsInput) + + extraFlagsFunc(c, set, f) + + return set +} + +func (c *Command) Run(args []string) int { + initFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "storage bucket" + switch c.Func { + case "list": + c.plural = "storage buckets" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + if c.FlagAttributes != "" && len(c.FlagAttrs) > 0 { + c.PrintCliError(errors.New("-attributes flag cannot be used along with the following flags: attr, bool-attr, num-attr, string-attr")) + return base.CommandUserError + } + + if c.FlagSecrets != "" && len(c.FlagScrts) > 0 { + c.PrintCliError(errors.New("-secrets flag cannot be used along with the following flags: secret, bool-secret, num-secret, string-secret")) + return base.CommandUserError + } + + var opts []storagebuckets.Option + + if strutil.StrListContains(flagsMap[c.Func], "scope-id") { + switch c.Func { + + case "create": + if c.FlagScopeId == "" { + c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_SCOPE_ID")) + return base.CommandUserError + } + + case "list": + if c.FlagScopeId == "" { + c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_SCOPE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + storagebucketsClient := storagebuckets.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, storagebuckets.DefaultName()) + default: + opts = append(opts, storagebuckets.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, storagebuckets.DefaultDescription()) + default: + opts = append(opts, storagebuckets.WithDescription(c.FlagDescription)) + } + + switch c.FlagRecursive { + case true: + opts = append(opts, storagebuckets.WithRecursive(true)) + } + + if c.FlagFilter != "" { + opts = append(opts, storagebuckets.WithFilter(c.FlagFilter)) + } + + switch c.FlagPluginId { + case "": + default: + opts = append(opts, storagebuckets.WithPluginId(c.FlagPluginId)) + } + switch c.FlagPluginName { + case "": + default: + opts = append(opts, storagebuckets.WithPluginName(c.FlagPluginName)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, storagebuckets.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if err := common.HandleAttributeFlags( + c.Command, + "attr", + c.FlagAttributes, + c.FlagAttrs, + func() { + opts = append(opts, storagebuckets.DefaultAttributes()) + }, + func(in map[string]interface{}) { + opts = append(opts, storagebuckets.WithAttributes(in)) + }); err != nil { + c.PrintCliError(fmt.Errorf("Error evaluating attribute flags to: %s", err.Error())) + return base.CommandCliError + } + + if err := common.HandleAttributeFlags( + c.Command, + "secret", + c.FlagSecrets, + c.FlagScrts, + func() { + opts = append(opts, storagebuckets.DefaultSecrets()) + }, + func(in map[string]interface{}) { + opts = append(opts, storagebuckets.WithSecrets(in)) + }); err != nil { + c.PrintCliError(fmt.Errorf("Error evaluating secret flags to: %s", err.Error())) + return base.CommandCliError + } + + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *storagebuckets.StorageBucket + + var items []*storagebuckets.StorageBucket + + var createResult *storagebuckets.StorageBucketCreateResult + + var updateResult *storagebuckets.StorageBucketUpdateResult + + var readResult *storagebuckets.StorageBucketReadResult + + var deleteResult *storagebuckets.StorageBucketDeleteResult + + var listResult *storagebuckets.StorageBucketListResult + + switch c.Func { + + case "create": + createResult, err = storagebucketsClient.Create(c.Context, c.FlagScopeId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = storagebucketsClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + case "read": + readResult, err = storagebucketsClient.Read(c.Context, c.FlagId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = readResult.GetResponse() + item = readResult.GetItem() + + case "delete": + deleteResult, err = storagebucketsClient.Delete(c.Context, c.FlagId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = deleteResult.GetResponse() + + case "list": + listResult, err = storagebucketsClient.List(c.Context, c.FlagScopeId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = listResult.GetResponse() + items = listResult.GetItems() + + } + + resp, item, items, err = executeExtraActions(c, resp, item, items, err, storagebucketsClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + case "delete": + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + + case "table": + c.UI.Output("The delete operation completed successfully.") + } + + return base.CommandSuccess + + case "list": + switch base.Format(c.UI) { + case "json": + if ok := c.PrintJsonItems(resp); !ok { + return base.CommandCliError + } + + case "table": + c.UI.Output(c.printListTable(items)) + } + + return base.CommandSuccess + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *Command) checkFuncError(err error) int { + if err == nil { + return 0 + } + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError +} + +var ( + flagsOnce = new(sync.Once) + + extraActionsFlagsMapFunc = func() map[string][]string { return nil } + extraSynopsisFunc = func(*Command) string { return "" } + extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]storagebuckets.Option) bool { return true } + executeExtraActions = func(_ *Command, inResp *api.Response, inItem *storagebuckets.StorageBucket, inItems []*storagebuckets.StorageBucket, inErr error, _ *storagebuckets.Client, _ uint32, _ []storagebuckets.Option) (*api.Response, *storagebuckets.StorageBucket, []*storagebuckets.StorageBucket, error) { + return inResp, inItem, inItems, inErr + } + printCustomActionOutput = func(*Command) (bool, error) { return false, nil } +) diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 8b34aaa64..df19ae77e 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -566,6 +566,25 @@ var inputStructs = map[string][]*cmdInfo{ VersionedActions: []string{"cancel"}, }, }, + "storagebuckets": { + { + ResourceType: resource.StorageBucket.String(), + Pkg: "storagebuckets", + StdActions: []string{"create", "update", "read", "delete", "list"}, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasExtraCommandVars: true, + HasId: true, + HasName: true, + HasDescription: true, + Container: "Scope", + IsPluginType: true, + VersionedActions: []string{"update"}, + NeedsSubtypeInCreate: true, + HasGenericAttributes: true, + HasGenericSecrets: true, + }, + }, "targets": { { ResourceType: resource.Target.String(),