From a581c4ff091c0b3b5594d2a6ab02b2d73f98a6ca Mon Sep 17 00:00:00 2001 From: Ricardo Maraschini Date: Mon, 21 Oct 2024 19:02:37 +0200 Subject: [PATCH] feat: add admin-console reset-password command (#1345) * feat: add admin-console reset-password command add a new command to reset the admin console password. * chore: add step to reset admin console password adds a step to our single node installation test to reset the admin console password and to login once more. --- cmd/embedded-cluster/admin_console.go | 56 +++++++++++++++++++ cmd/embedded-cluster/k0s.go | 7 +++ cmd/embedded-cluster/main.go | 1 + e2e/install_test.go | 10 ++++ .../login-with-custom-password/test.spec.ts | 8 +++ e2e/playwright/tests/shared/login.ts | 4 +- e2e/scripts/playwright.sh | 2 + pkg/helpers/command.go | 5 ++ pkg/kotscli/kotscli.go | 21 +++++++ 9 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 cmd/embedded-cluster/admin_console.go create mode 100644 e2e/playwright/tests/login-with-custom-password/test.spec.ts diff --git a/cmd/embedded-cluster/admin_console.go b/cmd/embedded-cluster/admin_console.go new file mode 100644 index 000000000..0aafa37b9 --- /dev/null +++ b/cmd/embedded-cluster/admin_console.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "os" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + "github.com/replicatedhq/embedded-cluster/pkg/defaults" + "github.com/replicatedhq/embedded-cluster/pkg/kotscli" +) + +func adminConsoleCommand() *cli.Command { + return &cli.Command{ + Name: "admin-console", + Usage: fmt.Sprintf("Manage the %s Admin Console", defaults.BinaryName()), + Subcommands: []*cli.Command{ + adminConsoleResetPassswordCommand(), + }, + } +} + +func adminConsoleResetPassswordCommand() *cli.Command { + return &cli.Command{ + Name: "reset-password", + Usage: "Reset the Admin Console password", + Before: func(c *cli.Context) error { + if os.Getuid() != 0 { + return fmt.Errorf("reset-password command must be run as root") + } + if len(c.Args().Slice()) != 1 { + return fmt.Errorf("expected admin console password as argument") + } + return nil + }, + Action: func(c *cli.Context) error { + provider, err := getProviderFromCluster(c.Context) + if err != nil { + return err + } + + password := c.Args().Get(0) + if !validateAdminConsolePassword(password, password) { + return ErrNothingElseToAdd + } + + if err := kotscli.ResetPassword(provider, password); err != nil { + return err + } + + logrus.Info("Admin Console password reset successfully") + return nil + }, + } +} diff --git a/cmd/embedded-cluster/k0s.go b/cmd/embedded-cluster/k0s.go index d8b729573..fd2a35e28 100644 --- a/cmd/embedded-cluster/k0s.go +++ b/cmd/embedded-cluster/k0s.go @@ -3,9 +3,12 @@ package main import ( "context" "encoding/json" + "fmt" + "os" "os/exec" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/defaults" ) var ( @@ -27,6 +30,10 @@ type k0sVars struct { // getK0sStatus returns the status of the k0s service. func getK0sStatus(ctx context.Context) (*k0sStatus, error) { + if _, err := os.Stat(k0s); err != nil { + return nil, fmt.Errorf("%s does not seem to be installed on this node", defaults.BinaryName()) + } + // get k0s status json out, err := exec.CommandContext(ctx, k0s, "status", "-o", "json").Output() if err != nil { diff --git a/cmd/embedded-cluster/main.go b/cmd/embedded-cluster/main.go index 8474b668b..333ce38c3 100644 --- a/cmd/embedded-cluster/main.go +++ b/cmd/embedded-cluster/main.go @@ -37,6 +37,7 @@ func main() { materializeCommand(), updateCommand(), restoreCommand(), + adminConsoleCommand(), }, } if err := app.RunContext(ctx, os.Args); err != nil { diff --git a/e2e/install_test.go b/e2e/install_test.go index 386b12851..f5f89667f 100644 --- a/e2e/install_test.go +++ b/e2e/install_test.go @@ -60,6 +60,16 @@ func TestSingleNodeInstallation(t *testing.T) { t.Fatalf("fail to check postupgrade state: %v: %s: %s", err, stdout, stderr) } + t.Logf("%s: resetting admin console password", time.Now().Format(time.RFC3339)) + newPassword := "newpass" + line = []string{"embedded-cluster", "admin-console", "reset-password", newPassword} + _, _, err := tc.RunCommandOnNode(0, line) + require.NoError(t, err, "unable to reset admin console password") + + t.Logf("%s: logging in with the new password", time.Now().Format(time.RFC3339)) + _, _, err = tc.RunPlaywrightTest("login-with-custom-password", newPassword) + require.NoError(t, err, "unable to login with the new password") + t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } diff --git a/e2e/playwright/tests/login-with-custom-password/test.spec.ts b/e2e/playwright/tests/login-with-custom-password/test.spec.ts new file mode 100644 index 000000000..9922cdc95 --- /dev/null +++ b/e2e/playwright/tests/login-with-custom-password/test.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from '@playwright/test'; +import { login } from '../shared'; + +test('login with custom password', async ({ page }) => { + test.setTimeout(30 * 1000); // 30 seconds + await login(page, process.env.ADMIN_CONSOLE_PASSWORD); + await expect(page.locator('.NavItem').getByText('Cluster Management')).toBeVisible(); +}); diff --git a/e2e/playwright/tests/shared/login.ts b/e2e/playwright/tests/shared/login.ts index 09c24847c..0b90c0189 100644 --- a/e2e/playwright/tests/shared/login.ts +++ b/e2e/playwright/tests/shared/login.ts @@ -1,6 +1,6 @@ -export const login = async (page) => { +export const login = async (page, password = 'password') => { await page.goto('/'); await page.getByPlaceholder('password').click(); - await page.getByPlaceholder('password').fill('password'); + await page.getByPlaceholder('password').fill(password); await page.getByRole('button', { name: 'Log in' }).click(); }; diff --git a/e2e/scripts/playwright.sh b/e2e/scripts/playwright.sh index f6d69551f..59498395e 100755 --- a/e2e/scripts/playwright.sh +++ b/e2e/scripts/playwright.sh @@ -18,6 +18,8 @@ main() { elif [ "$test_name" == "deploy-upgrade" ]; then export APP_UPGRADE_VERSION="$2" export SKIP_CLUSTER_UPGRADE_CHECK="${3:-}" + elif [ "$test_name" == "login-with-custom-password" ]; then + export ADMIN_CONSOLE_PASSWORD="$2" fi export BASE_URL="${BASE_URL:-http://10.0.0.2:30003}" diff --git a/pkg/helpers/command.go b/pkg/helpers/command.go index 86c5185f3..54d8d49a6 100644 --- a/pkg/helpers/command.go +++ b/pkg/helpers/command.go @@ -14,6 +14,8 @@ type RunCommandOptions struct { Writer io.Writer // Env is a map of additional environment variables to set for the command. Env map[string]string + // Stdin is the standard input to be used when running the command. + Stdin io.Reader } // RunCommandWithOptions runs a the provided command with the options specified. @@ -28,6 +30,9 @@ func RunCommandWithOptions(opts RunCommandOptions, bin string, args ...string) e if opts.Writer != nil { cmd.Stdout = io.MultiWriter(opts.Writer, stdout) } + if opts.Stdin != nil { + cmd.Stdin = opts.Stdin + } cmd.Stderr = stderr cmdEnv := cmd.Environ() for k, v := range opts.Env { diff --git a/pkg/kotscli/kotscli.go b/pkg/kotscli/kotscli.go index d3ab8c8c1..1f152cc88 100644 --- a/pkg/kotscli/kotscli.go +++ b/pkg/kotscli/kotscli.go @@ -84,6 +84,27 @@ func Install(provider *defaults.Provider, opts InstallOptions, msg *spinner.Mess return nil } +func ResetPassword(provider *defaults.Provider, password string) error { + materializer := goods.NewMaterializer(provider) + kotsBinPath, err := materializer.InternalBinary("kubectl-kots") + if err != nil { + return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err) + } + defer os.Remove(kotsBinPath) + + runCommandOptions := helpers.RunCommandOptions{ + Env: map[string]string{"KUBECONFIG": provider.PathToKubeConfig()}, + Stdin: strings.NewReader(fmt.Sprintf("%s\n", password)), + } + + resetArgs := []string{"reset-password", "kotsadm"} + if err := helpers.RunCommandWithOptions(runCommandOptions, kotsBinPath, resetArgs...); err != nil { + return fmt.Errorf("unable to reset admin console password: %w", err) + } + + return nil +} + type AirgapUpdateOptions struct { AppSlug string Namespace string