Update mount command
This commit is contained in:
parent
1047792f2d
commit
5cc5b6c6a6
297
command/mount.go
297
command/mount.go
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user