Skip to content

feat: use appslug for default data directory in install wizard #2380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -343,14 +343,24 @@ create-node%: DISTRO = debian-bookworm
create-node%: NODE_PORT = 30000
create-node%: MANAGER_NODE_PORT = 30080
create-node%: K0S_DATA_DIR = /var/lib/embedded-cluster/k0s
create-node%: K0S_DATA_DIR_V3 = $(shell \
if [ -n "$(REPLICATED_APP)" ]; then \
echo "/var/lib/$(shell echo '$(REPLICATED_APP)' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')/k0s"; \
else \
echo "/var/lib/embedded-cluster-smoke-test-staging-app/k0s"; \
fi)
create-node%: ENABLE_V3 = 0
create-node%:
@echo "Mounting data directories:"
@echo " v2: $(K0S_DATA_DIR)"
@echo " v3: $(K0S_DATA_DIR_V3)"
@docker run -d \
--name node$* \
--hostname node$* \
--privileged \
--restart=unless-stopped \
-v $(K0S_DATA_DIR) \
-v $(K0S_DATA_DIR_V3) \
-v $(shell pwd):/replicatedhq/embedded-cluster \
-v $(shell dirname $(shell pwd))/kots:/replicatedhq/kots \
$(if $(filter node0,node$*),-p $(NODE_PORT):$(NODE_PORT)) \
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ Additionally, it includes a Registry when deployed in air gap mode, and SeaweedF
make list-distros
```

**Note:** The development environment automatically mounts both data directories to support v2 and v3:
- **v2 mode:** Uses `/var/lib/embedded-cluster/k0s`
- **v3 mode:** Uses `/var/lib/{app-slug}/k0s` (determined from `REPLICATED_APP`)

Both directories are mounted automatically, so the embedded cluster binary can use whichever one it needs without any manual configuration.

1. In the Vendor Portal, create and download a license that is assigned to the channel.
We recommend storing this license in the `local-dev/` directory, as it is gitignored and not otherwise used by the CI.

Expand Down
71 changes: 47 additions & 24 deletions api/controllers/linux/install/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,73 +52,96 @@ var warnPreflightOutput = &types.HostPreflightsOutput{
func TestGetInstallationConfig(t *testing.T) {
tests := []struct {
name string
setupMock func(*installation.MockInstallationManager)
setupMock func(*installation.MockInstallationManager, string)
expectedErr bool
expectedValue types.InstallationConfig
expectedValue func(string) types.InstallationConfig
}{
{
name: "successful get",
setupMock: func(m *installation.MockInstallationManager) {
name: "successful read and defaults",
setupMock: func(m *installation.MockInstallationManager, tempDir string) {
config := types.InstallationConfig{
AdminConsolePort: 9000,
GlobalCIDR: "10.0.0.1/16",
}

mock.InOrder(
m.On("GetConfig").Return(config, nil),
m.On("SetConfigDefaults", &config).Return(nil),
m.On("ValidateConfig", config, 9001).Return(nil),
m.On("SetConfigDefaults", &config, mock.AnythingOfType("*runtimeconfig.runtimeConfig")).Run(func(args mock.Arguments) {
cfg := args.Get(0).(*types.InstallationConfig)
cfg.DataDirectory = tempDir
}).Return(nil),
m.On("ValidateConfig", mock.MatchedBy(func(cfg types.InstallationConfig) bool {
return cfg.AdminConsolePort == 9000 &&
cfg.GlobalCIDR == "10.0.0.1/16" &&
cfg.DataDirectory == tempDir
}), 9001).Return(nil),
)
},
expectedErr: false,
expectedValue: types.InstallationConfig{
AdminConsolePort: 9000,
GlobalCIDR: "10.0.0.1/16",
expectedValue: func(tempDir string) types.InstallationConfig {
return types.InstallationConfig{
AdminConsolePort: 9000,
GlobalCIDR: "10.0.0.1/16",
DataDirectory: tempDir,
}
},
},
{
name: "read config error",
setupMock: func(m *installation.MockInstallationManager) {
setupMock: func(m *installation.MockInstallationManager, tempDir string) {
m.On("GetConfig").Return(nil, errors.New("read error"))
},
expectedErr: true,
expectedValue: types.InstallationConfig{},
expectedErr: true,
expectedValue: func(tempDir string) types.InstallationConfig {
return types.InstallationConfig{}
},
},
{
name: "set defaults error",
setupMock: func(m *installation.MockInstallationManager) {
setupMock: func(m *installation.MockInstallationManager, tempDir string) {
config := types.InstallationConfig{}
mock.InOrder(
m.On("GetConfig").Return(config, nil),
m.On("SetConfigDefaults", &config).Return(errors.New("defaults error")),
m.On("SetConfigDefaults", &config, mock.AnythingOfType("*runtimeconfig.runtimeConfig")).Return(errors.New("defaults error")),
)
},
expectedErr: true,
expectedValue: types.InstallationConfig{},
expectedErr: true,
expectedValue: func(tempDir string) types.InstallationConfig {
return types.InstallationConfig{}
},
},
{
name: "validate error",
setupMock: func(m *installation.MockInstallationManager) {
setupMock: func(m *installation.MockInstallationManager, tempDir string) {
config := types.InstallationConfig{}

mock.InOrder(
m.On("GetConfig").Return(config, nil),
m.On("SetConfigDefaults", &config).Return(nil),
m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")),
m.On("SetConfigDefaults", &config, mock.AnythingOfType("*runtimeconfig.runtimeConfig")).Run(func(args mock.Arguments) {
cfg := args.Get(0).(*types.InstallationConfig)
cfg.DataDirectory = tempDir
}).Return(nil),
m.On("ValidateConfig", mock.MatchedBy(func(cfg types.InstallationConfig) bool {
return cfg.DataDirectory == tempDir
}), 9001).Return(errors.New("validation error")),
)
},
expectedErr: true,
expectedValue: types.InstallationConfig{},
expectedErr: true,
expectedValue: func(tempDir string) types.InstallationConfig {
return types.InstallationConfig{}
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir()
rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{}))
rc.SetDataDir(t.TempDir())
rc.SetDataDir(tempDir)
rc.SetManagerPort(9001)

mockManager := &installation.MockInstallationManager{}
tt.setupMock(mockManager)
tt.setupMock(mockManager, rc.EmbeddedClusterHomeDirectory())

controller, err := NewInstallController(
WithRuntimeConfig(rc),
Expand All @@ -133,7 +156,7 @@ func TestGetInstallationConfig(t *testing.T) {
assert.Equal(t, types.InstallationConfig{}, result)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedValue, result)
assert.Equal(t, tt.expectedValue(rc.EmbeddedClusterHomeDirectory()), result)
}

mockManager.AssertExpectations(t)
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/linux/install/installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (c *InstallController) GetInstallationConfig(ctx context.Context) (types.In
return types.InstallationConfig{}, err
}

if err := c.installationManager.SetConfigDefaults(&config); err != nil {
if err := c.installationManager.SetConfigDefaults(&config, c.rc); err != nil {
return types.InstallationConfig{}, fmt.Errorf("set defaults: %w", err)
}

Expand Down
36 changes: 24 additions & 12 deletions api/integration/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ func TestConfigureInstallation(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Create a runtime config
rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{}))
// Set the expected data directory to match the test case
if tc.config.DataDirectory != "" {
rc.SetDataDir(tc.config.DataDirectory)
}

// Create an install controller with the config manager
installController, err := linuxinstall.NewInstallController(
Expand Down Expand Up @@ -342,7 +346,7 @@ func TestConfigureInstallation(t *testing.T) {
err = json.NewDecoder(rec.Body).Decode(&status)
require.NoError(t, err)

// Verify that the status is not pending. We cannot check for an end state here because the hots config is async
// Verify that the status is not pending. We cannot check for an end state here because the host config is async
// so the state might have moved from running to a final state before we get the response.
assert.NotEqual(t, types.StatePending, status.State)
}
Expand All @@ -363,7 +367,7 @@ func TestConfigureInstallation(t *testing.T) {
// Verify that the config is in the store
storedConfig, err := installController.GetInstallationConfig(t.Context())
require.NoError(t, err)
assert.Equal(t, tc.config.DataDirectory, storedConfig.DataDirectory)
assert.Equal(t, rc.EmbeddedClusterHomeDirectory(), storedConfig.DataDirectory)
assert.Equal(t, tc.config.AdminConsolePort, storedConfig.AdminConsolePort)

// Verify that the runtime config is updated
Expand All @@ -372,7 +376,7 @@ func TestConfigureInstallation(t *testing.T) {
assert.Equal(t, tc.config.LocalArtifactMirrorPort, rc.LocalArtifactMirrorPort())
}

// Verify host confiuration was performed for successful tests
// Verify host configuration was performed for successful tests
tc.mockHostUtils.AssertExpectations(t)
tc.mockNetUtils.AssertExpectations(t)

Expand Down Expand Up @@ -534,7 +538,8 @@ func TestConfigureInstallationControllerError(t *testing.T) {
// Test the getInstall endpoint returns installation data correctly
func TestGetInstallationConfig(t *testing.T) {
rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{}))
rc.SetDataDir(t.TempDir())
tempDir := t.TempDir()
rc.SetDataDir(tempDir)

// Create a config manager
installationManager := installation.NewInstallationManager()
Expand All @@ -548,7 +553,7 @@ func TestGetInstallationConfig(t *testing.T) {

// Set some initial config
initialConfig := types.InstallationConfig{
DataDirectory: "/tmp/test-data",
DataDirectory: rc.EmbeddedClusterHomeDirectory(),
AdminConsolePort: 8080,
LocalArtifactMirrorPort: 8081,
GlobalCIDR: "10.0.0.0/16",
Expand Down Expand Up @@ -592,7 +597,7 @@ func TestGetInstallationConfig(t *testing.T) {
require.NoError(t, err)

// Verify the installation data matches what we expect
assert.Equal(t, initialConfig.DataDirectory, config.DataDirectory)
assert.Equal(t, rc.EmbeddedClusterHomeDirectory(), config.DataDirectory)
assert.Equal(t, initialConfig.AdminConsolePort, config.AdminConsolePort)
assert.Equal(t, initialConfig.LocalArtifactMirrorPort, config.LocalArtifactMirrorPort)
assert.Equal(t, initialConfig.GlobalCIDR, config.GlobalCIDR)
Expand All @@ -606,7 +611,8 @@ func TestGetInstallationConfig(t *testing.T) {
netUtils.On("DetermineBestNetworkInterface").Return("eth0", nil).Once()

rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{}))
rc.SetDataDir(t.TempDir())
defaultTempDir := t.TempDir()
rc.SetDataDir(defaultTempDir)

// Create a fresh config manager without writing anything
emptyInstallationManager := installation.NewInstallationManager(
Expand Down Expand Up @@ -653,7 +659,8 @@ func TestGetInstallationConfig(t *testing.T) {
require.NoError(t, err)

// Verify the installation data contains defaults or empty values
assert.Equal(t, "/var/lib/embedded-cluster", config.DataDirectory)
// Note: DataDirectory gets overridden with the temp directory from RuntimeConfig
assert.Equal(t, rc.EmbeddedClusterHomeDirectory(), config.DataDirectory)
assert.Equal(t, 30000, config.AdminConsolePort)
assert.Equal(t, 50000, config.LocalArtifactMirrorPort)
assert.Equal(t, "10.244.0.0/16", config.GlobalCIDR)
Expand Down Expand Up @@ -992,7 +999,8 @@ func TestInstallWithAPIClient(t *testing.T) {

// Create a runtimeconfig to be used in the install process
rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{}))
rc.SetDataDir(t.TempDir())
tempDir := t.TempDir()
rc.SetDataDir(tempDir)

// Create a mock hostutils
mockHostUtils := &hostutils.MockHostUtils{}
Expand All @@ -1012,7 +1020,7 @@ func TestInstallWithAPIClient(t *testing.T) {

// Set some initial config
initialConfig := types.InstallationConfig{
DataDirectory: "/tmp/test-data-for-client",
DataDirectory: rc.EmbeddedClusterHomeDirectory(),
AdminConsolePort: 9080,
LocalArtifactMirrorPort: 9081,
GlobalCIDR: "192.168.0.0/16",
Expand Down Expand Up @@ -1058,7 +1066,8 @@ func TestInstallWithAPIClient(t *testing.T) {
require.NoError(t, err, "GetInstallationConfig should succeed")

// Verify values
assert.Equal(t, "/tmp/test-data-for-client", config.DataDirectory)
// Note: DataDirectory gets overridden with the temp directory from RuntimeConfig
assert.Equal(t, rc.EmbeddedClusterHomeDirectory(), config.DataDirectory)
assert.Equal(t, 9080, config.AdminConsolePort)
assert.Equal(t, 9081, config.LocalArtifactMirrorPort)
assert.Equal(t, "192.168.0.0/16", config.GlobalCIDR)
Expand All @@ -1084,6 +1093,9 @@ func TestInstallWithAPIClient(t *testing.T) {
NetworkInterface: "eth0",
}

// Update runtime config to match expected data directory for this test
rc.SetDataDir(config.DataDirectory)

// Configure the installation using the client
_, err = c.ConfigureInstallation(config)
require.NoError(t, err, "ConfigureInstallation should succeed with valid config")
Expand All @@ -1102,7 +1114,7 @@ func TestInstallWithAPIClient(t *testing.T) {
// Get the config to verify it persisted
newConfig, err := c.GetInstallationConfig()
require.NoError(t, err, "GetInstallationConfig should succeed after setting config")
assert.Equal(t, config.DataDirectory, newConfig.DataDirectory)
assert.Equal(t, rc.EmbeddedClusterHomeDirectory(), newConfig.DataDirectory)
assert.Equal(t, config.AdminConsolePort, newConfig.AdminConsolePort)
assert.Equal(t, config.NetworkInterface, newConfig.NetworkInterface)

Expand Down
4 changes: 2 additions & 2 deletions api/internal/managers/installation/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,13 @@ func (m *installationManager) validateDataDirectory(config types.InstallationCon
}

// SetConfigDefaults sets default values for the installation configuration
func (m *installationManager) SetConfigDefaults(config *types.InstallationConfig) error {
func (m *installationManager) SetConfigDefaults(config *types.InstallationConfig, rc runtimeconfig.RuntimeConfig) error {
if config.AdminConsolePort == 0 {
config.AdminConsolePort = ecv1beta1.DefaultAdminConsolePort
}

if config.DataDirectory == "" {
config.DataDirectory = ecv1beta1.DefaultDataDir
config.DataDirectory = rc.EmbeddedClusterHomeDirectory()
}

if config.LocalArtifactMirrorPort == 0 {
Expand Down
Loading