diff --git a/cmd/crc/cmd/console.go b/cmd/crc/cmd/console.go index bdd5c6a385..f95c42c8b6 100644 --- a/cmd/crc/cmd/console.go +++ b/cmd/crc/cmd/console.go @@ -123,7 +123,7 @@ func toConsoleClusterConfig(result *client.ConsoleResult) *clusterConfig { }, DeveloperCredentials: credentials{ Username: "developer", - Password: "developer", + Password: result.ClusterConfig.DeveloperPass, }, } } diff --git a/cmd/crc/cmd/console_test.go b/cmd/crc/cmd/console_test.go index 0910dfb809..ab83781c36 100644 --- a/cmd/crc/cmd/console_test.go +++ b/cmd/crc/cmd/console_test.go @@ -20,6 +20,7 @@ var DummyClusterConfig = types.ClusterConfig{ ClusterCACert: "MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ", KubeConfig: "/tmp/kubeconfig", KubeAdminPass: "foobar", + DeveloperPass: "foobar", ClusterAPI: "https://foo.testing:6443", WebConsoleURL: "https://console.foo.testing:6443", ProxyConfig: nil, @@ -60,9 +61,9 @@ func TestConsolePlainError(t *testing.T) { } func TestConsoleWithPrintCredentialsPlainSuccess(t *testing.T) { - expectedOut := fmt.Sprintf(`To login as a regular user, run 'oc login -u developer -p developer %s'. + expectedOut := fmt.Sprintf(`To login as a regular user, run 'oc login -u developer -p %s %s'. To login as an admin, run 'oc login -u kubeadmin -p %s %s' -`, fakemachine.DummyClusterConfig.ClusterAPI, fakemachine.DummyClusterConfig.KubeAdminPass, fakemachine.DummyClusterConfig.ClusterAPI) +`, fakemachine.DummyClusterConfig.DeveloperPass, fakemachine.DummyClusterConfig.ClusterAPI, fakemachine.DummyClusterConfig.KubeAdminPass, fakemachine.DummyClusterConfig.ClusterAPI) out := new(bytes.Buffer) assert.NoError(t, runConsole(out, setUpClientForConsole(t), false, true, "")) assert.Equal(t, expectedOut, out.String()) @@ -70,9 +71,9 @@ To login as an admin, run 'oc login -u kubeadmin -p %s %s' func TestConsoleWithPrintCredentialsAndURLPlainSuccess(t *testing.T) { expectedOut := fmt.Sprintf(`%s -To login as a regular user, run 'oc login -u developer -p developer %s'. +To login as a regular user, run 'oc login -u developer -p %s %s'. To login as an admin, run 'oc login -u kubeadmin -p %s %s' -`, fakemachine.DummyClusterConfig.WebConsoleURL, fakemachine.DummyClusterConfig.ClusterAPI, fakemachine.DummyClusterConfig.KubeAdminPass, fakemachine.DummyClusterConfig.ClusterAPI) +`, fakemachine.DummyClusterConfig.WebConsoleURL, fakemachine.DummyClusterConfig.DeveloperPass, fakemachine.DummyClusterConfig.ClusterAPI, fakemachine.DummyClusterConfig.KubeAdminPass, fakemachine.DummyClusterConfig.ClusterAPI) out := new(bytes.Buffer) assert.NoError(t, runConsole(out, setUpClientForConsole(t), true, true, "")) assert.Equal(t, expectedOut, out.String()) @@ -80,22 +81,22 @@ To login as an admin, run 'oc login -u kubeadmin -p %s %s' func TestConsoleJSONSuccess(t *testing.T) { expectedJSONOut := fmt.Sprintf(`{ - "success": true, - "clusterConfig": { - "clusterType": "openshift", - "cacert": "%s", - "webConsoleUrl": "%s", - "url": "%s", - "adminCredentials": { - "username": "kubeadmin", - "password": "%s" - }, - "developerCredentials": { - "username": "developer", - "password": "developer" - } - } -}`, fakemachine.DummyClusterConfig.ClusterCACert, fakemachine.DummyClusterConfig.WebConsoleURL, fakemachine.DummyClusterConfig.ClusterAPI, fakemachine.DummyClusterConfig.KubeAdminPass) + "success": true, + "clusterConfig": { + "clusterType": "openshift", + "cacert": "%s", + "webConsoleUrl": "%s", + "url": "%s", + "adminCredentials": { + "username": "kubeadmin", + "password": "%s" + }, + "developerCredentials": { + "username": "developer", + "password": "%s" + } + } + }`, fakemachine.DummyClusterConfig.ClusterCACert, fakemachine.DummyClusterConfig.WebConsoleURL, fakemachine.DummyClusterConfig.ClusterAPI, fakemachine.DummyClusterConfig.KubeAdminPass, fakemachine.DummyClusterConfig.DeveloperPass) out := new(bytes.Buffer) assert.NoError(t, runConsole(out, setUpClientForConsole(t), false, false, jsonFormat)) assert.JSONEq(t, expectedJSONOut, out.String()) diff --git a/cmd/crc/cmd/start.go b/cmd/crc/cmd/start.go index 82c3a6513b..e33d1fba2b 100644 --- a/cmd/crc/cmd/start.go +++ b/cmd/crc/cmd/start.go @@ -140,7 +140,7 @@ func toClusterConfig(result *types.StartResult) *clusterConfig { }, DeveloperCredentials: credentials{ Username: "developer", - Password: "developer", + Password: result.ClusterConfig.DeveloperPass, }, } } diff --git a/cmd/crc/cmd/start_test.go b/cmd/crc/cmd/start_test.go index 43a466b892..d089c6c2c1 100644 --- a/cmd/crc/cmd/start_test.go +++ b/cmd/crc/cmd/start_test.go @@ -32,7 +32,7 @@ func TestRenderActionPlainSuccess(t *testing.T) { }, DeveloperCredentials: credentials{ Username: "developer", - Password: "developer", + Password: "secret", }, }, }, out, "")) @@ -118,7 +118,7 @@ Log in as administrator: Log in as user: Username: developer - Password: developer + Password: secret Use the 'oc' command line interface: $ eval $(crc oc-env) @@ -136,7 +136,7 @@ Log in as administrator: Log in as user: Username: developer - Password: developer + Password: secret Use the 'oc' command line interface: PS> & crc oc-env | Invoke-Expression @@ -154,7 +154,7 @@ Log in as administrator: Log in as user: Username: developer - Password: developer + Password: secret Use the 'oc' command line interface: > @FOR /f "tokens=*" %i IN ('crc oc-env') DO @call %i diff --git a/pkg/crc/api/api_client_test.go b/pkg/crc/api/api_client_test.go index 84b5f39010..619026c610 100644 --- a/pkg/crc/api/api_client_test.go +++ b/pkg/crc/api/api_client_test.go @@ -94,6 +94,7 @@ func TestStart(t *testing.T) { ClusterCACert: "MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ", KubeConfig: "/tmp/kubeconfig", KubeAdminPass: "foobar", + DeveloperPass: "foobar", ClusterAPI: "https://foo.testing:6443", WebConsoleURL: "https://console.foo.testing:6443", ProxyConfig: nil, diff --git a/pkg/crc/api/api_http_test.go b/pkg/crc/api/api_http_test.go index f8219bf2ec..7096a8e44b 100644 --- a/pkg/crc/api/api_http_test.go +++ b/pkg/crc/api/api_http_test.go @@ -167,11 +167,11 @@ var testCases = []testCase{ // start { request: post("start"), - response: jSon(`{"Status":"","ClusterConfig":{"ClusterType":"openshift","ClusterCACert":"MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ","KubeConfig":"/tmp/kubeconfig","KubeAdminPass":"foobar","ClusterAPI":"https://foo.testing:6443","WebConsoleURL":"https://console.foo.testing:6443","ProxyConfig":null},"KubeletStarted":true}`), + response: jSon(`{"Status":"","ClusterConfig":{"ClusterType":"openshift","ClusterCACert":"MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ","KubeConfig":"/tmp/kubeconfig","KubeAdminPass":"foobar","DeveloperPass":"foobar","ClusterAPI":"https://foo.testing:6443","WebConsoleURL":"https://console.foo.testing:6443","ProxyConfig":null},"KubeletStarted":true}`), }, { request: get("start"), - response: jSon(`{"Status":"","ClusterConfig":{"ClusterType":"openshift","ClusterCACert":"MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ","KubeConfig":"/tmp/kubeconfig","KubeAdminPass":"foobar","ClusterAPI":"https://foo.testing:6443","WebConsoleURL":"https://console.foo.testing:6443","ProxyConfig":null},"KubeletStarted":true}`), + response: jSon(`{"Status":"","ClusterConfig":{"ClusterType":"openshift","ClusterCACert":"MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ","KubeConfig":"/tmp/kubeconfig","KubeAdminPass":"foobar","DeveloperPass":"foobar","ClusterAPI":"https://foo.testing:6443","WebConsoleURL":"https://console.foo.testing:6443","ProxyConfig":null},"KubeletStarted":true}`), }, // start with failure @@ -273,7 +273,7 @@ var testCases = []testCase{ // webconsoleurl { request: get("webconsoleurl"), - response: jSon(`{"ClusterConfig":{"ClusterType":"openshift","ClusterCACert":"MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ","KubeConfig":"/tmp/kubeconfig","KubeAdminPass":"foobar","ClusterAPI":"https://foo.testing:6443","WebConsoleURL":"https://console.foo.testing:6443","ProxyConfig":null},"State":"Running"}`), + response: jSon(`{"ClusterConfig":{"ClusterType":"openshift","ClusterCACert":"MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ","KubeConfig":"/tmp/kubeconfig","KubeAdminPass":"foobar","DeveloperPass":"foobar","ClusterAPI":"https://foo.testing:6443","WebConsoleURL":"https://console.foo.testing:6443","ProxyConfig":null},"State":"Running"}`), }, // webconsoleurl with failure diff --git a/pkg/crc/cluster/kubeadmin_password.go b/pkg/crc/cluster/kubeadmin_password.go index f8beb9fb13..cc12fdae1f 100644 --- a/pkg/crc/cluster/kubeadmin_password.go +++ b/pkg/crc/cluster/kubeadmin_password.go @@ -17,33 +17,21 @@ import ( "golang.org/x/crypto/bcrypt" ) -// GenerateKubeAdminUserPassword creates and put updated kubeadmin password to ~/.crc/machine/crc/kubeadmin-password -func GenerateKubeAdminUserPassword() error { - logging.Infof("Generating new password for the kubeadmin user") - kubeAdminPasswordFile := constants.GetKubeAdminPasswordPath() - kubeAdminPassword, err := GenerateRandomPasswordHash(23) +// GenerateUserPassword creates and put updated password to ~/.crc/machine/crc/ directory +func GenerateUserPassword(passwordFile string, user string) error { + logging.Infof("Generating new password for the %s user", user) + password, err := GenerateRandomPasswordHash(23) if err != nil { - return fmt.Errorf("Cannot generate the kubeadmin user password: %w", err) + return fmt.Errorf("cannot generate the %s user password: %w", user, err) } - return os.WriteFile(kubeAdminPasswordFile, []byte(kubeAdminPassword), 0600) + return os.WriteFile(passwordFile, []byte(password), 0600) } -// UpdateKubeAdminUserPassword updates the htpasswd secret -func UpdateKubeAdminUserPassword(ctx context.Context, ocConfig oc.Config, newPassword string) error { - if newPassword != "" { - logging.Infof("Overriding password for kubeadmin user") - if err := os.WriteFile(constants.GetKubeAdminPasswordPath(), []byte(strings.TrimSpace(newPassword)), 0600); err != nil { - return err - } - } - - kubeAdminPassword, err := GetKubeadminPassword() +// UpdateUserPasswords updates the htpasswd secret +func UpdateUserPasswords(ctx context.Context, ocConfig oc.Config, newKubeAdminPassword string, newDeveloperPassword string) error { + credentials, err := resolveUserPasswords(newKubeAdminPassword, newDeveloperPassword, constants.GetKubeAdminPasswordPath(), constants.GetDeveloperPasswordPath()) if err != nil { - return fmt.Errorf("Cannot read the kubeadmin user password from file: %w", err) - } - credentials := map[string]string{ - "developer": "developer", - "kubeadmin": kubeAdminPassword, + return err } if err := WaitForOpenshiftResource(ctx, ocConfig, "secret"); err != nil { @@ -62,7 +50,7 @@ func UpdateKubeAdminUserPassword(ctx context.Context, ocConfig oc.Config, newPas return nil } - logging.Infof("Changing the password for the kubeadmin user") + logging.Infof("Changing the password for the users") expected, err := getHtpasswd(credentials, externals) if err != nil { return err @@ -72,14 +60,13 @@ func UpdateKubeAdminUserPassword(ctx context.Context, ocConfig oc.Config, newPas "-n", "openshift-config", "--type", "merge"} _, stderr, err = ocConfig.RunOcCommandPrivate(cmdArgs...) if err != nil { - return fmt.Errorf("Failed to update kubeadmin password %v: %s", err, stderr) + return fmt.Errorf("failed to update user passwords %v: %s", err, stderr) } return nil } -func GetKubeadminPassword() (string, error) { - kubeAdminPasswordFile := constants.GetKubeAdminPasswordPath() - rawData, err := os.ReadFile(kubeAdminPasswordFile) +func GetUserPassword(passwordFile string) (string, error) { + rawData, err := os.ReadFile(passwordFile) if err != nil { return "", err } @@ -192,3 +179,31 @@ func testBCryptPassword(password, hash string) (bool, error) { } return true, nil } + +func resolveUserPasswords(newKubeAdminPassword string, newDeveloperPassword string, kubeAdminPasswordPath string, developerPasswordPath string) (map[string]string, error) { + if newKubeAdminPassword != "" { + logging.Infof("Overriding password for kubeadmin user") + if err := os.WriteFile(kubeAdminPasswordPath, []byte(strings.TrimSpace(newKubeAdminPassword)), 0600); err != nil { + return nil, err + } + } + if newDeveloperPassword != "" { + logging.Infof("Overriding password for developer user") + if err := os.WriteFile(developerPasswordPath, []byte(strings.TrimSpace(newDeveloperPassword)), 0600); err != nil { + return nil, err + } + } + + kubeAdminPassword, err := GetUserPassword(kubeAdminPasswordPath) + if err != nil { + return nil, fmt.Errorf("cannot read the kubeadmin user password from file: %w", err) + } + developerPassword, err := GetUserPassword(developerPasswordPath) + if err != nil { + return nil, fmt.Errorf("cannot read the developer user password from file: %w", err) + } + return map[string]string{ + "developer": developerPassword, + "kubeadmin": kubeAdminPassword, + }, nil +} diff --git a/pkg/crc/cluster/kubeadmin_password_test.go b/pkg/crc/cluster/kubeadmin_password_test.go index 524d7be310..b484e27430 100644 --- a/pkg/crc/cluster/kubeadmin_password_test.go +++ b/pkg/crc/cluster/kubeadmin_password_test.go @@ -1,6 +1,8 @@ package cluster import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -56,3 +58,58 @@ func TestCompareFalseWithCustomEntries(t *testing.T) { assert.NoError(t, err) assert.True(t, ok) } + +func TestGenerateUserPassword_WhenValidFileProvided_ThenWritePasswordToFile(t *testing.T) { + // Given + dir := t.TempDir() + userPasswordPath := filepath.Join(dir, "test-user-password") + // When + err := GenerateUserPassword(userPasswordPath, "test-user") + // Then + assert.NoError(t, err) + actualPasswordFileContents, err := os.ReadFile(userPasswordPath) + assert.NoError(t, err) + assert.Equal(t, 23, len(actualPasswordFileContents)) +} + +var testResolveUserPasswordArguments = map[string]struct { + kubeAdminPasswordViaConfig string + developerPasswordViaConfig string + expectedKubeAdminPassword string + expectedDeveloperPassword string +}{ + "When no password configured in config, then read kubeadmin and developer passwords from password files": { + "", "", "kubeadmin-password-via-file", "developer-password-via-file", + }, + "When developer password configured in config, then use developer password from config": { + "", "developer-password-via-config", "kubeadmin-password-via-file", "developer-password-via-config", + }, + "When kube admin password configured in config, then use kube admin password from config": { + "kubeadmin-password-via-config", "", "kubeadmin-password-via-config", "developer-password-via-file", + }, + "When kube admin and developer password configured in config, then use kube admin and developer passwords from config": { + "kubeadmin-password-via-config", "developer-password-via-config", "kubeadmin-password-via-config", "developer-password-via-config", + }, +} + +func TestResolveUserPassword_WhenNothingProvided_ThenUsePasswordFromFiles(t *testing.T) { + for name, test := range testResolveUserPasswordArguments { + t.Run(name, func(t *testing.T) { + // Given + dir := t.TempDir() + kubeAdminPasswordPath := filepath.Join(dir, "kubeadmin-password") + err := os.WriteFile(kubeAdminPasswordPath, []byte("kubeadmin-password-via-file"), 0600) + assert.NoError(t, err) + developerPasswordPath := filepath.Join(dir, "developer-password") + err = os.WriteFile(developerPasswordPath, []byte("developer-password-via-file"), 0600) + assert.NoError(t, err) + + // When + credentials, err := resolveUserPasswords(test.kubeAdminPasswordViaConfig, test.developerPasswordViaConfig, kubeAdminPasswordPath, developerPasswordPath) + + // Then + assert.NoError(t, err) + assert.Equal(t, map[string]string{"developer": test.expectedDeveloperPassword, "kubeadmin": test.expectedKubeAdminPassword}, credentials) + }) + } +} diff --git a/pkg/crc/constants/constants.go b/pkg/crc/constants/constants.go index 383ec89c2d..c76afb6ee3 100644 --- a/pkg/crc/constants/constants.go +++ b/pkg/crc/constants/constants.go @@ -194,6 +194,10 @@ func GetKubeAdminPasswordPath() string { return filepath.Join(MachineInstanceDir, DefaultName, "kubeadmin-password") } +func GetDeveloperPasswordPath() string { + return filepath.Join(MachineInstanceDir, DefaultName, "developer-password") +} + func GetWin32BackgroundLauncherDownloadURL() string { return fmt.Sprintf(BackgroundLauncherURL, version.GetWin32BackgroundLauncherVersion()) diff --git a/pkg/crc/machine/fakemachine/client.go b/pkg/crc/machine/fakemachine/client.go index b10146954b..6d367ab673 100644 --- a/pkg/crc/machine/fakemachine/client.go +++ b/pkg/crc/machine/fakemachine/client.go @@ -29,6 +29,7 @@ var DummyClusterConfig = types.ClusterConfig{ ClusterCACert: "MIIDODCCAiCgAwIBAgIIRVfCKNUa1wIwDQYJ", KubeConfig: "/tmp/kubeconfig", KubeAdminPass: "foobar", + DeveloperPass: "foobar", ClusterAPI: "https://foo.testing:6443", WebConsoleURL: "https://console.foo.testing:6443", ProxyConfig: nil, diff --git a/pkg/crc/machine/machine.go b/pkg/crc/machine/machine.go index 4e8389f0cc..5c38f64c36 100644 --- a/pkg/crc/machine/machine.go +++ b/pkg/crc/machine/machine.go @@ -21,10 +21,14 @@ func getClusterConfig(bundleInfo *bundle.CrcBundleInfo) (*types.ClusterConfig, e }, nil } - kubeadminPassword, err := cluster.GetKubeadminPassword() + kubeadminPassword, err := cluster.GetUserPassword(constants.GetKubeAdminPasswordPath()) if err != nil { return nil, fmt.Errorf("Error reading kubeadmin password from bundle %v", err) } + developerPassword, err := cluster.GetUserPassword(constants.GetDeveloperPasswordPath()) + if err != nil { + return nil, fmt.Errorf("error reading developer password from bundle %v", err) + } proxyConfig, err := getProxyConfig(bundleInfo) if err != nil { return nil, err @@ -38,6 +42,7 @@ func getClusterConfig(bundleInfo *bundle.CrcBundleInfo) (*types.ClusterConfig, e ClusterCACert: base64.StdEncoding.EncodeToString(clusterCACert), KubeConfig: bundleInfo.GetKubeConfigPath(), KubeAdminPass: kubeadminPassword, + DeveloperPass: developerPassword, WebConsoleURL: fmt.Sprintf("https://%s", bundleInfo.GetAppHostname("console-openshift-console")), ClusterAPI: fmt.Sprintf("https://%s:6443", bundleInfo.GetAPIHostname()), ProxyConfig: proxyConfig, diff --git a/pkg/crc/machine/start.go b/pkg/crc/machine/start.go index 17eacb2c33..5eb8182f67 100644 --- a/pkg/crc/machine/start.go +++ b/pkg/crc/machine/start.go @@ -579,7 +579,7 @@ func (client *client) Start(ctx context.Context, startConfig types.StartConfig) return nil, errors.Wrap(err, "Failed to update pull secret on the disk") } - if err := cluster.UpdateKubeAdminUserPassword(ctx, ocConfig, startConfig.KubeAdminPassword); err != nil { + if err := cluster.UpdateUserPasswords(ctx, ocConfig, startConfig.KubeAdminPassword, startConfig.DeveloperPassword); err != nil { return nil, errors.Wrap(err, "Failed to update kubeadmin user password") } @@ -686,9 +686,12 @@ func createHost(machineConfig config.MachineConfig, preset crcPreset.Preset) err return fmt.Errorf("Error generating ssh key pair: %v", err) } if preset == crcPreset.OpenShift || preset == crcPreset.OKD { - if err := cluster.GenerateKubeAdminUserPassword(); err != nil { + if err := cluster.GenerateUserPassword(constants.GetKubeAdminPasswordPath(), "kubeadmin"); err != nil { return errors.Wrap(err, "Error generating new kubeadmin password") } + if err = os.WriteFile(constants.GetDeveloperPasswordPath(), []byte(constants.DefaultDeveloperPassword), 0600); err != nil { + return errors.Wrap(err, "Error writing developer password") + } } if err := api.SetExists(vm.Name); err != nil { return fmt.Errorf("Failed to record VM existence: %s", err) diff --git a/pkg/crc/machine/types/types.go b/pkg/crc/machine/types/types.go index 3b5e5442ec..ef810a7218 100644 --- a/pkg/crc/machine/types/types.go +++ b/pkg/crc/machine/types/types.go @@ -53,6 +53,7 @@ type ClusterConfig struct { ClusterCACert string KubeConfig string KubeAdminPass string + DeveloperPass string ClusterAPI string WebConsoleURL string ProxyConfig *httpproxy.ProxyConfig