diff --git a/api/controllers/app/install/controller.go b/api/controllers/app/install/controller.go index 5c86420492..71359ec132 100644 --- a/api/controllers/app/install/controller.go +++ b/api/controllers/app/install/controller.go @@ -16,7 +16,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/sirupsen/logrus" - "k8s.io/cli-runtime/pkg/genericclioptions" kyaml "sigs.k8s.io/yaml" ) @@ -28,7 +27,7 @@ type Controller interface { GetAppPreflightStatus(ctx context.Context) (types.Status, error) GetAppPreflightOutput(ctx context.Context) (*types.PreflightsOutput, error) GetAppPreflightTitles(ctx context.Context) ([]string, error) - InstallApp(ctx context.Context, opts InstallAppOptions) error + InstallApp(ctx context.Context, ignoreAppPreflights bool) error GetAppInstallStatus(ctx context.Context) (types.AppInstall, error) } @@ -48,8 +47,6 @@ type InstallController struct { clusterID string airgapBundle string privateCACertConfigMapName string - restClientGetter genericclioptions.RESTClientGetter - kubeConfigPath string } type InstallControllerOption func(*InstallController) @@ -132,18 +129,6 @@ func WithPrivateCACertConfigMapName(configMapName string) InstallControllerOptio } } -func WithRESTClientGetter(restClientGetter genericclioptions.RESTClientGetter) InstallControllerOption { - return func(c *InstallController) { - c.restClientGetter = restClientGetter - } -} - -func WithKubeConfigPath(kubeConfigPath string) InstallControllerOption { - return func(c *InstallController) { - c.kubeConfigPath = kubeConfigPath - } -} - func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { controller := &InstallController{ logger: logger.NewDiscardLogger(), @@ -220,8 +205,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, appinstallmanager.WithClusterID(controller.clusterID), appinstallmanager.WithAirgapBundle(controller.airgapBundle), appinstallmanager.WithAppInstallStore(controller.store.AppInstallStore()), - appinstallmanager.WithRESTClientGetter(controller.restClientGetter), - appinstallmanager.WithKubeConfigPath(controller.kubeConfigPath), ) if err != nil { return nil, fmt.Errorf("create app install manager: %w", err) diff --git a/api/controllers/app/install/controller_mock.go b/api/controllers/app/install/controller_mock.go index bd6eb7df3a..c1b5f5389e 100644 --- a/api/controllers/app/install/controller_mock.go +++ b/api/controllers/app/install/controller_mock.go @@ -69,8 +69,8 @@ func (m *MockController) GetAppPreflightTitles(ctx context.Context) ([]string, e } // InstallApp mocks the InstallApp method -func (m *MockController) InstallApp(ctx context.Context, opts InstallAppOptions) error { - args := m.Called(ctx, opts) +func (m *MockController) InstallApp(ctx context.Context, ignoreAppPreflights bool) error { + args := m.Called(ctx, ignoreAppPreflights) return args.Error(0) } diff --git a/api/controllers/app/install/install.go b/api/controllers/app/install/install.go index 4f0e73e5d6..d2bb177398 100644 --- a/api/controllers/app/install/install.go +++ b/api/controllers/app/install/install.go @@ -8,21 +8,14 @@ import ( states "github.com/replicatedhq/embedded-cluster/api/internal/states/install" "github.com/replicatedhq/embedded-cluster/api/types" - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" ) var ( ErrAppPreflightChecksFailed = errors.New("app preflight checks failed") ) -type InstallAppOptions struct { - IgnoreAppPreflights bool - ProxySpec *ecv1beta1.ProxySpec - RegistrySettings *types.RegistrySettings -} - // InstallApp triggers app installation with proper state transitions and panic handling -func (c *InstallController) InstallApp(ctx context.Context, opts InstallAppOptions) (finalErr error) { +func (c *InstallController) InstallApp(ctx context.Context, ignoreAppPreflights bool) (finalErr error) { lock, err := c.stateMachine.AcquireLock() if err != nil { return types.NewConflictError(err) @@ -40,7 +33,7 @@ func (c *InstallController) InstallApp(ctx context.Context, opts InstallAppOptio // Check if app preflights have failed and if we should ignore them if c.stateMachine.CurrentState() == states.StateAppPreflightsFailed { allowIgnoreAppPreflights := true // TODO: implement once we check for strict app preflights - if !opts.IgnoreAppPreflights || !allowIgnoreAppPreflights { + if !ignoreAppPreflights || !allowIgnoreAppPreflights { return types.NewBadRequestError(ErrAppPreflightChecksFailed) } err = c.stateMachine.Transition(lock, states.StateAppPreflightsFailedBypassed) @@ -54,15 +47,9 @@ func (c *InstallController) InstallApp(ctx context.Context, opts InstallAppOptio } // Get config values for app installation - appConfigValues, err := c.GetAppConfigValues(ctx) - if err != nil { - return fmt.Errorf("get app config values for app install: %w", err) - } - - // Get KOTS config values for the KOTS CLI - kotsConfigValues, err := c.appConfigManager.GetKotsadmConfigValues() + configValues, err := c.appConfigManager.GetKotsadmConfigValues() if err != nil { - return fmt.Errorf("get kots config values for app install: %w", err) + return fmt.Errorf("get kotsadm config values for app install: %w", err) } err = c.stateMachine.Transition(lock, states.StateAppInstalling) @@ -93,14 +80,8 @@ func (c *InstallController) InstallApp(ctx context.Context, opts InstallAppOptio } }() - // Extract installable Helm charts from release manager - installableCharts, err := c.appReleaseManager.ExtractInstallableHelmCharts(ctx, appConfigValues, opts.ProxySpec, opts.RegistrySettings) - if err != nil { - return fmt.Errorf("extract installable helm charts: %w", err) - } - - // Install the app with installable charts and kots config values - err = c.appInstallManager.Install(ctx, installableCharts, kotsConfigValues) + // Install the app + err := c.appInstallManager.Install(ctx, configValues) if err != nil { return fmt.Errorf("install app: %w", err) } diff --git a/api/controllers/app/install/test_suite.go b/api/controllers/app/install/test_suite.go index 37992c92e4..b91bc8bdfa 100644 --- a/api/controllers/app/install/test_suite.go +++ b/api/controllers/app/install/test_suite.go @@ -13,7 +13,6 @@ import ( states "github.com/replicatedhq/embedded-cluster/api/internal/states/install" "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/types" - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" @@ -504,18 +503,16 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { tests := []struct { name string ignoreAppPreflights bool - proxySpec *ecv1beta1.ProxySpec - registrySettings *types.RegistrySettings currentState statemachine.State expectedState statemachine.State - setupMocks func(*appconfig.MockAppConfigManager, *appreleasemanager.MockAppReleaseManager, *appinstallmanager.MockAppInstallManager) + setupMocks func(*appconfig.MockAppConfigManager, *appinstallmanager.MockAppInstallManager) expectedErr bool }{ { name: "invalid state transition from succeeded state", currentState: states.StateSucceeded, expectedState: states.StateSucceeded, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { + setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) { // No mocks needed for invalid state transition }, expectedErr: true, @@ -524,37 +521,27 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { name: "invalid state transition from infrastructure installing state", currentState: states.StateInfrastructureInstalling, expectedState: states.StateInfrastructureInstalling, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { + setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) { // No mocks needed for invalid state transition }, expectedErr: true, }, { - name: "successful app installation from app preflights succeeded state with helm charts", + name: "successful app installation from app preflights succeeded state", currentState: states.StateAppPreflightsSucceeded, expectedState: states.StateSucceeded, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { - configValues := kotsv1beta1.ConfigValues{ - Spec: kotsv1beta1.ConfigValuesSpec{ - Values: map[string]kotsv1beta1.ConfigValue{ - "test-key": {Value: "test-value"}, - }, - }, - } - expectedCharts := []types.InstallableHelmChart{ - { - Archive: []byte("chart-archive-data"), - Values: map[string]any{"key": "value"}, - }, - } - appConfigValues := types.AppConfigValues{ - "test-key": types.AppConfigValue{Value: "test-value"}, - } + setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) { mock.InOrder( - acm.On("GetConfigValues").Return(appConfigValues, nil), - acm.On("GetKotsadmConfigValues").Return(configValues, nil), - arm.On("ExtractInstallableHelmCharts", mock.Anything, appConfigValues, mock.AnythingOfType("*v1beta1.ProxySpec"), mock.AnythingOfType("*types.RegistrySettings")).Return(expectedCharts, nil), - aim.On("Install", mock.Anything, expectedCharts, configValues).Return(nil), + acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ + Values: map[string]kotsv1beta1.ConfigValue{ + "test-key": {Value: "test-value"}, + }, + }, + }, nil), + aim.On("Install", mock.Anything, mock.MatchedBy(func(cv kotsv1beta1.ConfigValues) bool { + return cv.Spec.Values["test-key"].Value == "test-value" + })).Return(nil), ) }, expectedErr: false, @@ -563,22 +550,18 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { name: "successful app installation from app preflights failed bypassed state", currentState: states.StateAppPreflightsFailedBypassed, expectedState: states.StateSucceeded, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { - configValues := kotsv1beta1.ConfigValues{ - Spec: kotsv1beta1.ConfigValuesSpec{ - Values: map[string]kotsv1beta1.ConfigValue{ - "test-key": {Value: "test-value"}, - }, - }, - } - appConfigValues := types.AppConfigValues{ - "test-key": types.AppConfigValue{Value: "test-value"}, - } + setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) { mock.InOrder( - acm.On("GetConfigValues").Return(appConfigValues, nil), - acm.On("GetKotsadmConfigValues").Return(configValues, nil), - arm.On("ExtractInstallableHelmCharts", mock.Anything, appConfigValues, mock.AnythingOfType("*v1beta1.ProxySpec"), mock.AnythingOfType("*types.RegistrySettings")).Return([]types.InstallableHelmChart{}, nil), - aim.On("Install", mock.Anything, []types.InstallableHelmChart{}, configValues).Return(nil), + acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ + Values: map[string]kotsv1beta1.ConfigValue{ + "test-key": {Value: "test-value"}, + }, + }, + }, nil), + aim.On("Install", mock.Anything, mock.MatchedBy(func(cv kotsv1beta1.ConfigValues) bool { + return cv.Spec.Values["test-key"].Value == "test-value" + })).Return(nil), ) }, expectedErr: false, @@ -587,11 +570,7 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { name: "get config values error", currentState: states.StateAppPreflightsSucceeded, expectedState: states.StateAppPreflightsSucceeded, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { - appConfigValues := types.AppConfigValues{ - "test-key": types.AppConfigValue{Value: "test-value"}, - } - acm.On("GetConfigValues").Return(appConfigValues, nil) + setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) { acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{}, errors.New("config values error")) }, expectedErr: true, @@ -601,22 +580,18 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { ignoreAppPreflights: true, currentState: states.StateAppPreflightsFailed, expectedState: states.StateSucceeded, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { - configValues := kotsv1beta1.ConfigValues{ - Spec: kotsv1beta1.ConfigValuesSpec{ - Values: map[string]kotsv1beta1.ConfigValue{ - "test-key": {Value: "test-value"}, - }, - }, - } - appConfigValues := types.AppConfigValues{ - "test-key": types.AppConfigValue{Value: "test-value"}, - } + setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) { mock.InOrder( - acm.On("GetConfigValues").Return(appConfigValues, nil), - acm.On("GetKotsadmConfigValues").Return(configValues, nil), - arm.On("ExtractInstallableHelmCharts", mock.Anything, appConfigValues, mock.AnythingOfType("*v1beta1.ProxySpec"), mock.AnythingOfType("*types.RegistrySettings")).Return([]types.InstallableHelmChart{}, nil), - aim.On("Install", mock.Anything, []types.InstallableHelmChart{}, configValues).Return(nil), + acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{ + Spec: kotsv1beta1.ConfigValuesSpec{ + Values: map[string]kotsv1beta1.ConfigValue{ + "test-key": {Value: "test-value"}, + }, + }, + }, nil), + aim.On("Install", mock.Anything, mock.MatchedBy(func(cv kotsv1beta1.ConfigValues) bool { + return cv.Spec.Values["test-key"].Value == "test-value" + })).Return(nil), ) }, expectedErr: false, @@ -626,54 +601,11 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { ignoreAppPreflights: false, currentState: states.StateAppPreflightsFailed, expectedState: states.StateAppPreflightsFailed, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { + setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) { // No mocks needed as method should return early with error }, expectedErr: true, }, - { - name: "successful app installation with proxy spec passed to helm chart extraction", - currentState: states.StateAppPreflightsSucceeded, - expectedState: states.StateSucceeded, - proxySpec: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://proxy.example.com:8080", - HTTPSProxy: "https://proxy.example.com:8080", - NoProxy: "localhost,127.0.0.1", - }, - registrySettings: &types.RegistrySettings{ - HasLocalRegistry: true, - Host: "10.128.0.11:5000", - }, - setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) { - configValues := kotsv1beta1.ConfigValues{ - Spec: kotsv1beta1.ConfigValuesSpec{ - Values: map[string]kotsv1beta1.ConfigValue{ - "test-key": {Value: "test-value"}, - }, - }, - } - appConfigValues := types.AppConfigValues{ - "test-key": types.AppConfigValue{Value: "test-value"}, - } - expectedCharts := []types.InstallableHelmChart{ - { - Archive: []byte("chart-with-proxy-template"), - Values: map[string]any{"proxy_url": "http://proxy.example.com:8080"}, - }, - } - mock.InOrder( - acm.On("GetConfigValues").Return(appConfigValues, nil), - acm.On("GetKotsadmConfigValues").Return(configValues, nil), - arm.On("ExtractInstallableHelmCharts", mock.Anything, appConfigValues, mock.MatchedBy(func(proxySpec *ecv1beta1.ProxySpec) bool { - return proxySpec != nil - }), mock.MatchedBy(func(registrySettings *types.RegistrySettings) bool { - return registrySettings != nil - })).Return(expectedCharts, nil), - aim.On("Install", mock.Anything, expectedCharts, configValues).Return(nil), - ) - }, - expectedErr: false, - }, } for _, tt := range tests { @@ -695,12 +627,8 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { ) require.NoError(t, err, "failed to create install controller") - tt.setupMocks(appConfigManager, appReleaseManager, appInstallManager) - err = controller.InstallApp(t.Context(), InstallAppOptions{ - IgnoreAppPreflights: tt.ignoreAppPreflights, - ProxySpec: tt.proxySpec, - RegistrySettings: tt.registrySettings, - }) + tt.setupMocks(appConfigManager, appInstallManager) + err = controller.InstallApp(t.Context(), tt.ignoreAppPreflights) if tt.expectedErr { assert.Error(t, err) @@ -715,7 +643,6 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() { assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after app installation") appConfigManager.AssertExpectations(s.T()) - appReleaseManager.AssertExpectations(s.T()) appInstallManager.AssertExpectations(s.T()) }) } diff --git a/api/controllers/kubernetes/install/controller.go b/api/controllers/kubernetes/install/controller.go index f2702b8f8d..ebee11fa04 100644 --- a/api/controllers/kubernetes/install/controller.go +++ b/api/controllers/kubernetes/install/controller.go @@ -192,7 +192,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, appcontroller.WithConfigValues(controller.configValues), appcontroller.WithAirgapBundle(controller.airgapBundle), appcontroller.WithPrivateCACertConfigMapName(""), // Private CA ConfigMap functionality not yet implemented for Kubernetes installations - appcontroller.WithRESTClientGetter(controller.restClientGetter), ) if err != nil { return nil, fmt.Errorf("create app install controller: %w", err) diff --git a/api/controllers/kubernetes/install/controller_mock.go b/api/controllers/kubernetes/install/controller_mock.go index 158512ecfd..25e1a744d3 100644 --- a/api/controllers/kubernetes/install/controller_mock.go +++ b/api/controllers/kubernetes/install/controller_mock.go @@ -109,8 +109,8 @@ func (m *MockController) RunAppPreflights(ctx context.Context, opts appcontrolle } // InstallApp mocks the InstallApp method -func (m *MockController) InstallApp(ctx context.Context, opts appcontroller.InstallAppOptions) error { - args := m.Called(ctx, opts) +func (m *MockController) InstallApp(ctx context.Context, ignoreAppPreflights bool) error { + args := m.Called(ctx, ignoreAppPreflights) return args.Error(0) } diff --git a/api/controllers/linux/install/controller.go b/api/controllers/linux/install/controller.go index 3f74608380..580d062b08 100644 --- a/api/controllers/linux/install/controller.go +++ b/api/controllers/linux/install/controller.go @@ -266,7 +266,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, appcontroller.WithClusterID(controller.clusterID), appcontroller.WithAirgapBundle(controller.airgapBundle), appcontroller.WithPrivateCACertConfigMapName(adminconsole.PrivateCASConfigMapName), // Linux installations use the ConfigMap - appcontroller.WithKubeConfigPath(controller.rc.PathToKubeConfig()), ) if err != nil { return nil, fmt.Errorf("create app install controller: %w", err) diff --git a/api/controllers/linux/install/controller_mock.go b/api/controllers/linux/install/controller_mock.go index 70354b49ae..4751894864 100644 --- a/api/controllers/linux/install/controller_mock.go +++ b/api/controllers/linux/install/controller_mock.go @@ -142,8 +142,8 @@ func (m *MockController) RunAppPreflights(ctx context.Context, opts appcontrolle } // InstallApp mocks the InstallApp method -func (m *MockController) InstallApp(ctx context.Context, opts appcontroller.InstallAppOptions) error { - args := m.Called(ctx, opts) +func (m *MockController) InstallApp(ctx context.Context, ignoreAppPreflights bool) error { + args := m.Called(ctx, ignoreAppPreflights) return args.Error(0) } diff --git a/api/integration/kubernetes/install/appinstall_test.go b/api/integration/kubernetes/install/appinstall_test.go index 42cf8c728e..138a14fa37 100644 --- a/api/integration/kubernetes/install/appinstall_test.go +++ b/api/integration/kubernetes/install/appinstall_test.go @@ -15,19 +15,15 @@ import ( kubernetesinstall "github.com/replicatedhq/embedded-cluster/api/controllers/kubernetes/install" "github.com/replicatedhq/embedded-cluster/api/integration" "github.com/replicatedhq/embedded-cluster/api/integration/auth" - appconfig "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/config" appinstallmanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/install" - appreleasemanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/release" states "github.com/replicatedhq/embedded-cluster/api/internal/states/install" "github.com/replicatedhq/embedded-cluster/api/internal/store" appinstallstore "github.com/replicatedhq/embedded-cluster/api/internal/store/app/install" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -179,63 +175,11 @@ func TestGetAppInstallStatus(t *testing.T) { // TestPostInstallApp tests the POST /kubernetes/install/app/install endpoint func TestPostInstallApp(t *testing.T) { - // Create test release data - releaseData := &release.ReleaseData{ - EmbeddedClusterConfig: &ecv1beta1.Config{}, - ChannelRelease: &release.ChannelRelease{ - DefaultDomains: release.Domains{ - ReplicatedAppDomain: "replicated.example.com", - ProxyRegistryDomain: "some-proxy.example.com", - }, - }, - AppConfig: &kotsv1beta1.Config{}, - } - t.Run("Success", func(t *testing.T) { - // Create mock app and kots config values - testAppConfigValues := types.AppConfigValues{ - "service": { - Value: "ClusterIP", - }, - } - testKotsConfigValues := kotsv1beta1.ConfigValues{ - Spec: kotsv1beta1.ConfigValuesSpec{ - Values: map[string]kotsv1beta1.ConfigValue{ - "enable_ingress": { - Value: "1", - }, - }, - }, - } - - // Create app config manager with mock store - mockAppConfigManager := &appconfig.MockAppConfigManager{} - mockAppConfigManager.On("GetConfigValues").Return(testAppConfigValues, nil) - mockAppConfigManager.On("GetKotsadmConfigValues").Return(testKotsConfigValues, nil) - - // Create mock app release manager that returns installable charts - mockAppReleaseManager := &appreleasemanager.MockAppReleaseManager{} - testInstallableCharts := []types.InstallableHelmChart{ - { - Archive: []byte("mock-helm-chart-archive-data"), - Values: map[string]any{ - "service": map[string]any{ - "port": 80, - }, - }, - CR: &kotsv1beta2.HelmChart{ - Spec: kotsv1beta2.HelmChartSpec{ - ReleaseName: "test-app", - Namespace: "default", - }, - }, - }, - } - mockAppReleaseManager.On("ExtractInstallableHelmCharts", mock.Anything, testAppConfigValues, mock.AnythingOfType("*v1beta1.ProxySpec"), mock.AnythingOfType("*types.RegistrySettings")).Return(testInstallableCharts, nil) // Create mock app install manager that succeeds mockAppInstallManager := &appinstallmanager.MockAppInstallManager{} - mockAppInstallManager.On("Install", mock.Anything, testInstallableCharts, testKotsConfigValues).Return(nil) + mockAppInstallManager.On("Install", mock.Anything, mock.Anything).Return(nil) mockAppInstallManager.On("GetStatus").Return(types.AppInstall{ Status: types.Status{ State: types.StateRunning, @@ -243,19 +187,22 @@ func TestPostInstallApp(t *testing.T) { }, }, nil) + // Create mock store + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{}, nil) + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + // Create state machine starting from AppPreflightsSucceeded (valid state for app install) stateMachine := kubernetesinstall.NewStateMachine( kubernetesinstall.WithCurrentState(states.StateAppPreflightsSucceeded), ) - // Create real app install controller with mock managers + // Create real app install controller with mock manager appInstallController, err := appinstall.NewInstallController( appinstall.WithAppInstallManager(mockAppInstallManager), - appinstall.WithAppReleaseManager(mockAppReleaseManager), - appinstall.WithAppConfigManager(mockAppConfigManager), appinstall.WithStateMachine(stateMachine), - appinstall.WithStore(&store.MockStore{}), - appinstall.WithReleaseData(releaseData), + appinstall.WithStore(mockStore), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -263,24 +210,25 @@ func TestPostInstallApp(t *testing.T) { installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppInstallController(appInstallController), - kubernetesinstall.WithReleaseData(releaseData), + kubernetesinstall.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{ + DefaultDomains: release.Domains{ + ReplicatedAppDomain: "replicated.example.com", + ProxyRegistryDomain: "some-proxy.example.com", + }, + }, + AppConfig: &kotsv1beta1.Config{}, + }), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - KubernetesConfig: types.KubernetesConfig{ - Installation: kubernetesinstallation.New(nil), - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) // Create a new router and register API routes router := mux.NewRouter() @@ -303,8 +251,6 @@ func TestPostInstallApp(t *testing.T) { }, 10*time.Second, 100*time.Millisecond, "state should transition to Succeeded") // Verify that the app install manager was called (no metrics reporting verification) - mockAppConfigManager.AssertExpectations(t) - mockAppReleaseManager.AssertExpectations(t) mockAppInstallManager.AssertExpectations(t) }) @@ -319,7 +265,7 @@ func TestPostInstallApp(t *testing.T) { appInstallController, err := appinstall.NewInstallController( appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -327,24 +273,16 @@ func TestPostInstallApp(t *testing.T) { installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppInstallController(appInstallController), - kubernetesinstall.WithReleaseData(releaseData), + kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - KubernetesConfig: types.KubernetesConfig{ - Installation: kubernetesinstallation.New(nil), - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) // Create a new router and register API routes router := mux.NewRouter() @@ -365,7 +303,7 @@ func TestPostInstallApp(t *testing.T) { t.Run("App installation failure", func(t *testing.T) { // Create mock app install manager that fails mockAppInstallManager := &appinstallmanager.MockAppInstallManager{} - mockAppInstallManager.On("Install", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("installation failed")) + mockAppInstallManager.On("Install", mock.Anything, mock.Anything).Return(errors.New("installation failed")) mockAppInstallManager.On("GetStatus").Return(types.AppInstall{ Status: types.Status{ State: types.StateFailed, @@ -388,7 +326,7 @@ func TestPostInstallApp(t *testing.T) { appinstall.WithAppInstallManager(mockAppInstallManager), appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -396,24 +334,16 @@ func TestPostInstallApp(t *testing.T) { installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppInstallController(appInstallController), - kubernetesinstall.WithReleaseData(releaseData), + kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - KubernetesConfig: types.KubernetesConfig{ - Installation: kubernetesinstallation.New(nil), - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) // Create a new router and register API routes router := mux.NewRouter() @@ -442,7 +372,7 @@ func TestPostInstallApp(t *testing.T) { t.Run("Authorization error", func(t *testing.T) { // Create simple Kubernetes install controller installController, err := kubernetesinstall.NewInstallController( - kubernetesinstall.WithReleaseData(releaseData), + kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -477,7 +407,7 @@ func TestPostInstallApp(t *testing.T) { // Create mock app install manager that succeeds mockAppInstallManager := &appinstallmanager.MockAppInstallManager{} - mockAppInstallManager.On("Install", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockAppInstallManager.On("Install", mock.Anything, mock.Anything).Return(nil) mockAppInstallManager.On("GetStatus").Return(types.AppInstall{ Status: types.Status{ State: types.StateRunning, @@ -495,7 +425,7 @@ func TestPostInstallApp(t *testing.T) { appinstall.WithAppInstallManager(mockAppInstallManager), appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -503,24 +433,16 @@ func TestPostInstallApp(t *testing.T) { installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppInstallController(appInstallController), - kubernetesinstall.WithReleaseData(releaseData), + kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - KubernetesConfig: types.KubernetesConfig{ - Installation: kubernetesinstallation.New(nil), - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) router := mux.NewRouter() apiInstance.RegisterRoutes(router) @@ -566,7 +488,7 @@ func TestPostInstallApp(t *testing.T) { appInstallController, err := appinstall.NewInstallController( appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -574,24 +496,16 @@ func TestPostInstallApp(t *testing.T) { installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppInstallController(appInstallController), - kubernetesinstall.WithReleaseData(releaseData), + kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - KubernetesConfig: types.KubernetesConfig{ - Installation: kubernetesinstallation.New(nil), - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) router := mux.NewRouter() apiInstance.RegisterRoutes(router) diff --git a/api/integration/linux/install/appinstall_test.go b/api/integration/linux/install/appinstall_test.go index 8f47dcb640..49a44eae1c 100644 --- a/api/integration/linux/install/appinstall_test.go +++ b/api/integration/linux/install/appinstall_test.go @@ -15,9 +15,7 @@ import ( linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/integration" "github.com/replicatedhq/embedded-cluster/api/integration/auth" - appconfig "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/config" appinstallmanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/install" - appreleasemanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/release" states "github.com/replicatedhq/embedded-cluster/api/internal/states/install" "github.com/replicatedhq/embedded-cluster/api/internal/store" appinstallstore "github.com/replicatedhq/embedded-cluster/api/internal/store/app/install" @@ -28,7 +26,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -36,22 +33,6 @@ import ( // TestGetAppInstallStatus tests the GET /linux/install/app/status endpoint func TestGetAppInstallStatus(t *testing.T) { - // Create mock helm chart archive - mockChartArchive := []byte("mock-helm-chart-archive-data") - - // Create test release data with helm chart archives - releaseData := &release.ReleaseData{ - HelmChartArchives: [][]byte{mockChartArchive}, - EmbeddedClusterConfig: &ecv1beta1.Config{}, - ChannelRelease: &release.ChannelRelease{ - DefaultDomains: release.Domains{ - ReplicatedAppDomain: "replicated.example.com", - ProxyRegistryDomain: "some-proxy.example.com", - }, - }, - AppConfig: &kotsv1beta1.Config{}, - } - t.Run("Success", func(t *testing.T) { // Create app install status appInstallStatus := types.AppInstall{ @@ -78,7 +59,7 @@ func TestGetAppInstallStatus(t *testing.T) { appinstall.WithAppInstallManager(appInstallManager), appinstall.WithStateMachine(linuxinstall.NewStateMachine()), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -86,7 +67,16 @@ func TestGetAppInstallStatus(t *testing.T) { installController, err := linuxinstall.NewInstallController( linuxinstall.WithStateMachine(linuxinstall.NewStateMachine()), linuxinstall.WithAppInstallController(appInstallController), - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{ + DefaultDomains: release.Domains{ + ReplicatedAppDomain: "replicated.example.com", + ProxyRegistryDomain: "some-proxy.example.com", + }, + }, + AppConfig: &kotsv1beta1.Config{}, + }), linuxinstall.WithRuntimeConfig(runtimeconfig.New(nil)), ) require.NoError(t, err) @@ -128,7 +118,7 @@ func TestGetAppInstallStatus(t *testing.T) { t.Run("Authorization error", func(t *testing.T) { // Create simple Linux install controller installController, err := linuxinstall.NewInstallController( - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -188,18 +178,6 @@ func TestGetAppInstallStatus(t *testing.T) { // TestPostInstallApp tests the POST /linux/install/app/install endpoint func TestPostInstallApp(t *testing.T) { - // Create test release data - releaseData := &release.ReleaseData{ - EmbeddedClusterConfig: &ecv1beta1.Config{}, - ChannelRelease: &release.ChannelRelease{ - DefaultDomains: release.Domains{ - ReplicatedAppDomain: "replicated.example.com", - ProxyRegistryDomain: "some-proxy.example.com", - }, - }, - AppConfig: &kotsv1beta1.Config{}, - } - t.Run("Success", func(t *testing.T) { // Create a real runtime config with temp directory rc := runtimeconfig.New(nil) @@ -209,50 +187,9 @@ func TestPostInstallApp(t *testing.T) { mockReporter := &metrics.MockReporter{} mockReporter.On("ReportInstallationSucceeded", mock.Anything) - // Create mock app and kots config values - testAppConfigValues := types.AppConfigValues{ - "service": { - Value: "ClusterIP", - }, - } - testKotsConfigValues := kotsv1beta1.ConfigValues{ - Spec: kotsv1beta1.ConfigValuesSpec{ - Values: map[string]kotsv1beta1.ConfigValue{ - "enable_ingress": { - Value: "1", - }, - }, - }, - } - - // Create app config manager with mock store - mockAppConfigManager := &appconfig.MockAppConfigManager{} - mockAppConfigManager.On("GetConfigValues").Return(testAppConfigValues, nil) - mockAppConfigManager.On("GetKotsadmConfigValues").Return(testKotsConfigValues, nil) - - // Create mock app release manager that returns installable charts - mockAppReleaseManager := &appreleasemanager.MockAppReleaseManager{} - testInstallableCharts := []types.InstallableHelmChart{ - { - Archive: []byte("mock-helm-chart-archive-data"), - Values: map[string]any{ - "service": map[string]any{ - "port": 80, - }, - }, - CR: &kotsv1beta2.HelmChart{ - Spec: kotsv1beta2.HelmChartSpec{ - ReleaseName: "test-app", - Namespace: "default", - }, - }, - }, - } - mockAppReleaseManager.On("ExtractInstallableHelmCharts", mock.Anything, testAppConfigValues, mock.AnythingOfType("*v1beta1.ProxySpec"), mock.AnythingOfType("*types.RegistrySettings")).Return(testInstallableCharts, nil) - // Create mock app install manager that succeeds mockAppInstallManager := &appinstallmanager.MockAppInstallManager{} - mockAppInstallManager.On("Install", mock.Anything, testInstallableCharts, testKotsConfigValues).Return(nil) + mockAppInstallManager.On("Install", mock.Anything, mock.Anything).Return(nil) mockAppInstallManager.On("GetStatus").Return(types.AppInstall{ Status: types.Status{ State: types.StateRunning, @@ -260,19 +197,22 @@ func TestPostInstallApp(t *testing.T) { }, }, nil) + // Create mock store + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{}, nil) + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + // Create state machine starting from AppPreflightsSucceeded (valid state for app install) stateMachine := linuxinstall.NewStateMachine( linuxinstall.WithCurrentState(states.StateAppPreflightsSucceeded), ) - // Create real app install controller with mock managers + // Create real app install controller with mock manager appInstallController, err := appinstall.NewInstallController( appinstall.WithAppInstallManager(mockAppInstallManager), - appinstall.WithAppReleaseManager(mockAppReleaseManager), - appinstall.WithAppConfigManager(mockAppConfigManager), appinstall.WithStateMachine(stateMachine), - appinstall.WithStore(&store.MockStore{}), - appinstall.WithReleaseData(releaseData), + appinstall.WithStore(mockStore), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -281,25 +221,26 @@ func TestPostInstallApp(t *testing.T) { linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppInstallController(appInstallController), linuxinstall.WithMetricsReporter(mockReporter), - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{ + DefaultDomains: release.Domains{ + ReplicatedAppDomain: "replicated.example.com", + ProxyRegistryDomain: "some-proxy.example.com", + }, + }, + AppConfig: &kotsv1beta1.Config{}, + }), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - LinuxConfig: types.LinuxConfig{ - RuntimeConfig: rc, - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) // Create a new router and register API routes router := mux.NewRouter() @@ -323,16 +264,10 @@ func TestPostInstallApp(t *testing.T) { // Verify that ReportInstallationSucceeded was called mockReporter.AssertExpectations(t) - mockAppConfigManager.AssertExpectations(t) - mockAppReleaseManager.AssertExpectations(t) mockAppInstallManager.AssertExpectations(t) }) t.Run("Invalid state transition", func(t *testing.T) { - // Create a real runtime config with temp directory - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - // Create state machine starting from invalid state (infrastructure installing) stateMachine := linuxinstall.NewStateMachine( linuxinstall.WithCurrentState(states.StateInfrastructureInstalling), @@ -343,7 +278,7 @@ func TestPostInstallApp(t *testing.T) { appInstallController, err := appinstall.NewInstallController( appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -351,24 +286,16 @@ func TestPostInstallApp(t *testing.T) { installController, err := linuxinstall.NewInstallController( linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppInstallController(appInstallController), - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - LinuxConfig: types.LinuxConfig{ - RuntimeConfig: rc, - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) // Create a new router and register API routes router := mux.NewRouter() @@ -399,7 +326,7 @@ func TestPostInstallApp(t *testing.T) { // Create mock app install manager that fails mockAppInstallManager := &appinstallmanager.MockAppInstallManager{} - mockAppInstallManager.On("Install", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("installation failed")) + mockAppInstallManager.On("Install", mock.Anything, mock.Anything).Return(errors.New("installation failed")) mockAppInstallManager.On("GetStatus").Return(types.AppInstall{ Status: types.Status{ State: types.StateFailed, @@ -426,7 +353,7 @@ func TestPostInstallApp(t *testing.T) { appinstall.WithAppInstallManager(mockAppInstallManager), appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -436,25 +363,17 @@ func TestPostInstallApp(t *testing.T) { linuxinstall.WithAppInstallController(appInstallController), linuxinstall.WithMetricsReporter(mockReporter), linuxinstall.WithStore(mockStore), - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(integration.DefaultReleaseData()), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - LinuxConfig: types.LinuxConfig{ - RuntimeConfig: rc, - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) // Create a new router and register API routes router := mux.NewRouter() @@ -483,30 +402,18 @@ func TestPostInstallApp(t *testing.T) { }) t.Run("Authorization error", func(t *testing.T) { - // Create a real runtime config with temp directory - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - // Create simple Linux install controller installController, err := linuxinstall.NewInstallController( - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - LinuxConfig: types.LinuxConfig{ - RuntimeConfig: rc, - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) // Create a new router and register API routes router := mux.NewRouter() @@ -525,10 +432,6 @@ func TestPostInstallApp(t *testing.T) { // Test app preflight bypass - should succeed when ignoreAppPreflights is true t.Run("App preflight bypass with failed preflights - ignored", func(t *testing.T) { - // Create a real runtime config with temp directory - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - // Create mock store mockStore := &store.MockStore{} mockStore.AppConfigMockStore.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{}, nil) @@ -536,7 +439,7 @@ func TestPostInstallApp(t *testing.T) { // Create mock app install manager that succeeds mockAppInstallManager := &appinstallmanager.MockAppInstallManager{} - mockAppInstallManager.On("Install", mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockAppInstallManager.On("Install", mock.Anything, mock.Anything).Return(nil) mockAppInstallManager.On("GetStatus").Return(types.AppInstall{ Status: types.Status{ State: types.StateRunning, @@ -554,7 +457,7 @@ func TestPostInstallApp(t *testing.T) { appinstall.WithAppInstallManager(mockAppInstallManager), appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -562,24 +465,16 @@ func TestPostInstallApp(t *testing.T) { installController, err := linuxinstall.NewInstallController( linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppInstallController(appInstallController), - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - LinuxConfig: types.LinuxConfig{ - RuntimeConfig: rc, - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) router := mux.NewRouter() apiInstance.RegisterRoutes(router) @@ -613,10 +508,6 @@ func TestPostInstallApp(t *testing.T) { // Test app preflight bypass denied - should fail when ignoreAppPreflights is false and preflights failed t.Run("App preflight bypass denied with failed preflights", func(t *testing.T) { - // Create a real runtime config with temp directory - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - // Create mock store mockStore := &store.MockStore{} @@ -629,7 +520,7 @@ func TestPostInstallApp(t *testing.T) { appInstallController, err := appinstall.NewInstallController( appinstall.WithStateMachine(stateMachine), appinstall.WithStore(mockStore), - appinstall.WithReleaseData(releaseData), + appinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) @@ -637,24 +528,16 @@ func TestPostInstallApp(t *testing.T) { installController, err := linuxinstall.NewInstallController( linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppInstallController(appInstallController), - linuxinstall.WithReleaseData(releaseData), + linuxinstall.WithReleaseData(integration.DefaultReleaseData()), ) require.NoError(t, err) // Create the API - cfg := types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - LinuxConfig: types.LinuxConfig{ - RuntimeConfig: rc, - }, - } - apiInstance, err := api.New(cfg, + apiInstance := integration.NewAPIWithReleaseData(t, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) - require.NoError(t, err) router := mux.NewRouter() apiInstance.RegisterRoutes(router) diff --git a/api/internal/handlers/kubernetes/install.go b/api/internal/handlers/kubernetes/install.go index 6977d626cd..911c638ea2 100644 --- a/api/internal/handlers/kubernetes/install.go +++ b/api/internal/handlers/kubernetes/install.go @@ -300,11 +300,7 @@ func (h *Handler) PostInstallApp(w http.ResponseWriter, r *http.Request) { return } - err := h.installController.InstallApp(r.Context(), appinstall.InstallAppOptions{ - IgnoreAppPreflights: req.IgnoreAppPreflights, - ProxySpec: h.cfg.Installation.ProxySpec(), - RegistrySettings: nil, - }) + err := h.installController.InstallApp(r.Context(), req.IgnoreAppPreflights) if err != nil { utils.LogError(r, err, h.logger, "failed to install app") utils.JSONError(w, r, err, h.logger) diff --git a/api/internal/handlers/linux/install.go b/api/internal/handlers/linux/install.go index d74124bd6b..3c789fd3a3 100644 --- a/api/internal/handlers/linux/install.go +++ b/api/internal/handlers/linux/install.go @@ -380,18 +380,7 @@ func (h *Handler) PostInstallApp(w http.ResponseWriter, r *http.Request) { return } - registrySettings, err := h.installController.CalculateRegistrySettings(r.Context()) - if err != nil { - utils.LogError(r, err, h.logger, "failed to calculate registry settings") - utils.JSONError(w, r, err, h.logger) - return - } - - err = h.installController.InstallApp(r.Context(), appinstall.InstallAppOptions{ - IgnoreAppPreflights: req.IgnoreAppPreflights, - ProxySpec: h.cfg.RuntimeConfig.ProxySpec(), - RegistrySettings: registrySettings, - }) + err := h.installController.InstallApp(r.Context(), req.IgnoreAppPreflights) if err != nil { utils.LogError(r, err, h.logger, "failed to install app") utils.JSONError(w, r, err, h.logger) diff --git a/api/internal/managers/app/install/install.go b/api/internal/managers/app/install/install.go index b8f1309b40..a239666bf8 100644 --- a/api/internal/managers/app/install/install.go +++ b/api/internal/managers/app/install/install.go @@ -10,14 +10,13 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" kotscli "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" "github.com/replicatedhq/embedded-cluster/pkg-new/constants" - "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/netutils" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" kyaml "sigs.k8s.io/yaml" ) -// Install installs the app with the provided installable Helm charts and config values -func (m *appInstallManager) Install(ctx context.Context, installableCharts []types.InstallableHelmChart, kotsConfigValues kotsv1beta1.ConfigValues) (finalErr error) { +// Install installs the app with the provided config values +func (m *appInstallManager) Install(ctx context.Context, configValues kotsv1beta1.ConfigValues) (finalErr error) { if err := m.setStatus(types.StateRunning, "Installing application"); err != nil { return fmt.Errorf("set status: %w", err) } @@ -37,43 +36,34 @@ func (m *appInstallManager) Install(ctx context.Context, installableCharts []typ } }() - if err := m.install(ctx, installableCharts, kotsConfigValues); err != nil { + if err := m.install(ctx, configValues); err != nil { return err } return nil } -func (m *appInstallManager) install(ctx context.Context, installableCharts []types.InstallableHelmChart, kotsConfigValues kotsv1beta1.ConfigValues) error { +func (m *appInstallManager) install(ctx context.Context, configValues kotsv1beta1.ConfigValues) error { license := &kotsv1beta1.License{} if err := kyaml.Unmarshal(m.license, license); err != nil { return fmt.Errorf("parse license: %w", err) } - // Setup Helm client - if err := m.setupHelmClient(); err != nil { - return fmt.Errorf("setup helm client: %w", err) - } - - // Install Helm charts - if err := m.installHelmCharts(ctx, installableCharts); err != nil { - return fmt.Errorf("install helm charts: %w", err) - } - ecDomains := utils.GetDomains(m.releaseData) installOpts := kotscli.InstallOptions{ - AppSlug: license.Spec.AppSlug, - License: m.license, - Namespace: constants.KotsadmNamespace, - ClusterID: m.clusterID, - AirgapBundle: m.airgapBundle, - SkipPreflights: true, + AppSlug: license.Spec.AppSlug, + License: m.license, + Namespace: constants.KotsadmNamespace, + ClusterID: m.clusterID, + AirgapBundle: m.airgapBundle, + // Skip running the KOTS app preflights in the Admin Console; they run in the manager experience installer when ENABLE_V3 is enabled + SkipPreflights: os.Getenv("ENABLE_V3") == "1", ReplicatedAppEndpoint: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), Stdout: m.newLogWriter(), } - configValuesFile, err := m.createConfigValuesFile(kotsConfigValues) + configValuesFile, err := m.createConfigValuesFile(configValues) if err != nil { return fmt.Errorf("creating config values file: %w", err) } @@ -87,9 +77,9 @@ func (m *appInstallManager) install(ctx context.Context, installableCharts []typ } // createConfigValuesFile creates a temporary file with the config values -func (m *appInstallManager) createConfigValuesFile(kotsConfigValues kotsv1beta1.ConfigValues) (string, error) { +func (m *appInstallManager) createConfigValuesFile(configValues kotsv1beta1.ConfigValues) (string, error) { // Use Kubernetes-specific YAML serialization to properly handle TypeMeta and ObjectMeta - data, err := kyaml.Marshal(kotsConfigValues) + data, err := kyaml.Marshal(configValues) if err != nil { return "", fmt.Errorf("marshaling config values: %w", err) } @@ -107,53 +97,3 @@ func (m *appInstallManager) createConfigValuesFile(kotsConfigValues kotsv1beta1. return configValuesFile.Name(), nil } - -func (m *appInstallManager) installHelmCharts(ctx context.Context, installableCharts []types.InstallableHelmChart) error { - logFn := m.logFn("app-helm") - - if len(installableCharts) == 0 { - return fmt.Errorf("no helm charts found") - } - - logFn("installing %d helm charts", len(installableCharts)) - - for _, installableChart := range installableCharts { - logFn("installing %s helm chart", installableChart.CR.GetChartName()) - - if err := m.installHelmChart(ctx, installableChart); err != nil { - return fmt.Errorf("install %s helm chart: %w", installableChart.CR.GetChartName(), err) - } - - logFn("successfully installed %s helm chart", installableChart.CR.GetChartName()) - } - - return nil -} - -func (m *appInstallManager) installHelmChart(ctx context.Context, installableChart types.InstallableHelmChart) error { - // Write chart archive to temp file - chartPath, err := m.writeChartArchiveToTemp(installableChart.Archive) - if err != nil { - return fmt.Errorf("write chart archive to temp: %w", err) - } - defer os.Remove(chartPath) - - // Fallback to admin console namespace if namespace is not set - namespace := installableChart.CR.GetNamespace() - if namespace == "" { - namespace = constants.KotsadmNamespace - } - - // Install chart using Helm client with pre-processed values - _, err = m.hcli.Install(ctx, helm.InstallOptions{ - ChartPath: chartPath, - Namespace: namespace, - ReleaseName: installableChart.CR.GetReleaseName(), - Values: installableChart.Values, - }) - if err != nil { - return fmt.Errorf("helm install: %w", err) - } - - return nil -} diff --git a/api/internal/managers/app/install/install_test.go b/api/internal/managers/app/install/install_test.go index ae66ad078f..ce3d944251 100644 --- a/api/internal/managers/app/install/install_test.go +++ b/api/internal/managers/app/install/install_test.go @@ -1,11 +1,7 @@ package install import ( - "archive/tar" - "bytes" - "compress/gzip" "context" - "fmt" "os" "testing" "time" @@ -14,15 +10,11 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" kotscli "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" - "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - helmrelease "helm.sh/helm/v3/pkg/release" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kyaml "sigs.k8s.io/yaml" ) @@ -36,12 +28,8 @@ func TestAppInstallManager_Install(t *testing.T) { licenseBytes, err := kyaml.Marshal(license) require.NoError(t, err) - // Create valid helm chart archive - mockChartArchive := createTestChartArchive(t, "test-chart", "0.1.0") - - // Create test release data with helm chart archives + // Create test release data releaseData := &release.ReleaseData{ - HelmChartArchives: [][]byte{mockChartArchive}, ChannelRelease: &release.ChannelRelease{ DefaultDomains: release.Domains{ ReplicatedAppDomain: "replicated.app", @@ -49,8 +37,8 @@ func TestAppInstallManager_Install(t *testing.T) { }, } - t.Run("Success", func(t *testing.T) { - kotsConfigValues := kotsv1beta1.ConfigValues{ + t.Run("Config values should be passed to the installer", func(t *testing.T) { + configValues := kotsv1beta1.ConfigValues{ Spec: kotsv1beta1.ConfigValuesSpec{ Values: map[string]kotsv1beta1.ConfigValue{ "key1": { @@ -63,57 +51,7 @@ func TestAppInstallManager_Install(t *testing.T) { }, } - // Create InstallableHelmCharts with pre-processed values - installableCharts := []types.InstallableHelmChart{ - createTestInstallableHelmChart(t, "nginx-chart", "1.0.0", "web-server", "web", map[string]any{ - "image": map[string]any{ - "repository": "nginx", - "tag": "latest", - }, - "replicas": 3, - }), - createTestInstallableHelmChart(t, "postgres-chart", "2.0.0", "database", "data", map[string]any{ - "database": map[string]any{ - "host": "postgres.example.com", - "password": "secret", - }, - }), - } - - // Create mock helm client that validates pre-processed values - mockHelmClient := &helm.MockClient{} - - // Chart 1 installation (nginx chart) - mockHelmClient.On("Install", mock.Anything, mock.MatchedBy(func(opts helm.InstallOptions) bool { - if opts.ReleaseName != "web-server" { - return false - } - if opts.Namespace != "web" { - return false - } - // Check if values contain expected pre-processed data for nginx chart - if vals, ok := opts.Values["image"].(map[string]any); ok { - return vals["repository"] == "nginx" && vals["tag"] == "latest" && opts.Values["replicas"] == 3 - } - return false - })).Return(&helmrelease.Release{Name: "web-server"}, nil) - - // Chart 2 installation (database chart) - mockHelmClient.On("Install", mock.Anything, mock.MatchedBy(func(opts helm.InstallOptions) bool { - if opts.ReleaseName != "database" { - return false - } - if opts.Namespace != "data" { - return false - } - // Check if values contain expected pre-processed database data - if vals, ok := opts.Values["database"].(map[string]any); ok { - return vals["host"] == "postgres.example.com" && vals["password"] == "secret" - } - return false - })).Return(&helmrelease.Release{Name: "database"}, nil) - - // Create mock installer with detailed verification of config values + // Create mock installer with detailed verification mockInstaller := &MockKotsCLIInstaller{} mockInstaller.On("Install", mock.MatchedBy(func(opts kotscli.InstallOptions) bool { // Verify basic install options @@ -175,30 +113,18 @@ func TestAppInstallManager_Install(t *testing.T) { WithAirgapBundle("test-airgap.tar.gz"), WithReleaseData(releaseData), WithKotsCLI(mockInstaller), - WithHelmClient(mockHelmClient), WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) - // Run installation with InstallableHelmCharts and config values - err = manager.Install(context.Background(), installableCharts, kotsConfigValues) + // Run installation + err = manager.Install(context.Background(), configValues) require.NoError(t, err) mockInstaller.AssertExpectations(t) - mockHelmClient.AssertExpectations(t) }) t.Run("Install updates status correctly", func(t *testing.T) { - installableCharts := []types.InstallableHelmChart{ - createTestInstallableHelmChart(t, "monitoring-chart", "1.0.0", "prometheus", "monitoring", map[string]any{"key": "value"}), - } - - // Create mock helm client - mockHelmClient := &helm.MockClient{} - mockHelmClient.On("Install", mock.Anything, mock.MatchedBy(func(opts helm.InstallOptions) bool { - return opts.ChartPath != "" && opts.ReleaseName == "prometheus" && opts.Namespace == "monitoring" - })).Return(&helmrelease.Release{Name: "prometheus"}, nil) - // Create mock installer that succeeds mockInstaller := &MockKotsCLIInstaller{} mockInstaller.On("Install", mock.Anything).Return(nil) @@ -212,7 +138,6 @@ func TestAppInstallManager_Install(t *testing.T) { WithClusterID("test-cluster"), WithReleaseData(releaseData), WithKotsCLI(mockInstaller), - WithHelmClient(mockHelmClient), WithLogger(logger.NewDiscardLogger()), WithAppInstallStore(store), ) @@ -224,7 +149,7 @@ func TestAppInstallManager_Install(t *testing.T) { assert.Equal(t, types.StatePending, appInstall.Status.State) // Run installation - err = manager.Install(context.Background(), installableCharts, kotsv1beta1.ConfigValues{}) + err = manager.Install(context.Background(), kotsv1beta1.ConfigValues{}) require.NoError(t, err) // Verify final status @@ -234,21 +159,14 @@ func TestAppInstallManager_Install(t *testing.T) { assert.Equal(t, "Installation complete", appInstall.Status.Description) mockInstaller.AssertExpectations(t) - mockHelmClient.AssertExpectations(t) }) t.Run("Install handles errors correctly", func(t *testing.T) { - installableCharts := []types.InstallableHelmChart{ - createTestInstallableHelmChart(t, "logging-chart", "1.0.0", "fluentd", "logging", map[string]any{"key": "value"}), - } - - // Create mock helm client that fails - mockHelmClient := &helm.MockClient{} - mockHelmClient.On("Install", mock.Anything, mock.MatchedBy(func(opts helm.InstallOptions) bool { - return opts.ChartPath != "" && opts.ReleaseName == "fluentd" && opts.Namespace == "logging" - })).Return((*helmrelease.Release)(nil), assert.AnError) + // Create mock installer that fails + mockInstaller := &MockKotsCLIInstaller{} + mockInstaller.On("Install", mock.Anything).Return(assert.AnError) - // Create manager with initialized store (no need for KOTS installer mock since Helm fails first) + // Create manager with initialized store store := appinstallstore.NewMemoryStore(appinstallstore.WithAppInstall(types.AppInstall{ Status: types.Status{State: types.StatePending}, })) @@ -256,23 +174,23 @@ func TestAppInstallManager_Install(t *testing.T) { WithLicense(licenseBytes), WithClusterID("test-cluster"), WithReleaseData(releaseData), - WithHelmClient(mockHelmClient), + WithKotsCLI(mockInstaller), WithLogger(logger.NewDiscardLogger()), WithAppInstallStore(store), ) require.NoError(t, err) // Run installation (should fail) - err = manager.Install(context.Background(), installableCharts, kotsv1beta1.ConfigValues{}) + err = manager.Install(context.Background(), kotsv1beta1.ConfigValues{}) assert.Error(t, err) // Verify final status appInstall, err := manager.GetStatus() require.NoError(t, err) assert.Equal(t, types.StateFailed, appInstall.Status.State) - assert.Contains(t, appInstall.Status.Description, "install helm charts") + assert.Equal(t, assert.AnError.Error(), appInstall.Status.Description) - mockHelmClient.AssertExpectations(t) + mockInstaller.AssertExpectations(t) }) t.Run("GetStatus returns current app install state", func(t *testing.T) { @@ -300,13 +218,12 @@ func TestAppInstallManager_Install(t *testing.T) { assert.Equal(t, "Installing application", appInstall.Status.Description) assert.Equal(t, "Installation started\n", appInstall.Logs) }) - } func TestAppInstallManager_createConfigValuesFile(t *testing.T) { manager := &appInstallManager{} - kotsConfigValues := kotsv1beta1.ConfigValues{ + configValues := kotsv1beta1.ConfigValues{ Spec: kotsv1beta1.ConfigValuesSpec{ Values: map[string]kotsv1beta1.ConfigValue{ "testKey": { @@ -316,7 +233,7 @@ func TestAppInstallManager_createConfigValuesFile(t *testing.T) { }, } - filename, err := manager.createConfigValuesFile(kotsConfigValues) + filename, err := manager.createConfigValuesFile(configValues) assert.NoError(t, err) assert.NotEmpty(t, filename) @@ -332,66 +249,3 @@ func TestAppInstallManager_createConfigValuesFile(t *testing.T) { // Clean up os.Remove(filename) } - -// createTarGzArchive creates a tar.gz archive with the given files -func createTarGzArchive(t *testing.T, files map[string]string) []byte { - t.Helper() - - var buf bytes.Buffer - gw := gzip.NewWriter(&buf) - tw := tar.NewWriter(gw) - - for filename, content := range files { - header := &tar.Header{ - Name: filename, - Mode: 0600, - Size: int64(len(content)), - } - require.NoError(t, tw.WriteHeader(header)) - _, err := tw.Write([]byte(content)) - require.NoError(t, err) - } - - require.NoError(t, tw.Close()) - require.NoError(t, gw.Close()) - - return buf.Bytes() -} - -func createTestChartArchive(t *testing.T, name, version string) []byte { - chartYaml := fmt.Sprintf(`apiVersion: v2 -name: %s -version: %s -description: A test Helm chart -type: application -`, name, version) - - return createTarGzArchive(t, map[string]string{ - fmt.Sprintf("%s/Chart.yaml", name): chartYaml, - }) -} - -// Helper functions to create test data that can be reused across test cases -func createTestHelmChartCR(name, releaseName, namespace string) *kotsv1beta2.HelmChart { - return &kotsv1beta2.HelmChart{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "kots.io/v1beta2", - Kind: "HelmChart", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - Spec: kotsv1beta2.HelmChartSpec{ - ReleaseName: releaseName, - Namespace: namespace, - }, - } -} - -func createTestInstallableHelmChart(t *testing.T, chartName, chartVersion, releaseName, namespace string, values map[string]any) types.InstallableHelmChart { - return types.InstallableHelmChart{ - Archive: createTestChartArchive(t, chartName, chartVersion), - Values: values, - CR: createTestHelmChartCR(chartName, releaseName, namespace), - } -} diff --git a/api/internal/managers/app/install/manager.go b/api/internal/managers/app/install/manager.go index 15073b4a36..69f498a613 100644 --- a/api/internal/managers/app/install/manager.go +++ b/api/internal/managers/app/install/manager.go @@ -7,11 +7,9 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" kotscli "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" - "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/sirupsen/logrus" - "k8s.io/cli-runtime/pkg/genericclioptions" ) var _ AppInstallManager = &appInstallManager{} @@ -23,24 +21,21 @@ type KotsCLIInstaller interface { // AppInstallManager provides methods for managing app installation type AppInstallManager interface { - // Install installs the app with the provided installable Helm charts and config values - Install(ctx context.Context, installableCharts []types.InstallableHelmChart, kotsConfigValues kotsv1beta1.ConfigValues) error + // Install installs the app with the provided config values + Install(ctx context.Context, configValues kotsv1beta1.ConfigValues) error // GetStatus returns the current app installation status GetStatus() (types.AppInstall, error) } // appInstallManager is an implementation of the AppInstallManager interface type appInstallManager struct { - appInstallStore appinstallstore.Store - releaseData *release.ReleaseData - license []byte - clusterID string - airgapBundle string - kotsCLI KotsCLIInstaller - logger logrus.FieldLogger - hcli helm.Client - kubeConfigPath string - restClientGetter genericclioptions.RESTClientGetter + appInstallStore appinstallstore.Store + releaseData *release.ReleaseData + license []byte + clusterID string + airgapBundle string + kotsCLI KotsCLIInstaller + logger logrus.FieldLogger } type AppInstallManagerOption func(*appInstallManager) @@ -87,25 +82,6 @@ func WithKotsCLI(kotsCLI KotsCLIInstaller) AppInstallManagerOption { } } -// Add constructor options following infra manager pattern -func WithHelmClient(hcli helm.Client) AppInstallManagerOption { - return func(m *appInstallManager) { - m.hcli = hcli - } -} - -func WithKubeConfigPath(path string) AppInstallManagerOption { - return func(m *appInstallManager) { - m.kubeConfigPath = path - } -} - -func WithRESTClientGetter(restClientGetter genericclioptions.RESTClientGetter) AppInstallManagerOption { - return func(m *appInstallManager) { - m.restClientGetter = restClientGetter - } -} - // NewAppInstallManager creates a new AppInstallManager with the provided options func NewAppInstallManager(opts ...AppInstallManagerOption) (*appInstallManager, error) { manager := &appInstallManager{} diff --git a/api/internal/managers/app/install/mock.go b/api/internal/managers/app/install/mock.go index 6d927dfe4f..221845e53c 100644 --- a/api/internal/managers/app/install/mock.go +++ b/api/internal/managers/app/install/mock.go @@ -15,8 +15,8 @@ type MockAppInstallManager struct { } // Install mocks the Install method -func (m *MockAppInstallManager) Install(ctx context.Context, installableCharts []types.InstallableHelmChart, kotsConfigValues kotsv1beta1.ConfigValues) error { - args := m.Called(ctx, installableCharts, kotsConfigValues) +func (m *MockAppInstallManager) Install(ctx context.Context, configValues kotsv1beta1.ConfigValues) error { + args := m.Called(ctx, configValues) return args.Error(0) } diff --git a/api/internal/managers/app/install/status.go b/api/internal/managers/app/install/status.go index 7a3db96b28..55f9da21e9 100644 --- a/api/internal/managers/app/install/status.go +++ b/api/internal/managers/app/install/status.go @@ -1,6 +1,7 @@ package install import ( + "fmt" "time" "github.com/replicatedhq/embedded-cluster/api/types" @@ -17,3 +18,10 @@ func (m *appInstallManager) setStatus(state types.State, description string) err LastUpdated: time.Now(), }) } + +func (m *appInstallManager) addLogs(format string, v ...interface{}) { + msg := fmt.Sprintf(format, v...) + if err := m.appInstallStore.AddLogs(msg); err != nil { + m.logger.WithError(err).Error("add log") + } +} diff --git a/api/internal/managers/app/install/util.go b/api/internal/managers/app/install/util.go index 97869d111e..4c18e6bb4f 100644 --- a/api/internal/managers/app/install/util.go +++ b/api/internal/managers/app/install/util.go @@ -1,13 +1,8 @@ package install import ( - "fmt" "io" - "os" "strings" - - "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/versions" ) // logWriter is an io.Writer that captures output and feeds it to the logs @@ -22,54 +17,7 @@ func (m *appInstallManager) newLogWriter() io.Writer { func (lw *logWriter) Write(p []byte) (n int, err error) { output := strings.TrimSpace(string(p)) if output != "" { - lw.manager.addLogs("kots", "%s", output) + lw.manager.addLogs("[kots] %s", output) } return len(p), nil } - -func (m *appInstallManager) setupHelmClient() error { - if m.hcli != nil { - return nil - } - - hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: m.kubeConfigPath, - RESTClientGetter: m.restClientGetter, - K0sVersion: versions.K0sVersion, - LogFn: m.logFn("app-helm"), - }) - if err != nil { - return fmt.Errorf("create helm client: %w", err) - } - m.hcli = hcli - return nil -} - -func (m *appInstallManager) logFn(component string) func(format string, v ...interface{}) { - return func(format string, v ...interface{}) { - m.logger.WithField("component", component).Debugf(format, v...) - m.addLogs(component, format, v...) - } -} - -func (m *appInstallManager) addLogs(component string, format string, v ...interface{}) { - msg := fmt.Sprintf("[%s] %s", component, fmt.Sprintf(format, v...)) - if err := m.appInstallStore.AddLogs(msg); err != nil { - m.logger.WithError(err).Error("add log") - } -} - -func (m *appInstallManager) writeChartArchiveToTemp(chartArchive []byte) (string, error) { - tmpFile, err := os.CreateTemp("", "helm-chart-*.tgz") - if err != nil { - return "", fmt.Errorf("create temp file: %w", err) - } - defer tmpFile.Close() - - if _, err := tmpFile.Write(chartArchive); err != nil { - _ = os.Remove(tmpFile.Name()) - return "", fmt.Errorf("write chart archive: %w", err) - } - - return tmpFile.Name(), nil -} diff --git a/api/internal/managers/app/release/manager.go b/api/internal/managers/app/release/manager.go index 041da089bf..a8326d02b9 100644 --- a/api/internal/managers/app/release/manager.go +++ b/api/internal/managers/app/release/manager.go @@ -17,7 +17,6 @@ import ( // AppReleaseManager provides methods for managing the release of an app type AppReleaseManager interface { ExtractAppPreflightSpec(ctx context.Context, configValues types.AppConfigValues, proxySpec *ecv1beta1.ProxySpec, registrySettings *types.RegistrySettings) (*troubleshootv1beta2.PreflightSpec, error) - ExtractInstallableHelmCharts(ctx context.Context, configValues types.AppConfigValues, proxySpec *ecv1beta1.ProxySpec, registrySettings *types.RegistrySettings) ([]types.InstallableHelmChart, error) } type appReleaseManager struct { diff --git a/api/internal/managers/app/release/manager_mock.go b/api/internal/managers/app/release/manager_mock.go index 941e72ac2f..4bdeb2652e 100644 --- a/api/internal/managers/app/release/manager_mock.go +++ b/api/internal/managers/app/release/manager_mock.go @@ -24,12 +24,3 @@ func (m *MockAppReleaseManager) ExtractAppPreflightSpec(ctx context.Context, con } return args.Get(0).(*troubleshootv1beta2.PreflightSpec), args.Error(1) } - -// ExtractInstallableHelmCharts mocks the ExtractInstallableHelmCharts method -func (m *MockAppReleaseManager) ExtractInstallableHelmCharts(ctx context.Context, configValues types.AppConfigValues, proxySpec *ecv1beta1.ProxySpec, registrySettings *types.RegistrySettings) ([]types.InstallableHelmChart, error) { - args := m.Called(ctx, configValues, proxySpec, registrySettings) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]types.InstallableHelmChart), args.Error(1) -} diff --git a/api/internal/managers/app/release/template.go b/api/internal/managers/app/release/template.go index 5206f0d9f1..d381ad2f09 100644 --- a/api/internal/managers/app/release/template.go +++ b/api/internal/managers/app/release/template.go @@ -56,54 +56,6 @@ func (m *appReleaseManager) ExtractAppPreflightSpec(ctx context.Context, configV return mergedSpec, nil } -// ExtractInstallableHelmCharts extracts and processes installable Helm charts from app releases -func (m *appReleaseManager) ExtractInstallableHelmCharts(ctx context.Context, configValues types.AppConfigValues, proxySpec *ecv1beta1.ProxySpec, registrySettings *types.RegistrySettings) ([]types.InstallableHelmChart, error) { - // Template Helm chart CRs with config values - templatedCRs, err := m.templateHelmChartCRs(configValues, proxySpec, registrySettings) - if err != nil { - return nil, fmt.Errorf("template helm chart CRs: %w", err) - } - - var installableCharts []types.InstallableHelmChart - - // Iterate over each templated CR and create installable chart with processed values - for _, cr := range templatedCRs { - // Check if the chart should be excluded - if !cr.Spec.Exclude.IsEmpty() { - exclude, err := cr.Spec.Exclude.Boolean() - if err != nil { - return nil, fmt.Errorf("parse templated CR exclude for %s: %w", cr.Name, err) - } - if exclude { - continue - } - } - - // Find the corresponding chart archive for this HelmChart CR - chartArchive, err := findChartArchive(m.releaseData.HelmChartArchives, cr) - if err != nil { - return nil, fmt.Errorf("find chart archive for %s: %w", cr.Name, err) - } - - // Generate Helm values from the templated CR - values, err := generateHelmValues(cr) - if err != nil { - return nil, fmt.Errorf("generate helm values for chart %s: %w", cr.Name, err) - } - - // Create installable chart with archive, processed values, and CR - installableChart := types.InstallableHelmChart{ - Archive: chartArchive, - Values: values, - CR: cr, - } - - installableCharts = append(installableCharts, installableChart) - } - - return installableCharts, nil -} - // templateHelmChartCRs templates the HelmChart CRs from release data using the template engine and config values func (m *appReleaseManager) templateHelmChartCRs(configValues types.AppConfigValues, proxySpec *ecv1beta1.ProxySpec, registrySettings *types.RegistrySettings) ([]*kotsv1beta2.HelmChart, error) { if m.templateEngine == nil { @@ -180,7 +132,7 @@ func (m *appReleaseManager) dryRunHelmChart(ctx context.Context, templatedCR *ko } // Create a Helm client for dry run templating - helmClient, err := helm.NewClient(helm.HelmOptions{}) // TODO: pass K0sVersion (maybe rename to kubernetesVersion) + helmClient, err := helm.NewClient(helm.HelmOptions{}) if err != nil { return nil, fmt.Errorf("create helm client: %w", err) } @@ -224,7 +176,7 @@ func generateHelmValues(templatedCR *kotsv1beta2.HelmChart) (map[string]any, err } // Start with the base values - mergedValues := maps.Clone(templatedCR.Spec.Values) + mergedValues := templatedCR.Spec.Values if mergedValues == nil { mergedValues = map[string]kotsv1beta2.MappedChartValue{} } diff --git a/api/internal/managers/app/release/template_test.go b/api/internal/managers/app/release/template_test.go index 7392f9b912..5170922f95 100644 --- a/api/internal/managers/app/release/template_test.go +++ b/api/internal/managers/app/release/template_test.go @@ -34,6 +34,7 @@ func TestAppReleaseManager_ExtractAppPreflightSpec(t *testing.T) { name: "no helm charts returns nil", helmChartCRs: [][]byte{}, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, expectedSpec: nil, expectError: false, }, @@ -78,6 +79,7 @@ spec: configValues: types.AppConfigValues{ "check_name": {Value: "K8s Version Validation"}, }, + proxySpec: &ecv1beta1.ProxySpec{}, expectedSpec: &troubleshootv1beta2.PreflightSpec{ Analyzers: []*troubleshootv1beta2.Analyze{ { @@ -175,6 +177,7 @@ spec: "version_check_name": {Value: "Custom K8s Version Check"}, "resource_check_name": {Value: "Custom Node Resource Check"}, }, + proxySpec: &ecv1beta1.ProxySpec{}, expectedSpec: &troubleshootv1beta2.PreflightSpec{ Analyzers: []*troubleshootv1beta2.Analyze{ { @@ -232,6 +235,7 @@ spec: createTestChartArchive(t, "simple-chart", "1.0.0"), }, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, expectedSpec: nil, expectError: false, }, @@ -371,6 +375,7 @@ func TestAppReleaseManager_templateHelmChartCRs(t *testing.T) { name: "empty helm chart CRs", helmChartCRs: [][]byte{}, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, registrySettings: nil, expected: []*kotsv1beta2.HelmChart{}, expectError: false, @@ -411,6 +416,7 @@ spec: "enable_persistence": {Value: "true"}, "disable_monitoring": {Value: "false"}, }, + proxySpec: &ecv1beta1.ProxySpec{}, expected: []*kotsv1beta2.HelmChart{ createHelmChartCRFromYAML(` apiVersion: kots.io/v1beta2 @@ -495,6 +501,7 @@ spec: "enable_resources": {Value: "false"}, "redis_persistence": {Value: "true"}, }, + proxySpec: &ecv1beta1.ProxySpec{}, expected: []*kotsv1beta2.HelmChart{ createHelmChartCRFromYAML(` apiVersion: kots.io/v1beta2 @@ -557,6 +564,7 @@ spec: `), }, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, expected: []*kotsv1beta2.HelmChart{ createHelmChartCRFromYAML(` apiVersion: kots.io/v1beta2 @@ -576,6 +584,7 @@ spec: name: "nil helm chart CRs", helmChartCRs: nil, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, expected: []*kotsv1beta2.HelmChart{}, expectError: false, }, @@ -669,6 +678,7 @@ spec: `), }, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, // Empty proxy spec expected: []*kotsv1beta2.HelmChart{ createHelmChartCRFromYAML(` apiVersion: kots.io/v1beta2 @@ -722,6 +732,7 @@ spec: `), }, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, registrySettings: &types.RegistrySettings{ HasLocalRegistry: true, Host: "10.128.0.11:5000", @@ -776,6 +787,7 @@ spec: `), }, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, registrySettings: &types.RegistrySettings{ HasLocalRegistry: false, }, @@ -820,6 +832,7 @@ spec: `), }, configValues: types.AppConfigValues{}, + proxySpec: &ecv1beta1.ProxySpec{}, registrySettings: nil, // No registry settings provided expected: []*kotsv1beta2.HelmChart{ createHelmChartCRFromYAML(` @@ -1801,635 +1814,6 @@ data: } } -func TestAppReleaseManager_ExtractInstallableHelmCharts(t *testing.T) { - tests := []struct { - name string - helmChartCRs [][]byte - chartArchives [][]byte - configValues types.AppConfigValues - proxySpec *ecv1beta1.ProxySpec - registrySettings *types.RegistrySettings - expectError bool - errorContains string - expected []types.InstallableHelmChart - validateCharts func(t *testing.T, charts []types.InstallableHelmChart, testHelmChartCRs [][]byte, testChartArchives [][]byte) - }{ - { - name: "no helm charts returns empty slice", - helmChartCRs: [][]byte{}, - chartArchives: [][]byte{}, - configValues: types.AppConfigValues{}, - expectError: false, - expected: nil, - }, - { - name: "single chart with basic values", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: nginx-chart - namespace: default -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - replicaCount: "3" - image: - repository: nginx - tag: '{{repl ConfigOption "image_tag"}}' - service: - type: ClusterIP - port: 80 - optionalValues: - - when: '{{repl ConfigOptionEquals "enable_ingress" "true"}}' - values: - ingress: - enabled: true - host: '{{repl ConfigOption "ingress_host"}}'`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - }, - configValues: types.AppConfigValues{ - "image_tag": {Value: "1.20.0"}, - "enable_ingress": {Value: "true"}, - "ingress_host": {Value: "nginx.example.com"}, - }, - expectError: false, - expected: []types.InstallableHelmChart{ - { - Archive: createTestChartArchive(t, "nginx", "1.0.0"), - Values: map[string]any{ - "replicaCount": "3", - "image": map[string]any{ - "repository": "nginx", - "tag": "1.20.0", - }, - "service": map[string]any{ - "type": "ClusterIP", - "port": float64(80), - }, - "ingress": map[string]any{ - "enabled": true, - "host": "nginx.example.com", - }, - }, - CR: createHelmChartCRFromYAML(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: nginx-chart - namespace: default -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - replicaCount: "3" - image: - repository: nginx - tag: "1.20.0" - service: - type: ClusterIP - port: 80 - optionalValues: - - when: "true" - values: - ingress: - enabled: true - host: "nginx.example.com"`), - }, - }, - }, - { - name: "chart with exclude=true should be skipped", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: excluded-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - exclude: '{{repl ConfigOptionEquals "skip_nginx" "true"}}' - values: - replicaCount: "2"`), - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: included-chart -spec: - chart: - name: redis - chartVersion: "2.0.0" - exclude: false - values: - persistence: - enabled: true`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - createTestChartArchive(t, "redis", "2.0.0"), - }, - configValues: types.AppConfigValues{ - "skip_nginx": {Value: "true"}, - }, - expectError: false, - validateCharts: func(t *testing.T, charts []types.InstallableHelmChart, testHelmChartCRs [][]byte, testChartArchives [][]byte) { - expectedValues := map[string]any{ - "persistence": map[string]any{ - "enabled": true, - }, - } - - expectedCharts := []types.InstallableHelmChart{ - { - Archive: testChartArchives[1], // redis chart (index 1) - Values: expectedValues, - CR: createHelmChartCRFromYAML(string(testHelmChartCRs[1])), // redis CR (index 1) - }, - } - - assert.Equal(t, expectedCharts, charts) - }, - }, - { - name: "chart with recursive merge optional values", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: merge-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - service: - type: '{{repl ConfigOption "service_type"}}' - port: 80 - replicaCount: "1" - optionalValues: - - when: '{{repl ConfigOption "enable_ssl"}}' - recursiveMerge: true - values: - service: - type: LoadBalancer - ssl: - enabled: true`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - }, - configValues: types.AppConfigValues{ - "service_type": {Value: "ClusterIP"}, - "enable_ssl": {Value: "true"}, - }, - expectError: false, - expected: []types.InstallableHelmChart{ - { - Archive: createTestChartArchive(t, "nginx", "1.0.0"), - Values: map[string]any{ - "replicaCount": "1", - "service": map[string]any{ - "type": "LoadBalancer", // from optional values (overrode base value) - "port": float64(80), // from base values (preserved) - }, - "ssl": map[string]any{ - "enabled": true, // from optional values (added) - }, - }, - CR: createHelmChartCRFromYAML(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: merge-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - service: - type: "ClusterIP" - port: 80 - replicaCount: "1" - optionalValues: - - when: "true" - recursiveMerge: true - values: - service: - type: LoadBalancer - ssl: - enabled: true`), - }, - }, - }, - { - name: "chart with direct replacement optional values", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: replace-chart -spec: - chart: - name: redis - chartVersion: "2.0.0" - values: - persistence: - enabled: '{{repl ConfigOption "enable_persistence"}}' - size: "5Gi" - optionalValues: - - when: '{{repl ConfigOption "redis_persistence"}}' - recursiveMerge: false - values: - persistence: - size: "20Gi"`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "redis", "2.0.0"), - }, - configValues: types.AppConfigValues{ - "enable_persistence": {Value: "true"}, - "redis_persistence": {Value: "true"}, - }, - expectError: false, - expected: []types.InstallableHelmChart{ - { - Archive: createTestChartArchive(t, "redis", "2.0.0"), - Values: map[string]any{ - "persistence": map[string]any{ - "size": "20Gi", // from optional values (direct replacement) - // Note: enabled=true is GONE because entire persistence key was replaced - }, - }, - CR: createHelmChartCRFromYAML(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: replace-chart -spec: - chart: - name: redis - chartVersion: "2.0.0" - values: - persistence: - enabled: "true" - size: "5Gi" - optionalValues: - - when: "true" - recursiveMerge: false - values: - persistence: - size: "20Gi"`), - }, - }, - }, - { - name: "chart with proxy template functions", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: proxy-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - proxy: - http: '{{repl HTTPProxy}}' - https: '{{repl HTTPSProxy}}' - noProxy: '{{repl NoProxy | join ","}}' - optionalValues: - - when: '{{repl if HTTPProxy}}true{{repl else}}false{{repl end}}' - values: - proxyEnabled: true`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - }, - configValues: types.AppConfigValues{}, - proxySpec: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://proxy.example.com:8080", - HTTPSProxy: "https://proxy.example.com:8443", - NoProxy: "localhost,127.0.0.1,.cluster.local", - }, - expectError: false, - expected: []types.InstallableHelmChart{ - { - Archive: createTestChartArchive(t, "nginx", "1.0.0"), - Values: map[string]any{ - "proxy": map[string]any{ - "http": "http://proxy.example.com:8080", - "https": "https://proxy.example.com:8443", - "noProxy": "localhost,127.0.0.1,.cluster.local", - }, - "proxyEnabled": true, - }, - CR: createHelmChartCRFromYAML(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: proxy-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - proxy: - http: "http://proxy.example.com:8080" - https: "https://proxy.example.com:8443" - noProxy: "localhost,127.0.0.1,.cluster.local" - optionalValues: - - when: "true" - values: - proxyEnabled: true`), - }, - }, - }, - { - name: "chart archive not found", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: missing-chart -spec: - chart: - name: nonexistent - chartVersion: "1.0.0" - values: - replicaCount: "1"`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), // Different chart - }, - configValues: types.AppConfigValues{}, - expectError: true, - errorContains: "find chart archive for missing-chart", - }, - { - name: "invalid when condition in optional values", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: invalid-when-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - replicaCount: "1" - optionalValues: - - when: "not-a-boolean-value" - values: - debug: true`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - }, - configValues: types.AppConfigValues{}, - expectError: true, - errorContains: "generate helm values for chart invalid-when-chart", - }, - { - name: "chart with mixed when conditions", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: mixed-conditions-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - replicaCount: "1" - optionalValues: - - when: '{{repl ConfigOption "enable_persistence"}}' - values: - persistence: - enabled: true - - when: '{{repl ConfigOption "disable_monitoring"}}' - values: - monitoring: - enabled: false`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - }, - configValues: types.AppConfigValues{ - "enable_persistence": {Value: "true"}, - "disable_monitoring": {Value: "false"}, - }, - expectError: false, - expected: []types.InstallableHelmChart{ - { - Archive: createTestChartArchive(t, "nginx", "1.0.0"), - Values: map[string]any{ - "replicaCount": "1", // from base values - "persistence": map[string]any{ - "enabled": true, // from optional values (when=true) - }, - // monitoring should NOT be present (when condition evaluated to false) - }, - CR: createHelmChartCRFromYAML(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: mixed-conditions-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - replicaCount: "1" - optionalValues: - - when: "true" - values: - persistence: - enabled: true - - when: "false" - values: - monitoring: - enabled: false`), - }, - }, - }, - { - name: "nil helm chart CRs", - helmChartCRs: nil, - chartArchives: [][]byte{}, - configValues: types.AppConfigValues{}, - expectError: false, - validateCharts: func(t *testing.T, charts []types.InstallableHelmChart, testHelmChartCRs [][]byte, testChartArchives [][]byte) { - assert.Empty(t, charts) - }, - }, - { - name: "skip nil helm chart CR in collection", - helmChartCRs: [][]byte{ - nil, - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: valid-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - replicaCount: "2"`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - }, - configValues: types.AppConfigValues{}, - expectError: false, - validateCharts: func(t *testing.T, charts []types.InstallableHelmChart, testHelmChartCRs [][]byte, testChartArchives [][]byte) { - expectedValues := map[string]any{ - "replicaCount": "2", - } - - expectedCharts := []types.InstallableHelmChart{ - { - Archive: testChartArchives[0], - Values: expectedValues, - CR: createHelmChartCRFromYAML(string(testHelmChartCRs[1])), // Skip the nil CR at index 0 - }, - } - - assert.Equal(t, expectedCharts, charts) - }, - }, - { - name: "chart with registry template functions - airgap mode", - helmChartCRs: [][]byte{ - []byte(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: registry-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - image: - repository: '{{repl HasLocalRegistry | ternary LocalRegistryHost "proxy.replicated.com"}}/{{repl HasLocalRegistry | ternary LocalRegistryNamespace "external/path"}}/nginx' - tag: "1.20.0" - imagePullSecrets: - - name: '{{repl ImagePullSecretName}}' - registry: - host: '{{repl LocalRegistryHost}}' - address: '{{repl LocalRegistryAddress}}' - namespace: '{{repl LocalRegistryNamespace}}' - secret: '{{repl LocalRegistryImagePullSecret}}' - optionalValues: - - when: '{{repl HasLocalRegistry}}' - values: - airgapMode: true`), - }, - chartArchives: [][]byte{ - createTestChartArchive(t, "nginx", "1.0.0"), - }, - configValues: types.AppConfigValues{}, - registrySettings: &types.RegistrySettings{ - HasLocalRegistry: true, - Host: "10.128.0.11:5000", - Address: "10.128.0.11:5000/myapp", - Namespace: "myapp", - ImagePullSecretName: "embedded-cluster-registry", - ImagePullSecretValue: "dGVzdC1zZWNyZXQtdmFsdWU=", - }, - expectError: false, - expected: []types.InstallableHelmChart{ - { - Archive: createTestChartArchive(t, "nginx", "1.0.0"), - Values: map[string]any{ - "image": map[string]any{ - "repository": "10.128.0.11:5000/myapp/nginx", - "tag": "1.20.0", - }, - "imagePullSecrets": []any{ - map[string]any{"name": "embedded-cluster-registry"}, - }, - "registry": map[string]any{ - "host": "10.128.0.11:5000", - "address": "10.128.0.11:5000/myapp", - "namespace": "myapp", - "secret": "dGVzdC1zZWNyZXQtdmFsdWU=", - }, - "airgapMode": true, - }, - CR: createHelmChartCRFromYAML(`apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: registry-chart -spec: - chart: - name: nginx - chartVersion: "1.0.0" - values: - image: - repository: "10.128.0.11:5000/myapp/nginx" - tag: "1.20.0" - imagePullSecrets: - - name: "embedded-cluster-registry" - registry: - host: "10.128.0.11:5000" - address: "10.128.0.11:5000/myapp" - namespace: "myapp" - secret: "dGVzdC1zZWNyZXQtdmFsdWU=" - optionalValues: - - when: "true" - values: - airgapMode: true`), - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create release data - releaseData := &release.ReleaseData{ - HelmChartCRs: tt.helmChartCRs, - HelmChartArchives: tt.chartArchives, - } - - // Create manager - config := createTestConfig() - manager, err := NewAppReleaseManager( - config, - WithReleaseData(releaseData), - ) - require.NoError(t, err) - - // Execute the function - result, err := manager.ExtractInstallableHelmCharts(context.Background(), tt.configValues, tt.proxySpec, tt.registrySettings) - - // Check error expectation - if tt.expectError { - require.Error(t, err) - if tt.errorContains != "" { - assert.Contains(t, err.Error(), tt.errorContains) - } - assert.Nil(t, result) - return - } - - require.NoError(t, err) - - // Use expectedInstallableCharts if provided, otherwise use validateCharts - if tt.expected != nil { - assert.Equal(t, tt.expected, result) - } else if tt.validateCharts != nil { - tt.validateCharts(t, result, tt.helmChartCRs, tt.chartArchives) - } - }) - } -} - // Helper function to create HelmChart from YAML string func createHelmChartCRFromYAML(yamlStr string) *kotsv1beta2.HelmChart { var chart kotsv1beta2.HelmChart @@ -2576,16 +1960,6 @@ func createTestConfig() kotsv1beta1.Config { {Name: "node_count", Type: "text", Value: multitype.FromString("3")}, {Name: "version_check_name", Type: "text", Value: multitype.FromString("Custom K8s Version Check")}, {Name: "resource_check_name", Type: "text", Value: multitype.FromString("Custom Node Resource Check")}, - // Additional items for ExtractInstallableHelmCharts test - {Name: "image_tag", Type: "text", Value: multitype.FromString("1.20.0")}, - {Name: "enable_ingress", Type: "text", Value: multitype.FromString("true")}, - {Name: "ingress_host", Type: "text", Value: multitype.FromString("nginx.example.com")}, - {Name: "skip_nginx", Type: "text", Value: multitype.FromString("true")}, - {Name: "frontend_replicas", Type: "text", Value: multitype.FromString("3")}, - {Name: "frontend_tag", Type: "text", Value: multitype.FromString("1.20.0")}, - {Name: "enable_ssl", Type: "text", Value: multitype.FromString("true")}, - {Name: "redis_persistence", Type: "text", Value: multitype.FromString("true")}, - {Name: "invalid_boolean", Type: "text", Value: multitype.FromString("not-a-boolean")}, }, }, }, diff --git a/api/internal/managers/app/release/util.go b/api/internal/managers/app/release/util.go index e4f74e0229..4f2c282a65 100644 --- a/api/internal/managers/app/release/util.go +++ b/api/internal/managers/app/release/util.go @@ -62,7 +62,7 @@ func writeChartArchiveToTemp(chartArchive []byte) (string, error) { // Write the chart archive to the temporary file if _, err := tmpFile.Write(chartArchive); err != nil { - _ = os.Remove(tmpFile.Name()) + os.Remove(tmpFile.Name()) return "", fmt.Errorf("write chart archive: %w", err) } diff --git a/api/internal/managers/kubernetes/infra/install.go b/api/internal/managers/kubernetes/infra/install.go index 0acf583774..07ff4ed7b8 100644 --- a/api/internal/managers/kubernetes/infra/install.go +++ b/api/internal/managers/kubernetes/infra/install.go @@ -157,7 +157,6 @@ func (m *infraManager) getAddonInstallOpts(license *kotsv1beta1.License, ki kube EmbeddedConfigSpec: m.getECConfigSpec(), EndUserConfigSpec: m.getEndUserConfigSpec(), ProxySpec: ki.ProxySpec(), - IsV3: true, } // TODO: no kots app install for now diff --git a/api/internal/managers/linux/infra/install.go b/api/internal/managers/linux/infra/install.go index 6c5019e970..74cd811932 100644 --- a/api/internal/managers/linux/infra/install.go +++ b/api/internal/managers/linux/infra/install.go @@ -298,7 +298,6 @@ func (m *infraManager) getAddonInstallOpts(ctx context.Context, license *kotsv1b K0sDataDir: rc.EmbeddedClusterK0sSubDir(), OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), ServiceCIDR: rc.ServiceCIDR(), - IsV3: true, } return opts diff --git a/api/types/app.go b/api/types/app.go index f5a82572e5..14ca5aef47 100644 --- a/api/types/app.go +++ b/api/types/app.go @@ -1,9 +1,6 @@ package types -import ( - kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" -) +import kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" // AppConfig represents the configuration for an app. This is an alias for the // kotsv1beta1.ConfigSpec type. @@ -23,10 +20,3 @@ type AppInstall struct { Status Status `json:"status"` Logs string `json:"logs"` } - -// InstallableHelmChart represents a Helm chart with pre-processed values ready for installation -type InstallableHelmChart struct { - Archive []byte - Values map[string]any - CR *kotsv1beta2.HelmChart -} diff --git a/pkg/addons/adminconsole/adminconsole.go b/pkg/addons/adminconsole/adminconsole.go index 32da1b8765..79663a6035 100644 --- a/pkg/addons/adminconsole/adminconsole.go +++ b/pkg/addons/adminconsole/adminconsole.go @@ -23,7 +23,6 @@ type AdminConsole struct { IsMultiNodeEnabled bool Proxy *ecv1beta1.ProxySpec AdminConsolePort int - IsV3 bool // Linux specific options ClusterID string diff --git a/pkg/addons/adminconsole/values.go b/pkg/addons/adminconsole/values.go index 0eaec05db6..a414d1c091 100644 --- a/pkg/addons/adminconsole/values.go +++ b/pkg/addons/adminconsole/values.go @@ -50,7 +50,6 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien copiedValues["isHA"] = a.IsHA copiedValues["isMultiNodeEnabled"] = a.IsMultiNodeEnabled copiedValues["isAirgap"] = a.IsAirgap - copiedValues["isEmbeddedClusterV3"] = a.IsV3 if domains.ReplicatedAppDomain != "" { copiedValues["replicatedAppEndpoint"] = netutils.MaybeAddHTTPS(domains.ReplicatedAppDomain) diff --git a/pkg/addons/adminconsole/values_test.go b/pkg/addons/adminconsole/values_test.go index 31bf916d0a..b5083c7916 100644 --- a/pkg/addons/adminconsole/values_test.go +++ b/pkg/addons/adminconsole/values_test.go @@ -95,7 +95,6 @@ func TestGenerateHelmValues_Target(t *testing.T) { IsMultiNodeEnabled: false, Proxy: nil, AdminConsolePort: 8080, - IsV3: true, ClusterID: "123", ServiceCIDR: "10.0.0.0/24", @@ -107,10 +106,10 @@ func TestGenerateHelmValues_Target(t *testing.T) { values, err := adminConsole.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") + assert.Contains(t, values, "embeddedClusterID") assert.Equal(t, "123", values["embeddedClusterID"]) assert.Equal(t, dataDir, values["embeddedClusterDataDir"]) assert.Equal(t, filepath.Join(dataDir, "k0s"), values["embeddedClusterK0sDir"]) - assert.Equal(t, true, values["isEmbeddedClusterV3"]) assert.Contains(t, values["extraEnv"], map[string]interface{}{ "name": "SSL_CERT_CONFIGMAP", @@ -129,7 +128,6 @@ func TestGenerateHelmValues_Target(t *testing.T) { IsMultiNodeEnabled: false, Proxy: nil, AdminConsolePort: 8080, - IsV3: true, } values, err := adminConsole.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) @@ -138,7 +136,6 @@ func TestGenerateHelmValues_Target(t *testing.T) { assert.NotContains(t, values, "embeddedClusterID") assert.NotContains(t, values, "embeddedClusterDataDir") assert.NotContains(t, values, "embeddedClusterK0sDir") - assert.Equal(t, true, values["isEmbeddedClusterV3"]) for _, env := range values["extraEnv"].([]map[string]interface{}) { assert.NotEqual(t, "SSL_CERT_CONFIGMAP", env["name"], "SSL_CERT_CONFIGMAP environment variable should not be set") diff --git a/pkg/addons/highavailability.go b/pkg/addons/highavailability.go index 901a76c9a0..8ffd5dddaf 100644 --- a/pkg/addons/highavailability.go +++ b/pkg/addons/highavailability.go @@ -251,7 +251,6 @@ func (a *AddOns) EnableAdminConsoleHA(ctx context.Context, opts EnableHAOptions) DataDir: opts.DataDir, K0sDataDir: opts.K0sDataDir, AdminConsolePort: opts.AdminConsolePort, - IsV3: false, } if err := ac.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.domains, a.addOnOverrides(ac, opts.EmbeddedConfigSpec, opts.EndUserConfigSpec)); err != nil { return errors.Wrap(err, "upgrade admin console") diff --git a/pkg/addons/install.go b/pkg/addons/install.go index 598d729fcb..3f5ddcde70 100644 --- a/pkg/addons/install.go +++ b/pkg/addons/install.go @@ -28,7 +28,6 @@ type InstallOptions struct { EndUserConfigSpec *ecv1beta1.ConfigSpec KotsInstaller adminconsole.KotsInstaller ProxySpec *ecv1beta1.ProxySpec - IsV3 bool // Linux only options ClusterID string @@ -53,7 +52,6 @@ type KubernetesInstallOptions struct { EndUserConfigSpec *ecv1beta1.ConfigSpec KotsInstaller adminconsole.KotsInstaller ProxySpec *ecv1beta1.ProxySpec - IsV3 bool } func (a *AddOns) Install(ctx context.Context, opts InstallOptions) error { @@ -162,7 +160,6 @@ func GetAddOnsForInstall(opts InstallOptions) []types.AddOn { DataDir: opts.DataDir, K0sDataDir: opts.K0sDataDir, AdminConsolePort: opts.AdminConsolePort, - IsV3: opts.IsV3, Password: opts.AdminConsolePwd, TLSCertBytes: opts.TLSCertBytes, @@ -198,7 +195,6 @@ func GetAddOnsForKubernetesInstall(opts KubernetesInstallOptions) []types.AddOn IsMultiNodeEnabled: opts.IsMultiNodeEnabled, Proxy: opts.ProxySpec, AdminConsolePort: opts.AdminConsolePort, - IsV3: opts.IsV3, Password: opts.AdminConsolePwd, TLSCertBytes: opts.TLSCertBytes, diff --git a/pkg/addons/upgrade.go b/pkg/addons/upgrade.go index fa3241b54e..1a14dbf9ba 100644 --- a/pkg/addons/upgrade.go +++ b/pkg/addons/upgrade.go @@ -118,7 +118,6 @@ func (a *AddOns) getAddOnsForUpgrade(meta *ectypes.ReleaseMetadata, opts Upgrade DataDir: opts.DataDir, K0sDataDir: opts.K0sDataDir, AdminConsolePort: opts.AdminConsolePort, - IsV3: false, }) return addOns, nil diff --git a/proposals/helm_direct_install.md b/proposals/helm_direct_install.md deleted file mode 100644 index 07e6195ec8..0000000000 --- a/proposals/helm_direct_install.md +++ /dev/null @@ -1,55 +0,0 @@ -# Proposal: Direct Helm Chart Installation for V3 API - -**Status:** Proposed -**Epic Proposal:** [V3 App Deployment Transition](./v3_app_deployment_transition.md) -**Story:** [sc-128045](https://app.shortcut.com/replicated/story/128045) -**Iteration:** 1 (Foundation) - -## TL;DR - -Enhance the V3 API app installation manager to deploy Helm charts directly from releaseData.HelmChartArchives before calling KOTS CLI. This is the foundational story for transitioning application -deployment ownership from KOTS to the V3 embedded-cluster binary. - -## Scope - -This proposal covers only the basic Helm chart installation functionality needed for Iteration 1: -- Add Helm client to V3 API installation manager -- Install charts from releaseData.HelmChartArchives using default configuration -- Coordinate with KOTS CLI for metadata management - -**Out of scope for this story:** -- HelmChart custom resource field support (covered in Iteration 2) -- Airgap image handling (covered in Iteration 3) -- Progress reporting UI (covered in Iteration 4) - -See the [epic proposal](./v3_app_deployment_transition.md) for the complete implementation plan and architectural vision. - -## Implementation - -**Core Changes:** -- `api/internal/managers/app/install/manager.go` - Add Helm client field and constructor options -- `api/internal/managers/app/install/install.go` - Add `installHelmCharts()` method before KOTS CLI call -- `api/internal/managers/app/install/util.go` - Add Helm client setup utilities - -**Key Technical Decisions:** -1. **Basic Chart Installation Only:** Use chart name as release name, install to kotsadm namespace -2. **Sequential Processing:** Install charts one at a time for reliability -3. **No Rollback Logic:** Fail fast on errors, consistent with existing patterns - -## Testing - -- Unit tests with Helm client mocks in `install_test.go` -- Integration tests using unified releaseData structure -- Compatibility verification that non-V3 installations remain unchanged - -## Dependencies - -- Requires KOTS skip deployment changes (sc-128049) to prevent conflicts -- Foundation for HelmChart custom resource support in Iteration 2 - -The key changes: -1. Reduced scope - Focus only on the basic Helm installation for this specific story -2. Clear references - Point to the epic proposal for the bigger picture -3. Iteration context - Explicitly state this is Iteration 1 foundation work -4. Dependencies - Clear about what this depends on and enables -5. Removed duplicate content - Architecture, risk assessment, etc. are in the epic proposal \ No newline at end of file diff --git a/proposals/helm_direct_install_research.md b/proposals/helm_direct_install_research.md deleted file mode 100644 index da2ffea200..0000000000 --- a/proposals/helm_direct_install_research.md +++ /dev/null @@ -1,160 +0,0 @@ -# Research: Direct Helm Chart Installation Without KOTS CLI - -## Executive Summary -This research document analyzes the current implementation and identifies key areas for modifying the app install endpoint to install Helm charts directly while maintaining KOTS CLI for version record creation. - -## Current Architecture - -### 1. Installation Flow -The current installation flow follows this sequence: -1. `/kubernetes/install/app/install` endpoint receives request -2. `InstallController.InstallApp()` validates state transitions -3. `AppInstallManager.Install()` invokes KOTS CLI -4. KOTS CLI handles entire installation including: - - Creating version records - - Installing Helm charts - - Managing application state - -### 2. Key Components - -#### AppInstallManager (`api/internal/managers/app/install/`) -- Primary interface for app installation -- Current implementation delegates entirely to KOTS CLI via `kotscli.Install()` -- Manages installation status and logging - -#### KOTS CLI Integration (`cmd/installer/kotscli/kotscli.go`) -- `Install()` function executes kubectl-kots binary -- Passes configuration via command-line arguments -- Handles both online and airgap installations - -#### Release Data (`pkg/release/release.go`) -- Contains `ReleaseData` struct with `HelmChartArchives [][]byte` -- Helm charts are already extracted and available in memory -- Also contains `HelmChartCRs [][]byte` for Helm chart custom resources - -#### Helm Client (`pkg/helm/client.go`) -- Existing robust Helm client implementation -- Supports install, upgrade, uninstall operations -- Already used throughout the codebase for addon installation - -## Technical Analysis - -### 1. Available Helm Chart Data -The `ReleaseData` structure already contains: -```go -type ReleaseData struct { - // ... other fields - HelmChartCRs [][]byte // Helm chart custom resources - HelmChartArchives [][]byte // Actual Helm chart archives -} -``` - -### 2. Existing Helm Infrastructure -The codebase has a complete Helm client implementation that: -- Supports chart installation from archives -- Handles namespace creation -- Manages releases -- Provides timeout and retry logic - -### 3. KOTS CLI Invocation Pattern -Current KOTS CLI invocation uses these key parameters: -- `--exclude-admin-console`: Already excludes admin console deployment -- `--app-version-label`: Version tracking -- `--config-values`: Application configuration -- `--skip-preflights`: Conditionally skip preflight checks - -## Implementation Considerations - -### 1. Dual Path Architecture -Need to implement: -- KOTS CLI path: Version record creation only (coordinated with sc-128049) -- Direct Helm path: Actual chart deployment - -### 2. Chart Installation Requirements -- Extract charts from `HelmChartArchives` -- Apply configuration values -- Maintain proper installation order -- Handle dependencies - -### 3. State Management -- Coordinate state transitions between KOTS and Helm operations -- Handle partial failures -- Implement rollback capabilities - -### 4. Configuration Mapping -- Transform KOTS config values to Helm values -- Handle templating requirements -- Manage secrets and sensitive data - -## Identified Challenges - -### 1. Coordination with sc-128049 -- Need to ensure KOTS CLI changes are compatible -- Timing of deployment between stories -- Feature flag or version detection strategy - -### 2. Error Handling Complexity -- Dual-path failures (KOTS success, Helm failure) -- Partial installation states -- Recovery mechanisms - -### 3. Backward Compatibility -- Support for existing installations -- Migration path for current deployments -- Feature detection and gradual rollout - -### 4. Observability -- Logging across two systems -- Metrics collection -- Debugging capabilities - -## Key Files to Modify - -### Primary Changes -1. `api/internal/managers/app/install/install.go` - Core installation logic -2. `api/internal/managers/app/install/manager.go` - Manager interface updates -3. `cmd/installer/kotscli/kotscli.go` - KOTS CLI invocation modifications - -### Supporting Changes -1. `api/internal/handlers/kubernetes/install.go` - Endpoint handlers -2. `api/controllers/app/install/install.go` - Controller logic -3. Configuration and test files - -## Dependencies and Risks - -### Dependencies -- Story sc-128049 (KOTS CLI modification) -- Existing Helm client functionality -- Release data structure - -### Risks -1. **High Risk**: Dual deployment path complexity -2. **Medium Risk**: State synchronization issues -3. **Medium Risk**: Rollback complexity -4. **Low Risk**: Performance impact - -## Recommendations - -### 1. Phased Implementation -- Phase 1: Add Helm installation capability alongside KOTS -- Phase 2: Modify KOTS CLI invocation (with sc-128049) -- Phase 3: Full integration and testing - -### 2. Feature Flag Strategy -- Implement feature flag for gradual rollout -- Allow fallback to original behavior -- Enable A/B testing in production - -### 3. Comprehensive Testing -- Unit tests for new Helm installation logic -- Integration tests for dual-path scenario -- E2E tests for complete workflow -- Rollback scenario testing - -### 4. Monitoring Strategy -- Add detailed logging at each step -- Implement metrics for success/failure rates -- Create dashboards for deployment monitoring - -## Conclusion -The architecture supports this change with existing Helm infrastructure and available chart data. The primary complexity lies in coordinating the dual deployment paths and ensuring proper state management. A phased approach with feature flags and comprehensive testing will minimize risk. \ No newline at end of file diff --git a/proposals/helmchart_values_support.md b/proposals/helmchart_values_support.md deleted file mode 100644 index 8addc9a4f9..0000000000 --- a/proposals/helmchart_values_support.md +++ /dev/null @@ -1,112 +0,0 @@ -# Proposal: HelmChart Values and OptionalValues Field Support - -**Status:** Proposed -**Epic Proposal:** [V3 App Deployment Transition](./v3_app_deployment_transition.md) -**Story:** [sc-128065](https://app.shortcut.com/replicated/story/128065) -**Iteration:** 2 (HelmChart Resource Implementation) - -## TL;DR - -Add a new method to the app release manager to extract templated HelmChart CRs with processed values, then pass this data to the app install manager. This follows the established API architecture pattern where the controller orchestrates data flow between managers, similar to how `ExtractAppPreflightSpec` works. - -## Scope - -This proposal covers the integration of HelmChart custom resource values processing into the V3 installation flow: -- Add `ExtractHelmCharts` method to app release manager interface -- Process HelmChart CRs and generate values using existing `generateHelmValues` function -- Pass installable Helm charts to app install manager - -**Out of scope for this story:** -- Other HelmChart CR fields (helmUpgradeFlags, weight, etc. - covered in other Iteration 2 stories) -- Template function enhancements (using existing functionality) -- Progress reporting (covered in Iteration 4) - -See the [epic proposal](./v3_app_deployment_transition.md) for the complete implementation plan and architectural vision. - -## Implementation - -Following the established API architecture where **controllers orchestrate data flow between managers without cross-manager dependencies**: - -**Core Changes:** -- `api/internal/managers/app/release/manager.go` - Add `ExtractInstallableHelmCharts` method to interface -- `api/internal/managers/app/release/template.go` - Implement extraction method using existing `generateHelmValues` -- `api/controllers/app/install/appinstall.go` - Call release manager to extract installable charts, then pass to install manager -- `api/internal/managers/app/install/install.go` - Update `Install` method to accept installable helm charts - -**Technical Approach:** - -1. **App Release Manager (Complete Data Processing):** -```go -type InstallableHelmChart struct { - Archive []byte - Values map[string]any - CR *kotsv1beta2.HelmChart -} - -type AppReleaseManager interface { - ExtractAppPreflightSpec(ctx context.Context, configValues types.AppConfigValues, proxySpec *ecv1beta1.ProxySpec) (*troubleshootv1beta2.PreflightSpec, error) - ExtractInstallableHelmCharts(ctx context.Context, configValues types.AppConfigValues, proxySpec *ecv1beta1.ProxySpec) ([]InstallableHelmChart, error) // New method -} -``` - -2. **Controller Orchestration (similar to preflight pattern):** -```go -// In app install controller -configValues, err := c.GetAppConfigValues(ctx) -installableCharts, err := c.appReleaseManager.ExtractInstallableHelmCharts(ctx, configValues, proxySpec) -err = c.appInstallManager.Install(ctx, installableCharts) -``` - -3. **App Install Manager (Installation Only):** -```go -// Update existing Install method signature -func (m *appInstallManager) Install(ctx context.Context, installableCharts []InstallableHelmChart) error -``` - -4. **Installation with Pre-Processed Values:** -```go -func (m *appInstallManager) installHelmChart(ctx context.Context, installableChart InstallableHelmChart) error { - // Fallback to admin console namespace if namespace is not set - namespace := installableChart.CR.GetNamespace() - if namespace == "" { - namespace = constants.KotsadmNamespace - } - - // Values are already processed by release manager - _, err = m.hcli.Install(ctx, helm.InstallOptions{ - ChartPath: chartPath, - Namespace: namespace, // From HelmChart CR - ReleaseName: installableChart.CR.GetReleaseName(), // From HelmChart CR - Values: installableChart.Values, // Pre-processed from HelmChart CR - }) -} -``` - -**Key Technical Decisions:** -1. **Clean Separation of Concerns:** Release manager handles all data processing, install manager focuses on installation mechanics -2. **Single Source of Truth:** All chart data processing happens in the release manager -3. **Pre-Processed Data:** Values are processed once by the release manager, not during installation -4. **Complete Data Package:** Each InstallableHelmChart contains everything needed for installation - -## Dependencies - -- **Requires:** Direct Helm Chart Installation (sc-128045) - provides the basic installation infrastructure -- **Follows:** Same pattern as `ExtractAppPreflightSpec` - controller orchestration between release and install managers -- **Enables:** Other HelmChart CR field support stories in Iteration 2 - -## Testing - -- Unit tests for `ExtractHelmCharts` method with various HelmChart CRs -- Unit tests for updated `Install` method with templated CRs -- Integration tests validating controller orchestration between managers -- Backward compatibility tests ensuring basic installation still works when CRs are nil -- Values precedence testing (base values vs optionalValues) - -## Risk Assessment - -**Low Risk Implementation:** -- Follows established API architecture patterns (`ExtractAppPreflightSpec`) -- Leverages existing, proven `generateHelmValues` function -- Clear separation of concerns between managers via controller orchestration -- Isolated to V3 API installations only -- Graceful fallback when HelmChart CRs are not present \ No newline at end of file diff --git a/proposals/helmchart_values_support_research.md b/proposals/helmchart_values_support_research.md deleted file mode 100644 index d1bf856345..0000000000 --- a/proposals/helmchart_values_support_research.md +++ /dev/null @@ -1,145 +0,0 @@ -# Research: HelmChart Values and OptionalValues Support - -## Executive Summary -This research document analyzes the existing `generateHelmValues` function and identifies the integration points needed to support values and optionalValues fields from HelmChart custom resources during V3 direct Helm installation. - -## Current Implementation Analysis - -### 1. Value Processing Function -Located at `api/internal/managers/app/release/template.go:generateHelmValues()`: - -**Function Signature:** -```go -func generateHelmValues(templatedCR *kotsv1beta2.HelmChart) (map[string]any, error) -``` - -**Processing Logic:** -1. Starts with base values from `templatedCR.Spec.Values` -2. Iterates through `templatedCR.Spec.OptionalValues` -3. Evaluates "when" condition for each optionalValue -4. Merges values based on `RecursiveMerge` flag -5. Converts MappedChartValue to standard Go interfaces - -### 2. HelmChart CR Templating -Located at `api/internal/managers/app/release/template.go:templateHelmChartCRs()`: - -**Function Purpose:** -- Templates HelmChart CRs using config values -- Executes template engine with proxy spec support -- Returns templated HelmChart CR objects - -**Key Operations:** -1. Parses YAML as template -2. Executes template with config values -3. Unmarshals back to HelmChart CR struct - -### 3. Current Install Manager Gap - -**Current Implementation (`api/internal/managers/app/install/install.go`):** -```go -func (m *appInstallManager) installHelmChart(ctx context.Context, chartArchive []byte, chartIndex int) error { - // TODO: namespace should come from HelmChart custom resource - // TODO: release name should come from HelmChart custom resource - _, err = m.hcli.Install(ctx, helm.InstallOptions{ - ChartPath: chartPath, - Namespace: constants.KotsadmNamespace, - ReleaseName: ch.Metadata.Name, - // Missing: Values field not populated - }) -} -``` - -**Identified Gaps:** -- No HelmChart CR processing -- No values extraction or processing -- No template engine integration -- Config values not available in install manager - -## Integration Requirements - -### 1. Data Flow -``` -Config Values → Template Engine → HelmChart CR → generateHelmValues() → Helm Install -``` - -### 2. Required Components - -**Template Engine:** -- Already exists in release manager -- Needs to be added to install manager -- Used for processing HelmChart CRs with config values - -**Config Values:** -- Currently passed to KOTS CLI -- Need to be available in install manager -- Required for templating HelmChart CRs - -**HelmChart CR Access:** -- Available in `m.releaseData.HelmChartCRs` -- Need correlation with chart archives by index -- Must handle missing CRs gracefully - -### 3. Value Merging Logic - -**Base Values:** -- Direct key-value pairs from `Spec.Values` -- Serve as foundation for configuration - -**Optional Values:** -- Conditional based on "when" expression -- Two merge strategies: - - Recursive merge using `kotsv1beta2.MergeHelmChartValues()` - - Direct key replacement using `maps.Copy()` - -## Implementation Considerations - -### 1. Error Handling -- Template parsing failures -- Invalid "when" expressions -- Missing or malformed CRs -- Value conversion errors - -### 2. Backward Compatibility -- Must handle releases without HelmChart CRs -- Default to no values if CRs unavailable -- Maintain existing behavior for non-V3 installs - -### 3. Dependencies -The implementation depends on: -- `kotsv1beta2.HelmChart` type definitions -- `kotsv1beta2.MergeHelmChartValues()` function -- Template engine from `api/pkg/template` -- Existing `generateHelmValues` function - -## Testing Requirements - -### 1. Unit Test Coverage -- Template execution with various config values -- Value merging with different strategies -- Conditional optionalValues evaluation -- Error cases and edge conditions - -### 2. Integration Test Scenarios -- Chart installation with complex values -- Multiple charts with different configurations -- Charts without HelmChart CRs -- Malformed or invalid CRs - -## Risk Assessment - -**Low Risk:** -- Reusing existing, tested functions -- Clear separation of concerns -- Graceful fallback behavior - -**Medium Risk:** -- Template engine initialization complexity -- Config value threading through managers -- Index correlation between CRs and archives - -## Recommendations - -1. **Minimize Code Duplication:** Reuse existing `generateHelmValues` function rather than reimplementing -2. **Fail Gracefully:** Continue installation without values if CR processing fails -3. **Comprehensive Logging:** Add detailed logging for debugging value processing -4. **Incremental Testing:** Test each component independently before integration \ No newline at end of file diff --git a/proposals/v3_app_deployment_transition.md b/proposals/v3_app_deployment_transition.md deleted file mode 100644 index 45510cfaa9..0000000000 --- a/proposals/v3_app_deployment_transition.md +++ /dev/null @@ -1,204 +0,0 @@ -# Proposal: Transition Application Deployment to V3 Embedded-Cluster - -**Status:** Proposed -**Epic:** [Installer Experience v2 - Milestone 7](https://app.shortcut.com/replicated/epic/126565) -**Related Stories:** sc-128045, sc-128049 - -## TL;DR - -Transition application deployment from KOTS to the V3 embedded-cluster binary to deliver Helm charts directly through our installer instead of KOTS. This enables better control, reliability, and -user experience while maintaining KOTS functionality for upgrades and management, delivered through iterative milestones. - -## The Problem - -This proposal addresses the larger architectural goal of controlling application lifecycle management outside the cluster through the embedded-cluster binary, while ensuring KOTS continues to -function end-to-end during the transition. - -**Long-term vision:** Move entire application deployment and lifecycle management from in-cluster KOTS to the external embedded-cluster binary for better control, reliability, and user experience. - -**Current iteration goal:** Enable the V3 embedded-cluster binary to handle initial application deployment with full HelmChart resource support while allowing KOTS to seamlessly take over lifecycle -management post-install. - -**Current technical problem:** The V3 API installation manager delegates entirely to KOTS CLI for application deployment, preventing direct control over Helm chart installation and HelmChart custom -resource configuration. Both KOTS and the V3 installer attempt to deploy applications during initial install, creating resource conflicts and unclear ownership. - -## Prototype / Design - -The solution transitions application deployment from KOTS to the V3 embedded-cluster binary: - -┌─────────────────┐ -│ V3 Install API │ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ ┌──────────────────┐ -│ Setup Helm │────▶│ Install Charts │ -│ Client │ │ (V3 Binary) │ -└─────────────────┘ └──────────┬───────┘ - │ - ▼ - ┌──────────────────┐ - │ Call KOTS CLI │ - │ (Skip Deploy) │ - └──────────────────┘ - -**Flow Details:** -1. V3 binary takes ownership of application deployment -2. Setup Helm client following infra manager patterns -3. Install charts directly from releaseData.HelmChartArchives with full HelmChart custom resource support -4. Call KOTS CLI with SkipDeploy: true for metadata management only -5. Fail fast on errors without complex rollback logic - -**KOTS Coordination:** KOTS skips application deployment during V3 initial installs when `EMBEDDED_CLUSTER_V3=true` environment variable is set, preventing resource conflicts while maintaining -version records and metadata. - -## Implementation Plan - -### Iteration 1: Core Deployment Transition (Stories sc-128045, sc-128049) -- **V3 Binary Takes Ownership** - - Add Helm client to appInstallManager with constructor options - - Update install() method to deploy charts before KOTS CLI - - Implement chart installation from releaseData.HelmChartArchives - -- **KOTS Delegates to V3** - - Modify KOTS DeployApp() to skip deployment for V3 EC initial installs - - Maintain version records and metadata creation - - Add V3 detection via EMBEDDED_CLUSTER_V3 environment variable - -### Iteration 2: Full HelmChart Resource Implementation -- **HelmChart Fields** - - Support namespace field configuration (sc-128055) - - Support exclude field for conditional chart deployment (sc-128056) - - Support chart weight ordering for installation sequence (sc-128057) - - Support helmUpgradeFlags for deployment customization (sc-128058) - - Support releaseName field customization (sc-128062) - - Support values and optionalValues field configuration (sc-128065) - -### Iteration 3: Complete Airgap Transition -- **V3 Binary Handles Airgap** - - Push airgap images from bundle to EC registry without KOTS (sc-128061) - - Create image pull secrets for applications (sc-128059) - - Add support for image pull secret template functions (sc-128060) - -### Iteration 4: Enhanced User Experience -- **V3 Binary Progress Reporting** - - Update app install status endpoint to return chart installation progress (sc-128046) - - Update installation page UI to show charts being installed with progress (sc-128047) - -## Key Technical Decisions - -1. **V3 Binary Ownership** - - **Decision:** V3 embedded-cluster binary takes full ownership of application deployment - - **Rationale:** Enables better control, reliability, and iterative improvement outside KOTS - -2. **KOTS Delegation Model** - - **Decision:** KOTS delegates deployment to V3 binary but maintains metadata management - - **Rationale:** Preserves KOTS functionality for upgrades while transitioning deployment control - -3. **V3 API Isolation** - - **Decision:** Only apply changes to V3 API installations - - **Rationale:** Zero risk to existing production installations - - **Benefit:** Can iterate and improve without backward compatibility concerns - -4. **Environment Variable Toggle** - - **Decision:** Use EMBEDDED_CLUSTER_V3 environment variable for detection - - **Rationale:** Clear delegation mechanism, no feature flags required - -## Files to Modify - -**Core Changes:** -- `api/internal/managers/app/install/manager.go` - Add Helm client fields and constructor options -- `api/internal/managers/app/install/install.go` - Update install() method to call Helm before KOTS CLI -- `api/internal/managers/app/install/util.go` - Move utility functions following code organization patterns -- `pkg/operator/operator.go` - Modify DeployApp() function to skip deployment for V3 EC initial installs -- `pkg/util/util.go` - Add V3 detection utilities - -**Test Updates:** -- `api/internal/managers/app/install/install_test.go` - Enhance unit tests with Helm client mocks -- `api/integration/kubernetes/install/appinstall_test.go` - Update integration tests with unified releaseData -- `api/integration/linux/install/appinstall_test.go` - Update integration tests with unified releaseData -- `pkg/operator/operator_test.go` - Add unit tests for V3 detection logic - -## External Contracts - -No changes to external APIs or events: -- Same request/response structure for `/install/app` endpoint -- Version record format unchanged -- API contracts preserved - -## Risk Assessment - -**Technical Risks:** -- V3 binary deployment integration issues (Low probability, Medium impact) -- Chart installation failures (Medium probability, Medium impact) -- KOTS delegation coordination issues (Low probability, High impact) - -**Business Risks:** -- V3 deployment regressions (Low probability, High impact) -- Increased complexity during transition (Low probability, Medium impact) -- Support burden during dual-mode operation (Low probability, Low impact) - -## Testing - -**Unit Tests:** -- Enhance TestAppInstallManager_Install with Helm client mocks -- Test coverage for V3 binary chart installation and error handling -- V3 detection logic and initial install vs upgrade scenarios - -**Integration Tests:** -- Modify TestPostInstallApp tests to validate V3 binary deployment -- Validate V3 API installation flow with direct Helm charts -- Ensure HelmChart custom resource fields are respected by V3 binary - -**Compatibility Tests:** -- Non-V3 installations continue to work unchanged (KOTS deployment) -- Existing V3 API functionality preserved -- KOTS CLI integration maintained for metadata management - -## Backward Compatibility - -Fully backward compatible: -- Only affects V3 API installations when EMBEDDED_CLUSTER_V3=true -- All existing installation types unchanged (continue using KOTS deployment) -- KOTS CLI integration preserved for metadata management -- API contracts preserved - -## Trade-offs - -**Optimizing for:** Complete transition of application deployment ownership from KOTS to V3 embedded-cluster binary while maintaining end-to-end functionality - -**Trade-offs made:** -- **Deployment Ownership Split:** V3 binary handles deployment, KOTS handles metadata - - *Mitigation:* Clear delegation model with environment variable detection -- **Testing surface:** Need to test both V3 binary deployment and KOTS delegation - - *Mitigation:* Comprehensive test coverage for both integration points -- **Sequential processing:** Charts installed one at a time vs parallel installation - - *Mitigation:* Simpler, more reliable approach; can optimize later if needed -- **Transition complexity:** Dual-mode operation during migration period - - *Mitigation:* Clear boundaries and comprehensive testing - -## Alternative Solutions Considered - -1. **Remove KOTS from V3 Entirely** - - *Rejected:* Need admin console for upgrades and management - - Would require significant changes beyond epic scope - -2. **Gradual Feature Migration** - - *Rejected:* Creates more complexity than clean delegation model - - Would increase API surface area unnecessarily - -3. **New Installation Interface** - - *Rejected:* User noted no new methods needed, just update existing Install method - - Clean delegation model achieves the same goal - -## Research - -**Prior Art in Codebase:** -- Helm Client Setup: `api/internal/managers/linux/infra/util.go` patterns for Helm client initialization -- Component Logging: `api/internal/managers/linux/infra/util.go` patterns for structured logging -- Release Data: Using existing `releaseData.HelmChartArchives` data structure - -**External References:** -- Helm Go SDK Documentation -- KOTS CLI Architecture -- Kubernetes operator deployment strategies \ No newline at end of file diff --git a/tests/dryrun/install_test.go b/tests/dryrun/install_test.go index 97c9e591d8..ce1240e7d4 100644 --- a/tests/dryrun/install_test.go +++ b/tests/dryrun/install_test.go @@ -101,7 +101,6 @@ func testDefaultInstallationImpl(t *testing.T) { "embeddedClusterID": in.Spec.ClusterID, "embeddedClusterDataDir": "/var/lib/embedded-cluster", "embeddedClusterK0sDir": "/var/lib/embedded-cluster/k0s", - "isEmbeddedClusterV3": false, }) assertHelmValuePrefixes(t, adminConsoleOpts.Values, map[string]string{ "images.kotsadm": "fake-replicated-proxy.test.net/anonymous",