From 36ef06a5417d9ec1e931a726eeefa1b628d7feda Mon Sep 17 00:00:00 2001 From: Salah Aldeen Al Saleh Date: Fri, 11 Jul 2025 11:49:49 -0700 Subject: [PATCH 1/2] Support the password config item type --- api/client/client.go | 2 + api/client/client_test.go | 140 ++++++ api/client/install.go | 70 +++ api/controllers/kubernetes/install/app.go | 16 +- .../kubernetes/install/controller.go | 4 +- .../kubernetes/install/controller_mock.go | 8 +- api/controllers/linux/install/app.go | 16 +- api/controllers/linux/install/controller.go | 4 +- .../linux/install/controller_mock.go | 8 +- api/docs/docs.go | 4 +- api/docs/swagger.json | 4 +- api/docs/swagger.yaml | 32 +- .../kubernetes/install/app_test.go | 14 +- api/integration/linux/install/app_test.go | 14 +- api/internal/handlers/kubernetes/install.go | 18 +- api/internal/handlers/linux/install.go | 18 +- api/internal/managers/app/config/config.go | 64 ++- .../managers/app/config/config_test.go | 289 ++++++++++- api/internal/managers/app/config/manager.go | 8 +- .../managers/app/config/manager_mock.go | 18 +- api/routes.go | 4 +- api/types/requests.go | 4 +- web/package-lock.json | 10 - web/src/components/common/Input.tsx | 3 + .../wizard/config/ConfigurationStep.tsx | 79 ++- .../wizard/tests/ConfigurationStep.test.tsx | 455 +++++++++++++++--- 26 files changed, 1102 insertions(+), 204 deletions(-) diff --git a/api/client/client.go b/api/client/client.go index 3e2a89c44..4e7820762 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -18,6 +18,7 @@ type Client interface { GetLinuxInfraStatus() (types.Infra, error) GetLinuxAppConfig() (types.AppConfig, error) GetLinuxAppConfigValues() (map[string]string, error) + PatchLinuxAppConfigValues(values map[string]string) (types.AppConfig, error) GetKubernetesInstallationConfig() (types.KubernetesInstallationConfig, error) ConfigureKubernetesInstallation(config types.KubernetesInstallationConfig) (types.Status, error) @@ -26,6 +27,7 @@ type Client interface { GetKubernetesInfraStatus() (types.Infra, error) GetKubernetesAppConfig() (types.AppConfig, error) GetKubernetesAppConfigValues() (map[string]string, error) + PatchKubernetesAppConfigValues(values map[string]string) (types.AppConfig, error) } type client struct { diff --git a/api/client/client_test.go b/api/client/client_test.go index 24c1ff1f5..ddea2d9c3 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -858,3 +858,143 @@ func TestKubernetesGetAppConfigValues(t *testing.T) { assert.Equal(t, http.StatusInternalServerError, apiErr.StatusCode) assert.Equal(t, "Internal Server Error", apiErr.Message) } + +func TestLinuxPatchAppConfigValues(t *testing.T) { + // Define expected config once + expectedConfig := types.AppConfig{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "test-group", + Title: "Test Group", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "test-item", + Type: "text", + Title: "Test Item", + Default: multitype.BoolOrString{StrVal: "default"}, + Value: multitype.BoolOrString{StrVal: "value"}, + }, + }, + }, + }, + } + + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/api/linux/install/app/config/values", r.URL.Path) + + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + + // Decode and verify request body + var req types.PatchAppConfigValuesRequest + err := json.NewDecoder(r.Body).Decode(&req) + require.NoError(t, err) + assert.Equal(t, "new-value", req.Values["test-item"]) + + // Return successful response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(expectedConfig) + })) + defer server.Close() + + // Test successful set + c := New(server.URL, WithToken("test-token")) + values := map[string]string{ + "test-item": "new-value", + } + config, err := c.PatchLinuxAppConfigValues(values) + assert.NoError(t, err) + assert.Equal(t, expectedConfig, config) + + // Test error response + errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(types.APIError{ + StatusCode: http.StatusInternalServerError, + Message: "Internal Server Error", + }) + })) + defer errorServer.Close() + + c = New(errorServer.URL, WithToken("test-token")) + config, err = c.PatchLinuxAppConfigValues(values) + assert.Error(t, err) + assert.Equal(t, types.AppConfig{}, config) + + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Expected err to be of type *types.APIError") + assert.Equal(t, http.StatusInternalServerError, apiErr.StatusCode) + assert.Equal(t, "Internal Server Error", apiErr.Message) +} + +func TestKubernetesPatchAppConfigValues(t *testing.T) { + // Define expected config once + expectedConfig := types.AppConfig{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "test-group", + Title: "Test Group", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "test-item", + Type: "text", + Title: "Test Item", + Default: multitype.BoolOrString{StrVal: "default"}, + Value: multitype.BoolOrString{StrVal: "value"}, + }, + }, + }, + }, + } + + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "/api/kubernetes/install/app/config/values", r.URL.Path) + + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + + // Decode and verify request body + var req types.PatchAppConfigValuesRequest + err := json.NewDecoder(r.Body).Decode(&req) + require.NoError(t, err) + assert.Equal(t, "new-value", req.Values["test-item"]) + + // Return successful response + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(expectedConfig) + })) + defer server.Close() + + // Test successful set + c := New(server.URL, WithToken("test-token")) + values := map[string]string{ + "test-item": "new-value", + } + config, err := c.PatchKubernetesAppConfigValues(values) + assert.NoError(t, err) + assert.Equal(t, expectedConfig, config) + + // Test error response + errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(types.APIError{ + StatusCode: http.StatusInternalServerError, + Message: "Internal Server Error", + }) + })) + defer errorServer.Close() + + c = New(errorServer.URL, WithToken("test-token")) + config, err = c.PatchKubernetesAppConfigValues(values) + assert.Error(t, err) + assert.Equal(t, types.AppConfig{}, config) + + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Expected err to be of type *types.APIError") + assert.Equal(t, http.StatusInternalServerError, apiErr.StatusCode) + assert.Equal(t, "Internal Server Error", apiErr.Message) +} diff --git a/api/client/install.go b/api/client/install.go index 7beb5c059..3a6eae483 100644 --- a/api/client/install.go +++ b/api/client/install.go @@ -349,6 +349,41 @@ func (c *client) GetLinuxAppConfigValues() (map[string]string, error) { return response.Values, nil } +func (c *client) PatchLinuxAppConfigValues(values map[string]string) (types.AppConfig, error) { + req := types.PatchAppConfigValuesRequest{ + Values: values, + } + b, err := json.Marshal(req) + if err != nil { + return types.AppConfig{}, err + } + + httpReq, err := http.NewRequest("PATCH", c.apiURL+"/api/linux/install/app/config/values", bytes.NewBuffer(b)) + if err != nil { + return types.AppConfig{}, err + } + httpReq.Header.Set("Content-Type", "application/json") + setAuthorizationHeader(httpReq, c.token) + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return types.AppConfig{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return types.AppConfig{}, errorFromResponse(resp) + } + + var config types.AppConfig + err = json.NewDecoder(resp.Body).Decode(&config) + if err != nil { + return types.AppConfig{}, err + } + + return config, nil +} + func (c *client) GetKubernetesAppConfig() (types.AppConfig, error) { req, err := http.NewRequest("GET", c.apiURL+"/api/kubernetes/install/app/config", nil) if err != nil { @@ -402,3 +437,38 @@ func (c *client) GetKubernetesAppConfigValues() (map[string]string, error) { return response.Values, nil } + +func (c *client) PatchKubernetesAppConfigValues(values map[string]string) (types.AppConfig, error) { + req := types.PatchAppConfigValuesRequest{ + Values: values, + } + b, err := json.Marshal(req) + if err != nil { + return types.AppConfig{}, err + } + + httpReq, err := http.NewRequest("PATCH", c.apiURL+"/api/kubernetes/install/app/config/values", bytes.NewBuffer(b)) + if err != nil { + return types.AppConfig{}, err + } + httpReq.Header.Set("Content-Type", "application/json") + setAuthorizationHeader(httpReq, c.token) + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return types.AppConfig{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return types.AppConfig{}, errorFromResponse(resp) + } + + var config types.AppConfig + err = json.NewDecoder(resp.Body).Decode(&config) + if err != nil { + return types.AppConfig{}, err + } + + return config, nil +} diff --git a/api/controllers/kubernetes/install/app.go b/api/controllers/kubernetes/install/app.go index 2a3c23b17..7d36d2a4c 100644 --- a/api/controllers/kubernetes/install/app.go +++ b/api/controllers/kubernetes/install/app.go @@ -18,7 +18,7 @@ func (c *InstallController) GetAppConfig(ctx context.Context) (kotsv1beta1.Confi return c.appConfigManager.GetConfig(*c.releaseData.AppConfig) } -func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[string]string) (finalErr error) { +func (c *InstallController) PatchAppConfigValues(ctx context.Context, values map[string]string) (finalErr error) { if c.releaseData == nil || c.releaseData.AppConfig == nil { return errors.New("app config not found") } @@ -51,9 +51,9 @@ func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[s } }() - err = c.appConfigManager.SetConfigValues(*c.releaseData.AppConfig, values) + err = c.appConfigManager.PatchConfigValues(ctx, *c.releaseData.AppConfig, values) if err != nil { - return fmt.Errorf("set app config values: %w", err) + return fmt.Errorf("patch app config values: %w", err) } err = c.stateMachine.Transition(lock, StateApplicationConfigured) @@ -64,6 +64,12 @@ func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[s return nil } -func (c *InstallController) GetAppConfigValues(ctx context.Context) (map[string]string, error) { - return c.appConfigManager.GetConfigValues() +func (c *InstallController) GetAppConfigValues(ctx context.Context, maskPasswords bool) (map[string]string, error) { + // Get the app config to determine which fields are password type + appConfig, err := c.GetAppConfig(ctx) + if err != nil { + return nil, fmt.Errorf("get app config: %w", err) + } + + return c.appConfigManager.GetConfigValues(ctx, appConfig, maskPasswords) } diff --git a/api/controllers/kubernetes/install/controller.go b/api/controllers/kubernetes/install/controller.go index 1e0a3aa13..4e836eda6 100644 --- a/api/controllers/kubernetes/install/controller.go +++ b/api/controllers/kubernetes/install/controller.go @@ -29,8 +29,8 @@ type Controller interface { SetupInfra(ctx context.Context) error GetInfra(ctx context.Context) (types.Infra, error) GetAppConfig(ctx context.Context) (kotsv1beta1.Config, error) - SetAppConfigValues(ctx context.Context, values map[string]string) error - GetAppConfigValues(ctx context.Context) (map[string]string, error) + PatchAppConfigValues(ctx context.Context, values map[string]string) error + GetAppConfigValues(ctx context.Context, maskPasswords bool) (map[string]string, error) } var _ Controller = (*InstallController)(nil) diff --git a/api/controllers/kubernetes/install/controller_mock.go b/api/controllers/kubernetes/install/controller_mock.go index 7aef94dec..04680290e 100644 --- a/api/controllers/kubernetes/install/controller_mock.go +++ b/api/controllers/kubernetes/install/controller_mock.go @@ -60,15 +60,15 @@ func (m *MockController) GetAppConfig(ctx context.Context) (kotsv1beta1.Config, return args.Get(0).(kotsv1beta1.Config), args.Error(1) } -// SetAppConfigValues mocks the SetAppConfigValues method -func (m *MockController) SetAppConfigValues(ctx context.Context, values map[string]string) error { +// PatchAppConfigValues mocks the PatchAppConfigValues method +func (m *MockController) PatchAppConfigValues(ctx context.Context, values map[string]string) error { args := m.Called(ctx, values) return args.Error(0) } // GetAppConfigValues mocks the GetAppConfigValues method -func (m *MockController) GetAppConfigValues(ctx context.Context) (map[string]string, error) { - args := m.Called(ctx) +func (m *MockController) GetAppConfigValues(ctx context.Context, maskPasswords bool) (map[string]string, error) { + args := m.Called(ctx, maskPasswords) if args.Get(0) == nil { return nil, args.Error(1) } diff --git a/api/controllers/linux/install/app.go b/api/controllers/linux/install/app.go index 2a3c23b17..7d36d2a4c 100644 --- a/api/controllers/linux/install/app.go +++ b/api/controllers/linux/install/app.go @@ -18,7 +18,7 @@ func (c *InstallController) GetAppConfig(ctx context.Context) (kotsv1beta1.Confi return c.appConfigManager.GetConfig(*c.releaseData.AppConfig) } -func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[string]string) (finalErr error) { +func (c *InstallController) PatchAppConfigValues(ctx context.Context, values map[string]string) (finalErr error) { if c.releaseData == nil || c.releaseData.AppConfig == nil { return errors.New("app config not found") } @@ -51,9 +51,9 @@ func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[s } }() - err = c.appConfigManager.SetConfigValues(*c.releaseData.AppConfig, values) + err = c.appConfigManager.PatchConfigValues(ctx, *c.releaseData.AppConfig, values) if err != nil { - return fmt.Errorf("set app config values: %w", err) + return fmt.Errorf("patch app config values: %w", err) } err = c.stateMachine.Transition(lock, StateApplicationConfigured) @@ -64,6 +64,12 @@ func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[s return nil } -func (c *InstallController) GetAppConfigValues(ctx context.Context) (map[string]string, error) { - return c.appConfigManager.GetConfigValues() +func (c *InstallController) GetAppConfigValues(ctx context.Context, maskPasswords bool) (map[string]string, error) { + // Get the app config to determine which fields are password type + appConfig, err := c.GetAppConfig(ctx) + if err != nil { + return nil, fmt.Errorf("get app config: %w", err) + } + + return c.appConfigManager.GetConfigValues(ctx, appConfig, maskPasswords) } diff --git a/api/controllers/linux/install/controller.go b/api/controllers/linux/install/controller.go index 35f42c190..b11a68644 100644 --- a/api/controllers/linux/install/controller.go +++ b/api/controllers/linux/install/controller.go @@ -34,8 +34,8 @@ type Controller interface { SetupInfra(ctx context.Context, ignoreHostPreflights bool) error GetInfra(ctx context.Context) (types.Infra, error) GetAppConfig(ctx context.Context) (kotsv1beta1.Config, error) - SetAppConfigValues(ctx context.Context, values map[string]string) error - GetAppConfigValues(ctx context.Context) (map[string]string, error) + PatchAppConfigValues(ctx context.Context, values map[string]string) error + GetAppConfigValues(ctx context.Context, maskPasswords bool) (map[string]string, error) } type RunHostPreflightsOptions struct { diff --git a/api/controllers/linux/install/controller_mock.go b/api/controllers/linux/install/controller_mock.go index 37acf8253..9aba31122 100644 --- a/api/controllers/linux/install/controller_mock.go +++ b/api/controllers/linux/install/controller_mock.go @@ -93,15 +93,15 @@ func (m *MockController) GetAppConfig(ctx context.Context) (kotsv1beta1.Config, return args.Get(0).(kotsv1beta1.Config), args.Error(1) } -// SetAppConfigValues mocks the SetAppConfigValues method -func (m *MockController) SetAppConfigValues(ctx context.Context, values map[string]string) error { +// PatchAppConfigValues mocks the PatchAppConfigValues method +func (m *MockController) PatchAppConfigValues(ctx context.Context, values map[string]string) error { args := m.Called(ctx, values) return args.Error(0) } // GetAppConfigValues mocks the GetAppConfigValues method -func (m *MockController) GetAppConfigValues(ctx context.Context) (map[string]string, error) { - args := m.Called(ctx) +func (m *MockController) GetAppConfigValues(ctx context.Context, maskPasswords bool) (map[string]string, error) { + args := m.Called(ctx, maskPasswords) if args.Get(0) == nil { return nil, args.Error(1) } diff --git a/api/docs/docs.go b/api/docs/docs.go index 7806f1047..4d3b83af0 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -6,10 +6,10 @@ import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, - "components": {"schemas":{"github_com_replicatedhq_embedded-cluster_api_types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"github_com_replicatedhq_kotskinds_multitype.BoolOrString":{"type":"object"},"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AppConfig":{"properties":{"groups":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigGroup"},"type":"array","uniqueItems":false}},"type":"object"},"types.AppConfigValuesResponse":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.GetListAvailableNetworkInterfacesResponse":{"properties":{"networkInterfaces":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.KubernetesInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"noProxy":{"type":"string"}},"type":"object"},"types.LinuxInfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.LinuxInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.SetAppConfigValuesRequest":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"},"v1beta1.ConfigChildItem":{"properties":{"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"name":{"type":"string"},"recommended":{"type":"boolean"},"title":{"type":"string"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"}},"type":"object"},"v1beta1.ConfigGroup":{"properties":{"description":{"type":"string"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigItem"},"type":"array","uniqueItems":false},"name":{"type":"string"},"title":{"type":"string"},"when":{"type":"string"}},"type":"object"},"v1beta1.ConfigItem":{"properties":{"affix":{"type":"string"},"countByGroup":{"additionalProperties":{"type":"integer"},"type":"object"},"data":{"type":"string"},"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"error":{"type":"string"},"filename":{"type":"string"},"help_text":{"type":"string"},"hidden":{"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigChildItem"},"type":"array","uniqueItems":false},"minimumCount":{"type":"integer"},"multi_value":{"items":{"type":"string"},"type":"array","uniqueItems":false},"multiple":{"type":"boolean"},"name":{"type":"string"},"readonly":{"type":"boolean"},"recommended":{"type":"boolean"},"repeatable":{"type":"boolean"},"required":{"type":"boolean"},"templates":{"items":{"$ref":"#/components/schemas/v1beta1.RepeatTemplate"},"type":"array","uniqueItems":false},"title":{"type":"string"},"type":{"type":"string"},"validation":{"$ref":"#/components/schemas/v1beta1.ConfigItemValidation"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"valuesByGroup":{"$ref":"#/components/schemas/v1beta1.ValuesByGroup"},"when":{"type":"string"},"write_once":{"type":"boolean"}},"type":"object"},"v1beta1.ConfigItemValidation":{"properties":{"regex":{"$ref":"#/components/schemas/v1beta1.RegexValidator"}},"type":"object"},"v1beta1.GroupValues":{"additionalProperties":{"type":"string"},"type":"object"},"v1beta1.RegexValidator":{"properties":{"message":{"type":"string"},"pattern":{"type":"string"}},"type":"object"},"v1beta1.RepeatTemplate":{"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"namespace":{"type":"string"},"yamlPath":{"type":"string"}},"type":"object"},"v1beta1.ValuesByGroup":{"additionalProperties":{"$ref":"#/components/schemas/v1beta1.GroupValues"},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"github_com_replicatedhq_embedded-cluster_api_types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"github_com_replicatedhq_kotskinds_multitype.BoolOrString":{"type":"object"},"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AppConfig":{"properties":{"groups":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigGroup"},"type":"array","uniqueItems":false}},"type":"object"},"types.AppConfigValuesResponse":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.GetListAvailableNetworkInterfacesResponse":{"properties":{"networkInterfaces":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.KubernetesInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"noProxy":{"type":"string"}},"type":"object"},"types.LinuxInfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.LinuxInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PatchAppConfigValuesRequest":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"},"v1beta1.ConfigChildItem":{"properties":{"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"name":{"type":"string"},"recommended":{"type":"boolean"},"title":{"type":"string"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"}},"type":"object"},"v1beta1.ConfigGroup":{"properties":{"description":{"type":"string"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigItem"},"type":"array","uniqueItems":false},"name":{"type":"string"},"title":{"type":"string"},"when":{"type":"string"}},"type":"object"},"v1beta1.ConfigItem":{"properties":{"affix":{"type":"string"},"countByGroup":{"additionalProperties":{"type":"integer"},"type":"object"},"data":{"type":"string"},"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"error":{"type":"string"},"filename":{"type":"string"},"help_text":{"type":"string"},"hidden":{"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigChildItem"},"type":"array","uniqueItems":false},"minimumCount":{"type":"integer"},"multi_value":{"items":{"type":"string"},"type":"array","uniqueItems":false},"multiple":{"type":"boolean"},"name":{"type":"string"},"readonly":{"type":"boolean"},"recommended":{"type":"boolean"},"repeatable":{"type":"boolean"},"required":{"type":"boolean"},"templates":{"items":{"$ref":"#/components/schemas/v1beta1.RepeatTemplate"},"type":"array","uniqueItems":false},"title":{"type":"string"},"type":{"type":"string"},"validation":{"$ref":"#/components/schemas/v1beta1.ConfigItemValidation"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"valuesByGroup":{"$ref":"#/components/schemas/v1beta1.ValuesByGroup"},"when":{"type":"string"},"write_once":{"type":"boolean"}},"type":"object"},"v1beta1.ConfigItemValidation":{"properties":{"regex":{"$ref":"#/components/schemas/v1beta1.RegexValidator"}},"type":"object"},"v1beta1.GroupValues":{"additionalProperties":{"type":"string"},"type":"object"},"v1beta1.RegexValidator":{"properties":{"message":{"type":"string"},"pattern":{"type":"string"}},"type":"object"},"v1beta1.RepeatTemplate":{"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"namespace":{"type":"string"},"yamlPath":{"type":"string"}},"type":"object"},"v1beta1.ValuesByGroup":{"additionalProperties":{"$ref":"#/components/schemas/v1beta1.GroupValues"},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"{{escape .Description}}","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"{{.Title}}","version":"{{.Version}}"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, - "paths": {"/auth/login":{"post":{"description":"Authenticate a user","operationId":"postAuthLogin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/console/available-network-interfaces":{"get":{"description":"List available network interfaces","operationId":"getConsoleListAvailableNetworkInterfaces","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.GetListAvailableNetworkInterfacesResponse"}}},"description":"OK"}},"summary":"List available network interfaces","tags":["console"]}},"/health":{"get":{"description":"get the health of the API","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/github_com_replicatedhq_embedded-cluster_api_types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/kubernetes/install/app/config":{"get":{"description":"get the app config","operationId":"getKubernetesInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["kubernetes-install"]}},"/kubernetes/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getKubernetesInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["kubernetes-install"]},"post":{"description":"Set the app config values","operationId":"postKubernetesInstallSetAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.SetAppConfigValuesRequest"}}},"description":"Set App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["kubernetes-install"]}},"/kubernetes/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postKubernetesInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["kubernetes-install"]}},"/kubernetes/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getKubernetesInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["kubernetes-install"]}},"/kubernetes/install/installation/config":{"get":{"description":"get the Kubernetes installation config","operationId":"getKubernetesInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the Kubernetes installation config","tags":["kubernetes-install"]}},"/kubernetes/install/installation/configure":{"post":{"description":"configure the Kubernetes installation for install","operationId":"postKubernetesInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the Kubernetes installation for install","tags":["kubernetes-install"]}},"/kubernetes/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getKubernetesInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["kubernetes-install"]}},"/linux/install/app/config":{"get":{"description":"get the app config","operationId":"getLinuxInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["linux-install"]}},"/linux/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getLinuxInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["linux-install"]},"post":{"description":"Set the app config values","operationId":"postLinuxInstallSetAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.SetAppConfigValuesRequest"}}},"description":"Set App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["linux-install"]}},"/linux/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","operationId":"postLinuxInstallRunHostPreflights","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["linux-install"]}},"/linux/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","operationId":"getLinuxInstallHostPreflightsStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["linux-install"]}},"/linux/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postLinuxInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["linux-install"]}},"/linux/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getLinuxInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["linux-install"]}},"/linux/install/installation/config":{"get":{"description":"get the installation config","operationId":"getLinuxInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["linux-install"]}},"/linux/install/installation/configure":{"post":{"description":"configure the installation for install","operationId":"postLinuxInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["linux-install"]}},"/linux/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getLinuxInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["linux-install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","operationId":"postAuthLogin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/console/available-network-interfaces":{"get":{"description":"List available network interfaces","operationId":"getConsoleListAvailableNetworkInterfaces","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.GetListAvailableNetworkInterfacesResponse"}}},"description":"OK"}},"summary":"List available network interfaces","tags":["console"]}},"/health":{"get":{"description":"get the health of the API","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/github_com_replicatedhq_embedded-cluster_api_types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/kubernetes/install/app/config":{"get":{"description":"get the app config","operationId":"getKubernetesInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["kubernetes-install"]}},"/kubernetes/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getKubernetesInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["kubernetes-install"]},"patch":{"description":"Set the app config values with partial updates","operationId":"patchKubernetesInstallAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PatchAppConfigValuesRequest"}}},"description":"Patch App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["kubernetes-install"]}},"/kubernetes/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postKubernetesInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["kubernetes-install"]}},"/kubernetes/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getKubernetesInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["kubernetes-install"]}},"/kubernetes/install/installation/config":{"get":{"description":"get the Kubernetes installation config","operationId":"getKubernetesInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the Kubernetes installation config","tags":["kubernetes-install"]}},"/kubernetes/install/installation/configure":{"post":{"description":"configure the Kubernetes installation for install","operationId":"postKubernetesInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the Kubernetes installation for install","tags":["kubernetes-install"]}},"/kubernetes/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getKubernetesInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["kubernetes-install"]}},"/linux/install/app/config":{"get":{"description":"get the app config","operationId":"getLinuxInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["linux-install"]}},"/linux/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getLinuxInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["linux-install"]},"patch":{"description":"Set the app config values with partial updates","operationId":"patchLinuxInstallAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PatchAppConfigValuesRequest"}}},"description":"Patch App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["linux-install"]}},"/linux/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","operationId":"postLinuxInstallRunHostPreflights","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["linux-install"]}},"/linux/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","operationId":"getLinuxInstallHostPreflightsStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["linux-install"]}},"/linux/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postLinuxInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["linux-install"]}},"/linux/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getLinuxInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["linux-install"]}},"/linux/install/installation/config":{"get":{"description":"get the installation config","operationId":"getLinuxInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["linux-install"]}},"/linux/install/installation/configure":{"post":{"description":"configure the installation for install","operationId":"postLinuxInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["linux-install"]}},"/linux/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getLinuxInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["linux-install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.json b/api/docs/swagger.json index daff677b9..1071d7f7a 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1,8 +1,8 @@ { - "components": {"schemas":{"github_com_replicatedhq_embedded-cluster_api_types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"github_com_replicatedhq_kotskinds_multitype.BoolOrString":{"type":"object"},"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AppConfig":{"properties":{"groups":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigGroup"},"type":"array","uniqueItems":false}},"type":"object"},"types.AppConfigValuesResponse":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.GetListAvailableNetworkInterfacesResponse":{"properties":{"networkInterfaces":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.KubernetesInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"noProxy":{"type":"string"}},"type":"object"},"types.LinuxInfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.LinuxInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.SetAppConfigValuesRequest":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"},"v1beta1.ConfigChildItem":{"properties":{"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"name":{"type":"string"},"recommended":{"type":"boolean"},"title":{"type":"string"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"}},"type":"object"},"v1beta1.ConfigGroup":{"properties":{"description":{"type":"string"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigItem"},"type":"array","uniqueItems":false},"name":{"type":"string"},"title":{"type":"string"},"when":{"type":"string"}},"type":"object"},"v1beta1.ConfigItem":{"properties":{"affix":{"type":"string"},"countByGroup":{"additionalProperties":{"type":"integer"},"type":"object"},"data":{"type":"string"},"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"error":{"type":"string"},"filename":{"type":"string"},"help_text":{"type":"string"},"hidden":{"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigChildItem"},"type":"array","uniqueItems":false},"minimumCount":{"type":"integer"},"multi_value":{"items":{"type":"string"},"type":"array","uniqueItems":false},"multiple":{"type":"boolean"},"name":{"type":"string"},"readonly":{"type":"boolean"},"recommended":{"type":"boolean"},"repeatable":{"type":"boolean"},"required":{"type":"boolean"},"templates":{"items":{"$ref":"#/components/schemas/v1beta1.RepeatTemplate"},"type":"array","uniqueItems":false},"title":{"type":"string"},"type":{"type":"string"},"validation":{"$ref":"#/components/schemas/v1beta1.ConfigItemValidation"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"valuesByGroup":{"$ref":"#/components/schemas/v1beta1.ValuesByGroup"},"when":{"type":"string"},"write_once":{"type":"boolean"}},"type":"object"},"v1beta1.ConfigItemValidation":{"properties":{"regex":{"$ref":"#/components/schemas/v1beta1.RegexValidator"}},"type":"object"},"v1beta1.GroupValues":{"additionalProperties":{"type":"string"},"type":"object"},"v1beta1.RegexValidator":{"properties":{"message":{"type":"string"},"pattern":{"type":"string"}},"type":"object"},"v1beta1.RepeatTemplate":{"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"namespace":{"type":"string"},"yamlPath":{"type":"string"}},"type":"object"},"v1beta1.ValuesByGroup":{"additionalProperties":{"$ref":"#/components/schemas/v1beta1.GroupValues"},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"github_com_replicatedhq_embedded-cluster_api_types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"github_com_replicatedhq_kotskinds_multitype.BoolOrString":{"type":"object"},"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AppConfig":{"properties":{"groups":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigGroup"},"type":"array","uniqueItems":false}},"type":"object"},"types.AppConfigValuesResponse":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.GetListAvailableNetworkInterfacesResponse":{"properties":{"networkInterfaces":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.KubernetesInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"noProxy":{"type":"string"}},"type":"object"},"types.LinuxInfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.LinuxInstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PatchAppConfigValuesRequest":{"properties":{"values":{"additionalProperties":{"type":"string"},"type":"object"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"},"v1beta1.ConfigChildItem":{"properties":{"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"name":{"type":"string"},"recommended":{"type":"boolean"},"title":{"type":"string"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"}},"type":"object"},"v1beta1.ConfigGroup":{"properties":{"description":{"type":"string"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigItem"},"type":"array","uniqueItems":false},"name":{"type":"string"},"title":{"type":"string"},"when":{"type":"string"}},"type":"object"},"v1beta1.ConfigItem":{"properties":{"affix":{"type":"string"},"countByGroup":{"additionalProperties":{"type":"integer"},"type":"object"},"data":{"type":"string"},"default":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"error":{"type":"string"},"filename":{"type":"string"},"help_text":{"type":"string"},"hidden":{"type":"boolean"},"items":{"items":{"$ref":"#/components/schemas/v1beta1.ConfigChildItem"},"type":"array","uniqueItems":false},"minimumCount":{"type":"integer"},"multi_value":{"items":{"type":"string"},"type":"array","uniqueItems":false},"multiple":{"type":"boolean"},"name":{"type":"string"},"readonly":{"type":"boolean"},"recommended":{"type":"boolean"},"repeatable":{"type":"boolean"},"required":{"type":"boolean"},"templates":{"items":{"$ref":"#/components/schemas/v1beta1.RepeatTemplate"},"type":"array","uniqueItems":false},"title":{"type":"string"},"type":{"type":"string"},"validation":{"$ref":"#/components/schemas/v1beta1.ConfigItemValidation"},"value":{"$ref":"#/components/schemas/github_com_replicatedhq_kotskinds_multitype.BoolOrString"},"valuesByGroup":{"$ref":"#/components/schemas/v1beta1.ValuesByGroup"},"when":{"type":"string"},"write_once":{"type":"boolean"}},"type":"object"},"v1beta1.ConfigItemValidation":{"properties":{"regex":{"$ref":"#/components/schemas/v1beta1.RegexValidator"}},"type":"object"},"v1beta1.GroupValues":{"additionalProperties":{"type":"string"},"type":"object"},"v1beta1.RegexValidator":{"properties":{"message":{"type":"string"},"pattern":{"type":"string"}},"type":"object"},"v1beta1.RepeatTemplate":{"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"namespace":{"type":"string"},"yamlPath":{"type":"string"}},"type":"object"},"v1beta1.ValuesByGroup":{"additionalProperties":{"$ref":"#/components/schemas/v1beta1.GroupValues"},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"This is the API for the Embedded Cluster project.","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"Embedded Cluster API","version":"0.1"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, - "paths": {"/auth/login":{"post":{"description":"Authenticate a user","operationId":"postAuthLogin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/console/available-network-interfaces":{"get":{"description":"List available network interfaces","operationId":"getConsoleListAvailableNetworkInterfaces","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.GetListAvailableNetworkInterfacesResponse"}}},"description":"OK"}},"summary":"List available network interfaces","tags":["console"]}},"/health":{"get":{"description":"get the health of the API","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/github_com_replicatedhq_embedded-cluster_api_types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/kubernetes/install/app/config":{"get":{"description":"get the app config","operationId":"getKubernetesInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["kubernetes-install"]}},"/kubernetes/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getKubernetesInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["kubernetes-install"]},"post":{"description":"Set the app config values","operationId":"postKubernetesInstallSetAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.SetAppConfigValuesRequest"}}},"description":"Set App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["kubernetes-install"]}},"/kubernetes/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postKubernetesInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["kubernetes-install"]}},"/kubernetes/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getKubernetesInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["kubernetes-install"]}},"/kubernetes/install/installation/config":{"get":{"description":"get the Kubernetes installation config","operationId":"getKubernetesInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the Kubernetes installation config","tags":["kubernetes-install"]}},"/kubernetes/install/installation/configure":{"post":{"description":"configure the Kubernetes installation for install","operationId":"postKubernetesInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the Kubernetes installation for install","tags":["kubernetes-install"]}},"/kubernetes/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getKubernetesInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["kubernetes-install"]}},"/linux/install/app/config":{"get":{"description":"get the app config","operationId":"getLinuxInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["linux-install"]}},"/linux/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getLinuxInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["linux-install"]},"post":{"description":"Set the app config values","operationId":"postLinuxInstallSetAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.SetAppConfigValuesRequest"}}},"description":"Set App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["linux-install"]}},"/linux/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","operationId":"postLinuxInstallRunHostPreflights","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["linux-install"]}},"/linux/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","operationId":"getLinuxInstallHostPreflightsStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["linux-install"]}},"/linux/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postLinuxInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["linux-install"]}},"/linux/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getLinuxInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["linux-install"]}},"/linux/install/installation/config":{"get":{"description":"get the installation config","operationId":"getLinuxInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["linux-install"]}},"/linux/install/installation/configure":{"post":{"description":"configure the installation for install","operationId":"postLinuxInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["linux-install"]}},"/linux/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getLinuxInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["linux-install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","operationId":"postAuthLogin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/console/available-network-interfaces":{"get":{"description":"List available network interfaces","operationId":"getConsoleListAvailableNetworkInterfaces","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.GetListAvailableNetworkInterfacesResponse"}}},"description":"OK"}},"summary":"List available network interfaces","tags":["console"]}},"/health":{"get":{"description":"get the health of the API","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/github_com_replicatedhq_embedded-cluster_api_types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/kubernetes/install/app/config":{"get":{"description":"get the app config","operationId":"getKubernetesInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["kubernetes-install"]}},"/kubernetes/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getKubernetesInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["kubernetes-install"]},"patch":{"description":"Set the app config values with partial updates","operationId":"patchKubernetesInstallAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PatchAppConfigValuesRequest"}}},"description":"Patch App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["kubernetes-install"]}},"/kubernetes/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postKubernetesInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["kubernetes-install"]}},"/kubernetes/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getKubernetesInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["kubernetes-install"]}},"/kubernetes/install/installation/config":{"get":{"description":"get the Kubernetes installation config","operationId":"getKubernetesInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the Kubernetes installation config","tags":["kubernetes-install"]}},"/kubernetes/install/installation/configure":{"post":{"description":"configure the Kubernetes installation for install","operationId":"postKubernetesInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.KubernetesInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the Kubernetes installation for install","tags":["kubernetes-install"]}},"/kubernetes/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getKubernetesInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["kubernetes-install"]}},"/linux/install/app/config":{"get":{"description":"get the app config","operationId":"getLinuxInstallAppConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config","tags":["linux-install"]}},"/linux/install/app/config/values":{"get":{"description":"Get the app config values","operationId":"getLinuxInstallAppConfigValues","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfigValuesResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the app config values","tags":["linux-install"]},"patch":{"description":"Set the app config values with partial updates","operationId":"patchLinuxInstallAppConfigValues","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PatchAppConfigValuesRequest"}}},"description":"Patch App Config Values Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AppConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the app config values","tags":["linux-install"]}},"/linux/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","operationId":"postLinuxInstallRunHostPreflights","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["linux-install"]}},"/linux/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","operationId":"getLinuxInstallHostPreflightsStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["linux-install"]}},"/linux/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postLinuxInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["linux-install"]}},"/linux/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getLinuxInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["linux-install"]}},"/linux/install/installation/config":{"get":{"description":"get the installation config","operationId":"getLinuxInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["linux-install"]}},"/linux/install/installation/configure":{"post":{"description":"configure the installation for install","operationId":"postLinuxInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.LinuxInstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["linux-install"]}},"/linux/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getLinuxInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["linux-install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index d012669b4..675469387 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -151,18 +151,18 @@ components: serviceCidr: type: string type: object - types.PostInstallRunHostPreflightsRequest: - properties: - isUi: - type: boolean - type: object - types.SetAppConfigValuesRequest: + types.PatchAppConfigValuesRequest: properties: values: additionalProperties: type: string type: object type: object + types.PostInstallRunHostPreflightsRequest: + properties: + isUi: + type: boolean + type: object types.State: type: string x-enum-varnames: @@ -414,15 +414,15 @@ paths: summary: Get the app config values tags: - kubernetes-install - post: - description: Set the app config values - operationId: postKubernetesInstallSetAppConfigValues + patch: + description: Set the app config values with partial updates + operationId: patchKubernetesInstallAppConfigValues requestBody: content: application/json: schema: - $ref: '#/components/schemas/types.SetAppConfigValuesRequest' - description: Set App Config Values Request + $ref: '#/components/schemas/types.PatchAppConfigValuesRequest' + description: Patch App Config Values Request required: true responses: "200": @@ -560,15 +560,15 @@ paths: summary: Get the app config values tags: - linux-install - post: - description: Set the app config values - operationId: postLinuxInstallSetAppConfigValues + patch: + description: Set the app config values with partial updates + operationId: patchLinuxInstallAppConfigValues requestBody: content: application/json: schema: - $ref: '#/components/schemas/types.SetAppConfigValuesRequest' - description: Set App Config Values Request + $ref: '#/components/schemas/types.PatchAppConfigValuesRequest' + description: Patch App Config Values Request required: true responses: "200": diff --git a/api/integration/kubernetes/install/app_test.go b/api/integration/kubernetes/install/app_test.go index 7d5761a03..dcc58ebd0 100644 --- a/api/integration/kubernetes/install/app_test.go +++ b/api/integration/kubernetes/install/app_test.go @@ -120,7 +120,7 @@ func TestKubernetesGetAppConfig(t *testing.T) { }) } -func TestKubernetesSetAppConfigValues(t *testing.T) { +func TestKubernetesPatchAppConfigValues(t *testing.T) { // Create an app config appConfig := kotsv1beta1.Config{ Spec: kotsv1beta1.ConfigSpec{ @@ -175,7 +175,7 @@ func TestKubernetesSetAppConfigValues(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request to set config values - setRequest := types.SetAppConfigValuesRequest{ + setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ "test-item": "new-value", }, @@ -185,7 +185,7 @@ func TestKubernetesSetAppConfigValues(t *testing.T) { require.NoError(t, err) // Create a request to set config values - req := httptest.NewRequest(http.MethodPost, "/kubernetes/install/app/config/values", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPatch, "/kubernetes/install/app/config/values", bytes.NewReader(reqBodyBytes)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -233,7 +233,7 @@ func TestKubernetesSetAppConfigValues(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request to set config values - setRequest := types.SetAppConfigValuesRequest{ + setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ "test-item": "new-value", }, @@ -243,7 +243,7 @@ func TestKubernetesSetAppConfigValues(t *testing.T) { require.NoError(t, err) // Create a request with invalid token - req := httptest.NewRequest(http.MethodPost, "/kubernetes/install/app/config/values", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPatch, "/kubernetes/install/app/config/values", bytes.NewReader(reqBodyBytes)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"NOT_A_TOKEN") rec := httptest.NewRecorder() @@ -289,7 +289,7 @@ func TestKubernetesSetAppConfigValues(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request to set config values - setRequest := types.SetAppConfigValuesRequest{ + setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ "test-item": "new-value", }, @@ -299,7 +299,7 @@ func TestKubernetesSetAppConfigValues(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/kubernetes/install/app/config/values", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPatch, "/kubernetes/install/app/config/values", bytes.NewReader(reqBodyBytes)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() diff --git a/api/integration/linux/install/app_test.go b/api/integration/linux/install/app_test.go index 0e9d8b38d..23132353c 100644 --- a/api/integration/linux/install/app_test.go +++ b/api/integration/linux/install/app_test.go @@ -117,7 +117,7 @@ func TestLinuxGetAppConfig(t *testing.T) { }) } -func TestLinuxSetAppConfigValues(t *testing.T) { +func TestLinuxPatchAppConfigValues(t *testing.T) { // Create an app config appConfig := kotsv1beta1.Config{ Spec: kotsv1beta1.ConfigSpec{ @@ -173,7 +173,7 @@ func TestLinuxSetAppConfigValues(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request to set config values - setRequest := types.SetAppConfigValuesRequest{ + setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ "test-item": "new-value", }, @@ -183,7 +183,7 @@ func TestLinuxSetAppConfigValues(t *testing.T) { require.NoError(t, err) // Create a request to set config values - req := httptest.NewRequest(http.MethodPost, "/linux/install/app/config/values", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPatch, "/linux/install/app/config/values", bytes.NewReader(reqBodyBytes)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -232,7 +232,7 @@ func TestLinuxSetAppConfigValues(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request to set config values - setRequest := types.SetAppConfigValuesRequest{ + setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ "test-item": "new-value", }, @@ -242,7 +242,7 @@ func TestLinuxSetAppConfigValues(t *testing.T) { require.NoError(t, err) // Create a request with invalid token - req := httptest.NewRequest(http.MethodPost, "/linux/install/app/config/values", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPatch, "/linux/install/app/config/values", bytes.NewReader(reqBodyBytes)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"NOT_A_TOKEN") rec := httptest.NewRecorder() @@ -288,7 +288,7 @@ func TestLinuxSetAppConfigValues(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request to set config values - setRequest := types.SetAppConfigValuesRequest{ + setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ "test-item": "new-value", }, @@ -298,7 +298,7 @@ func TestLinuxSetAppConfigValues(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/linux/install/app/config/values", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPatch, "/linux/install/app/config/values", bytes.NewReader(reqBodyBytes)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() diff --git a/api/internal/handlers/kubernetes/install.go b/api/internal/handlers/kubernetes/install.go index 6c6333eac..12e9dd0e0 100644 --- a/api/internal/handlers/kubernetes/install.go +++ b/api/internal/handlers/kubernetes/install.go @@ -140,25 +140,25 @@ func (h *Handler) GetAppConfig(w http.ResponseWriter, r *http.Request) { utils.JSON(w, r, http.StatusOK, types.AppConfig(appConfig.Spec), h.logger) } -// PostSetAppConfigValues handler to set the app config values +// PatchConfigValues handler to set the app config values // -// @ID postKubernetesInstallSetAppConfigValues +// @ID patchKubernetesInstallAppConfigValues // @Summary Set the app config values -// @Description Set the app config values +// @Description Set the app config values with partial updates // @Tags kubernetes-install // @Security bearerauth // @Accept json // @Produce json -// @Param request body types.SetAppConfigValuesRequest true "Set App Config Values Request" +// @Param request body types.PatchAppConfigValuesRequest true "Patch App Config Values Request" // @Success 200 {object} types.AppConfig -// @Router /kubernetes/install/app/config/values [post] -func (h *Handler) PostSetAppConfigValues(w http.ResponseWriter, r *http.Request) { - var req types.SetAppConfigValuesRequest +// @Router /kubernetes/install/app/config/values [patch] +func (h *Handler) PatchConfigValues(w http.ResponseWriter, r *http.Request) { + var req types.PatchAppConfigValuesRequest if err := utils.BindJSON(w, r, &req, h.logger); err != nil { return } - err := h.installController.SetAppConfigValues(r.Context(), req.Values) + err := h.installController.PatchAppConfigValues(r.Context(), req.Values) if err != nil { utils.LogError(r, err, h.logger, "failed to set app config values") utils.JSONError(w, r, err, h.logger) @@ -179,7 +179,7 @@ func (h *Handler) PostSetAppConfigValues(w http.ResponseWriter, r *http.Request) // @Success 200 {object} types.AppConfigValuesResponse // @Router /kubernetes/install/app/config/values [get] func (h *Handler) GetAppConfigValues(w http.ResponseWriter, r *http.Request) { - configValues, err := h.installController.GetAppConfigValues(r.Context()) + configValues, err := h.installController.GetAppConfigValues(r.Context(), true) if err != nil { utils.LogError(r, err, h.logger, "failed to get app config values") utils.JSONError(w, r, err, h.logger) diff --git a/api/internal/handlers/linux/install.go b/api/internal/handlers/linux/install.go index ae6731391..d3cfdb2e6 100644 --- a/api/internal/handlers/linux/install.go +++ b/api/internal/handlers/linux/install.go @@ -219,25 +219,25 @@ func (h *Handler) GetAppConfig(w http.ResponseWriter, r *http.Request) { utils.JSON(w, r, http.StatusOK, types.AppConfig(appConfig.Spec), h.logger) } -// PostSetAppConfigValues handler to set the app config values +// PatchConfigValues handler to set the app config values // -// @ID postLinuxInstallSetAppConfigValues +// @ID patchLinuxInstallAppConfigValues // @Summary Set the app config values -// @Description Set the app config values +// @Description Set the app config values with partial updates // @Tags linux-install // @Security bearerauth // @Accept json // @Produce json -// @Param request body types.SetAppConfigValuesRequest true "Set App Config Values Request" +// @Param request body types.PatchAppConfigValuesRequest true "Patch App Config Values Request" // @Success 200 {object} types.AppConfig -// @Router /linux/install/app/config/values [post] -func (h *Handler) PostSetAppConfigValues(w http.ResponseWriter, r *http.Request) { - var req types.SetAppConfigValuesRequest +// @Router /linux/install/app/config/values [patch] +func (h *Handler) PatchConfigValues(w http.ResponseWriter, r *http.Request) { + var req types.PatchAppConfigValuesRequest if err := utils.BindJSON(w, r, &req, h.logger); err != nil { return } - err := h.installController.SetAppConfigValues(r.Context(), req.Values) + err := h.installController.PatchAppConfigValues(r.Context(), req.Values) if err != nil { utils.LogError(r, err, h.logger, "failed to set app config values") utils.JSONError(w, r, err, h.logger) @@ -258,7 +258,7 @@ func (h *Handler) PostSetAppConfigValues(w http.ResponseWriter, r *http.Request) // @Success 200 {object} types.AppConfigValuesResponse // @Router /linux/install/app/config/values [get] func (h *Handler) GetAppConfigValues(w http.ResponseWriter, r *http.Request) { - configValues, err := h.installController.GetAppConfigValues(r.Context()) + configValues, err := h.installController.GetAppConfigValues(r.Context(), true) if err != nil { utils.LogError(r, err, h.logger, "failed to get app config values") utils.JSONError(w, r, err, h.logger) diff --git a/api/internal/managers/app/config/config.go b/api/internal/managers/app/config/config.go index 0884bf05e..326c34087 100644 --- a/api/internal/managers/app/config/config.go +++ b/api/internal/managers/app/config/config.go @@ -1,7 +1,9 @@ package config import ( + "context" "fmt" + "maps" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" @@ -9,27 +11,39 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // PasswordMask is the string used to mask password values in config responses + PasswordMask = "••••••••" +) + func (m *appConfigManager) GetConfig(config kotsv1beta1.Config) (kotsv1beta1.Config, error) { return filterAppConfig(config) } -func (m *appConfigManager) GetConfigValues() (map[string]string, error) { - return m.appConfigStore.GetConfigValues() -} +// PatchConfigValues performs a partial update by merging new values with existing ones +func (m *appConfigManager) PatchConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, newValues map[string]string) error { + // Get existing values + existingValues, err := m.appConfigStore.GetConfigValues() + if err != nil { + return fmt.Errorf("get config values: %w", err) + } -func (m *appConfigManager) SetConfigValues(config kotsv1beta1.Config, configValues map[string]string) error { - filteredValues := make(map[string]string) + // Merge new values with existing ones + mergedValues := make(map[string]string) + maps.Copy(mergedValues, existingValues) + maps.Copy(mergedValues, newValues) - // only include values for enabled groups and items - for _, g := range config.Spec.Groups { + // only keep values for enabled groups and items + filteredValues := make(map[string]string) + for _, g := range appConfig.Spec.Groups { for _, i := range g.Items { if isItemEnabled(g.When) && isItemEnabled(i.When) { - value, ok := configValues[i.Name] + value, ok := mergedValues[i.Name] if ok { filteredValues[i.Name] = value } for _, c := range i.Items { - value, ok := configValues[c.Name] + value, ok := mergedValues[c.Name] if ok { filteredValues[c.Name] = value } @@ -41,6 +55,38 @@ func (m *appConfigManager) SetConfigValues(config kotsv1beta1.Config, configValu return m.appConfigStore.SetConfigValues(filteredValues) } +// GetConfigValues returns config values with optional password field masking +func (m *appConfigManager) GetConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { + configValues, err := m.appConfigStore.GetConfigValues() + if err != nil { + return nil, err + } + + // If masking is not requested, return the original values + if !maskPasswords { + return configValues, nil + } + + // Create a copy of the config values to mask password fields + maskedValues := make(map[string]string) + for key, value := range configValues { + maskedValues[key] = value + } + + // Mask password fields + for _, group := range appConfig.Spec.Groups { + for _, item := range group.Items { + if item.Type == "password" { + if _, exists := maskedValues[item.Name]; exists { + maskedValues[item.Name] = PasswordMask + } + } + } + } + + return maskedValues, nil +} + func (m *appConfigManager) GetKotsadmConfigValues(config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { filteredConfig, err := m.GetConfig(config) if err != nil { diff --git a/api/internal/managers/app/config/config_test.go b/api/internal/managers/app/config/config_test.go index a0f879863..cfeac59b8 100644 --- a/api/internal/managers/app/config/config_test.go +++ b/api/internal/managers/app/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "context" "errors" "testing" @@ -327,13 +328,13 @@ func TestAppConfigManager_GetConfig(t *testing.T) { } } -func TestAppConfigManager_SetConfigValues(t *testing.T) { +func TestAppConfigManager_PatchConfigValues(t *testing.T) { tests := []struct { - name string - config kotsv1beta1.Config - configValues map[string]string - setupMock func(*config.MockStore) - wantErr bool + name string + config kotsv1beta1.Config + newValues map[string]string + setupMock func(*config.MockStore) + wantErr bool }{ { name: "enabled group and items with new values", @@ -371,12 +372,13 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "enabled-item-1": "new-value-1", "enabled-item-2": "new-value-2", "enabled-child-item": "new-child-value", }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{ "enabled-item-1": "new-value-1", "enabled-item-2": "new-value-2", @@ -422,12 +424,13 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "item-in-disabled-group": "new-value", "child-in-disabled-group": "new-child-value", "grandchild-in-disabled-group": "new-grandchild-value", }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{} mockStore.On("SetConfigValues", expectedValues).Return(nil) }, @@ -469,12 +472,13 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "enabled-item": "new-enabled-value", "disabled-item": "new-disabled-value", "child-of-disabled-item": "new-child-value", }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{ "enabled-item": "new-enabled-value", } @@ -525,7 +529,7 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "enabled-item": "new-enabled-value", "disabled-item": "new-disabled-value", "enabled-item-with-children": "new-parent-value", @@ -533,6 +537,7 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { "disabled-child": "new-disabled-child-value", }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{ "enabled-item": "new-enabled-value", "enabled-item-with-children": "new-parent-value", @@ -564,8 +569,9 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{}, + newValues: map[string]string{}, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{} mockStore.On("SetConfigValues", expectedValues).Return(nil) }, @@ -593,10 +599,11 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "enabled-item": "new-value", }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{ "enabled-item": "new-value", } @@ -626,10 +633,11 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "item-with-empty-when": "new-value", }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{ "item-with-empty-when": "new-value", } @@ -673,11 +681,12 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "item-1": "new-value-1", "item-2": "new-value-2", }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{} mockStore.On("SetConfigValues", expectedValues).Return(nil) }, @@ -712,11 +721,12 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, }, }, - configValues: map[string]string{ + newValues: map[string]string{ "enabled-item-1": "new-value-1", // enabled-item-2 intentionally omitted }, setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) expectedValues := map[string]string{ "enabled-item-1": "new-value-1", // enabled-item-2 should not be included since no value provided @@ -725,6 +735,251 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { }, wantErr: false, }, + { + name: "patch with existing values - new values override existing", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "enabled-group", + Title: "Enabled Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "item-1", + Title: "Item 1", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-1"}, + When: "true", + }, + { + Name: "item-2", + Title: "Item 2", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-2"}, + When: "true", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "item-1": "new-value-1", + "item-2": "new-value-2", + }, + setupMock: func(mockStore *config.MockStore) { + existingValues := map[string]string{ + "item-1": "existing-value-1", + "item-2": "existing-value-2", + } + mockStore.On("GetConfigValues").Return(existingValues, nil) + expectedValues := map[string]string{ + "item-1": "new-value-1", + "item-2": "new-value-2", + } + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, + { + name: "patch with existing values - partial update", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "enabled-group", + Title: "Enabled Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "item-1", + Title: "Item 1", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-1"}, + When: "true", + }, + { + Name: "item-2", + Title: "Item 2", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-2"}, + When: "true", + }, + { + Name: "item-3", + Title: "Item 3", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-3"}, + When: "true", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "item-1": "new-value-1", + // item-2 not provided, should keep existing value + "item-3": "new-value-3", + }, + setupMock: func(mockStore *config.MockStore) { + existingValues := map[string]string{ + "item-1": "existing-value-1", + "item-2": "existing-value-2", + "item-3": "existing-value-3", + } + mockStore.On("GetConfigValues").Return(existingValues, nil) + expectedValues := map[string]string{ + "item-1": "new-value-1", + "item-2": "existing-value-2", + "item-3": "new-value-3", + } + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, + { + name: "patch with empty string values", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "enabled-group", + Title: "Enabled Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "item-1", + Title: "Item 1", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-1"}, + When: "true", + }, + { + Name: "item-2", + Title: "Item 2", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-2"}, + When: "true", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "item-1": "", + "item-2": "new-value-2", + }, + setupMock: func(mockStore *config.MockStore) { + existingValues := map[string]string{ + "item-1": "existing-value-1", + "item-2": "existing-value-2", + } + mockStore.On("GetConfigValues").Return(existingValues, nil) + expectedValues := map[string]string{ + "item-1": "", + "item-2": "new-value-2", + } + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, + { + name: "patch with existing values and disabled items", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "enabled-group", + Title: "Enabled Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "enabled-item", + Title: "Enabled Item", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-enabled"}, + When: "true", + }, + { + Name: "disabled-item", + Title: "Disabled Item", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-disabled"}, + When: "false", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "enabled-item": "new-enabled-value", + "disabled-item": "new-disabled-value", + }, + setupMock: func(mockStore *config.MockStore) { + existingValues := map[string]string{ + "enabled-item": "existing-enabled-value", + "disabled-item": "existing-disabled-value", + } + mockStore.On("GetConfigValues").Return(existingValues, nil) + expectedValues := map[string]string{ + "enabled-item": "new-enabled-value", + // disabled-item should not be included in filtered values + } + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, + { + name: "patch with existing values - new item added", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "enabled-group", + Title: "Enabled Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "existing-item", + Title: "Existing Item", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-existing"}, + When: "true", + }, + { + Name: "new-item", + Title: "New Item", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-new"}, + When: "true", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "existing-item": "updated-existing-value", + "new-item": "brand-new-value", + }, + setupMock: func(mockStore *config.MockStore) { + existingValues := map[string]string{ + "existing-item": "existing-value", + // new-item not in existing values + } + mockStore.On("GetConfigValues").Return(existingValues, nil) + expectedValues := map[string]string{ + "existing-item": "updated-existing-value", + "new-item": "brand-new-value", + } + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, } for _, tt := range tests { @@ -738,8 +993,8 @@ func TestAppConfigManager_SetConfigValues(t *testing.T) { appConfigStore: mockStore, } - // Call SetConfigValues - err := manager.SetConfigValues(tt.config, tt.configValues) + // Call PatchConfigValues + err := manager.PatchConfigValues(context.Background(), tt.config, tt.newValues) // Verify expectations if tt.wantErr { diff --git a/api/internal/managers/app/config/manager.go b/api/internal/managers/app/config/manager.go index 4e0789529..f5049b415 100644 --- a/api/internal/managers/app/config/manager.go +++ b/api/internal/managers/app/config/manager.go @@ -1,6 +1,8 @@ package config import ( + "context" + configstore "github.com/replicatedhq/embedded-cluster/api/internal/store/app/config" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -13,10 +15,10 @@ var _ AppConfigManager = &appConfigManager{} type AppConfigManager interface { // GetConfig returns the config with disabled groups and items filtered out GetConfig(config kotsv1beta1.Config) (kotsv1beta1.Config, error) + // PatchConfigValues patches the current config values + PatchConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, values map[string]string) error // GetConfigValues returns the current config values - GetConfigValues() (map[string]string, error) - // SetConfigValues sets the config values - SetConfigValues(config kotsv1beta1.Config, values map[string]string) error + GetConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) // GetKotsadmConfigValues merges the config values with the app config defaults and returns a // kotsv1beta1.ConfigValues struct. GetKotsadmConfigValues(config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) diff --git a/api/internal/managers/app/config/manager_mock.go b/api/internal/managers/app/config/manager_mock.go index 781ae09ee..9409769c6 100644 --- a/api/internal/managers/app/config/manager_mock.go +++ b/api/internal/managers/app/config/manager_mock.go @@ -1,6 +1,8 @@ package config import ( + "context" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/stretchr/testify/mock" ) @@ -18,21 +20,21 @@ func (m *MockAppConfigManager) GetConfig(config kotsv1beta1.Config) (kotsv1beta1 return args.Get(0).(kotsv1beta1.Config), args.Error(1) } +// PatchConfigValues mocks the PatchConfigValues method +func (m *MockAppConfigManager) PatchConfigValues(ctx context.Context, config kotsv1beta1.Config, values map[string]string) error { + args := m.Called(ctx, config, values) + return args.Error(0) +} + // GetConfigValues mocks the GetConfigValues method -func (m *MockAppConfigManager) GetConfigValues() (map[string]string, error) { - args := m.Called() +func (m *MockAppConfigManager) GetConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { + args := m.Called(ctx, appConfig, maskPasswords) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(map[string]string), args.Error(1) } -// SetConfigValues mocks the SetConfigValues method -func (m *MockAppConfigManager) SetConfigValues(config kotsv1beta1.Config, configValues map[string]string) error { - args := m.Called(config, configValues) - return args.Error(0) -} - // GetKotsadmConfigValues mocks the GetKotsadmConfigValues method func (m *MockAppConfigManager) GetKotsadmConfigValues(config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { args := m.Called(config) diff --git a/api/routes.go b/api/routes.go index e3e512157..02ef1dd07 100644 --- a/api/routes.go +++ b/api/routes.go @@ -48,7 +48,7 @@ func (a *API) registerLinuxRoutes(router *mux.Router) { installRouter.HandleFunc("/app/config", a.handlers.linux.GetAppConfig).Methods("GET") installRouter.HandleFunc("/app/config/values", a.handlers.linux.GetAppConfigValues).Methods("GET") - installRouter.HandleFunc("/app/config/values", a.handlers.linux.PostSetAppConfigValues).Methods("POST") + installRouter.HandleFunc("/app/config/values", a.handlers.linux.PatchConfigValues).Methods("PATCH") } func (a *API) registerKubernetesRoutes(router *mux.Router) { @@ -64,7 +64,7 @@ func (a *API) registerKubernetesRoutes(router *mux.Router) { installRouter.HandleFunc("/app/config", a.handlers.kubernetes.GetAppConfig).Methods("GET") installRouter.HandleFunc("/app/config/values", a.handlers.kubernetes.GetAppConfigValues).Methods("GET") - installRouter.HandleFunc("/app/config/values", a.handlers.kubernetes.PostSetAppConfigValues).Methods("POST") + installRouter.HandleFunc("/app/config/values", a.handlers.kubernetes.PatchConfigValues).Methods("PATCH") } func (a *API) registerConsoleRoutes(router *mux.Router) { diff --git a/api/types/requests.go b/api/types/requests.go index 976dc4ab3..e0645b33c 100644 --- a/api/types/requests.go +++ b/api/types/requests.go @@ -1,6 +1,6 @@ package types -// SetAppConfigValuesRequest represents the request when setting the app config values -type SetAppConfigValuesRequest struct { +// PatchAppConfigValuesRequest represents the request when patching the app config values +type PatchAppConfigValuesRequest struct { Values map[string]string `json:"values"` } diff --git a/web/package-lock.json b/web/package-lock.json index 937bd4de7..f7f889537 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -5142,16 +5142,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", diff --git a/web/src/components/common/Input.tsx b/web/src/components/common/Input.tsx index c8561dcfb..beb1a846c 100644 --- a/web/src/components/common/Input.tsx +++ b/web/src/components/common/Input.tsx @@ -8,6 +8,7 @@ interface InputProps { value: string; onChange: (e: React.ChangeEvent) => void; onKeyDown?: (e: React.KeyboardEvent) => void; + onFocus?: (e: React.FocusEvent) => void; placeholder?: string; required?: boolean; disabled?: boolean; @@ -26,6 +27,7 @@ const Input: React.FC = ({ value, onChange, onKeyDown, + onFocus, placeholder = '', required = false, disabled = false, @@ -57,6 +59,7 @@ const Input: React.FC = ({ value={value} onChange={onChange} onKeyDown={onKeyDown} + onFocus={onFocus} placeholder={placeholder} disabled={disabled} required={required} diff --git a/web/src/components/wizard/config/ConfigurationStep.tsx b/web/src/components/wizard/config/ConfigurationStep.tsx index 797df6413..d4fa136f1 100644 --- a/web/src/components/wizard/config/ConfigurationStep.tsx +++ b/web/src/components/wizard/config/ConfigurationStep.tsx @@ -23,7 +23,8 @@ const ConfigurationStep: React.FC = ({ onNext }) => { const { token } = useAuth(); const { settings } = useSettings(); const [activeTab, setActiveTab] = useState(''); - const [changedValues, setChangedValues] = useState>({}); + const [configValues, setConfigValues] = useState>({}); + const [dirtyFields, setDirtyFields] = useState>(new Set()); const [submitError, setSubmitError] = useState(null); const themeColor = settings.themeColor; @@ -50,7 +51,7 @@ const ConfigurationStep: React.FC = ({ onNext }) => { }); // Fetch current config values - const { data: configValues, isLoading: isConfigValuesLoading, error: getConfigValuesError } = useQuery>({ + const { data: apiConfigValues, isLoading: isConfigValuesLoading, error: getConfigValuesError } = useQuery>({ queryKey: ['appConfigValues', target], queryFn: async () => { const response = await fetch(`/api/${target}/install/app/config/values`, { @@ -74,13 +75,21 @@ const ConfigurationStep: React.FC = ({ onNext }) => { // Mutation to save config values const { mutate: submitConfigValues } = useMutation({ mutationFn: async () => { + // Build payload with only dirty fields + const dirtyValues: Record = {}; + dirtyFields.forEach(fieldName => { + if (configValues[fieldName] !== undefined) { + dirtyValues[fieldName] = configValues[fieldName]; + } + }); + const response = await fetch(`/api/${target}/install/app/config/values`, { - method: 'POST', + method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ values: changedValues }), + body: JSON.stringify({ values: dirtyValues }), }); if (!response.ok) { @@ -96,6 +105,8 @@ const ConfigurationStep: React.FC = ({ onNext }) => { }, onSuccess: () => { setSubmitError(null); + // Clear dirty fields after successful submission + setDirtyFields(new Set()); onNext(); }, onError: (error: Error) => { @@ -110,40 +121,31 @@ const ConfigurationStep: React.FC = ({ onNext }) => { } }, [appConfig, activeTab]); - // Initialize changedValues with current values when they load + // Initialize configValues with initial values when they load useEffect(() => { - if (configValues && Object.keys(changedValues).length === 0) { - setChangedValues(configValues); + if (apiConfigValues && Object.keys(configValues).length === 0) { + setConfigValues(apiConfigValues); } - }, [configValues]); + }, [apiConfigValues]); // Helper function to get the display value for a config item (no defaults) const getDisplayValue = (item: AppConfigItem): string => { - // First check user value, then config item value - return changedValues?.[item.name] || item.value || ''; + // First check user value, then config item value (use ?? to allow empty strings from the user) + return configValues?.[item.name] ?? (item.value || ''); }; // Helper function to get the effective value for a config item (includes defaults) const getEffectiveValue = (item: AppConfigItem): string => { - // First check user value, then config item value, then default - return changedValues?.[item.name] || item.value || item.default || ''; + // First check user value, then config item value, then default (use ?? to allow empty strings from the user) + return configValues?.[item.name] ?? (item.value || item.default || ''); }; const updateConfigValue = (itemName: string, value: string) => { - // Update the changed values map - setChangedValues(prev => { - const newValues = { ...prev }; - - if (value === '') { - // Remove the item if it's empty - delete newValues[itemName]; - } else { - // Add or update the item with the new value - newValues[itemName] = value; - } + // Update the config values map + setConfigValues(prev => ({ ...prev, [itemName]: value })); - return newValues; - }); + // Mark field as dirty + setDirtyFields(prev => new Set(prev).add(itemName)); }; const handleInputChange = (e: React.ChangeEvent) => { @@ -151,6 +153,19 @@ const ConfigurationStep: React.FC = ({ onNext }) => { updateConfigValue(id, value); }; + const handlePasswordFocus = (e: React.FocusEvent) => { + // Auto-select entire text for password fields + e.target.select(); + }; + + const handlePasswordKeyDown = (itemName: string, e: React.KeyboardEvent) => { + // If field is not dirty and user types a character, clear the field first + if (!dirtyFields.has(itemName) && e.key.length === 1) { + // Clear the field before the character is added + updateConfigValue(itemName, ''); + } + }; + const handleCheckboxChange = (e: React.ChangeEvent) => { const { id, checked } = e.target; updateConfigValue(id, checked ? '1' : '0'); @@ -175,6 +190,20 @@ const ConfigurationStep: React.FC = ({ onNext }) => { helpText={item.help_text} /> ); + + case 'password': + return ( + handlePasswordKeyDown(item.name, e)} + onFocus={handlePasswordFocus} + dataTestId={`password-input-${item.name}`} + /> + ); case 'textarea': return ( diff --git a/web/src/components/wizard/tests/ConfigurationStep.test.tsx b/web/src/components/wizard/tests/ConfigurationStep.test.tsx index 5dbc927d4..ea3af9adf 100644 --- a/web/src/components/wizard/tests/ConfigurationStep.test.tsx +++ b/web/src/components/wizard/tests/ConfigurationStep.test.tsx @@ -117,7 +117,7 @@ const createServer = (target: string) => setupServer( }), // Mock config values submission endpoint - http.post(`*/api/${target}/install/app/config/values`, async ({ request }) => { + http.patch(`*/api/${target}/install/app/config/values`, async ({ request }) => { const body = await request.json() as { values: Record }; const updatedConfig = createMockConfigWithValues(body.values); return HttpResponse.json(updatedConfig); @@ -160,14 +160,11 @@ describe.each([ // Check initial loading state expect(screen.getByTestId("configuration-step-loading")).toBeInTheDocument(); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); - // Check main container is rendered - expect(screen.getByTestId("configuration-step")).toBeInTheDocument(); - // Check for title and description await screen.findByText("Configuration"); await screen.findByText("Configure your My App installation by providing the information below."); @@ -277,9 +274,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Wait for the content to be rendered @@ -318,9 +315,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Wait for the content to be rendered @@ -344,9 +341,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Find and update textarea input @@ -365,9 +362,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Check textarea with value @@ -390,9 +387,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Wait for the content to be rendered @@ -413,7 +410,7 @@ describe.each([ it("handles form submission error gracefully", async () => { server.use( - http.post(`*/api/${target}/install/app/config/values`, () => { + http.patch(`*/api/${target}/install/app/config/values`, () => { return new HttpResponse(JSON.stringify({ message: "Invalid configuration values" }), { status: 400 }); }) ); @@ -425,9 +422,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Submit form @@ -448,7 +445,7 @@ describe.each([ let submittedValues: { values: Record } | null = null; server.use( - http.post(`*/api/${target}/install/app/config/values`, async ({ request }) => { + http.patch(`*/api/${target}/install/app/config/values`, async ({ request }) => { // Verify auth header expect(request.headers.get("Authorization")).toBe("Bearer test-token"); const body = await request.json() as { values: Record }; @@ -465,9 +462,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Wait for the content to be rendered @@ -574,7 +571,7 @@ describe.each([ let submittedValues: { values: Record } | null = null; server.use( - http.post(`*/api/${target}/install/app/config/values`, async ({ request }) => { + http.patch(`*/api/${target}/install/app/config/values`, async ({ request }) => { const body = await request.json() as { values: Record }; submittedValues = body; const updatedConfig = createMockConfigWithValues(body.values); @@ -589,9 +586,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Wait for the content to be rendered @@ -683,7 +680,7 @@ describe.each([ // Wait for the content to be rendered await waitFor(() => { - expect(screen.getByTestId("configuration-step")).toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Check that text input shows empty value (not default) @@ -772,6 +769,38 @@ describe.each([ title: "Required" } ] + }, + { + name: "notification_method", + title: "Notification Method", + type: "radio", + default: "notification_method_email", + items: [ + { + name: "notification_method_email", + title: "Email" + }, + { + name: "notification_method_slack", + title: "Slack" + } + ] + }, + { + name: "backup_schedule", + title: "Backup Schedule", + type: "radio", + value: "backup_schedule_daily", + items: [ + { + name: "backup_schedule_daily", + title: "Daily" + }, + { + name: "backup_schedule_weekly", + title: "Weekly" + } + ] } ] } @@ -782,6 +811,15 @@ describe.each([ server.use( http.get(`*/api/${target}/install/app/config`, () => { return HttpResponse.json(comprehensiveConfig); + }), + http.get(`*/api/${target}/install/app/config/values`, () => { + // Provide config values to test priority: configValues > item.value > item.default + return HttpResponse.json({ + values: { + notification_method: "notification_method_slack", // configValues overrides schema default "notification_method_email" + backup_schedule: "backup_schedule_weekly" // configValues overrides schema value "backup_schedule_daily" + } + }); }) ); @@ -792,9 +830,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Wait for the content to be rendered @@ -807,6 +845,8 @@ describe.each([ expect(screen.getByTestId("config-item-database_type")).toBeInTheDocument(); expect(screen.getByTestId("config-item-logging_level")).toBeInTheDocument(); expect(screen.getByTestId("config-item-ssl_mode")).toBeInTheDocument(); + expect(screen.getByTestId("config-item-notification_method")).toBeInTheDocument(); + expect(screen.getByTestId("config-item-backup_schedule")).toBeInTheDocument(); // Test scenario 1: Has value, no default (value should be selected) const localAuthRadio = screen.getByTestId("radio-input-authentication_method_local") as HTMLInputElement; @@ -834,6 +874,18 @@ describe.each([ expect(sslDisabledRadio).not.toBeChecked(); expect(sslRequiredRadio).not.toBeChecked(); + // Test scenario 5: Has default, but configValues overrides (configValues should be selected) + const emailNotificationRadio = screen.getByTestId("radio-input-notification_method_email") as HTMLInputElement; + const slackNotificationRadio = screen.getByTestId("radio-input-notification_method_slack") as HTMLInputElement; + expect(emailNotificationRadio).not.toBeChecked(); + expect(slackNotificationRadio).toBeChecked(); // configValues "notification_method_slack" overrides default "notification_method_email" + + // Test scenario 6: Has value, but configValues overrides (configValues should be selected) + const dailyBackupRadio = screen.getByTestId("radio-input-backup_schedule_daily") as HTMLInputElement; + const weeklyBackupRadio = screen.getByTestId("radio-input-backup_schedule_weekly") as HTMLInputElement; + expect(dailyBackupRadio).not.toBeChecked(); + expect(weeklyBackupRadio).toBeChecked(); // configValues "backup_schedule_weekly" overrides value "backup_schedule_daily" + // Test radio button selection behavior fireEvent.click(localAuthRadio); expect(localAuthRadio).toBeChecked(); @@ -847,7 +899,7 @@ describe.each([ // Test form submission with radio button changes let submittedValues: { values: Record } | null = null; server.use( - http.post(`*/api/${target}/install/app/config/values`, async ({ request }) => { + http.patch(`*/api/${target}/install/app/config/values`, async ({ request }) => { const body = await request.json() as { values: Record }; submittedValues = body; return HttpResponse.json(comprehensiveConfig); @@ -886,9 +938,9 @@ describe.each([ }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Wait for radio buttons to appear @@ -899,21 +951,21 @@ describe.each([ // Get all radio buttons in the authentication group const anonymousRadio = screen.getByTestId("radio-input-auth_type_anonymous") as HTMLInputElement; const passwordRadio = screen.getByTestId("radio-input-auth_type_password") as HTMLInputElement; - + // Initially, Password should be selected expect(passwordRadio).toBeChecked(); expect(anonymousRadio).not.toBeChecked(); // Click on Anonymous fireEvent.click(anonymousRadio); - + // Now only Anonymous should be selected expect(anonymousRadio).toBeChecked(); expect(passwordRadio).not.toBeChecked(); // Click on Password fireEvent.click(passwordRadio); - + // Now only Password should be selected expect(anonymousRadio).not.toBeChecked(); expect(passwordRadio).toBeChecked(); @@ -955,7 +1007,7 @@ describe.each([ // Check that URL is converted to a clickable link const infoLabel = screen.getByTestId("label-info_label"); expect(infoLabel).toBeInTheDocument(); - + // Check that the link is present and has correct attributes const docsLink = screen.getByRole("link", { name: /docs.example.com/ }); expect(docsLink).toBeInTheDocument(); @@ -984,17 +1036,17 @@ describe.each([ // Check that markdown is rendered correctly const markdownLabel = screen.getByTestId("label-markdown_label"); expect(markdownLabel).toBeInTheDocument(); - + // Check for bold text const boldText = markdownLabel.querySelector("strong"); expect(boldText).toBeInTheDocument(); expect(boldText).toHaveTextContent("bold"); - + // Check for italic text const italicText = markdownLabel.querySelector("em"); expect(italicText).toBeInTheDocument(); expect(italicText).toHaveTextContent("italic"); - + // Check for markdown link const markdownLink = screen.getByRole("link", { name: "link" }); expect(markdownLink).toBeInTheDocument(); @@ -1021,12 +1073,12 @@ describe.each([ // Check that the database warning label is rendered const dbWarningLabel = screen.getByTestId("label-db_warning"); expect(dbWarningLabel).toBeInTheDocument(); - + // Check for bold text in the warning const importantText = dbWarningLabel.querySelector("strong"); expect(importantText).toBeInTheDocument(); expect(importantText).toHaveTextContent("Important"); - + // Check for automatic link detection const helpLink = screen.getByRole("link", { name: /help.example.com/ }); expect(helpLink).toBeInTheDocument(); @@ -1038,7 +1090,7 @@ describe.each([ let submittedValues: { values: Record } | null = null; server.use( - http.post(`*/api/${target}/install/app/config/values`, async ({ request }) => { + http.patch(`*/api/${target}/install/app/config/values`, async ({ request }) => { const body = await request.json() as { values: Record }; submittedValues = body; const updatedConfig = createMockConfigWithValues(body.values); @@ -1081,15 +1133,168 @@ describe.each([ app_name: "Test App" } }); - + // Verify label fields are not included in submission expect(submittedValues!.values).not.toHaveProperty("info_label"); expect(submittedValues!.values).not.toHaveProperty("markdown_label"); expect(submittedValues!.values).not.toHaveProperty("db_warning"); }); }); -describe("Initial values", () => { - it("initializes changed values from retrieved config values and only submits retrieved values plus changes", async () => { + + describe("Bool field behavior", () => { + it("tests all bool field scenarios with different value/default combinations", async () => { + // Create config with bool fields that mirror the radio button test scenarios + const comprehensiveConfig: AppConfig = { + groups: [ + { + name: "bool_test_scenarios", + title: "Bool Test Scenarios", + description: "Testing different bool field scenarios", + items: [ + { + name: "authentication_method", + title: "Authentication Method", + type: "bool", + value: "1" + }, + { + name: "database_type", + title: "Database Type", + type: "bool", + default: "1" + }, + { + name: "logging_level", + title: "Logging Level", + type: "bool", + value: "1", + default: "0" + }, + { + name: "ssl_mode", + title: "SSL Mode", + type: "bool" + }, + { + name: "notification_method", + title: "Notification Method", + type: "bool", + default: "0" + }, + { + name: "backup_schedule", + title: "Backup Schedule", + type: "bool", + value: "0" + } + ] + } + ] + }; + + // Override the server to return our comprehensive config + server.use( + http.get(`*/api/${target}/install/app/config`, () => { + return HttpResponse.json(comprehensiveConfig); + }), + http.get(`*/api/${target}/install/app/config/values`, () => { + // Provide config values to test priority: configValues > item.value > item.default + return HttpResponse.json({ + values: { + notification_method: "1", // configValues overrides schema default "0" + backup_schedule: "1" // configValues overrides schema value "0" + } + }); + }) + ); + + renderWithProviders(, { + wrapperProps: { + authenticated: true, + target: target, + }, + }); + + // Wait for the content to be rendered + await waitFor(() => { + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); + }); + + // Check that all bool fields are rendered + expect(screen.getByTestId("config-item-authentication_method")).toBeInTheDocument(); + expect(screen.getByTestId("config-item-database_type")).toBeInTheDocument(); + expect(screen.getByTestId("config-item-logging_level")).toBeInTheDocument(); + expect(screen.getByTestId("config-item-ssl_mode")).toBeInTheDocument(); + expect(screen.getByTestId("config-item-notification_method")).toBeInTheDocument(); + expect(screen.getByTestId("config-item-backup_schedule")).toBeInTheDocument(); + + // Test scenario 1: Has value, no default (value should be used) + const authenticationMethodCheckbox = screen.getByTestId("bool-input-authentication_method") as HTMLInputElement; + expect(authenticationMethodCheckbox).toBeChecked(); // value is "1" + + // Test scenario 2: Has default, no value (default should be used) + const databaseTypeCheckbox = screen.getByTestId("bool-input-database_type") as HTMLInputElement; + expect(databaseTypeCheckbox).toBeChecked(); // default is "1" + + // Test scenario 3: Has both value and default (value should take precedence) + const loggingLevelCheckbox = screen.getByTestId("bool-input-logging_level") as HTMLInputElement; + expect(loggingLevelCheckbox).toBeChecked(); // value is "1" takes precedence over default "0" + + // Test scenario 4: Has neither value nor default (should be unchecked) + const sslModeCheckbox = screen.getByTestId("bool-input-ssl_mode") as HTMLInputElement; + expect(sslModeCheckbox).not.toBeChecked(); // no value or default + + // Test scenario 5: Has default, but configValues overrides (configValues should be used) + const notificationMethodCheckbox = screen.getByTestId("bool-input-notification_method") as HTMLInputElement; + expect(notificationMethodCheckbox).toBeChecked(); // configValues "1" overrides default "0" + + // Test scenario 6: Has value, but configValues overrides (configValues should be used) + const backupScheduleCheckbox = screen.getByTestId("bool-input-backup_schedule") as HTMLInputElement; + expect(backupScheduleCheckbox).toBeChecked(); // configValues "1" overrides value "0" + + // Test checkbox toggling behavior + fireEvent.click(authenticationMethodCheckbox); + expect(authenticationMethodCheckbox).not.toBeChecked(); + + fireEvent.click(authenticationMethodCheckbox); + expect(authenticationMethodCheckbox).toBeChecked(); + + // Test form submission with checkbox changes + let submittedValues: { values: Record } | null = null; + server.use( + http.patch(`*/api/${target}/install/app/config/values`, async ({ request }) => { + const body = await request.json() as { values: Record }; + submittedValues = body; + return HttpResponse.json(comprehensiveConfig); + }) + ); + + // Change a checkbox + fireEvent.click(databaseTypeCheckbox); + + // Submit form + const nextButton = screen.getByTestId("config-next-button"); + fireEvent.click(nextButton); + + // Wait for the mutation to complete + await waitFor( + () => { + expect(mockOnNext).toHaveBeenCalled(); + }, + { timeout: 3000 } + ); + + // Verify the checkbox change was submitted + expect(submittedValues).not.toBeNull(); + expect(submittedValues!).toMatchObject({ + values: { + database_type: "0" // changed from checked to unchecked + } + }); + }); + }); + + it("initializes changed values from retrieved config values and only submits changed values (PATCH behavior)", async () => { // Mock the config values endpoint to return only a subset of values const retrievedConfigValues = { app_name: "Retrieved App Name", @@ -1103,7 +1308,7 @@ describe("Initial values", () => { http.get(`*/api/${target}/install/app/config/values`, () => { return HttpResponse.json({ values: retrievedConfigValues }); }), - http.post(`*/api/${target}/install/app/config/values`, async ({ request }) => { + http.patch(`*/api/${target}/install/app/config/values`, async ({ request }) => { const body = await request.json() as { values: Record }; submittedValues = body; return HttpResponse.json(MOCK_APP_CONFIG); @@ -1117,14 +1322,14 @@ describe("Initial values", () => { }, }); - // Wait for loading to complete + // Wait for the content to be rendered await waitFor(() => { - expect(screen.queryByTestId("configuration-step-loading")).not.toBeInTheDocument(); + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); }); // Make changes to form fields const appNameInput = screen.getByTestId("text-input-app_name"); - fireEvent.change(appNameInput, { target: { value: "Updated App Name" } }); + fireEvent.change(appNameInput, { target: { value: "" } }); // Clear app_name to test empty string submission // Switch to database tab and change a field that wasn't in retrieved values fireEvent.click(screen.getByTestId("config-tab-database")); @@ -1143,21 +1348,163 @@ describe("Initial values", () => { { timeout: 3000 } ); - // Verify that only retrieved values + changes are submitted + // Verify that only changed values are submitted (PATCH behavior) expect(submittedValues).not.toBeNull(); expect(submittedValues!).toMatchObject({ values: { - // Retrieved values that were initialized - app_name: "Updated App Name", // changed from retrieved value - auth_type: "auth_type_anonymous", // unchanged retrieved value + // Retrieved values that were changed + app_name: "", // cleared to empty string (should be submitted for deletion) // New changes to fields not in retrieved values db_host: "new-db-host" + // auth_type should NOT be submitted since it wasn't changed // enable_feature should NOT be submitted since it wasn't retrieved and wasn't changed } }); - - // Explicitly verify enable_feature is not submitted + + // Explicitly verify unchanged values are not submitted expect(submittedValues!.values).not.toHaveProperty("enable_feature"); + expect(submittedValues!.values).not.toHaveProperty("auth_type"); + }); + + it("clears password field on first keystroke when field is not dirty", async () => { + // Create config with password field + const configWithPassword: AppConfig = { + groups: [ + { + name: "auth", + title: "Authentication", + items: [ + { + name: "user_password", + title: "User Password", + type: "password", + value: "masked_value", + default: "default_password" + } + ] + } + ] + }; + + server.use( + http.get(`*/api/${target}/install/app/config`, () => { + return HttpResponse.json(configWithPassword); + }), + http.get(`*/api/${target}/install/app/config/values`, () => { + return HttpResponse.json({ values: { user_password: "••••••••" } }); + }) + ); + + renderWithProviders(, { + wrapperProps: { + authenticated: true, + target: target, + }, }); + + // Wait for the content to be rendered + await waitFor(() => { + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); + }); + + // Get the password input + const passwordInput = screen.getByTestId("password-input-user_password") as HTMLInputElement; + + // Test the clear-on-keystroke behavior - field should show API masked value + expect(passwordInput.value).toBe("••••••••"); + + // Simulate first keystroke - keydown should clear the field + fireEvent.keyDown(passwordInput, { key: 'a' }); + + // Field should be empty after keydown (before change event) + expect(passwordInput.value).toBe(''); + + // Then change event sets the typed character + fireEvent.change(passwordInput, { target: { value: 'a' } }); + + // Field should now show only the typed character + expect(passwordInput.value).toBe('a'); + + // Type more characters normally + fireEvent.change(passwordInput, { target: { value: 'abc' } }); + expect(passwordInput.value).toBe('abc'); + }); + + it("handles empty string values vs undefined/null values correctly", async () => { + // Create config with realistic field names + const configWithDefaults: AppConfig = { + groups: [ + { + name: "database", + title: "Database Settings", + items: [ + { + name: "db_name", + title: "Database Name", + type: "text", + value: "myapp", + default: "default_db" + }, + { + name: "db_password", + title: "Database Password", + type: "text", + value: "secret123", + default: "changeme" + }, + { + name: "db_port", + title: "Database Port", + type: "text", + default: "5432" + }, + { + name: "enable_ssl", + title: "Enable SSL", + type: "bool", + default: "1" + } + ] + } + ] + }; + + server.use( + http.get(`*/api/${target}/install/app/config`, () => { + return HttpResponse.json(configWithDefaults); + }), + http.get(`*/api/${target}/install/app/config/values`, () => { + // API returns empty string for db_name, nothing for db_password or db_port + return HttpResponse.json({ values: { db_name: "" } }); + }) + ); + + renderWithProviders(, { + wrapperProps: { + authenticated: true, + target: target, + }, + }); + + // Wait for the content to be rendered + await waitFor(() => { + expect(screen.queryByTestId("configuration-step")).toBeInTheDocument(); + }); + + // Field with empty string in configValues should show empty string (not schema value or default) + const dbNameField = screen.getByTestId("text-input-db_name") as HTMLInputElement; + expect(dbNameField.value).toBe(""); // configValues empty string takes precedence + + // Field with no configValues entry should show schema value (current behavior with getDisplayValue) + const dbPasswordField = screen.getByTestId("text-input-db_password") as HTMLInputElement; + expect(dbPasswordField.value).toBe("secret123"); // schema value shows since no configValues entry + + // Field with no value in config but has default should show empty string (getDisplayValue doesn't use default) + const dbPortField = screen.getByTestId("text-input-db_port") as HTMLInputElement; + expect(dbPortField.value).toBe(""); // no value in config, default is not used as getDisplayValue doesn't use default + + // Bool field with no value in config but has default should use default (getEffectiveValue includes default) + const enableSslField = screen.getByTestId("bool-input-enable_ssl") as HTMLInputElement; + expect(enableSslField.checked).toBe(true); // default "1" is used since getEffectiveValue includes default }); }); From 8a6a1a3cedfe53a6563db6e57ed7cfdab4ac9239 Mon Sep 17 00:00:00 2001 From: Salah Aldeen Al Saleh Date: Fri, 11 Jul 2025 13:01:10 -0700 Subject: [PATCH 2/2] unit tests --- api/controllers/kubernetes/install/infra.go | 2 +- api/controllers/linux/install/infra.go | 2 +- api/internal/managers/app/config/config.go | 24 +++-- .../managers/app/config/config_test.go | 92 ++++++++++++++++++- api/internal/managers/app/config/manager.go | 6 +- .../managers/app/config/manager_mock.go | 8 +- 6 files changed, 116 insertions(+), 18 deletions(-) diff --git a/api/controllers/kubernetes/install/infra.go b/api/controllers/kubernetes/install/infra.go index 5ed0639c4..28ed8695a 100644 --- a/api/controllers/kubernetes/install/infra.go +++ b/api/controllers/kubernetes/install/infra.go @@ -28,7 +28,7 @@ func (c *InstallController) SetupInfra(ctx context.Context) (finalErr error) { } }() - configValues, err := c.appConfigManager.GetKotsadmConfigValues(*c.releaseData.AppConfig) + configValues, err := c.appConfigManager.GetKotsadmConfigValues(ctx, *c.releaseData.AppConfig) if err != nil { return fmt.Errorf("failed to get kotsadm config values: %w", err) } diff --git a/api/controllers/linux/install/infra.go b/api/controllers/linux/install/infra.go index 21076071a..b4497d390 100644 --- a/api/controllers/linux/install/infra.go +++ b/api/controllers/linux/install/infra.go @@ -43,7 +43,7 @@ func (c *InstallController) SetupInfra(ctx context.Context, ignoreHostPreflights } } - configValues, err := c.appConfigManager.GetKotsadmConfigValues(*c.releaseData.AppConfig) + configValues, err := c.appConfigManager.GetKotsadmConfigValues(ctx, *c.releaseData.AppConfig) if err != nil { return fmt.Errorf("failed to get kotsadm config values: %w", err) } diff --git a/api/internal/managers/app/config/config.go b/api/internal/managers/app/config/config.go index 326c34087..62fd09385 100644 --- a/api/internal/managers/app/config/config.go +++ b/api/internal/managers/app/config/config.go @@ -21,7 +21,7 @@ func (m *appConfigManager) GetConfig(config kotsv1beta1.Config) (kotsv1beta1.Con } // PatchConfigValues performs a partial update by merging new values with existing ones -func (m *appConfigManager) PatchConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, newValues map[string]string) error { +func (m *appConfigManager) PatchConfigValues(ctx context.Context, config kotsv1beta1.Config, newValues map[string]string) error { // Get existing values existingValues, err := m.appConfigStore.GetConfigValues() if err != nil { @@ -35,7 +35,7 @@ func (m *appConfigManager) PatchConfigValues(ctx context.Context, appConfig kots // only keep values for enabled groups and items filteredValues := make(map[string]string) - for _, g := range appConfig.Spec.Groups { + for _, g := range config.Spec.Groups { for _, i := range g.Items { if isItemEnabled(g.When) && isItemEnabled(i.When) { value, ok := mergedValues[i.Name] @@ -56,7 +56,7 @@ func (m *appConfigManager) PatchConfigValues(ctx context.Context, appConfig kots } // GetConfigValues returns config values with optional password field masking -func (m *appConfigManager) GetConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { +func (m *appConfigManager) GetConfigValues(ctx context.Context, config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { configValues, err := m.appConfigStore.GetConfigValues() if err != nil { return nil, err @@ -74,7 +74,7 @@ func (m *appConfigManager) GetConfigValues(ctx context.Context, appConfig kotsv1 } // Mask password fields - for _, group := range appConfig.Spec.Groups { + for _, group := range config.Spec.Groups { for _, item := range group.Items { if item.Type == "password" { if _, exists := maskedValues[item.Name]; exists { @@ -87,13 +87,13 @@ func (m *appConfigManager) GetConfigValues(ctx context.Context, appConfig kotsv1 return maskedValues, nil } -func (m *appConfigManager) GetKotsadmConfigValues(config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { +func (m *appConfigManager) GetKotsadmConfigValues(ctx context.Context, config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { filteredConfig, err := m.GetConfig(config) if err != nil { return kotsv1beta1.ConfigValues{}, fmt.Errorf("get config: %w", err) } - storedValues, err := m.GetConfigValues() + storedValues, err := m.GetConfigValues(ctx, filteredConfig, false) if err != nil { return kotsv1beta1.ConfigValues{}, fmt.Errorf("get config values: %w", err) } @@ -115,12 +115,20 @@ func (m *appConfigManager) GetKotsadmConfigValues(config kotsv1beta1.Config) (ko for _, group := range filteredConfig.Spec.Groups { for _, item := range group.Items { configValue := kotsv1beta1.ConfigValue{ - Value: item.Value.String(), Default: item.Default.String(), } + if item.Type == "password" { + configValue.ValuePlaintext = item.Value.String() + } else { + configValue.Value = item.Value.String() + } // override values from the config values store if value, ok := storedValues[item.Name]; ok { - configValue.Value = value + if item.Type == "password" { + configValue.ValuePlaintext = value + } else { + configValue.Value = value + } } kotsadmConfigValues.Spec.Values[item.Name] = configValue diff --git a/api/internal/managers/app/config/config_test.go b/api/internal/managers/app/config/config_test.go index cfeac59b8..96257cb21 100644 --- a/api/internal/managers/app/config/config_test.go +++ b/api/internal/managers/app/config/config_test.go @@ -1603,6 +1603,96 @@ func TestAppConfigManager_GetKotsadmConfigValues(t *testing.T) { }, wantErr: false, }, + { + name: "password fields use ValuePlaintext", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "auth-group", + Title: "Authentication Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "username", + Title: "Username", + Type: "text", + Value: multitype.BoolOrString{StrVal: "admin"}, + Default: multitype.BoolOrString{StrVal: "user"}, + When: "true", + }, + { + Name: "password", + Title: "Password", + Type: "password", + Value: multitype.BoolOrString{StrVal: "schema-password"}, + Default: multitype.BoolOrString{StrVal: "default-password"}, + When: "true", + }, + { + Name: "api-key", + Title: "API Key", + Type: "password", + Value: multitype.BoolOrString{StrVal: "schema-api-key"}, + Default: multitype.BoolOrString{StrVal: "default-api-key"}, + When: "true", + }, + { + Name: "secret-token", + Title: "Secret Token", + Type: "password", + Value: multitype.BoolOrString{StrVal: "schema-token"}, + Default: multitype.BoolOrString{StrVal: "default-token"}, + When: "true", + }, + }, + }, + }, + }, + }, + setupMock: func(mockStore *config.MockStore) { + storeValues := map[string]string{ + "username": "stored-username", + "password": "stored-password", + "api-key": "stored-api-key", + // secret-token intentionally omitted to test fallback behavior + } + mockStore.On("GetConfigValues").Return(storeValues, nil) + }, + expected: kotsv1beta1.ConfigValues{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kots.io/v1beta1", + Kind: "ConfigValues", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "kots-app-config", + }, + Spec: kotsv1beta1.ConfigValuesSpec{ + Values: map[string]kotsv1beta1.ConfigValue{ + "username": { + Value: "stored-username", // text field uses stored value + Default: "user", + }, + "password": { + Value: "", + ValuePlaintext: "stored-password", // password with stored value: uses stored value + Default: "default-password", + }, + "api-key": { + Value: "", + ValuePlaintext: "stored-api-key", // password with stored value: uses stored value + Default: "default-api-key", + }, + "secret-token": { + Value: "", + ValuePlaintext: "schema-token", // password without stored value: uses value from schema + Default: "default-token", + }, + }, + }, + }, + wantErr: false, + }, } for _, tt := range tests { @@ -1617,7 +1707,7 @@ func TestAppConfigManager_GetKotsadmConfigValues(t *testing.T) { } // Call GetKotsadmConfigValues - result, err := manager.GetKotsadmConfigValues(tt.config) + result, err := manager.GetKotsadmConfigValues(t.Context(), tt.config) // Verify expectations if tt.wantErr { diff --git a/api/internal/managers/app/config/manager.go b/api/internal/managers/app/config/manager.go index f5049b415..7444f81e2 100644 --- a/api/internal/managers/app/config/manager.go +++ b/api/internal/managers/app/config/manager.go @@ -16,12 +16,12 @@ type AppConfigManager interface { // GetConfig returns the config with disabled groups and items filtered out GetConfig(config kotsv1beta1.Config) (kotsv1beta1.Config, error) // PatchConfigValues patches the current config values - PatchConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, values map[string]string) error + PatchConfigValues(ctx context.Context, config kotsv1beta1.Config, values map[string]string) error // GetConfigValues returns the current config values - GetConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) + GetConfigValues(ctx context.Context, config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) // GetKotsadmConfigValues merges the config values with the app config defaults and returns a // kotsv1beta1.ConfigValues struct. - GetKotsadmConfigValues(config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) + GetKotsadmConfigValues(ctx context.Context, config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) } // appConfigManager is an implementation of the AppConfigManager interface diff --git a/api/internal/managers/app/config/manager_mock.go b/api/internal/managers/app/config/manager_mock.go index 9409769c6..90eb32566 100644 --- a/api/internal/managers/app/config/manager_mock.go +++ b/api/internal/managers/app/config/manager_mock.go @@ -27,8 +27,8 @@ func (m *MockAppConfigManager) PatchConfigValues(ctx context.Context, config kot } // GetConfigValues mocks the GetConfigValues method -func (m *MockAppConfigManager) GetConfigValues(ctx context.Context, appConfig kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { - args := m.Called(ctx, appConfig, maskPasswords) +func (m *MockAppConfigManager) GetConfigValues(ctx context.Context, config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { + args := m.Called(ctx, config, maskPasswords) if args.Get(0) == nil { return nil, args.Error(1) } @@ -36,7 +36,7 @@ func (m *MockAppConfigManager) GetConfigValues(ctx context.Context, appConfig ko } // GetKotsadmConfigValues mocks the GetKotsadmConfigValues method -func (m *MockAppConfigManager) GetKotsadmConfigValues(config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { - args := m.Called(config) +func (m *MockAppConfigManager) GetKotsadmConfigValues(ctx context.Context, config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { + args := m.Called(ctx, config) return args.Get(0).(kotsv1beta1.ConfigValues), args.Error(1) }