Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kv rollback #4774

Merged
merged 3 commits into from
Jun 15, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
BaseCommand: getBaseCommand(),
}, nil
},
"kv rollback": func() (cli.Command, error) {
return &KVRollbackCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"kv get": func() (cli.Command, error) {
return &KVGetCommand{
BaseCommand: getBaseCommand(),
Expand Down
2 changes: 1 addition & 1 deletion command/kv_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type KVPatchCommand struct {
}

func (c *KVPatchCommand) Synopsis() string {
return "Sets or updates data in the KV store without overwriting."
return "Sets or updates data in the KV store without overwriting"
}

func (c *KVPatchCommand) Help() string {
Expand Down
258 changes: 258 additions & 0 deletions command/kv_rollback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package command

import (
"flag"
"fmt"
"strings"

"github.com/mitchellh/cli"
"github.com/posener/complete"
)

var _ cli.Command = (*KVRollbackCommand)(nil)
var _ cli.CommandAutocomplete = (*KVRollbackCommand)(nil)

type KVRollbackCommand struct {
*BaseCommand

flagVersion int
}

func (c *KVRollbackCommand) Synopsis() string {
return "Rolls back to a previous version of data"
}

func (c *KVRollbackCommand) Help() string {
helpText := `
Usage: vault kv rollback [options] KEY

*NOTE*: This is only supported for KV v2 engine mounts.

Restores a given previous version to the current version at the given path.
The value is written as a new version; for instance, if the current version
is 5 and the rollback version is 2, the data from version 2 will become
version 6.

$ vault kv rollback -version=2 secret/foo

Additional flags and more advanced use cases are detailed below.

` + c.Flags().Help()
return strings.TrimSpace(helpText)
}

func (c *KVRollbackCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)

// Common Options
f := set.NewFlagSet("Common Options")

f.IntVar(&IntVar{
Name: "version",
Target: &c.flagVersion,
Usage: `Specifies the version number that should be made current again.`,
})

return set
}

func (c *KVRollbackCommand) AutocompleteArgs() complete.Predictor {
return nil
}

func (c *KVRollbackCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}

func (c *KVRollbackCommand) Run(args []string) int {
f := c.Flags()

if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}

var version *int
f.Visit(func(fl *flag.Flag) {
if fl.Name == "version" {
version = &c.flagVersion
}
})

args = f.Args()

switch {
case len(args) != 1:
c.UI.Error(fmt.Sprintf("Invalid number of arguments (expected 1, got %d)", len(args)))
return 1
case version == nil:
c.UI.Error(fmt.Sprintf("Version flag must be specified"))
return 1
case c.flagVersion <= 0:
c.UI.Error(fmt.Sprintf("Invalid value %d for the version flag", c.flagVersion))
return 1
}

var err error
path := sanitizePath(args[0])

client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}

mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}

if !v2 {
c.UI.Error(fmt.Sprintf("K/V engine mount must be version 2 for rollback support"))
return 2
}

path = addPrefixToVKVPath(path, mountPath, "data")
if err != nil {
c.UI.Error(err.Error())
return 2
}

// First, do a read to get the current version for check-and-set
var meta map[string]interface{}
{
secret, err := kvReadRequest(client, path, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", path, err))
return 2
}

// Make sure a value already exists
if secret == nil || secret.Data == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
return 2
}

// Verify metadata found
rawMeta, ok := secret.Data["metadata"]
if !ok || rawMeta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
return 2
}
meta, ok = rawMeta.(map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", path))
return 2
}
if meta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
return 2
}

// Verify current data found
Copy link
Contributor

@briankassouf briankassouf Jun 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this step is necessary. If the version has been marked deleted there wouldn't be any data returned, but we should still allow a rollback on that key.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

rawData, ok := secret.Data["data"]
if !ok || rawData == nil {
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", path))
return 2
}
data, ok := rawData.(map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Data found at %s is not the expected type (JSON object)", path))
return 2
}
if data == nil {
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", path))
return 2
}
}

casVersion := meta["version"]

// Set the version parameter
versionParam := map[string]string{
"version": fmt.Sprintf("%d", c.flagVersion),
}

// Now run it again and read the version we want to roll back to
var data map[string]interface{}
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never seen this syntax before, do it create a new lexical scope?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Here it's just for logical separation so I could use the same mechanism but not worry about declaration vs assignment.

Copy link
Contributor

@jippi jippi Jun 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats a nice trick, the lazy version of a func

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's enough difference that a function having to check parameters would have ended up as long, and quite possibly more complicated.

secret, err := kvReadRequest(client, path, versionParam)
if err != nil {
c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", path, err))
return 2
}

// Make sure a value already exists
if secret == nil || secret.Data == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
return 2
}

// Verify metadata found
rawMeta, ok := secret.Data["metadata"]
if !ok || rawMeta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
return 2
}
meta, ok := rawMeta.(map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", path))
return 2
}
if meta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
return 2
}

// Verify old data found
rawData, ok := secret.Data["data"]
if !ok || rawData == nil {
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", path))
return 2
}
data, ok = rawData.(map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Data found at %s is not the expected type (JSON object)", path))
return 2
}
if data == nil {
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", path))
return 2
}

if meta["deletion_time"] != nil && meta["deletion_time"].(string) != "" {
c.UI.Error(fmt.Sprintf("Cannot roll back to a version that has been deleted"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you able to test if this check works? I think if the version has been deleted then no data would be returned and the above data checks would fail first.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought if it was deleted it 404s but still returns metadata. I'll check.

return 2
}

if meta["destroyed"] != nil && meta["destroyed"].(bool) {
c.UI.Error(fmt.Sprintf("Cannot roll back to a version that has been destroyed"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

return 2
}
}

secret, err := client.Logical().Write(path, map[string]interface{}{
"data": data,
"options": map[string]interface{}{
"cas": casVersion,
},
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
return 2
}
if secret == nil {
// Don't output anything unless using the "table" format
if Format(c.UI) == "table" {
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
}
return 0
}

if c.flagField != "" {
return PrintRawField(c.UI, secret, c.flagField)
}

return OutputSecret(c.UI, secret)
}