Skip to content

Commit

Permalink
VAULT-7256: Add custom_metadata to namespaces (#16640)
Browse files Browse the repository at this point in the history
* add mapstructure tags to Namespace struct

* add custom metadata Parse helper

* add ns custom metadata and patch
  • Loading branch information
ccapurso committed Aug 9, 2022
1 parent c203b64 commit b9d4c0f
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 5 deletions.
5 changes: 5 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 4 additions & 0 deletions command/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions command/namespace_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var (

type NamespaceCreateCommand struct {
*BaseCommand

flagCustomMetadata map[string]string
}

func (c *NamespaceCreateCommand) Synopsis() string {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
137 changes: 137 additions & 0 deletions command/namespace_patch.go
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 3 additions & 3 deletions helper/namespace/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions sdk/helper/custommetadata/custom_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package custommetadata
import (
"fmt"

"github.com/mitchellh/mapstructure"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/strutil"
)
Expand All @@ -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
Expand Down

0 comments on commit b9d4c0f

Please sign in to comment.