From 03b3041265ca8380d4a6de507a4979fac7945848 Mon Sep 17 00:00:00 2001 From: Pratyoy Mukhopadhyay <35388175+pmmukh@users.noreply.github.com> Date: Fri, 18 Feb 2022 08:50:05 -0800 Subject: [PATCH] remount cli changes (#14159) --- command/auth_move.go | 123 +++++++++++++++++++++++++++++ command/auth_move_test.go | 146 +++++++++++++++++++++++++++++++++++ command/commands.go | 5 ++ command/secrets_move.go | 38 +++++++-- command/secrets_move_test.go | 7 +- 5 files changed, 308 insertions(+), 11 deletions(-) create mode 100644 command/auth_move.go create mode 100644 command/auth_move_test.go diff --git a/command/auth_move.go b/command/auth_move.go new file mode 100644 index 0000000000..9e591ba64f --- /dev/null +++ b/command/auth_move.go @@ -0,0 +1,123 @@ +package command + +import ( + "fmt" + "strings" + "time" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*AuthMoveCommand)(nil) + _ cli.CommandAutocomplete = (*AuthMoveCommand)(nil) +) + +type AuthMoveCommand struct { + *BaseCommand +} + +func (c *AuthMoveCommand) Synopsis() string { + return "Move an auth method to a new path" +} + +func (c *AuthMoveCommand) Help() string { + helpText := ` +Usage: vault auth move [options] SOURCE DESTINATION + + Moves an existing auth method to a new path. Any leases from the old + auth method are revoked, but all configuration associated with the method + is preserved. It initiates the migration and intermittently polls its status, + exiting if a final state is reached. + + This command works within or across namespaces, both source and destination paths + can be prefixed with a namespace heirarchy relative to the current namespace. + + WARNING! Moving an auth method will revoke any leases from the + old method. + + Move the auth method at approle/ to generic/: + + $ vault auth move approle/ generic/ + + Move the auth method at ns1/approle/ across namespaces to ns2/generic/, + where ns1 and ns2 are child namespaces of the current namespace: + + $ vault auth move ns1/approle/ ns2/generic/ + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *AuthMoveCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *AuthMoveCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultMounts() +} + +func (c *AuthMoveCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuthMoveCommand) 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) < 2: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 2, got %d)", len(args))) + return 1 + case len(args) > 2: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 2, got %d)", len(args))) + return 1 + } + + // Grab the source and destination + source := ensureTrailingSlash(args[0]) + destination := ensureTrailingSlash(args[1]) + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + remountResp, err := client.Sys().StartRemount(source, destination) + if err != nil { + c.UI.Error(fmt.Sprintf("Error moving auth method %s to %s: %s", source, destination, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Started moving auth method %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + + // Poll the status endpoint with the returned migration ID + // Exit if a terminal status is reached, else wait and retry + for { + remountStatusResp, err := client.Sys().RemountStatus(remountResp.MigrationID) + if err != nil { + c.UI.Error(fmt.Sprintf("Error checking migration status of auth method %s to %s: %s", source, destination, err)) + return 2 + } + if remountStatusResp.MigrationInfo.MigrationStatus == MountMigrationStatusSuccess { + c.UI.Output(fmt.Sprintf("Success! Finished moving auth method %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + return 0 + } + if remountStatusResp.MigrationInfo.MigrationStatus == MountMigrationStatusFailure { + c.UI.Error(fmt.Sprintf("Failure! Error encountered moving auth method %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + return 0 + } + c.UI.Output(fmt.Sprintf("Waiting for terminal status in migration of auth method %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + time.Sleep(10 * time.Second) + } + + return 0 +} diff --git a/command/auth_move_test.go b/command/auth_move_test.go new file mode 100644 index 0000000000..035938efe5 --- /dev/null +++ b/command/auth_move_test.go @@ -0,0 +1,146 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" +) + +func testAuthMoveCommand(tb testing.TB) (*cli.MockUi, *AuthMoveCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &AuthMoveCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestAuthMoveCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + []string{}, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar", "baz"}, + "Too many arguments", + 1, + }, + { + "non_existent", + []string{"not_real", "over_here"}, + "Error moving auth method not_real/ to over_here/", + 2, + }, + } + + t.Run("validations", func(t *testing.T) { + t.Parallel() + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuthMoveCommand(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) + } + }) + } + }) + + t.Run("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuthMoveCommand(t) + cmd.client = client + + if err := client.Sys().EnableAuthWithOptions("my-auth", &api.EnableAuthOptions{ + Type: "userpass", + }); err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "auth/my-auth/", "auth/my-auth-2/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Finished moving auth method auth/my-auth/ to auth/my-auth-2/" + 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().ListAuth() + if err != nil { + t.Fatal(err) + } + + if _, ok := mounts["my-auth-2/"]; !ok { + t.Errorf("expected mount at my-auth-2/: %#v", mounts) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthMoveCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "auth/my-auth/", "auth/my-auth-2/", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error moving auth method auth/my-auth/ to auth/my-auth-2/:" + 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 := testAuthMoveCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/command/commands.go b/command/commands.go index a0d82b6872..3cbd6b15f6 100644 --- a/command/commands.go +++ b/command/commands.go @@ -278,6 +278,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { BaseCommand: getBaseCommand(), }, nil }, + "auth move": func() (cli.Command, error) { + return &AuthMoveCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "debug": func() (cli.Command, error) { return &DebugCommand{ BaseCommand: getBaseCommand(), diff --git a/command/secrets_move.go b/command/secrets_move.go index ff33310476..458e3bbece 100644 --- a/command/secrets_move.go +++ b/command/secrets_move.go @@ -3,6 +3,7 @@ package command import ( "fmt" "strings" + "time" "github.com/mitchellh/cli" "github.com/posener/complete" @@ -13,6 +14,11 @@ var ( _ cli.CommandAutocomplete = (*SecretsMoveCommand)(nil) ) +const ( + MountMigrationStatusSuccess = "success" + MountMigrationStatusFailure = "failure" +) + type SecretsMoveCommand struct { *BaseCommand } @@ -27,19 +33,20 @@ Usage: vault secrets move [options] SOURCE DESTINATION Moves an existing secrets engine to a new path. Any leases from the old secrets engine are revoked, but all configuration associated with the engine - is preserved. + is preserved. It initiates the migration and intermittently polls its status, + exiting if a final state is reached. This command works within or across namespaces, both source and destination paths can be prefixed with a namespace heirarchy relative to the current namespace. - WARNING! Moving an existing secrets engine will revoke any leases from the + WARNING! Moving a secrets engine will revoke any leases from the old engine. - Move the existing secrets engine at secret/ to generic/: + Move the secrets engine at secret/ to generic/: $ vault secrets move secret/ generic/ - Move the existing secrets engine at ns1/secret/ across namespaces to ns2/generic/, + Move the secrets engine at ns1/secret/ across namespaces to ns2/generic/, where ns1 and ns2 are child namespaces of the current namespace: $ vault secrets move ns1/secret/ ns2/generic/ @@ -95,6 +102,27 @@ func (c *SecretsMoveCommand) Run(args []string) int { return 2 } - c.UI.Output(fmt.Sprintf("Success! Started moving secrets engine %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + c.UI.Output(fmt.Sprintf("Started moving secrets engine %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + + // Poll the status endpoint with the returned migration ID + // Exit if a terminal status is reached, else wait and retry + for { + remountStatusResp, err := client.Sys().RemountStatus(remountResp.MigrationID) + if err != nil { + c.UI.Error(fmt.Sprintf("Error checking migration status of secrets engine %s to %s: %s", source, destination, err)) + return 2 + } + if remountStatusResp.MigrationInfo.MigrationStatus == MountMigrationStatusSuccess { + c.UI.Output(fmt.Sprintf("Success! Finished moving secrets engine %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + return 0 + } + if remountStatusResp.MigrationInfo.MigrationStatus == MountMigrationStatusFailure { + c.UI.Error(fmt.Sprintf("Failure! Error encountered moving secrets engine %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + return 0 + } + c.UI.Output(fmt.Sprintf("Waiting for terminal status in migration of secrets engine %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + time.Sleep(10 * time.Second) + } + return 0 } diff --git a/command/secrets_move_test.go b/command/secrets_move_test.go index bca2a530fc..153fbeb2cd 100644 --- a/command/secrets_move_test.go +++ b/command/secrets_move_test.go @@ -3,7 +3,6 @@ package command import ( "strings" "testing" - "time" "github.com/mitchellh/cli" ) @@ -92,16 +91,12 @@ func TestSecretsMoveCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Started moving secrets engine secret/ to generic/" + expected := "Success! Finished moving secrets engine secret/ to generic/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) } - // Wait for the move command to complete. Ideally we'd check remount status - // explicitly but we don't have migration id here - time.Sleep(1 * time.Second) - mounts, err := client.Sys().ListMounts() if err != nil { t.Fatal(err)