Skip to content

Commit dff1177

Browse files
authored
feat(api): app config state transitions (#2436)
* feat(api): app config state transitions * f * f * f * f * f * f * f * feedback
1 parent efecd4b commit dff1177

File tree

23 files changed

+775
-146
lines changed

23 files changed

+775
-146
lines changed

api/controllers/kubernetes/install/app.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"runtime/debug"
78

9+
"github.com/replicatedhq/embedded-cluster/api/types"
810
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
911
)
1012

@@ -26,6 +28,44 @@ func (c *InstallController) GetAppConfig(ctx context.Context) (kotsv1beta1.Confi
2628
return appConfig, nil
2729
}
2830

29-
func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[string]string) error {
30-
return c.appConfigManager.SetConfigValues(ctx, values)
31+
func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[string]string) (finalErr error) {
32+
lock, err := c.stateMachine.AcquireLock()
33+
if err != nil {
34+
return types.NewConflictError(err)
35+
}
36+
defer lock.Release()
37+
38+
err = c.stateMachine.ValidateTransition(lock, StateApplicationConfiguring, StateApplicationConfigured)
39+
if err != nil {
40+
return types.NewConflictError(err)
41+
}
42+
43+
err = c.stateMachine.Transition(lock, StateApplicationConfiguring)
44+
if err != nil {
45+
return fmt.Errorf("failed to transition states: %w", err)
46+
}
47+
48+
defer func() {
49+
if r := recover(); r != nil {
50+
finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack()))
51+
}
52+
53+
if finalErr != nil {
54+
if err := c.stateMachine.Transition(lock, StateApplicationConfigurationFailed); err != nil {
55+
c.logger.Errorf("failed to transition states: %w", err)
56+
}
57+
}
58+
}()
59+
60+
err = c.appConfigManager.SetConfigValues(ctx, values)
61+
if err != nil {
62+
return fmt.Errorf("set app config values: %w", err)
63+
}
64+
65+
err = c.stateMachine.Transition(lock, StateApplicationConfigured)
66+
if err != nil {
67+
return fmt.Errorf("failed to transition states: %w", err)
68+
}
69+
70+
return nil
3171
}

api/controllers/kubernetes/install/controller_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func TestConfigureInstallation(t *testing.T) {
130130
HTTPSProxy: "https://proxy.example.com:3128",
131131
NoProxy: "localhost,127.0.0.1",
132132
},
133-
currentState: StateNew,
133+
currentState: StateApplicationConfigured,
134134
expectedState: StateInstallationConfigured,
135135
setupMock: func(m *installation.MockInstallationManager, ki *kubernetesinstallation.MockInstallation, config types.KubernetesInstallationConfig) {
136136
mock.InOrder(
@@ -142,7 +142,7 @@ func TestConfigureInstallation(t *testing.T) {
142142
{
143143
name: "configure installation error",
144144
config: types.KubernetesInstallationConfig{},
145-
currentState: StateNew,
145+
currentState: StateApplicationConfigured,
146146
expectedState: StateInstallationConfigurationFailed,
147147
setupMock: func(m *installation.MockInstallationManager, ki *kubernetesinstallation.MockInstallation, config types.KubernetesInstallationConfig) {
148148
m.On("ConfigureInstallation", mock.Anything, ki, config).Return(errors.New("validation error"))

api/controllers/kubernetes/install/installation.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@ func (c *InstallController) ConfigureInstallation(ctx context.Context, config ty
3232
}
3333
defer lock.Release()
3434

35-
if err := c.stateMachine.ValidateTransition(lock, StateInstallationConfigured); err != nil {
35+
if err := c.stateMachine.ValidateTransition(lock, StateInstallationConfiguring, StateInstallationConfigured); err != nil {
3636
return types.NewConflictError(err)
3737
}
3838

39+
err = c.stateMachine.Transition(lock, StateInstallationConfiguring)
40+
if err != nil {
41+
return fmt.Errorf("failed to transition states: %w", err)
42+
}
43+
3944
defer func() {
4045
if r := recover(); r != nil {
4146
finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack()))

api/controllers/kubernetes/install/statemachine.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import (
88
const (
99
// StateNew is the initial state of the install process
1010
StateNew statemachine.State = "New"
11+
// StateApplicationConfiguring is the state of the install process when the application is being configured
12+
StateApplicationConfiguring statemachine.State = "ApplicationConfiguring"
13+
// StateApplicationConfigurationFailed is the state of the install process when the application failed to be configured
14+
StateApplicationConfigurationFailed statemachine.State = "ApplicationConfigurationFailed"
15+
// StateApplicationConfigured is the state of the install process when the application is configured
16+
StateApplicationConfigured statemachine.State = "ApplicationConfigured"
17+
// StateInstallationConfiguring is the state of the install process when the installation is being configured
18+
StateInstallationConfiguring statemachine.State = "InstallationConfiguring"
1119
// StateInstallationConfigurationFailed is the state of the install process when the installation failed to be configured
1220
StateInstallationConfigurationFailed statemachine.State = "InstallationConfigurationFailed"
1321
// StateInstallationConfigured is the state of the install process when the installation is configured
@@ -21,12 +29,17 @@ const (
2129
)
2230

2331
var validStateTransitions = map[statemachine.State][]statemachine.State{
24-
StateNew: {StateInstallationConfigured, StateInstallationConfigurationFailed},
25-
StateInstallationConfigurationFailed: {StateInstallationConfigured, StateInstallationConfigurationFailed},
26-
StateInstallationConfigured: {StateInfrastructureInstalling, StateInstallationConfigured, StateInstallationConfigurationFailed},
32+
StateNew: {StateApplicationConfiguring},
33+
StateApplicationConfigurationFailed: {StateApplicationConfiguring},
34+
StateApplicationConfiguring: {StateApplicationConfigured, StateApplicationConfigurationFailed},
35+
StateApplicationConfigured: {StateApplicationConfiguring, StateInstallationConfiguring},
36+
StateInstallationConfiguring: {StateInstallationConfigured, StateInstallationConfigurationFailed},
37+
StateInstallationConfigurationFailed: {StateApplicationConfiguring, StateInstallationConfiguring},
38+
StateInstallationConfigured: {StateApplicationConfiguring, StateInstallationConfiguring, StateInfrastructureInstalling},
2739
StateInfrastructureInstalling: {StateSucceeded, StateInfrastructureInstallFailed},
28-
StateInfrastructureInstallFailed: {},
29-
StateSucceeded: {},
40+
// final states
41+
StateInfrastructureInstallFailed: {},
42+
StateSucceeded: {},
3043
}
3144

3245
type StateMachineOptions struct {

api/controllers/kubernetes/install/statemachine_test.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,58 @@ func TestStateMachineTransitions(t *testing.T) {
1515
validTransitions []statemachine.State
1616
}{
1717
{
18-
name: `State "New" can transition to "InstallationConfigured" or "InstallationConfigurationFailed"`,
18+
name: `State "New" can transition to "ApplicationConfiguring"`,
1919
startState: StateNew,
20+
validTransitions: []statemachine.State{
21+
StateApplicationConfiguring,
22+
},
23+
},
24+
{
25+
name: `State "ApplicationConfiguring" can transition to "ApplicationConfigured" or "ApplicationConfigurationFailed"`,
26+
startState: StateApplicationConfiguring,
27+
validTransitions: []statemachine.State{
28+
StateApplicationConfigured,
29+
StateApplicationConfigurationFailed,
30+
},
31+
},
32+
{
33+
name: `State "ApplicationConfigurationFailed" can transition to "ApplicationConfiguring"`,
34+
startState: StateApplicationConfigurationFailed,
35+
validTransitions: []statemachine.State{
36+
StateApplicationConfiguring,
37+
},
38+
},
39+
{
40+
name: `State "ApplicationConfigured" can transition to "ApplicationConfiguring" or "InstallationConfiguring"`,
41+
startState: StateApplicationConfigured,
42+
validTransitions: []statemachine.State{
43+
StateApplicationConfiguring,
44+
StateInstallationConfiguring,
45+
},
46+
},
47+
{
48+
name: `State "InstallationConfiguring" can transition to "InstallationConfigured" or "InstallationConfigurationFailed"`,
49+
startState: StateInstallationConfiguring,
2050
validTransitions: []statemachine.State{
2151
StateInstallationConfigured,
2252
StateInstallationConfigurationFailed,
2353
},
2454
},
2555
{
26-
name: `State "InstallationConfigurationFailed" can transition to "InstallationConfigured" or "InstallationConfigurationFailed"`,
56+
name: `State "InstallationConfigurationFailed" can transition to "ApplicationConfiguring" or "InstallationConfiguring"`,
2757
startState: StateInstallationConfigurationFailed,
2858
validTransitions: []statemachine.State{
29-
StateInstallationConfigured,
30-
StateInstallationConfigurationFailed,
59+
StateApplicationConfiguring,
60+
StateInstallationConfiguring,
3161
},
3262
},
3363
{
34-
name: `State "InstallationConfigured" can transition to "InfrastructureInstalling" or "InstallationConfigured" or "InstallationConfigurationFailed"`,
64+
name: `State "InstallationConfigured" can transition to "ApplicationConfiguring" or "InstallationConfiguring" or "InfrastructureInstalling"`,
3565
startState: StateInstallationConfigured,
3666
validTransitions: []statemachine.State{
67+
StateApplicationConfiguring,
68+
StateInstallationConfiguring,
3769
StateInfrastructureInstalling,
38-
StateInstallationConfigured,
39-
StateInstallationConfigurationFailed,
4070
},
4171
},
4272
{
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Kubernetes Installation State Machine
2+
3+
## State Transition Diagram
4+
5+
```mermaid
6+
stateDiagram-v2
7+
[*] --> New
8+
9+
New --> ApplicationConfiguring
10+
11+
ApplicationConfiguring --> ApplicationConfigured
12+
ApplicationConfiguring --> ApplicationConfigurationFailed
13+
14+
ApplicationConfigurationFailed --> ApplicationConfiguring
15+
16+
ApplicationConfigured --> ApplicationConfiguring
17+
ApplicationConfigured --> InstallationConfiguring
18+
19+
InstallationConfiguring --> InstallationConfigured
20+
InstallationConfiguring --> InstallationConfigurationFailed
21+
22+
InstallationConfigurationFailed --> ApplicationConfiguring
23+
InstallationConfigurationFailed --> InstallationConfiguring
24+
25+
InstallationConfigured --> ApplicationConfiguring
26+
InstallationConfigured --> InstallationConfiguring
27+
InstallationConfigured --> InfrastructureInstalling
28+
29+
InfrastructureInstalling --> Succeeded
30+
InfrastructureInstalling --> InfrastructureInstallFailed
31+
32+
InfrastructureInstallFailed --> [*]
33+
Succeeded --> [*]
34+
```
35+
36+
## State Descriptions
37+
38+
- **New**: Initial state of the install process
39+
- **ApplicationConfiguring**: Application is being configured
40+
- **ApplicationConfigured**: Application is configured
41+
- **ApplicationConfigurationFailed**: Application failed to be configured
42+
- **InstallationConfiguring**: Installation is being configured
43+
- **InstallationConfigured**: Installation is configured
44+
- **InstallationConfigurationFailed**: Installation failed to be configured
45+
- **InfrastructureInstalling**: Infrastructure is being installed
46+
- **InfrastructureInstallFailed**: Infrastructure failed to install (final state)
47+
- **Succeeded**: Installation has succeeded (final state)
48+
49+
## Key Observations
50+
51+
1. **Final States**: Only `InfrastructureInstallFailed` and `Succeeded` are final states (no outgoing transitions)
52+
2. **Recovery Paths**: Most failure states allow recovery by transitioning back to earlier states
53+
3. **Configuration States**: The system has separate configuring/configured states for Application and Installation phases
54+
4. **Simplified Flow**: This state machine is simpler than the Linux version, lacking Host configuration and Preflight phases
55+
5. **Bidirectional Flow**: Most states can transition back to earlier configuration states for retry scenarios
56+
57+
## Comparison with Linux State Machine
58+
59+
The Kubernetes installation state machine is a simplified version of the Linux state machine:
60+
61+
- **Missing Phases**: No Host configuration or Preflight phases
62+
- **Streamlined Process**: Direct progre
63+
sion from Installation configuration to Infrastructure installation
64+
- **Fewer States**: 10 states vs 16 states in the Linux version
65+
- **Similar Recovery**: Maintains the same recovery patterns for configuration failures

api/controllers/linux/install/app.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"runtime/debug"
78

9+
"github.com/replicatedhq/embedded-cluster/api/types"
810
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
911
)
1012

@@ -26,6 +28,44 @@ func (c *InstallController) GetAppConfig(ctx context.Context) (kotsv1beta1.Confi
2628
return appConfig, nil
2729
}
2830

29-
func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[string]string) error {
30-
return c.appConfigManager.SetConfigValues(ctx, values)
31+
func (c *InstallController) SetAppConfigValues(ctx context.Context, values map[string]string) (finalErr error) {
32+
lock, err := c.stateMachine.AcquireLock()
33+
if err != nil {
34+
return types.NewConflictError(err)
35+
}
36+
defer lock.Release()
37+
38+
err = c.stateMachine.ValidateTransition(lock, StateApplicationConfiguring, StateApplicationConfigured)
39+
if err != nil {
40+
return types.NewConflictError(err)
41+
}
42+
43+
err = c.stateMachine.Transition(lock, StateApplicationConfiguring)
44+
if err != nil {
45+
return fmt.Errorf("failed to transition states: %w", err)
46+
}
47+
48+
defer func() {
49+
if r := recover(); r != nil {
50+
finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack()))
51+
}
52+
53+
if finalErr != nil {
54+
if err := c.stateMachine.Transition(lock, StateApplicationConfigurationFailed); err != nil {
55+
c.logger.Errorf("failed to transition states: %w", err)
56+
}
57+
}
58+
}()
59+
60+
err = c.appConfigManager.SetConfigValues(ctx, values)
61+
if err != nil {
62+
return fmt.Errorf("set app config values: %w", err)
63+
}
64+
65+
err = c.stateMachine.Transition(lock, StateApplicationConfigured)
66+
if err != nil {
67+
return fmt.Errorf("failed to transition states: %w", err)
68+
}
69+
70+
return nil
3171
}

api/controllers/linux/install/controller_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func TestConfigureInstallation(t *testing.T) {
179179
LocalArtifactMirrorPort: 9000,
180180
DataDirectory: t.TempDir(),
181181
},
182-
currentState: StateNew,
182+
currentState: StateApplicationConfigured,
183183
expectedState: StateHostConfigured,
184184

185185
setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.LinuxInstallationConfig, st *store.MockStore, mr *metrics.MockReporter) {
@@ -194,7 +194,7 @@ func TestConfigureInstallation(t *testing.T) {
194194
{
195195
name: "validatation error",
196196
config: types.LinuxInstallationConfig{},
197-
currentState: StateNew,
197+
currentState: StateApplicationConfigured,
198198
expectedState: StateInstallationConfigurationFailed,
199199
setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.LinuxInstallationConfig, st *store.MockStore, mr *metrics.MockReporter) {
200200
mock.InOrder(
@@ -248,7 +248,7 @@ func TestConfigureInstallation(t *testing.T) {
248248
{
249249
name: "set config error",
250250
config: types.LinuxInstallationConfig{},
251-
currentState: StateNew,
251+
currentState: StateApplicationConfigured,
252252
expectedState: StateInstallationConfigurationFailed,
253253
setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.LinuxInstallationConfig, st *store.MockStore, mr *metrics.MockReporter) {
254254
mock.InOrder(
@@ -308,7 +308,7 @@ func TestConfigureInstallation(t *testing.T) {
308308
LocalArtifactMirrorPort: 9000,
309309
DataDirectory: t.TempDir(),
310310
},
311-
currentState: StateNew,
311+
currentState: StateApplicationConfigured,
312312
expectedState: StateHostConfigurationFailed,
313313
setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.LinuxInstallationConfig, st *store.MockStore, mr *metrics.MockReporter) {
314314
mock.InOrder(
@@ -365,7 +365,7 @@ func TestConfigureInstallation(t *testing.T) {
365365
GlobalCIDR: "10.0.0.0/16",
366366
DataDirectory: t.TempDir(),
367367
},
368-
currentState: StateNew,
368+
currentState: StateApplicationConfigured,
369369
expectedState: StateHostConfigured,
370370
setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.LinuxInstallationConfig, st *store.MockStore, mr *metrics.MockReporter) {
371371
// Create a copy with expected CIDR values after computation

0 commit comments

Comments
 (0)