Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/controllers/app/install/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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, ignoreAppPreflights bool) error
InstallApp(ctx context.Context, opts InstallAppOptions) error
GetAppInstallStatus(ctx context.Context) (types.AppInstall, error)
}

Expand Down
4 changes: 2 additions & 2 deletions api/controllers/app/install/controller_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ignoreAppPreflights bool) error {
args := m.Called(ctx, ignoreAppPreflights)
func (m *MockController) InstallApp(ctx context.Context, opts InstallAppOptions) error {
args := m.Called(ctx, opts)
return args.Error(0)
}

Expand Down
31 changes: 25 additions & 6 deletions api/controllers/app/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ 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, ignoreAppPreflights bool) (finalErr error) {
func (c *InstallController) InstallApp(ctx context.Context, opts InstallAppOptions) (finalErr error) {
lock, err := c.stateMachine.AcquireLock()
if err != nil {
return types.NewConflictError(err)
Expand All @@ -33,7 +40,7 @@ func (c *InstallController) InstallApp(ctx context.Context, ignoreAppPreflights
// 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 !ignoreAppPreflights || !allowIgnoreAppPreflights {
if !opts.IgnoreAppPreflights || !allowIgnoreAppPreflights {
return types.NewBadRequestError(ErrAppPreflightChecksFailed)
}
err = c.stateMachine.Transition(lock, states.StateAppPreflightsFailedBypassed)
Expand All @@ -47,9 +54,15 @@ func (c *InstallController) InstallApp(ctx context.Context, ignoreAppPreflights
}

// Get config values for app installation
configValues, err := c.appConfigManager.GetKotsadmConfigValues()
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()
if err != nil {
return fmt.Errorf("get kotsadm config values for app install: %w", err)
return fmt.Errorf("get kots config values for app install: %w", err)
}

err = c.stateMachine.Transition(lock, states.StateAppInstalling)
Expand Down Expand Up @@ -80,8 +93,14 @@ func (c *InstallController) InstallApp(ctx context.Context, ignoreAppPreflights
}
}()

// Install the app
err := c.appInstallManager.Install(ctx, configValues)
// 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)
if err != nil {
return fmt.Errorf("install app: %w", err)
}
Expand Down
155 changes: 114 additions & 41 deletions api/controllers/app/install/test_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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"
Expand Down Expand Up @@ -503,16 +504,18 @@ 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, *appinstallmanager.MockAppInstallManager)
setupMocks func(*appconfig.MockAppConfigManager, *appreleasemanager.MockAppReleaseManager, *appinstallmanager.MockAppInstallManager)
expectedErr bool
}{
{
name: "invalid state transition from succeeded state",
currentState: states.StateSucceeded,
expectedState: states.StateSucceeded,
setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) {
setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) {
// No mocks needed for invalid state transition
},
expectedErr: true,
Expand All @@ -521,27 +524,37 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() {
name: "invalid state transition from infrastructure installing state",
currentState: states.StateInfrastructureInstalling,
expectedState: states.StateInfrastructureInstalling,
setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) {
setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, aim *appinstallmanager.MockAppInstallManager) {
// No mocks needed for invalid state transition
},
expectedErr: true,
},
{
name: "successful app installation from app preflights succeeded state",
name: "successful app installation from app preflights succeeded state with helm charts",
currentState: states.StateAppPreflightsSucceeded,
expectedState: states.StateSucceeded,
setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) {
mock.InOrder(
acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{
Spec: kotsv1beta1.ConfigValuesSpec{
Values: map[string]kotsv1beta1.ConfigValue{
"test-key": {Value: "test-value"},
},
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"},
},
}, nil),
aim.On("Install", mock.Anything, mock.MatchedBy(func(cv kotsv1beta1.ConfigValues) bool {
return cv.Spec.Values["test-key"].Value == "test-value"
})).Return(nil),
},
}
expectedCharts := []types.InstallableHelmChart{
{
Archive: []byte("chart-archive-data"),
Values: map[string]any{"key": "value"},
},
}
appConfigValues := types.AppConfigValues{
"test-key": types.AppConfigValue{Value: "test-value"},
}
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),
)
},
expectedErr: false,
Expand All @@ -550,18 +563,22 @@ 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, aim *appinstallmanager.MockAppInstallManager) {
mock.InOrder(
acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{
Spec: kotsv1beta1.ConfigValuesSpec{
Values: map[string]kotsv1beta1.ConfigValue{
"test-key": {Value: "test-value"},
},
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"},
},
}, nil),
aim.On("Install", mock.Anything, mock.MatchedBy(func(cv kotsv1beta1.ConfigValues) bool {
return cv.Spec.Values["test-key"].Value == "test-value"
})).Return(nil),
},
}
appConfigValues := types.AppConfigValues{
"test-key": types.AppConfigValue{Value: "test-value"},
}
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),
)
},
expectedErr: false,
Expand All @@ -570,7 +587,11 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() {
name: "get config values error",
currentState: states.StateAppPreflightsSucceeded,
expectedState: states.StateAppPreflightsSucceeded,
setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) {
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)
acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{}, errors.New("config values error"))
},
expectedErr: true,
Expand All @@ -580,18 +601,22 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() {
ignoreAppPreflights: true,
currentState: states.StateAppPreflightsFailed,
expectedState: states.StateSucceeded,
setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) {
mock.InOrder(
acm.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{
Spec: kotsv1beta1.ConfigValuesSpec{
Values: map[string]kotsv1beta1.ConfigValue{
"test-key": {Value: "test-value"},
},
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"},
},
}, nil),
aim.On("Install", mock.Anything, mock.MatchedBy(func(cv kotsv1beta1.ConfigValues) bool {
return cv.Spec.Values["test-key"].Value == "test-value"
})).Return(nil),
},
}
appConfigValues := types.AppConfigValues{
"test-key": types.AppConfigValue{Value: "test-value"},
}
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),
)
},
expectedErr: false,
Expand All @@ -601,11 +626,54 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() {
ignoreAppPreflights: false,
currentState: states.StateAppPreflightsFailed,
expectedState: states.StateAppPreflightsFailed,
setupMocks: func(acm *appconfig.MockAppConfigManager, aim *appinstallmanager.MockAppInstallManager) {
setupMocks: func(acm *appconfig.MockAppConfigManager, arm *appreleasemanager.MockAppReleaseManager, 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 {
Expand All @@ -627,8 +695,12 @@ func (s *AppInstallControllerTestSuite) TestInstallApp() {
)
require.NoError(t, err, "failed to create install controller")

tt.setupMocks(appConfigManager, appInstallManager)
err = controller.InstallApp(t.Context(), tt.ignoreAppPreflights)
tt.setupMocks(appConfigManager, appReleaseManager, appInstallManager)
err = controller.InstallApp(t.Context(), InstallAppOptions{
IgnoreAppPreflights: tt.ignoreAppPreflights,
ProxySpec: tt.proxySpec,
RegistrySettings: tt.registrySettings,
})

if tt.expectedErr {
assert.Error(t, err)
Expand All @@ -643,6 +715,7 @@ 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())
})
}
Expand Down
4 changes: 2 additions & 2 deletions api/controllers/kubernetes/install/controller_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ignoreAppPreflights bool) error {
args := m.Called(ctx, ignoreAppPreflights)
func (m *MockController) InstallApp(ctx context.Context, opts appcontroller.InstallAppOptions) error {
args := m.Called(ctx, opts)
return args.Error(0)
}

Expand Down
4 changes: 2 additions & 2 deletions api/controllers/linux/install/controller_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ignoreAppPreflights bool) error {
args := m.Called(ctx, ignoreAppPreflights)
func (m *MockController) InstallApp(ctx context.Context, opts appcontroller.InstallAppOptions) error {
args := m.Called(ctx, opts)
return args.Error(0)
}

Expand Down
Loading
Loading