diff --git a/api/client/client_test.go b/api/client/client_test.go index ddea2d9c3..35a2741f1 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -881,17 +881,22 @@ func TestLinuxPatchAppConfigValues(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check request method and path assert.Equal(t, "PATCH", r.Method) assert.Equal(t, "/api/linux/install/app/config/values", r.URL.Path) + // Check headers 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 + // Decode request body var req types.PatchAppConfigValuesRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) + require.NoError(t, err, "Failed to decode request body") + + // Verify the request contains expected values assert.Equal(t, "new-value", req.Values["test-item"]) + assert.Equal(t, "required-value", req.Values["required-item"]) // Return successful response w.WriteHeader(http.StatusOK) @@ -901,32 +906,33 @@ func TestLinuxPatchAppConfigValues(t *testing.T) { // Test successful set c := New(server.URL, WithToken("test-token")) - values := map[string]string{ - "test-item": "new-value", + configValues := map[string]string{ + "test-item": "new-value", + "required-item": "required-value", } - config, err := c.PatchLinuxAppConfigValues(values) - assert.NoError(t, err) + config, err := c.PatchLinuxAppConfigValues(configValues) + require.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) + w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(types.APIError{ - StatusCode: http.StatusInternalServerError, - Message: "Internal Server Error", + StatusCode: http.StatusBadRequest, + Message: "Bad Request", }) })) defer errorServer.Close() c = New(errorServer.URL, WithToken("test-token")) - config, err = c.PatchLinuxAppConfigValues(values) + config, err = c.PatchLinuxAppConfigValues(configValues) 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) + assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode) + assert.Equal(t, "Bad Request", apiErr.Message) } func TestKubernetesPatchAppConfigValues(t *testing.T) { @@ -951,17 +957,22 @@ func TestKubernetesPatchAppConfigValues(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check request method and path assert.Equal(t, "PATCH", r.Method) assert.Equal(t, "/api/kubernetes/install/app/config/values", r.URL.Path) + // Check headers 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 + // Decode request body var req types.PatchAppConfigValuesRequest err := json.NewDecoder(r.Body).Decode(&req) - require.NoError(t, err) + require.NoError(t, err, "Failed to decode request body") + + // Verify the request contains expected values assert.Equal(t, "new-value", req.Values["test-item"]) + assert.Equal(t, "required-value", req.Values["required-item"]) // Return successful response w.WriteHeader(http.StatusOK) @@ -971,30 +982,31 @@ func TestKubernetesPatchAppConfigValues(t *testing.T) { // Test successful set c := New(server.URL, WithToken("test-token")) - values := map[string]string{ - "test-item": "new-value", + configValues := map[string]string{ + "test-item": "new-value", + "required-item": "required-value", } - config, err := c.PatchKubernetesAppConfigValues(values) + config, err := c.PatchKubernetesAppConfigValues(configValues) 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) + w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(types.APIError{ - StatusCode: http.StatusInternalServerError, - Message: "Internal Server Error", + StatusCode: http.StatusBadRequest, + Message: "Bad Request", }) })) defer errorServer.Close() c = New(errorServer.URL, WithToken("test-token")) - config, err = c.PatchKubernetesAppConfigValues(values) + config, err = c.PatchKubernetesAppConfigValues(configValues) 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) + assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode) + assert.Equal(t, "Bad Request", apiErr.Message) } diff --git a/api/client/install.go b/api/client/install.go index 3a6eae483..840e0a771 100644 --- a/api/client/install.go +++ b/api/client/install.go @@ -439,10 +439,11 @@ func (c *client) GetKubernetesAppConfigValues() (map[string]string, error) { } func (c *client) PatchKubernetesAppConfigValues(values map[string]string) (types.AppConfig, error) { - req := types.PatchAppConfigValuesRequest{ + request := types.PatchAppConfigValuesRequest{ Values: values, } - b, err := json.Marshal(req) + + b, err := json.Marshal(request) if err != nil { return types.AppConfig{}, err } diff --git a/api/controllers/kubernetes/install/app.go b/api/controllers/kubernetes/install/app.go index 7d36d2a4c..4efd8ed84 100644 --- a/api/controllers/kubernetes/install/app.go +++ b/api/controllers/kubernetes/install/app.go @@ -51,7 +51,12 @@ func (c *InstallController) PatchAppConfigValues(ctx context.Context, values map } }() - err = c.appConfigManager.PatchConfigValues(ctx, *c.releaseData.AppConfig, values) + err = c.appConfigManager.ValidateConfigValues(*c.releaseData.AppConfig, values) + if err != nil { + return fmt.Errorf("validate app config values: %w", err) + } + + err = c.appConfigManager.PatchConfigValues(*c.releaseData.AppConfig, values) if err != nil { return fmt.Errorf("patch app config values: %w", err) } @@ -71,5 +76,5 @@ func (c *InstallController) GetAppConfigValues(ctx context.Context, maskPassword return nil, fmt.Errorf("get app config: %w", err) } - return c.appConfigManager.GetConfigValues(ctx, appConfig, maskPasswords) + return c.appConfigManager.GetConfigValues(appConfig, maskPasswords) } diff --git a/api/controllers/kubernetes/install/app_test.go b/api/controllers/kubernetes/install/app_test.go new file mode 100644 index 000000000..737325aac --- /dev/null +++ b/api/controllers/kubernetes/install/app_test.go @@ -0,0 +1,288 @@ +package install + +import ( + "errors" + "testing" + "time" + + appconfig "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/config" + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" + "github.com/replicatedhq/embedded-cluster/api/internal/store" + "github.com/replicatedhq/embedded-cluster/pkg/release" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/multitype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestInstallController_PatchAppConfigValues(t *testing.T) { + // Create an app config for testing + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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"}, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + values map[string]string + currentState statemachine.State + expectedState statemachine.State + setupMocks func(*appconfig.MockAppConfigManager, *store.MockStore) + expectedErr bool + }{ + { + name: "successful set app config values", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateNew, + expectedState: StateApplicationConfigured, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful set app config values from application configuration failed state", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateApplicationConfigurationFailed, + expectedState: StateApplicationConfigured, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful set app config values from application configured state", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateApplicationConfigured, + expectedState: StateApplicationConfigured, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "validation error", + values: map[string]string{ + "test-item": "invalid-value", + }, + currentState: StateNew, + expectedState: StateApplicationConfigurationFailed, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "invalid-value"}).Return(errors.New("validation error")), + ) + }, + expectedErr: true, + }, + { + name: "set config values error", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateNew, + expectedState: StateApplicationConfigurationFailed, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(errors.New("set config error")), + ) + }, + expectedErr: true, + }, + { + name: "invalid state transition", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateInfrastructureInstalling, + expectedState: StateInfrastructureInstalling, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + }, + expectedErr: true, + }, + { + name: "app config not found", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateNew, + expectedState: StateNew, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + }, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sm := NewStateMachine(WithCurrentState(tt.currentState)) + + mockAppConfigManager := &appconfig.MockAppConfigManager{} + mockStore := &store.MockStore{} + + tt.setupMocks(mockAppConfigManager, mockStore) + + // For the "app config not found" test case, pass nil as AppConfig + var releaseData *release.ReleaseData + if tt.name == "app config not found" { + releaseData = getTestReleaseData(nil) + } else { + releaseData = getTestReleaseData(&appConfig) + } + + controller, err := NewInstallController( + WithStateMachine(sm), + WithAppConfigManager(mockAppConfigManager), + WithReleaseData(releaseData), + WithStore(mockStore), + ) + require.NoError(t, err) + + err = controller.PatchAppConfigValues(t.Context(), tt.values) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Eventually(t, func() bool { + return sm.CurrentState() == tt.expectedState + }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after setting app config values") + + mockAppConfigManager.AssertExpectations(t) + mockStore.LinuxInfraMockStore.AssertExpectations(t) + mockStore.LinuxInstallationMockStore.AssertExpectations(t) + mockStore.LinuxPreflightMockStore.AssertExpectations(t) + mockStore.AppConfigMockStore.AssertExpectations(t) + }) + } +} + +func TestInstallController_GetAppConfigValues(t *testing.T) { + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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"}, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + setupMocks func(*appconfig.MockAppConfigManager, *store.MockStore) + expectedValues map[string]string + expectedErr bool + }{ + { + name: "successful get app config values", + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + expectedValues := map[string]string{ + "test-item": "test-value", + "another-item": "another-value", + } + am.On("GetConfig", appConfig).Return(appConfig, nil) + am.On("GetConfigValues", appConfig, false).Return(expectedValues, nil) + }, + expectedValues: map[string]string{ + "test-item": "test-value", + "another-item": "another-value", + }, + expectedErr: false, + }, + { + name: "get config values error", + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + am.On("GetConfig", appConfig).Return(appConfig, nil) + am.On("GetConfigValues", appConfig, false).Return(nil, errors.New("get config values error")) + }, + expectedValues: nil, + expectedErr: true, + }, + { + name: "empty config values", + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + am.On("GetConfig", appConfig).Return(appConfig, nil) + am.On("GetConfigValues", appConfig, false).Return(map[string]string{}, nil) + }, + expectedValues: map[string]string{}, + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAppConfigManager := &appconfig.MockAppConfigManager{} + mockStore := &store.MockStore{} + + tt.setupMocks(mockAppConfigManager, mockStore) + + controller, err := NewInstallController( + WithAppConfigManager(mockAppConfigManager), + WithStore(mockStore), + WithReleaseData(getTestReleaseData(&appConfig)), + ) + require.NoError(t, err) + + values, err := controller.GetAppConfigValues(t.Context(), false) + + if tt.expectedErr { + assert.Error(t, err) + assert.Nil(t, values) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValues, values) + } + + mockAppConfigManager.AssertExpectations(t) + mockStore.LinuxInfraMockStore.AssertExpectations(t) + mockStore.LinuxInstallationMockStore.AssertExpectations(t) + mockStore.LinuxPreflightMockStore.AssertExpectations(t) + mockStore.AppConfigMockStore.AssertExpectations(t) + }) + } +} diff --git a/api/controllers/kubernetes/install/controller.go b/api/controllers/kubernetes/install/controller.go index 4e836eda6..2d8258fe9 100644 --- a/api/controllers/kubernetes/install/controller.go +++ b/api/controllers/kubernetes/install/controller.go @@ -2,6 +2,7 @@ package install import ( "context" + "errors" "fmt" "sync" @@ -164,13 +165,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, opt(controller) } - if controller.configValues != nil { - err := controller.store.AppConfigStore().SetConfigValues(controller.configValues) - if err != nil { - return nil, fmt.Errorf("set app config values: %w", err) - } - } - // If none is provided, use the default env settings from helm to create a RESTClientGetter if controller.restClientGetter == nil { controller.restClientGetter = helmcli.New().RESTClientGetter() @@ -208,5 +202,19 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, ) } + if controller.configValues != nil { + if controller.releaseData == nil || controller.releaseData.AppConfig == nil { + return nil, errors.New("app config not found") + } + err := controller.appConfigManager.ValidateConfigValues(*controller.releaseData.AppConfig, controller.configValues) + if err != nil { + return nil, fmt.Errorf("validate app config values: %w", err) + } + err = controller.appConfigManager.PatchConfigValues(*controller.releaseData.AppConfig, controller.configValues) + if err != nil { + return nil, fmt.Errorf("set app config values: %w", err) + } + } + return controller, nil } diff --git a/api/controllers/kubernetes/install/controller_test.go b/api/controllers/kubernetes/install/controller_test.go index 557f8f54b..7cbb31532 100644 --- a/api/controllers/kubernetes/install/controller_test.go +++ b/api/controllers/kubernetes/install/controller_test.go @@ -299,7 +299,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateSucceeded, setupMocks: func(ki kubernetesinstallation.Installation, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore, am *appconfig.MockAppConfigManager) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, ki, configValues).Return(nil), // TODO: we are not yet reporting // mr.On("ReportInstallationSucceeded", mock.Anything), @@ -313,7 +313,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateInfrastructureInstallFailed, setupMocks: func(ki kubernetesinstallation.Installation, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore, am *appconfig.MockAppConfigManager) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, ki, configValues).Return(errors.New("install error")), st.LinuxInfraMockStore.On("GetStatus").Return(types.Status{Description: "install error"}, nil), // TODO: we are not yet reporting @@ -328,7 +328,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateInfrastructureInstallFailed, setupMocks: func(ki kubernetesinstallation.Installation, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore, am *appconfig.MockAppConfigManager) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, ki, configValues).Return(errors.New("install error")), st.LinuxInfraMockStore.On("GetStatus").Return(nil, assert.AnError), ) @@ -341,7 +341,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateInfrastructureInstallFailed, setupMocks: func(ki kubernetesinstallation.Installation, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore, am *appconfig.MockAppConfigManager) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, ki, configValues).Panic("this is a panic"), st.LinuxInfraMockStore.On("GetStatus").Return(types.Status{Description: "this is a panic"}, nil), // TODO: we are not yet reporting @@ -364,7 +364,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateInstallationConfigured, setupMocks: func(ki kubernetesinstallation.Installation, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore, am *appconfig.MockAppConfigManager) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(kotsv1beta1.ConfigValues{}, assert.AnError), + am.On("GetKotsadmConfigValues", appConfig).Return(kotsv1beta1.ConfigValues{}, assert.AnError), ) }, expectedErr: assert.AnError, diff --git a/api/controllers/kubernetes/install/infra.go b/api/controllers/kubernetes/install/infra.go index 28ed8695a..5ed0639c4 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(ctx, *c.releaseData.AppConfig) + configValues, err := c.appConfigManager.GetKotsadmConfigValues(*c.releaseData.AppConfig) if err != nil { return fmt.Errorf("failed to get kotsadm config values: %w", err) } diff --git a/api/controllers/linux/install/app.go b/api/controllers/linux/install/app.go index 7d36d2a4c..4efd8ed84 100644 --- a/api/controllers/linux/install/app.go +++ b/api/controllers/linux/install/app.go @@ -51,7 +51,12 @@ func (c *InstallController) PatchAppConfigValues(ctx context.Context, values map } }() - err = c.appConfigManager.PatchConfigValues(ctx, *c.releaseData.AppConfig, values) + err = c.appConfigManager.ValidateConfigValues(*c.releaseData.AppConfig, values) + if err != nil { + return fmt.Errorf("validate app config values: %w", err) + } + + err = c.appConfigManager.PatchConfigValues(*c.releaseData.AppConfig, values) if err != nil { return fmt.Errorf("patch app config values: %w", err) } @@ -71,5 +76,5 @@ func (c *InstallController) GetAppConfigValues(ctx context.Context, maskPassword return nil, fmt.Errorf("get app config: %w", err) } - return c.appConfigManager.GetConfigValues(ctx, appConfig, maskPasswords) + return c.appConfigManager.GetConfigValues(appConfig, maskPasswords) } diff --git a/api/controllers/linux/install/app_test.go b/api/controllers/linux/install/app_test.go new file mode 100644 index 000000000..737325aac --- /dev/null +++ b/api/controllers/linux/install/app_test.go @@ -0,0 +1,288 @@ +package install + +import ( + "errors" + "testing" + "time" + + appconfig "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/config" + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" + "github.com/replicatedhq/embedded-cluster/api/internal/store" + "github.com/replicatedhq/embedded-cluster/pkg/release" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/replicatedhq/kotskinds/multitype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestInstallController_PatchAppConfigValues(t *testing.T) { + // Create an app config for testing + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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"}, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + values map[string]string + currentState statemachine.State + expectedState statemachine.State + setupMocks func(*appconfig.MockAppConfigManager, *store.MockStore) + expectedErr bool + }{ + { + name: "successful set app config values", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateNew, + expectedState: StateApplicationConfigured, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful set app config values from application configuration failed state", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateApplicationConfigurationFailed, + expectedState: StateApplicationConfigured, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful set app config values from application configured state", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateApplicationConfigured, + expectedState: StateApplicationConfigured, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "validation error", + values: map[string]string{ + "test-item": "invalid-value", + }, + currentState: StateNew, + expectedState: StateApplicationConfigurationFailed, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "invalid-value"}).Return(errors.New("validation error")), + ) + }, + expectedErr: true, + }, + { + name: "set config values error", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateNew, + expectedState: StateApplicationConfigurationFailed, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + mock.InOrder( + am.On("ValidateConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(nil), + am.On("PatchConfigValues", appConfig, map[string]string{"test-item": "new-value"}).Return(errors.New("set config error")), + ) + }, + expectedErr: true, + }, + { + name: "invalid state transition", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateInfrastructureInstalling, + expectedState: StateInfrastructureInstalling, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + }, + expectedErr: true, + }, + { + name: "app config not found", + values: map[string]string{ + "test-item": "new-value", + }, + currentState: StateNew, + expectedState: StateNew, + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + }, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sm := NewStateMachine(WithCurrentState(tt.currentState)) + + mockAppConfigManager := &appconfig.MockAppConfigManager{} + mockStore := &store.MockStore{} + + tt.setupMocks(mockAppConfigManager, mockStore) + + // For the "app config not found" test case, pass nil as AppConfig + var releaseData *release.ReleaseData + if tt.name == "app config not found" { + releaseData = getTestReleaseData(nil) + } else { + releaseData = getTestReleaseData(&appConfig) + } + + controller, err := NewInstallController( + WithStateMachine(sm), + WithAppConfigManager(mockAppConfigManager), + WithReleaseData(releaseData), + WithStore(mockStore), + ) + require.NoError(t, err) + + err = controller.PatchAppConfigValues(t.Context(), tt.values) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Eventually(t, func() bool { + return sm.CurrentState() == tt.expectedState + }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after setting app config values") + + mockAppConfigManager.AssertExpectations(t) + mockStore.LinuxInfraMockStore.AssertExpectations(t) + mockStore.LinuxInstallationMockStore.AssertExpectations(t) + mockStore.LinuxPreflightMockStore.AssertExpectations(t) + mockStore.AppConfigMockStore.AssertExpectations(t) + }) + } +} + +func TestInstallController_GetAppConfigValues(t *testing.T) { + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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"}, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + setupMocks func(*appconfig.MockAppConfigManager, *store.MockStore) + expectedValues map[string]string + expectedErr bool + }{ + { + name: "successful get app config values", + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + expectedValues := map[string]string{ + "test-item": "test-value", + "another-item": "another-value", + } + am.On("GetConfig", appConfig).Return(appConfig, nil) + am.On("GetConfigValues", appConfig, false).Return(expectedValues, nil) + }, + expectedValues: map[string]string{ + "test-item": "test-value", + "another-item": "another-value", + }, + expectedErr: false, + }, + { + name: "get config values error", + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + am.On("GetConfig", appConfig).Return(appConfig, nil) + am.On("GetConfigValues", appConfig, false).Return(nil, errors.New("get config values error")) + }, + expectedValues: nil, + expectedErr: true, + }, + { + name: "empty config values", + setupMocks: func(am *appconfig.MockAppConfigManager, st *store.MockStore) { + am.On("GetConfig", appConfig).Return(appConfig, nil) + am.On("GetConfigValues", appConfig, false).Return(map[string]string{}, nil) + }, + expectedValues: map[string]string{}, + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAppConfigManager := &appconfig.MockAppConfigManager{} + mockStore := &store.MockStore{} + + tt.setupMocks(mockAppConfigManager, mockStore) + + controller, err := NewInstallController( + WithAppConfigManager(mockAppConfigManager), + WithStore(mockStore), + WithReleaseData(getTestReleaseData(&appConfig)), + ) + require.NoError(t, err) + + values, err := controller.GetAppConfigValues(t.Context(), false) + + if tt.expectedErr { + assert.Error(t, err) + assert.Nil(t, values) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValues, values) + } + + mockAppConfigManager.AssertExpectations(t) + mockStore.LinuxInfraMockStore.AssertExpectations(t) + mockStore.LinuxInstallationMockStore.AssertExpectations(t) + mockStore.LinuxPreflightMockStore.AssertExpectations(t) + mockStore.AppConfigMockStore.AssertExpectations(t) + }) + } +} diff --git a/api/controllers/linux/install/controller.go b/api/controllers/linux/install/controller.go index b11a68644..088d04e90 100644 --- a/api/controllers/linux/install/controller.go +++ b/api/controllers/linux/install/controller.go @@ -2,6 +2,7 @@ package install import ( "context" + "errors" "fmt" "sync" @@ -201,13 +202,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, opt(controller) } - if controller.configValues != nil { - err := controller.store.AppConfigStore().SetConfigValues(controller.configValues) - if err != nil { - return nil, fmt.Errorf("set app config values: %w", err) - } - } - if controller.stateMachine == nil { controller.stateMachine = NewStateMachine(WithStateMachineLogger(controller.logger)) } @@ -262,6 +256,20 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, ) } + if controller.configValues != nil { + if controller.releaseData == nil || controller.releaseData.AppConfig == nil { + return nil, errors.New("app config not found") + } + err := controller.appConfigManager.ValidateConfigValues(*controller.releaseData.AppConfig, controller.configValues) + if err != nil { + return nil, fmt.Errorf("validate app config values: %w", err) + } + err = controller.appConfigManager.PatchConfigValues(*controller.releaseData.AppConfig, controller.configValues) + if err != nil { + return nil, fmt.Errorf("set app config values: %w", err) + } + } + controller.registerReportingHandlers() return controller, nil diff --git a/api/controllers/linux/install/controller_test.go b/api/controllers/linux/install/controller_test.go index 43719fdb4..4f52f6038 100644 --- a/api/controllers/linux/install/controller_test.go +++ b/api/controllers/linux/install/controller_test.go @@ -1058,7 +1058,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateSucceeded, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, am *appconfig.MockAppConfigManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, rc, configValues).Return(nil), mr.On("ReportInstallationSucceeded", mock.Anything), ) @@ -1075,7 +1075,7 @@ func TestSetupInfra(t *testing.T) { mock.InOrder( st.LinuxPreflightMockStore.On("GetOutput").Return(failedPreflightOutput, nil), mr.On("ReportPreflightsBypassed", mock.Anything, failedPreflightOutput), - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, rc, configValues).Return(nil), mr.On("ReportInstallationSucceeded", mock.Anything), ) @@ -1100,7 +1100,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateInfrastructureInstallFailed, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, am *appconfig.MockAppConfigManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, rc, configValues).Return(errors.New("install error")), st.LinuxInfraMockStore.On("GetStatus").Return(types.Status{Description: "install error"}, nil), mr.On("ReportInstallationFailed", mock.Anything, errors.New("install error")), @@ -1116,7 +1116,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateInfrastructureInstallFailed, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, am *appconfig.MockAppConfigManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, rc, configValues).Return(errors.New("install error")), st.LinuxInfraMockStore.On("GetStatus").Return(nil, assert.AnError), ) @@ -1131,7 +1131,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StateInfrastructureInstallFailed, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, am *appconfig.MockAppConfigManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(configValues, nil), + am.On("GetKotsadmConfigValues", appConfig).Return(configValues, nil), fm.On("Install", mock.Anything, rc, configValues).Panic("this is a panic"), st.LinuxInfraMockStore.On("GetStatus").Return(types.Status{Description: "this is a panic"}, nil), mr.On("ReportInstallationFailed", mock.Anything, errors.New("this is a panic")), @@ -1177,7 +1177,7 @@ func TestSetupInfra(t *testing.T) { expectedState: StatePreflightsSucceeded, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, am *appconfig.MockAppConfigManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( - am.On("GetKotsadmConfigValues", mock.Anything, appConfig).Return(kotsv1beta1.ConfigValues{}, assert.AnError), + am.On("GetKotsadmConfigValues", appConfig).Return(kotsv1beta1.ConfigValues{}, assert.AnError), ) }, expectedErr: assert.AnError, diff --git a/api/controllers/linux/install/infra.go b/api/controllers/linux/install/infra.go index b4497d390..21076071a 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(ctx, *c.releaseData.AppConfig) + configValues, err := c.appConfigManager.GetKotsadmConfigValues(*c.releaseData.AppConfig) if err != nil { return fmt.Errorf("failed to get kotsadm config values: %w", err) } diff --git a/api/docs/docs.go b/api/docs/docs.go index 4d3b83af0..186a35764 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -9,7 +9,7 @@ const docTemplate = `{ "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"]},"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"]}}}, + "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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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 1071d7f7a..0693ae495 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -2,7 +2,7 @@ "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"]},"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"]}}}, + "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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Bad Request"}},"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 675469387..5ffd72ad2 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -431,6 +431,12 @@ paths: schema: $ref: '#/components/schemas/types.AppConfig' description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/types.APIError' + description: Bad Request security: - bearerauth: [] summary: Set the app config values @@ -507,6 +513,12 @@ paths: schema: $ref: '#/components/schemas/types.Status' description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/types.APIError' + description: Bad Request security: - bearerauth: [] summary: Configure the Kubernetes installation for install @@ -577,6 +589,12 @@ paths: schema: $ref: '#/components/schemas/types.AppConfig' description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/types.APIError' + description: Bad Request security: - bearerauth: [] summary: Set the app config values @@ -696,6 +714,12 @@ paths: schema: $ref: '#/components/schemas/types.Status' description: OK + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/types.APIError' + description: Bad Request security: - bearerauth: [] summary: Configure the installation for install diff --git a/api/integration/kubernetes/install/app_test.go b/api/integration/kubernetes/install/app_test.go index dcc58ebd0..ec3c5dfcb 100644 --- a/api/integration/kubernetes/install/app_test.go +++ b/api/integration/kubernetes/install/app_test.go @@ -4,13 +4,13 @@ import ( "bytes" _ "embed" "encoding/json" - "fmt" "net/http" "net/http/httptest" "testing" "github.com/gorilla/mux" "github.com/replicatedhq/embedded-cluster/api" + apiclient "github.com/replicatedhq/embedded-cluster/api/client" kubernetesinstall "github.com/replicatedhq/embedded-cluster/api/controllers/kubernetes/install" "github.com/replicatedhq/embedded-cluster/api/integration/auth" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" @@ -87,8 +87,6 @@ func TestKubernetesGetAppConfig(t *testing.T) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) - fmt.Printf("response: %+v\n", rec.Body.String()) - // Parse the response body var response types.AppConfig err = json.NewDecoder(rec.Body).Decode(&response) @@ -143,6 +141,12 @@ func TestKubernetesPatchAppConfigValues(t *testing.T) { Default: multitype.BoolOrString{StrVal: "default2"}, Value: multitype.BoolOrString{StrVal: "value2"}, }, + { + Name: "required-item", + Type: "text", + Title: "Required Item", + Required: true, + }, }, }, }, @@ -177,7 +181,8 @@ func TestKubernetesPatchAppConfigValues(t *testing.T) { // Create a request to set config values setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ - "test-item": "new-value", + "test-item": "new-value", + "required-item": "required-value", }, } @@ -235,7 +240,8 @@ func TestKubernetesPatchAppConfigValues(t *testing.T) { // Create a request to set config values setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ - "test-item": "new-value", + "test-item": "new-value", + "required-item": "required-value", }, } @@ -291,7 +297,8 @@ func TestKubernetesPatchAppConfigValues(t *testing.T) { // Create a request to set config values setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ - "test-item": "new-value", + "test-item": "new-value", + "required-item": "required-value", }, } @@ -318,6 +325,65 @@ func TestKubernetesPatchAppConfigValues(t *testing.T) { assert.Equal(t, http.StatusConflict, apiError.StatusCode) assert.Contains(t, apiError.Message, "invalid transition") }) + + // Test missing required item + t.Run("Missing required item", func(t *testing.T) { + // Create an install controller with the app config + installController, err := kubernetesinstall.NewInstallController( + kubernetesinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + }, + api.WithKubernetesInstallController(installController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request to set config values without the required item + setRequest := types.PatchAppConfigValuesRequest{ + Values: map[string]string{ + "test-item": "new-value", + // required-item is intentionally omitted + }, + } + + reqBodyBytes, err := json.Marshal(setRequest) + require.NoError(t, err) + + // Create a request + 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() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) + + // Parse the response body + var apiError types.APIError + err = json.NewDecoder(rec.Body).Decode(&apiError) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, apiError.StatusCode) + assert.Len(t, apiError.Errors, 1) + assert.Equal(t, apiError.Errors[0].Field, "required-item") + assert.Equal(t, apiError.Errors[0].Message, "item is required") + }) } func TestKubernetesGetAppConfigValues(t *testing.T) { @@ -415,3 +481,241 @@ func TestKubernetesGetAppConfigValues(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, apiError.StatusCode) }) } + +// TestInstallController_PatchAppConfigValuesWithAPIClient tests the PatchAppConfigValues endpoint using the API client +func TestInstallController_PatchAppConfigValuesWithAPIClient(t *testing.T) { + password := "test-password" + + // Create an app config + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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"}, + }, + { + Name: "required-item", + Type: "text", + Title: "Required Item", + Required: true, + }, + }, + }, + }, + }, + } + + // Create an install controller with the app config + installController, err := kubernetesinstall.NewInstallController( + kubernetesinstall.WithStateMachine(kubernetesinstall.NewStateMachine(kubernetesinstall.WithCurrentState(kubernetesinstall.StateNew))), + kubernetesinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: password, + }, + api.WithKubernetesInstallController(installController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router.PathPrefix("/api").Subrouter()) + + // Create a test server using the router + server := httptest.NewServer(router) + defer server.Close() + + // Create client with the predefined token + c := apiclient.New(server.URL, apiclient.WithToken("TOKEN")) + + // Test PatchKubernetesAppConfigValues + t.Run("PatchKubernetesAppConfigValues", func(t *testing.T) { + // Create config values to set + configValues := map[string]string{ + "test-item": "new-value", + "required-item": "required-value", + } + + // Set the app config values using the client + config, err := c.PatchKubernetesAppConfigValues(configValues) + require.NoError(t, err, "PatchKubernetesAppConfigValues should succeed") + + // Verify the raw app config is returned (not the applied values) + assert.Equal(t, "value", config.Groups[0].Items[0].Value.String(), "first item should return raw config schema value") + assert.Equal(t, "", config.Groups[0].Items[1].Value.String(), "second item should return empty value since it has no default") + }) + + // Test PatchKubernetesAppConfigValues with missing required item + t.Run("PatchKubernetesAppConfigValues missing required", func(t *testing.T) { + // Create config values without the required item + configValues := map[string]string{ + "test-item": "new-value", + // required-item is intentionally omitted + } + + // Set the app config values using the client + _, err := c.PatchKubernetesAppConfigValues(configValues) + require.Error(t, err, "PatchKubernetesAppConfigValues should fail with missing required item") + + // Check that the error is of correct type + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Error should be of type *types.APIError") + assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode, "Error should have BadRequest status code") + assert.Len(t, apiErr.Errors, 1, "Should have one validation error") + assert.Equal(t, "required-item", apiErr.Errors[0].Field, "Error should be for required-item field") + assert.Equal(t, "item is required", apiErr.Errors[0].Message, "Error should indicate item is required") + }) + + // Test PatchKubernetesAppConfigValues with invalid state transition + t.Run("PatchKubernetesAppConfigValues invalid state", func(t *testing.T) { + // Create an install controller in a completed state + completedInstallController, err := kubernetesinstall.NewInstallController( + kubernetesinstall.WithStateMachine(kubernetesinstall.NewStateMachine(kubernetesinstall.WithCurrentState(kubernetesinstall.StateSucceeded))), + kubernetesinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the completed install controller + completedAPIInstance, err := api.New( + types.APIConfig{ + Password: password, + }, + api.WithKubernetesInstallController(completedInstallController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + completedRouter := mux.NewRouter() + completedAPIInstance.RegisterRoutes(completedRouter.PathPrefix("/api").Subrouter()) + + // Create a test server using the router + completedServer := httptest.NewServer(completedRouter) + defer completedServer.Close() + + // Create client with the predefined token + completedClient := apiclient.New(completedServer.URL, apiclient.WithToken("TOKEN")) + + // Create config values to set + configValues := map[string]string{ + "test-item": "new-value", + "required-item": "required-value", + } + + // Set the app config values using the client + _, err = completedClient.PatchKubernetesAppConfigValues(configValues) + require.Error(t, err, "PatchKubernetesAppConfigValues should fail with invalid state transition") + + // Check that the error is of correct type + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Error should be of type *types.APIError") + assert.Equal(t, http.StatusConflict, apiErr.StatusCode, "Error should have Conflict status code") + assert.Contains(t, apiErr.Message, "invalid transition", "Error should mention invalid transition") + }) +} + +// TestInstallController_GetAppConfigValuesWithAPIClient tests the GetAppConfigValues endpoint using the API client +func TestInstallController_GetAppConfigValuesWithAPIClient(t *testing.T) { + password := "test-password" + + // Create an app config + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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 config values that should be applied to the config + configValues := map[string]string{ + "test-item": "applied-value", + } + + // Create an install controller with the config values + installController, err := kubernetesinstall.NewInstallController( + kubernetesinstall.WithConfigValues(configValues), + kubernetesinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: password, + }, + api.WithKubernetesInstallController(installController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router.PathPrefix("/api").Subrouter()) + + // Create a test server using the router + server := httptest.NewServer(router) + defer server.Close() + + // Create client with the predefined token + c := apiclient.New(server.URL, apiclient.WithToken("TOKEN")) + + // Test GetKubernetesAppConfigValues + t.Run("GetKubernetesAppConfigValues", func(t *testing.T) { + // Get the app config values using the client + values, err := c.GetKubernetesAppConfigValues() + require.NoError(t, err, "GetKubernetesAppConfigValues should succeed") + + // Verify the app config values are returned from the store + assert.Equal(t, configValues, values, "app config values should be returned from store") + }) + + // Test GetKubernetesAppConfigValues with invalid token + t.Run("GetKubernetesAppConfigValues unauthorized", func(t *testing.T) { + // Create client with invalid token + invalidClient := apiclient.New(server.URL, apiclient.WithToken("INVALID_TOKEN")) + + // Get the app config values using the client + _, err := invalidClient.GetKubernetesAppConfigValues() + require.Error(t, err, "GetKubernetesAppConfigValues should fail with invalid token") + + // Check that the error is of correct type + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Error should be of type *types.APIError") + assert.Equal(t, http.StatusUnauthorized, apiErr.StatusCode, "Error should have Unauthorized status code") + }) +} diff --git a/api/integration/linux/install/app_test.go b/api/integration/linux/install/app_test.go index 23132353c..d742d399d 100644 --- a/api/integration/linux/install/app_test.go +++ b/api/integration/linux/install/app_test.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/mux" "github.com/replicatedhq/embedded-cluster/api" + apiclient "github.com/replicatedhq/embedded-cluster/api/client" linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/integration/auth" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" @@ -140,6 +141,12 @@ func TestLinuxPatchAppConfigValues(t *testing.T) { Default: multitype.BoolOrString{StrVal: "default2"}, Value: multitype.BoolOrString{StrVal: "value2"}, }, + { + Name: "required-item", + Type: "text", + Title: "Required Item", + Required: true, + }, }, }, }, @@ -175,7 +182,8 @@ func TestLinuxPatchAppConfigValues(t *testing.T) { // Create a request to set config values setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ - "test-item": "new-value", + "test-item": "new-value", + "required-item": "required-value", }, } @@ -234,7 +242,8 @@ func TestLinuxPatchAppConfigValues(t *testing.T) { // Create a request to set config values setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ - "test-item": "new-value", + "test-item": "new-value", + "required-item": "required-value", }, } @@ -290,7 +299,8 @@ func TestLinuxPatchAppConfigValues(t *testing.T) { // Create a request to set config values setRequest := types.PatchAppConfigValuesRequest{ Values: map[string]string{ - "test-item": "new-value", + "test-item": "new-value", + "required-item": "required-value", }, } @@ -317,6 +327,65 @@ func TestLinuxPatchAppConfigValues(t *testing.T) { assert.Equal(t, http.StatusConflict, apiError.StatusCode) assert.Contains(t, apiError.Message, "invalid transition") }) + + // Test missing required item + t.Run("Missing required item", func(t *testing.T) { + // Create an install controller with the app config + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request to set config values without the required item + setRequest := types.PatchAppConfigValuesRequest{ + Values: map[string]string{ + "test-item": "new-value", + // required-item is intentionally omitted + }, + } + + reqBodyBytes, err := json.Marshal(setRequest) + require.NoError(t, err) + + // Create a request + 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() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) + + // Parse the response body + var apiError types.APIError + err = json.NewDecoder(rec.Body).Decode(&apiError) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, apiError.StatusCode) + assert.Len(t, apiError.Errors, 1) + assert.Equal(t, apiError.Errors[0].Field, "required-item") + assert.Equal(t, apiError.Errors[0].Message, "item is required") + }) } func TestLinuxGetAppConfigValues(t *testing.T) { @@ -414,3 +483,241 @@ func TestLinuxGetAppConfigValues(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, apiError.StatusCode) }) } + +// TestInstallController_PatchAppConfigValuesWithAPIClient tests the PatchAppConfigValues endpoint using the API client +func TestInstallController_PatchAppConfigValuesWithAPIClient(t *testing.T) { + password := "test-password" + + // Create an app config + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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"}, + }, + { + Name: "required-item", + Type: "text", + Title: "Required Item", + Required: true, + }, + }, + }, + }, + }, + } + + // Create an install controller with the app config + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateNew))), + linuxinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: password, + }, + api.WithLinuxInstallController(installController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router.PathPrefix("/api").Subrouter()) + + // Create a test server using the router + server := httptest.NewServer(router) + defer server.Close() + + // Create client with the predefined token + c := apiclient.New(server.URL, apiclient.WithToken("TOKEN")) + + // Test PatchLinuxAppConfigValues + t.Run("PatchLinuxAppConfigValues", func(t *testing.T) { + // Create config values to set + configValues := map[string]string{ + "test-item": "new-value", + "required-item": "required-value", + } + + // Set the app config values using the client + config, err := c.PatchLinuxAppConfigValues(configValues) + require.NoError(t, err, "PatchLinuxAppConfigValues should succeed") + + // Verify the raw app config is returned (not the applied values) + assert.Equal(t, "value", config.Groups[0].Items[0].Value.String(), "first item should return raw config schema value") + assert.Equal(t, "", config.Groups[0].Items[1].Value.String(), "second item should return empty value since it has no default") + }) + + // Test PatchLinuxAppConfigValues with missing required item + t.Run("PatchLinuxAppConfigValues missing required", func(t *testing.T) { + // Create config values without the required item + configValues := map[string]string{ + "test-item": "new-value", + // required-item is intentionally omitted + } + + // Set the app config values using the client + _, err := c.PatchLinuxAppConfigValues(configValues) + require.Error(t, err, "PatchLinuxAppConfigValues should fail with missing required item") + + // Check that the error is of correct type + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Error should be of type *types.APIError") + assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode, "Error should have BadRequest status code") + assert.Len(t, apiErr.Errors, 1, "Should have one validation error") + assert.Equal(t, "required-item", apiErr.Errors[0].Field, "Error should be for required-item field") + assert.Equal(t, "item is required", apiErr.Errors[0].Message, "Error should indicate item is required") + }) + + // Test PatchLinuxAppConfigValues with invalid state transition + t.Run("PatchLinuxAppConfigValues invalid state", func(t *testing.T) { + // Create an install controller in a completed state + completedInstallController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateSucceeded))), + linuxinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the completed install controller + completedAPIInstance, err := api.New( + types.APIConfig{ + Password: password, + }, + api.WithLinuxInstallController(completedInstallController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + completedRouter := mux.NewRouter() + completedAPIInstance.RegisterRoutes(completedRouter.PathPrefix("/api").Subrouter()) + + // Create a test server using the router + completedServer := httptest.NewServer(completedRouter) + defer completedServer.Close() + + // Create client with the predefined token + completedClient := apiclient.New(completedServer.URL, apiclient.WithToken("TOKEN")) + + // Create config values to set + configValues := map[string]string{ + "test-item": "new-value", + "required-item": "required-value", + } + + // Set the app config values using the client + _, err = completedClient.PatchLinuxAppConfigValues(configValues) + require.Error(t, err, "PatchLinuxAppConfigValues should fail with invalid state transition") + + // Check that the error is of correct type + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Error should be of type *types.APIError") + assert.Equal(t, http.StatusConflict, apiErr.StatusCode, "Error should have Conflict status code") + assert.Contains(t, apiErr.Message, "invalid transition", "Error should mention invalid transition") + }) +} + +// TestInstallController_GetAppConfigValuesWithAPIClient tests the GetAppConfigValues endpoint using the API client +func TestInstallController_GetAppConfigValuesWithAPIClient(t *testing.T) { + password := "test-password" + + // Create an app config + appConfig := kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + 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 config values that should be applied to the config + configValues := map[string]string{ + "test-item": "applied-value", + } + + // Create an install controller with the config values + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithConfigValues(configValues), + linuxinstall.WithReleaseData(&release.ReleaseData{ + AppConfig: &appConfig, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: password, + }, + api.WithLinuxInstallController(installController), + api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router.PathPrefix("/api").Subrouter()) + + // Create a test server using the router + server := httptest.NewServer(router) + defer server.Close() + + // Create client with the predefined token + c := apiclient.New(server.URL, apiclient.WithToken("TOKEN")) + + // Test GetLinuxAppConfigValues + t.Run("GetLinuxAppConfigValues", func(t *testing.T) { + // Get the app config values using the client + values, err := c.GetLinuxAppConfigValues() + require.NoError(t, err, "GetLinuxAppConfigValues should succeed") + + // Verify the app config values are returned from the store + assert.Equal(t, configValues, values, "app config values should be returned from store") + }) + + // Test GetLinuxAppConfigValues with invalid token + t.Run("GetLinuxAppConfigValues unauthorized", func(t *testing.T) { + // Create client with invalid token + invalidClient := apiclient.New(server.URL, apiclient.WithToken("INVALID_TOKEN")) + + // Get the app config values using the client + _, err := invalidClient.GetLinuxAppConfigValues() + require.Error(t, err, "GetLinuxAppConfigValues should fail with invalid token") + + // Check that the error is of correct type + apiErr, ok := err.(*types.APIError) + require.True(t, ok, "Error should be of type *types.APIError") + assert.Equal(t, http.StatusUnauthorized, apiErr.StatusCode, "Error should have Unauthorized status code") + }) +} diff --git a/api/internal/handlers/kubernetes/install.go b/api/internal/handlers/kubernetes/install.go index 12e9dd0e0..91207a874 100644 --- a/api/internal/handlers/kubernetes/install.go +++ b/api/internal/handlers/kubernetes/install.go @@ -39,6 +39,7 @@ func (h *Handler) GetInstallationConfig(w http.ResponseWriter, r *http.Request) // @Produce json // @Param installationConfig body types.KubernetesInstallationConfig true "Installation config" // @Success 200 {object} types.Status +// @Failure 400 {object} types.APIError // @Router /kubernetes/install/installation/configure [post] func (h *Handler) PostConfigureInstallation(w http.ResponseWriter, r *http.Request) { var config types.KubernetesInstallationConfig @@ -151,6 +152,7 @@ func (h *Handler) GetAppConfig(w http.ResponseWriter, r *http.Request) { // @Produce json // @Param request body types.PatchAppConfigValuesRequest true "Patch App Config Values Request" // @Success 200 {object} types.AppConfig +// @Failure 400 {object} types.APIError // @Router /kubernetes/install/app/config/values [patch] func (h *Handler) PatchConfigValues(w http.ResponseWriter, r *http.Request) { var req types.PatchAppConfigValuesRequest diff --git a/api/internal/handlers/linux/install.go b/api/internal/handlers/linux/install.go index d3cfdb2e6..04a611fd4 100644 --- a/api/internal/handlers/linux/install.go +++ b/api/internal/handlers/linux/install.go @@ -40,6 +40,7 @@ func (h *Handler) GetInstallationConfig(w http.ResponseWriter, r *http.Request) // @Produce json // @Param installationConfig body types.LinuxInstallationConfig true "Installation config" // @Success 200 {object} types.Status +// @Failure 400 {object} types.APIError // @Router /linux/install/installation/configure [post] func (h *Handler) PostConfigureInstallation(w http.ResponseWriter, r *http.Request) { var config types.LinuxInstallationConfig @@ -230,6 +231,7 @@ func (h *Handler) GetAppConfig(w http.ResponseWriter, r *http.Request) { // @Produce json // @Param request body types.PatchAppConfigValuesRequest true "Patch App Config Values Request" // @Success 200 {object} types.AppConfig +// @Failure 400 {object} types.APIError // @Router /linux/install/app/config/values [patch] func (h *Handler) PatchConfigValues(w http.ResponseWriter, r *http.Request) { var req types.PatchAppConfigValuesRequest diff --git a/api/internal/managers/app/config/config.go b/api/internal/managers/app/config/config.go index ace611f03..99cbe8451 100644 --- a/api/internal/managers/app/config/config.go +++ b/api/internal/managers/app/config/config.go @@ -1,10 +1,11 @@ package config import ( - "context" + "errors" "fmt" "maps" + "github.com/replicatedhq/embedded-cluster/api/types" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" "github.com/tiendc/go-deepcopy" @@ -16,12 +17,38 @@ const ( PasswordMask = "••••••••" ) +var ( + // ErrConfigItemRequired is returned when a required item is not set + ErrConfigItemRequired = errors.New("item is required") +) + func (m *appConfigManager) GetConfig(config kotsv1beta1.Config) (kotsv1beta1.Config, error) { return filterAppConfig(config) } +func (m *appConfigManager) ValidateConfigValues(config kotsv1beta1.Config, configValues map[string]string) error { + var ve *types.APIError + + filteredConfig, err := filterAppConfig(config) + if err != nil { + return fmt.Errorf("filter app config: %w", err) + } + + // check required items + for _, group := range filteredConfig.Spec.Groups { + for _, item := range group.Items { + configValue := getConfigValueFromItem(item, configValues) + if isRequiredItem(item) && isUnsetItem(configValue) { + ve = types.AppendFieldError(ve, item.Name, ErrConfigItemRequired) + } + } + } + + return ve.ErrorOrNil() +} + // PatchConfigValues performs a partial update by merging new values with existing ones -func (m *appConfigManager) PatchConfigValues(ctx context.Context, config kotsv1beta1.Config, newValues map[string]string) error { +func (m *appConfigManager) PatchConfigValues(config kotsv1beta1.Config, newValues map[string]string) error { // Get existing values existingValues, err := m.appConfigStore.GetConfigValues() if err != nil { @@ -56,7 +83,7 @@ func (m *appConfigManager) PatchConfigValues(ctx context.Context, config kotsv1b } // GetConfigValues returns config values with optional password field masking -func (m *appConfigManager) GetConfigValues(ctx context.Context, config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { +func (m *appConfigManager) GetConfigValues(config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { configValues, err := m.appConfigStore.GetConfigValues() if err != nil { return nil, err @@ -92,13 +119,13 @@ func (m *appConfigManager) GetConfigValues(ctx context.Context, config kotsv1bet return maskedValues, nil } -func (m *appConfigManager) GetKotsadmConfigValues(ctx context.Context, config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { +func (m *appConfigManager) GetKotsadmConfigValues(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(ctx, filteredConfig, false) + storedValues, err := m.GetConfigValues(filteredConfig, false) if err != nil { return kotsv1beta1.ConfigValues{}, fmt.Errorf("get config values: %w", err) } @@ -119,42 +146,10 @@ func (m *appConfigManager) GetKotsadmConfigValues(ctx context.Context, config ko // add values from the filtered config for _, group := range filteredConfig.Spec.Groups { for _, item := range group.Items { - configValue := kotsv1beta1.ConfigValue{ - 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 { - if item.Type == "password" { - configValue.ValuePlaintext = value - } else { - configValue.Value = value - } - } - kotsadmConfigValues.Spec.Values[item.Name] = configValue + kotsadmConfigValues.Spec.Values[item.Name] = getConfigValueFromItem(item, storedValues) - for _, subItem := range item.Items { - subConfigValue := kotsv1beta1.ConfigValue{ - Default: subItem.Default.String(), - } - if item.Type == "password" { - subConfigValue.ValuePlaintext = subItem.Value.String() - } else { - subConfigValue.Value = subItem.Value.String() - } - // override values from the config values store - if value, ok := storedValues[subItem.Name]; ok { - if item.Type == "password" { - subConfigValue.ValuePlaintext = value - } else { - subConfigValue.Value = value - } - } - kotsadmConfigValues.Spec.Values[subItem.Name] = subConfigValue + for _, childItem := range item.Items { + kotsadmConfigValues.Spec.Values[childItem.Name] = getConfigValueFromChildItem(item.Type, childItem, storedValues) } } } @@ -162,6 +157,50 @@ func (m *appConfigManager) GetKotsadmConfigValues(ctx context.Context, config ko return kotsadmConfigValues, nil } +func getConfigValueFromItem(item kotsv1beta1.ConfigItem, configValues map[string]string) kotsv1beta1.ConfigValue { + configValue := kotsv1beta1.ConfigValue{ + 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 := configValues[item.Name]; ok { + if item.Type == "password" { + configValue.ValuePlaintext = value + } else { + configValue.Value = value + } + } + + return configValue +} + +func getConfigValueFromChildItem(itemType string, childItem kotsv1beta1.ConfigChildItem, configValues map[string]string) kotsv1beta1.ConfigValue { + configValue := kotsv1beta1.ConfigValue{ + Default: childItem.Default.String(), + } + if itemType == "password" { + configValue.ValuePlaintext = childItem.Value.String() + } else { + configValue.Value = childItem.Value.String() + } + + // override values from the config values store + if value, ok := configValues[childItem.Name]; ok { + if itemType == "password" { + configValue.ValuePlaintext = value + } else { + configValue.Value = value + } + } + + return configValue +} + // filterAppConfig filters out disabled groups and items based on their 'when' condition func filterAppConfig(config kotsv1beta1.Config) (kotsv1beta1.Config, error) { // deepcopy the config to avoid mutating the original config @@ -196,3 +235,23 @@ func filterAppConfig(config kotsv1beta1.Config) (kotsv1beta1.Config, error) { func isItemEnabled(when multitype.QuotedBool) bool { return when != "false" } + +// isRequiredItem checks if an item is required based on whether Required is true and the item is +// enabled and not hidden +func isRequiredItem(item kotsv1beta1.ConfigItem) bool { + if !item.Required { + return false + } + if !isItemEnabled(item.When) { + return false + } + if item.Hidden { + return false + } + return true +} + +func isUnsetItem(configValue kotsv1beta1.ConfigValue) bool { + // TODO: repeatable items + return configValue.Value == "" && configValue.Default == "" +} diff --git a/api/internal/managers/app/config/config_test.go b/api/internal/managers/app/config/config_test.go index 366ca36d2..7479f87f2 100644 --- a/api/internal/managers/app/config/config_test.go +++ b/api/internal/managers/app/config/config_test.go @@ -1,7 +1,6 @@ package config import ( - "context" "errors" "testing" @@ -12,6 +11,8 @@ import ( "github.com/stretchr/testify/require" "github.com/tiendc/go-deepcopy" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/replicatedhq/embedded-cluster/api/types" ) func TestAppConfigManager_GetConfig(t *testing.T) { @@ -389,7 +390,7 @@ func TestAppConfigManager_PatchConfigValues(t *testing.T) { wantErr: false, }, { - name: "disabled group keeps original values", + name: "disabled group with enabled items - items should be filtered out", config: kotsv1beta1.Config{ Spec: kotsv1beta1.ConfigSpec{ Groups: []kotsv1beta1.ConfigGroup{ @@ -437,7 +438,47 @@ func TestAppConfigManager_PatchConfigValues(t *testing.T) { wantErr: false, }, { - name: "disabled item keeps original value", + name: "disabled group with disabled items - items should be filtered out", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "disabled-group", + Title: "Disabled Group", + When: "false", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "disabled-item-in-disabled-group", + Title: "Disabled Item in Disabled Group", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-value"}, + When: "false", + }, + { + Name: "enabled-item-in-disabled-group", + Title: "Enabled Item in Disabled Group", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-enabled-value"}, + When: "true", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "disabled-item-in-disabled-group": "new-disabled-value", + "enabled-item-in-disabled-group": "new-enabled-value", + }, + setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) + expectedValues := map[string]string{} + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, + { + name: "enabled group with disabled item - disabled item should be filtered out", config: kotsv1beta1.Config{ Spec: kotsv1beta1.ConfigSpec{ Groups: []kotsv1beta1.ConfigGroup{ @@ -487,7 +528,7 @@ func TestAppConfigManager_PatchConfigValues(t *testing.T) { wantErr: false, }, { - name: "mixed enabled and disabled items", + name: "mixed enabled and disabled items in enabled group", config: kotsv1beta1.Config{ Spec: kotsv1beta1.ConfigSpec{ Groups: []kotsv1beta1.ConfigGroup{ @@ -547,6 +588,79 @@ func TestAppConfigManager_PatchConfigValues(t *testing.T) { }, wantErr: false, }, + { + name: "multiple groups with mixed enabled/disabled states", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "enabled-group-1", + Title: "Enabled Group 1", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "enabled-item-1", + Title: "Enabled Item 1", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-1"}, + When: "true", + }, + { + Name: "disabled-item-1", + Title: "Disabled Item 1", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-disabled-1"}, + When: "false", + }, + }, + }, + { + Name: "disabled-group", + Title: "Disabled Group", + When: "false", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "item-in-disabled-group", + Title: "Item in Disabled Group", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-disabled-group"}, + When: "true", + }, + }, + }, + { + Name: "enabled-group-2", + Title: "Enabled Group 2", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "enabled-item-2", + Title: "Enabled Item 2", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-2"}, + When: "true", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "enabled-item-1": "new-value-1", + "disabled-item-1": "new-disabled-value-1", + "item-in-disabled-group": "new-disabled-group-value", + "enabled-item-2": "new-value-2", + }, + 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", + } + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, { name: "empty config values", config: kotsv1beta1.Config{ @@ -980,6 +1094,87 @@ func TestAppConfigManager_PatchConfigValues(t *testing.T) { }, wantErr: false, }, + { + name: "empty config with no groups", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{}, + }, + }, + newValues: map[string]string{ + "some-item": "some-value", + }, + setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) + expectedValues := map[string]string{} + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, + { + name: "enabled group with empty items", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "empty-group", + Title: "Empty Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{}, + }, + }, + }, + }, + newValues: map[string]string{ + "some-item": "some-value", + }, + setupMock: func(mockStore *config.MockStore) { + mockStore.On("GetConfigValues").Return(map[string]string{}, nil) + expectedValues := map[string]string{} + mockStore.On("SetConfigValues", expectedValues).Return(nil) + }, + wantErr: false, + }, + { + name: "enabled group with all disabled items", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "all-disabled-items-group", + Title: "All Disabled Items Group", + When: "true", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "disabled-item-1", + Title: "Disabled Item 1", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-1"}, + When: "false", + }, + { + Name: "disabled-item-2", + Title: "Disabled Item 2", + Type: "text", + Value: multitype.BoolOrString{StrVal: "original-2"}, + When: "false", + }, + }, + }, + }, + }, + }, + newValues: map[string]string{ + "disabled-item-1": "new-value-1", + "disabled-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) + }, + wantErr: false, + }, } for _, tt := range tests { @@ -994,7 +1189,7 @@ func TestAppConfigManager_PatchConfigValues(t *testing.T) { } // Call PatchConfigValues - err := manager.PatchConfigValues(context.Background(), tt.config, tt.newValues) + err := manager.PatchConfigValues(tt.config, tt.newValues) // Verify expectations if tt.wantErr { @@ -1309,7 +1504,7 @@ func TestAppConfigManager_GetConfigValues(t *testing.T) { } // Call GetConfigValues - result, err := manager.GetConfigValues(context.Background(), tt.appConfig, tt.maskPasswords) + result, err := manager.GetConfigValues(tt.appConfig, tt.maskPasswords) // Verify expectations if tt.wantErr { @@ -2064,7 +2259,7 @@ func TestAppConfigManager_GetKotsadmConfigValues(t *testing.T) { } // Call GetKotsadmConfigValues - result, err := manager.GetKotsadmConfigValues(t.Context(), tt.config) + result, err := manager.GetKotsadmConfigValues(tt.config) // Verify expectations if tt.wantErr { @@ -2079,3 +2274,298 @@ func TestAppConfigManager_GetKotsadmConfigValues(t *testing.T) { }) } } + +func TestValidateConfigValues(t *testing.T) { + tests := []struct { + name string + config kotsv1beta1.Config + configValues map[string]string + wantErr bool + errorFields []string + }{ + { + name: "valid config with all required items set", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "required_item", + Required: true, + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{}, + }, + { + Name: "optional_item", + Required: false, + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{ + "required_item": "value1", + "optional_item": "value2", + }, + wantErr: false, + }, + { + name: "missing required item", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "required_item", + Required: true, + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{ + "optional_item": "value1", + }, + wantErr: true, + errorFields: []string{"required_item"}, + }, + { + name: "required item with default value should not be required", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "required_with_default", + Required: true, + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{StrVal: "default_value"}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{}, + wantErr: false, + }, + { + name: "required item with value and no config value should not be required", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "required_with_value", + Required: true, + Value: multitype.BoolOrString{StrVal: "item_value"}, + Default: multitype.BoolOrString{}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{}, + wantErr: false, + }, + { + name: "required item with value and empty config value should be required", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "required_with_value", + Required: true, + Value: multitype.BoolOrString{StrVal: "item_value"}, + Default: multitype.BoolOrString{}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{ + "required_with_value": "", + }, + wantErr: true, + errorFields: []string{"required_with_value"}, + }, + { + name: "hidden required item should not be required", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "hidden_required", + Required: true, + Hidden: true, + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{}, + wantErr: false, + }, + { + name: "disabled required item should not be required", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "disabled_required", + Required: true, + When: "false", + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{}, + wantErr: false, + }, + { + name: "child item validation", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "parent_item", + Items: []kotsv1beta1.ConfigChildItem{ + { + Name: "child_item", + }, + }, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{ + "child_item": "child_value", + }, + wantErr: false, + }, + { + name: "multiple validation errors", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{ + { + Name: "group1", + Items: []kotsv1beta1.ConfigItem{ + { + Name: "required_item1", + Required: true, + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{}, + }, + { + Name: "required_item2", + Required: true, + Value: multitype.BoolOrString{}, + Default: multitype.BoolOrString{}, + }, + }, + }, + }, + }, + }, + configValues: map[string]string{ + "unknown_item1": "value1", + "unknown_item2": "value2", + }, + wantErr: true, + errorFields: []string{"required_item1", "required_item2"}, + }, + { + name: "empty config and values", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{}, + }, + }, + configValues: map[string]string{}, + wantErr: false, + }, + { + name: "empty config with unknown values", + config: kotsv1beta1.Config{ + Spec: kotsv1beta1.ConfigSpec{ + Groups: []kotsv1beta1.ConfigGroup{}, + }, + }, + configValues: map[string]string{ + "unknown_item": "value1", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a real appConfigManager instance for testing + manager := &appConfigManager{} + + // Run the validation + err := manager.ValidateConfigValues(tt.config, tt.configValues) + + // Check if error is expected + if tt.wantErr { + require.Error(t, err, "Expected validation to fail") + + // Check if it's an APIError with field errors + var apiErr *types.APIError + if assert.ErrorAs(t, err, &apiErr) { + // Verify that all expected error fields are present + for _, field := range tt.errorFields { + found := false + for _, fieldErr := range apiErr.Errors { + if fieldErr.Field == field { + found = true + break + } + } + assert.True(t, found, "Expected error for field %s", field) + } + } + } else { + assert.NoError(t, err, "Expected validation to succeed") + } + }) + } +} diff --git a/api/internal/managers/app/config/manager.go b/api/internal/managers/app/config/manager.go index 7444f81e2..eb926c059 100644 --- a/api/internal/managers/app/config/manager.go +++ b/api/internal/managers/app/config/manager.go @@ -1,8 +1,6 @@ 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" @@ -15,13 +13,15 @@ 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, config kotsv1beta1.Config, values map[string]string) error // GetConfigValues returns the current config values - GetConfigValues(ctx context.Context, config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) + GetConfigValues(config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) + // ValidateConfigValues validates the config values + ValidateConfigValues(config kotsv1beta1.Config, values map[string]string) error + // PatchConfigValues patches the current config values + PatchConfigValues(config kotsv1beta1.Config, values map[string]string) error // GetKotsadmConfigValues merges the config values with the app config defaults and returns a // kotsv1beta1.ConfigValues struct. - GetKotsadmConfigValues(ctx context.Context, config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) + GetKotsadmConfigValues(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 90eb32566..b72026687 100644 --- a/api/internal/managers/app/config/manager_mock.go +++ b/api/internal/managers/app/config/manager_mock.go @@ -1,8 +1,6 @@ package config import ( - "context" - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/stretchr/testify/mock" ) @@ -20,23 +18,29 @@ 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(ctx context.Context, config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { - args := m.Called(ctx, config, maskPasswords) +func (m *MockAppConfigManager) GetConfigValues(config kotsv1beta1.Config, maskPasswords bool) (map[string]string, error) { + args := m.Called(config, maskPasswords) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(map[string]string), args.Error(1) } +// ValidateConfigValues mocks the ValidateConfigValues method +func (m *MockAppConfigManager) ValidateConfigValues(config kotsv1beta1.Config, configValues map[string]string) error { + args := m.Called(config, configValues) + return args.Error(0) +} + +// PatchConfigValues mocks the PatchConfigValues method +func (m *MockAppConfigManager) PatchConfigValues(config kotsv1beta1.Config, values map[string]string) error { + args := m.Called(config, values) + return args.Error(0) +} + // GetKotsadmConfigValues mocks the GetKotsadmConfigValues method -func (m *MockAppConfigManager) GetKotsadmConfigValues(ctx context.Context, config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { - args := m.Called(ctx, config) +func (m *MockAppConfigManager) GetKotsadmConfigValues(config kotsv1beta1.Config) (kotsv1beta1.ConfigValues, error) { + args := m.Called(config) return args.Get(0).(kotsv1beta1.ConfigValues), args.Error(1) } diff --git a/e2e/scripts/check-airgap-post-ha-state.sh b/e2e/scripts/check-airgap-post-ha-state.sh index 0b1ed2872..cd6a44084 100755 --- a/e2e/scripts/check-airgap-post-ha-state.sh +++ b/e2e/scripts/check-airgap-post-ha-state.sh @@ -73,7 +73,7 @@ main() { kubectl scale -n "$APP_NAMESPACE" deployment/second --replicas=4 echo "waiting for the second deployment to scale up" for _ in {1..60}; do - if kubectl get pods -n "$APP_NAMESPACE" -o wide | grep -q "second-"; then + if kubectl get pods -n "$APP_NAMESPACE" | grep -q "second-"; then break fi sleep 1 diff --git a/e2e/scripts/common.sh b/e2e/scripts/common.sh index 030b1d3b4..314b57125 100755 --- a/e2e/scripts/common.sh +++ b/e2e/scripts/common.sh @@ -137,7 +137,7 @@ wait_for_pods_running() { current_time=$(date +%s) elapsed_time=$((current_time - start_time)) if [ "$elapsed_time" -ge "$timeout" ]; then - kubectl get pods -A -o yaml || true + kubectl get pods -A || true kubectl describe nodes || true echo "Timed out waiting for all pods to be running." return 1 @@ -146,7 +146,6 @@ wait_for_pods_running() { non_running_pods=$(kubectl get pods --all-namespaces --no-headers 2>/dev/null | awk '$4 != "Running" && $4 != "Completed" { print $0 }' | wc -l || echo 1) if [ "$non_running_pods" -ne 0 ]; then echo "Not all pods are running. Waiting." - kubectl get pods,nodes -A || true sleep 5 continue fi