VAULT-7256: Add custom_metadata to namespaces (#16640)

* add mapstructure tags to Namespace struct

* add custom metadata Parse helper

* add ns custom metadata and patch
This commit is contained in:
Chris Capurso
2022-08-09 11:38:03 -04:00
committed by GitHub
parent c203b64a22
commit b9d4c0f48f
6 changed files with 195 additions and 5 deletions

View File

@@ -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(),

View File

@@ -36,6 +36,10 @@ Usage: vault namespace <subcommand> [options] [args]
$ vault namespace create
Patch an existing namespace:
$ vault namespace patch
Delete an existing namespace:
$ vault namespace delete

View File

@@ -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

137
command/namespace_patch.go Normal file
View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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