diff --git a/api/sys_lease.go b/api/sys_lease.go index 25598fe5f0..4c29fdcaf6 100644 --- a/api/sys_lease.go +++ b/api/sys_lease.go @@ -1,7 +1,13 @@ package api -func (c *Sys) Renew(id string) (*Secret, error) { +func (c *Sys) Renew(id string, increment int) (*Secret, error) { r := c.c.NewRequest("PUT", "/v1/sys/renew/"+id) + + body := map[string]interface{}{"increment": increment} + if err := r.SetJSONBody(body); err != nil { + return nil, err + } + resp, err := c.c.RawRequest(r) if err != nil { return nil, err diff --git a/cli/commands.go b/cli/commands.go index 389ed474a7..acf6b01976 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -132,6 +132,12 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { }, nil }, + "renew": func() (cli.Command, error) { + return &command.RenewCommand{ + Meta: meta, + }, nil + }, + "revoke": func() (cli.Command, error) { return &command.RevokeCommand{ Meta: meta, diff --git a/command/read.go b/command/read.go index 167da0dcd1..0ead0c2b98 100644 --- a/command/read.go +++ b/command/read.go @@ -46,6 +46,10 @@ func (c *ReadCommand) Run(args []string) int { return 1 } + return c.output(format, secret) +} + +func (c *ReadCommand) output(format string, secret *api.Secret) int { switch format { case "json": return c.formatJSON(secret) @@ -54,8 +58,6 @@ func (c *ReadCommand) Run(args []string) int { default: return c.formatTable(secret, true) } - - return 0 } func (c *ReadCommand) formatJSON(s *api.Secret) int { diff --git a/command/renew.go b/command/renew.go new file mode 100644 index 0000000000..479a2d9045 --- /dev/null +++ b/command/renew.go @@ -0,0 +1,104 @@ +package command + +import ( + "fmt" + "strconv" + "strings" +) + +// RenewCommand is a Command that mounts a new mount. +type RenewCommand struct { + Meta +} + +func (c *RenewCommand) Run(args []string) int { + var format string + flags := c.Meta.FlagSet("renew", FlagSetDefault) + flags.StringVar(&format, "format", "table", "") + flags.Usage = func() { c.Ui.Error(c.Help()) } + if err := flags.Parse(args); err != nil { + return 1 + } + + args = flags.Args() + if len(args) < 1 || len(args) >= 3 { + flags.Usage() + c.Ui.Error(fmt.Sprintf( + "\nRenew expects at least one argument: the lease ID to renew")) + return 1 + } + + var increment int + leaseId := args[0] + if len(args) > 1 { + parsed, err := strconv.ParseInt(args[1], 10, 0) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Invalid increment, must be an int: %s", err)) + return 1 + } + + increment = int(parsed) + } + + client, err := c.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error initializing client: %s", err)) + return 2 + } + + secret, err := client.Sys().Renew(leaseId, increment) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Renew error: %s", err)) + return 1 + } + + // Use the ReadCommand in order to format our output + var read ReadCommand + read.Meta = c.Meta + return read.output(format, secret) +} + +func (c *RenewCommand) Synopsis() string { + return "Renew the lease of a secret" +} + +func (c *RenewCommand) Help() string { + helpText := ` +Usage: vault renew [options] id [increment] + + Renew the lease on a secret, extending the time that it can be used + before it is revoked by Vault. + + Every secret in Vault has a lease associated with it. If the user of + the secret wants to use it longer than the lease, then it must be + renewed. Renewing the lease will not change the contents of the secret. + + To renew a secret, run this command with the lease ID returned when it + was read. Optionally, request a specific increment in seconds. Vault may + is not required to honor this request. + +General Options: + + -address=TODO The address of the Vault server. + + -ca-cert=path Path to a PEM encoded CA cert file to use to + verify the Vault server SSL certificate. + + -ca-path=path Path to a directory of PEM encoded CA cert files + to verify the Vault server SSL certificate. If both + -ca-cert and -ca-path are specified, -ca-path is used. + + -insecure Do not verify TLS certificate. This is highly + not recommended. This is especially not recommended + for unsealing a vault. + +Renew Options: + + -format=table The format for output. By default it is a whitespace- + delimited table. This can also be json. +` + return strings.TrimSpace(helpText) +} diff --git a/command/renew_test.go b/command/renew_test.go new file mode 100644 index 0000000000..bf6cfbde6c --- /dev/null +++ b/command/renew_test.go @@ -0,0 +1,47 @@ +package command + +import ( + "testing" + + "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/vault" + "github.com/mitchellh/cli" +) + +func TestRenew(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := http.TestServer(t, core) + defer ln.Close() + + ui := new(cli.MockUi) + c := &RenewCommand{ + Meta: Meta{ + ClientToken: token, + Ui: ui, + }, + } + + // write a secret with a lease + client := testClient(t, addr, token) + _, err := client.Logical().Write("secret/foo", map[string]interface{}{ + "key": "value", + "lease": "1m", + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + // read the secret to get its lease ID + secret, err := client.Logical().Read("secret/foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + args := []string{ + "-address", addr, + secret.LeaseID, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +}