diff --git a/command/commands.go b/command/commands.go index 6b9f5c89e7..f6aad476d1 100644 --- a/command/commands.go +++ b/command/commands.go @@ -347,6 +347,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { BaseCommand: getBaseCommand(), }, nil }, + "namespace patch": func() (cli.Command, error) { + return &NamespacePatchCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "namespace delete": func() (cli.Command, error) { return &NamespaceDeleteCommand{ BaseCommand: getBaseCommand(), diff --git a/command/namespace.go b/command/namespace.go index 89cf2e0296..702395753d 100644 --- a/command/namespace.go +++ b/command/namespace.go @@ -36,6 +36,10 @@ Usage: vault namespace [options] [args] $ vault namespace create + Patch an existing namespace: + + $ vault namespace patch + Delete an existing namespace: $ vault namespace delete diff --git a/command/namespace_create.go b/command/namespace_create.go index 80ce589f96..7d1f52fa8c 100644 --- a/command/namespace_create.go +++ b/command/namespace_create.go @@ -15,6 +15,8 @@ var ( type NamespaceCreateCommand struct { *BaseCommand + + flagCustomMetadata map[string]string } func (c *NamespaceCreateCommand) Synopsis() string { @@ -43,7 +45,18 @@ Usage: vault namespace create [options] PATH } func (c *NamespaceCreateCommand) Flags() *FlagSets { - return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + f.StringMapVar(&StringMapVar{ + Name: "custom-metadata", + Target: &c.flagCustomMetadata, + Default: map[string]string{}, + Usage: "Specifies arbitrary key=value metadata meant to describe a namespace." + + "This can be specified multiple times to add multiple pieces of metadata.", + }) + + return set } func (c *NamespaceCreateCommand) AutocompleteArgs() complete.Predictor { @@ -80,7 +93,11 @@ func (c *NamespaceCreateCommand) Run(args []string) int { return 2 } - secret, err := client.Logical().Write("sys/namespaces/"+namespacePath, nil) + data := map[string]interface{}{ + "custom_metadata": c.flagCustomMetadata, + } + + secret, err := client.Logical().Write("sys/namespaces/"+namespacePath, data) if err != nil { c.UI.Error(fmt.Sprintf("Error creating namespace: %s", err)) return 2 diff --git a/command/namespace_patch.go b/command/namespace_patch.go new file mode 100644 index 0000000000..6f5b8390f1 --- /dev/null +++ b/command/namespace_patch.go @@ -0,0 +1,137 @@ +package command + +import ( + "context" + "fmt" + "strings" + + "github.com/posener/complete" + + "github.com/mitchellh/cli" +) + +var ( + _ cli.Command = (*NamespacePatchCommand)(nil) + _ cli.CommandAutocomplete = (*NamespacePatchCommand)(nil) +) + +type NamespacePatchCommand struct { + *BaseCommand + + flagCustomMetadata map[string]string + flagRemoveCustomMetadata []string +} + +func (c *NamespacePatchCommand) Synopsis() string { + return "Patch an existing namespace" +} + +func (c *NamespacePatchCommand) Help() string { + helpText := ` +Usage: vault namespace patch [options] PATH + + Patch an existing namespace. The namespace patched will be relative to the + namespace provided in either the VAULT_NAMESPACE environment variable or + -namespace CLI flag. + + Patch an existing child namespace by adding and removing custom-metadata (e.g. ns1/): + + $ vault namespace patch ns1 -custom-metadata=foo=abc -remove-custom-metadata=bar + + Patch an existing child namespace from a parent namespace (e.g. ns1/ns2/): + + $ vault namespace patch -namespace=ns1 ns2 -custom-metadata=foo=abc + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *NamespacePatchCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + f.StringMapVar(&StringMapVar{ + Name: "custom-metadata", + Target: &c.flagCustomMetadata, + Default: map[string]string{}, + Usage: "Specifies arbitrary key=value metadata meant to describe a namespace." + + "This can be specified multiple times to add multiple pieces of metadata.", + }) + + f.StringSliceVar(&StringSliceVar{ + Name: "remove-custom-metadata", + Target: &c.flagRemoveCustomMetadata, + Default: []string{}, + Usage: "Key to remove from custom metadata. To specify multiple values, specify this flag multiple times.", + }) + + return set +} + +func (c *NamespacePatchCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *NamespacePatchCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *NamespacePatchCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) + return 1 + case len(args) > 1: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + namespacePath := strings.TrimSpace(args[0]) + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + data := make(map[string]interface{}) + customMetadata := make(map[string]interface{}) + + for key, value := range c.flagCustomMetadata { + customMetadata[key] = value + } + + for _, key := range c.flagRemoveCustomMetadata { + // A null in a JSON merge patch payload will remove the associated key + customMetadata[key] = nil + } + + data["custom_metadata"] = customMetadata + + secret, err := client.Logical().JSONMergePatch(context.Background(), "sys/namespaces/"+namespacePath, data) + if err != nil { + c.UI.Error(fmt.Sprintf("Error patching namespace: %s", err)) + return 2 + } + + if secret == nil || secret.Data == nil { + c.UI.Error(fmt.Sprintf("No namespace found: %s", err)) + return 2 + } + + // Handle single field output + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + return OutputSecret(c.UI, secret) +} diff --git a/helper/namespace/namespace.go b/helper/namespace/namespace.go index b196047121..93d68622de 100644 --- a/helper/namespace/namespace.go +++ b/helper/namespace/namespace.go @@ -12,9 +12,9 @@ import ( type contextValues struct{} type Namespace struct { - ID string `json:"id"` - Path string `json:"path"` - CustomMetadata map[string]string `json:"custom_metadata"` + ID string `json:"id" mapstructure:"id"` + Path string `json:"path" mapstructure:"path"` + CustomMetadata map[string]string `json:"custom_metadata" mapstructure:"custom_metadata"` } func (n *Namespace) String() string { diff --git a/sdk/helper/custommetadata/custom_metadata.go b/sdk/helper/custommetadata/custom_metadata.go index af8d4c163a..7d4ff8763d 100644 --- a/sdk/helper/custommetadata/custom_metadata.go +++ b/sdk/helper/custommetadata/custom_metadata.go @@ -3,6 +3,8 @@ package custommetadata import ( "fmt" + "github.com/mitchellh/mapstructure" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/strutil" ) @@ -16,6 +18,31 @@ const ( validationErrorPrefix = "custom_metadata validation failed" ) +// Parse is used to effectively convert the TypeMap +// (map[string]interface{}) into a TypeKVPairs (map[string]string) +// which is how custom_metadata is stored. Defining custom_metadata +// as a TypeKVPairs will convert nulls into empty strings. A null, +// however, is essential for a PATCH operation in that it signals +// the handler to remove the field. The filterNils flag should +// only be used during a patch operation. +func Parse(raw map[string]interface{}, filterNils bool) (map[string]string, error) { + customMetadata := map[string]string{} + for k, v := range raw { + if filterNils && v == nil { + continue + } + + var s string + if err := mapstructure.WeakDecode(v, &s); err != nil { + return nil, err + } + + customMetadata[k] = s + } + + return customMetadata, nil +} + // Validate will perform input validation for custom metadata. // CustomMetadata should be arbitrary user-provided key-value pairs meant to // provide supplemental information about a resource. If the key count