Skip to content

Commit 3da0c87

Browse files
authored
Add logic to app controller to run app preflights (#2605)
* Add logic to app controller to run app preflights
1 parent 80fb2ff commit 3da0c87

25 files changed

+921
-305
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package install
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"runtime/debug"
7+
8+
apppreflightmanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/preflight"
9+
"github.com/replicatedhq/embedded-cluster/api/internal/statemachine"
10+
states "github.com/replicatedhq/embedded-cluster/api/internal/states/install"
11+
"github.com/replicatedhq/embedded-cluster/api/types"
12+
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
13+
"github.com/replicatedhq/embedded-cluster/pkg-new/preflights"
14+
)
15+
16+
type RunAppPreflightOptions struct {
17+
ConfigValues types.AppConfigValues
18+
PreflightBinaryPath string
19+
ProxySpec *ecv1beta1.ProxySpec
20+
ExtraPaths []string
21+
}
22+
23+
func (c *InstallController) RunAppPreflights(ctx context.Context, opts RunAppPreflightOptions) (finalErr error) {
24+
lock, err := c.stateMachine.AcquireLock()
25+
if err != nil {
26+
return types.NewConflictError(err)
27+
}
28+
29+
defer func() {
30+
if r := recover(); r != nil {
31+
finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack()))
32+
}
33+
if finalErr != nil {
34+
lock.Release()
35+
}
36+
}()
37+
38+
if err := c.stateMachine.ValidateTransition(lock, states.StateAppPreflightsRunning); err != nil {
39+
return types.NewConflictError(err)
40+
}
41+
42+
// Extract app preflight spec from Helm charts
43+
appPreflightSpec, err := c.appReleaseManager.ExtractAppPreflightSpec(ctx, opts.ConfigValues)
44+
if err != nil {
45+
return fmt.Errorf("extract app preflight spec: %w", err)
46+
}
47+
48+
err = c.stateMachine.Transition(lock, states.StateAppPreflightsRunning)
49+
if err != nil {
50+
return fmt.Errorf("transition states: %w", err)
51+
}
52+
53+
go func() (finalErr error) {
54+
// Background context is used to avoid canceling the operation if the context is canceled
55+
ctx := context.Background()
56+
57+
defer lock.Release()
58+
59+
defer func() {
60+
if r := recover(); r != nil {
61+
finalErr = fmt.Errorf("panic running app preflights: %v: %s", r, string(debug.Stack()))
62+
}
63+
// Handle errors from preflight execution
64+
if finalErr != nil {
65+
c.logger.Error(finalErr)
66+
67+
if err := c.stateMachine.Transition(lock, states.StateAppPreflightsExecutionFailed); err != nil {
68+
c.logger.Errorf("failed to transition states: %w", err)
69+
}
70+
return
71+
}
72+
73+
// Get the state from the preflights output
74+
state := c.getStateFromAppPreflightsOutput(ctx)
75+
// Transition to the appropriate state based on preflight results
76+
if err := c.stateMachine.Transition(lock, state); err != nil {
77+
c.logger.Errorf("failed to transition states: %w", err)
78+
}
79+
}()
80+
81+
// Create RunOptions from the provided options
82+
runOpts := preflights.RunOptions{
83+
PreflightBinaryPath: opts.PreflightBinaryPath,
84+
ProxySpec: opts.ProxySpec,
85+
ExtraPaths: opts.ExtraPaths,
86+
}
87+
88+
err := c.appPreflightManager.RunAppPreflights(ctx, apppreflightmanager.RunAppPreflightOptions{
89+
AppPreflightSpec: appPreflightSpec,
90+
RunOptions: runOpts,
91+
})
92+
if err != nil {
93+
return fmt.Errorf("run app preflights: %w", err)
94+
}
95+
96+
return nil
97+
}()
98+
99+
return nil
100+
}
101+
102+
func (c *InstallController) getStateFromAppPreflightsOutput(ctx context.Context) statemachine.State {
103+
output, err := c.GetAppPreflightOutput(ctx)
104+
// If there was an error getting the state we assume preflight execution failed
105+
if err != nil {
106+
c.logger.WithError(err).Error("error getting app preflight output")
107+
return states.StateAppPreflightsExecutionFailed
108+
}
109+
// If there is no output, we assume preflights succeeded
110+
if output == nil || !output.HasFail() {
111+
return states.StateAppPreflightsSucceeded
112+
}
113+
return states.StateAppPreflightsFailed
114+
}
115+
116+
func (c *InstallController) GetAppPreflightStatus(ctx context.Context) (types.Status, error) {
117+
return c.appPreflightManager.GetAppPreflightStatus(ctx)
118+
}
119+
120+
func (c *InstallController) GetAppPreflightOutput(ctx context.Context) (*types.PreflightsOutput, error) {
121+
return c.appPreflightManager.GetAppPreflightOutput(ctx)
122+
}
123+
124+
func (c *InstallController) GetAppPreflightTitles(ctx context.Context) ([]string, error) {
125+
return c.appPreflightManager.GetAppPreflightTitles(ctx)
126+
}

api/controllers/app/install/controller.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66

77
appconfig "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/config"
8+
apppreflightmanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/preflight"
9+
appreleasemanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/release"
810
"github.com/replicatedhq/embedded-cluster/api/internal/statemachine"
911
"github.com/replicatedhq/embedded-cluster/api/pkg/logger"
1012
"github.com/replicatedhq/embedded-cluster/api/types"
@@ -15,14 +17,20 @@ type Controller interface {
1517
TemplateAppConfig(ctx context.Context, values types.AppConfigValues, maskPasswords bool) (types.AppConfig, error)
1618
PatchAppConfigValues(ctx context.Context, values types.AppConfigValues) error
1719
GetAppConfigValues(ctx context.Context) (types.AppConfigValues, error)
20+
RunAppPreflights(ctx context.Context, opts RunAppPreflightOptions) error
21+
GetAppPreflightStatus(ctx context.Context) (types.Status, error)
22+
GetAppPreflightOutput(ctx context.Context) (*types.PreflightsOutput, error)
23+
GetAppPreflightTitles(ctx context.Context) ([]string, error)
1824
}
1925

2026
var _ Controller = (*InstallController)(nil)
2127

2228
type InstallController struct {
23-
appConfigManager appconfig.AppConfigManager
24-
stateMachine statemachine.Interface
25-
logger logrus.FieldLogger
29+
appConfigManager appconfig.AppConfigManager
30+
appPreflightManager apppreflightmanager.AppPreflightManager
31+
appReleaseManager appreleasemanager.AppReleaseManager
32+
stateMachine statemachine.Interface
33+
logger logrus.FieldLogger
2634
}
2735

2836
type InstallControllerOption func(*InstallController)
@@ -45,6 +53,18 @@ func WithStateMachine(stateMachine statemachine.Interface) InstallControllerOpti
4553
}
4654
}
4755

56+
func WithAppPreflightManager(appPreflightManager apppreflightmanager.AppPreflightManager) InstallControllerOption {
57+
return func(c *InstallController) {
58+
c.appPreflightManager = appPreflightManager
59+
}
60+
}
61+
62+
func WithAppReleaseManager(appReleaseManager appreleasemanager.AppReleaseManager) InstallControllerOption {
63+
return func(c *InstallController) {
64+
c.appReleaseManager = appReleaseManager
65+
}
66+
}
67+
4868
func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) {
4969
controller := &InstallController{
5070
logger: logger.NewDiscardLogger(),
@@ -68,5 +88,11 @@ func (c *InstallController) validateInit() error {
6888
if c.stateMachine == nil {
6989
return errors.New("stateMachine is required for App Install Controller")
7090
}
91+
if c.appPreflightManager == nil {
92+
return errors.New("appPreflightManager is required for App Install Controller")
93+
}
94+
if c.appReleaseManager == nil {
95+
return errors.New("appReleaseManager is required for App Install Controller")
96+
}
7197
return nil
7298
}

api/controllers/app/install/controller_mock.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,36 @@ func (m *MockController) GetAppConfigValues(ctx context.Context) (types.AppConfi
3434
}
3535
return args.Get(0).(types.AppConfigValues), args.Error(1)
3636
}
37+
38+
// RunAppPreflights mocks the RunAppPreflights method
39+
func (m *MockController) RunAppPreflights(ctx context.Context, opts RunAppPreflightOptions) error {
40+
args := m.Called(ctx, opts)
41+
return args.Error(0)
42+
}
43+
44+
// GetAppPreflightStatus mocks the GetAppPreflightStatus method
45+
func (m *MockController) GetAppPreflightStatus(ctx context.Context) (types.Status, error) {
46+
args := m.Called(ctx)
47+
if args.Get(0) == nil {
48+
return types.Status{}, args.Error(1)
49+
}
50+
return args.Get(0).(types.Status), args.Error(1)
51+
}
52+
53+
// GetAppPreflightOutput mocks the GetAppPreflightOutput method
54+
func (m *MockController) GetAppPreflightOutput(ctx context.Context) (*types.PreflightsOutput, error) {
55+
args := m.Called(ctx)
56+
if args.Get(0) == nil {
57+
return nil, args.Error(1)
58+
}
59+
return args.Get(0).(*types.PreflightsOutput), args.Error(1)
60+
}
61+
62+
// GetAppPreflightTitles mocks the GetAppPreflightTitles method
63+
func (m *MockController) GetAppPreflightTitles(ctx context.Context) ([]string, error) {
64+
args := m.Called(ctx)
65+
if args.Get(0) == nil {
66+
return nil, args.Error(1)
67+
}
68+
return args.Get(0).([]string), args.Error(1)
69+
}

api/controllers/app/install/test_suite.go

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"time"
77

88
appconfig "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/config"
9+
apppreflightmanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/preflight"
10+
appreleasemanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/release"
911
"github.com/replicatedhq/embedded-cluster/api/internal/statemachine"
1012
states "github.com/replicatedhq/embedded-cluster/api/internal/states/install"
1113
"github.com/replicatedhq/embedded-cluster/api/types"
@@ -37,10 +39,10 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
3739
},
3840
currentState: states.StateNew,
3941
expectedState: states.StateApplicationConfigured,
40-
setupMocks: func(am *appconfig.MockAppConfigManager) {
42+
setupMocks: func(acm *appconfig.MockAppConfigManager) {
4143
mock.InOrder(
42-
am.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
43-
am.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
44+
acm.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
45+
acm.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
4446
)
4547
},
4648
expectedErr: false,
@@ -52,10 +54,10 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
5254
},
5355
currentState: states.StateApplicationConfigurationFailed,
5456
expectedState: states.StateApplicationConfigured,
55-
setupMocks: func(am *appconfig.MockAppConfigManager) {
57+
setupMocks: func(acm *appconfig.MockAppConfigManager) {
5658
mock.InOrder(
57-
am.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
58-
am.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
59+
acm.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
60+
acm.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
5961
)
6062
},
6163
expectedErr: false,
@@ -67,10 +69,10 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
6769
},
6870
currentState: states.StateApplicationConfigured,
6971
expectedState: states.StateApplicationConfigured,
70-
setupMocks: func(am *appconfig.MockAppConfigManager) {
72+
setupMocks: func(acm *appconfig.MockAppConfigManager) {
7173
mock.InOrder(
72-
am.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
73-
am.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
74+
acm.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
75+
acm.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
7476
)
7577
},
7678
expectedErr: false,
@@ -82,9 +84,9 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
8284
},
8385
currentState: states.StateNew,
8486
expectedState: states.StateApplicationConfigurationFailed,
85-
setupMocks: func(am *appconfig.MockAppConfigManager) {
87+
setupMocks: func(acm *appconfig.MockAppConfigManager) {
8688
mock.InOrder(
87-
am.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "invalid-value"}}).Return(errors.New("validation error")),
89+
acm.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "invalid-value"}}).Return(errors.New("validation error")),
8890
)
8991
},
9092
expectedErr: true,
@@ -96,10 +98,10 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
9698
},
9799
currentState: states.StateNew,
98100
expectedState: states.StateApplicationConfigurationFailed,
99-
setupMocks: func(am *appconfig.MockAppConfigManager) {
101+
setupMocks: func(acm *appconfig.MockAppConfigManager) {
100102
mock.InOrder(
101-
am.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
102-
am.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(errors.New("set config error")),
103+
acm.On("ValidateConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(nil),
104+
acm.On("PatchConfigValues", types.AppConfigValues{"test-item": types.AppConfigValue{Value: "new-item"}}).Return(errors.New("set config error")),
103105
)
104106
},
105107
expectedErr: true,
@@ -111,7 +113,7 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
111113
},
112114
currentState: states.StateInfrastructureInstalling,
113115
expectedState: states.StateInfrastructureInstalling,
114-
setupMocks: func(am *appconfig.MockAppConfigManager) {
116+
setupMocks: func(acm *appconfig.MockAppConfigManager) {
115117
},
116118
expectedErr: true,
117119
},
@@ -120,15 +122,19 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
120122
for _, tt := range tests {
121123
s.T().Run(tt.name, func(t *testing.T) {
122124

123-
manager := &appconfig.MockAppConfigManager{}
125+
appConfigManager := &appconfig.MockAppConfigManager{}
126+
appPreflightManager := &apppreflightmanager.MockAppPreflightManager{}
127+
appReleaseManager := &appreleasemanager.MockAppReleaseManager{}
124128
sm := s.CreateStateMachine(tt.currentState)
125129
controller, err := NewInstallController(
126130
WithStateMachine(sm),
127-
WithAppConfigManager(manager),
131+
WithAppConfigManager(appConfigManager),
132+
WithAppPreflightManager(appPreflightManager),
133+
WithAppReleaseManager(appReleaseManager),
128134
)
129135
require.NoError(t, err, "failed to create install controller")
130136

131-
tt.setupMocks(manager)
137+
tt.setupMocks(appConfigManager)
132138
err = controller.PatchAppConfigValues(t.Context(), tt.values)
133139

134140
if tt.expectedErr {
@@ -141,7 +147,7 @@ func (s *AppInstallControllerTestSuite) TestPatchAppConfigValues() {
141147
return sm.CurrentState() == tt.expectedState
142148
}, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState())
143149
assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after setting app config values")
144-
manager.AssertExpectations(s.T())
150+
appConfigManager.AssertExpectations(s.T())
145151

146152
})
147153
}

api/controllers/kubernetes/install/controller.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77

88
appcontroller "github.com/replicatedhq/embedded-cluster/api/controllers/app/install"
99
appconfig "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/config"
10+
apppreflightmanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/preflight"
11+
appreleasemanager "github.com/replicatedhq/embedded-cluster/api/internal/managers/app/release"
1012
"github.com/replicatedhq/embedded-cluster/api/internal/managers/kubernetes/infra"
1113
"github.com/replicatedhq/embedded-cluster/api/internal/managers/kubernetes/installation"
1214
"github.com/replicatedhq/embedded-cluster/api/internal/statemachine"
@@ -38,6 +40,8 @@ type InstallController struct {
3840
installationManager installation.InstallationManager
3941
infraManager infra.InfraManager
4042
appConfigManager appconfig.AppConfigManager
43+
appPreflightManager apppreflightmanager.AppPreflightManager
44+
appReleaseManager appreleasemanager.AppReleaseManager
4145
metricsReporter metrics.ReporterInterface
4246
restClientGetter genericclioptions.RESTClientGetter
4347
releaseData *release.ReleaseData
@@ -224,9 +228,29 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController,
224228
}
225229
}
226230

231+
if controller.appPreflightManager == nil {
232+
controller.appPreflightManager = apppreflightmanager.NewAppPreflightManager(
233+
apppreflightmanager.WithLogger(controller.logger),
234+
)
235+
}
236+
237+
if controller.appReleaseManager == nil {
238+
appReleaseManager, err := appreleasemanager.NewAppReleaseManager(
239+
*controller.releaseData.AppConfig,
240+
appreleasemanager.WithLogger(controller.logger),
241+
appreleasemanager.WithReleaseData(controller.releaseData),
242+
)
243+
if err != nil {
244+
return nil, fmt.Errorf("create app release manager: %w", err)
245+
}
246+
controller.appReleaseManager = appReleaseManager
247+
}
248+
227249
// Initialize the app controller with the app config manager and state machine
228250
appInstallController, err := appcontroller.NewInstallController(
229251
appcontroller.WithAppConfigManager(controller.appConfigManager),
252+
appcontroller.WithAppPreflightManager(controller.appPreflightManager),
253+
appcontroller.WithAppReleaseManager(controller.appReleaseManager),
230254
appcontroller.WithStateMachine(controller.stateMachine),
231255
appcontroller.WithLogger(controller.logger),
232256
)

0 commit comments

Comments
 (0)