1
0

Update mount command

This commit is contained in:
Seth Vargo 2017-09-05 00:02:24 -04:00
parent 1047792f2d
commit 5cc5b6c6a6
No known key found for this signature in database
GPG Key ID: C921994F9C27E0FF
2 changed files with 324 additions and 199 deletions

View File

@ -3,162 +3,207 @@ package command
import (
"fmt"
"strings"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// Ensure we are implementing the right interfaces.
var _ cli.Command = (*MountCommand)(nil)
var _ cli.CommandAutocomplete = (*MountCommand)(nil)
// MountCommand is a Command that mounts a new mount.
type MountCommand struct {
meta.Meta
}
*BaseCommand
func (c *MountCommand) Run(args []string) int {
var description, path, defaultLeaseTTL, maxLeaseTTL, pluginName string
var local, forceNoCache bool
flags := c.Meta.FlagSet("mount", meta.FlagSetDefault)
flags.StringVar(&description, "description", "", "")
flags.StringVar(&path, "path", "", "")
flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "")
flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "")
flags.StringVar(&pluginName, "plugin-name", "", "")
flags.BoolVar(&forceNoCache, "force-no-cache", false, "")
flags.BoolVar(&local, "local", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\nmount expects one argument: the type to mount."))
return 1
}
mountType := args[0]
// If no path is specified, we default the path to the backend type
// or use the plugin name if it's a plugin backend
if path == "" {
if mountType == "plugin" {
path = pluginName
} else {
path = mountType
}
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
mountInfo := &api.MountInput{
Type: mountType,
Description: description,
Config: api.MountConfigInput{
DefaultLeaseTTL: defaultLeaseTTL,
MaxLeaseTTL: maxLeaseTTL,
ForceNoCache: forceNoCache,
PluginName: pluginName,
},
Local: local,
}
if err := client.Sys().Mount(path, mountInfo); err != nil {
c.Ui.Error(fmt.Sprintf(
"Mount error: %s", err))
return 2
}
mountTypeOutput := fmt.Sprintf("'%s'", mountType)
if mountType == "plugin" {
mountTypeOutput = fmt.Sprintf("plugin '%s'", pluginName)
}
c.Ui.Output(fmt.Sprintf(
"Successfully mounted %s at '%s'!",
mountTypeOutput, path))
return 0
flagDescription string
flagPath string
flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration
flagForceNoCache bool
flagPluginName string
flagLocal bool
}
func (c *MountCommand) Synopsis() string {
return "Mount a logical backend"
return "Mounts a secret backend at a path"
}
func (c *MountCommand) Help() string {
helpText := `
Usage: vault mount [options] type
Usage: vault mount [options] TYPE
Mount a logical backend.
Mount a secret backend at a particular path. By default, secret backends are
mounted at the path corresponding to their "type", but users can customize
the mount point using the -path option.
This command mounts a logical backend for storing and/or generating
secrets.
Once mounted at a path, Vault will route all requests which begin with the
path to the secret backend.
General Options:
` + meta.GeneralOptionsUsage() + `
Mount Options:
Mount the AWS backend at aws/:
-description=<desc> Human-friendly description of the purpose for
the mount. This shows up in the mounts command.
$ vault mount aws
-path=<path> Mount point for the logical backend. This
defaults to the type of the mount.
Mount the SSH backend at ssh-prod/:
-default-lease-ttl=<duration> Default lease time-to-live for this backend.
If not specified, uses the global default, or
the previously set value. Set to '0' to
explicitly set it to use the global default.
$ vault mount -path=ssh-prod ssh
-max-lease-ttl=<duration> Max lease time-to-live for this backend.
If not specified, uses the global default, or
the previously set value. Set to '0' to
explicitly set it to use the global default.
Mount the database backend with an explicit maximum TTL of 30m:
-force-no-cache Forces the backend to disable caching. If not
specified, uses the global default. This does
not affect caching of the underlying encrypted
data storage.
$ vault mount -max-lease-ttl=30m database
-plugin-name Name of the plugin to mount based from the name
in the plugin catalog.
Mount a custom plugin (after it is registered in the plugin registry):
$ vault mount -path=my-secrets -plugin-name=my-custom-plugin plugin
For a full list of secret backends and examples, please see the documentation.
` + c.Flags().Help()
-local Mark the mount as a local mount. Local mounts
are not replicated nor (if a secondary)
removed by replication.
`
return strings.TrimSpace(helpText)
}
func (c *MountCommand) AutocompleteArgs() complete.Predictor {
// This list does not contain deprecated backends
return complete.PredictSet(
"aws",
"consul",
"pki",
"transit",
"ssh",
"rabbitmq",
"database",
"totp",
"plugin",
)
func (c *MountCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "description",
Target: &c.flagDescription,
Completion: complete.PredictAnything,
Usage: "Human-friendly description for the purpose of this mount.",
})
f.StringVar(&StringVar{
Name: "path",
Target: &c.flagPath,
Default: "", // The default is complex, so we have to manually document
Completion: complete.PredictAnything,
Usage: "Place where the mount will be accessible. This must be " +
"unique across all mounts. This defaults to the \"type\" of the mount.",
})
f.DurationVar(&DurationVar{
Name: "default-lease-ttl",
Target: &c.flagDefaultLeaseTTL,
Completion: complete.PredictAnything,
Usage: "The default lease TTL for this backend. If unspecified, this " +
"defaults to the Vault server's globally configured default lease TTL.",
})
f.DurationVar(&DurationVar{
Name: "max-lease-ttl",
Target: &c.flagMaxLeaseTTL,
Completion: complete.PredictAnything,
Usage: "The maximum lease TTL for this backend. If unspecified, this " +
"defaults to the Vault server's globally configured maximum lease TTL.",
})
f.BoolVar(&BoolVar{
Name: "force-no-cache",
Target: &c.flagForceNoCache,
Default: false,
Usage: "Force the backend to disable caching. If unspecified, this " +
"defaults to the Vault server's globally configured cache settings. " +
"This does not affect caching of the underlying encrypted data storage.",
})
f.StringVar(&StringVar{
Name: "plugin-name",
Target: &c.flagPluginName,
Completion: complete.PredictAnything,
Usage: "Name of the plugin to mount. This plugin name must already exist " +
"in the Vault server's plugin catalog.",
})
f.BoolVar(&BoolVar{
Name: "local",
Target: &c.flagLocal,
Default: false,
Usage: "Mark the mount as a local-only mount. Local mounts are not " +
"replicated nor removed by replication.",
})
return set
}
func (c *MountCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultAvailableMounts()
}
func (c *MountCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-description": complete.PredictNothing,
"-path": complete.PredictNothing,
"-default-lease-ttl": complete.PredictNothing,
"-max-lease-ttl": complete.PredictNothing,
"-force-no-cache": complete.PredictNothing,
"-plugin-name": complete.PredictNothing,
"-local": complete.PredictNothing,
}
return c.Flags().Completions()
}
func (c *MountCommand) 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 len(args) {
case 0:
c.UI.Error("Missing TYPE!")
return 1
case 1:
// OK
default:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
// Get the mount type (first arg)
mountType := strings.TrimSpace(args[0])
// If no path is specified, we default the path to the backend type
// or use the plugin name if it's a plugin backend
mountPath := c.flagPath
if mountPath == "" {
if mountType == "plugin" {
mountPath = c.flagPluginName
} else {
mountPath = mountType
}
}
// Append a trailing slash to indicate it's a path in output
mountPath = ensureTrailingSlash(mountPath)
// Build mount input
mountInput := &api.MountInput{
Type: mountType,
Description: c.flagDescription,
Local: c.flagLocal,
Config: api.MountConfigInput{
DefaultLeaseTTL: c.flagDefaultLeaseTTL.String(),
MaxLeaseTTL: c.flagMaxLeaseTTL.String(),
ForceNoCache: c.flagForceNoCache,
PluginName: c.flagPluginName,
},
}
if err := client.Sys().Mount(mountPath, mountInput); err != nil {
c.UI.Error(fmt.Sprintf("Error mounting: %s", err))
return 2
}
mountThing := mountType + " secret backend"
if mountType == "plugin" {
mountThing = c.flagPluginName + " plugin"
}
c.UI.Output(fmt.Sprintf("Success! Mounted the %s at: %s", mountThing, mountPath))
return 0
}

View File

@ -1,90 +1,170 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestMount(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testMountCommand(tb testing.TB) (*cli.MockUi, *MountCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &MountCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &MountCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestMountCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"empty",
nil,
"Missing TYPE!",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
{
"not_a_valid_mount",
[]string{"nope_definitely_not_a_valid_mount_like_ever"},
"",
2,
},
{
"mount",
[]string{"transit"},
"Success! Mounted the transit secret backend at: transit/",
0,
},
{
"mount_path",
[]string{
"-path", "transit_mount_point",
"transit",
},
"Success! Mounted the transit secret backend at: transit_mount_point/",
0,
},
}
args := []string{
"-address", addr,
"kv",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testMountCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
t.Run("integration", func(t *testing.T) {
t.Parallel()
mounts, err := client.Sys().ListMounts()
if err != nil {
t.Fatalf("err: %s", err)
}
client, closer := testVaultServer(t)
defer closer()
mount, ok := mounts["kv/"]
if !ok {
t.Fatal("should have kv mount")
}
if mount.Type != "kv" {
t.Fatal("should be kv type")
}
}
func TestMount_Generic(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &MountCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"generic",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
mounts, err := client.Sys().ListMounts()
if err != nil {
t.Fatalf("err: %s", err)
}
mount, ok := mounts["generic/"]
if !ok {
t.Fatal("should have generic mount path")
}
if mount.Type != "generic" {
t.Fatal("should be generic type")
}
ui, cmd := testMountCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-path", "mount_integration/",
"-description", "The best kind of test",
"-default-lease-ttl", "30m",
"-max-lease-ttl", "1h",
"-force-no-cache",
"pki",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Mounted the pki secret backend at: mount_integration/"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
mounts, err := client.Sys().ListMounts()
if err != nil {
t.Fatal(err)
}
mountInfo, ok := mounts["mount_integration/"]
if !ok {
t.Fatalf("expected mount to exist")
}
if exp := "pki"; mountInfo.Type != exp {
t.Errorf("expected %q to be %q", mountInfo.Type, exp)
}
if exp := "The best kind of test"; mountInfo.Description != exp {
t.Errorf("expected %q to be %q", mountInfo.Description, exp)
}
if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp {
t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp)
}
if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp {
t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp)
}
if exp := true; mountInfo.Config.ForceNoCache != exp {
t.Errorf("expected %t to be %t", mountInfo.Config.ForceNoCache, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testMountCommand(t)
cmd.client = client
code := cmd.Run([]string{
"pki",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error mounting: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testMountCommand(t)
assertNoTabs(t, cmd)
})
}