diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ef3bf57cb7..789184ea2c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -761,24 +761,6 @@ jobs: export RELEASE_YAML_DIR=e2e/kots-release-install ./scripts/ci-release-app.sh - # then install a version with alternate unsupported overrides - export EC_VERSION="${{ needs.output-vars.outputs.ec_version }}" - export APP_VERSION="appver-${SHORT_SHA}-unsupported-overrides" - export RELEASE_YAML_DIR=e2e/kots-release-unsupported-overrides - ./scripts/ci-release-app.sh - - # then install a version with additional failing host preflights - export EC_VERSION="${{ needs.output-vars.outputs.ec_version }}" - export APP_VERSION="appver-${SHORT_SHA}-failing-preflights" - export RELEASE_YAML_DIR=e2e/kots-release-install-failing-preflights - ./scripts/ci-release-app.sh - - # then install a version with additional warning host preflights - export EC_VERSION="${{ needs.output-vars.outputs.ec_version }}" - export APP_VERSION="appver-${SHORT_SHA}-warning-preflights" - export RELEASE_YAML_DIR=e2e/kots-release-install-warning-preflights - ./scripts/ci-release-app.sh - # promote a release with improved dr support export EC_VERSION="${{ needs.output-vars.outputs.ec_version }}" export APP_VERSION="appver-${SHORT_SHA}-legacydr" @@ -892,19 +874,9 @@ jobs: test: - TestPreflights - TestPreflightsNoexec - - TestMaterialize - - TestHostPreflightCustomSpec - - TestHostPreflightInBuiltSpec - TestSingleNodeInstallation - - TestSingleNodeInstallationAlmaLinux8 - - TestSingleNodeInstallationDebian11 - - TestSingleNodeInstallationDebian12 - - TestSingleNodeInstallationCentos9Stream - TestSingleNodeUpgradePreviousStable - - TestInstallFromReplicatedApp - TestUpgradeFromReplicatedAppPreviousK0s - - TestResetAndReinstall - - TestInstallSnapshotFromReplicatedApp - TestMultiNodeInstallation - TestMultiNodeHAInstallation - TestSingleNodeDisasterRecovery @@ -912,11 +884,7 @@ jobs: - TestSingleNodeResumeDisasterRecovery - TestMultiNodeHADisasterRecovery - TestSingleNodeInstallationNoopUpgrade - - TestCustomCIDR - - TestLocalArtifactMirror - - TestMultiNodeReset - TestCollectSupportBundle - - TestUnsupportedOverrides - TestHostCollectSupportBundleInCluster - TestInstallWithConfigValues steps: @@ -982,13 +950,9 @@ jobs: fail-fast: false matrix: test: - - TestResetAndReinstallAirgap - TestSingleNodeAirgapUpgrade - TestSingleNodeAirgapUpgradeSelinux - TestSingleNodeAirgapUpgradeConfigValues - - TestSingleNodeAirgapUpgradeCustomCIDR - - TestMultiNodeAirgapUpgrade - - TestMultiNodeAirgapUpgradeSameK0s - TestMultiNodeAirgapUpgradePreviousStable - TestMultiNodeAirgapHAInstallation - TestSingleNodeAirgapDisasterRecovery diff --git a/.github/workflows/release-prod.yaml b/.github/workflows/release-prod.yaml index f58ff0426d..dd3db380bf 100644 --- a/.github/workflows/release-prod.yaml +++ b/.github/workflows/release-prod.yaml @@ -341,24 +341,6 @@ jobs: export APP_VERSION="appver-${{ github.ref_name }}" export RELEASE_YAML_DIR=e2e/kots-release-install ./scripts/ci-release-app.sh - - # then install a version with alternate unsupported overrides - export EC_VERSION="${{ github.ref_name }}" - export APP_VERSION="appver-${{ github.ref_name }}-unsupported-overrides" - export RELEASE_YAML_DIR=e2e/kots-release-unsupported-overrides - ./scripts/ci-release-app.sh - - # then install a version with additional failing host preflights - export EC_VERSION="${{ github.ref_name }}" - export APP_VERSION="appver-${{ github.ref_name }}-failing-preflights" - export RELEASE_YAML_DIR=e2e/kots-release-install-failing-preflights - ./scripts/ci-release-app.sh - - # then install a version with additional warning host preflights - export EC_VERSION="${{ github.ref_name }}" - export APP_VERSION="appver-${{ github.ref_name }}-warning-preflights" - export RELEASE_YAML_DIR=e2e/kots-release-install-warning-preflights - ./scripts/ci-release-app.sh # then a noop upgrade export EC_VERSION="${{ github.ref_name }}" @@ -508,30 +490,15 @@ jobs: test: - TestPreflights - TestPreflightsNoexec - - TestMaterialize - - TestHostPreflightCustomSpec - - TestHostPreflightInBuiltSpec - TestSingleNodeInstallation - - TestSingleNodeInstallationAlmaLinux8 - - TestSingleNodeInstallationDebian11 - - TestSingleNodeInstallationDebian12 - - TestSingleNodeInstallationCentos9Stream - TestSingleNodeUpgradePreviousStable - - TestInstallFromReplicatedApp - - TestUpgradeFromReplicatedApp - - TestResetAndReinstall - - TestInstallSnapshotFromReplicatedApp - TestMultiNodeInstallation - TestMultiNodeHAInstallation - TestSingleNodeDisasterRecovery - TestSingleNodeResumeDisasterRecovery - TestMultiNodeHADisasterRecovery - TestSingleNodeInstallationNoopUpgrade - - TestCustomCIDR - - TestLocalArtifactMirror - - TestMultiNodeReset - TestCollectSupportBundle - - TestUnsupportedOverrides - TestHostCollectSupportBundleInCluster - TestInstallWithConfigValues steps: @@ -597,13 +564,9 @@ jobs: fail-fast: false matrix: test: - - TestResetAndReinstallAirgap - TestSingleNodeAirgapUpgrade - TestSingleNodeAirgapUpgradeSelinux - TestSingleNodeAirgapUpgradeConfigValues - - TestSingleNodeAirgapUpgradeCustomCIDR - - TestMultiNodeAirgapUpgrade - - TestMultiNodeAirgapUpgradeSameK0s - TestMultiNodeAirgapUpgradePreviousStable - TestMultiNodeAirgapHAInstallation - TestSingleNodeAirgapDisasterRecovery diff --git a/api/controllers/app/controller.go b/api/controllers/app/controller.go index b73544151b..7ce12f289b 100644 --- a/api/controllers/app/controller.go +++ b/api/controllers/app/controller.go @@ -210,6 +210,17 @@ func NewAppController(opts ...AppControllerOption) (*AppController, error) { if err != nil { return nil, fmt.Errorf("patch app config values: %w", err) } + // Warm the template engine cache with the existing values + _, err = controller.appConfigManager.TemplateConfig(controller.configValues, false, false) + if err != nil { + return nil, fmt.Errorf("template config: %w", err) + } + } else { + // Warm the template engine cache with empty values + _, err := controller.appConfigManager.TemplateConfig(make(types.AppConfigValues), false, false) + if err != nil { + return nil, fmt.Errorf("template config: %w", err) + } } if controller.appPreflightManager == nil { diff --git a/api/controllers/app/tests/test_suite.go b/api/controllers/app/tests/test_suite.go index f7acd2cb90..8d4a767f16 100644 --- a/api/controllers/app/tests/test_suite.go +++ b/api/controllers/app/tests/test_suite.go @@ -149,12 +149,14 @@ func (s *AppControllerTestSuite) TestPatchAppConfigValues() { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - appConfigManager := &appconfig.MockAppConfigManager{} appPreflightManager := &apppreflightmanager.MockAppPreflightManager{} appReleaseManager := &appreleasemanager.MockAppReleaseManager{} appInstallManager := &appinstallmanager.MockAppInstallManager{} sm := scenario.createSM(tt.currentState) + appConfigManager := &appconfig.MockAppConfigManager{} + appConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + controller, err := appcontroller.NewAppController( appcontroller.WithStateMachine(sm), appcontroller.WithAppConfigManager(appConfigManager), @@ -426,10 +428,13 @@ func (s *AppControllerTestSuite) TestRunAppPreflights() { for _, tt := range tests { s.T().Run(tt.name, func(t *testing.T) { - appConfigManager := &appconfig.MockAppConfigManager{} appPreflightManager := &apppreflightmanager.MockAppPreflightManager{} appReleaseManager := &appreleasemanager.MockAppReleaseManager{} sm := s.CreateInstallStateMachine(tt.currentState) + + appConfigManager := &appconfig.MockAppConfigManager{} + appConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + controller, err := appcontroller.NewAppController( appcontroller.WithStateMachine(sm), appcontroller.WithAppConfigManager(appConfigManager), @@ -499,12 +504,14 @@ func (s *AppControllerTestSuite) TestGetAppInstallStatus() { for _, tt := range tests { s.T().Run(tt.name, func(t *testing.T) { - appConfigManager := &appconfig.MockAppConfigManager{} appPreflightManager := &apppreflightmanager.MockAppPreflightManager{} appReleaseManager := &appreleasemanager.MockAppReleaseManager{} appInstallManager := &appinstallmanager.MockAppInstallManager{} sm := s.CreateInstallStateMachine(states.StateNew) + appConfigManager := &appconfig.MockAppConfigManager{} + appConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + controller, err := appcontroller.NewAppController( appcontroller.WithStateMachine(sm), appcontroller.WithAppConfigManager(appConfigManager), @@ -527,6 +534,7 @@ func (s *AppControllerTestSuite) TestGetAppInstallStatus() { assert.Equal(t, expectedAppInstall, result) } + appConfigManager.AssertExpectations(s.T()) appInstallManager.AssertExpectations(s.T()) }) } @@ -681,12 +689,14 @@ func (s *AppControllerTestSuite) TestInstallApp() { for _, tt := range tests { s.T().Run(tt.name, func(t *testing.T) { - appConfigManager := &appconfig.MockAppConfigManager{} appPreflightManager := &apppreflightmanager.MockAppPreflightManager{} appReleaseManager := &appreleasemanager.MockAppReleaseManager{} appInstallManager := &appinstallmanager.MockAppInstallManager{} sm := s.CreateInstallStateMachine(tt.currentState) + appConfigManager := &appconfig.MockAppConfigManager{} + appConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + controller, err := appcontroller.NewAppController( appcontroller.WithStateMachine(sm), appcontroller.WithAppConfigManager(appConfigManager), @@ -820,13 +830,15 @@ func (s *AppControllerTestSuite) TestUpgradeApp() { for _, tt := range tests { s.T().Run(tt.name, func(t *testing.T) { - appConfigManager := &appconfig.MockAppConfigManager{} appPreflightManager := &apppreflightmanager.MockAppPreflightManager{} appReleaseManager := &appreleasemanager.MockAppReleaseManager{} appInstallManager := &appinstallmanager.MockAppInstallManager{} appUpgradeManager := &appupgrademanager.MockAppUpgradeManager{} sm := s.CreateUpgradeStateMachine(tt.currentState) + appConfigManager := &appconfig.MockAppConfigManager{} + appConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + controller, err := appcontroller.NewAppController( appcontroller.WithStateMachine(sm), appcontroller.WithAppConfigManager(appConfigManager), @@ -896,13 +908,15 @@ func (s *AppControllerTestSuite) TestGetAppUpgradeStatus() { for _, tt := range tests { s.T().Run(tt.name, func(t *testing.T) { - appConfigManager := &appconfig.MockAppConfigManager{} appPreflightManager := &apppreflightmanager.MockAppPreflightManager{} appReleaseManager := &appreleasemanager.MockAppReleaseManager{} appInstallManager := &appinstallmanager.MockAppInstallManager{} appUpgradeManager := &appupgrademanager.MockAppUpgradeManager{} sm := s.CreateUpgradeStateMachine(states.StateNew) + appConfigManager := &appconfig.MockAppConfigManager{} + appConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + controller, err := appcontroller.NewAppController( appcontroller.WithStateMachine(sm), appcontroller.WithAppConfigManager(appConfigManager), diff --git a/api/controllers/kubernetes/install/controller_test.go b/api/controllers/kubernetes/install/controller_test.go index 6d5a4839ff..763176065e 100644 --- a/api/controllers/kubernetes/install/controller_test.go +++ b/api/controllers/kubernetes/install/controller_test.go @@ -219,7 +219,9 @@ func TestConfigureInstallation(t *testing.T) { mockManager := &installation.MockInstallationManager{} mockMetricsReporter := &metrics.MockReporter{} + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) tt.setupMock(mockManager, mockInstallation, tt.config, mockStore, mockMetricsReporter) @@ -421,7 +423,10 @@ func TestSetupInfra(t *testing.T) { mockInfraManager := &infra.MockInfraManager{} mockMetricsReporter := &metrics.MockReporter{} mockStore := &store.MockStore{} + mockAppConfigManager := &appconfig.MockAppConfigManager{} + mockAppConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + tt.setupMocks(ki, mockInstallationManager, mockInfraManager, mockMetricsReporter, mockStore, mockAppConfigManager) appController, err := appcontroller.NewAppController( diff --git a/api/controllers/linux/install/controller_test.go b/api/controllers/linux/install/controller_test.go index 8d3db7ba2f..e6de4ee6c0 100644 --- a/api/controllers/linux/install/controller_test.go +++ b/api/controllers/linux/install/controller_test.go @@ -399,7 +399,9 @@ func TestConfigureInstallation(t *testing.T) { mockManager := &installation.MockInstallationManager{} mockMetricsReporter := &metrics.MockReporter{} + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) tt.setupMock(mockManager, rc, tt.config, mockStore, mockMetricsReporter) @@ -732,6 +734,9 @@ func TestRunHostPreflights(t *testing.T) { tt.setupMocks(mockPreflightManager, rc, mockMetricsReporter, mockStore) + // Mock GetConfigValues call that happens during controller initialization + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + controller, err := NewInstallController( WithRuntimeConfig(rc), WithStateMachine(sm), @@ -1152,10 +1157,12 @@ func TestSetupInfra(t *testing.T) { mockInfraManager := &infra.MockInfraManager{} mockMetricsReporter := &metrics.MockReporter{} mockStore := &store.MockStore{} - mockAppConfigManager := &appconfig.MockAppConfigManager{} mockAppPreflightManager := &apppreflightmanager.MockAppPreflightManager{} mockAppReleaseManager := &appreleasemanager.MockAppReleaseManager{} + mockAppConfigManager := &appconfig.MockAppConfigManager{} + mockAppConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) + tt.setupMocks(rc, mockPreflightManager, mockInstallationManager, mockInfraManager, mockAppConfigManager, mockMetricsReporter, mockStore) appController, err := appcontroller.NewAppController( @@ -1397,6 +1404,9 @@ func TestProcessAirgap(t *testing.T) { tt.setupMocks(mockAirgapManager, mockInstallationManager, expectedRegistrySettings, rc) + // Mock GetConfigValues call that happens during controller initialization + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + controller, err := NewInstallController( WithRuntimeConfig(rc), WithStateMachine(sm), diff --git a/api/controllers/linux/upgrade/controller_test.go b/api/controllers/linux/upgrade/controller_test.go index bb72fdd270..e5fa0043f8 100644 --- a/api/controllers/linux/upgrade/controller_test.go +++ b/api/controllers/linux/upgrade/controller_test.go @@ -81,9 +81,11 @@ func TestUpgradeInfra(t *testing.T) { rc.SetManagerPort(9001) mockInfraManager := &infra.MockInfraManager{} - mockStore := &store.MockStore{} mockInstallationManager := &installation.MockInstallationManager{} + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + tt.setupMocks(rc, mockInfraManager, mockInstallationManager) sm := NewStateMachine( @@ -187,7 +189,9 @@ func TestGetInfra(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockInfraManager := &infra.MockInfraManager{} + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) tt.setupMock(mockInfraManager) @@ -321,9 +325,11 @@ func TestReportingHandlers(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockMetricsReporter := &metrics.MockReporter{} - mockStore := &store.MockStore{} mockInfraManager := &infra.MockInfraManager{} + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + tt.setupMocks(mockMetricsReporter, mockStore) // Mock RequiresUpgrade which is called during controller initialization @@ -452,6 +458,8 @@ func TestProcessAirgap(t *testing.T) { tt.setupMocks(mockAirgapManager, mockInstallationManager, expectedRegistrySettings, rc) + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + controller, err := NewUpgradeController( WithRuntimeConfig(rc), WithStateMachine(sm), diff --git a/api/integration/app/upgrade/apppreflight_test.go b/api/integration/app/upgrade/apppreflight_test.go index cf6667b3c4..80fbbbc4cc 100644 --- a/api/integration/app/upgrade/apppreflight_test.go +++ b/api/integration/app/upgrade/apppreflight_test.go @@ -342,10 +342,14 @@ func (s *AppPreflightTestSuite) TestPostRunAppPreflights() { // Create state machine with wrong state stateMachine := s.createStateMachine(states.StateNew) + // Create mock store with GetConfigValues expectation + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + // Create simple app controller appController, err := appcontroller.NewAppController( appcontroller.WithStateMachine(stateMachine), - appcontroller.WithStore(&store.MockStore{}), + appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), appcontroller.WithHelmClient(&helm.MockClient{}), ) @@ -380,10 +384,14 @@ func (s *AppPreflightTestSuite) TestPostRunAppPreflights() { // Create state machine stateMachine := s.createStateMachine(states.StateApplicationConfigured) + // Create mock store with GetConfigValues expectation + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + // Create simple app controller appController, err := appcontroller.NewAppController( appcontroller.WithStateMachine(stateMachine), - appcontroller.WithStore(&store.MockStore{}), + appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), appcontroller.WithHelmClient(&helm.MockClient{}), ) diff --git a/api/integration/app/upgrade/upgrade_test.go b/api/integration/app/upgrade/upgrade_test.go index b82f9c24f8..18d0857abe 100644 --- a/api/integration/app/upgrade/upgrade_test.go +++ b/api/integration/app/upgrade/upgrade_test.go @@ -61,6 +61,7 @@ func (s *AppUpgradeTestSuite) TestPostUpgradeApp() { // Create mock store that will be shared between AppController and UpgradeController mockStore := &store.MockStore{} + mockAppConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) // Create state machine that will be shared between AppController and UpgradeController stateMachine := s.createStateMachine(states.StateAppPreflightsSucceeded) @@ -124,10 +125,14 @@ func (s *AppUpgradeTestSuite) TestPostUpgradeApp() { // Create state machine stateMachine := s.createStateMachine(states.StateAppPreflightsSucceeded) + // Create mock store + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + // Create simple app controller for auth test appController, err := appcontroller.NewAppController( appcontroller.WithStateMachine(stateMachine), - appcontroller.WithStore(&store.MockStore{}), + appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), appcontroller.WithHelmClient(&helm.MockClient{}), ) @@ -171,6 +176,7 @@ func (s *AppUpgradeTestSuite) TestPostUpgradeApp() { // Create mock app config manager mockAppConfigManager := &appconfig.MockAppConfigManager{} mockAppConfigManager.On("GetKotsadmConfigValues").Return(kotsv1beta1.ConfigValues{}, nil) + mockAppConfigManager.On("TemplateConfig", types.AppConfigValues{}, false, false).Return(types.AppConfig{}, nil) // Create mock app upgrade manager that fails mockAppUpgradeManager := &appupgrademanager.MockAppUpgradeManager{} @@ -265,11 +271,15 @@ func (s *AppUpgradeTestSuite) TestGetAppUpgradeStatus() { stateMachine := s.createStateMachine(states.StateAppPreflightsSucceeded) + // Create mock store + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + // Create app controller with real upgrade manager appController, err := appcontroller.NewAppController( appcontroller.WithAppUpgradeManager(appUpgradeManager), appcontroller.WithStateMachine(stateMachine), - appcontroller.WithStore(&store.MockStore{}), + appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), appcontroller.WithHelmClient(&helm.MockClient{}), ) @@ -309,9 +319,14 @@ func (s *AppUpgradeTestSuite) TestGetAppUpgradeStatus() { stateMachine := s.createStateMachine(states.StateAppPreflightsSucceeded) // Create simple app controller for auth test + + // Create mock store + mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + appController, err := appcontroller.NewAppController( appcontroller.WithStateMachine(stateMachine), - appcontroller.WithStore(&store.MockStore{}), + appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), appcontroller.WithHelmClient(&helm.MockClient{}), ) diff --git a/api/integration/kubernetes/install/appinstall_test.go b/api/integration/kubernetes/install/appinstall_test.go index 2f4136eb97..f2f4a0d9bd 100644 --- a/api/integration/kubernetes/install/appinstall_test.go +++ b/api/integration/kubernetes/install/appinstall_test.go @@ -53,6 +53,7 @@ func TestGetAppInstallStatus(t *testing.T) { // Create mock store mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) // Create real app install controller appController, err := appcontroller.NewAppController( @@ -269,6 +270,8 @@ func TestPostInstallApp(t *testing.T) { // Create simple app install controller mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + appController, err := appcontroller.NewAppController( appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), @@ -506,6 +509,7 @@ func TestPostInstallApp(t *testing.T) { t.Run("App preflight bypass denied with failed preflights", func(t *testing.T) { // Create mock store mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) // Create mock app preflight manager that returns non-strict failures (method should be called but bypass denied) mockAppPreflightManager := &apppreflightmanager.MockAppPreflightManager{} @@ -586,6 +590,7 @@ func TestPostInstallApp(t *testing.T) { t.Run("Strict app preflight bypass blocked", func(t *testing.T) { // Create mock store mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) // Create mock app preflight manager that returns strict failures (cannot be bypassed) mockAppPreflightManager := &apppreflightmanager.MockAppPreflightManager{} diff --git a/api/integration/linux/install/appinstall_test.go b/api/integration/linux/install/appinstall_test.go index 3fd8cc6347..79165312e8 100644 --- a/api/integration/linux/install/appinstall_test.go +++ b/api/integration/linux/install/appinstall_test.go @@ -55,6 +55,7 @@ func TestGetAppInstallStatus(t *testing.T) { // Create mock store mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) // Create real app install controller appController, err := appcontroller.NewAppController( @@ -286,6 +287,8 @@ func TestPostInstallApp(t *testing.T) { // Create simple app install controller mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) + appController, err := appcontroller.NewAppController( appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), @@ -546,6 +549,7 @@ func TestPostInstallApp(t *testing.T) { t.Run("App preflight bypass denied with failed preflights", func(t *testing.T) { // Create mock store mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) // Create mock app preflight manager that returns non-strict failures (method should be called but bypass denied) mockAppPreflightManager := &apppreflightmanager.MockAppPreflightManager{} @@ -626,6 +630,7 @@ func TestPostInstallApp(t *testing.T) { t.Run("Strict app preflight bypass blocked", func(t *testing.T) { // Create mock store mockStore := &store.MockStore{} + mockStore.AppConfigMockStore.On("GetConfigValues").Return(types.AppConfigValues{}, nil) // Create mock app preflight manager that returns strict failures (cannot be bypassed) mockAppPreflightManager := &apppreflightmanager.MockAppPreflightManager{} diff --git a/api/integration/linux/install/infra_test.go b/api/integration/linux/install/infra_test.go index f33a5c20d7..50a02942db 100644 --- a/api/integration/linux/install/infra_test.go +++ b/api/integration/linux/install/infra_test.go @@ -171,7 +171,8 @@ func TestLinuxPostSetupInfra(t *testing.T) { } mock.InOrder( k0sMock.On("IsInstalled").Return(false, nil), - k0sMock.On("WriteK0sConfig", mock.Anything, "eth0", "", "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("NewK0sConfig", "eth0", false, "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("WriteK0sConfig", mock.Anything, k0sConfig).Return(nil), hostutilsMock.On("CreateSystemdUnitFiles", mock.Anything, mock.Anything, rc, hostname, false).Return(nil), k0sMock.On("Install", rc, hostname).Return(nil), k0sMock.On("WaitForK0s").Return(nil), @@ -704,7 +705,8 @@ func TestLinuxPostSetupInfra(t *testing.T) { k0sConfig := &k0sv1beta1.ClusterConfig{} mock.InOrder( k0sMock.On("IsInstalled").Return(false, nil), - k0sMock.On("WriteK0sConfig", mock.Anything, "eth0", "", "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("NewK0sConfig", "eth0", false, "10.244.0.0/16", "10.96.0.0/12", mock.Anything, mock.Anything).Return(k0sConfig, nil), + k0sMock.On("WriteK0sConfig", mock.Anything, k0sConfig).Return(nil), hostutilsMock.On("CreateSystemdUnitFiles", mock.Anything, mock.Anything, rc, hostname, false).Return(nil), k0sMock.On("Install", rc, hostname).Return(errors.New("failed to install k0s")), ) diff --git a/api/internal/managers/airgap/util.go b/api/internal/managers/airgap/util.go index 9676c5c549..e9348ccbe5 100644 --- a/api/internal/managers/airgap/util.go +++ b/api/internal/managers/airgap/util.go @@ -6,7 +6,7 @@ import ( "strings" ) -func (m *airgapManager) addLogs(format string, v ...interface{}) { +func (m *airgapManager) addLogs(format string, v ...any) { msg := fmt.Sprintf(format, v...) if err := m.airgapStore.AddLogs(msg); err != nil { m.logger.WithError(err).Error("add log") @@ -28,6 +28,7 @@ func (lw *logWriter) Write(p []byte) (n int, err error) { output := strings.TrimSpace(string(p)) if output != "" { lw.manager.addLogs("%s", output) + lw.manager.logger.WithField("component", "kots").Debug(output) } return len(p), nil } diff --git a/api/internal/managers/app/config/manager_mock.go b/api/internal/managers/app/config/manager_mock.go index 3dd813add6..7f62a058da 100644 --- a/api/internal/managers/app/config/manager_mock.go +++ b/api/internal/managers/app/config/manager_mock.go @@ -27,7 +27,7 @@ func (m *MockAppConfigManager) PatchConfigValues(values types.AppConfigValues) e // TemplateConfig mocks the TemplateConfig method func (m *MockAppConfigManager) TemplateConfig(configValues types.AppConfigValues, maskPasswords bool, filterHiddenItems bool) (types.AppConfig, error) { - args := m.Called(configValues, maskPasswords) + args := m.Called(configValues, maskPasswords, filterHiddenItems) return args.Get(0).(types.AppConfig), args.Error(1) } diff --git a/api/internal/managers/app/install/status.go b/api/internal/managers/app/install/status.go index 55f9da21e9..5a9fda1b5d 100644 --- a/api/internal/managers/app/install/status.go +++ b/api/internal/managers/app/install/status.go @@ -19,7 +19,7 @@ func (m *appInstallManager) setStatus(state types.State, description string) err }) } -func (m *appInstallManager) addLogs(format string, v ...interface{}) { +func (m *appInstallManager) addLogs(format string, v ...any) { 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 7c93faa312..086e49d90a 100644 --- a/api/internal/managers/app/install/util.go +++ b/api/internal/managers/app/install/util.go @@ -22,6 +22,7 @@ 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.logger.WithField("component", "kots").Debug(output) } return len(p), nil } diff --git a/api/internal/managers/app/upgrade/util.go b/api/internal/managers/app/upgrade/util.go index cd9ee129c7..3936b4889f 100644 --- a/api/internal/managers/app/upgrade/util.go +++ b/api/internal/managers/app/upgrade/util.go @@ -28,16 +28,17 @@ func (lw *logWriter) Write(p []byte) (n int, err error) { } // log logs a message to the structured logger and adds it to the logs store -func (m *appUpgradeManager) log(fields interface{}, format string, v ...interface{}) { +func (m *appUpgradeManager) log(fields any, format string, v ...any) { + logger := m.logger.WithField("component", "kots") if fields != nil { f, err := json.Marshal(fields) if err == nil { - m.logger.WithField("fields", string(f)).Debugf(format, v...) + logger.WithField("fields", string(f)).Debugf(format, v...) } else { - m.logger.Debugf(format, v...) + logger.Debugf(format, v...) } } else { - m.logger.Debugf(format, v...) + logger.Debugf(format, v...) } m.addLogs(format, v...) } diff --git a/api/internal/managers/linux/infra/install.go b/api/internal/managers/linux/infra/install.go index 3d8f34379c..5d7bd3aced 100644 --- a/api/internal/managers/linux/infra/install.go +++ b/api/internal/managers/linux/infra/install.go @@ -162,8 +162,11 @@ func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeC logFn := m.logFn("k0s") logFn("creating k0s configuration file") - k0sCfg, err = m.k0scli.WriteK0sConfig(ctx, rc.NetworkInterface(), m.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) + k0sCfg, err = m.k0scli.NewK0sConfig(rc.NetworkInterface(), m.airgapBundle != "", rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) if err != nil { + return nil, fmt.Errorf("new k0s config: %w", err) + } + if err := m.k0scli.WriteK0sConfig(ctx, k0sCfg); err != nil { return nil, fmt.Errorf("create config file: %w", err) } diff --git a/api/internal/managers/linux/infra/util.go b/api/internal/managers/linux/infra/util.go index bcc78d30e7..1422a162f8 100644 --- a/api/internal/managers/linux/infra/util.go +++ b/api/internal/managers/linux/infra/util.go @@ -3,8 +3,6 @@ package infra import ( "context" "fmt" - "io" - "strings" "github.com/replicatedhq/embedded-cluster/api/internal/clients" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" @@ -15,27 +13,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// logWriter is an io.Writer that captures output and feeds it to the logs -type logWriter struct { - manager *infraManager - component string -} - -func (m *infraManager) newLogWriter(component string) io.Writer { - return &logWriter{ - manager: m, - component: component, - } -} - -func (lw *logWriter) Write(p []byte) (n int, err error) { - output := strings.TrimSpace(string(p)) - if output != "" { - lw.manager.addLogs(lw.component, "%s", output) - } - return len(p), nil -} - func (m *infraManager) waitForNode(ctx context.Context, kcli client.Client) error { nodename, err := nodeutil.GetHostname("") if err != nil { diff --git a/api/pkg/logger/logger.go b/api/pkg/logger/logger.go index b93990aabb..f41bbe55a3 100644 --- a/api/pkg/logger/logger.go +++ b/api/pkg/logger/logger.go @@ -20,6 +20,8 @@ func NewLogger() (*logrus.Logger, error) { } logger := logrus.New() + // Set to debug by default to capture all the manager and execution logs + logger.SetLevel(logrus.DebugLevel) logger.SetOutput(logfile) logger.Infof("versions: embedded-cluster=%s, k0s=%s", versions.Version, versions.K0sVersion) diff --git a/api/pkg/logger/middleware.go b/api/pkg/logger/middleware.go new file mode 100644 index 0000000000..88b441c6cb --- /dev/null +++ b/api/pkg/logger/middleware.go @@ -0,0 +1,77 @@ +package logger + +import ( + "net/http" + "time" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" +) + +// responseWriter wraps http.ResponseWriter to capture status code and response size +type responseWriter struct { + http.ResponseWriter + statusCode int + written int64 +} + +// WriteHeader captures the status code before writing it +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +// Write captures the number of bytes written +func (rw *responseWriter) Write(b []byte) (int, error) { + n, err := rw.ResponseWriter.Write(b) + rw.written += int64(n) + return n, err +} + +// HTTPLoggingMiddleware creates a middleware that logs HTTP requests with structured fields. +// It logs after the request is handled, capturing the response status code and duration. +func HTTPLoggingMiddleware(logger logrus.FieldLogger) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Wrap the response writer to capture status code and bytes written + wrapped := &responseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, // default if WriteHeader is not called + } + + // Call the next handler + next.ServeHTTP(wrapped, r) + + // Log after the request is handled + duration := time.Since(start) + + // Build structured log fields + fields := logrus.Fields{ + "component": "request-logger", + "method": r.Method, + "path": r.URL.Path, + "status": wrapped.statusCode, + "duration_ms": duration.Milliseconds(), + "response_bytes": wrapped.written, + } + + // Add query parameters if present + if r.URL.RawQuery != "" { + fields["query"] = r.URL.RawQuery + } + + // Determine log level based on status code + entry := logger.WithFields(fields) + switch { + case wrapped.statusCode >= 500: + entry.Error("HTTP request") + case wrapped.statusCode >= 400: + entry.Warn("HTTP request") + default: + entry.Info("HTTP request") + } + }) + } +} diff --git a/api/routes.go b/api/routes.go index 62d1410441..b0e7d43429 100644 --- a/api/routes.go +++ b/api/routes.go @@ -5,6 +5,7 @@ import ( "github.com/gorilla/mux" "github.com/replicatedhq/embedded-cluster/api/docs" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" httpSwagger "github.com/swaggo/http-swagger/v2" ) @@ -23,9 +24,13 @@ func (a *API) RegisterRoutes(router *mux.Router) { }).Methods("GET") router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) - router.HandleFunc("/auth/login", a.handlers.auth.PostLogin).Methods("POST") + // Routes with logging middleware + routerWithLogging := router.PathPrefix("/").Subrouter() + routerWithLogging.Use(logger.HTTPLoggingMiddleware(a.logger)) - authenticatedRouter := router.PathPrefix("/").Subrouter() + routerWithLogging.HandleFunc("/auth/login", a.handlers.auth.PostLogin).Methods("POST") + + authenticatedRouter := routerWithLogging.PathPrefix("/").Subrouter() authenticatedRouter.Use(a.handlers.auth.Middleware) if a.cfg.InstallTarget == types.InstallTargetLinux { diff --git a/cmd/installer/cli/cidr_test.go b/cmd/installer/cli/cidr_test.go deleted file mode 100644 index 2cc36e67fa..0000000000 --- a/cmd/installer/cli/cidr_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package cli - -import ( - "testing" - - "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" - newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/stretchr/testify/require" - "k8s.io/utils/ptr" -) - -func Test_getCIDRConfig(t *testing.T) { - tests := []struct { - name string - setFlags func(flagSet *pflag.FlagSet) - expected *newconfig.CIDRConfig - }{ - { - name: "with pod and service flags", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: "10.1.0.0/24", - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - flagSet.Set("service-cidr", "10.1.0.0/24") - }, - }, - { - name: "with pod flag", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: v1beta1.DefaultNetwork().ServiceCIDR, - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - }, - }, - { - name: "with pod, service and cidr flags", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: "10.1.0.0/24", - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - flagSet.Set("service-cidr", "10.1.0.0/24") - flagSet.Set("cidr", "10.2.0.0/24") - }, - }, - { - name: "with pod and cidr flags", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.0.0.0/24", - ServiceCIDR: v1beta1.DefaultNetwork().ServiceCIDR, - GlobalCIDR: nil, - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("pod-cidr", "10.0.0.0/24") - flagSet.Set("cidr", "10.2.0.0/24") - }, - }, - { - name: "with cidr flag", - expected: &newconfig.CIDRConfig{ - PodCIDR: "10.2.0.0/25", - ServiceCIDR: "10.2.0.128/25", - GlobalCIDR: ptr.To("10.2.0.0/24"), - }, - setFlags: func(flagSet *pflag.FlagSet) { - flagSet.Set("cidr", "10.2.0.0/24") - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - req := require.New(t) - - cmd := &cobra.Command{} - mustAddCIDRFlags(cmd.Flags()) - - test.setFlags(cmd.Flags()) - - got, err := getCIDRConfig(cmd) - req.NoError(err) - req.Equal(test.expected, got) - }) - } -} diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 5f866144ea..e09f728c3c 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -8,16 +8,13 @@ import ( "io/fs" "os" "path/filepath" - "slices" "strings" "syscall" "time" "github.com/AlecAivazis/survey/v2/terminal" - "github.com/google/uuid" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" apitypes "github.com/replicatedhq/embedded-cluster/api/types" - "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/cloudutils" @@ -28,12 +25,10 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" ecmetadata "github.com/replicatedhq/embedded-cluster/pkg-new/metadata" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" - "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" addontypes "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/airgap" - "github.com/replicatedhq/embedded-cluster/pkg/configutils" "github.com/replicatedhq/embedded-cluster/pkg/extensions" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" @@ -59,13 +54,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -type InstallCmdFlags struct { +type installFlags struct { adminConsolePassword string adminConsolePort int airgapBundle string - airgapMetadata *airgap.AirgapMetadata - embeddedAssetsSize int64 - isAirgap bool licenseFile string assumeYes bool overrides string @@ -78,28 +70,33 @@ type InstallCmdFlags struct { ignoreHostPreflights bool ignoreAppPreflights bool networkInterface string + cidrConfig *newconfig.CIDRConfig + proxySpec *ecv1beta1.ProxySpec // kubernetes flags kubernetesEnvSettings *helmcli.EnvSettings // guided UI flags - enableManagerExperience bool - target string - managerPort int - tlsCertFile string - tlsKeyFile string - hostname string - - installConfig + target string + managerPort int + tlsCertFile string + tlsKeyFile string + hostname string } +// installConfig holds computed/derived values from install flags type installConfig struct { - clusterID string - license *kotsv1beta1.License - licenseBytes []byte - tlsCert tls.Certificate - tlsCertBytes []byte - tlsKeyBytes []byte + clusterID string + isAirgap bool + enableManagerExperience bool + licenseBytes []byte + license *kotsv1beta1.License + airgapMetadata *airgap.AirgapMetadata + embeddedAssetsSize int64 + endUserConfig *ecv1beta1.Config + tlsCert tls.Certificate + tlsCertBytes []byte + tlsKeyBytes []byte } // webAssetsFS is the filesystem to be used by the web component. Defaults to nil allowing the web server to use the default assets embedded in the binary. Useful for testing. @@ -107,7 +104,7 @@ var webAssetsFS fs.FS = nil // InstallCmd returns a cobra command for installing the embedded cluster. func InstallCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { - var flags InstallCmdFlags + var flags installFlags ctx, cancel := context.WithCancel(ctx) @@ -128,21 +125,22 @@ func InstallCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { cancel() // Cancel context when command completes }, RunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc, ki); err != nil { + installCfg, err := preRunInstall(cmd, &flags, rc, ki) + if err != nil { return err } - if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, prompts.New()); err != nil { + if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, installCfg, prompts.New()); err != nil { return err } metricsReporter := newInstallReporter( replicatedAppURL(), cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), - flags.license.Spec.LicenseID, flags.clusterID, flags.license.Spec.AppSlug, + installCfg.license.Spec.LicenseID, installCfg.clusterID, installCfg.license.Spec.AppSlug, ) metricsReporter.ReportInstallationStarted(ctx) - if flags.enableManagerExperience { - return runManagerExperienceInstall(ctx, flags, rc, ki, metricsReporter.reporter, appTitle) + if installCfg.enableManagerExperience { + return runManagerExperienceInstall(ctx, flags, installCfg, rc, ki, metricsReporter.reporter, appTitle) } _ = rc.SetEnv() @@ -152,7 +150,7 @@ func InstallCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { metricsReporter.ReportSignalAborted(ctx, sig) }) - if err := runInstall(cmd.Context(), flags, rc, metricsReporter); err != nil { + if err := runInstall(cmd.Context(), flags, installCfg, rc, metricsReporter); err != nil { // Check if this is an interrupt error from the terminal if errors.Is(err, terminal.InterruptErr) { metricsReporter.ReportSignalAborted(ctx, syscall.SIGINT) @@ -212,7 +210,7 @@ func installCmdExample(appSlug string) string { return fmt.Sprintf(installCmdExampleText, appSlug, appSlug) } -func mustAddInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) { +func mustAddInstallFlags(cmd *cobra.Command, flags *installFlags) { enableV3 := isV3Enabled() normalizeFuncs := []func(f *pflag.FlagSet, name string) pflag.NormalizedName{} @@ -246,7 +244,7 @@ func mustAddInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) { }) } -func newCommonInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { +func newCommonInstallFlags(flags *installFlags, enableV3 bool) *pflag.FlagSet { flagSet := pflag.NewFlagSet("common", pflag.ContinueOnError) flagSet.StringVar(&flags.target, "target", "", "The target platform to install to. Valid options are 'linux' or 'kubernetes'.") @@ -269,7 +267,7 @@ func newCommonInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet return flagSet } -func newLinuxInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { +func newLinuxInstallFlags(flags *installFlags, enableV3 bool) *pflag.FlagSet { flagSet := pflag.NewFlagSet("linux", pflag.ContinueOnError) // Use the app slug as default data directory only when ENABLE_V3 is set @@ -302,7 +300,7 @@ func newLinuxInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet return flagSet } -func newKubernetesInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { +func newKubernetesInstallFlags(flags *installFlags, enableV3 bool) *pflag.FlagSet { flagSet := pflag.NewFlagSet("kubernetes", pflag.ContinueOnError) addKubernetesCLIFlags(flagSet, flags) @@ -317,13 +315,13 @@ func newKubernetesInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.Fla return flagSet } -func addKubernetesCLIFlags(flagSet *pflag.FlagSet, flags *InstallCmdFlags) { +func addKubernetesCLIFlags(flagSet *pflag.FlagSet, flags *installFlags) { s := helmcli.New() helm.AddKubernetesCLIFlags(flagSet, s) flags.kubernetesEnvSettings = s } -func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { +func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *installFlags) error { cmd.Flags().StringVar(&flags.adminConsolePassword, "admin-console-password", "", "Password for the Admin Console") cmd.Flags().IntVar(&flags.adminConsolePort, "admin-console-port", ecv1beta1.DefaultAdminConsolePort, "Port on which the Admin Console will be served") cmd.Flags().StringVarP(&flags.licenseFile, "license", "l", "", "Path to the license file") @@ -333,7 +331,7 @@ func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) err return nil } -func addTLSFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { +func addTLSFlags(cmd *cobra.Command, flags *installFlags) error { managerName := "Admin Console" if isV3Enabled() { managerName = "Manager" @@ -346,7 +344,7 @@ func addTLSFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { return nil } -func addManagementConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { +func addManagementConsoleFlags(cmd *cobra.Command, flags *installFlags) error { cmd.Flags().IntVar(&flags.managerPort, "manager-port", ecv1beta1.DefaultManagerPort, "Port on which the Manager will be served") // If the ENABLE_V3 environment variable is set, default to the new manager experience and do @@ -360,141 +358,60 @@ func addManagementConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) error return nil } -func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) error { - if !isV3Enabled() { - flags.target = "linux" - } - - if !slices.Contains([]string{"linux", "kubernetes"}, flags.target) { - return fmt.Errorf(`invalid --target (must be one of: "linux", "kubernetes")`) - } - - flags.clusterID = uuid.New().String() - - if err := preRunInstallCommon(cmd, flags, rc, ki); err != nil { - return err - } - - switch flags.target { - case "linux": - return preRunInstallLinux(cmd, flags, rc) - case "kubernetes": - return preRunInstallKubernetes(cmd, flags, ki) +func preRunInstall(cmd *cobra.Command, flags *installFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) (*installConfig, error) { + // Hydrate flags + if err := buildInstallFlags(cmd, flags); err != nil { + return nil, err } - return nil -} - -func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) error { - flags.enableManagerExperience = isV3Enabled() - - // license file can be empty for restore - if flags.licenseFile != "" { - b, err := os.ReadFile(flags.licenseFile) - if err != nil { - return fmt.Errorf("failed to read license file: %w", err) - } - flags.licenseBytes = b - - // validate the the license is indeed a license file - l, err := helpers.ParseLicense(flags.licenseFile) - if err != nil { - if err == helpers.ErrNotALicenseFile { - return fmt.Errorf("license file is not a valid license file") - } - - return fmt.Errorf("failed to parse license file: %w", err) - } - flags.license = l + // Build installCfg config + installCfg, err := buildInstallConfig(flags) + if err != nil { + return nil, err } - if flags.configValues != "" { - err := configutils.ValidateKotsConfigValues(flags.configValues) + // sync the license if we are in the manager experience and a license is provided and we are + // not in airgap mode + if installCfg.enableManagerExperience && installCfg.license != nil && !installCfg.isAirgap { + replicatedAPI, err := newReplicatedAPIClient(installCfg.license, installCfg.clusterID) if err != nil { - return fmt.Errorf("config values file is not valid: %w", err) + return nil, fmt.Errorf("failed to create replicated API client: %w", err) } - } - flags.isAirgap = flags.airgapBundle != "" - if flags.airgapBundle != "" { - metadata, err := airgap.AirgapMetadataFromPath(flags.airgapBundle) + updatedLicense, licenseBytes, err := syncLicense(cmd.Context(), replicatedAPI, installCfg.license) if err != nil { - return fmt.Errorf("failed to get airgap info: %w", err) + return nil, fmt.Errorf("failed to sync license: %w", err) } - flags.airgapMetadata = metadata - } - - var err error - flags.embeddedAssetsSize, err = goods.SizeOfEmbeddedAssets() - if err != nil { - return fmt.Errorf("failed to get size of embedded files: %w", err) - } - - if flags.managerPort != 0 && flags.adminConsolePort != 0 { - if flags.managerPort == flags.adminConsolePort { - return fmt.Errorf("manager port cannot be the same as admin console port") - } - } - - proxy, err := proxyConfigFromCmd(cmd, flags.assumeYes) - if err != nil { - return err + installCfg.license = updatedLicense + installCfg.licenseBytes = licenseBytes } + // Set runtime config values from flags rc.SetAdminConsolePort(flags.adminConsolePort) ki.SetAdminConsolePort(flags.adminConsolePort) rc.SetManagerPort(flags.managerPort) ki.SetManagerPort(flags.managerPort) - rc.SetProxySpec(proxy) - ki.SetProxySpec(proxy) - - // Process TLS certificate configuration if provided - if err := processTLSConfig(flags); err != nil { - return fmt.Errorf("process TLS configuration: %w", err) - } - - return nil -} + rc.SetProxySpec(flags.proxySpec) + ki.SetProxySpec(flags.proxySpec) -// processTLSConfig validates and processes TLS certificate configuration for both traditional and manager experience flows -func processTLSConfig(flags *InstallCmdFlags) error { - // If both cert and key are provided, validate and load them - if flags.tlsCertFile != "" && flags.tlsKeyFile != "" { - cert, err := tls.LoadX509KeyPair(flags.tlsCertFile, flags.tlsKeyFile) - if err != nil { - return fmt.Errorf("load tls certificate: %w", err) - } - certData, err := os.ReadFile(flags.tlsCertFile) - if err != nil { - return fmt.Errorf("failed to read tls cert file: %w", err) + // Target-specific configuration + switch flags.target { + case "linux": + if err := preRunInstallLinux(flags, installCfg, rc); err != nil { + return nil, err } - keyData, err := os.ReadFile(flags.tlsKeyFile) - if err != nil { - return fmt.Errorf("failed to read tls key file: %w", err) + case "kubernetes": + if err := preRunInstallKubernetes(flags, ki); err != nil { + return nil, err } - flags.tlsCert = cert - flags.tlsCertBytes = certData - flags.tlsKeyBytes = keyData - - return nil - } - - // If only one of cert or key is provided, return an error - if flags.tlsCertFile != "" || flags.tlsKeyFile != "" { - return fmt.Errorf("both --tls-cert and --tls-key must be provided together") } - // If neither is provided, no TLS configuration (will use default behavior) - return nil + return installCfg, nil } -func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { - if !cmd.Flags().Changed("skip-host-preflights") && (os.Getenv("SKIP_HOST_PREFLIGHTS") == "1" || os.Getenv("SKIP_HOST_PREFLIGHTS") == "true") { - flags.skipHostPreflights = true - } - +func preRunInstallLinux(flags *installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { if os.Getuid() != 0 { return fmt.Errorf("install command must be run as root") } @@ -509,38 +426,14 @@ func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeco } logrus.Debugf("using host CA bundle: %s", hostCABundlePath) - // if a network interface flag was not provided, attempt to discover it - if flags.networkInterface == "" { - autoInterface, err := newconfig.DetermineBestNetworkInterface() - if err == nil { - flags.networkInterface = autoInterface - } - } - - if flags.localArtifactMirrorPort != 0 && flags.adminConsolePort != 0 { - if flags.localArtifactMirrorPort == flags.adminConsolePort { - return fmt.Errorf("local artifact mirror port cannot be the same as admin console port") - } - } - - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return fmt.Errorf("process overrides file: %w", err) - } - - cidrCfg, err := cidrConfigFromCmd(cmd) - if err != nil { - return err - } - - k0sCfg, err := k0s.NewK0sConfig(flags.networkInterface, flags.isAirgap, cidrCfg.PodCIDR, cidrCfg.ServiceCIDR, eucfg, nil) + k0sCfg, err := k0sConfigFromFlags(flags, installCfg) if err != nil { return fmt.Errorf("failed to create k0s config: %w", err) } networkSpec := helpers.NetworkSpecFromK0sConfig(k0sCfg) networkSpec.NetworkInterface = flags.networkInterface - if cidrCfg.GlobalCIDR != nil { - networkSpec.GlobalCIDR = *cidrCfg.GlobalCIDR + if flags.cidrConfig.GlobalCIDR != nil { + networkSpec.GlobalCIDR = *flags.cidrConfig.GlobalCIDR } // TODO: validate that a single port isn't used for multiple services @@ -557,7 +450,7 @@ func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeco return nil } -func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, ki kubernetesinstallation.Installation) error { +func preRunInstallKubernetes(flags *installFlags, ki kubernetesinstallation.Installation) error { // TODO: we only support amd64 clusters for target=kubernetes installs helpers.SetClusterArch("amd64") @@ -590,86 +483,15 @@ func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, ki kubern return nil } -func proxyConfigFromCmd(cmd *cobra.Command, assumeYes bool) (*ecv1beta1.ProxySpec, error) { - proxy, err := parseProxyFlags(cmd) - if err != nil { - return nil, err - } - - if err := verifyProxyConfig(proxy, prompts.New(), assumeYes); err != nil { - return nil, err - } - - return proxy, nil -} - -func cidrConfigFromCmd(cmd *cobra.Command) (*newconfig.CIDRConfig, error) { - if err := validateCIDRFlags(cmd); err != nil { - return nil, err - } - - // parse the various cidr flags to make sure we have exactly what we want - cidrCfg, err := getCIDRConfig(cmd) - if err != nil { - return nil, fmt.Errorf("failed to determine pod and service CIDRs: %w", err) - } - - return cidrCfg, nil -} - func runManagerExperienceInstall( - ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation, + ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation, metricsReporter metrics.ReporterInterface, appTitle string, ) (finalErr error) { - kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, nil) - if err != nil { - return fmt.Errorf("get kotsadm namespace: %w", err) - } - - // this is necessary because the api listens on all interfaces, - // and we only know the interface to use when the user selects it in the ui - ipAddresses, err := netutils.ListAllValidIPAddresses() - if err != nil { - return fmt.Errorf("failed to list all valid IP addresses: %w", err) - } - passwordHash, err := bcrypt.GenerateFromPassword([]byte(flags.adminConsolePassword), 10) if err != nil { return fmt.Errorf("failed to generate password hash: %w", err) } - // For manager experience, generate self-signed cert if none provided, with user confirmation - if flags.tlsCertFile == "" || flags.tlsKeyFile == "" { - logrus.Warn("\nNo certificate files provided. A self-signed certificate will be used, and your browser will show a security warning.") - logrus.Info("To use your own certificate, provide both --tls-key and --tls-cert flags.") - - if !flags.assumeYes { - logrus.Info("") // newline so the prompt is separated from the warning - confirmed, err := prompts.New().Confirm("Do you want to continue with a self-signed certificate?", false) - if err != nil { - return fmt.Errorf("failed to get confirmation: %w", err) - } - if !confirmed { - logrus.Infof("\nInstallation cancelled. Please run the command again with the --tls-key and --tls-cert flags.\n") - return nil - } - } - - // Generate self-signed certificate - cert, certData, keyData, err := tlsutils.GenerateCertificate(flags.hostname, ipAddresses, kotsadmNamespace) - if err != nil { - return fmt.Errorf("generate tls certificate: %w", err) - } - flags.tlsCert = cert - flags.tlsCertBytes = certData - flags.tlsKeyBytes = keyData - } - - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return fmt.Errorf("process overrides file: %w", err) - } - var configValues apitypes.AppConfigValues if flags.configValues != "" { kotsConfigValues, err := helpers.ParseConfigValues(flags.configValues) @@ -685,18 +507,18 @@ func runManagerExperienceInstall( Password: flags.adminConsolePassword, PasswordHash: passwordHash, TLSConfig: apitypes.TLSConfig{ - CertBytes: flags.tlsCertBytes, - KeyBytes: flags.tlsKeyBytes, + CertBytes: installCfg.tlsCertBytes, + KeyBytes: installCfg.tlsKeyBytes, Hostname: flags.hostname, }, - License: flags.licenseBytes, + License: installCfg.licenseBytes, AirgapBundle: flags.airgapBundle, - AirgapMetadata: flags.airgapMetadata, - EmbeddedAssetsSize: flags.embeddedAssetsSize, + AirgapMetadata: installCfg.airgapMetadata, + EmbeddedAssetsSize: installCfg.embeddedAssetsSize, ConfigValues: configValues, ReleaseData: release.GetReleaseData(), - EndUserConfig: eucfg, - ClusterID: flags.clusterID, + EndUserConfig: installCfg.endUserConfig, + ClusterID: installCfg.clusterID, Mode: apitypes.ModeInstall, RequiresInfraUpgrade: false, // Always false for install @@ -717,7 +539,7 @@ func runManagerExperienceInstall( ctx, cancel := context.WithCancel(ctx) defer cancel() - if err := startAPI(ctx, flags.tlsCert, apiConfig, cancel); err != nil { + if err := startAPI(ctx, installCfg.tlsCert, apiConfig, cancel); err != nil { return fmt.Errorf("failed to start api: %w", err) } @@ -729,25 +551,25 @@ func runManagerExperienceInstall( return nil } -func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, metricsReporter *installReporter) (finalErr error) { - if flags.enableManagerExperience { +func runInstall(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, metricsReporter *installReporter) (finalErr error) { + if installCfg.enableManagerExperience { return nil } logrus.Debug("initializing install") - if err := initializeInstall(ctx, flags, rc); err != nil { + if err := initializeInstall(ctx, flags, installCfg, rc); err != nil { return fmt.Errorf("failed to initialize install: %w", err) } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, rc, metricsReporter.reporter); err != nil { + if err := runInstallPreflights(ctx, flags, installCfg, rc, metricsReporter.reporter); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } return fmt.Errorf("failed to run install preflights: %w", err) } - if _, err := installAndStartCluster(ctx, flags, rc, nil); err != nil { + if _, err := installAndStartCluster(ctx, flags, installCfg, rc, nil); err != nil { return fmt.Errorf("failed to install cluster: %w", err) } @@ -764,7 +586,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run errCh := kubeutils.WaitForKubernetes(ctx, kcli) defer logKubernetesErrors(errCh) - in, err := recordInstallation(ctx, kcli, flags, rc) + in, err := recordInstallation(ctx, kcli, flags, installCfg, rc) if err != nil { return fmt.Errorf("failed to record installation: %w", err) } @@ -785,7 +607,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run } airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } @@ -801,7 +623,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run defer hcli.Close() logrus.Debugf("installing addons") - if err := installAddons(ctx, kcli, mcli, hcli, flags, rc); err != nil { + if err := installAddons(ctx, kcli, mcli, hcli, flags, installCfg, rc); err != nil { return err } @@ -820,24 +642,24 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run isHeadlessInstall := flags.configValues != "" && flags.adminConsolePassword != "" - printSuccessMessage(flags.license, flags.hostname, flags.networkInterface, rc, isHeadlessInstall) + printSuccessMessage(installCfg.license, flags.hostname, flags.networkInterface, rc, isHeadlessInstall) return nil } -func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, loading **spinner.MessageWriter) (*addons.InstallOptions, error) { +func k0sConfigFromFlags(flags *installFlags, installCfg *installConfig) (*k0sv1beta1.ClusterConfig, error) { + return k0s.NewK0sConfig(flags.networkInterface, installCfg.isAirgap, flags.cidrConfig.PodCIDR, flags.cidrConfig.ServiceCIDR, installCfg.endUserConfig, nil) +} + +func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, loading **spinner.MessageWriter) (*addons.InstallOptions, error) { var embCfgSpec *ecv1beta1.ConfigSpec if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { embCfgSpec = &embCfg.Spec } - euCfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return nil, fmt.Errorf("failed to process overrides file: %w", err) - } var euCfgSpec *ecv1beta1.ConfigSpec - if euCfg != nil { - euCfgSpec = &euCfg.Spec + if installCfg.endUserConfig != nil { + euCfgSpec = &installCfg.endUserConfig.Spec } kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(ctx, kcli) @@ -846,16 +668,16 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags InstallC } opts := &addons.InstallOptions{ - ClusterID: flags.clusterID, + ClusterID: installCfg.clusterID, AdminConsolePwd: flags.adminConsolePassword, AdminConsolePort: rc.AdminConsolePort(), - License: flags.license, + License: installCfg.license, IsAirgap: flags.airgapBundle != "", - TLSCertBytes: flags.tlsCertBytes, - TLSKeyBytes: flags.tlsKeyBytes, + TLSCertBytes: installCfg.tlsCertBytes, + TLSKeyBytes: installCfg.tlsKeyBytes, Hostname: flags.hostname, - DisasterRecoveryEnabled: flags.license.Spec.IsDisasterRecoverySupported, - IsMultiNodeEnabled: flags.license.Spec.IsEmbeddedClusterMultiNodeEnabled, + DisasterRecoveryEnabled: installCfg.license.Spec.IsDisasterRecoverySupported, + IsMultiNodeEnabled: installCfg.license.Spec.IsEmbeddedClusterMultiNodeEnabled, EmbeddedConfigSpec: embCfgSpec, EndUserConfigSpec: euCfgSpec, ProxySpec: rc.ProxySpec(), @@ -867,10 +689,10 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags InstallC ServiceCIDR: rc.ServiceCIDR(), KotsInstaller: func() error { opts := kotscli.InstallOptions{ - AppSlug: flags.license.Spec.AppSlug, - License: flags.licenseBytes, + AppSlug: installCfg.license.Spec.AppSlug, + License: installCfg.licenseBytes, Namespace: kotsadmNamespace, - ClusterID: flags.clusterID, + ClusterID: installCfg.clusterID, AirgapBundle: flags.airgapBundle, ConfigValuesFile: flags.configValues, ReplicatedAppEndpoint: replicatedAppURL(), @@ -883,31 +705,31 @@ func getAddonInstallOpts(ctx context.Context, kcli client.Client, flags InstallC return opts, nil } -func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, flags *InstallCmdFlags, prompt prompts.Prompt) error { +func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, flags *installFlags, installCfg *installConfig, prompt prompts.Prompt) error { logrus.Debugf("checking if k0s is already installed") err := verifyNoInstallation(appSlug, "reinstall") if err != nil { return err } - err = verifyChannelRelease("installation", flags.isAirgap, flags.assumeYes) + err = verifyChannelRelease("installation", installCfg.isAirgap, flags.assumeYes) if err != nil { return err } logrus.Debugf("checking license matches") - license, err := getLicenseFromFilepath(flags.licenseFile) + license, err := verifyLicense(installCfg.license) if err != nil { return err } - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { logrus.Debugf("checking airgap bundle matches binary") - if err := checkAirgapMatches(flags.airgapMetadata.AirgapInfo); err != nil { + if err := checkAirgapMatches(installCfg.airgapMetadata.AirgapInfo); err != nil { return err // we want the user to see the error message without a prefix } } - if !flags.isAirgap { + if !installCfg.isAirgap { if err := maybePromptForAppUpdate(ctx, prompt, license, flags.assumeYes); err != nil { if errors.As(err, &ErrorNothingElseToAdd{}) { return err @@ -922,8 +744,14 @@ func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, fl return err } + // TODO (@salah): figure out how we can move this to buildInstallFlags without changing product behavior + if err := verifyProxyConfig(flags.proxySpec, prompts.New(), flags.assumeYes); err != nil { + return err + } + // restore command doesn't have a password flag if cmd.Flags().Lookup("admin-console-password") != nil { + // TODO (@salah): figure out how we can move this to buildInstallFlags without changing product behavior if err := ensureAdminConsolePassword(flags); err != nil { return err } @@ -932,7 +760,7 @@ func verifyAndPrompt(ctx context.Context, cmd *cobra.Command, appSlug string, fl return nil } -func ensureAdminConsolePassword(flags *InstallCmdFlags) error { +func ensureAdminConsolePassword(flags *installFlags) error { if flags.adminConsolePassword == "" { // no password was provided if flags.assumeYes { @@ -968,29 +796,24 @@ func ensureAdminConsolePassword(flags *InstallCmdFlags) error { return nil } -func getLicenseFromFilepath(licenseFile string) (*kotsv1beta1.License, error) { +func verifyLicense(license *kotsv1beta1.License) (*kotsv1beta1.License, error) { rel := release.GetChannelRelease() // handle the three cases that do not require parsing the license file // 1. no release and no license, which is OK // 2. no license and a release, which is not OK // 3. a license and no release, which is not OK - if rel == nil && licenseFile == "" { + if rel == nil && license == nil { // no license and no release, this is OK return nil, nil - } else if rel == nil && licenseFile != "" { + } else if rel == nil && license != nil { // license is present but no release, this means we would install without vendor charts and k0s overrides return nil, fmt.Errorf("a license was provided but no release was found in binary, please rerun without the license flag") - } else if rel != nil && licenseFile == "" { + } else if rel != nil && license == nil { // release is present but no license, this is not OK return nil, fmt.Errorf("no license was provided for %s and one is required, please rerun with '--license '", rel.AppSlug) } - license, err := helpers.ParseLicense(licenseFile) - if err != nil { - return nil, fmt.Errorf("failed to parse the license file at %q, please ensure it is not corrupt: %w", licenseFile, err) - } - // Check if the license matches the application version data if rel.AppSlug != license.Spec.AppSlug { // if the app is different, we will not be able to provide the correct vendor supplied charts and k0s overrides @@ -1079,18 +902,13 @@ func verifyNoInstallation(appSlug string, cmdName string) error { return nil } -func initializeInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func initializeInstall(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { logrus.Info("") spinner := spinner.Start() spinner.Infof("Initializing") - licenseBytes, err := os.ReadFile(flags.licenseFile) - if err != nil { - return fmt.Errorf("failed to read license file: %w", err) - } - if err := hostutils.ConfigureHost(ctx, rc, hostutils.InitForInstallOptions{ - License: licenseBytes, + License: installCfg.licenseBytes, AirgapBundle: flags.airgapBundle, }); err != nil { spinner.ErrorClosef("Initialization failed") @@ -1101,7 +919,7 @@ func initializeInstall(ctx context.Context, flags InstallCmdFlags, rc runtimecon return nil } -func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { +func installAndStartCluster(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { loading := spinner.Start() loading.Infof("Installing node") @@ -1114,12 +932,12 @@ func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runti logrus.Debugf("creating k0s configuration file") - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) + cfg, err := k0sConfigFromFlags(&flags, installCfg) if err != nil { - return nil, fmt.Errorf("process overrides file: %w", err) + return nil, fmt.Errorf("unable to create k0s config: %w", err) } - cfg, err := k0s.WriteK0sConfig(ctx, flags.networkInterface, flags.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), eucfg, mutate) + err = k0s.WriteK0sConfig(ctx, cfg) if err != nil { loading.ErrorClosef("Failed to install node") return nil, fmt.Errorf("create config file: %w", err) @@ -1154,7 +972,7 @@ func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runti return cfg, nil } -func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { progressChan := make(chan addontypes.AddOnProgress) defer close(progressChan) @@ -1182,7 +1000,7 @@ func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interf addons.WithProgressChannel(progressChan), ) - opts, err := getAddonInstallOpts(ctx, kcli, flags, rc, &loading) + opts, err := getAddonInstallOpts(ctx, kcli, flags, installCfg, rc, &loading) if err != nil { return fmt.Errorf("get addon install opts: %w", err) } @@ -1349,16 +1167,6 @@ func validateAdminConsolePassword(password, passwordCheck string) bool { return true } -func replicatedAppURL() string { - domains := getDomains() - return netutils.MaybeAddHTTPS(domains.ReplicatedAppDomain) -} - -func proxyRegistryURL() string { - domains := getDomains() - return netutils.MaybeAddHTTPS(domains.ProxyRegistryDomain) -} - func waitForNode(ctx context.Context) error { kcli, err := kubeutils.KubeClient() if err != nil { @@ -1375,7 +1183,7 @@ func waitForNode(ctx context.Context) error { } func recordInstallation( - ctx context.Context, kcli client.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, + ctx context.Context, kcli client.Client, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, ) (*ecv1beta1.Installation, error) { // get the embedded cluster config cfg := release.GetEmbeddedClusterConfig() @@ -1384,27 +1192,21 @@ func recordInstallation( cfgspec = &cfg.Spec } - // parse the end user config - eucfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return nil, fmt.Errorf("process overrides file: %w", err) - } - // extract airgap uncompressed size if airgap info is provided var airgapUncompressedSize int64 - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { - airgapUncompressedSize = flags.airgapMetadata.AirgapInfo.Spec.UncompressedSize + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { + airgapUncompressedSize = installCfg.airgapMetadata.AirgapInfo.Spec.UncompressedSize } // record the installation installation, err := kubeutils.RecordInstallation(ctx, kcli, kubeutils.RecordInstallationOptions{ - ClusterID: flags.clusterID, - IsAirgap: flags.isAirgap, - License: flags.license, + ClusterID: installCfg.clusterID, + IsAirgap: installCfg.isAirgap, + License: installCfg.license, ConfigSpec: cfgspec, MetricsBaseURL: replicatedAppURL(), RuntimeConfig: rc.Get(), - EndUserConfig: eucfg, + EndUserConfig: installCfg.endUserConfig, AirgapUncompressedSize: airgapUncompressedSize, }) if err != nil { diff --git a/cmd/installer/cli/install_config.go b/cmd/installer/cli/install_config.go new file mode 100644 index 0000000000..5fb903a258 --- /dev/null +++ b/cmd/installer/cli/install_config.go @@ -0,0 +1,229 @@ +package cli + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "os" + + "github.com/google/uuid" + "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" + newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" + "github.com/replicatedhq/embedded-cluster/pkg/airgap" + "github.com/replicatedhq/embedded-cluster/pkg/configutils" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "github.com/replicatedhq/embedded-cluster/pkg/prompts" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// Hop: buildInstallFlags maps cobra command flags to install flags +func buildInstallFlags(cmd *cobra.Command, flags *installFlags) error { + // Target defaulting (if not V3) + if !isV3Enabled() { + flags.target = "linux" + } + + // Target validation + if flags.target != "linux" && flags.target != "kubernetes" { + return fmt.Errorf(`invalid --target (must be one of: "linux", "kubernetes")`) + } + + // If only one of cert or key is provided, return an error + if (flags.tlsCertFile != "" && flags.tlsKeyFile == "") || (flags.tlsCertFile == "" && flags.tlsKeyFile != "") { + return fmt.Errorf("both --tls-cert and --tls-key must be provided together") + } + + // Skip host preflights from env var (if flag not explicitly set) + if !cmd.Flags().Changed("skip-host-preflights") { + if os.Getenv("SKIP_HOST_PREFLIGHTS") == "1" || os.Getenv("SKIP_HOST_PREFLIGHTS") == "true" { + flags.skipHostPreflights = true + } + } + + // Network interface auto-detection (if not provided) + if flags.networkInterface == "" && flags.target == "linux" { + autoInterface, err := newconfig.DetermineBestNetworkInterface() + if err == nil { + flags.networkInterface = autoInterface + } + // If error, leave empty and validation will catch it later + } + + // Port conflict validations + if flags.managerPort != 0 && flags.adminConsolePort != 0 { + if flags.managerPort == flags.adminConsolePort { + return fmt.Errorf("manager port cannot be the same as admin console port") + } + } + + if flags.localArtifactMirrorPort != 0 && flags.adminConsolePort != 0 { + if flags.localArtifactMirrorPort == flags.adminConsolePort { + return fmt.Errorf("local artifact mirror port cannot be the same as admin console port") + } + } + + // CIDR configuration + cidrCfg, err := cidrConfigFromCmd(cmd) + if err != nil { + return err + } + flags.cidrConfig = cidrCfg + + // Proxy configuration + proxy, err := parseProxyFlags(cmd, flags.networkInterface, flags.cidrConfig) + if err != nil { + return err + } + flags.proxySpec = proxy + + return nil +} + +// Hop: buildInstallConfig builds the install config from install flags +func buildInstallConfig(flags *installFlags) (*installConfig, error) { + installCfg := &installConfig{ + clusterID: uuid.New().String(), + enableManagerExperience: isV3Enabled(), + } + + // License file reading + if flags.licenseFile != "" { + b, err := os.ReadFile(flags.licenseFile) + if err != nil { + return nil, fmt.Errorf("failed to read license file: %w", err) + } + installCfg.licenseBytes = b + + // validate the license is indeed a license file + l, err := helpers.ParseLicenseFromBytes(b) + if err != nil { + var notALicenseFileErr helpers.ErrNotALicenseFile + if errors.As(err, ¬ALicenseFileErr) { + return nil, fmt.Errorf("failed to parse the license file at %q, please ensure it is not corrupt: %w", flags.licenseFile, err) + } + + return nil, fmt.Errorf("failed to parse license file: %w", err) + } + installCfg.license = l + } + + // Config values validation + if flags.configValues != "" { + err := configutils.ValidateKotsConfigValues(flags.configValues) + if err != nil { + return nil, fmt.Errorf("config values file is not valid: %w", err) + } + } + + // Airgap detection and metadata + installCfg.isAirgap = flags.airgapBundle != "" + if flags.airgapBundle != "" { + metadata, err := airgap.AirgapMetadataFromPath(flags.airgapBundle) + if err != nil { + return nil, fmt.Errorf("failed to get airgap info: %w", err) + } + installCfg.airgapMetadata = metadata + } + + // Embedded assets size + size, err := goods.SizeOfEmbeddedAssets() + if err != nil { + return nil, fmt.Errorf("failed to get size of embedded files: %w", err) + } + installCfg.embeddedAssetsSize = size + + // End user config (overrides file) + eucfg, err := helpers.ParseEndUserConfig(flags.overrides) + if err != nil { + return nil, fmt.Errorf("process overrides file: %w", err) + } + installCfg.endUserConfig = eucfg + + // TLS Certificate Processing + if err := processTLSConfig(flags, installCfg); err != nil { + return nil, fmt.Errorf("process TLS config: %w", err) + } + + return installCfg, nil +} + +func cidrConfigFromCmd(cmd *cobra.Command) (*newconfig.CIDRConfig, error) { + if err := validateCIDRFlags(cmd); err != nil { + return nil, err + } + + // parse the various cidr flags to make sure we have exactly what we want + cidrCfg, err := getCIDRConfig(cmd) + if err != nil { + return nil, fmt.Errorf("failed to determine pod and service CIDRs: %w", err) + } + + return cidrCfg, nil +} + +func processTLSConfig(flags *installFlags, installCfg *installConfig) error { + // If both cert and key are provided, load them + if flags.tlsCertFile != "" && flags.tlsKeyFile != "" { + certBytes, err := os.ReadFile(flags.tlsCertFile) + if err != nil { + return fmt.Errorf("failed to read TLS certificate: %w", err) + } + keyBytes, err := os.ReadFile(flags.tlsKeyFile) + if err != nil { + return fmt.Errorf("failed to read TLS key: %w", err) + } + + cert, err := tls.X509KeyPair(certBytes, keyBytes) + if err != nil { + return fmt.Errorf("failed to parse TLS certificate: %w", err) + } + + installCfg.tlsCert = cert + installCfg.tlsCertBytes = certBytes + installCfg.tlsKeyBytes = keyBytes + } else if installCfg.enableManagerExperience { + // For manager experience, generate self-signed certificate if none provided + logrus.Warn("\nNo certificate files provided. A self-signed certificate will be used, and your browser will show a security warning.") + logrus.Info("To use your own certificate, provide both --tls-key and --tls-cert flags.") + + if !flags.assumeYes { + logrus.Info("") // newline so the prompt is separated from the warning + confirmed, err := prompts.New().Confirm("Do you want to continue with a self-signed certificate?", false) + if err != nil { + return fmt.Errorf("failed to get confirmation: %w", err) + } + if !confirmed { + logrus.Infof("\nInstallation cancelled. Please run the command again with the --tls-key and --tls-cert flags.\n") + return fmt.Errorf("installation cancelled by user") + } + } + + // Get all IP addresses for the certificate + ipAddresses, err := netutils.ListAllValidIPAddresses() + if err != nil { + return fmt.Errorf("failed to list all valid IP addresses: %w", err) + } + + // Determine the namespace for the certificate + kotsadmNamespace, err := runtimeconfig.KotsadmNamespace(context.Background(), nil) + if err != nil { + return fmt.Errorf("get kotsadm namespace: %w", err) + } + + // Generate self-signed certificate + cert, certData, keyData, err := tlsutils.GenerateCertificate(flags.hostname, ipAddresses, kotsadmNamespace) + if err != nil { + return fmt.Errorf("generate tls certificate: %w", err) + } + installCfg.tlsCert = cert + installCfg.tlsCertBytes = certData + installCfg.tlsKeyBytes = keyData + } + + return nil +} diff --git a/cmd/installer/cli/install_config_test.go b/cmd/installer/cli/install_config_test.go new file mode 100644 index 0000000000..631429d7d9 --- /dev/null +++ b/cmd/installer/cli/install_config_test.go @@ -0,0 +1,447 @@ +package cli + +import ( + "net" + "testing" + + "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" +) + +// Mock network interface for testing +type mockNetworkLookup struct{} + +func (m *mockNetworkLookup) FirstValidIPNet(networkInterface string) (*net.IPNet, error) { + _, ipnet, _ := net.ParseCIDR("192.168.1.0/24") + return ipnet, nil +} + +// Helper function to create bool pointer +func boolPtr(b bool) *bool { + return &b +} + +func Test_buildInstallFlags_ProxyConfig(t *testing.T) { + tests := []struct { + name string + init func(t *testing.T, flagSet *pflag.FlagSet) + want *ecv1beta1.ProxySpec + }{ + { + name: "no flags set and no env vars should not set proxy", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + // No env vars, no flags + }, + want: nil, + }, + { + name: "lowercase env vars should be used when no flags set", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + t.Setenv("https_proxy", "https://lower-proxy") + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://lower-proxy", + HTTPSProxy: "https://lower-proxy", + ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "uppercase env vars should be used when no flags set and no lowercase vars", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("HTTP_PROXY", "http://upper-proxy") + t.Setenv("HTTPS_PROXY", "https://upper-proxy") + t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://upper-proxy", + HTTPSProxy: "https://upper-proxy", + ProvidedNoProxy: "upper-no-proxy-1,upper-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,upper-no-proxy-1,upper-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "lowercase should take precedence over uppercase", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + t.Setenv("https_proxy", "https://lower-proxy") + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + t.Setenv("HTTP_PROXY", "http://upper-proxy") + t.Setenv("HTTPS_PROXY", "https://upper-proxy") + t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://lower-proxy", + HTTPSProxy: "https://lower-proxy", + ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "proxy flags should override env vars", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + t.Setenv("https_proxy", "https://lower-proxy") + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + t.Setenv("HTTP_PROXY", "http://upper-proxy") + t.Setenv("HTTPS_PROXY", "https://upper-proxy") + t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") + + flagSet.Set("http-proxy", "http://flag-proxy") + flagSet.Set("https-proxy", "https://flag-proxy") + flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://flag-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "pod and service CIDR should override default no proxy", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("http-proxy", "http://flag-proxy") + flagSet.Set("https-proxy", "https://flag-proxy") + flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") + + flagSet.Set("pod-cidr", "1.1.1.1/24") + flagSet.Set("service-cidr", "2.2.2.2/24") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://flag-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,1.1.1.1/24,2.2.2.2/24,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "custom --cidr should be present in the no-proxy", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("http-proxy", "http://flag-proxy") + flagSet.Set("https-proxy", "https://flag-proxy") + flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") + + flagSet.Set("cidr", "10.0.0.0/16") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://flag-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.0.0.0/17,10.0.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", + }, + }, + { + name: "partial env vars with partial flag vars", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + t.Setenv("http_proxy", "http://lower-proxy") + // No https_proxy set + t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") + + // Only set https-proxy flag + flagSet.Set("https-proxy", "https://flag-proxy") + }, + want: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://lower-proxy", + HTTPSProxy: "https://flag-proxy", + ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", + NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup flags struct + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + } + + // Setup cobra command with flags + cmd := &cobra.Command{} + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + flagSet := cmd.Flags() + if tt.init != nil { + tt.init(t, flagSet) + } + + // Override the network lookup with our mock + defaultNetworkLookupImpl = &mockNetworkLookup{} + + err := buildInstallFlags(cmd, flags) + assert.NoError(t, err, "unexpected error") + assert.Equal(t, tt.want, flags.proxySpec) + }) + } +} + +func Test_buildInstallFlags_SkipHostPreflightsEnvVar(t *testing.T) { + tests := []struct { + name string + envVarValue string + flagValue *bool // nil means not set, true/false means explicitly set + expectedSkipPreflights bool + }{ + { + name: "env var set to 1, no flag", + envVarValue: "1", + flagValue: nil, + expectedSkipPreflights: true, + }, + { + name: "env var set to true, no flag", + envVarValue: "true", + flagValue: nil, + expectedSkipPreflights: true, + }, + { + name: "env var set, flag explicitly false (flag takes precedence)", + envVarValue: "1", + flagValue: boolPtr(false), + expectedSkipPreflights: false, + }, + { + name: "env var set, flag explicitly true", + envVarValue: "1", + flagValue: boolPtr(true), + expectedSkipPreflights: true, + }, + { + name: "env var not set, no flag", + envVarValue: "", + flagValue: nil, + expectedSkipPreflights: false, + }, + { + name: "env var not set, flag explicitly false", + envVarValue: "", + flagValue: boolPtr(false), + expectedSkipPreflights: false, + }, + { + name: "env var not set, flag explicitly true", + envVarValue: "", + flagValue: boolPtr(true), + expectedSkipPreflights: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up environment variable + if tt.envVarValue != "" { + t.Setenv("SKIP_HOST_PREFLIGHTS", tt.envVarValue) + } + + // Create a mock cobra command to simulate flag behavior + cmd := &cobra.Command{} + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + } + + // Add the flags + cmd.Flags().BoolVar(&flags.skipHostPreflights, "skip-host-preflights", false, "Skip host preflight checks") + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + // Set the flag if explicitly provided in test + if tt.flagValue != nil { + err := cmd.Flags().Set("skip-host-preflights", "true") + if *tt.flagValue { + assert.NoError(t, err) + } else { + // For false, we need to mark the flag as changed but set to false + cmd.Flags().Set("skip-host-preflights", "false") + } + } + + err := buildInstallFlags(cmd, flags) + assert.NoError(t, err) + + // Verify the flag was set correctly + assert.Equal(t, tt.expectedSkipPreflights, flags.skipHostPreflights) + }) + } +} + +func Test_buildInstallFlags_CIDRConfig(t *testing.T) { + // Compute expected split CIDR values for the default CIDR + defaultPodCIDR, defaultServiceCIDR, err := newconfig.SplitCIDR(ecv1beta1.DefaultNetworkCIDR) + assert.NoError(t, err, "failed to split default CIDR") + + tests := []struct { + name string + init func(t *testing.T, flagSet *pflag.FlagSet) + expected *newconfig.CIDRConfig + expectError bool + }{ + { + name: "with pod and service flags", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + flagSet.Set("service-cidr", "10.1.0.0/24") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: "10.0.0.0/24", + ServiceCIDR: "10.1.0.0/24", + GlobalCIDR: nil, + }, + }, + { + name: "with pod flag", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: "10.0.0.0/24", + ServiceCIDR: v1beta1.DefaultNetwork().ServiceCIDR, + GlobalCIDR: nil, + }, + }, + { + name: "with pod, service and cidr flags - should error", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + flagSet.Set("service-cidr", "10.1.0.0/24") + flagSet.Set("cidr", "10.2.0.0/24") + }, + expectError: true, + }, + { + name: "with pod and cidr flags - should error", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("pod-cidr", "10.0.0.0/24") + flagSet.Set("cidr", "10.2.0.0/24") + }, + expectError: true, + }, + { + name: "with service flag only", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("service-cidr", "10.1.0.0/24") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: v1beta1.DefaultNetwork().PodCIDR, + ServiceCIDR: "10.1.0.0/24", + GlobalCIDR: nil, + }, + }, + { + name: "with cidr flag", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + flagSet.Set("cidr", "10.2.0.0/16") + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: "10.2.0.0/17", + ServiceCIDR: "10.2.128.0/17", + GlobalCIDR: ptr.To("10.2.0.0/16"), + }, + }, + { + name: "with no flags (defaults)", + init: func(t *testing.T, flagSet *pflag.FlagSet) { + // No flags set, should use default cidr value and split it + }, + expected: &newconfig.CIDRConfig{ + PodCIDR: defaultPodCIDR, + ServiceCIDR: defaultServiceCIDR, + GlobalCIDR: ptr.To(ecv1beta1.DefaultNetworkCIDR), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup flags struct + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + } + + // Setup cobra command with flags + cmd := &cobra.Command{} + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + flagSet := cmd.Flags() + if tt.init != nil { + tt.init(t, flagSet) + } + + err := buildInstallFlags(cmd, flags) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err, "unexpected error") + assert.Equal(t, tt.expected, flags.cidrConfig) + } + }) + } +} + +func Test_buildInstallFlags_TLSValidation(t *testing.T) { + tests := []struct { + name string + tlsCertFile string + tlsKeyFile string + wantErr string + }{ + { + name: "both cert and key provided", + tlsCertFile: "/path/to/cert.pem", + tlsKeyFile: "/path/to/key.pem", + wantErr: "", + }, + { + name: "neither cert nor key provided", + tlsCertFile: "", + tlsKeyFile: "", + wantErr: "", + }, + { + name: "only cert file provided", + tlsCertFile: "/path/to/cert.pem", + tlsKeyFile: "", + wantErr: "both --tls-cert and --tls-key must be provided together", + }, + { + name: "only key file provided", + tlsCertFile: "", + tlsKeyFile: "/path/to/key.pem", + wantErr: "both --tls-cert and --tls-key must be provided together", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup flags struct + flags := &installFlags{ + networkInterface: "eth0", // Skip network interface auto-detection + tlsCertFile: tt.tlsCertFile, + tlsKeyFile: tt.tlsKeyFile, + } + + // Setup cobra command with flags + cmd := &cobra.Command{} + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) + + err := buildInstallFlags(cmd, flags) + + if tt.wantErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/installer/cli/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index 9d6ae68968..77a6e1a17d 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "os" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" @@ -23,7 +22,7 @@ import ( var ErrPreflightsHaveFail = metrics.NewErrorNoFail(fmt.Errorf("host preflight failures detected")) func InstallRunPreflightsCmd(ctx context.Context, appSlug string) *cobra.Command { - var flags InstallCmdFlags + var flags installFlags rc := runtimeconfig.New(nil) ki := kubernetesinstallation.New(nil) @@ -36,16 +35,17 @@ func InstallRunPreflightsCmd(ctx context.Context, appSlug string) *cobra.Command rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc, ki); err != nil { + installCfg, err := preRunInstall(cmd, &flags, rc, ki) + if err != nil { return err } - if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, prompts.New()); err != nil { + if err := verifyAndPrompt(ctx, cmd, appSlug, &flags, installCfg, prompts.New()); err != nil { return err } _ = rc.SetEnv() - if err := runInstallRunPreflights(cmd.Context(), flags, rc); err != nil { + if err := runInstallRunPreflights(cmd.Context(), flags, installCfg, rc); err != nil { return err } @@ -62,22 +62,17 @@ func InstallRunPreflightsCmd(ctx context.Context, appSlug string) *cobra.Command return cmd } -func runInstallRunPreflights(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { - licenseBytes, err := os.ReadFile(flags.licenseFile) - if err != nil { - return fmt.Errorf("unable to read license file: %w", err) - } - +func runInstallRunPreflights(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { logrus.Debugf("configuring host") if err := hostutils.ConfigureHost(ctx, rc, hostutils.InitForInstallOptions{ - License: licenseBytes, + License: installCfg.licenseBytes, AirgapBundle: flags.airgapBundle, }); err != nil { return fmt.Errorf("configure host: %w", err) } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, rc, nil); err != nil { + if err := runInstallPreflights(ctx, flags, installCfg, rc, nil); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } @@ -89,7 +84,7 @@ func runInstallRunPreflights(ctx context.Context, flags InstallCmdFlags, rc runt return nil } -func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, metricsReporter metrics.ReporterInterface) error { +func runInstallPreflights(ctx context.Context, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, metricsReporter metrics.ReporterInterface) error { replicatedAppURL := replicatedAppURL() proxyRegistryURL := proxyRegistryURL() @@ -100,11 +95,11 @@ func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtime // Calculate airgap storage space requirement var controllerAirgapStorageSpace string - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { controllerAirgapStorageSpace = preflights.CalculateAirgapStorageSpace(preflights.AirgapStorageSpaceCalcArgs{ - UncompressedSize: flags.airgapMetadata.AirgapInfo.Spec.UncompressedSize, - EmbeddedAssetsSize: flags.embeddedAssetsSize, - K0sImageSize: flags.airgapMetadata.K0sImageSize, + UncompressedSize: installCfg.airgapMetadata.AirgapInfo.Spec.UncompressedSize, + EmbeddedAssetsSize: installCfg.embeddedAssetsSize, + K0sImageSize: installCfg.airgapMetadata.K0sImageSize, IsController: true, }) } @@ -122,7 +117,7 @@ func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtime PodCIDR: rc.PodCIDR(), ServiceCIDR: rc.ServiceCIDR(), NodeIP: nodeIP, - IsAirgap: flags.isAirgap, + IsAirgap: installCfg.isAirgap, ControllerAirgapStorageSpace: controllerAirgapStorageSpace, } if globalCIDR := rc.GlobalCIDR(); globalCIDR != "" { diff --git a/cmd/installer/cli/install_test.go b/cmd/installer/cli/install_test.go index a7e959b5cf..50499ca744 100644 --- a/cmd/installer/cli/install_test.go +++ b/cmd/installer/cli/install_test.go @@ -11,10 +11,10 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/prompts/plain" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" @@ -111,7 +111,7 @@ func Test_ensureAdminConsolePassword(t *testing.T) { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - flags := &InstallCmdFlags{ + flags := &installFlags{ assumeYes: tt.noPrompt, adminConsolePassword: tt.userPassword, } @@ -335,12 +335,12 @@ func getReleasesHandler(t *testing.T, channelID string, apiHandler http.HandlerF } } -func Test_getLicenseFromFilepath(t *testing.T) { +func Test_verifyLicense(t *testing.T) { tests := []struct { - name string - licenseContents string - wantErr string - useRelease bool + name string + license *kotsv1beta1.License + wantErr string + useRelease bool }{ { name: "no license, no release", @@ -353,136 +353,158 @@ func Test_getLicenseFromFilepath(t *testing.T) { }, { name: "valid license, no release", - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" - isEmbeddedClusterDownloadEnabled: true - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", + IsEmbeddedClusterDownloadEnabled: true, + }, + }, wantErr: "a license was provided but no release was found in binary, please rerun without the license flag", }, { name: "valid license, with release", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" - isEmbeddedClusterDownloadEnabled: true - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", + IsEmbeddedClusterDownloadEnabled: true, + }, + }, }, { name: "valid multi-channel license, with release", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "OtherChannelID" - isEmbeddedClusterDownloadEnabled: true - channels: - - channelID: OtherChannelID - channelName: OtherChannel - channelSlug: other-channel - isDefault: true - - channelID: 2cHXb1RCttzpR0xvnNWyaZCgDBP - channelName: ExpectedChannel - channelSlug: expected-channel - isDefault: false - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "OtherChannelID", + IsEmbeddedClusterDownloadEnabled: true, + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "OtherChannelID", + ChannelName: "OtherChannel", + ChannelSlug: "other-channel", + IsDefault: true, + }, + { + ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", + ChannelName: "ExpectedChannel", + ChannelSlug: "expected-channel", + IsDefault: false, + }, + }, + }, + }, }, { name: "expired license, with release", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" - isEmbeddedClusterDownloadEnabled: true - entitlements: - expires_at: - description: License Expiration - signature: {} - title: Expiration - value: "2024-06-03T00:00:00Z" - valueType: String - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", + IsEmbeddedClusterDownloadEnabled: true, + Entitlements: map[string]kotsv1beta1.EntitlementField{ + "expires_at": { + Value: kotsv1beta1.EntitlementValue{ + Type: kotsv1beta1.String, + StrVal: "2024-06-03T00:00:00Z", + }, + }, + }, + }, + }, wantErr: "license expired on 2024-06-03 00:00:00 +0000 UTC, please provide a valid license", }, { name: "license with no expiration, with release", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" - isEmbeddedClusterDownloadEnabled: true - entitlements: - expires_at: - description: License Expiration - signature: {} - title: Expiration - value: "" - valueType: String - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", + IsEmbeddedClusterDownloadEnabled: true, + Entitlements: map[string]kotsv1beta1.EntitlementField{ + "expires_at": { + Value: kotsv1beta1.EntitlementValue{ + Type: kotsv1beta1.String, + StrVal: "", + }, + }, + }, + }, + }, }, { name: "license with 100 year expiration, with release", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" - isEmbeddedClusterDownloadEnabled: true - entitlements: - expires_at: - description: License Expiration - signature: {} - title: Expiration - value: "2124-06-03T00:00:00Z" - valueType: String - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", + IsEmbeddedClusterDownloadEnabled: true, + Entitlements: map[string]kotsv1beta1.EntitlementField{ + "expires_at": { + Value: kotsv1beta1.EntitlementValue{ + Type: kotsv1beta1.String, + StrVal: "2124-06-03T00:00:00Z", + }, + }, + }, + }, + }, }, { name: "embedded cluster not enabled, with release", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP" - isEmbeddedClusterDownloadEnabled: false - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2cHXb1RCttzpR0xvnNWyaZCgDBP", + IsEmbeddedClusterDownloadEnabled: false, + }, + }, wantErr: "license does not have embedded cluster enabled, please provide a valid license", }, { name: "incorrect license (multichan license)", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK" - isEmbeddedClusterDownloadEnabled: false - channels: - - channelID: 2i9fCbxTNIhuAOaC6MoKMVeGzuK - channelName: Stable - channelSlug: stable - isDefault: true - - channelID: 4l9fCbxTNIhuAOaC6MoKMVeV3K - channelName: Alternate - channelSlug: alternate - isDefault: false - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK", + IsEmbeddedClusterDownloadEnabled: false, + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK", + ChannelName: "Stable", + ChannelSlug: "stable", + IsDefault: true, + }, + { + ChannelID: "4l9fCbxTNIhuAOaC6MoKMVeV3K", + ChannelName: "Alternate", + ChannelSlug: "alternate", + IsDefault: false, + }, + }, + }, + }, wantErr: "binary channel 2cHXb1RCttzpR0xvnNWyaZCgDBP (CI) not present in license, channels allowed by license are: stable (2i9fCbxTNIhuAOaC6MoKMVeGzuK), alternate (4l9fCbxTNIhuAOaC6MoKMVeV3K)", }, { name: "incorrect license (pre-multichan license)", useRelease: true, - licenseContents: ` -spec: - appSlug: embedded-cluster-smoke-test-staging-app - channelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK" - channelName: "Stable" - isEmbeddedClusterDownloadEnabled: false - `, + license: &kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "embedded-cluster-smoke-test-staging-app", + ChannelID: "2i9fCbxTNIhuAOaC6MoKMVeGzuK", + ChannelName: "Stable", + IsEmbeddedClusterDownloadEnabled: false, + }, + }, wantErr: "binary channel 2cHXb1RCttzpR0xvnNWyaZCgDBP (CI) not present in license, channels allowed by license are: Stable (2i9fCbxTNIhuAOaC6MoKMVeGzuK)", }, } @@ -490,15 +512,6 @@ spec: t.Run(tt.name, func(t *testing.T) { req := require.New(t) - tmpdir, err := os.MkdirTemp("", "license") - defer os.RemoveAll(tmpdir) - req.NoError(err) - - licenseFile, err := os.Create(tmpdir + "/license.yaml") - req.NoError(err) - _, err = licenseFile.Write([]byte(tt.licenseContents)) - req.NoError(err) - dataMap := map[string][]byte{} if tt.useRelease { dataMap["release.yaml"] = []byte(` @@ -509,19 +522,14 @@ appSlug: "embedded-cluster-smoke-test-staging-app" versionLabel: testversion `) } - err = release.SetReleaseDataForTests(dataMap) + err := release.SetReleaseDataForTests(dataMap) req.NoError(err) t.Cleanup(func() { release.SetReleaseDataForTests(nil) }) - if tt.licenseContents != "" { - _, err = getLicenseFromFilepath(filepath.Join(tmpdir, "license.yaml")) - } else { - _, err = getLicenseFromFilepath("") - } - + _, err = verifyLicense(tt.license) if tt.wantErr != "" { req.EqualError(err, tt.wantErr) } else { @@ -610,100 +618,6 @@ func Test_verifyProxyConfig(t *testing.T) { } } -func Test_preRunInstall_SkipHostPreflightsEnvVar(t *testing.T) { - tests := []struct { - name string - envVarValue string - flagValue *bool // nil means not set, true/false means explicitly set - expectedSkipPreflights bool - }{ - { - name: "env var set to 1, no flag", - envVarValue: "1", - flagValue: nil, - expectedSkipPreflights: true, - }, - { - name: "env var set to true, no flag", - envVarValue: "true", - flagValue: nil, - expectedSkipPreflights: true, - }, - { - name: "env var set, flag explicitly false (flag takes precedence)", - envVarValue: "1", - flagValue: boolPtr(false), - expectedSkipPreflights: false, - }, - { - name: "env var set, flag explicitly true", - envVarValue: "1", - flagValue: boolPtr(true), - expectedSkipPreflights: true, - }, - { - name: "env var not set, no flag", - envVarValue: "", - flagValue: nil, - expectedSkipPreflights: false, - }, - { - name: "env var not set, flag explicitly false", - envVarValue: "", - flagValue: boolPtr(false), - expectedSkipPreflights: false, - }, - { - name: "env var not set, flag explicitly true", - envVarValue: "", - flagValue: boolPtr(true), - expectedSkipPreflights: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Set up environment variable - if tt.envVarValue != "" { - t.Setenv("SKIP_HOST_PREFLIGHTS", tt.envVarValue) - } - - // Create a mock cobra command to simulate flag behavior - cmd := &cobra.Command{} - flags := &InstallCmdFlags{} - - // Add the flag to the command (similar to addInstallFlags) - cmd.Flags().BoolVar(&flags.skipHostPreflights, "skip-host-preflights", false, "Skip host preflight checks") - - // Set the flag if explicitly provided in test - if tt.flagValue != nil { - err := cmd.Flags().Set("skip-host-preflights", fmt.Sprintf("%t", *tt.flagValue)) - require.NoError(t, err) - } - - // Create a minimal runtime config for the test - rc := runtimeconfig.New(nil) - - // Call preRunInstall (this would normally require root, but we're just testing the flag logic) - // We expect this to fail due to non-root execution, but we can check the flag value before it fails - err := preRunInstallLinux(cmd, flags, rc) - - // The function will fail due to non-root check, but we can verify the flag was set correctly - // by checking the flag value before the root check fails - assert.Equal(t, tt.expectedSkipPreflights, flags.skipHostPreflights) - - // We expect an error due to non-root execution - assert.Error(t, err) - assert.Contains(t, err.Error(), "install command must be run as root") - }) - } -} - -// Helper function to create bool pointer -func boolPtr(b bool) *bool { - return &b -} - func Test_ignoreAppPreflights_FlagVisibility(t *testing.T) { tests := []struct { name string @@ -732,7 +646,7 @@ func Test_ignoreAppPreflights_FlagVisibility(t *testing.T) { t.Setenv("ENABLE_V3", tt.enableV3EnvVar) } - flags := &InstallCmdFlags{} + flags := &installFlags{} enableV3 := isV3Enabled() flagSet := newLinuxInstallFlags(flags, enableV3) @@ -797,7 +711,7 @@ func Test_ignoreAppPreflights_FlagParsing(t *testing.T) { } // Create a flagset similar to how newLinuxInstallFlags works - flags := &InstallCmdFlags{} + flags := &installFlags{} flagSet := newLinuxInstallFlags(flags, tt.enableV3) // Create a command to test flag parsing @@ -820,7 +734,223 @@ func Test_ignoreAppPreflights_FlagParsing(t *testing.T) { } } -func Test_processTLSConfig(t *testing.T) { +func Test_k0sConfigFromFlags(t *testing.T) { + tests := []struct { + name string + podCIDR string + serviceCIDR string + globalCIDR *string + expectedPodCIDR string + expectedServiceCIDR string + wantErr bool + }{ + { + name: "pod and service CIDRs set", + podCIDR: "10.0.0.0/24", + serviceCIDR: "10.1.0.0/24", + globalCIDR: nil, + expectedPodCIDR: "10.0.0.0/24", + expectedServiceCIDR: "10.1.0.0/24", + wantErr: false, + }, + { + name: "custom pod and service CIDRs", + podCIDR: "192.168.0.0/16", + serviceCIDR: "10.96.0.0/12", + globalCIDR: nil, + expectedPodCIDR: "192.168.0.0/16", + expectedServiceCIDR: "10.96.0.0/12", + wantErr: false, + }, + { + name: "global CIDR should not affect k0s config", + podCIDR: "10.0.0.0/25", + serviceCIDR: "10.0.0.128/25", + globalCIDR: stringPtr("10.0.0.0/24"), + expectedPodCIDR: "10.0.0.0/25", + expectedServiceCIDR: "10.0.0.128/25", + wantErr: false, + }, + { + name: "IPv4 CIDRs with different masks", + podCIDR: "172.16.0.0/20", + serviceCIDR: "172.17.0.0/20", + globalCIDR: nil, + expectedPodCIDR: "172.16.0.0/20", + expectedServiceCIDR: "172.17.0.0/20", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + + flags := &installFlags{ + cidrConfig: &newconfig.CIDRConfig{ + PodCIDR: tt.podCIDR, + ServiceCIDR: tt.serviceCIDR, + GlobalCIDR: tt.globalCIDR, + }, + networkInterface: "", + overrides: "", + } + installCfg := &installConfig{} + + cfg, err := k0sConfigFromFlags(flags, installCfg) + + if tt.wantErr { + req.Error(err) + return + } + + req.NoError(err) + req.NotNil(cfg) + req.NotNil(cfg.Spec) + req.NotNil(cfg.Spec.Network) + + // Verify pod CIDR is set correctly if expected + if tt.expectedPodCIDR != "" { + req.Equal(tt.expectedPodCIDR, cfg.Spec.Network.PodCIDR, + "Pod CIDR should be set correctly in k0s config") + } + + // Verify service CIDR is set correctly if expected + if tt.expectedServiceCIDR != "" { + req.Equal(tt.expectedServiceCIDR, cfg.Spec.Network.ServiceCIDR, + "Service CIDR should be set correctly in k0s config") + } + }) + } +} + +func stringPtr(s string) *string { + return &s +} + +func Test_buildInstallConfig_License(t *testing.T) { + // Create a temporary directory for test license files + tmpdir := t.TempDir() + + // Valid test license data (YAML format for a kotsv1beta1.License) + validLicenseData := `apiVersion: kots.io/v1beta1 +kind: License +metadata: + name: test-license +spec: + licenseID: test-license-id + appSlug: test-app + channelID: test-channel-id + channelName: Test Channel + customerName: Test Customer + endpoint: https://replicated.app + entitlements: + expires_at: + title: Expiration + value: "2030-01-01T00:00:00Z" + valueType: String + isEmbeddedClusterDownloadEnabled: true` + + // Create a valid license file + validLicensePath := filepath.Join(tmpdir, "valid-license.yaml") + err := os.WriteFile(validLicensePath, []byte(validLicenseData), 0644) + require.NoError(t, err) + + tests := []struct { + name string + licenseFile string + wantErr string + expectLicense bool + }{ + { + name: "no license file provided", + licenseFile: "", + wantErr: "", + expectLicense: false, + }, + { + name: "license file does not exist", + licenseFile: filepath.Join(tmpdir, "nonexistent.yaml"), + wantErr: "failed to read license file", + expectLicense: false, + }, + { + name: "invalid license file - not YAML", + licenseFile: func() string { + invalidPath := filepath.Join(tmpdir, "invalid-license.txt") + os.WriteFile(invalidPath, []byte("this is not a valid license file"), 0644) + return invalidPath + }(), + wantErr: "failed to parse the license file", + expectLicense: false, + }, + { + name: "invalid license file - wrong kind", + licenseFile: func() string { + wrongKindPath := filepath.Join(tmpdir, "wrong-kind.yaml") + wrongKindData := `apiVersion: v1 +kind: ConfigMap +metadata: + name: not-a-license` + os.WriteFile(wrongKindPath, []byte(wrongKindData), 0644) + return wrongKindPath + }(), + wantErr: "failed to parse the license file", + expectLicense: false, + }, + { + name: "corrupt license file - invalid YAML", + licenseFile: func() string { + corruptPath := filepath.Join(tmpdir, "corrupt-license.yaml") + corruptData := `apiVersion: kots.io/v1beta1 +kind: License +metadata: + name: test +spec: + this is not valid yaml: [[[` + os.WriteFile(corruptPath, []byte(corruptData), 0644) + return corruptPath + }(), + wantErr: "failed to parse the license file", + expectLicense: false, + }, + { + name: "valid license file", + licenseFile: validLicensePath, + wantErr: "", + expectLicense: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flags := &installFlags{ + licenseFile: tt.licenseFile, + } + + installCfg, err := buildInstallConfig(flags) + + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + require.NoError(t, err) + + if tt.expectLicense { + assert.NotEmpty(t, installCfg.licenseBytes, "License bytes should be populated") + assert.NotNil(t, installCfg.license, "License should be parsed") + assert.Equal(t, "test-license-id", installCfg.license.Spec.LicenseID) + assert.Equal(t, "test-app", installCfg.license.Spec.AppSlug) + } else { + assert.Empty(t, installCfg.licenseBytes, "License bytes should be empty") + assert.Nil(t, installCfg.license, "License should be nil") + } + } + }) + } +} + +func Test_buildInstallConfig_TLS(t *testing.T) { // Create a temporary directory for test certificates tmpdir := t.TempDir() @@ -899,32 +1029,18 @@ oxhVqyhpk86rf0rT5DcD/sBw wantErr: "", expectTLS: false, }, - { - name: "only cert file provided", - tlsCertFile: certPath, - tlsKeyFile: "", - wantErr: "both --tls-cert and --tls-key must be provided together", - expectTLS: false, - }, - { - name: "only key file provided", - tlsCertFile: "", - tlsKeyFile: keyPath, - wantErr: "both --tls-cert and --tls-key must be provided together", - expectTLS: false, - }, { name: "cert file does not exist", tlsCertFile: filepath.Join(tmpdir, "nonexistent.pem"), tlsKeyFile: keyPath, - wantErr: "load tls certificate", + wantErr: "failed to read TLS certificate", expectTLS: false, }, { name: "key file does not exist", tlsCertFile: certPath, tlsKeyFile: filepath.Join(tmpdir, "nonexistent.key"), - wantErr: "load tls certificate", + wantErr: "failed to read TLS key", expectTLS: false, }, { @@ -935,7 +1051,7 @@ oxhVqyhpk86rf0rT5DcD/sBw return invalidCertPath }(), tlsKeyFile: keyPath, - wantErr: "load tls certificate", + wantErr: "failed to parse TLS certificate", expectTLS: false, }, { @@ -949,26 +1065,27 @@ oxhVqyhpk86rf0rT5DcD/sBw for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - flags := &InstallCmdFlags{ + flags := &installFlags{ tlsCertFile: tt.tlsCertFile, tlsKeyFile: tt.tlsKeyFile, } - err := processTLSConfig(flags) + installCfg, err := buildInstallConfig(flags) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) } else { require.NoError(t, err) - } - if tt.expectTLS { - assert.NotEmpty(t, flags.tlsCertBytes, "TLS cert bytes should be populated") - assert.NotEmpty(t, flags.tlsKeyBytes, "TLS key bytes should be populated") - } else { - assert.Empty(t, flags.tlsCertBytes, "TLS cert bytes should be empty") - assert.Empty(t, flags.tlsKeyBytes, "TLS key bytes should be empty") + if tt.expectTLS { + assert.NotEmpty(t, installCfg.tlsCertBytes, "TLS cert bytes should be populated") + assert.NotEmpty(t, installCfg.tlsKeyBytes, "TLS key bytes should be populated") + assert.NotNil(t, installCfg.tlsCert.Certificate, "TLS cert should be loaded") + } else { + assert.Empty(t, installCfg.tlsCertBytes, "TLS cert bytes should be empty") + assert.Empty(t, installCfg.tlsKeyBytes, "TLS key bytes should be empty") + } } }) } diff --git a/cmd/installer/cli/lint.go b/cmd/installer/cli/lint.go new file mode 100644 index 0000000000..771421a2f1 --- /dev/null +++ b/cmd/installer/cli/lint.go @@ -0,0 +1,124 @@ +package cli + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/replicatedhq/embedded-cluster/pkg/lint" + "github.com/spf13/cobra" +) + +// LintCmd creates a hidden command for linting embedded cluster configuration files +func LintCmd(ctx context.Context) *cobra.Command { + var verbose bool + var outputFormat string + + cmd := &cobra.Command{ + Use: "lint [flags] [file...]", + Short: "Lint embedded cluster configuration files", + Hidden: true, // Hidden command as requested + Long: `Lint embedded cluster configuration files to validate: +- YAML syntax (duplicate keys, unclosed quotes, invalid structure) +- Port specifications in unsupportedOverrides that are already supported +- Custom domains against the Replicated app's configured domains + +Environment variables (optional for custom domain validation): +- REPLICATED_API_TOKEN: Authentication token for Replicated API +- REPLICATED_API_ORIGIN: API endpoint (e.g., https://api.replicated.com/vendor) +- REPLICATED_APP: Application ID or slug`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + // Validate output format + if outputFormat != "text" && outputFormat != "json" { + return fmt.Errorf("invalid output format %q: must be 'text' or 'json'", outputFormat) + } + + // Get environment variables + apiToken := os.Getenv("REPLICATED_API_TOKEN") + apiOrigin := os.Getenv("REPLICATED_API_ORIGIN") + appID := os.Getenv("REPLICATED_APP") + + // Create validator with verbose flag + validator := lint.NewValidator(apiToken, apiOrigin, appID) + validator.SetVerbose(verbose && outputFormat != "json") // Disable verbose in JSON mode + + // For JSON output, accumulate all results + var jsonResults lint.JSONOutput + hasErrors := false + + // Validate each file + for _, file := range args { + if outputFormat != "json" { + fmt.Printf("Linting %s...\n", file) + } + + result, err := validator.ValidateFile(file) + if err != nil { + if outputFormat == "json" { + // Add as a file with error + jsonResults.Files = append(jsonResults.Files, lint.FileResult{ + Path: file, + Valid: false, + Errors: []lint.ValidationIssue{{Field: "", Message: err.Error()}}, + }) + } else { + fmt.Fprintf(os.Stderr, "ERROR: Failed to validate %s: %v\n", file, err) + } + hasErrors = true + continue + } + + if outputFormat == "json" { + // Add to JSON results + jsonResults.Files = append(jsonResults.Files, result.ToJSON(file)) + if len(result.Errors) > 0 { + hasErrors = true + } + } else { + // Display warnings + for _, warning := range result.Warnings { + fmt.Fprintf(os.Stderr, "WARNING: %s: %s\n", file, warning) + } + + // Display errors + for _, validationErr := range result.Errors { + fmt.Fprintf(os.Stderr, "ERROR: %s: %s\n", file, validationErr) + hasErrors = true + } + + // Display result + if len(result.Errors) == 0 && len(result.Warnings) == 0 { + fmt.Printf("✓ %s passed validation\n", file) + } else if len(result.Errors) == 0 && len(result.Warnings) > 0 { + fmt.Printf("âš  %s passed with %d warning(s)\n", file, len(result.Warnings)) + } else { + fmt.Printf("✗ %s failed validation\n", file) + } + } + } + + // Output JSON if requested + if outputFormat == "json" { + output, err := json.MarshalIndent(jsonResults, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON output: %w", err) + } + fmt.Println(string(output)) + } + + // Only fail if there are errors (not warnings) + if hasErrors { + return fmt.Errorf("validation failed with errors") + } + + return nil + }, + } + + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output showing API endpoints and detailed information") + cmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "Output format: text or json") + + return cmd +} diff --git a/cmd/installer/cli/proxy.go b/cmd/installer/cli/proxy.go index 5573e9afce..87c87b6c33 100644 --- a/cmd/installer/cli/proxy.go +++ b/cmd/installer/cli/proxy.go @@ -30,8 +30,8 @@ func mustAddProxyFlags(flagSet *pflag.FlagSet) { flagSet.String("no-proxy", "", "Comma-separated list of hosts for which not to use a proxy (overrides no_proxy/NO_PROXY environment variables)") } -func parseProxyFlags(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { - p, err := getProxySpec(cmd) +func parseProxyFlags(cmd *cobra.Command, networkInterface string, cidrCfg *newconfig.CIDRConfig) (*ecv1beta1.ProxySpec, error) { + p, err := getProxySpec(cmd, networkInterface, cidrCfg) if err != nil { return nil, fmt.Errorf("unable to get proxy spec from flags: %w", err) } @@ -40,7 +40,7 @@ func parseProxyFlags(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { return p, nil } -func getProxySpec(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { +func getProxySpec(cmd *cobra.Command, networkInterface string, cidrCfg *newconfig.CIDRConfig) (*ecv1beta1.ProxySpec, error) { // Command-line flags have the highest precedence httpProxy, err := cmd.Flags().GetString("http-proxy") if err != nil { @@ -54,14 +54,6 @@ func getProxySpec(cmd *cobra.Command) (*ecv1beta1.ProxySpec, error) { if err != nil { return nil, fmt.Errorf("unable to get no-proxy flag: %w", err) } - networkInterface, err := cmd.Flags().GetString("network-interface") - if err != nil { - return nil, fmt.Errorf("unable to get network-interface flag: %w", err) - } - cidrCfg, err := getCIDRConfig(cmd) - if err != nil { - return nil, fmt.Errorf("unable to determine pod and service CIDRs: %w", err) - } proxy, err := newconfig.GetProxySpec(httpProxy, httpsProxy, noProxy, cidrCfg.PodCIDR, cidrCfg.ServiceCIDR, networkInterface, defaultNetworkLookupImpl) if err != nil { return nil, fmt.Errorf("unable to get proxy spec: %w", err) diff --git a/cmd/installer/cli/proxy_test.go b/cmd/installer/cli/proxy_test.go deleted file mode 100644 index d6e320b88a..0000000000 --- a/cmd/installer/cli/proxy_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package cli - -import ( - "net" - "testing" - - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/stretchr/testify/assert" -) - -// Mock network interface for testing -type mockNetworkLookup struct{} - -func (m *mockNetworkLookup) FirstValidIPNet(networkInterface string) (*net.IPNet, error) { - _, ipnet, _ := net.ParseCIDR("192.168.1.0/24") - return ipnet, nil -} - -func Test_getProxySpecFromFlags(t *testing.T) { - tests := []struct { - name string - init func(t *testing.T, flagSet *pflag.FlagSet) - want *ecv1beta1.ProxySpec - }{ - { - name: "no flags set and no env vars should not set proxy", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - // No env vars, no flags - }, - want: nil, - }, - { - name: "lowercase env vars should be used when no flags set", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - t.Setenv("https_proxy", "https://lower-proxy") - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://lower-proxy", - HTTPSProxy: "https://lower-proxy", - ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "uppercase env vars should be used when no flags set and no lowercase vars", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("HTTP_PROXY", "http://upper-proxy") - t.Setenv("HTTPS_PROXY", "https://upper-proxy") - t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://upper-proxy", - HTTPSProxy: "https://upper-proxy", - ProvidedNoProxy: "upper-no-proxy-1,upper-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,upper-no-proxy-1,upper-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "lowercase should take precedence over uppercase", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - t.Setenv("https_proxy", "https://lower-proxy") - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - t.Setenv("HTTP_PROXY", "http://upper-proxy") - t.Setenv("HTTPS_PROXY", "https://upper-proxy") - t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://lower-proxy", - HTTPSProxy: "https://lower-proxy", - ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "proxy flags should override env vars", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - t.Setenv("https_proxy", "https://lower-proxy") - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - t.Setenv("HTTP_PROXY", "http://upper-proxy") - t.Setenv("HTTPS_PROXY", "https://upper-proxy") - t.Setenv("NO_PROXY", "upper-no-proxy-1,upper-no-proxy-2") - - flagSet.Set("http-proxy", "http://flag-proxy") - flagSet.Set("https-proxy", "https://flag-proxy") - flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://flag-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "pod and service CIDR should override default no proxy", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - flagSet.Set("http-proxy", "http://flag-proxy") - flagSet.Set("https-proxy", "https://flag-proxy") - flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") - - flagSet.Set("pod-cidr", "1.1.1.1/24") - flagSet.Set("service-cidr", "2.2.2.2/24") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://flag-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,1.1.1.1/24,2.2.2.2/24,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "custom --cidr should be present in the no-proxy", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - flagSet.Set("http-proxy", "http://flag-proxy") - flagSet.Set("https-proxy", "https://flag-proxy") - flagSet.Set("no-proxy", "flag-no-proxy-1,flag-no-proxy-2") - - flagSet.Set("cidr", "10.0.0.0/16") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://flag-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "flag-no-proxy-1,flag-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.0.0.0/17,10.0.128.0/17,flag-no-proxy-1,flag-no-proxy-2,192.168.1.0/24", - }, - }, - { - name: "partial env vars with partial flag vars", - init: func(t *testing.T, flagSet *pflag.FlagSet) { - t.Setenv("http_proxy", "http://lower-proxy") - // No https_proxy set - t.Setenv("no_proxy", "lower-no-proxy-1,lower-no-proxy-2") - - // Only set https-proxy flag - flagSet.Set("https-proxy", "https://flag-proxy") - }, - want: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://lower-proxy", - HTTPSProxy: "https://flag-proxy", - ProvidedNoProxy: "lower-no-proxy-1,lower-no-proxy-2", - NoProxy: "localhost,127.0.0.1,.cluster.local,.svc,169.254.169.254,10.244.0.0/17,10.244.128.0/17,lower-no-proxy-1,lower-no-proxy-2,192.168.1.0/24", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmd := &cobra.Command{} - mustAddCIDRFlags(cmd.Flags()) - mustAddProxyFlags(cmd.Flags()) - cmd.Flags().String("network-interface", "", "The network interface to use for the cluster") - - flagSet := cmd.Flags() - if tt.init != nil { - tt.init(t, flagSet) - } - - // Override the network lookup with our mock - defaultNetworkLookupImpl = &mockNetworkLookup{} - - got, err := getProxySpec(cmd) - assert.NoError(t, err, "unexpected error received") - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/cmd/installer/cli/replicatedapi.go b/cmd/installer/cli/replicatedapi.go new file mode 100644 index 0000000000..097a333e7f --- /dev/null +++ b/cmd/installer/cli/replicatedapi.go @@ -0,0 +1,50 @@ +package cli + +import ( + "context" + "fmt" + + "github.com/replicatedhq/embedded-cluster/pkg-new/replicatedapi" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "github.com/replicatedhq/embedded-cluster/pkg/release" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/sirupsen/logrus" +) + +func replicatedAppURL() string { + domains := getDomains() + return netutils.MaybeAddHTTPS(domains.ReplicatedAppDomain) +} + +func proxyRegistryURL() string { + domains := getDomains() + return netutils.MaybeAddHTTPS(domains.ProxyRegistryDomain) +} + +func newReplicatedAPIClient(license *kotsv1beta1.License, clusterID string) (replicatedapi.Client, error) { + return replicatedapi.NewClient( + replicatedAppURL(), + license, + release.GetReleaseData(), + replicatedapi.WithClusterID(clusterID), + ) +} + +func syncLicense(ctx context.Context, client replicatedapi.Client, license *kotsv1beta1.License) (*kotsv1beta1.License, []byte, error) { + logrus.Debug("Syncing license") + + updatedLicense, licenseBytes, err := client.SyncLicense(ctx) + if err != nil { + return nil, nil, fmt.Errorf("get latest license: %w", err) + } + + if updatedLicense.Spec.LicenseSequence != license.Spec.LicenseSequence { + logrus.Debugf("License synced successfully (sequence %d -> %d)", + license.Spec.LicenseSequence, + updatedLicense.Spec.LicenseSequence) + } else { + logrus.Debug("License is already up to date") + } + + return updatedLicense, licenseBytes, nil +} diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index bcc16c5c44..8fa9e8edae 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -87,7 +87,7 @@ const ( ) func RestoreCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { - var flags InstallCmdFlags + var flags installFlags var s3Store s3BackupStore var skipStoreValidation bool @@ -102,13 +102,14 @@ func RestoreCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc, ki); err != nil { + installCfg, err := preRunInstall(cmd, &flags, rc, ki) + if err != nil { return err } _ = rc.SetEnv() - if err := runRestore(cmd.Context(), appSlug, appTitle, flags, rc, s3Store, skipStoreValidation); err != nil { + if err := runRestore(cmd.Context(), appSlug, appTitle, flags, installCfg, rc, s3Store, skipStoreValidation); err != nil { return err } @@ -124,15 +125,15 @@ func RestoreCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command { return cmd } -func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, s3Store s3BackupStore, skipStoreValidation bool) error { - err := verifyChannelRelease("restore", flags.isAirgap, flags.assumeYes) +func runRestore(ctx context.Context, appSlug, appTitle string, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, s3Store s3BackupStore, skipStoreValidation bool) error { + err := verifyChannelRelease("restore", installCfg.isAirgap, flags.assumeYes) if err != nil { return err } - if flags.airgapMetadata != nil && flags.airgapMetadata.AirgapInfo != nil { + if installCfg.airgapMetadata != nil && installCfg.airgapMetadata.AirgapInfo != nil { logrus.Debugf("checking airgap bundle matches binary") - if err := checkAirgapMatches(flags.airgapMetadata.AirgapInfo); err != nil { + if err := checkAirgapMatches(installCfg.airgapMetadata.AirgapInfo); err != nil { return err // we want the user to see the error message without a prefix } } @@ -157,7 +158,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF if state != ecRestoreStateNew { logrus.Debugf("getting backup from restore state") var err error - backupToRestore, err = getBackupFromRestoreState(ctx, flags.isAirgap, rc) + backupToRestore, err = getBackupFromRestoreState(ctx, installCfg.isAirgap, rc) if err != nil { return fmt.Errorf("unable to resume: %w", err) } @@ -190,7 +191,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF switch state { case ecRestoreStateNew: - err = runRestoreStepNew(ctx, appSlug, appTitle, flags, rc, &s3Store, skipStoreValidation) + err = runRestoreStepNew(ctx, appSlug, appTitle, flags, installCfg, rc, &s3Store, skipStoreValidation) if err != nil { return err } @@ -204,7 +205,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - backup, ok, err := runRestoreStepConfirmBackup(ctx, flags, rc) + backup, ok, err := runRestoreStepConfirmBackup(ctx, installCfg, rc) if err != nil { return err } else if !ok { @@ -263,7 +264,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreSeaweedFS(ctx, flags, backupToRestore) + err = runRestoreSeaweedFS(ctx, installCfg, backupToRestore) if err != nil { return err } @@ -277,7 +278,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreRegistry(ctx, flags, backupToRestore) + err = runRestoreRegistry(ctx, installCfg, backupToRestore) if err != nil { return err } @@ -291,7 +292,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreEnableAdminConsoleHA(ctx, flags, rc, backupToRestore) + err = runRestoreEnableAdminConsoleHA(ctx, installCfg, rc, backupToRestore) if err != nil { return err } @@ -319,7 +320,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreExtensions(ctx, flags, rc) + err = runRestoreExtensions(ctx, installCfg, rc) if err != nil { return err } @@ -345,7 +346,7 @@ func runRestore(ctx context.Context, appSlug, appTitle string, flags InstallCmdF return nil } -func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, s3Store *s3BackupStore, skipStoreValidation bool) error { +func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags installFlags, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, s3Store *s3BackupStore, skipStoreValidation bool) error { logrus.Debugf("checking if k0s is already installed") err := verifyNoInstallation(appSlug, "restore") if err != nil { @@ -377,14 +378,14 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, rc, nil); err != nil { + if err := runInstallPreflights(ctx, flags, installCfg, rc, nil); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } return fmt.Errorf("unable to run install preflights: %w", err) } - _, err = installAndStartCluster(ctx, flags, rc, nil) + _, err = installAndStartCluster(ctx, flags, installCfg, rc, nil) if err != nil { return err } @@ -400,7 +401,7 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst } airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } @@ -421,7 +422,7 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst // TODO (@salah): update installation status to reflect what's happening logrus.Debugf("installing addons") - if err := installAddonsForRestore(ctx, kcli, mcli, hcli, rc, flags); err != nil { + if err := installAddonsForRestore(ctx, kcli, mcli, hcli, rc); err != nil { return err } @@ -451,7 +452,7 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst return nil } -func installAddonsForRestore(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, flags InstallCmdFlags) error { +func installAddonsForRestore(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig) error { embCfg := release.GetEmbeddedClusterConfig() var embCfgSpec *ecv1beta1.ConfigSpec if embCfg != nil { @@ -500,7 +501,7 @@ func installAddonsForRestore(ctx context.Context, kcli client.Client, mcli metad return nil } -func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) (*disasterrecovery.ReplicatedBackup, bool, error) { +func runRestoreStepConfirmBackup(ctx context.Context, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) (*disasterrecovery.ReplicatedBackup, bool, error) { kcli, err := kubeutils.KubeClient() if err != nil { return nil, false, fmt.Errorf("unable to create kube client: %w", err) @@ -512,7 +513,7 @@ func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags, rc } logrus.Debugf("waiting for backups to become available") - backups, err := waitForBackups(ctx, os.Stdout, kcli, k0sCfg, rc, flags.isAirgap) + backups, err := waitForBackups(ctx, os.Stdout, kcli, k0sCfg, rc, installCfg.isAirgap) if err != nil { return nil, false, err } @@ -564,7 +565,7 @@ func runRestoreAdminConsole(ctx context.Context, backupToRestore *disasterrecove return nil } -func runRestoreWaitForNodes(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreWaitForNodes(ctx context.Context, flags installFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { logrus.Debugf("checking if backup is high availability") highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { @@ -580,7 +581,7 @@ func runRestoreWaitForNodes(ctx context.Context, flags InstallCmdFlags, rc runti return nil } -func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreEnableAdminConsoleHA(ctx context.Context, installCfg *installConfig, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { return err @@ -609,17 +610,13 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, } airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } - euCfg, err := helpers.ParseEndUserConfig(flags.overrides) - if err != nil { - return fmt.Errorf("parse end user config: %w", err) - } var euCfgSpec *ecv1beta1.ConfigSpec - if euCfg != nil { - euCfgSpec = &euCfg.Spec + if installCfg.endUserConfig != nil { + euCfgSpec = &installCfg.endUserConfig.Spec } hcli, err := helm.NewClient(helm.HelmOptions{ @@ -672,11 +669,11 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, return nil } -func runRestoreSeaweedFS(ctx context.Context, flags InstallCmdFlags, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreSeaweedFS(ctx context.Context, installCfg *installConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { return err - } else if !flags.isAirgap || !highAvailability { + } else if !installCfg.isAirgap || !highAvailability { // only restore seaweedfs in case of high availability and airgap return nil } @@ -689,9 +686,9 @@ func runRestoreSeaweedFS(ctx context.Context, flags InstallCmdFlags, backupToRes return nil } -func runRestoreRegistry(ctx context.Context, flags InstallCmdFlags, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreRegistry(ctx context.Context, installCfg *installConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { // only restore registry in case of airgap - if !flags.isAirgap { + if !installCfg.isAirgap { return nil } @@ -721,9 +718,9 @@ func runRestoreECO(ctx context.Context, backupToRestore *disasterrecovery.Replic return nil } -func runRestoreExtensions(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func runRestoreExtensions(ctx context.Context, installCfg *installConfig, rc runtimeconfig.RuntimeConfig) error { airgapChartsPath := "" - if flags.isAirgap { + if installCfg.isAirgap { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } diff --git a/cmd/installer/cli/root.go b/cmd/installer/cli/root.go index 647d3ea303..5ad0f0eaad 100644 --- a/cmd/installer/cli/root.go +++ b/cmd/installer/cli/root.go @@ -121,6 +121,7 @@ func RootCmd(ctx context.Context) *cobra.Command { cmd.AddCommand(RestoreCmd(ctx, appSlug, appTitle)) cmd.AddCommand(AdminConsoleCmd(ctx, appTitle)) cmd.AddCommand(SupportBundleCmd(ctx)) + cmd.AddCommand(LintCmd(ctx)) return cmd } diff --git a/cmd/installer/cli/upgrade.go b/cmd/installer/cli/upgrade.go index 3aba509bd7..53c9a1fffc 100644 --- a/cmd/installer/cli/upgrade.go +++ b/cmd/installer/cli/upgrade.go @@ -236,18 +236,39 @@ func preRunUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeConfig *up return fmt.Errorf("failed to stat data directory: %w", err) } - // Validate the license is indeed a license file - license, err := getLicenseFromFilepath(flags.licenseFile) - if err != nil { - return err - } - upgradeConfig.license = license data, err := os.ReadFile(flags.licenseFile) if err != nil { return fmt.Errorf("failed to read license file: %w", err) } upgradeConfig.licenseBytes = data + // validate the license is indeed a license file + l, err := helpers.ParseLicenseFromBytes(data) + if err != nil { + var notALicenseFileErr helpers.ErrNotALicenseFile + if errors.As(err, ¬ALicenseFileErr) { + return fmt.Errorf("failed to parse the license file at %q, please ensure it is not corrupt: %w", flags.licenseFile, err) + } + + return fmt.Errorf("failed to parse license file: %w", err) + } + upgradeConfig.license = l + + // sync the license if a license is provided and we are not in airgap mode + if upgradeConfig.license != nil && flags.airgapBundle == "" { + replicatedAPI, err := newReplicatedAPIClient(upgradeConfig.license, upgradeConfig.clusterID) + if err != nil { + return fmt.Errorf("failed to create replicated API client: %w", err) + } + + updatedLicense, licenseBytes, err := syncLicense(ctx, replicatedAPI, upgradeConfig.license) + if err != nil { + return fmt.Errorf("failed to sync license: %w", err) + } + upgradeConfig.license = updatedLicense + upgradeConfig.licenseBytes = licenseBytes + } + // Continue using "kotsadm" namespace if it exists for backwards compatibility, otherwise use the appSlug ns, err := runtimeconfig.KotsadmNamespace(ctx, kcli) if err != nil { diff --git a/e2e/install_test.go b/e2e/install_test.go index 6a72a883e0..1a286a6184 100644 --- a/e2e/install_test.go +++ b/e2e/install_test.go @@ -13,7 +13,6 @@ import ( "github.com/replicatedhq/embedded-cluster/e2e/cluster/cmx" "github.com/replicatedhq/embedded-cluster/e2e/cluster/docker" - "github.com/replicatedhq/embedded-cluster/e2e/cluster/lxd" ) func TestSingleNodeInstallation(t *testing.T) { @@ -67,262 +66,6 @@ func TestSingleNodeInstallation(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -func TestSingleNodeInstallationAlmaLinux8(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "almalinux-8", - LicensePath: "licenses/multi-node-disabled-license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - - t.Logf("%s: installing tar", time.Now().Format(time.RFC3339)) - line := []string{"yum-install-tar.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check postupgrade state: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: configuring firewalld", time.Now().Format(time.RFC3339)) - line = []string{"firewalld-configure.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to configure firewalld: %v: %s: %s", err, stdout, stderr) - } - - installSingleNode(t, tc) - - isMultiNodeEnabled := "false" - testArgs := []string{isMultiNodeEnabled} - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - checkNodeJoinCommand(t, tc, 0) - - t.Logf("%s: validating firewalld", time.Now().Format(time.RFC3339)) - line = []string{"firewalld-validate.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to validate firewalld: %v: %s: %s", err, stdout, stderr) - } - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs = []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: resetting firewalld", time.Now().Format(time.RFC3339)) - line = []string{"firewalld-reset.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to reset firewalld: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestSingleNodeInstallationDebian12(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - LicensePath: "licenses/multi-node-disabled-license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - - installSingleNode(t, tc) - - isMultiNodeEnabled := "false" - testArgs := []string{isMultiNodeEnabled} - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - checkNodeJoinCommand(t, tc, 0) - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs = []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestSingleNodeInstallationDebian11(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bullseye", - LicensePath: "licenses/multi-node-disabled-license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - - installSingleNode(t, tc) - - isMultiNodeEnabled := "false" - testArgs := []string{isMultiNodeEnabled} - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - checkNodeJoinCommand(t, tc, 0) - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs = []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestSingleNodeInstallationCentos9Stream(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "centos-9", - LicensePath: "licenses/multi-node-disabled-license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - - t.Logf("%s: installing tar", time.Now().Format(time.RFC3339)) - line := []string{"yum-install-tar.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check postupgrade state: %v: %s: %s", err, stdout, stderr) - } - - installSingleNode(t, tc) - - isMultiNodeEnabled := "false" - testArgs := []string{isMultiNodeEnabled} - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - checkNodeJoinCommand(t, tc, 0) - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs = []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestHostPreflightCustomSpec(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "centos-9", - }) - defer tc.Cleanup() - - t.Logf("%s: installing test dependencies on node 0", time.Now().Format(time.RFC3339)) - line := []string{"yum", "install", "-y", "fio"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to install dependencies on node 0: %v: %s: %s", err, stdout, stderr) - } - - downloadECReleaseWithOptions(t, tc, 0, downloadECReleaseOptions{ - version: fmt.Sprintf("appver-%s-failing-preflights", os.Getenv("SHORT_SHA")), - }) - - t.Logf("%s: moving embedded-cluster to /usr/local/bin/embedded-cluster-failing-preflights", time.Now().Format(time.RFC3339)) - line = []string{"mv", "/usr/local/bin/embedded-cluster", "/usr/local/bin/embedded-cluster-failing-preflights"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to move embedded-cluster on node 0: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: removing the original license file", time.Now().Format(time.RFC3339)) - line = []string{"rm", "/assets/license.yaml"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to remove /assets/license.yaml on node 0: %v: %s: %s", err, stdout, stderr) - } - - downloadECReleaseWithOptions(t, tc, 0, downloadECReleaseOptions{ - version: fmt.Sprintf("appver-%s-warning-preflights", os.Getenv("SHORT_SHA")), - }) - - t.Logf("%s: running embedded-cluster preflights on node 0", time.Now().Format(time.RFC3339)) - line = []string{"embedded-preflight.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to install embedded-cluster on node 0: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestHostPreflightInBuiltSpec(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "centos-9", - LicensePath: "licenses/license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - - t.Logf("%s: install single node with in-built host preflights", time.Now().Format(time.RFC3339)) - line := []string{"single-node-host-preflight-install.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to install embedded-cluster node with host preflights: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - // This test creates 4 nodes, installs on the first one and then generate 2 join tokens // for controllers and one join token for worker nodes. Joins the nodes and then waits // for them to report ready. @@ -372,40 +115,6 @@ func TestMultiNodeInstallation(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -func TestInstallFromReplicatedApp(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - }) - defer tc.Cleanup() - - downloadECRelease(t, tc, 0) - installSingleNode(t, tc) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs := []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - func TestSingleNodeUpgradePreviousStable(t *testing.T) { t.Parallel() @@ -466,52 +175,7 @@ func TestSingleNodeUpgradePreviousStable(t *testing.T) { checkPostUpgradeState(t, tc) - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestUpgradeFromReplicatedApp(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - }) - defer tc.Cleanup() - - // Previous stable EC version with a -1 minor k0s version - initialVersion := fmt.Sprintf("appver-%s-previous-stable", os.Getenv("SHORT_SHA")) - - downloadECReleaseWithOptions(t, tc, 0, downloadECReleaseOptions{ - version: initialVersion, - }) - - installSingleNodeWithOptions(t, tc, installOptions{ - version: initialVersion, - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationStateWithOptions(t, tc, installationStateOptions{ - version: initialVersion, - k8sVersion: k8sVersionPreviousStable(), - }) - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs := []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - line := []string{"collect-support-bundle-host-in-cluster.sh"} + line = []string{"collect-support-bundle-host-in-cluster.sh"} stdout, stderr, err := tc.RunCommandOnNode(0, line) if err != nil { t.Fatalf("fail to collect host support bundle: %v: %s: %s", err, stdout, stderr) @@ -529,647 +193,85 @@ func TestUpgradeFromReplicatedAppPreviousK0s(t *testing.T) { tc := docker.NewCluster(&docker.ClusterInput{ T: t, Nodes: 1, - Distro: "debian-bookworm", - }) - defer tc.Cleanup() - - initialVersion := fmt.Sprintf("appver-%s-previous-k0s-3", os.Getenv("SHORT_SHA")) - - downloadECReleaseWithOptions(t, tc, 0, downloadECReleaseOptions{ - version: initialVersion, - }) - - installSingleNodeWithOptions(t, tc, installOptions{ - version: initialVersion, - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationStateWithOptions(t, tc, installationStateOptions{ - version: initialVersion, - k8sVersion: k8sVersionPrevious(3), - }) - - appUpgradeVersion := fmt.Sprintf("appver-%s-previous-k0s-2", os.Getenv("SHORT_SHA")) - testArgs := []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationStateWithOptions(t, tc, installationStateOptions{ - version: appUpgradeVersion, - k8sVersion: k8sVersionPrevious(2), - }) - - appUpgradeVersion = fmt.Sprintf("appver-%s-previous-k0s-1", os.Getenv("SHORT_SHA")) - testArgs = []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationStateWithOptions(t, tc, installationStateOptions{ - version: appUpgradeVersion, - k8sVersion: k8sVersionPrevious(1), - }) - - line := []string{"collect-support-bundle-host-in-cluster.sh"} - stdout, stderr, err := tc.RunCommandOnNode(0, line) - if err != nil { - t.Fatalf("fail to collect host support bundle: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestResetAndReinstall(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - LicensePath: "licenses/license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", + Distro: "debian-bullseye", }) defer tc.Cleanup() - installSingleNode(t, tc) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - resetInstallation(t, tc, 0) - - t.Logf("%s: waiting for nodes to reboot", time.Now().Format(time.RFC3339)) - time.Sleep(30 * time.Second) - - installSingleNode(t, tc) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestResetAndReinstallAirgap(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := cmx.NewCluster(&cmx.ClusterInput{ - T: t, - Nodes: 1, - Distribution: "ubuntu", - Version: "22.04", - }) - defer tc.Cleanup() - - t.Logf("%s: downloading airgap file on node 0", time.Now().Format(time.RFC3339)) - err := downloadAirgapBundleOnNode(t, tc, 0, fmt.Sprintf("appver-%s", os.Getenv("SHORT_SHA")), AirgapInstallBundlePath, AirgapLicenseID) - if err != nil { - t.Fatal(err) - } - - t.Logf("%s: airgapping cluster", time.Now().Format(time.RFC3339)) - if err := tc.Airgap(); err != nil { - t.Fatalf("failed to airgap cluster: %v", err) - } - - t.Logf("%s: preparing embedded cluster airgap files", time.Now().Format(time.RFC3339)) - line := []string{"airgap-prepare.sh"} - - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to prepare airgap files on node %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) - } - - installSingleNodeWithOptions(t, tc, installOptions{ - isAirgap: true, - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{"check-airgap-installation-state.sh", fmt.Sprintf("appver-%s", os.Getenv("SHORT_SHA")), k8sVersion()} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check installation state: %v: %s: %s", err, stdout, stderr) - } - - checkNodeJoinCommand(t, tc, 0) - resetInstallation(t, tc, 0) - - t.Logf("%s: waiting for nodes to reboot", time.Now().Format(time.RFC3339)) - tc.WaitForReboot() - - installSingleNodeWithOptions(t, tc, installOptions{ - isAirgap: true, - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{"check-airgap-installation-state.sh", fmt.Sprintf("appver-%s", os.Getenv("SHORT_SHA")), k8sVersion()} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check installation state: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestSingleNodeAirgapUpgrade(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := cmx.NewCluster(&cmx.ClusterInput{ - T: t, - Nodes: 1, - Distribution: "ubuntu", - Version: "22.04", - }) - defer tc.Cleanup() - - t.Logf("%s: downloading airgap files on node 0", time.Now().Format(time.RFC3339)) - // Previous stable EC version with a -1 minor k0s version - initialVersion := fmt.Sprintf("appver-%s-previous-stable", os.Getenv("SHORT_SHA")) - runInParallel(t, - func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, initialVersion, AirgapInstallBundlePath, AirgapLicenseID) - }, func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")), AirgapUpgradeBundlePath, AirgapLicenseID) - }, - ) - - t.Logf("%s: airgapping cluster", time.Now().Format(time.RFC3339)) - if err := tc.Airgap(); err != nil { - t.Fatalf("failed to airgap cluster: %v", err) - } - - t.Logf("%s: preparing embedded cluster airgap files", time.Now().Format(time.RFC3339)) - line := []string{"airgap-prepare.sh"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to prepare airgap files on node %s: %v", tc.Nodes[0], err) - } - - installSingleNodeWithOptions(t, tc, installOptions{ - isAirgap: true, - version: initialVersion, - localArtifactMirrorPort: "50001", // choose an alternate lam port - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{"check-airgap-installation-state.sh", initialVersion, k8sVersionPreviousStable()} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check installation state: %v", err) - } - - checkNodeJoinCommand(t, tc, 0) - - t.Logf("%s: running airgap update", time.Now().Format(time.RFC3339)) - line = []string{"airgap-update.sh"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to run airgap update: %v", err) - } - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs := []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestSingleNodeAirgapUpgradeSelinux(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := cmx.NewCluster(&cmx.ClusterInput{ - T: t, - Nodes: 1, - Distribution: "almalinux", - Version: "8", - }) - defer tc.Cleanup() - - t.Logf("%s: downloading airgap files on node 0", time.Now().Format(time.RFC3339)) - // Previous stable EC version with a -1 minor k0s version - initialVersion := fmt.Sprintf("appver-%s-previous-stable", os.Getenv("SHORT_SHA")) - runInParallel(t, - func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, initialVersion, AirgapInstallBundlePath, AirgapLicenseID) - }, func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")), AirgapUpgradeBundlePath, AirgapLicenseID) - }, - ) - - t.Logf("%s: installing policycoreutils-python-utils", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunCommandOnNode(0, []string{"sudo dnf makecache --refresh && sudo dnf install -y policycoreutils-python-utils"}); err != nil { - t.Fatalf("fail to install policycoreutils-python-utils on node %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) - } - - t.Logf("%s: airgapping cluster", time.Now().Format(time.RFC3339)) - if err := tc.Airgap(); err != nil { - t.Fatalf("failed to airgap cluster: %v", err) - } - - t.Logf("%s: setting selinux to Enforcing mode", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunCommandOnNode(0, []string{"setenforce 1"}); err != nil { - t.Fatalf("fail to set selinux to Enforcing mode %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) - } - - t.Logf("%s: preparing embedded cluster airgap files", time.Now().Format(time.RFC3339)) - line := []string{"/usr/local/bin/airgap-prepare.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to prepare airgap files on node %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) - } - - t.Logf("%s: correcting selinux label for embedded cluster binary directory", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunCommandOnNode(0, []string{"sudo semanage fcontext -a -t bin_t \"/var/lib/embedded-cluster/bin(/.*)?\""}); err != nil { - t.Fatalf("fail to correct selinux label for embedded cluster binary directory on node %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) - } - - installSingleNodeWithOptions(t, tc, installOptions{ - isAirgap: true, - version: initialVersion, - localArtifactMirrorPort: "50001", // choose an alternate lam port - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{"/usr/local/bin/check-airgap-installation-state.sh", initialVersion, k8sVersionPreviousStable()} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check installation state: %v", err) - } - - checkNodeJoinCommand(t, tc, 0) - - t.Logf("%s: running airgap update", time.Now().Format(time.RFC3339)) - line = []string{"/usr/local/bin/airgap-update.sh"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to run airgap update: %v", err) - } - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs := []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestSingleNodeAirgapUpgradeCustomCIDR(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := cmx.NewCluster(&cmx.ClusterInput{ - T: t, - Nodes: 1, - Distribution: "ubuntu", - Version: "22.04", - }) - defer tc.Cleanup() - - t.Logf("%s: downloading airgap files on node 0", time.Now().Format(time.RFC3339)) - // Previous stable EC version with a -1 minor k0s version - initialVersion := fmt.Sprintf("appver-%s-previous-stable", os.Getenv("SHORT_SHA")) - runInParallel(t, - func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, initialVersion, AirgapInstallBundlePath, AirgapLicenseID) - }, func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")), AirgapUpgradeBundlePath, AirgapLicenseID) - }, - ) - - t.Logf("%s: airgapping cluster", time.Now().Format(time.RFC3339)) - if err := tc.Airgap(); err != nil { - t.Fatalf("failed to airgap cluster: %v", err) - } - - t.Logf("%s: preparing embedded cluster airgap files", time.Now().Format(time.RFC3339)) - line := []string{"airgap-prepare.sh"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to prepare airgap files on node %s: %v", tc.Nodes[0], err) - } - - installSingleNodeWithOptions(t, tc, installOptions{ - isAirgap: true, - version: initialVersion, - cidr: "172.16.0.0/15", - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{"check-airgap-installation-state.sh", initialVersion, k8sVersionPreviousStable()} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check installation state: %v", err) - } - - t.Logf("%s: running airgap update", time.Now().Format(time.RFC3339)) - line = []string{"airgap-update.sh"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to run airgap update: %v", err) - } - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs := []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - // ensure that the cluster is using the right IP ranges. - t.Logf("%s: checking service and pod IP addresses", time.Now().Format(time.RFC3339)) - - // we have used --cidr 172.16.0.0/15 during install time so pods are - // expected to be in the 172.16.0.0/16 range while services are in the - // 172.17.0.0/16 range. - podregex := `172\\.16\\.[0-9]\\+\\.[0-9]\\+` - svcregex := `172\\.17\\.[0-9]\\+\\.[0-9]\\+` - - if stdout, stderr, err := tc.RunCommandOnNode(0, []string{"check-cidr-ranges.sh", podregex, svcregex}); err != nil { - t.Log(stdout) - t.Log(stderr) - t.Fatalf("fail to check addresses on node %s: %v", tc.Nodes[0], err) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -func TestAirgapUpgradeFromEC18(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - withEnv := map[string]string{"KUBECONFIG": "/var/lib/k0s/pki/admin.conf"} - - appVer := fmt.Sprintf("appver-%s-1.8.0-k8s-1.28", os.Getenv("SHORT_SHA")) - - t.Logf("%s: downloading airgap files", time.Now().Format(time.RFC3339)) - airgapInstallBundlePath := "/tmp/airgap-install-bundle.tar.gz" - airgapUpgradeBundlePath := "/tmp/airgap-upgrade-bundle.tar.gz" - airgapUpgrade2BundlePath := "/tmp/airgap-upgrade2-bundle.tar.gz" - runInParallel(t, - func(t *testing.T) error { - return downloadAirgapBundle(t, appVer, airgapInstallBundlePath, AirgapLicenseID) - }, func(t *testing.T) error { - return downloadAirgapBundle(t, fmt.Sprintf("appver-%s-noop", os.Getenv("SHORT_SHA")), airgapUpgradeBundlePath, AirgapLicenseID) - }, func(t *testing.T) error { - return downloadAirgapBundle(t, fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")), airgapUpgrade2BundlePath, AirgapLicenseID) - }, - ) - - tc := lxd.NewCluster(&lxd.ClusterInput{ - T: t, - Nodes: 2, - Image: "debian/12", - WithProxy: true, - AirgapInstallBundlePath: airgapInstallBundlePath, - AirgapUpgradeBundlePath: airgapUpgradeBundlePath, - AirgapUpgrade2BundlePath: airgapUpgrade2BundlePath, - LowercaseNodeNames: true, - }) - defer tc.Cleanup(withEnv) - - // delete airgap bundles once they've been copied to the nodes - if err := os.Remove(airgapInstallBundlePath); err != nil { - t.Logf("failed to remove airgap install bundle: %v", err) - } - if err := os.Remove(airgapUpgradeBundlePath); err != nil { - t.Logf("failed to remove airgap upgrade bundle: %v", err) - } - if err := os.Remove(airgapUpgrade2BundlePath); err != nil { - t.Logf("failed to remove airgap upgrade bundle: %v", err) - } - - // upgrade airgap bundle is only needed on the first node - line := []string{"rm", "/assets/ec-release-upgrade.tgz"} - if _, _, err := tc.RunCommandOnNode(1, line); err != nil { - t.Fatalf("fail to remove upgrade airgap bundle on node %s: %v", tc.Nodes[1], err) - } - - // install "curl" dependency on node 0 for app version checks. - tc.InstallTestDependenciesDebian(t, 0, true) - - t.Logf("%s: preparing embedded cluster airgap files", time.Now().Format(time.RFC3339)) - line = []string{"airgap-prepare.sh"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to prepare airgap files on node %s: %v", tc.Nodes[0], err) - } - - installSingleNodeWithOptions(t, tc, installOptions{ - isAirgap: true, - version: appVer, - withEnv: withEnv, - }) - // remove the airgap bundle after installation - line = []string{"rm", "/assets/release.airgap"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to remove airgap bundle on node %s: %v", tc.Nodes[0], err) - } - - if err := tc.SetupPlaywright(withEnv); err != nil { - t.Fatalf("fail to setup playwright: %v", err) - } - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-ec18-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-ec18-app: %v: %s: %s", err, stdout, stderr) - } - - // generate worker node join command. - t.Logf("%s: generating a new worker token command", time.Now().Format(time.RFC3339)) - stdout, stderr, err := tc.RunPlaywrightTest("get-ec18-join-worker-command") - if err != nil { - t.Fatalf("fail to generate worker join token:\nstdout: %s\nstderr: %s", stdout, stderr) - } - workerCommand, err := findJoinCommandInOutput(stdout) - if err != nil { - t.Fatalf("fail to find the join command in the output: %v", err) - } - t.Log("worker join token command:", workerCommand) - - // join the worker node - t.Logf("%s: preparing embedded cluster airgap files on worker node", time.Now().Format(time.RFC3339)) - line = []string{"airgap-prepare.sh"} - if _, _, err := tc.RunCommandOnNode(1, line); err != nil { - t.Fatalf("fail to prepare airgap files on worker node: %v", err) - } - t.Logf("%s: joining worker node to the cluster", time.Now().Format(time.RFC3339)) - if _, _, err := tc.RunCommandOnNode(1, strings.Split(workerCommand, " ")); err != nil { - t.Fatalf("fail to join worker node to the cluster: %v", err) - } - // remove artifacts after joining to save space - line = []string{"rm", "/assets/release.airgap"} - if _, _, err := tc.RunCommandOnNode(1, line); err != nil { - t.Fatalf("fail to remove airgap bundle on worker node: %v", err) - } - line = []string{"rm", "/usr/local/bin/embedded-cluster"} - if _, _, err := tc.RunCommandOnNode(1, line); err != nil { - t.Fatalf("fail to remove embedded-cluster binary on worker node: %v", err) - } - line = []string{"rm", "/var/lib/embedded-cluster/bin/embedded-cluster"} - if _, _, err := tc.RunCommandOnNode(1, line); err != nil { - t.Fatalf("fail to remove embedded-cluster binary on node %s: %v", tc.Nodes[0], err) - } - - // wait for the nodes to report as ready. - t.Logf("%s: all nodes joined, waiting for them to be ready", time.Now().Format(time.RFC3339)) - stdout, _, err = tc.RunCommandOnNode(0, []string{"wait-for-ready-nodes.sh", "2"}, withEnv) - if err != nil { - t.Log(stdout) - t.Fatalf("fail to wait for ready nodes: %v", err) - } + initialVersion := fmt.Sprintf("appver-%s-previous-k0s-3", os.Getenv("SHORT_SHA")) - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{ - "check-airgap-installation-state.sh", - // the initially installed version is 1.8.0+k8s-1.28 - // the '+' character is problematic in the regex used to validate the version, so we use '.' instead - appVer, - "v1.28.11", - } - if _, _, err := tc.RunCommandOnNode(0, line, withEnv); err != nil { - t.Fatalf("fail to check installation state: %v", err) - } + downloadECReleaseWithOptions(t, tc, 0, downloadECReleaseOptions{ + version: initialVersion, + }) - t.Logf("%s: running airgap update", time.Now().Format(time.RFC3339)) - line = []string{"airgap-update.sh"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to run airgap update: %v", err) - } - // remove the airgap bundle after upgrade - line = []string{"rm", "/assets/upgrade/release.airgap"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to remove airgap bundle on node %s: %v", tc.Nodes[0], err) + installSingleNodeWithOptions(t, tc, installOptions{ + version: initialVersion, + }) + + if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { + t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) } - appUpgradeVersion := fmt.Sprintf("appver-%s-noop", os.Getenv("SHORT_SHA")) + checkInstallationStateWithOptions(t, tc, installationStateOptions{ + version: initialVersion, + k8sVersion: k8sVersionPrevious(3), + }) + + appUpgradeVersion := fmt.Sprintf("appver-%s-previous-k0s-2", os.Getenv("SHORT_SHA")) testArgs := []string{appUpgradeVersion} t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: checking installation state after noop upgrade", time.Now().Format(time.RFC3339)) - line = []string{"check-airgap-installation-state.sh", appUpgradeVersion, k8sVersion()} - if stdout, stderr, err := tc.RunCommandOnNode(0, line, withEnv); err != nil { - t.Fatalf("fail to check installation state: %v: %s: %s", err, stdout, stderr) + t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) } - t.Logf("%s: running second airgap update", time.Now().Format(time.RFC3339)) - line = []string{"airgap-update2.sh"} - if _, _, err := tc.RunCommandOnNode(0, line, withEnv); err != nil { - t.Fatalf("fail to run airgap update: %v", err) - } - // remove the airgap bundle and binary after upgrade - line = []string{"rm", "/assets/upgrade2/release.airgap"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to remove airgap bundle on node %s: %v", tc.Nodes[0], err) - } - line = []string{"rm", "/usr/local/bin/embedded-cluster-upgrade2"} - if _, _, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to remove embedded-cluster-upgrade2 binary on node %s: %v", tc.Nodes[0], err) - } + checkInstallationStateWithOptions(t, tc, installationStateOptions{ + version: appUpgradeVersion, + k8sVersion: k8sVersionPrevious(2), + }) - appUpgradeVersion = fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) + appUpgradeVersion = fmt.Sprintf("appver-%s-previous-k0s-1", os.Getenv("SHORT_SHA")) testArgs = []string{appUpgradeVersion} - t.Logf("%s: upgrading cluster a second time", time.Now().Format(time.RFC3339)) + t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: checking installation state after second upgrade", time.Now().Format(time.RFC3339)) - line = []string{"check-postupgrade-state.sh", k8sVersion(), ecUpgradeTargetVersion()} - if stdout, stderr, err := tc.RunCommandOnNode(0, line, withEnv); err != nil { - t.Fatalf("fail to check postupgrade state: %v: %s: %s", err, stdout, stderr) + t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) } - // TODO: reset fails with the following error: - // error: could not reset k0s: exit status 1, time="2024-10-17 22:44:52" level=warning msg="To ensure a full reset, a node reboot is recommended." - // Error: errors received during clean-up: [failed to delete /run/k0s. err: unlinkat /run/k0s/containerd/io.containerd.grpc.v1.cri/sandboxes/.../shm: device or resource busy] - - // t.Logf("%s: resetting worker node", time.Now().Format(time.RFC3339)) - // line = []string{"reset-installation.sh"} - // if stdout, stderr, err := tc.RunCommandOnNode(1, line, withEnv); err != nil { - // t.Fatalf("fail to reset worker node: %v: %s: %s", err, stdout, stderr) - // } - - // // use upgrade binary for reset - // withUpgradeBin := map[string]string{"EMBEDDED_CLUSTER_BIN": "embedded-cluster-upgrade"} + checkInstallationStateWithOptions(t, tc, installationStateOptions{ + version: appUpgradeVersion, + k8sVersion: k8sVersionPrevious(1), + }) - // t.Logf("%s: resetting node 0", time.Now().Format(time.RFC3339)) - // line = []string{"reset-installation.sh"} - // if stdout, stderr, err := tc.RunCommandOnNode(0, line, withEnv, withUpgradeBin); err != nil { - // t.Fatalf("fail to reset node 0: %v: %s: %s", err, stdout, stderr) - // } + line := []string{"collect-support-bundle-host-in-cluster.sh"} + stdout, stderr, err := tc.RunCommandOnNode(0, line) + if err != nil { + t.Fatalf("fail to collect host support bundle: %v: %s: %s", err, stdout, stderr) + } t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -func TestMultiNodeAirgapUpgradeSameK0s(t *testing.T) { +func TestSingleNodeAirgapUpgrade(t *testing.T) { t.Parallel() RequireEnvVars(t, []string{"SHORT_SHA"}) tc := cmx.NewCluster(&cmx.ClusterInput{ T: t, - Nodes: 2, + Nodes: 1, Distribution: "ubuntu", Version: "22.04", - InstanceType: "r1.medium", }) defer tc.Cleanup() - t.Logf("%s: downloading airgap files", time.Now().Format(time.RFC3339)) - initialVersion := fmt.Sprintf("appver-%s", os.Getenv("SHORT_SHA")) - upgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) + t.Logf("%s: downloading airgap files on node 0", time.Now().Format(time.RFC3339)) + // Previous stable EC version with a -1 minor k0s version + initialVersion := fmt.Sprintf("appver-%s-previous-stable", os.Getenv("SHORT_SHA")) runInParallel(t, func(t *testing.T) error { return downloadAirgapBundleOnNode(t, tc, 0, initialVersion, AirgapInstallBundlePath, AirgapLicenseID) - }, - func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, upgradeVersion, AirgapUpgradeBundlePath, AirgapLicenseID) + }, func(t *testing.T) error { + return downloadAirgapBundleOnNode(t, tc, 0, fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")), AirgapUpgradeBundlePath, AirgapLicenseID) }, ) @@ -1178,36 +280,34 @@ func TestMultiNodeAirgapUpgradeSameK0s(t *testing.T) { t.Fatalf("failed to airgap cluster: %v", err) } - t.Logf("%s: preparing embedded cluster airgap files on node 0", time.Now().Format(time.RFC3339)) + t.Logf("%s: preparing embedded cluster airgap files", time.Now().Format(time.RFC3339)) line := []string{"airgap-prepare.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to prepare airgap files on node 0: %v: %s: %s", err, stdout, stderr) + if _, _, err := tc.RunCommandOnNode(0, line); err != nil { + t.Fatalf("fail to prepare airgap files on node %s: %v", tc.Nodes[0], err) } installSingleNodeWithOptions(t, tc, installOptions{ - isAirgap: true, + isAirgap: true, + version: initialVersion, + localArtifactMirrorPort: "50001", // choose an alternate lam port }) if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) } - // join a worker - joinWorkerNode(t, tc, 1) - - // wait for the nodes to report as ready. - waitForNodes(t, tc, 2, nil) - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{"check-airgap-installation-state.sh", fmt.Sprintf("appver-%s", os.Getenv("SHORT_SHA")), k8sVersion()} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check installation state: %v: %s: %s", err, stdout, stderr) + line = []string{"check-airgap-installation-state.sh", initialVersion, k8sVersionPreviousStable()} + if _, _, err := tc.RunCommandOnNode(0, line); err != nil { + t.Fatalf("fail to check installation state: %v", err) } + checkNodeJoinCommand(t, tc, 0) + t.Logf("%s: running airgap update", time.Now().Format(time.RFC3339)) line = []string{"airgap-update.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to run airgap update: %v: %s: %s", err, stdout, stderr) + if _, _, err := tc.RunCommandOnNode(0, line); err != nil { + t.Fatalf("fail to run airgap update: %v", err) } appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) @@ -1223,42 +323,54 @@ func TestMultiNodeAirgapUpgradeSameK0s(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -func TestMultiNodeAirgapUpgrade(t *testing.T) { +func TestSingleNodeAirgapUpgradeSelinux(t *testing.T) { t.Parallel() RequireEnvVars(t, []string{"SHORT_SHA"}) tc := cmx.NewCluster(&cmx.ClusterInput{ T: t, - Nodes: 2, - Distribution: "ubuntu", - Version: "22.04", - InstanceType: "r1.medium", + Nodes: 1, + Distribution: "almalinux", + Version: "8", }) defer tc.Cleanup() - t.Logf("%s: downloading airgap files", time.Now().Format(time.RFC3339)) + t.Logf("%s: downloading airgap files on node 0", time.Now().Format(time.RFC3339)) // Previous stable EC version with a -1 minor k0s version initialVersion := fmt.Sprintf("appver-%s-previous-stable", os.Getenv("SHORT_SHA")) - upgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) runInParallel(t, func(t *testing.T) error { return downloadAirgapBundleOnNode(t, tc, 0, initialVersion, AirgapInstallBundlePath, AirgapLicenseID) - }, - func(t *testing.T) error { - return downloadAirgapBundleOnNode(t, tc, 0, upgradeVersion, AirgapUpgradeBundlePath, AirgapLicenseID) + }, func(t *testing.T) error { + return downloadAirgapBundleOnNode(t, tc, 0, fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")), AirgapUpgradeBundlePath, AirgapLicenseID) }, ) + t.Logf("%s: installing policycoreutils-python-utils", time.Now().Format(time.RFC3339)) + if stdout, stderr, err := tc.RunCommandOnNode(0, []string{"sudo dnf makecache --refresh && sudo dnf install -y policycoreutils-python-utils"}); err != nil { + t.Fatalf("fail to install policycoreutils-python-utils on node %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) + } + t.Logf("%s: airgapping cluster", time.Now().Format(time.RFC3339)) if err := tc.Airgap(); err != nil { t.Fatalf("failed to airgap cluster: %v", err) } - t.Logf("%s: preparing embedded cluster airgap files on node 0", time.Now().Format(time.RFC3339)) - line := []string{"airgap-prepare.sh"} + t.Logf("%s: setting selinux to Enforcing mode", time.Now().Format(time.RFC3339)) + if stdout, stderr, err := tc.RunCommandOnNode(0, []string{"setenforce 1"}); err != nil { + t.Fatalf("fail to set selinux to Enforcing mode %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) + } + + t.Logf("%s: preparing embedded cluster airgap files", time.Now().Format(time.RFC3339)) + line := []string{"/usr/local/bin/airgap-prepare.sh"} if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to prepare airgap files on node 0: %v: %s: %s", err, stdout, stderr) + t.Fatalf("fail to prepare airgap files on node %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) + } + + t.Logf("%s: correcting selinux label for embedded cluster binary directory", time.Now().Format(time.RFC3339)) + if stdout, stderr, err := tc.RunCommandOnNode(0, []string{"sudo semanage fcontext -a -t bin_t \"/var/lib/embedded-cluster/bin(/.*)?\""}); err != nil { + t.Fatalf("fail to correct selinux label for embedded cluster binary directory on node %s: %v: %s: %s", tc.Nodes[0], err, stdout, stderr) } installSingleNodeWithOptions(t, tc, installOptions{ @@ -1271,22 +383,18 @@ func TestMultiNodeAirgapUpgrade(t *testing.T) { t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) } - // join a worker - joinWorkerNode(t, tc, 1) - - // wait for the nodes to report as ready. - waitForNodes(t, tc, 2, nil) - t.Logf("%s: checking installation state after app deployment", time.Now().Format(time.RFC3339)) - line = []string{"check-airgap-installation-state.sh", initialVersion, k8sVersionPreviousStable()} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check installation state: %v: %s: %s", err, stdout, stderr) + line = []string{"/usr/local/bin/check-airgap-installation-state.sh", initialVersion, k8sVersionPreviousStable()} + if _, _, err := tc.RunCommandOnNode(0, line); err != nil { + t.Fatalf("fail to check installation state: %v", err) } + checkNodeJoinCommand(t, tc, 0) + t.Logf("%s: running airgap update", time.Now().Format(time.RFC3339)) - line = []string{"airgap-update.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to run airgap update: %v: %s: %s", err, stdout, stderr) + line = []string{"/usr/local/bin/airgap-update.sh"} + if _, _, err := tc.RunCommandOnNode(0, line); err != nil { + t.Fatalf("fail to run airgap update: %v", err) } appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) @@ -1623,108 +731,6 @@ func TestMultiNodeAirgapHAInstallation(t *testing.T) { t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } -func TestInstallSnapshotFromReplicatedApp(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - }) - defer tc.Cleanup() - - downloadECReleaseWithOptions(t, tc, 0, downloadECReleaseOptions{ - version: fmt.Sprintf("appver-%s", os.Getenv("SHORT_SHA")), - licenseID: SnapshotLicenseID, - }) - - installSingleNode(t, tc) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - - t.Logf("%s: ensuring velero is installed", time.Now().Format(time.RFC3339)) - line := []string{"check-velero-state.sh", os.Getenv("SHORT_SHA")} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to check velero state: %v: %s: %s", err, stdout, stderr) - } - - appUpgradeVersion := fmt.Sprintf("appver-%s-upgrade", os.Getenv("SHORT_SHA")) - testArgs := []string{appUpgradeVersion} - - t.Logf("%s: upgrading cluster", time.Now().Format(time.RFC3339)) - if stdout, stderr, err := tc.RunPlaywrightTest("deploy-upgrade", testArgs...); err != nil { - t.Fatalf("fail to run playwright test deploy-upgrade: %v: %s: %s", err, stdout, stderr) - } - - checkPostUpgradeState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - -// TestCustomCIDR tests the installation with an alternate CIDR range -func TestCustomCIDR(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 4, - Distro: "debian-bookworm", - LicensePath: "licenses/license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - t.Log("non-proxied infrastructure created") - - installSingleNodeWithOptions(t, tc, installOptions{ - podCidr: "10.128.0.0/20", - serviceCidr: "10.129.0.0/20", - }) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - // join a controller node - joinControllerNode(t, tc, 1) - - // XXX If we are too aggressive joining nodes we can see the following error being - // thrown by kotsadm on its log (and we get a 500 back): - // " - // failed to get controller role name: failed to get cluster config: failed to get - // current installation: failed to list installations: etcdserver: leader changed - // " - t.Logf("node 1 joined, sleeping...") - time.Sleep(30 * time.Second) - - // join another controller node - joinControllerNode(t, tc, 2) - - // join a worker node - joinWorkerNode(t, tc, 3) - - // wait for the nodes to report as ready. - waitForNodes(t, tc, 4, nil) - - checkInstallationState(t, tc) - - // ensure that the cluster is using the right IP ranges. - t.Logf("%s: checking service and pod IP addresses", time.Now().Format(time.RFC3339)) - stdout, stderr, err := tc.RunCommandOnNode(0, []string{"check-cidr-ranges.sh", "^10.128.[0-9]*.[0-9]", "^10.129.[0-9]*.[0-9]"}) - if err != nil { - t.Fatalf("fail to check addresses on node 0: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} - func TestSingleNodeInstallationNoopUpgrade(t *testing.T) { t.Parallel() @@ -1733,7 +739,7 @@ func TestSingleNodeInstallationNoopUpgrade(t *testing.T) { tc := docker.NewCluster(&docker.ClusterInput{ T: t, Nodes: 1, - Distro: "debian-bookworm", + Distro: "centos-9", LicensePath: "licenses/license.yaml", ECBinaryPath: "../output/bin/embedded-cluster", }) diff --git a/e2e/kots-release-install-failing-preflights/backup.yaml b/e2e/kots-release-install-failing-preflights/backup.yaml deleted file mode 100644 index ce6f205deb..0000000000 --- a/e2e/kots-release-install-failing-preflights/backup.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: velero.io/v1 -kind: Backup -metadata: - name: backup - annotations: - preserve: me -spec: - ttl: 36h0m0s - includedNamespaces: - - kotsadm - - redis-app - - nginx-app - orLabelSelectors: - - matchExpressions: - - { key: kots.io/kotsadm, operator: NotIn, values: ["true"] } - hooks: - resources: - - name: test-hook - includedResources: - - 'pods' - labelSelector: - matchLabels: - app: example - component: nginx - pre: - - exec: - container: nginx - command: - - /bin/uname - - -a - post: - - exec: - command: - - /bin/uname - - -a diff --git a/e2e/kots-release-install-failing-preflights/cluster-config.yaml b/e2e/kots-release-install-failing-preflights/cluster-config.yaml deleted file mode 100644 index 2634327fd8..0000000000 --- a/e2e/kots-release-install-failing-preflights/cluster-config.yaml +++ /dev/null @@ -1,86 +0,0 @@ -apiVersion: embeddedcluster.replicated.com/v1beta1 -kind: Config -metadata: - name: "testconfig" -spec: - version: "__version_string__" - binaryOverrideUrl: "__release_url__" - metadataOverrideUrl: "__metadata_url__" - domains: - proxyRegistryDomain: "ec-e2e-proxy.testcluster.net" - replicatedAppDomain: "ec-e2e-replicated-app.testcluster.net" - roles: - controller: - labels: - controller-label: controller-label-value - name: controller-test - custom: - - labels: - abc-test-label: abc-test-label-value - abc-test-label-two: abc-test-label-value-2 - name: abc - - labels: - xyz-test-label: xyz-value - name: xyz - unsupportedOverrides: - builtInExtensions: - - name: admin-console - values: | - labels: - release-custom-label: release-clustom-value - - name: embedded-cluster-operator - values: | - global: - labels: - release-custom-label: release-clustom-value - k0s: | - config: - metadata: - name: foo - spec: - telemetry: - enabled: false - workerProfiles: - - name: ip-forward - values: - allowedUnsafeSysctls: - - net.ipv4.ip_forward - extensions: - helm: - repositories: - - name: ingress-nginx - url: https://kubernetes.github.io/ingress-nginx - - name: okgolove - url: https://okgolove.github.io/helm-charts/ - charts: - - name: ingress-nginx - chartname: ingress-nginx/ingress-nginx - namespace: ingress-nginx - version: "4.11.3" - values: | - global: - image: - registry: ec-e2e-proxy.testcluster.net/anonymous/registry.k8s.io - controller: - service: - type: NodePort - nodePorts: - http: "80" - https: "443" - image: - registry: ec-e2e-proxy.testcluster.net/anonymous/registry.k8s.io - digest: "" - digestChroot: "" - admissionWebhooks: - patch: - image: - registry: ec-e2e-proxy.testcluster.net/anonymous/registry.k8s.io - digest: "" - - name: goldpinger - chartname: okgolove/goldpinger - namespace: goldpinger - version: 6.1.2 - order: 11 - values: | - image: - repository: ec-e2e-proxy.testcluster.net/anonymous/bloomberg/goldpinger diff --git a/e2e/kots-release-install-failing-preflights/config.yaml b/e2e/kots-release-install-failing-preflights/config.yaml deleted file mode 100644 index f1c98e69a4..0000000000 --- a/e2e/kots-release-install-failing-preflights/config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: Config -spec: - groups: - - name: config_group - title: The First Config Group - items: - - name: hostname - title: Hostname - type: text - - name: pw - title: Password - type: password diff --git a/e2e/kots-release-install-failing-preflights/deployment-2.yaml b/e2e/kots-release-install-failing-preflights/deployment-2.yaml deleted file mode 100644 index 65c1960877..0000000000 --- a/e2e/kots-release-install-failing-preflights/deployment-2.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: second - labels: - app: second - replicated.com/disaster-recovery: app -spec: - replicas: 0 - selector: - matchLabels: - app: second - template: - metadata: - labels: - app: second - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - second - topologyKey: "kubernetes.io/hostname" - containers: - - name: nginx - image: us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test/nginx:1.24-alpine - resources: - limits: - memory: '32Mi' - cpu: '50m' diff --git a/e2e/kots-release-install-failing-preflights/deployment.yaml b/e2e/kots-release-install-failing-preflights/deployment.yaml deleted file mode 100644 index c4fed18dfd..0000000000 --- a/e2e/kots-release-install-failing-preflights/deployment.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx - labels: - app: example - component: nginx - replicated.com/disaster-recovery: app -spec: - replicas: 1 - selector: - matchLabels: - app: example - component: nginx - template: - metadata: - labels: - app: example - component: nginx - spec: - containers: - - name: nginx - image: us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test/nginx:1.24-alpine - resources: - limits: - memory: '64Mi' - cpu: '50m' - env: - - name: APP_SEQUENCE - value: "{{repl Cursor }}" - - name: APP_VERSION - value: "{{repl VersionLabel }}" - - name: APP_CHANNEL - value: "{{repl ChannelName }}" - - name: CONFIG_HOSTNAME - value: '{{repl ConfigOption "hostname" }}' - - name: CONFIG_PASSWORD - value: '{{repl ConfigOption "pw" }}' diff --git a/e2e/kots-release-install-failing-preflights/failing-preflights.yaml b/e2e/kots-release-install-failing-preflights/failing-preflights.yaml deleted file mode 100644 index 5c83e31ac4..0000000000 --- a/e2e/kots-release-install-failing-preflights/failing-preflights.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: HostPreflight -spec: - collectors: - - tcpPortStatus: - collectorName: Port 24 - port: 24 - - tcpPortStatus: - collectorName: Port 22 - port: 22 - analyzers: - - tcpPortStatus: - checkName: Port 24 - collectorName: Port 24 - outcomes: - - fail: - when: connection-refused - message: Connection to port 24 was refused. - - warn: - when: address-in-use - message: Another process was already listening on port 24. - - fail: - when: connection-timeout - message: Timed out connecting to port 24. - - fail: - when: error - message: Unexpected port status - - pass: - when: connected - message: Port 24 is available - - warn: - message: Unexpected port status - - tcpPortStatus: - checkName: Port 22 - collectorName: Port 22 - outcomes: - - fail: - when: connection-refused - message: Connection to port 22 was refused. - - fail: - when: address-in-use - message: Another process was already listening on port 22. - - fail: - when: connection-timeout - message: Timed out connecting to port 22. - - fail: - when: error - message: Unexpected port status - - pass: - when: connected - message: Port 22 is available - - warn: - message: Unexpected port status diff --git a/e2e/kots-release-install-failing-preflights/k8s-app.yaml b/e2e/kots-release-install-failing-preflights/k8s-app.yaml deleted file mode 100644 index c5f625eaed..0000000000 --- a/e2e/kots-release-install-failing-preflights/k8s-app.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: app.k8s.io/v1beta1 -kind: Application -metadata: - name: "nginx" -spec: - descriptor: - links: - - description: Open App - # needs to match applicationUrl in kots-app.yaml - url: "http://nginx" diff --git a/e2e/kots-release-install-failing-preflights/kots-app.yaml b/e2e/kots-release-install-failing-preflights/kots-app.yaml deleted file mode 100644 index 73aae5851b..0000000000 --- a/e2e/kots-release-install-failing-preflights/kots-app.yaml +++ /dev/null @@ -1,24 +0,0 @@ ---- -apiVersion: kots.io/v1beta1 -kind: Application -metadata: - name: nginx -spec: - title: Embedded Cluster Smoke Test Staging App - icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.png - proxyPublicImages: true - allowRollback: true - additionalNamespaces: - - "redis-app" - - "nginx-app" - statusInformers: - - deployment/nginx - - nginx-app/deployment/nginx-app - - nginx-app/deployment/nginx-app-client - - redis-app/deployment/redis-app - - redis-app/deployment/redis-app-client - ports: - - serviceName: "nginx" - servicePort: 80 - localPort: 8888 - applicationUrl: "http://nginx" diff --git a/e2e/kots-release-install-failing-preflights/kots-lint-config.yaml b/e2e/kots-release-install-failing-preflights/kots-lint-config.yaml deleted file mode 100644 index ba99c8b92a..0000000000 --- a/e2e/kots-release-install-failing-preflights/kots-lint-config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: LintConfig -metadata: - name: default-lint-config -spec: - rules: - - name: missing-kind-field - level: "error" - - name: missing-api-version-field - level: "error" - - name: preflight-spec - level: "warn" - - name: config-spec - level: "warn" - - name: troubleshoot-spec - level: "warn" - - name: application-spec - level: "warn" - - name: application-icon - level: "warn" - - name: application-statusInformers - level: "warn" - - name: invalid-target-kots-version - level: "error" - - name: invalid-min-kots-version - level: "error" - - name: invalid-kubernetes-installer - level: "error" - - name: deprecated-kubernetes-installer-version - level: "warn" - - name: duplicate-kots-kind - level: "error" - - name: invalid-helm-release-name - level: "error" - - name: duplicate-helm-release-name - level: "error" - - name: replicas-1 - level: "info" - - name: privileged - level: "info" - - name: allow-privilege-escalation - level: "info" - - name: container-image-latest-tag - level: "info" - - name: container-image-local-image-name - level: "error" - - name: container-resources - level: "info" - - name: container-resource-limits - level: "info" - - name: container-resource-requests - level: "info" - - name: resource-limits-cpu - level: "info" - - name: resource-limits-memory - level: "info" - - name: resource-requests-cpu - level: "info" - - name: resource-requests-memory - level: "info" - - name: volumes-host-paths - level: "info" - - name: volume-docker-sock - level: "info" - - name: hardcoded-namespace - level: "info" - - name: may-contain-secrets - level: "info" - - name: config-option-invalid-type - level: "error" - - name: repeat-option-missing-template - level: "error" - - name: repeat-option-missing-valuesByGroup - level: "error" - - name: repeat-option-malformed-yamlpath - level: "error" - - name: config-option-password-type - level: "warn" - - name: config-option-not-found - level: "warn" - - name: config-option-is-circular - level: "error" - - name: config-option-not-repeatable - level: "error" - - name: config-option-when-is-invalid - level: "error" - - name: config-option-invalid-regex-validator - level: "error" - - name: config-option-regex-validator-invalid-type - level: "error" diff --git a/e2e/kots-release-install-failing-preflights/nginx-app-helm-v1beta2.yaml b/e2e/kots-release-install-failing-preflights/nginx-app-helm-v1beta2.yaml deleted file mode 100644 index 0a16743190..0000000000 --- a/e2e/kots-release-install-failing-preflights/nginx-app-helm-v1beta2.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: nginx-app -spec: - chart: - name: nginx-app - chartVersion: 0.1.0 - namespace: nginx-app - values: - server: - image: - repository: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }}/repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "proxy/embedded-cluster-smoke-test-staging-app/us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test" }}/nginx - service: - type: ClusterIP - port: 80 - replicaCount: 1 - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - client: - enabled: true - image: - repository: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }}/repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "proxy/embedded-cluster-smoke-test-staging-app/us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test" }}/curl - replicaCount: 1 - intervalSeconds: 60 - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - replicated: - enabled: true - image: - registry: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }} - repository: repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "anonymous/registry.replicated.com/library" }}/replicated-sdk-image - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - builder: - replicated: - enabled: true diff --git a/e2e/kots-release-install-failing-preflights/preflight.yaml b/e2e/kots-release-install-failing-preflights/preflight.yaml deleted file mode 100644 index 3a9103a2a5..0000000000 --- a/e2e/kots-release-install-failing-preflights/preflight.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: Preflight -metadata: - name: preflight-checks -spec: - collectors: - - clusterInfo: {} - - clusterResources: {} - analyzers: - - customResourceDefinition: - customResourceDefinitionName: volumesnapshots.snapshot.storage.k8s.io - checkName: The Volume Snapshots CRD exists - outcomes: - - fail: - message: The Volume Snapshots CRD does not exist. - - pass: - message: The Volume Snapshots CRD exists. diff --git a/e2e/kots-release-install-failing-preflights/redis-app-helm-v1beta1.yaml b/e2e/kots-release-install-failing-preflights/redis-app-helm-v1beta1.yaml deleted file mode 100644 index 78b7d9368c..0000000000 --- a/e2e/kots-release-install-failing-preflights/redis-app-helm-v1beta1.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: HelmChart -metadata: - name: redis-app -spec: - chart: - name: redis-app - chartVersion: 0.1.0 - namespace: redis-app - useHelmInstall: true - values: - server: - image: - repository: redis - tag: "7.2" - service: - type: ClusterIP - port: 6379 - client: - enabled: true - image: - repository: redis - tag: "7.2" - intervalSeconds: 5 diff --git a/e2e/kots-release-install-failing-preflights/restore.yaml b/e2e/kots-release-install-failing-preflights/restore.yaml deleted file mode 100644 index 25832f6087..0000000000 --- a/e2e/kots-release-install-failing-preflights/restore.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: velero.io/v1 -kind: Restore -metadata: - name: restore - annotations: - preserve: me -spec: - backupName: backup - includedNamespaces: - - '*' - hooks: - resources: - - name: restore-hook-1 - includedNamespaces: - - kotsadm - labelSelector: - matchLabels: - app: example - postHooks: - - init: - initContainers: - - name: restore-hook-init1 - image: 'repl{{ LocalImageName "nginx:1.24-alpine" }}' - command: - - /bin/ash - - -c - - echo -n "FOOBARBAZ" > /tmp/foobarbaz diff --git a/e2e/kots-release-install-failing-preflights/troubleshoot.yaml b/e2e/kots-release-install-failing-preflights/troubleshoot.yaml deleted file mode 100644 index e817502d60..0000000000 --- a/e2e/kots-release-install-failing-preflights/troubleshoot.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: SupportBundle -metadata: - name: preflight-checks -spec: - collectors: - - clusterInfo: {} - - clusterResources: {} diff --git a/e2e/kots-release-install-legacydr/cluster-config.yaml b/e2e/kots-release-install-legacydr/cluster-config.yaml index 2634327fd8..a1e75cb419 100644 --- a/e2e/kots-release-install-legacydr/cluster-config.yaml +++ b/e2e/kots-release-install-legacydr/cluster-config.yaml @@ -78,7 +78,7 @@ spec: digest: "" - name: goldpinger chartname: okgolove/goldpinger - namespace: goldpinger + namespace: embedded-cluster version: 6.1.2 order: 11 values: | diff --git a/e2e/kots-release-install-stable/cluster-config.yaml b/e2e/kots-release-install-stable/cluster-config.yaml index 2b49dfa3e6..ad6a695916 100644 --- a/e2e/kots-release-install-stable/cluster-config.yaml +++ b/e2e/kots-release-install-stable/cluster-config.yaml @@ -76,7 +76,7 @@ spec: digest: "" - name: goldpinger chartname: okgolove/goldpinger - namespace: goldpinger + namespace: embedded-cluster version: 6.1.2 order: 11 values: | diff --git a/e2e/kots-release-install-v3/cluster-config.yaml b/e2e/kots-release-install-v3/cluster-config.yaml index 46abfdacb3..4a2818ecec 100644 --- a/e2e/kots-release-install-v3/cluster-config.yaml +++ b/e2e/kots-release-install-v3/cluster-config.yaml @@ -73,7 +73,7 @@ spec: digest: "" - name: goldpinger chartname: oci://ec-e2e-proxy.testcluster.net/anonymous/public.ecr.aws/q7i7m9q2/embedded-cluster-charts/goldpinger - namespace: goldpinger + namespace: embedded-cluster version: 6.1.2 order: 11 values: | diff --git a/e2e/kots-release-install-warning-preflights/backup.yaml b/e2e/kots-release-install-warning-preflights/backup.yaml deleted file mode 100644 index ce6f205deb..0000000000 --- a/e2e/kots-release-install-warning-preflights/backup.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: velero.io/v1 -kind: Backup -metadata: - name: backup - annotations: - preserve: me -spec: - ttl: 36h0m0s - includedNamespaces: - - kotsadm - - redis-app - - nginx-app - orLabelSelectors: - - matchExpressions: - - { key: kots.io/kotsadm, operator: NotIn, values: ["true"] } - hooks: - resources: - - name: test-hook - includedResources: - - 'pods' - labelSelector: - matchLabels: - app: example - component: nginx - pre: - - exec: - container: nginx - command: - - /bin/uname - - -a - post: - - exec: - command: - - /bin/uname - - -a diff --git a/e2e/kots-release-install-warning-preflights/cluster-config.yaml b/e2e/kots-release-install-warning-preflights/cluster-config.yaml deleted file mode 100644 index 2634327fd8..0000000000 --- a/e2e/kots-release-install-warning-preflights/cluster-config.yaml +++ /dev/null @@ -1,86 +0,0 @@ -apiVersion: embeddedcluster.replicated.com/v1beta1 -kind: Config -metadata: - name: "testconfig" -spec: - version: "__version_string__" - binaryOverrideUrl: "__release_url__" - metadataOverrideUrl: "__metadata_url__" - domains: - proxyRegistryDomain: "ec-e2e-proxy.testcluster.net" - replicatedAppDomain: "ec-e2e-replicated-app.testcluster.net" - roles: - controller: - labels: - controller-label: controller-label-value - name: controller-test - custom: - - labels: - abc-test-label: abc-test-label-value - abc-test-label-two: abc-test-label-value-2 - name: abc - - labels: - xyz-test-label: xyz-value - name: xyz - unsupportedOverrides: - builtInExtensions: - - name: admin-console - values: | - labels: - release-custom-label: release-clustom-value - - name: embedded-cluster-operator - values: | - global: - labels: - release-custom-label: release-clustom-value - k0s: | - config: - metadata: - name: foo - spec: - telemetry: - enabled: false - workerProfiles: - - name: ip-forward - values: - allowedUnsafeSysctls: - - net.ipv4.ip_forward - extensions: - helm: - repositories: - - name: ingress-nginx - url: https://kubernetes.github.io/ingress-nginx - - name: okgolove - url: https://okgolove.github.io/helm-charts/ - charts: - - name: ingress-nginx - chartname: ingress-nginx/ingress-nginx - namespace: ingress-nginx - version: "4.11.3" - values: | - global: - image: - registry: ec-e2e-proxy.testcluster.net/anonymous/registry.k8s.io - controller: - service: - type: NodePort - nodePorts: - http: "80" - https: "443" - image: - registry: ec-e2e-proxy.testcluster.net/anonymous/registry.k8s.io - digest: "" - digestChroot: "" - admissionWebhooks: - patch: - image: - registry: ec-e2e-proxy.testcluster.net/anonymous/registry.k8s.io - digest: "" - - name: goldpinger - chartname: okgolove/goldpinger - namespace: goldpinger - version: 6.1.2 - order: 11 - values: | - image: - repository: ec-e2e-proxy.testcluster.net/anonymous/bloomberg/goldpinger diff --git a/e2e/kots-release-install-warning-preflights/config.yaml b/e2e/kots-release-install-warning-preflights/config.yaml deleted file mode 100644 index f1c98e69a4..0000000000 --- a/e2e/kots-release-install-warning-preflights/config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: Config -spec: - groups: - - name: config_group - title: The First Config Group - items: - - name: hostname - title: Hostname - type: text - - name: pw - title: Password - type: password diff --git a/e2e/kots-release-install-warning-preflights/deployment-2.yaml b/e2e/kots-release-install-warning-preflights/deployment-2.yaml deleted file mode 100644 index 65c1960877..0000000000 --- a/e2e/kots-release-install-warning-preflights/deployment-2.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: second - labels: - app: second - replicated.com/disaster-recovery: app -spec: - replicas: 0 - selector: - matchLabels: - app: second - template: - metadata: - labels: - app: second - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - second - topologyKey: "kubernetes.io/hostname" - containers: - - name: nginx - image: us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test/nginx:1.24-alpine - resources: - limits: - memory: '32Mi' - cpu: '50m' diff --git a/e2e/kots-release-install-warning-preflights/deployment.yaml b/e2e/kots-release-install-warning-preflights/deployment.yaml deleted file mode 100644 index c4fed18dfd..0000000000 --- a/e2e/kots-release-install-warning-preflights/deployment.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx - labels: - app: example - component: nginx - replicated.com/disaster-recovery: app -spec: - replicas: 1 - selector: - matchLabels: - app: example - component: nginx - template: - metadata: - labels: - app: example - component: nginx - spec: - containers: - - name: nginx - image: us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test/nginx:1.24-alpine - resources: - limits: - memory: '64Mi' - cpu: '50m' - env: - - name: APP_SEQUENCE - value: "{{repl Cursor }}" - - name: APP_VERSION - value: "{{repl VersionLabel }}" - - name: APP_CHANNEL - value: "{{repl ChannelName }}" - - name: CONFIG_HOSTNAME - value: '{{repl ConfigOption "hostname" }}' - - name: CONFIG_PASSWORD - value: '{{repl ConfigOption "pw" }}' diff --git a/e2e/kots-release-install-warning-preflights/k8s-app.yaml b/e2e/kots-release-install-warning-preflights/k8s-app.yaml deleted file mode 100644 index c5f625eaed..0000000000 --- a/e2e/kots-release-install-warning-preflights/k8s-app.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: app.k8s.io/v1beta1 -kind: Application -metadata: - name: "nginx" -spec: - descriptor: - links: - - description: Open App - # needs to match applicationUrl in kots-app.yaml - url: "http://nginx" diff --git a/e2e/kots-release-install-warning-preflights/kots-app.yaml b/e2e/kots-release-install-warning-preflights/kots-app.yaml deleted file mode 100644 index 73aae5851b..0000000000 --- a/e2e/kots-release-install-warning-preflights/kots-app.yaml +++ /dev/null @@ -1,24 +0,0 @@ ---- -apiVersion: kots.io/v1beta1 -kind: Application -metadata: - name: nginx -spec: - title: Embedded Cluster Smoke Test Staging App - icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.png - proxyPublicImages: true - allowRollback: true - additionalNamespaces: - - "redis-app" - - "nginx-app" - statusInformers: - - deployment/nginx - - nginx-app/deployment/nginx-app - - nginx-app/deployment/nginx-app-client - - redis-app/deployment/redis-app - - redis-app/deployment/redis-app-client - ports: - - serviceName: "nginx" - servicePort: 80 - localPort: 8888 - applicationUrl: "http://nginx" diff --git a/e2e/kots-release-install-warning-preflights/kots-lint-config.yaml b/e2e/kots-release-install-warning-preflights/kots-lint-config.yaml deleted file mode 100644 index ba99c8b92a..0000000000 --- a/e2e/kots-release-install-warning-preflights/kots-lint-config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: LintConfig -metadata: - name: default-lint-config -spec: - rules: - - name: missing-kind-field - level: "error" - - name: missing-api-version-field - level: "error" - - name: preflight-spec - level: "warn" - - name: config-spec - level: "warn" - - name: troubleshoot-spec - level: "warn" - - name: application-spec - level: "warn" - - name: application-icon - level: "warn" - - name: application-statusInformers - level: "warn" - - name: invalid-target-kots-version - level: "error" - - name: invalid-min-kots-version - level: "error" - - name: invalid-kubernetes-installer - level: "error" - - name: deprecated-kubernetes-installer-version - level: "warn" - - name: duplicate-kots-kind - level: "error" - - name: invalid-helm-release-name - level: "error" - - name: duplicate-helm-release-name - level: "error" - - name: replicas-1 - level: "info" - - name: privileged - level: "info" - - name: allow-privilege-escalation - level: "info" - - name: container-image-latest-tag - level: "info" - - name: container-image-local-image-name - level: "error" - - name: container-resources - level: "info" - - name: container-resource-limits - level: "info" - - name: container-resource-requests - level: "info" - - name: resource-limits-cpu - level: "info" - - name: resource-limits-memory - level: "info" - - name: resource-requests-cpu - level: "info" - - name: resource-requests-memory - level: "info" - - name: volumes-host-paths - level: "info" - - name: volume-docker-sock - level: "info" - - name: hardcoded-namespace - level: "info" - - name: may-contain-secrets - level: "info" - - name: config-option-invalid-type - level: "error" - - name: repeat-option-missing-template - level: "error" - - name: repeat-option-missing-valuesByGroup - level: "error" - - name: repeat-option-malformed-yamlpath - level: "error" - - name: config-option-password-type - level: "warn" - - name: config-option-not-found - level: "warn" - - name: config-option-is-circular - level: "error" - - name: config-option-not-repeatable - level: "error" - - name: config-option-when-is-invalid - level: "error" - - name: config-option-invalid-regex-validator - level: "error" - - name: config-option-regex-validator-invalid-type - level: "error" diff --git a/e2e/kots-release-install-warning-preflights/nginx-app-helm-v1beta2.yaml b/e2e/kots-release-install-warning-preflights/nginx-app-helm-v1beta2.yaml deleted file mode 100644 index 0a16743190..0000000000 --- a/e2e/kots-release-install-warning-preflights/nginx-app-helm-v1beta2.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: nginx-app -spec: - chart: - name: nginx-app - chartVersion: 0.1.0 - namespace: nginx-app - values: - server: - image: - repository: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }}/repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "proxy/embedded-cluster-smoke-test-staging-app/us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test" }}/nginx - service: - type: ClusterIP - port: 80 - replicaCount: 1 - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - client: - enabled: true - image: - repository: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }}/repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "proxy/embedded-cluster-smoke-test-staging-app/us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test" }}/curl - replicaCount: 1 - intervalSeconds: 60 - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - replicated: - enabled: true - image: - registry: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }} - repository: repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "anonymous/registry.replicated.com/library" }}/replicated-sdk-image - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - builder: - replicated: - enabled: true diff --git a/e2e/kots-release-install-warning-preflights/preflight.yaml b/e2e/kots-release-install-warning-preflights/preflight.yaml deleted file mode 100644 index 3a9103a2a5..0000000000 --- a/e2e/kots-release-install-warning-preflights/preflight.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: Preflight -metadata: - name: preflight-checks -spec: - collectors: - - clusterInfo: {} - - clusterResources: {} - analyzers: - - customResourceDefinition: - customResourceDefinitionName: volumesnapshots.snapshot.storage.k8s.io - checkName: The Volume Snapshots CRD exists - outcomes: - - fail: - message: The Volume Snapshots CRD does not exist. - - pass: - message: The Volume Snapshots CRD exists. diff --git a/e2e/kots-release-install-warning-preflights/redis-app-helm-v1beta1.yaml b/e2e/kots-release-install-warning-preflights/redis-app-helm-v1beta1.yaml deleted file mode 100644 index 78b7d9368c..0000000000 --- a/e2e/kots-release-install-warning-preflights/redis-app-helm-v1beta1.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: HelmChart -metadata: - name: redis-app -spec: - chart: - name: redis-app - chartVersion: 0.1.0 - namespace: redis-app - useHelmInstall: true - values: - server: - image: - repository: redis - tag: "7.2" - service: - type: ClusterIP - port: 6379 - client: - enabled: true - image: - repository: redis - tag: "7.2" - intervalSeconds: 5 diff --git a/e2e/kots-release-install-warning-preflights/restore.yaml b/e2e/kots-release-install-warning-preflights/restore.yaml deleted file mode 100644 index 25832f6087..0000000000 --- a/e2e/kots-release-install-warning-preflights/restore.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: velero.io/v1 -kind: Restore -metadata: - name: restore - annotations: - preserve: me -spec: - backupName: backup - includedNamespaces: - - '*' - hooks: - resources: - - name: restore-hook-1 - includedNamespaces: - - kotsadm - labelSelector: - matchLabels: - app: example - postHooks: - - init: - initContainers: - - name: restore-hook-init1 - image: 'repl{{ LocalImageName "nginx:1.24-alpine" }}' - command: - - /bin/ash - - -c - - echo -n "FOOBARBAZ" > /tmp/foobarbaz diff --git a/e2e/kots-release-install-warning-preflights/troubleshoot.yaml b/e2e/kots-release-install-warning-preflights/troubleshoot.yaml deleted file mode 100644 index e817502d60..0000000000 --- a/e2e/kots-release-install-warning-preflights/troubleshoot.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: SupportBundle -metadata: - name: preflight-checks -spec: - collectors: - - clusterInfo: {} - - clusterResources: {} diff --git a/e2e/kots-release-install-warning-preflights/warning-preflights.yaml b/e2e/kots-release-install-warning-preflights/warning-preflights.yaml deleted file mode 100644 index 5baa582299..0000000000 --- a/e2e/kots-release-install-warning-preflights/warning-preflights.yaml +++ /dev/null @@ -1,53 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: HostPreflight -spec: - collectors: - - tcpPortStatus: - collectorName: Port 24 - port: 24 - - tcpPortStatus: - collectorName: Port 22 - port: 22 - analyzers: - - tcpPortStatus: - checkName: Port 24 - collectorName: Port 24 - outcomes: - - fail: - when: connection-refused - message: Connection to port 24 was refused. - - warn: - when: address-in-use - message: Another process was already listening on port 24. - - fail: - when: connection-timeout - message: Timed out connecting to port 24. - - fail: - when: error - message: Unexpected port status - - pass: - when: connected - message: Port 24 is available - - warn: - message: Unexpected port status - - tcpPortStatus: - checkName: Port 22 - collectorName: Port 22 - outcomes: - - fail: - when: connection-refused - message: Connection to port 22 was refused. - - warn: - when: address-in-use - message: Another process was already listening on port 22. - - fail: - when: connection-timeout - message: Timed out connecting to port 22. - - fail: - when: error - message: Unexpected port status - - pass: - when: connected - message: Port 22 is available - - warn: - message: Unexpected port status diff --git a/e2e/kots-release-install/cluster-config.yaml b/e2e/kots-release-install/cluster-config.yaml index 46abfdacb3..4a2818ecec 100644 --- a/e2e/kots-release-install/cluster-config.yaml +++ b/e2e/kots-release-install/cluster-config.yaml @@ -73,7 +73,7 @@ spec: digest: "" - name: goldpinger chartname: oci://ec-e2e-proxy.testcluster.net/anonymous/public.ecr.aws/q7i7m9q2/embedded-cluster-charts/goldpinger - namespace: goldpinger + namespace: embedded-cluster version: 6.1.2 order: 11 values: | diff --git a/e2e/kots-release-unsupported-overrides/backup.yaml b/e2e/kots-release-unsupported-overrides/backup.yaml deleted file mode 100644 index ce6f205deb..0000000000 --- a/e2e/kots-release-unsupported-overrides/backup.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: velero.io/v1 -kind: Backup -metadata: - name: backup - annotations: - preserve: me -spec: - ttl: 36h0m0s - includedNamespaces: - - kotsadm - - redis-app - - nginx-app - orLabelSelectors: - - matchExpressions: - - { key: kots.io/kotsadm, operator: NotIn, values: ["true"] } - hooks: - resources: - - name: test-hook - includedResources: - - 'pods' - labelSelector: - matchLabels: - app: example - component: nginx - pre: - - exec: - container: nginx - command: - - /bin/uname - - -a - post: - - exec: - command: - - /bin/uname - - -a diff --git a/e2e/kots-release-unsupported-overrides/cluster-config.yaml b/e2e/kots-release-unsupported-overrides/cluster-config.yaml deleted file mode 100644 index 3d9ef29eea..0000000000 --- a/e2e/kots-release-unsupported-overrides/cluster-config.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: embeddedcluster.replicated.com/v1beta1 -kind: Config -metadata: - name: "testconfig" -spec: - version: "__version_string__" - binaryOverrideUrl: "__release_url__" - metadataOverrideUrl: "__metadata_url__" - domains: - proxyRegistryDomain: "ec-e2e-proxy.testcluster.net" - replicatedAppDomain: "ec-e2e-replicated-app.testcluster.net" - unsupportedOverrides: - builtInExtensions: - - name: admin-console - values: | - labels: - release-custom-label: release-clustom-value - - name: embedded-cluster-operator - values: | - global: - labels: - release-custom-label: release-clustom-value - k0s: | - config: - metadata: - name: testing-overrides-k0s-name - spec: - telemetry: - enabled: true diff --git a/e2e/kots-release-unsupported-overrides/config.yaml b/e2e/kots-release-unsupported-overrides/config.yaml deleted file mode 100644 index f1c98e69a4..0000000000 --- a/e2e/kots-release-unsupported-overrides/config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: Config -spec: - groups: - - name: config_group - title: The First Config Group - items: - - name: hostname - title: Hostname - type: text - - name: pw - title: Password - type: password diff --git a/e2e/kots-release-unsupported-overrides/deployment-2.yaml b/e2e/kots-release-unsupported-overrides/deployment-2.yaml deleted file mode 100644 index 65c1960877..0000000000 --- a/e2e/kots-release-unsupported-overrides/deployment-2.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: second - labels: - app: second - replicated.com/disaster-recovery: app -spec: - replicas: 0 - selector: - matchLabels: - app: second - template: - metadata: - labels: - app: second - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - second - topologyKey: "kubernetes.io/hostname" - containers: - - name: nginx - image: us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test/nginx:1.24-alpine - resources: - limits: - memory: '32Mi' - cpu: '50m' diff --git a/e2e/kots-release-unsupported-overrides/deployment.yaml b/e2e/kots-release-unsupported-overrides/deployment.yaml deleted file mode 100644 index c4fed18dfd..0000000000 --- a/e2e/kots-release-unsupported-overrides/deployment.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx - labels: - app: example - component: nginx - replicated.com/disaster-recovery: app -spec: - replicas: 1 - selector: - matchLabels: - app: example - component: nginx - template: - metadata: - labels: - app: example - component: nginx - spec: - containers: - - name: nginx - image: us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test/nginx:1.24-alpine - resources: - limits: - memory: '64Mi' - cpu: '50m' - env: - - name: APP_SEQUENCE - value: "{{repl Cursor }}" - - name: APP_VERSION - value: "{{repl VersionLabel }}" - - name: APP_CHANNEL - value: "{{repl ChannelName }}" - - name: CONFIG_HOSTNAME - value: '{{repl ConfigOption "hostname" }}' - - name: CONFIG_PASSWORD - value: '{{repl ConfigOption "pw" }}' diff --git a/e2e/kots-release-unsupported-overrides/k8s-app.yaml b/e2e/kots-release-unsupported-overrides/k8s-app.yaml deleted file mode 100644 index c5f625eaed..0000000000 --- a/e2e/kots-release-unsupported-overrides/k8s-app.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: app.k8s.io/v1beta1 -kind: Application -metadata: - name: "nginx" -spec: - descriptor: - links: - - description: Open App - # needs to match applicationUrl in kots-app.yaml - url: "http://nginx" diff --git a/e2e/kots-release-unsupported-overrides/kots-app.yaml b/e2e/kots-release-unsupported-overrides/kots-app.yaml deleted file mode 100644 index 73aae5851b..0000000000 --- a/e2e/kots-release-unsupported-overrides/kots-app.yaml +++ /dev/null @@ -1,24 +0,0 @@ ---- -apiVersion: kots.io/v1beta1 -kind: Application -metadata: - name: nginx -spec: - title: Embedded Cluster Smoke Test Staging App - icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.png - proxyPublicImages: true - allowRollback: true - additionalNamespaces: - - "redis-app" - - "nginx-app" - statusInformers: - - deployment/nginx - - nginx-app/deployment/nginx-app - - nginx-app/deployment/nginx-app-client - - redis-app/deployment/redis-app - - redis-app/deployment/redis-app-client - ports: - - serviceName: "nginx" - servicePort: 80 - localPort: 8888 - applicationUrl: "http://nginx" diff --git a/e2e/kots-release-unsupported-overrides/kots-lint-config.yaml b/e2e/kots-release-unsupported-overrides/kots-lint-config.yaml deleted file mode 100644 index ba99c8b92a..0000000000 --- a/e2e/kots-release-unsupported-overrides/kots-lint-config.yaml +++ /dev/null @@ -1,90 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: LintConfig -metadata: - name: default-lint-config -spec: - rules: - - name: missing-kind-field - level: "error" - - name: missing-api-version-field - level: "error" - - name: preflight-spec - level: "warn" - - name: config-spec - level: "warn" - - name: troubleshoot-spec - level: "warn" - - name: application-spec - level: "warn" - - name: application-icon - level: "warn" - - name: application-statusInformers - level: "warn" - - name: invalid-target-kots-version - level: "error" - - name: invalid-min-kots-version - level: "error" - - name: invalid-kubernetes-installer - level: "error" - - name: deprecated-kubernetes-installer-version - level: "warn" - - name: duplicate-kots-kind - level: "error" - - name: invalid-helm-release-name - level: "error" - - name: duplicate-helm-release-name - level: "error" - - name: replicas-1 - level: "info" - - name: privileged - level: "info" - - name: allow-privilege-escalation - level: "info" - - name: container-image-latest-tag - level: "info" - - name: container-image-local-image-name - level: "error" - - name: container-resources - level: "info" - - name: container-resource-limits - level: "info" - - name: container-resource-requests - level: "info" - - name: resource-limits-cpu - level: "info" - - name: resource-limits-memory - level: "info" - - name: resource-requests-cpu - level: "info" - - name: resource-requests-memory - level: "info" - - name: volumes-host-paths - level: "info" - - name: volume-docker-sock - level: "info" - - name: hardcoded-namespace - level: "info" - - name: may-contain-secrets - level: "info" - - name: config-option-invalid-type - level: "error" - - name: repeat-option-missing-template - level: "error" - - name: repeat-option-missing-valuesByGroup - level: "error" - - name: repeat-option-malformed-yamlpath - level: "error" - - name: config-option-password-type - level: "warn" - - name: config-option-not-found - level: "warn" - - name: config-option-is-circular - level: "error" - - name: config-option-not-repeatable - level: "error" - - name: config-option-when-is-invalid - level: "error" - - name: config-option-invalid-regex-validator - level: "error" - - name: config-option-regex-validator-invalid-type - level: "error" diff --git a/e2e/kots-release-unsupported-overrides/nginx-app-helm-v1beta2.yaml b/e2e/kots-release-unsupported-overrides/nginx-app-helm-v1beta2.yaml deleted file mode 100644 index 0a16743190..0000000000 --- a/e2e/kots-release-unsupported-overrides/nginx-app-helm-v1beta2.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: kots.io/v1beta2 -kind: HelmChart -metadata: - name: nginx-app -spec: - chart: - name: nginx-app - chartVersion: 0.1.0 - namespace: nginx-app - values: - server: - image: - repository: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }}/repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "proxy/embedded-cluster-smoke-test-staging-app/us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test" }}/nginx - service: - type: ClusterIP - port: 80 - replicaCount: 1 - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - client: - enabled: true - image: - repository: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }}/repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "proxy/embedded-cluster-smoke-test-staging-app/us-east4-docker.pkg.dev/replicated-qa/ec-smoke-test" }}/curl - replicaCount: 1 - intervalSeconds: 60 - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - replicated: - enabled: true - image: - registry: repl{{ HasLocalRegistry | ternary LocalRegistryHost "ec-e2e-proxy.testcluster.net" }} - repository: repl{{ HasLocalRegistry | ternary LocalRegistryNamespace "anonymous/registry.replicated.com/library" }}/replicated-sdk-image - imagePullSecrets: - - name: '{{repl ImagePullSecretName }}' - builder: - replicated: - enabled: true diff --git a/e2e/kots-release-unsupported-overrides/preflight.yaml b/e2e/kots-release-unsupported-overrides/preflight.yaml deleted file mode 100644 index 3a9103a2a5..0000000000 --- a/e2e/kots-release-unsupported-overrides/preflight.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: Preflight -metadata: - name: preflight-checks -spec: - collectors: - - clusterInfo: {} - - clusterResources: {} - analyzers: - - customResourceDefinition: - customResourceDefinitionName: volumesnapshots.snapshot.storage.k8s.io - checkName: The Volume Snapshots CRD exists - outcomes: - - fail: - message: The Volume Snapshots CRD does not exist. - - pass: - message: The Volume Snapshots CRD exists. diff --git a/e2e/kots-release-unsupported-overrides/redis-app-helm-v1beta1.yaml b/e2e/kots-release-unsupported-overrides/redis-app-helm-v1beta1.yaml deleted file mode 100644 index 78b7d9368c..0000000000 --- a/e2e/kots-release-unsupported-overrides/redis-app-helm-v1beta1.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: kots.io/v1beta1 -kind: HelmChart -metadata: - name: redis-app -spec: - chart: - name: redis-app - chartVersion: 0.1.0 - namespace: redis-app - useHelmInstall: true - values: - server: - image: - repository: redis - tag: "7.2" - service: - type: ClusterIP - port: 6379 - client: - enabled: true - image: - repository: redis - tag: "7.2" - intervalSeconds: 5 diff --git a/e2e/kots-release-unsupported-overrides/restore.yaml b/e2e/kots-release-unsupported-overrides/restore.yaml deleted file mode 100644 index 25832f6087..0000000000 --- a/e2e/kots-release-unsupported-overrides/restore.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: velero.io/v1 -kind: Restore -metadata: - name: restore - annotations: - preserve: me -spec: - backupName: backup - includedNamespaces: - - '*' - hooks: - resources: - - name: restore-hook-1 - includedNamespaces: - - kotsadm - labelSelector: - matchLabels: - app: example - postHooks: - - init: - initContainers: - - name: restore-hook-init1 - image: 'repl{{ LocalImageName "nginx:1.24-alpine" }}' - command: - - /bin/ash - - -c - - echo -n "FOOBARBAZ" > /tmp/foobarbaz diff --git a/e2e/kots-release-unsupported-overrides/troubleshoot.yaml b/e2e/kots-release-unsupported-overrides/troubleshoot.yaml deleted file mode 100644 index e817502d60..0000000000 --- a/e2e/kots-release-unsupported-overrides/troubleshoot.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: troubleshoot.sh/v1beta2 -kind: SupportBundle -metadata: - name: preflight-checks -spec: - collectors: - - clusterInfo: {} - - clusterResources: {} diff --git a/e2e/kots-release-upgrade-v3/cluster-config.yaml b/e2e/kots-release-upgrade-v3/cluster-config.yaml index c0976d6d48..f2f07aea16 100644 --- a/e2e/kots-release-upgrade-v3/cluster-config.yaml +++ b/e2e/kots-release-upgrade-v3/cluster-config.yaml @@ -81,7 +81,7 @@ spec: digest: "" - name: goldpinger chartname: oci://ec-e2e-proxy.testcluster.net/anonymous/public.ecr.aws/q7i7m9q2/embedded-cluster-charts/goldpinger - namespace: goldpinger + namespace: embedded-cluster version: 6.1.2 order: 11 values: | diff --git a/e2e/kots-release-upgrade/cluster-config.yaml b/e2e/kots-release-upgrade/cluster-config.yaml index c0976d6d48..f2f07aea16 100644 --- a/e2e/kots-release-upgrade/cluster-config.yaml +++ b/e2e/kots-release-upgrade/cluster-config.yaml @@ -81,7 +81,7 @@ spec: digest: "" - name: goldpinger chartname: oci://ec-e2e-proxy.testcluster.net/anonymous/public.ecr.aws/q7i7m9q2/embedded-cluster-charts/goldpinger - namespace: goldpinger + namespace: embedded-cluster version: 6.1.2 order: 11 values: | diff --git a/e2e/local-artifact-mirror_test.go b/e2e/local-artifact-mirror_test.go deleted file mode 100644 index 67f8c56730..0000000000 --- a/e2e/local-artifact-mirror_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package e2e - -import ( - "strings" - "testing" - "time" - - "github.com/replicatedhq/embedded-cluster/e2e/cluster/docker" -) - -func TestLocalArtifactMirror(t *testing.T) { - t.Parallel() - - RequireEnvVars(t, []string{"SHORT_SHA"}) - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - LicensePath: "licenses/license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - - installSingleNodeWithOptions(t, tc, installOptions{ - localArtifactMirrorPort: "50001", - }) - - commands := [][]string{ - {"apt-get", "install", "curl", "-y"}, - {"systemctl", "status", "local-artifact-mirror"}, - {"systemctl", "stop", "local-artifact-mirror"}, - {"systemctl", "start", "local-artifact-mirror"}, - {"sleep", "10"}, - {"systemctl", "status", "local-artifact-mirror"}, - {"curl", "-o", "/tmp/kubectl-test", "127.0.0.1:50001/bin/kubectl"}, - {"chmod", "755", "/tmp/kubectl-test"}, - {"/tmp/kubectl-test", "version", "--client"}, - } - for _, cmd := range commands { - if stdout, stderr, err := tc.RunCommandOnNode(0, cmd); err != nil { - t.Fatalf("fail testing local artifact mirror: %v: %s: %s", err, stdout, stderr) - } - } - - command := []string{"cp", "/etc/passwd", "/var/log/embedded-cluster/passwd"} - if stdout, stderr, err := tc.RunCommandOnNode(0, command); err != nil { - t.Fatalf("fail to copy file: %v: %s: %s", err, stdout, stderr) - } - - command = []string{"curl", "-O", "--fail", "127.0.0.1:50001/passwd"} - t.Logf("running %v", command) - if _, _, err := tc.RunCommandOnNode(0, command); err == nil { - t.Fatalf("we should not be able to fetch logs from local artifact mirror") - } - - command = []string{"curl", "-O", "--fail", "127.0.0.1:50001/../../../etc/passwd"} - t.Logf("running %v", command) - if _, _, err := tc.RunCommandOnNode(0, command); err == nil { - t.Fatalf("we should not be able to fetch paths with ../") - } - - command = []string{"curl", "-I", "--fail", "127.0.0.1:50001/bin/kubectl"} - t.Logf("running %v", command) - if stdout, stderr, err := tc.RunCommandOnNode(0, command); err != nil { - t.Fatalf("we should be able to fetch the kubectl binary in the bin directory: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("testing local artifact mirror restart after materialize") - command = []string{"embedded-cluster", "materialize"} - if stdout, stderr, err := tc.RunCommandOnNode(0, command); err != nil { - t.Fatalf("fail materialize embedded cluster binaries: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("waiting to verify if local artifact mirror has restarted") - time.Sleep(20 * time.Second) - - command = []string{"journalctl", "-u", "local-artifact-mirror"} - stdout, stderr, err := tc.RunCommandOnNode(0, command) - if err != nil { - t.Fatalf("fail to get journalctl logs: %v: %s: %s", err, stdout, stderr) - } - - expected := []string{ - "Binary changed, sending signal to stop", - "Scheduled restart job, restart counter is at", - } - for _, str := range expected { - if !strings.Contains(stdout, str) { - t.Fatalf("expected %q in journalctl logs, got %q", str, stdout) - } - t.Logf("found %q in journalctl logs", str) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} diff --git a/e2e/materialize_test.go b/e2e/materialize_test.go deleted file mode 100644 index aa25542038..0000000000 --- a/e2e/materialize_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package e2e - -import ( - "testing" - "time" - - "github.com/replicatedhq/embedded-cluster/e2e/cluster/docker" -) - -func TestMaterialize(t *testing.T) { - t.Parallel() - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - ECBinaryPath: "../output/bin/embedded-cluster-original", - }) - defer tc.Cleanup() - - commands := [][]string{ - {"rm", "-rf", "/var/lib/embedded-cluster/bin/kubectl"}, - {"rm", "-rf", "/var/lib/embedded-cluster/bin/kubectl-preflight"}, - {"rm", "-rf", "/var/lib/embedded-cluster/bin/kubectl-support_bundle"}, - {"rm", "-rf", "/var/lib/embedded-cluster/bin/fio"}, - {"embedded-cluster", "materialize"}, - {"ls", "-la", "/var/lib/embedded-cluster/bin/kubectl"}, - {"ls", "-la", "/var/lib/embedded-cluster/bin/kubectl-preflight"}, - {"ls", "-la", "/var/lib/embedded-cluster/bin/kubectl-support_bundle"}, - {"ls", "-la", "/var/lib/embedded-cluster/bin/fio"}, - } - for _, cmd := range commands { - if stdout, stderr, err := tc.RunCommandOnNode(0, cmd); err != nil { - t.Fatalf("fail to run command %q: %v: %s: %s", cmd, err, stdout, stderr) - } - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} diff --git a/e2e/playwright/package-lock.json b/e2e/playwright/package-lock.json index 11112e529c..b48cee35da 100644 --- a/e2e/playwright/package-lock.json +++ b/e2e/playwright/package-lock.json @@ -31,9 +31,9 @@ } }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/e2e/reset_test.go b/e2e/reset_test.go deleted file mode 100644 index 3597020235..0000000000 --- a/e2e/reset_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package e2e - -import ( - "testing" - "time" - - "github.com/replicatedhq/embedded-cluster/e2e/cluster/docker" -) - -// This test creates 4 nodes, installs on the first one and then generate 2 join tokens -// for controllers and one join token for worker nodes. Joins the nodes and then waits -// for them to report ready and resets two of the nodes. -func TestMultiNodeReset(t *testing.T) { - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 4, - Distro: "debian-bookworm", - LicensePath: "licenses/license.yaml", - ECBinaryPath: "../output/bin/embedded-cluster", - }) - defer tc.Cleanup() - - installSingleNode(t, tc) - - if stdout, stderr, err := tc.SetupPlaywrightAndRunTest("deploy-app"); err != nil { - t.Fatalf("fail to run playwright test deploy-app: %v: %s: %s", err, stdout, stderr) - } - - // join a controller node - joinControllerNode(t, tc, 1) - - // XXX If we are too aggressive joining nodes we can see the following error being - // thrown by kotsadm on its log (and we get a 500 back): - // " - // failed to get controller role name: failed to get cluster config: failed to get - // current installation: failed to list installations: etcdserver: leader changed - // " - t.Logf("node 1 joined, sleeping...") - time.Sleep(30 * time.Second) - - // join another controller node - joinControllerNode(t, tc, 2) - - // join a worker node - joinWorkerNode(t, tc, 3) - - // wait for the nodes to report as ready. - waitForNodes(t, tc, 4, nil) - - checkInstallationState(t, tc) - - bin := "embedded-cluster" - // reset worker node - t.Logf("%s: resetting worker node", time.Now().Format(time.RFC3339)) - stdout, stderr, err := tc.RunCommandOnNode(3, []string{bin, "reset", "--yes"}) - if err != nil { - t.Fatalf("fail to reset worker node 3: %v: %s: %s", err, stdout, stderr) - } - - // reset a controller node - // this should fail with a prompt to override - t.Logf("%s: resetting controller node", time.Now().Format(time.RFC3339)) - stdout, stderr, err = tc.RunCommandOnNode(2, []string{bin, "reset", "--yes"}) - if err != nil { - t.Fatalf("fail to remove controller node 2: %v: %s: %s", err, stdout, stderr) - } - - stdout, stderr, err = tc.RunCommandOnNode(0, []string{"check-nodes-removed.sh", "2"}) - if err != nil { - t.Fatalf("fail to check nodes removed: %v: %s: %s", err, stdout, stderr) - } - - checkInstallationState(t, tc) - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} diff --git a/e2e/scripts/check-velero-state.sh b/e2e/scripts/check-velero-state.sh deleted file mode 100755 index 34f8d33c25..0000000000 --- a/e2e/scripts/check-velero-state.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -set -euox pipefail - -DIR=/usr/local/bin -. $DIR/common.sh - -wait_for_velero_pods() { - ready=$(kubectl get pods -n velero -o jsonpath='{.items[*].metadata.name} {.items[*].status.phase}' | grep "velero" | grep -c Running || true) - counter=0 - while [ "$ready" -lt "1" ]; do - if [ "$counter" -gt 36 ]; then - echo "velero pods did not appear" - kubectl get pods -n velero -o jsonpath='{.items[*].metadata.name} {.items[*].status.phase}' - kubectl get pods -n velero - return 1 - fi - sleep 5 - counter=$((counter+1)) - echo "Waiting for velero pods" - ready=$(kubectl get pods -n velero -o jsonpath='{.items[*].metadata.name} {.items[*].status.phase}' | grep "velero" | grep -c Running || true) - kubectl get pods -n velero 2>&1 || true - echo "ready: $ready" - done -} - -main() { - sleep 50 - - kubectl get pods -A - kubectl get installations -o yaml - kubectl get charts -A - - if ! wait_for_velero_pods; then - echo "Failed waiting for velero" - exit 1 - fi -} - -main "$@" diff --git a/e2e/scripts/embedded-preflight.sh b/e2e/scripts/embedded-preflight.sh deleted file mode 100755 index 9c01f45c2f..0000000000 --- a/e2e/scripts/embedded-preflight.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash -set -euox pipefail - -DIR=/usr/local/bin -. $DIR/common.sh - - -has_applied_host_preflight() { - if ! grep -q "Another process was already listening on port 22" /tmp/log ; then - return 1 - fi -} - -main() { - echo "installing with failing preflights" - if /usr/local/bin/embedded-cluster-failing-preflights install --yes --license /assets/license.yaml 2>&1 | tee /tmp/log ; then - cat /tmp/log - echo "preflight_with_failure: Expected installation to fail" - exit 1 - fi - if ! has_applied_host_preflight; then - echo "preflight_with_failure: Install hasn't applied host preflight" - cat /tmp/log - exit 1 - fi - if ! has_stored_host_preflight_results; then - echo "preflight_with_failure: Install hasn't stored host preflight results to disk" - cat /tmp/log - exit 1 - fi - rm "${EMBEDDED_CLUSTER_BASE_DIR}/support/host-preflight-results.json" - mv /tmp/log /tmp/log-failure - - # Warnings should not fail installations - echo "running preflights with warning preflights" - if ! /usr/local/bin/embedded-cluster install run-preflights --yes --license /assets/license.yaml 2>&1 | tee /tmp/log ; then - cat /etc/os-release - echo "preflight_with_warning: Failed to run embedded-cluster preflights" - exit 1 - fi - echo "installing with warning preflights" - if ! /usr/local/bin/embedded-cluster install --yes --license /assets/license.yaml 2>&1 | tee /tmp/log ; then - cat /etc/os-release - echo "preflight_with_warning: Failed to install embedded-cluster" - exit 1 - fi - if ! grep -q "Admin Console is ready" /tmp/log; then - echo "preflight_with_warning: Failed to validate that the Admin Console is ready" - exit 1 - fi - if ! has_applied_host_preflight; then - echo "preflight_with_warning: Install hasn't applied host preflight" - cat /tmp/log - exit 1 - fi - if ! has_stored_host_preflight_results; then - echo "preflight_with_warning: Install hasn't stored host preflight results to disk" - cat /tmp/log - exit 1 - fi - if ! wait_for_healthy_node; then - echo "Failed to wait for healthy node" - exit 1 - fi - if ! systemctl restart "${EMBEDDED_CLUSTER_BIN}"; then - echo "Failed to restart embedded-cluster service" - exit 1 - fi -} - -main diff --git a/e2e/scripts/single-node-host-preflight-install.sh b/e2e/scripts/single-node-host-preflight-install.sh deleted file mode 100755 index 82175a9498..0000000000 --- a/e2e/scripts/single-node-host-preflight-install.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euox pipefail - -DIR=/usr/local/bin -. $DIR/common.sh - -main() { - if ! embedded-cluster install run-preflights --yes --license /assets/license.yaml 2>&1 | tee /tmp/log ; then - cat /etc/os-release - echo "Failed to install embedded-cluster" - exit 1 - fi - if ! embedded-cluster install --yes --license /assets/license.yaml 2>&1 | tee /tmp/log ; then - cat /etc/os-release - echo "Failed to install embedded-cluster" - exit 1 - fi - if ! grep -q "Admin Console is ready" /tmp/log; then - echo "Failed to validate that the Admin Console is ready" - exit 1 - fi - if ! has_stored_host_preflight_results; then - echo "Install hasn't stored host preflight results to disk" - cat /tmp/log - exit 1 - fi - if ! wait_for_healthy_node; then - echo "Failed to wait for healthy node" - exit 1 - fi -} - -main diff --git a/e2e/scripts/single-node-install.sh b/e2e/scripts/single-node-install.sh index 6b5fbcb5cb..5660e0ba15 100755 --- a/e2e/scripts/single-node-install.sh +++ b/e2e/scripts/single-node-install.sh @@ -87,6 +87,11 @@ main() { kubectl get storageclass -A exit 1 fi + if ! has_stored_host_preflight_results; then + echo "Install hasn't stored host preflight results to disk" + cat /tmp/log + exit 1 + fi if ! grep -q "Admin Console is ready" /tmp/log; then echo "Failed to validate that the Admin Console is ready" exit 1 diff --git a/e2e/unsupported-overrides_test.go b/e2e/unsupported-overrides_test.go deleted file mode 100644 index 2bc11016b1..0000000000 --- a/e2e/unsupported-overrides_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package e2e - -import ( - "fmt" - "os" - "testing" - "time" - - "github.com/replicatedhq/embedded-cluster/e2e/cluster/docker" -) - -func TestUnsupportedOverrides(t *testing.T) { - t.Parallel() - - tc := docker.NewCluster(&docker.ClusterInput{ - T: t, - Nodes: 1, - Distro: "debian-bookworm", - }) - defer tc.Cleanup() - - downloadECReleaseWithOptions(t, tc, 0, downloadECReleaseOptions{ - version: fmt.Sprintf("appver-%s-unsupported-overrides", os.Getenv("SHORT_SHA")), - }) - - t.Logf("%s: installing embedded-cluster with unsupported overrides on node 0", time.Now().Format(time.RFC3339)) - line := []string{"unsupported-overrides.sh"} - if stdout, stderr, err := tc.RunCommandOnNode(0, line); err != nil { - t.Fatalf("fail to install embedded-cluster: %v: %s: %s", err, stdout, stderr) - } - - t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) -} diff --git a/go.mod b/go.mod index 4a028688a9..6c388f9494 100644 --- a/go.mod +++ b/go.mod @@ -27,16 +27,17 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/gosimple/slug v1.15.0 + github.com/hashicorp/go-retryablehttp v0.7.8 github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/k0sproject/k0s v1.33.5-0.20250819091818-6da1d9c31be6 github.com/mattn/go-isatty v0.0.20 github.com/ohler55/ojg v1.26.10 - github.com/onsi/ginkgo/v2 v2.26.0 + github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/pkg/errors v0.9.1 github.com/replicatedhq/embedded-cluster/kinds v0.0.0 github.com/replicatedhq/embedded-cluster/utils v0.0.0 - github.com/replicatedhq/kotskinds v0.0.0-20250609144916-baa60600998c + github.com/replicatedhq/kotskinds v0.0.0-20251024162531-2174a5b85a4d github.com/replicatedhq/troubleshoot v0.123.12 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.1 @@ -52,6 +53,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.43.0 golang.org/x/term v0.36.0 + gopkg.in/yaml.v2 v2.4.0 gotest.tools v2.2.0+incompatible helm.sh/helm/v3 v3.19.0 k8s.io/api v0.34.1 @@ -74,21 +76,21 @@ replace ( require ( cel.dev/expr v0.24.0 // indirect - cloud.google.com/go v0.121.1 // indirect - cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go v0.121.6 // indirect + cloud.google.com/go/auth v0.16.5 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.7.0 // indirect + cloud.google.com/go/compute/metadata v0.8.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect - cloud.google.com/go/storage v1.55.0 // indirect + cloud.google.com/go/storage v1.56.2 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -116,7 +118,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect + github.com/c9s/goprocinfo v0.0.0-20190309065803-0b2ad9ac246b // indirect github.com/casbin/govaluate v1.10.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect @@ -130,7 +132,7 @@ require ( github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/platforms v1.0.0-rc.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect @@ -190,7 +192,7 @@ require ( github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect @@ -230,7 +232,7 @@ require ( github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microsoft/go-mssqldb v1.9.3 // indirect github.com/miekg/dns v1.1.68 // indirect @@ -245,6 +247,7 @@ require ( github.com/moby/spdystream v0.5.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -276,7 +279,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect github.com/segmentio/ksuid v1.0.4 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sergi/go-diff v1.4.0 // indirect github.com/shirou/gopsutil/v4 v4.25.9 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sigstore/fulcio v1.6.6 // indirect @@ -317,13 +320,13 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/mod v0.29.0 // indirect @@ -335,15 +338,14 @@ require ( golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.38.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/api v0.241.0 // indirect - google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/api v0.249.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.34.1 // indirect k8s.io/component-base v0.34.1 // indirect diff --git a/go.sum b/go.sum index 7db60f97b8..c1ebe8e39a 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw= -cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= @@ -17,16 +17,16 @@ cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFs cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= -cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0= -cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY= +cloud.google.com/go/storage v1.56.2 h1:DzxQ4ppJe4OSTtZLtCqscC3knyW919eNl0zLLpojnqo= +cloud.google.com/go/storage v1.56.2/go.mod h1:C9xuCZgFl3buo2HZU/1FncgvvOgTAs/rnh4gF4lMg0s= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= @@ -50,12 +50,12 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -142,8 +142,9 @@ github.com/bombsimon/logrusr/v4 v4.1.0 h1:uZNPbwusB0eUXlO8hIUwStE6Lr5bLN6IgYgG+7 github.com/bombsimon/logrusr/v4 v4.1.0/go.mod h1:pjfHC5e59CvjTBIU3V3sGhFWFAnsnhOR03TRc6im0l8= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f h1:tRk+aBit+q3oqnj/1mF5HHhP2yxJM2lSa0afOJxQ3nE= github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= +github.com/c9s/goprocinfo v0.0.0-20190309065803-0b2ad9ac246b h1:4yfM1Zm+7U+m0inJ0g6JvdqGePXD8eG4nXUTbcLT6gk= +github.com/c9s/goprocinfo v0.0.0-20190309065803-0b2ad9ac246b/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= github.com/canonical/lxd v0.0.0-20251020131417-2c7a72f62ebd h1:PA59vJe841TYDnmDR3T4mB1+cKsgTlDQ+ycosKAFMiU= github.com/canonical/lxd v0.0.0-20251020131417-2c7a72f62ebd/go.mod h1:GLQ1aiMp7K4ZhSjjaGtQj6DqxMIpwllKTFAg/B+Ob6k= github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= @@ -186,8 +187,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= @@ -288,8 +289,8 @@ github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BN github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= -github.com/gkampitakis/go-snaps v0.5.14 h1:3fAqdB6BCPKHDMHAKRwtPUwYexKtGrNuw8HX/T/4neo= -github.com/gkampitakis/go-snaps v0.5.14/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -405,8 +406,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -434,8 +435,12 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-getter v1.8.2 h1:CGCK+bZQLl44PYiwJweVzfpjg7bBwtuXu3AGcLiod2o= github.com/hashicorp/go-getter v1.8.2/go.mod h1:CUTt9x2bCtJ/sV8ihgrITL3IUE+0BE1j/e4n5P/GIM4= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= @@ -536,8 +541,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= @@ -576,8 +581,8 @@ github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCnd github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= @@ -617,8 +622,8 @@ github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE= -github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= @@ -661,8 +666,6 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/proglottis/gpgme v0.1.4 h1:3nE7YNA70o2aLjcg63tXMOhPD7bplfE5CBdV+hLAm2M= github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -690,8 +693,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/replicatedhq/kotskinds v0.0.0-20250609144916-baa60600998c h1:lnCL/wYi2BFTnxOP/lmo9WJwVPG3fk/plgJ/9NrMFw4= -github.com/replicatedhq/kotskinds v0.0.0-20250609144916-baa60600998c/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= +github.com/replicatedhq/kotskinds v0.0.0-20251024162531-2174a5b85a4d h1:N8t9W5SYs1MKPsuAp4PA5Haje4cOyCyubAq65qB1wzE= +github.com/replicatedhq/kotskinds v0.0.0-20251024162531-2174a5b85a4d/go.mod h1:+k4PHo2wukoU9kdiKrqqgi89Wmj+9AiwppYGVK11zig= github.com/replicatedhq/troubleshoot v0.123.12 h1:XbgZJMSwIHyf1lvxIRNwI9AVsRzcA7N3AWLPLSkrr+w= github.com/replicatedhq/troubleshoot v0.123.12/go.mod h1:CKPCj8si77XuSL6sIAFdqtO23/eha159eEBlQF8HpVw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -712,8 +715,8 @@ github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3 github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU= github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -845,8 +848,8 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQ go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= @@ -861,8 +864,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZF go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA= go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= @@ -885,8 +888,6 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -1050,15 +1051,15 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE= -google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w= +google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f h1:OiFuztEyBivVKDvguQJYWq1yDcfAHIID/FVrPR4oiI0= google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f/go.mod h1:kprOiu9Tr0JYyD6DORrc4Hfyk3RFXqkQ3ctHEum3ZbM= google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4= diff --git a/hack/test-installation.yaml b/hack/test-installation.yaml index 6902906505..495fa23782 100644 --- a/hack/test-installation.yaml +++ b/hack/test-installation.yaml @@ -35,7 +35,7 @@ spec: version: 4.12.0-beta.0 - chartname: okgolove/goldpinger name: goldpinger - namespace: goldpinger + namespace: embedded-cluster order: 11 values: | image: diff --git a/operator/charts/embedded-cluster-operator/templates/embedded-cluster-troubleshoot-goldpinger.yaml b/operator/charts/embedded-cluster-operator/templates/embedded-cluster-troubleshoot-goldpinger.yaml index 3b0f9cb51d..6a67cf288c 100644 --- a/operator/charts/embedded-cluster-operator/templates/embedded-cluster-troubleshoot-goldpinger.yaml +++ b/operator/charts/embedded-cluster-operator/templates/embedded-cluster-troubleshoot-goldpinger.yaml @@ -18,10 +18,12 @@ data: spec: collectors: - goldpinger: - namespace: goldpinger + namespace: {{ .Release.Namespace }} image: {{ .Values.goldpingerImage }} + collectDelay: 15s podLaunchOptions: image: {{ .Values.utilsImage }} + namespace: {{ .Release.Namespace }} exclude: {{ .Values.isAirgap }} analyzers: - goldpinger: diff --git a/pkg-new/k0s/interface.go b/pkg-new/k0s/interface.go index ac5f145801..e0c9971021 100644 --- a/pkg-new/k0s/interface.go +++ b/pkg-new/k0s/interface.go @@ -37,7 +37,8 @@ type K0sInterface interface { GetStatus(ctx context.Context) (*K0sStatus, error) Install(rc runtimeconfig.RuntimeConfig, hostname string) error IsInstalled() (bool, error) - WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) + NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) + WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error PatchK0sConfig(path string, patch string) error WaitForK0s() error } @@ -54,8 +55,12 @@ func IsInstalled() (bool, error) { return _k0s.IsInstalled() } -func WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - return _k0s.WriteK0sConfig(ctx, networkInterface, airgapBundle, podCIDR, serviceCIDR, eucfg, mutate) +func NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { + return _k0s.NewK0sConfig(networkInterface, isAirgap, podCIDR, serviceCIDR, eucfg, mutate) +} + +func WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { + return _k0s.WriteK0sConfig(ctx, cfg) } func PatchK0sConfig(path string, patch string) error { diff --git a/pkg-new/k0s/k0s.go b/pkg-new/k0s/k0s.go index b88d686ca4..3c980ee269 100644 --- a/pkg-new/k0s/k0s.go +++ b/pkg-new/k0s/k0s.go @@ -95,7 +95,7 @@ func (k *K0s) IsInstalled() (bool, error) { } // NewK0sConfig creates a new k0sv1beta1.ClusterConfig object from the input parameters. -func NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { +func (k *K0s) NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { var embCfgSpec *ecv1beta1.ConfigSpec if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { embCfgSpec = &embCfg.Spec @@ -136,35 +136,30 @@ func NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, servic // WriteK0sConfig creates a new k0s.yaml configuration file. The file is saved in the // global location (as returned by runtimeconfig.K0sConfigPath). If a file already sits // there, this function returns an error. -func (k *K0s) WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - cfg, err := NewK0sConfig(networkInterface, airgapBundle != "", podCIDR, serviceCIDR, eucfg, mutate) - if err != nil { - return nil, fmt.Errorf("unable to create k0s config: %w", err) - } - +func (k *K0s) WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { cfgpath := runtimeconfig.K0sConfigPath if _, err := os.Stat(cfgpath); err == nil { - return nil, fmt.Errorf("configuration file already exists") + return fmt.Errorf("configuration file already exists") } if err := os.MkdirAll(filepath.Dir(cfgpath), 0755); err != nil { - return nil, fmt.Errorf("unable to create directory: %w", err) + return fmt.Errorf("unable to create directory: %w", err) } // This is necessary to install the previous version of k0s in e2e tests // TODO: remove this once the previous version is > 1.29 unstructured, err := helpers.K0sClusterConfigTo129Compat(cfg) if err != nil { - return nil, fmt.Errorf("unable to convert cluster config to 1.29 compat: %w", err) + return fmt.Errorf("unable to convert cluster config to 1.29 compat: %w", err) } data, err := k8syaml.Marshal(unstructured) if err != nil { - return nil, fmt.Errorf("unable to marshal config: %w", err) + return fmt.Errorf("unable to marshal config: %w", err) } if err := os.WriteFile(cfgpath, data, 0600); err != nil { - return nil, fmt.Errorf("unable to write config file: %w", err) + return fmt.Errorf("unable to write config file: %w", err) } - return cfg, nil + return nil } // applyUnsupportedOverrides applies overrides to the k0s configuration. Applies the diff --git a/pkg-new/k0s/mock.go b/pkg-new/k0s/mock.go index 1ab36da6d7..d64616c6d1 100644 --- a/pkg-new/k0s/mock.go +++ b/pkg-new/k0s/mock.go @@ -37,12 +37,21 @@ func (m *MockK0s) IsInstalled() (bool, error) { return args.Bool(0), args.Error(1) } -// WriteK0sConfig mocks the WriteK0sConfig method -func (m *MockK0s) WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - args := m.Called(ctx, networkInterface, airgapBundle, podCIDR, serviceCIDR, eucfg, mutate) +// NewK0sConfig mocks the NewK0sConfig method +func (m *MockK0s) NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { + args := m.Called(networkInterface, isAirgap, podCIDR, serviceCIDR, eucfg, mutate) + if args.Get(0) == nil { + return nil, args.Error(1) + } return args.Get(0).(*k0sv1beta1.ClusterConfig), args.Error(1) } +// WriteK0sConfig mocks the WriteK0sConfig method +func (m *MockK0s) WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { + args := m.Called(ctx, cfg) + return args.Error(0) +} + // PatchK0sConfig mocks the PatchK0sConfig method func (m *MockK0s) PatchK0sConfig(path string, patch string) error { args := m.Called(path, patch) diff --git a/pkg-new/replicatedapi/client.go b/pkg-new/replicatedapi/client.go new file mode 100644 index 0000000000..f2cfcdb9e5 --- /dev/null +++ b/pkg-new/replicatedapi/client.go @@ -0,0 +1,158 @@ +package replicatedapi + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/hashicorp/go-retryablehttp" + "github.com/replicatedhq/embedded-cluster/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/versions" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + kyaml "sigs.k8s.io/yaml" +) + +var defaultHTTPClient = newRetryableHTTPClient() + +type Client interface { + SyncLicense(ctx context.Context) (*kotsv1beta1.License, []byte, error) +} + +type client struct { + replicatedAppURL string + license *kotsv1beta1.License + releaseData *release.ReleaseData + clusterID string + httpClient *retryablehttp.Client +} + +type ClientOption func(*client) + +func WithClusterID(clusterID string) ClientOption { + return func(c *client) { + c.clusterID = clusterID + } +} + +func WithHTTPClient(httpClient *retryablehttp.Client) ClientOption { + return func(c *client) { + c.httpClient = httpClient + } +} + +func NewClient(replicatedAppURL string, license *kotsv1beta1.License, releaseData *release.ReleaseData, opts ...ClientOption) (Client, error) { + c := &client{ + replicatedAppURL: replicatedAppURL, + license: license, + releaseData: releaseData, + httpClient: defaultHTTPClient, + } + for _, opt := range opts { + opt(c) + } + if _, err := c.getChannelFromLicense(); err != nil { + return nil, err + } + return c, nil +} + +// SyncLicense fetches the latest license from the Replicated API +func (c *client) SyncLicense(ctx context.Context) (*kotsv1beta1.License, []byte, error) { + u := fmt.Sprintf("%s/license/%s", c.replicatedAppURL, c.license.Spec.AppSlug) + + params := url.Values{} + params.Set("licenseSequence", fmt.Sprintf("%d", c.license.Spec.LicenseSequence)) + if c.releaseData != nil && c.releaseData.ChannelRelease != nil { + params.Set("selectedChannelId", c.releaseData.ChannelRelease.ChannelID) + } + u = fmt.Sprintf("%s?%s", u, params.Encode()) + + req, err := c.newRetryableRequest(ctx, http.MethodGet, u, nil) + if err != nil { + return nil, nil, fmt.Errorf("create request: %w", err) + } + + req.Header.Set("Accept", "application/yaml") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("execute request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("read response body: %w", err) + } + + var licenseResp kotsv1beta1.License + if err := kyaml.Unmarshal(body, &licenseResp); err != nil { + return nil, nil, fmt.Errorf("unmarshal license response: %w", err) + } + + if licenseResp.Spec.LicenseID == "" { + return nil, nil, fmt.Errorf("license is empty") + } + + c.license = &licenseResp + + if _, err := c.getChannelFromLicense(); err != nil { + return nil, nil, fmt.Errorf("get channel from license: %w", err) + } + + return &licenseResp, body, nil +} + +// newRetryableRequest returns a retryablehttp.Request object with kots defaults set, including a User-Agent header. +func (c *client) newRetryableRequest(ctx context.Context, method string, url string, body io.Reader) (*retryablehttp.Request, error) { + req, err := retryablehttp.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, err + } + + c.injectHeaders(req.Header) + + return req, nil +} + +// injectHeaders injects the basic auth header, user agent header, and reporting info headers into the http.Header. +func (c *client) injectHeaders(header http.Header) { + header.Set("Authorization", "Basic "+basicAuth(c.license.Spec.LicenseID, c.license.Spec.LicenseID)) + header.Set("User-Agent", fmt.Sprintf("Embedded-Cluster/%s", versions.Version)) + + c.injectReportingInfoHeaders(header) +} + +func (c *client) getChannelFromLicense() (*kotsv1beta1.Channel, error) { + if c.releaseData == nil || c.releaseData.ChannelRelease == nil || c.releaseData.ChannelRelease.ChannelID == "" { + return nil, fmt.Errorf("channel release is empty") + } + if c.license == nil || c.license.Spec.LicenseID == "" { + return nil, fmt.Errorf("license is empty") + } + for _, channel := range c.license.Spec.Channels { + if channel.ChannelID == c.releaseData.ChannelRelease.ChannelID { + return &channel, nil + } + } + if c.license.Spec.ChannelID == c.releaseData.ChannelRelease.ChannelID { + return &kotsv1beta1.Channel{ + ChannelID: c.license.Spec.ChannelID, + ChannelName: c.license.Spec.ChannelName, + }, nil + } + return nil, fmt.Errorf("channel %s not found in license", c.releaseData.ChannelRelease.ChannelID) +} + +func basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} diff --git a/pkg-new/replicatedapi/client_test.go b/pkg-new/replicatedapi/client_test.go new file mode 100644 index 0000000000..3b8e97a8df --- /dev/null +++ b/pkg-new/replicatedapi/client_test.go @@ -0,0 +1,391 @@ +package replicatedapi + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/replicatedhq/embedded-cluster/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/versions" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestSyncLicense(t *testing.T) { + tests := []struct { + name string + license kotsv1beta1.License + releaseData *release.ReleaseData + serverHandler func(t *testing.T) http.HandlerFunc + expectedLicense *kotsv1beta1.License + wantErr string + }{ + { + name: "successful license sync", + license: kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 5, + ChannelID: "test-channel-123", + ChannelName: "Stable", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + releaseData: &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", + }, + }, + serverHandler: func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Validate request + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/license/test-app", r.URL.Path) + assert.Equal(t, "5", r.URL.Query().Get("licenseSequence")) + assert.Equal(t, "test-channel-123", r.URL.Query().Get("selectedChannelId")) + assert.Equal(t, "application/yaml", r.Header.Get("Accept")) + + // Validate auth header + authHeader := r.Header.Get("Authorization") + assert.NotEmpty(t, authHeader) + assert.Contains(t, authHeader, "Basic ") + + // Return response as YAML + resp := kotsv1beta1.License{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kots.io/v1beta1", + Kind: "License", + }, + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 6, + CustomerName: "Test Customer", + ChannelID: "test-channel-123", + ChannelName: "Stable", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + } + + w.WriteHeader(http.StatusOK) + yaml.NewEncoder(w).Encode(resp) + } + }, + expectedLicense: &kotsv1beta1.License{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kots.io/v1beta1", + Kind: "License", + }, + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 6, + CustomerName: "Test Customer", + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + { + name: "returns error on 401 unauthorized", + license: kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "wrong-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + releaseData: &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", + }, + }, + serverHandler: func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("unauthorized")) + } + }, + wantErr: "unexpected status code 401", + }, + { + name: "returns error on 404 not found", + license: kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "nonexistent-app", + LicenseID: "test-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + releaseData: &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", + }, + }, + serverHandler: func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("license not found")) + } + }, + wantErr: "unexpected status code 404", + }, + { + name: "returns error on 500 internal server error", + license: kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + releaseData: &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", + }, + }, + serverHandler: func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + } + }, + wantErr: "unexpected status code 500", + }, + { + name: "returns error on invalid YAML response", + license: kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + }, + releaseData: &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", + }, + }, + serverHandler: func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("invalid yaml")) + } + }, + wantErr: "unmarshal license response", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + + // Create test server + server := httptest.NewServer(tt.serverHandler(t)) + defer server.Close() + + // Create client + c, err := NewClient(server.URL, &tt.license, tt.releaseData) + req.NoError(err) + + // Execute test + license, rawLicense, err := c.SyncLicense(context.Background()) + + // Validate results + if tt.wantErr != "" { + req.Error(err) + req.Contains(err.Error(), tt.wantErr) + req.Nil(license) + req.Nil(rawLicense) + } else { + req.NoError(err) + req.NotNil(license) + req.NotNil(rawLicense) + assert.Equal(t, tt.expectedLicense.Spec.AppSlug, license.Spec.AppSlug) + assert.Equal(t, tt.expectedLicense.Spec.LicenseID, license.Spec.LicenseID) + assert.Equal(t, tt.expectedLicense.Spec.LicenseSequence, license.Spec.LicenseSequence) + + // Validate raw license is valid YAML + var parsedLicense kotsv1beta1.License + err = yaml.Unmarshal(rawLicense, &parsedLicense) + req.NoError(err, "rawLicense should be valid YAML") + } + }) + } +} + +func TestGetReportingInfoHeaders(t *testing.T) { + tests := []struct { + name string + clusterID string + expectedCount int + checkHeaders map[string]string + }{ + { + name: "with cluster ID", + clusterID: "cluster-123", + expectedCount: 7, // EmbeddedClusterID, ChannelID, ChannelName, K8sVersion, K8sDistribution, EmbeddedClusterVersion, IsKurl + checkHeaders: map[string]string{ + "X-Replicated-EmbeddedClusterID": "cluster-123", + "X-Replicated-DownstreamChannelID": "test-channel-123", + "X-Replicated-DownstreamChannelName": "Stable", + "X-Replicated-K8sVersion": versions.K0sVersion, + "X-Replicated-K8sDistribution": DistributionEmbeddedCluster, + "X-Replicated-EmbeddedClusterVersion": versions.Version, + "X-Replicated-IsKurl": "false", + }, + }, + { + name: "zero values should be skipped", + clusterID: "", + expectedCount: 6, // ChannelID, ChannelName, K8sVersion, K8sDistribution, EmbeddedClusterVersion, IsKurl + checkHeaders: map[string]string{ + "X-Replicated-IsKurl": "false", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := require.New(t) + + license := kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + ChannelName: "Stable", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + } + + releaseData := &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", + }, + } + + c := &client{ + license: &license, + releaseData: releaseData, + clusterID: tt.clusterID, + } + + headers := c.getReportingInfoHeaders() + + req.Len(headers, tt.expectedCount) + + for key, expectedValue := range tt.checkHeaders { + actualValue, exists := headers[key] + req.True(exists, "Expected header %s to exist", key) + req.Equal(expectedValue, actualValue, "Header %s has wrong value", key) + } + }) + } +} + +func TestInjectHeaders(t *testing.T) { + req := require.New(t) + + // Create client + license := kotsv1beta1.License{ + Spec: kotsv1beta1.LicenseSpec{ + AppSlug: "test-app", + LicenseID: "test-license-id", + LicenseSequence: 1, + ChannelID: "test-channel-123", + ChannelName: "Stable", + Channels: []kotsv1beta1.Channel{ + { + ChannelID: "test-channel-123", + ChannelName: "Stable", + }, + }, + }, + } + + releaseData := &release.ReleaseData{ + ChannelRelease: &release.ChannelRelease{ + ChannelID: "test-channel-123", + }, + } + + c := &client{ + license: &license, + releaseData: releaseData, + clusterID: "test-cluster-id", + } + + // Test the internal method directly + header := http.Header{} + c.injectHeaders(header) + + // Validate User-Agent header was injected + userAgent := header.Get("User-Agent") + req.NotEmpty(userAgent) + req.Contains(userAgent, "Embedded-Cluster/") + req.Contains(userAgent, versions.Version) + + // Validate Authorization header + authHeader := header.Get("Authorization") + req.NotEmpty(authHeader) + req.Contains(authHeader, "Basic ") + + // Validate reporting info headers were injected + req.Equal("test-cluster-id", header.Get("X-Replicated-EmbeddedClusterID")) + req.Equal("test-channel-123", header.Get("X-Replicated-DownstreamChannelID")) + req.Equal("Stable", header.Get("X-Replicated-DownstreamChannelName")) + req.Equal(versions.K0sVersion, header.Get("X-Replicated-K8sVersion")) + req.Equal(DistributionEmbeddedCluster, header.Get("X-Replicated-K8sDistribution")) + req.Equal(versions.Version, header.Get("X-Replicated-EmbeddedClusterVersion")) + req.Equal("false", header.Get("X-Replicated-IsKurl")) +} diff --git a/pkg-new/replicatedapi/http.go b/pkg-new/replicatedapi/http.go new file mode 100644 index 0000000000..abd8c29664 --- /dev/null +++ b/pkg-new/replicatedapi/http.go @@ -0,0 +1,22 @@ +package replicatedapi + +import ( + "net/http" + "time" + + "github.com/hashicorp/go-retryablehttp" +) + +// newRetryableHTTPClient returns a new retryablehttp.Client with default settings. +func newRetryableHTTPClient() *retryablehttp.Client { + client := retryablehttp.NewClient() + client.Logger = nil + // errorHandler mimics net/http rather than doing anything fancy like the retryablehttp library. + client.ErrorHandler = func(resp *http.Response, err error, attempt int) (*http.Response, error) { + return resp, err + } + + // Set the timeout to 30 seconds. + client.HTTPClient.Timeout = 30 * time.Second + return client +} diff --git a/pkg-new/replicatedapi/reporting.go b/pkg-new/replicatedapi/reporting.go new file mode 100644 index 0000000000..32823ad1ce --- /dev/null +++ b/pkg-new/replicatedapi/reporting.go @@ -0,0 +1,72 @@ +package replicatedapi + +import ( + "net/http" + + "github.com/replicatedhq/embedded-cluster/pkg/versions" +) + +const ( + DistributionEmbeddedCluster string = "embedded-cluster" +) + +func (c *client) injectReportingInfoHeaders(header http.Header) { + for key, value := range c.getReportingInfoHeaders() { + header.Set(key, value) + } +} + +func (c *client) getReportingInfoHeaders() map[string]string { + headers := make(map[string]string) + + // add headers from client + channel, _ := c.getChannelFromLicense() // ignore error + if channel != nil { + headers["X-Replicated-DownstreamChannelID"] = channel.ChannelID + headers["X-Replicated-DownstreamChannelName"] = channel.ChannelName + } + + headers["X-Replicated-K8sVersion"] = versions.K0sVersion + headers["X-Replicated-K8sDistribution"] = DistributionEmbeddedCluster + headers["X-Replicated-EmbeddedClusterVersion"] = versions.Version + + // TODO + // headers["X-Replicated-ClusterID"] = "TODO" + // headers["X-Replicated-InstanceID"] = "TODO" + headers["X-Replicated-EmbeddedClusterID"] = c.clusterID + + // Add static headers + headers["X-Replicated-IsKurl"] = "false" + + // remove empty headers + for key, value := range headers { + if value == "" { + delete(headers, key) + } + } + + return headers +} + +// TODO: the following headers are injected by KOTS and are not yet supported by Embedded Cluster +// X-Replicated-EmbeddedClusterNodes +// X-Replicated-ReplHelmInstalls +// X-Replicated-NativeHelmInstalls +// X-Replicated-AppStatus +// X-Replicated-InstallStatus +// X-Replicated-PreflightStatus +// X-Replicated-DownstreamChannelSequence +// X-Replicated-DownstreamSequence +// X-Replicated-DownstreamSource +// X-Replicated-SkipPreflights +// X-Replicated-KotsInstallID +// X-Replicated-KurlInstallID +// X-Replicated-KurlNodeCountTotal +// X-Replicated-KurlNodeCountReady +// X-Replicated-IsGitOpsEnabled +// X-Replicated-GitOpsProvider +// X-Replicated-SnapshotProvider +// X-Replicated-SnapshotFullSchedule +// X-Replicated-SnapshotFullTTL +// X-Replicated-SnapshotPartialSchedule +// X-Replicated-SnapshotPartialTTL diff --git a/pkg/addons/adminconsole/static/metadata.yaml b/pkg/addons/adminconsole/static/metadata.yaml index 13f3ea918d..dcaeb52ce3 100644 --- a/pkg/addons/adminconsole/static/metadata.yaml +++ b/pkg/addons/adminconsole/static/metadata.yaml @@ -5,26 +5,26 @@ # $ make buildtools # $ output/bin/buildtools update addon # -version: 1.128.2-ec.2 +version: 1.128.3 location: oci://proxy.replicated.com/library/admin-console images: kotsadm: repo: proxy.replicated.com/anonymous/kotsadm/kotsadm tag: - amd64: v1.128.2-ec.2-amd64@sha256:3b68bef00e43427f4cadac5895e6598016824a1e91eece320b217d68310feede - arm64: v1.128.2-ec.2-arm64@sha256:dbfa5f419eb0b58d6fb56f504aed2969850200771c8a4e972e33f689d62a45ee + amd64: v1.128.3-amd64@sha256:a54b6bcc2623d8712df92f19cfd290c9b791cfd71c20ba7962aed6c5442754c8 + arm64: v1.128.3-arm64@sha256:646ae8d1987064e047f06de39ae6800f4ba617b1fec2ccb4a10b6a2a4623f39f kotsadm-migrations: repo: proxy.replicated.com/anonymous/kotsadm/kotsadm-migrations tag: - amd64: v1.128.2-ec.2-amd64@sha256:7179bfd50f2093bb023d305c97ec515078de5d0a19e825bf058ad344a0ef2d77 - arm64: v1.128.2-ec.2-arm64@sha256:65862abf071ad7eb857ab1baa82a7a4895053d347a0ff6d09807b6b108471688 + amd64: v1.128.3-amd64@sha256:d4a66c0d05216e06eb30be8bcf1d541cf9922e9985e8d3dddc10a6a230d1e913 + arm64: v1.128.3-arm64@sha256:ee54465951a3ca488d9ea1539261a59e46bec98bfa2084f98c6155614fc0a084 kurl-proxy: repo: proxy.replicated.com/anonymous/kotsadm/kurl-proxy tag: - amd64: v1.128.2-ec.2-amd64@sha256:6242c8bcde340f9550be1794648980ab062a52585e8cb777394fe31ab555dcd8 - arm64: v1.128.2-ec.2-arm64@sha256:edc19860112dbbcce06626de3db3cdbde47ae4b5eb9e1dff32b90f8e89d42116 + amd64: v1.128.3-amd64@sha256:37a442ffdd1cff6b1d5909036a6e482356f74055a826e345e213b869f0c492d5 + arm64: v1.128.3-arm64@sha256:f743cabd0bea24e50e5b7878f94150190390b301f574098fb7bb09062c11b2d9 rqlite: repo: proxy.replicated.com/anonymous/kotsadm/rqlite tag: - amd64: 9.1.2-r0-amd64@sha256:86bc9da94ee401f6b06994c340717bfadb7f31936bc62defc15b47c00cfa66a1 - arm64: 9.1.2-r0-arm64@sha256:880691a21c013cc0d38afaef85228e78808319d0ff0e80de8b316b0ce9b4a459 + amd64: 9.1.3-r0-amd64@sha256:b156612409debdee543329e6b0736d29b7d57fe0946b8b202014426bc3a4c0de + arm64: 9.1.3-r0-arm64@sha256:e82e67661ae3fdcef06d3fda8a02362d7906edd7e8ed7a1941a5c382e128cf6b diff --git a/pkg/addons/openebs/static/metadata.yaml b/pkg/addons/openebs/static/metadata.yaml index 8f8163ad1b..ced9eeb58b 100644 --- a/pkg/addons/openebs/static/metadata.yaml +++ b/pkg/addons/openebs/static/metadata.yaml @@ -11,8 +11,8 @@ images: kubectl: repo: proxy.replicated.com/library/kubectl tag: - amd64: 1.34.1-amd64@sha256:786b31b8e965c32afa04c4282f7fd14dcac891d9005d4c9c3c8395c01b5f9f90 - arm64: 1.34.1-arm64@sha256:23e1a0341b29d10d620208fb13740b5e79ccaf6065c9482a5b46db84a331f660 + amd64: 1.34.1-amd64@sha256:05ab9a2bc5e1241312ebe79b03c142bf8ce1d2d0207e04cf69a387f201ab9732 + arm64: 1.34.1-arm64@sha256:7979af3389e6033c47bbd28a6a1f992d72d9d87ff0bc2a86beb1cb0ece97a6ec openebs-linux-utils: repo: proxy.replicated.com/library/openebs-linux-utils tag: diff --git a/pkg/addons/velero/static/metadata.yaml b/pkg/addons/velero/static/metadata.yaml index 2508393240..1bc250fefc 100644 --- a/pkg/addons/velero/static/metadata.yaml +++ b/pkg/addons/velero/static/metadata.yaml @@ -11,8 +11,8 @@ images: kubectl: repo: proxy.replicated.com/library/kubectl tag: - amd64: 1.34.1-amd64@sha256:786b31b8e965c32afa04c4282f7fd14dcac891d9005d4c9c3c8395c01b5f9f90 - arm64: 1.34.1-arm64@sha256:23e1a0341b29d10d620208fb13740b5e79ccaf6065c9482a5b46db84a331f660 + amd64: 1.34.1-amd64@sha256:05ab9a2bc5e1241312ebe79b03c142bf8ce1d2d0207e04cf69a387f201ab9732 + arm64: 1.34.1-arm64@sha256:7979af3389e6033c47bbd28a6a1f992d72d9d87ff0bc2a86beb1cb0ece97a6ec velero: repo: proxy.replicated.com/library/velero tag: diff --git a/pkg/config/static/metadata-1_31.yaml b/pkg/config/static/metadata-1_31.yaml index 746e20c0f4..0773d65714 100644 --- a/pkg/config/static/metadata-1_31.yaml +++ b/pkg/config/static/metadata-1_31.yaml @@ -9,40 +9,40 @@ images: calico-cni: repo: proxy.replicated.com/library/calico-cni tag: - amd64: v3.28.5-amd64@sha256:3b784ed277522deca9a92b464685e077a86dcad4a95897928b3598b6ebc75e5a - arm64: v3.28.5-arm64@sha256:15bc091e0293451033566284967f17acb79360d72dd19f6a2624e4b280892087 + amd64: v3.28.5-amd64@sha256:b65290c2b00456a06530409bdea24fb9058efae63a62a679b450b732cbdac0d0 + arm64: v3.28.5-arm64@sha256:26440d1381fa97b0191d1130c6f1e375f31b1142957db213ff1e995499c492a9 calico-kube-controllers: repo: proxy.replicated.com/library/calico-kube-controllers tag: - amd64: v3.28.5-amd64@sha256:5fc2576d846389a2132f92350bb28965f5edb04adc50260ec5a5b4421c31df4d - arm64: v3.28.5-arm64@sha256:d8efad1d5fa1444285fd8db5193b2baad1f81edde46ce2e2e966431982b3d02d + amd64: v3.28.5-amd64@sha256:d8d6a12f125990bca3adb951bf4e1232d685612fb5599ddfea23f09a9526db9c + arm64: v3.28.5-arm64@sha256:25b1c3a94028ff69cc63c1d164cb3b2d5c658965f3d9b4ad219c61ffa322e7d4 calico-node: repo: proxy.replicated.com/library/calico-node tag: - amd64: v3.28.5-amd64@sha256:05682568c1e184f91fab40f1a01dc22bdec9f1acda1991f26078f8a11fd276f4 - arm64: v3.28.5-arm64@sha256:331db2cff8075859402cc3479357126238c2953715c852f21a234913fe7f0f14 + amd64: v3.28.5-amd64@sha256:eca02f5330bb68ca379af3db64b5df0b2425b6523039b465a707acfeb8603c46 + arm64: v3.28.5-arm64@sha256:606653991d1193bd820b136e64ba9260c010150729b03e76fd14eec5e6635c0f coredns: repo: proxy.replicated.com/library/coredns tag: - amd64: v1.12.3-amd64@sha256:8f5f7a0b7e43ba7efc18d3c2d1eb4a9b0d92969be27abb87af1c3bb1ffb5f359 - arm64: v1.12.3-arm64@sha256:27b89e34043cd283d36be797f36da6e18b22c01a6547cfbda5fe21d6a9ea8d39 + amd64: 1.13.1-amd64@sha256:50f3bff150d550c51a3fcb35647b2382d4941385832ae669f13cdbb39e560d63 + arm64: 1.13.1-arm64@sha256:af215fb41f787102113aa279312625daa570425f9ec118f6663ad061db520fd9 envoy-distroless: repo: proxy.replicated.com/library/envoy tag: - amd64: 1.31.2-amd64@sha256:6d243ea9bce274e50106eb5cd7e16a9a6d73fcb9e54879b2be53656adb50ec5c - arm64: 1.31.2-arm64@sha256:8394aba8c1cbe52bd27e276c8c1e7333cefaffde0ba7591f78525700d5005ad9 + amd64: v1.31.2-amd64@sha256:b612e54c7d9d6cb1af0a6d1aed67f162146618bd050eab2cdb34ff107517e54c + arm64: v1.31.2-arm64@sha256:b0cae887eefe86f278f550156cb699a4025f320253bf4b4a2a7c809108455ce6 kube-proxy: repo: proxy.replicated.com/library/kube-proxy tag: - amd64: v1.31.13-amd64@sha256:d63d06ab7a723f6db85a79f7e1d0da7b2e9fb1b45b53b45519a3188c8da3585d - arm64: v1.31.13-arm64@sha256:4978486772d26b1fe52050e8addeef8c5d26ee52e4a147ed82d5a9f91e8098c5 + amd64: v1.31.13-amd64@sha256:660001f4ce4a2c965490a100046d052126ac0a780ee98091ec6666f915113e2e + arm64: v1.31.13-arm64@sha256:7b3bbcada1c3bcd4f385241578585c5d81829f106e71450f2850a3a0cd65cd0a metrics-server: repo: proxy.replicated.com/library/metrics-server tag: - amd64: v0.8.0-amd64@sha256:7a37510b20c6d506df5db21430f099e17e77ca2a6b70ca649c14600738465900 - arm64: v0.8.0-arm64@sha256:8535a486080aa611544a5d7f75da02d3896f7f5187cdcef36b9ed7f658e7bd7e + amd64: v0.8.0-amd64@sha256:f0a44c9f544557332a32bdc84e3b7c980b5ed0c4a60ac4d03ade9e970d16b8e5 + arm64: v0.8.0-arm64@sha256:ff4e5cf660f494305fd551c2272f7ee09bd1129225f66284adc10805b84529a3 pause: repo: proxy.replicated.com/library/pause tag: - amd64: 3.9-amd64@sha256:cd19cab33e09447f2f49b7661cf5058d88f26ee9f4a16f514b66794cd1dc8904 - arm64: 3.9-arm64@sha256:b73d34c3a5e7f8cab7a600fadf88c2d802cef024694fe8c7885da017e3d890b4 + amd64: 3.9-amd64@sha256:86665160218b2b1368f5f47424f1ee0fae0816cdc2534d49b0c1d962fc3bfe51 + arm64: 3.9-arm64@sha256:32762831e8554e74cf8871498164fc460605442ca25bcd7d756a4abf2fc21ef8 diff --git a/pkg/config/static/metadata-1_32.yaml b/pkg/config/static/metadata-1_32.yaml index 9310fc903a..50b0f872ee 100644 --- a/pkg/config/static/metadata-1_32.yaml +++ b/pkg/config/static/metadata-1_32.yaml @@ -9,40 +9,40 @@ images: calico-cni: repo: proxy.replicated.com/library/calico-cni tag: - amd64: v3.29.3-amd64@sha256:ba9e67b77a964fb95c836430140e77a3f3605a4453ceb7df1f4c43dd04852f97 - arm64: v3.29.3-arm64@sha256:bcb96f11f5aedbfb92e860e0006dfcd4e3a0e48aa94476d8b71fa186e4705b56 + amd64: v3.29.3-amd64@sha256:868a61fa5320bddf13fa82323f784813f42c7b077107cc04987919456790a4b3 + arm64: v3.29.3-arm64@sha256:20e16818bce1e9a309fa1cc211642c4a0ad7ae36e28f6c421b3e0a50c33ab90d calico-kube-controllers: repo: proxy.replicated.com/library/calico-kube-controllers tag: - amd64: v3.29.3-amd64@sha256:314527adba1b9f445e181ded800dfcb65873853d55333d92c9a2f5c0888fe72c - arm64: v3.29.3-arm64@sha256:69b1a6acb9aeef1324ee5574ef217b54c59cf2aee7933fc41c5c2f0402840955 + amd64: v3.29.3-amd64@sha256:7c4d739278941dba2538acad3a656a2b4ba02303b6f2b6fc0c3501524ce4a9ec + arm64: v3.29.3-arm64@sha256:1c743cf154600f234a17df98bbfe0b84406f4dc6f438077cd73c8a88e1543768 calico-node: repo: proxy.replicated.com/library/calico-node tag: - amd64: v3.29.3-amd64@sha256:48288e892adb30b610dff84424111d72671c3e179570482da5aae048ed2645b0 - arm64: v3.29.3-arm64@sha256:d6e97d84515fe4569a783a86d9410253ae7d8a1f0cefdbebc0ef7bbfb31405d9 + amd64: v3.29.3-amd64@sha256:44a95ffa32027be4faafbbebff19a19cab42420725ccd2b3e27ecbd8def940a3 + arm64: v3.29.3-arm64@sha256:47249309a6b83d2cbb067a41bc8e12ba100f9e6c6aa29cafe8b6b347cc7e69c9 coredns: repo: proxy.replicated.com/library/coredns tag: - amd64: v1.12.3-amd64@sha256:8f5f7a0b7e43ba7efc18d3c2d1eb4a9b0d92969be27abb87af1c3bb1ffb5f359 - arm64: v1.12.3-arm64@sha256:27b89e34043cd283d36be797f36da6e18b22c01a6547cfbda5fe21d6a9ea8d39 + amd64: 1.13.1-amd64@sha256:50f3bff150d550c51a3fcb35647b2382d4941385832ae669f13cdbb39e560d63 + arm64: 1.13.1-arm64@sha256:af215fb41f787102113aa279312625daa570425f9ec118f6663ad061db520fd9 envoy-distroless: repo: proxy.replicated.com/library/envoy tag: - amd64: 1.31.2-amd64@sha256:6d243ea9bce274e50106eb5cd7e16a9a6d73fcb9e54879b2be53656adb50ec5c - arm64: 1.31.2-arm64@sha256:8394aba8c1cbe52bd27e276c8c1e7333cefaffde0ba7591f78525700d5005ad9 + amd64: v1.31.2-amd64@sha256:b612e54c7d9d6cb1af0a6d1aed67f162146618bd050eab2cdb34ff107517e54c + arm64: v1.31.2-arm64@sha256:b0cae887eefe86f278f550156cb699a4025f320253bf4b4a2a7c809108455ce6 kube-proxy: repo: proxy.replicated.com/library/kube-proxy tag: - amd64: v1.32.9-amd64@sha256:8797e3485b7367544ae8546af1b9b6d883711934b6a68bed4a3f2cfe48c6d4b3 - arm64: v1.32.9-arm64@sha256:666b29c62f6b9b2b2824b6a605aa4a76c99c172f2ec54e4a789957fd2ddd1da0 + amd64: v1.32.9-amd64@sha256:e962e100fe98863790480425920eb20638727b6f972d124a5f75846a2b717000 + arm64: v1.32.9-arm64@sha256:7d22ebdd13aeeacc82473c7f9f5382e49425eba4ec69672e68a2fdb1f2d48498 metrics-server: repo: proxy.replicated.com/library/metrics-server tag: - amd64: v0.8.0-amd64@sha256:7a37510b20c6d506df5db21430f099e17e77ca2a6b70ca649c14600738465900 - arm64: v0.8.0-arm64@sha256:8535a486080aa611544a5d7f75da02d3896f7f5187cdcef36b9ed7f658e7bd7e + amd64: v0.8.0-amd64@sha256:f0a44c9f544557332a32bdc84e3b7c980b5ed0c4a60ac4d03ade9e970d16b8e5 + arm64: v0.8.0-arm64@sha256:ff4e5cf660f494305fd551c2272f7ee09bd1129225f66284adc10805b84529a3 pause: repo: proxy.replicated.com/library/pause tag: - amd64: 3.9-amd64@sha256:cd19cab33e09447f2f49b7661cf5058d88f26ee9f4a16f514b66794cd1dc8904 - arm64: 3.9-arm64@sha256:b73d34c3a5e7f8cab7a600fadf88c2d802cef024694fe8c7885da017e3d890b4 + amd64: 3.9-amd64@sha256:86665160218b2b1368f5f47424f1ee0fae0816cdc2534d49b0c1d962fc3bfe51 + arm64: 3.9-arm64@sha256:32762831e8554e74cf8871498164fc460605442ca25bcd7d756a4abf2fc21ef8 diff --git a/pkg/config/static/metadata-1_33.yaml b/pkg/config/static/metadata-1_33.yaml index 6380544641..845e8b35cb 100644 --- a/pkg/config/static/metadata-1_33.yaml +++ b/pkg/config/static/metadata-1_33.yaml @@ -9,40 +9,40 @@ images: calico-cni: repo: proxy.replicated.com/library/calico-cni tag: - amd64: v3.29.3-amd64@sha256:ba9e67b77a964fb95c836430140e77a3f3605a4453ceb7df1f4c43dd04852f97 - arm64: v3.29.3-arm64@sha256:bcb96f11f5aedbfb92e860e0006dfcd4e3a0e48aa94476d8b71fa186e4705b56 + amd64: v3.29.3-amd64@sha256:868a61fa5320bddf13fa82323f784813f42c7b077107cc04987919456790a4b3 + arm64: v3.29.3-arm64@sha256:20e16818bce1e9a309fa1cc211642c4a0ad7ae36e28f6c421b3e0a50c33ab90d calico-kube-controllers: repo: proxy.replicated.com/library/calico-kube-controllers tag: - amd64: v3.29.3-amd64@sha256:314527adba1b9f445e181ded800dfcb65873853d55333d92c9a2f5c0888fe72c - arm64: v3.29.3-arm64@sha256:69b1a6acb9aeef1324ee5574ef217b54c59cf2aee7933fc41c5c2f0402840955 + amd64: v3.29.3-amd64@sha256:7c4d739278941dba2538acad3a656a2b4ba02303b6f2b6fc0c3501524ce4a9ec + arm64: v3.29.3-arm64@sha256:1c743cf154600f234a17df98bbfe0b84406f4dc6f438077cd73c8a88e1543768 calico-node: repo: proxy.replicated.com/library/calico-node tag: - amd64: v3.29.3-amd64@sha256:48288e892adb30b610dff84424111d72671c3e179570482da5aae048ed2645b0 - arm64: v3.29.3-arm64@sha256:d6e97d84515fe4569a783a86d9410253ae7d8a1f0cefdbebc0ef7bbfb31405d9 + amd64: v3.29.3-amd64@sha256:44a95ffa32027be4faafbbebff19a19cab42420725ccd2b3e27ecbd8def940a3 + arm64: v3.29.3-arm64@sha256:47249309a6b83d2cbb067a41bc8e12ba100f9e6c6aa29cafe8b6b347cc7e69c9 coredns: repo: proxy.replicated.com/library/coredns tag: - amd64: v1.12.3-amd64@sha256:8f5f7a0b7e43ba7efc18d3c2d1eb4a9b0d92969be27abb87af1c3bb1ffb5f359 - arm64: v1.12.3-arm64@sha256:27b89e34043cd283d36be797f36da6e18b22c01a6547cfbda5fe21d6a9ea8d39 + amd64: 1.13.1-amd64@sha256:50f3bff150d550c51a3fcb35647b2382d4941385832ae669f13cdbb39e560d63 + arm64: 1.13.1-arm64@sha256:af215fb41f787102113aa279312625daa570425f9ec118f6663ad061db520fd9 envoy-distroless: repo: proxy.replicated.com/library/envoy tag: - amd64: 1.31.2-amd64@sha256:6d243ea9bce274e50106eb5cd7e16a9a6d73fcb9e54879b2be53656adb50ec5c - arm64: 1.31.2-arm64@sha256:8394aba8c1cbe52bd27e276c8c1e7333cefaffde0ba7591f78525700d5005ad9 + amd64: v1.32.8-amd64@sha256:c8d0eb139a78cf8839056dd5752ff2e66daaa7cd3c358f29e03f24e4959bfb75 + arm64: v1.32.8-arm64@sha256:5b490c800d22444fb4daf0edaabacf2511dfc1f9bf25795d8aaabb7abbf9c7aa kube-proxy: repo: proxy.replicated.com/library/kube-proxy tag: - amd64: v1.33.5-amd64@sha256:6f6dec84370a352a37f77b5452c8a467b64b510519dfbe917ce1a4bcba0d632a - arm64: v1.33.5-arm64@sha256:8a3a325957cb1f3b0398999cb1df4bdc1f90b3b0d506af8d5bfe651fa94c218b + amd64: v1.33.5-amd64@sha256:ab64431fa2dc735e6c5e351f5a3eac46781bb84a138554570ce2d1383c24000d + arm64: v1.33.5-arm64@sha256:2e7e4345f0c3b095fbdee1e5b0c4926c5f024f8b9bd6d3f120592bebfdbf289a metrics-server: repo: proxy.replicated.com/library/metrics-server tag: - amd64: v0.8.0-amd64@sha256:7a37510b20c6d506df5db21430f099e17e77ca2a6b70ca649c14600738465900 - arm64: v0.8.0-arm64@sha256:8535a486080aa611544a5d7f75da02d3896f7f5187cdcef36b9ed7f658e7bd7e + amd64: v0.8.0-amd64@sha256:f0a44c9f544557332a32bdc84e3b7c980b5ed0c4a60ac4d03ade9e970d16b8e5 + arm64: v0.8.0-arm64@sha256:ff4e5cf660f494305fd551c2272f7ee09bd1129225f66284adc10805b84529a3 pause: repo: proxy.replicated.com/library/pause tag: - amd64: 3.10.1-amd64@sha256:0733e6feb71bd4882f6b47135b2e21c2fe0e567e73cba1e4a0ca0f97fa87583c - arm64: 3.10.1-arm64@sha256:136810ca7962f9b7d78d6fa363c7b59e338b7353fa0e0f741d364e61024a5305 + amd64: 3.10.1-amd64@sha256:06bc277a20f6332c914444e16a6619f6b481627b36911d21a28ac8ef53b53614 + arm64: 3.10.1-arm64@sha256:0a149ef51bd9c9d50f6838de19f3dafe2f0ad8ccb29edfc5dce1247f36816760 diff --git a/pkg/dryrun/k0s.go b/pkg/dryrun/k0s.go index 0a8c84f7f6..ee7a55ed65 100644 --- a/pkg/dryrun/k0s.go +++ b/pkg/dryrun/k0s.go @@ -27,8 +27,12 @@ func (c *K0s) IsInstalled() (bool, error) { return c.Status != nil, nil } -func (c *K0s) WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - return k0s.New().WriteK0sConfig(ctx, networkInterface, airgapBundle, podCIDR, serviceCIDR, eucfg, mutate) // actual implementation accounts for dryrun +func (c *K0s) NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { + return k0s.New().NewK0sConfig(networkInterface, isAirgap, podCIDR, serviceCIDR, eucfg, mutate) // actual implementation accounts for dryrun +} + +func (c *K0s) WriteK0sConfig(ctx context.Context, cfg *k0sv1beta1.ClusterConfig) error { + return k0s.New().WriteK0sConfig(ctx, cfg) // actual implementation accounts for dryrun } func (c *K0s) PatchK0sConfig(path string, patch string) error { diff --git a/pkg/helpers/parse.go b/pkg/helpers/parse.go index a3ddd1b4a6..a846c2fa18 100644 --- a/pkg/helpers/parse.go +++ b/pkg/helpers/parse.go @@ -1,7 +1,6 @@ package helpers import ( - "errors" "fmt" "os" @@ -10,9 +9,13 @@ import ( kyaml "sigs.k8s.io/yaml" ) -var ( - ErrNotALicenseFile = errors.New("not a license file") -) +type ErrNotALicenseFile struct { + Err error +} + +func (e ErrNotALicenseFile) Error() string { + return e.Err.Error() +} // ParseEndUserConfig parses the end user configuration from the given file. func ParseEndUserConfig(fpath string) (*embeddedclusterv1beta1.Config, error) { @@ -36,9 +39,17 @@ func ParseLicense(fpath string) (*kotsv1beta1.License, error) { if err != nil { return nil, fmt.Errorf("failed to read license file: %w", err) } + return ParseLicenseFromBytes(data) +} + +// ParseLicenseFromBytes parses the license from a byte slice +func ParseLicenseFromBytes(data []byte) (*kotsv1beta1.License, error) { var license kotsv1beta1.License if err := kyaml.Unmarshal(data, &license); err != nil { - return nil, ErrNotALicenseFile + return nil, ErrNotALicenseFile{Err: err} + } + if license.Spec.LicenseID == "" { + return nil, ErrNotALicenseFile{Err: fmt.Errorf("license id is empty")} } return &license, nil } diff --git a/pkg/helpers/parse_test.go b/pkg/helpers/parse_test.go index 9e0acdfb4e..18a123ffe2 100644 --- a/pkg/helpers/parse_test.go +++ b/pkg/helpers/parse_test.go @@ -1,6 +1,7 @@ package helpers import ( + "errors" "os" "path/filepath" "testing" @@ -128,24 +129,16 @@ func TestParseLicense(t *testing.T) { fpath: "invalid.yaml", fileContent: `invalid: yaml: content: [ unclosed bracket`, - wantErr: ErrNotALicenseFile, + wantErr: ErrNotALicenseFile{}, }, { - name: "valid YAML but not a license succeeds (no validation)", + name: "valid YAML but not a license returns ErrNotALicenseFile", fpath: "not-license.yaml", fileContent: `apiVersion: v1 kind: ConfigMap metadata: name: test`, - expected: &kotsv1beta1.License{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - }, + wantErr: ErrNotALicenseFile{}, }, { name: "valid license", @@ -177,12 +170,17 @@ spec: name: "minimal valid license", fpath: "minimal-license.yaml", fileContent: `apiVersion: kots.io/v1beta1 -kind: License`, +kind: License +spec: + licenseID: "test-license-id"`, expected: &kotsv1beta1.License{ TypeMeta: metav1.TypeMeta{ APIVersion: "kots.io/v1beta1", Kind: "License", }, + Spec: kotsv1beta1.LicenseSpec{ + LicenseID: "test-license-id", + }, }, }, } @@ -207,8 +205,8 @@ kind: License`, if tt.wantErr != nil { req.Error(err) - if tt.wantErr == ErrNotALicenseFile { - req.Equal(ErrNotALicenseFile, err) + if errors.Is(tt.wantErr, ErrNotALicenseFile{}) { + req.ErrorAs(err, &tt.wantErr) } else { req.ErrorIs(err, tt.wantErr) } diff --git a/pkg/lint/README.md b/pkg/lint/README.md new file mode 100644 index 0000000000..9c51d84e58 --- /dev/null +++ b/pkg/lint/README.md @@ -0,0 +1,223 @@ +# Embedded Cluster Lint Package + +A hidden lint command for validating Embedded Cluster configuration files. + +## Purpose + +Validates Embedded Cluster YAML configuration files to catch common issues: +- **YAML syntax errors** (duplicate keys, unclosed quotes, invalid structure) +- **Port configuration issues** (ports in unsupportedOverrides that are already supported) +- **Custom domain validation** (domains must exist in the Replicated app) + +## Usage + +```bash +# Basic usage +embedded-cluster lint config.yaml + +# Multiple files +embedded-cluster lint config1.yaml config2.yaml config3.yaml + +# Verbose mode (shows API endpoints and configuration) +embedded-cluster lint -v config.yaml + +# JSON output (for CI/CD and scripting) +embedded-cluster lint -o json config.yaml +embedded-cluster lint --output json config.yaml # Long form + +# With custom domain validation enabled +REPLICATED_API_TOKEN="your-token" \ +REPLICATED_API_ORIGIN="https://api.replicated.com/vendor" \ +REPLICATED_APP="your-app-id" \ +embedded-cluster lint config.yaml + +# JSON output with custom domain validation +REPLICATED_API_TOKEN="your-token" \ +REPLICATED_API_ORIGIN="https://api.replicated.com/vendor" \ +REPLICATED_APP="your-app-id" \ +embedded-cluster lint -o json config.yaml +``` + +## Validation Rules + +### 1. YAML Syntax Validation (ERROR) +Validates basic YAML syntax before attempting content validation. + +**Catches:** +- Duplicate keys +- Unclosed quotes +- Invalid indentation +- Tabs mixed with spaces +- Malformed YAML structures + +**Example:** +``` +ERROR: Failed to validate config.yaml: YAML syntax error at line 6: key "version" already set in map +``` + +### 2. Port Range Validation (WARNING) +Checks if ports specified in `unsupportedOverrides` are within the default supported range (80-32767). + +**Why it matters:** Ports in this range don't need to be in unsupportedOverrides - they're already supported by default. + +**Example:** +``` +WARNING: config.yaml: unsupportedOverrides.builtInExtensions[adminconsole].service.nodePort: port 30000 is already supported (supported range: 80-32767) and should not be in unsupportedOverrides +``` + +### 3. Custom Domain Validation (ERROR) +When environment variables are provided, validates that custom domains exist in the app's configuration. + +**Requires all three environment variables:** +- `REPLICATED_API_TOKEN` - Authentication token for Replicated API +- `REPLICATED_API_ORIGIN` - API endpoint (e.g., `https://api.replicated.com/vendor`) +- `REPLICATED_APP` - Application ID or slug + +**If any are missing:** Shows informative message and skips domain validation. + +**Example:** +``` +ERROR: config.yaml: domains.replicatedAppDomain: custom domain "invalid.example.com" not found in app's configured domains +``` + +## Exit Codes + +- **0**: Validation passed (may have warnings) +- **1**: Validation failed (has errors) + +Warnings do NOT cause a non-zero exit code. + +## JSON Output + +Use the `-o json` or `--output json` flag for machine-parseable output: + +```bash +embedded-cluster lint -o json config.yaml +embedded-cluster lint --output json config.yaml +``` + +### JSON Format + +```json +{ + "files": [ + { + "path": "config.yaml", + "valid": true, + "warnings": [ + { + "field": "unsupportedOverrides.builtInExtensions[adminconsole].service.nodePort", + "message": "port 8080 is already supported (supported range: 80-32767)" + } + ] + } + ] +} +``` + +### JSON Fields + +- `files[]` - Array of file results + - `path` - File path + - `valid` - `true` if no errors (warnings don't affect this) + - `errors[]` - Array of validation errors (if any) + - `field` - YAML path to the problematic field + - `message` - Error description + - `warnings[]` - Array of validation warnings (if any) + - `field` - YAML path to the field + - `message` - Warning description + +### CI/CD Integration + +```bash +# Example: Fail CI build on errors, allow warnings +if ! embedded-cluster lint -o json config.yaml > results.json 2>&1; then + echo "Validation failed with errors" + cat results.json | jq '.files[].errors' + exit 1 +fi + +# Check if there are any warnings +if cat results.json | jq -e '.files[].warnings | length > 0' > /dev/null; then + echo "::warning::Lint warnings found" + cat results.json | jq '.files[].warnings' +fi +``` + +## Verbose Mode + +Use the `-v` flag to see detailed information: + +```bash +embedded-cluster lint -v config.yaml +``` + +**Shows:** +- Environment configuration (token shown as ``) +- API endpoints being called +- HTTP response status codes +- Custom domains found +- Fallback attempts to alternate endpoints + +**Example output:** +``` +Environment configuration: + REPLICATED_API_ORIGIN: https://api.replicated.com/vendor + REPLICATED_APP: my-app + REPLICATED_API_TOKEN: +Starting custom domain validation +Fetching channels from: https://api.replicated.com/vendor/v3/app/my-app/channels +Attempting to fetch custom domains from: https://api.replicated.com/vendor/v3/app/my-app/custom-hostnames +Response status: 200 200 OK +``` + +## Testing + +### Run all tests +```bash +go test ./pkg/lint/... -v +``` + +### Run specific test suites +```bash +# YAML syntax validation +go test ./pkg/lint/... -run TestValidateYAMLSyntax + +# Port validation +go test ./pkg/lint/... -run TestValidatePorts + +# API client +go test ./pkg/lint/... -run TestAPIClient +``` + +### Test with example specs +```bash +# Test syntax errors +./bin/embedded-cluster lint ./pkg/lint/testdata/specs/syntax-error-duplicate-key.yaml + +# Test port warnings +./bin/embedded-cluster lint ./pkg/lint/testdata/specs/01-warning-port-in-range.yaml + +# Test valid configuration +./bin/embedded-cluster lint ./pkg/lint/testdata/specs/04-valid-ports-outside-range.yaml +``` + +## Package Structure + +- `validator.go` - Core validation logic +- `validator_test.go` - Validation tests +- `api_client.go` - Replicated API client for custom domain fetching +- `api_client_test.go` - API client tests +- `testdata/specs/` - Example YAML files for testing + +## Development + +The lint command is currently **hidden** (not shown in `--help` output). To make it visible, modify `cmd/installer/cli/lint.go` and set `Hidden: false`. + +### Adding New Validation Rules + +1. Add validation function to `validator.go` +2. Call it from `ValidateFile()` +3. Return warnings or errors as appropriate +4. Add test cases to `validator_test.go` +5. Create example spec files in `testdata/specs/` \ No newline at end of file diff --git a/pkg/lint/api_client.go b/pkg/lint/api_client.go new file mode 100644 index 0000000000..b7e1d8e8ee --- /dev/null +++ b/pkg/lint/api_client.go @@ -0,0 +1,376 @@ +package lint + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "time" +) + +// APIClient handles communication with the Replicated API +type APIClient struct { + apiToken string + apiOrigin string + appID string + client *http.Client + verbose bool +} + +// NewAPIClient creates a new API client +func NewAPIClient(apiToken, apiOrigin, appID string) *APIClient { + return &APIClient{ + apiToken: apiToken, + apiOrigin: strings.TrimRight(apiOrigin, "/"), + appID: appID, + client: &http.Client{ + Timeout: 30 * time.Second, + }, + verbose: false, + } +} + +// SetVerbose enables or disables verbose mode +func (c *APIClient) SetVerbose(verbose bool) { + c.verbose = verbose +} + +// logConfiguration logs the detected configuration (for verbose mode) +func (c *APIClient) logConfiguration() { + fmt.Fprintf(os.Stderr, "Environment configuration:\n") + fmt.Fprintf(os.Stderr, " REPLICATED_API_ORIGIN: %s\n", c.apiOrigin) + fmt.Fprintf(os.Stderr, " REPLICATED_APP: %s\n", c.appID) + if c.apiToken != "" { + fmt.Fprintf(os.Stderr, " REPLICATED_API_TOKEN: \n") + } else { + fmt.Fprintf(os.Stderr, " REPLICATED_API_TOKEN: \n") + } +} + +// isConfigured checks if the API client has necessary configuration +func (c *APIClient) isConfigured() bool { + return c.apiToken != "" && c.apiOrigin != "" && c.appID != "" +} + +// getMissingConfig returns a list of missing configuration items +func (c *APIClient) getMissingConfig() []string { + var missing []string + if c.apiToken == "" { + missing = append(missing, "REPLICATED_API_TOKEN") + } + if c.apiOrigin == "" { + missing = append(missing, "REPLICATED_API_ORIGIN") + } + if c.appID == "" { + missing = append(missing, "REPLICATED_APP") + } + return missing +} + +// CustomDomainsResponse represents the API response for custom domains +type CustomDomainsResponse struct { + Domains []DomainInfo `json:"domains"` +} + +// DomainInfo represents a single domain configuration +type DomainInfo struct { + Domain string `json:"domain"` + Type string `json:"type"` // replicated_app, proxy_registry, replicated_registry +} + +// GetCustomDomains fetches the custom domains configured for the app +func (c *APIClient) GetCustomDomains() ([]string, error) { + if !c.isConfigured() { + missing := c.getMissingConfig() + return nil, fmt.Errorf("API client not configured - missing: %v", missing) + } + + // Try to fetch custom domains from the channel releases endpoint first + // This is more likely to have the domain information + if c.verbose { + fmt.Fprintf(os.Stderr, "Starting custom domain validation\n") + } + + domains, err := c.getDomainsFromChannelReleases() + if err == nil && len(domains) > 0 { + if c.verbose { + fmt.Fprintf(os.Stderr, "Successfully fetched %d custom domain(s) from channel releases\n", len(domains)) + for _, d := range domains { + fmt.Fprintf(os.Stderr, " - %s\n", d) + } + } + return domains, nil + } + + // If that doesn't work, try a direct custom domains endpoint + // Note: This endpoint path might need adjustment based on actual API + url := fmt.Sprintf("%s/v3/app/%s/custom-hostnames", c.apiOrigin, c.appID) + + if c.verbose { + fmt.Fprintf(os.Stderr, "Attempting to fetch custom domains from: %s\n", url) + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request for app %q: %w", c.appID, err) + } + + req.Header.Set("Authorization", c.apiToken) + req.Header.Set("Accept", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request for app %q: %w", c.appID, err) + } + defer resp.Body.Close() + + if c.verbose { + fmt.Fprintf(os.Stderr, "Response status: %d %s\n", resp.StatusCode, resp.Status) + } + + if resp.StatusCode == http.StatusNotFound { + if c.verbose { + fmt.Fprintf(os.Stderr, "Endpoint not found, trying alternate endpoint structure\n") + } + // Try alternate endpoint structure + return c.getDomainsFromApp() + } + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API request failed with status %d for app %q: %s", resp.StatusCode, c.appID, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var response CustomDomainsResponse + if err := json.Unmarshal(body, &response); err != nil { + // Try to parse as array of strings directly + var domainStrings []string + if err2 := json.Unmarshal(body, &domainStrings); err2 == nil { + return domainStrings, nil + } + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + // Extract domain strings from the response + var result []string + for _, d := range response.Domains { + if d.Domain != "" { + result = append(result, d.Domain) + } + } + + return result, nil +} + +// ChannelReleasesResponse represents the API response for channel releases +type ChannelReleasesResponse struct { + ChannelReleases []ChannelRelease `json:"channel_releases"` +} + +// ChannelRelease represents a release in a channel +type ChannelRelease struct { + ChannelID string `json:"channel_id"` + ReleaseSequence int `json:"release_sequence"` + VersionLabel string `json:"version_label"` + DefaultDomains *Domains `json:"default_domains,omitempty"` +} + +// Domains represents custom domain configuration +type Domains struct { + ReplicatedApp string `json:"replicated_app,omitempty"` + ProxyRegistry string `json:"proxy_registry,omitempty"` + ReplicatedRegistry string `json:"replicated_registry,omitempty"` +} + +// getDomainsFromChannelReleases attempts to get domains from channel releases +func (c *APIClient) getDomainsFromChannelReleases() ([]string, error) { + // First, get the list of channels + channelsURL := fmt.Sprintf("%s/v3/app/%s/channels", c.apiOrigin, c.appID) + + if c.verbose { + fmt.Fprintf(os.Stderr, "Fetching channels from: %s\n", channelsURL) + } + + req, err := http.NewRequest("GET", channelsURL, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", c.apiToken) + req.Header.Set("Accept", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get channels") + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var channelsResp struct { + Channels []struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"channels"` + } + + if err := json.Unmarshal(body, &channelsResp); err != nil { + return nil, err + } + + // Collect unique domains from all channels + domainSet := make(map[string]bool) + + for _, channel := range channelsResp.Channels { + // Get releases for this channel + releasesURL := fmt.Sprintf("%s/v3/app/%s/channel/%s/releases", c.apiOrigin, c.appID, channel.ID) + + req, err := http.NewRequest("GET", releasesURL, nil) + if err != nil { + continue + } + + req.Header.Set("Authorization", c.apiToken) + req.Header.Set("Accept", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + continue + } + + func() { + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + var releasesResp ChannelReleasesResponse + if err := json.Unmarshal(body, &releasesResp); err != nil { + return + } + + // Extract domains from releases + for _, release := range releasesResp.ChannelReleases { + if release.DefaultDomains != nil { + if release.DefaultDomains.ReplicatedApp != "" { + domainSet[release.DefaultDomains.ReplicatedApp] = true + } + if release.DefaultDomains.ProxyRegistry != "" { + domainSet[release.DefaultDomains.ProxyRegistry] = true + } + if release.DefaultDomains.ReplicatedRegistry != "" { + domainSet[release.DefaultDomains.ReplicatedRegistry] = true + } + } + } + }() + } + + // Convert set to slice + var domains []string + for domain := range domainSet { + domains = append(domains, domain) + } + + return domains, nil +} + +// getDomainsFromApp attempts to get domains from app configuration +func (c *APIClient) getDomainsFromApp() ([]string, error) { + url := fmt.Sprintf("%s/v3/app/%s", c.apiOrigin, c.appID) + + if c.verbose { + fmt.Fprintf(os.Stderr, "Fetching app details from: %s\n", url) + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", c.apiToken) + req.Header.Set("Accept", "application/json") + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute request for app %q: %w", c.appID, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API request failed with status %d for app %q: %s", resp.StatusCode, c.appID, string(body)) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Parse app response looking for custom domains + var appResp struct { + App struct { + CustomDomains *Domains `json:"custom_domains,omitempty"` + Domains *Domains `json:"domains,omitempty"` + } `json:"app"` + } + + if err := json.Unmarshal(body, &appResp); err != nil { + return nil, fmt.Errorf("failed to parse app response: %w", err) + } + + domainSet := make(map[string]bool) + + // Check both possible fields for domains + if appResp.App.CustomDomains != nil { + if appResp.App.CustomDomains.ReplicatedApp != "" { + domainSet[appResp.App.CustomDomains.ReplicatedApp] = true + } + if appResp.App.CustomDomains.ProxyRegistry != "" { + domainSet[appResp.App.CustomDomains.ProxyRegistry] = true + } + if appResp.App.CustomDomains.ReplicatedRegistry != "" { + domainSet[appResp.App.CustomDomains.ReplicatedRegistry] = true + } + } + + if appResp.App.Domains != nil { + if appResp.App.Domains.ReplicatedApp != "" { + domainSet[appResp.App.Domains.ReplicatedApp] = true + } + if appResp.App.Domains.ProxyRegistry != "" { + domainSet[appResp.App.Domains.ProxyRegistry] = true + } + if appResp.App.Domains.ReplicatedRegistry != "" { + domainSet[appResp.App.Domains.ReplicatedRegistry] = true + } + } + + // Convert set to slice + var domains []string + for domain := range domainSet { + domains = append(domains, domain) + } + + return domains, nil +} diff --git a/pkg/lint/api_client_test.go b/pkg/lint/api_client_test.go new file mode 100644 index 0000000000..01480614b1 --- /dev/null +++ b/pkg/lint/api_client_test.go @@ -0,0 +1,309 @@ +package lint + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAPIClient_GetCustomDomains(t *testing.T) { + tests := []struct { + name string + apiToken string + apiOrigin string + appID string + setupServer func(*testing.T) *httptest.Server + expectedDomains []string + expectError bool + errorMsg string + }{ + { + name: "successful fetch from channel releases", + apiToken: "test-token", + appID: "test-app", + setupServer: func(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check authorization header + assert.Equal(t, "test-token", r.Header.Get("Authorization")) + + switch r.URL.Path { + case "/v3/app/test-app/channels": + response := map[string]interface{}{ + "channels": []map[string]string{ + {"id": "channel-1", "name": "Stable"}, + {"id": "channel-2", "name": "Beta"}, + }, + } + json.NewEncoder(w).Encode(response) + case "/v3/app/test-app/channel/channel-1/releases", "/v3/app/test-app/channel/channel-2/releases": + response := ChannelReleasesResponse{ + ChannelReleases: []ChannelRelease{ + { + ChannelID: "channel-1", + ReleaseSequence: 1, + DefaultDomains: &Domains{ + ReplicatedApp: "custom.example.com", + ProxyRegistry: "proxy.example.com", + ReplicatedRegistry: "registry.example.com", + }, + }, + }, + } + json.NewEncoder(w).Encode(response) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + }, + expectedDomains: []string{"custom.example.com", "proxy.example.com", "registry.example.com"}, + expectError: false, + }, + { + name: "fallback to custom-hostnames endpoint", + apiToken: "test-token", + appID: "test-app", + setupServer: func(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v3/app/test-app/channels": + // Return empty channels + response := map[string]interface{}{ + "channels": []interface{}{}, + } + json.NewEncoder(w).Encode(response) + case "/v3/app/test-app/custom-hostnames": + response := CustomDomainsResponse{ + Domains: []DomainInfo{ + {Domain: "app.custom.io", Type: "replicated_app"}, + {Domain: "proxy.custom.io", Type: "proxy_registry"}, + }, + } + json.NewEncoder(w).Encode(response) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + }, + expectedDomains: []string{"app.custom.io", "proxy.custom.io"}, + expectError: false, + }, + { + name: "fallback to app endpoint", + apiToken: "test-token", + appID: "test-app", + setupServer: func(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v3/app/test-app/channels": + response := map[string]interface{}{ + "channels": []interface{}{}, + } + json.NewEncoder(w).Encode(response) + case "/v3/app/test-app/custom-hostnames": + w.WriteHeader(http.StatusNotFound) + case "/v3/app/test-app": + response := map[string]interface{}{ + "app": map[string]interface{}{ + "custom_domains": map[string]string{ + "replicated_app": "app.domain.com", + "proxy_registry": "proxy.domain.com", + "replicated_registry": "registry.domain.com", + }, + }, + } + json.NewEncoder(w).Encode(response) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + }, + expectedDomains: []string{"app.domain.com", "proxy.domain.com", "registry.domain.com"}, + expectError: false, + }, + { + name: "API returns array of strings directly", + apiToken: "test-token", + appID: "test-app", + setupServer: func(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v3/app/test-app/channels": + response := map[string]interface{}{ + "channels": []interface{}{}, + } + json.NewEncoder(w).Encode(response) + case "/v3/app/test-app/custom-hostnames": + domains := []string{"domain1.com", "domain2.com", "domain3.com"} + json.NewEncoder(w).Encode(domains) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + }, + expectedDomains: []string{"domain1.com", "domain2.com", "domain3.com"}, + expectError: false, + }, + { + name: "API error response", + apiToken: "test-token", + appID: "test-app", + setupServer: func(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v3/app/test-app/channels": + response := map[string]interface{}{ + "channels": []interface{}{}, + } + json.NewEncoder(w).Encode(response) + case "/v3/app/test-app/custom-hostnames": + w.WriteHeader(http.StatusNotFound) + case "/v3/app/test-app": + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Internal Server Error")) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + }, + expectedDomains: nil, + expectError: true, + errorMsg: "API request failed with status 500", + }, + { + name: "missing configuration", + apiToken: "", + apiOrigin: "", + appID: "", + setupServer: nil, + expectedDomains: nil, + expectError: true, + errorMsg: "API client not configured", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var server *httptest.Server + apiOrigin := tt.apiOrigin + + if tt.setupServer != nil { + server = tt.setupServer(t) + defer server.Close() + apiOrigin = server.URL + } + + client := NewAPIClient(tt.apiToken, apiOrigin, tt.appID) + + domains, err := client.GetCustomDomains() + + if tt.expectError { + require.Error(t, err) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg) + } + } else { + require.NoError(t, err) + assert.ElementsMatch(t, tt.expectedDomains, domains) + } + }) + } +} + +func TestAPIClient_isConfigured(t *testing.T) { + tests := []struct { + name string + apiToken string + apiOrigin string + appID string + expected bool + }{ + { + name: "fully configured", + apiToken: "token", + apiOrigin: "https://api.replicated.com/vendor", + appID: "app-id", + expected: true, + }, + { + name: "missing token", + apiToken: "", + apiOrigin: "https://api.replicated.com/vendor", + appID: "app-id", + expected: false, + }, + { + name: "missing origin", + apiToken: "token", + apiOrigin: "", + appID: "app-id", + expected: false, + }, + { + name: "missing app ID", + apiToken: "token", + apiOrigin: "https://api.replicated.com/vendor", + appID: "", + expected: false, + }, + { + name: "all missing", + apiToken: "", + apiOrigin: "", + appID: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewAPIClient(tt.apiToken, tt.apiOrigin, tt.appID) + assert.Equal(t, tt.expected, client.isConfigured()) + }) + } +} + +func TestNewAPIClient(t *testing.T) { + tests := []struct { + name string + apiToken string + apiOrigin string + appID string + expectedOrigin string + }{ + { + name: "origin without trailing slash", + apiToken: "token", + apiOrigin: "https://api.replicated.com/vendor", + appID: "app-id", + expectedOrigin: "https://api.replicated.com/vendor", + }, + { + name: "origin with trailing slash", + apiToken: "token", + apiOrigin: "https://api.replicated.com/vendor/", + appID: "app-id", + expectedOrigin: "https://api.replicated.com/vendor", + }, + { + name: "origin with multiple trailing slashes", + apiToken: "token", + apiOrigin: "https://api.replicated.com/vendor///", + appID: "app-id", + expectedOrigin: "https://api.replicated.com/vendor", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewAPIClient(tt.apiToken, tt.apiOrigin, tt.appID) + assert.Equal(t, tt.apiToken, client.apiToken) + assert.Equal(t, tt.expectedOrigin, client.apiOrigin) + assert.Equal(t, tt.appID, client.appID) + assert.NotNil(t, client.client) + }) + } +} diff --git a/pkg/lint/testdata/specs/01-warning-port-in-range.yaml b/pkg/lint/testdata/specs/01-warning-port-in-range.yaml new file mode 100644 index 0000000000..f31da4e065 --- /dev/null +++ b/pkg/lint/testdata/specs/01-warning-port-in-range.yaml @@ -0,0 +1,14 @@ +# This should WARN: Port 30000 is within the supported range (80-32767) +# Expected warning: port 30000 is already supported +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: test-config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + type: NodePort + nodePort: 30000 \ No newline at end of file diff --git a/pkg/lint/testdata/specs/02-warning-multiple-ports.yaml b/pkg/lint/testdata/specs/02-warning-multiple-ports.yaml new file mode 100644 index 0000000000..60b57e90ef --- /dev/null +++ b/pkg/lint/testdata/specs/02-warning-multiple-ports.yaml @@ -0,0 +1,28 @@ +# This should WARN: Multiple ports within supported range +# Expected warnings: +# - port 8080 is already supported +# - port 443 is already supported +# - port 9090 is already supported +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: multiple-invalid-ports +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 8080 + - name: openebs + values: | + apiServer: + service: + nodePort: 443 + localProvisioner: + hostpathClass: + nodePort: 50000 + - name: registry + values: | + service: + nodePort: 9090 \ No newline at end of file diff --git a/pkg/lint/testdata/specs/04-valid-ports-outside-range.yaml b/pkg/lint/testdata/specs/04-valid-ports-outside-range.yaml new file mode 100644 index 0000000000..dced2487a9 --- /dev/null +++ b/pkg/lint/testdata/specs/04-valid-ports-outside-range.yaml @@ -0,0 +1,29 @@ +# This should PASS: All ports are outside the supported range +# No errors expected +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: valid-config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + type: NodePort + nodePort: 50000 # OK: Above supported range + - name: openebs + values: | + apiServer: + service: + nodePort: 79 # OK: Below supported range + - name: registry + values: | + service: + nodePort: 32768 # OK: Above supported range + - name: monitoring + values: | + prometheus: + server: + service: + nodePort: 60000 # OK: Above supported range \ No newline at end of file diff --git a/pkg/lint/testdata/specs/06-error-custom-domains.yaml b/pkg/lint/testdata/specs/06-error-custom-domains.yaml new file mode 100644 index 0000000000..5e31dbc7b9 --- /dev/null +++ b/pkg/lint/testdata/specs/06-error-custom-domains.yaml @@ -0,0 +1,26 @@ +# This should ERROR when API credentials are provided with invalid domains +# Expected errors (when REPLICATED_API_TOKEN, REPLICATED_API_ORIGIN, REPLICATED_APP are set): +# - custom domain "invalid.example.com" not found +# - custom domain "fake.proxy.com" not found +# - custom domain "notreal.registry.com" not found +# +# To test: +# REPLICATED_API_TOKEN="your-token" \ +# REPLICATED_API_ORIGIN="https://api.replicated.com/vendor" \ +# REPLICATED_APP="your-app-id" \ +# embedded-cluster lint 06-error-custom-domains.yaml +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: invalid-domains +spec: + domains: + replicatedAppDomain: invalid.example.com # ERROR if API check enabled + proxyRegistryDomain: fake.proxy.com # ERROR if API check enabled + replicatedRegistryDomain: notreal.registry.com # ERROR if API check enabled + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 50000 # OK: Outside range \ No newline at end of file diff --git a/pkg/lint/testdata/specs/example-mixed-warnings-errors.yaml b/pkg/lint/testdata/specs/example-mixed-warnings-errors.yaml new file mode 100644 index 0000000000..6c99e81ac7 --- /dev/null +++ b/pkg/lint/testdata/specs/example-mixed-warnings-errors.yaml @@ -0,0 +1,26 @@ +# This example shows the difference between warnings and errors +# - Port issues generate WARNINGS (exit code 0) +# - Custom domain issues generate ERRORS (exit code 1) +# +# Without API credentials: Only port warnings will be shown +# With API credentials: Both port warnings AND domain errors will be shown +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +metadata: + name: mixed-example +spec: + # These domains will cause ERRORS if API validation is enabled + domains: + replicatedAppDomain: invalid.example.com # ERROR if API check enabled + proxyRegistryDomain: fake.proxy.com # ERROR if API check enabled + + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 8080 # WARNING: In supported range + - name: monitoring + values: | + service: + nodePort: 50000 # OK: Outside supported range \ No newline at end of file diff --git a/pkg/lint/testdata/specs/syntax-error-duplicate-key.yaml b/pkg/lint/testdata/specs/syntax-error-duplicate-key.yaml new file mode 100644 index 0000000000..0f61d0bda0 --- /dev/null +++ b/pkg/lint/testdata/specs/syntax-error-duplicate-key.yaml @@ -0,0 +1,6 @@ +# This should ERROR: Duplicate key in YAML (caught by strict mode) +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: "1.0.0" + version: "2.0.0" # ERROR: duplicate key "version" \ No newline at end of file diff --git a/pkg/lint/testdata/specs/syntax-error-unclosed-quote.yaml b/pkg/lint/testdata/specs/syntax-error-unclosed-quote.yaml new file mode 100644 index 0000000000..22f9cdd507 --- /dev/null +++ b/pkg/lint/testdata/specs/syntax-error-unclosed-quote.yaml @@ -0,0 +1,6 @@ +# This should ERROR: Unclosed quote +apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: "1.0.0 + # ERROR: Missing closing quote on line above \ No newline at end of file diff --git a/pkg/lint/validator.go b/pkg/lint/validator.go new file mode 100644 index 0000000000..a7c8a03b77 --- /dev/null +++ b/pkg/lint/validator.go @@ -0,0 +1,378 @@ +package lint + +import ( + "bytes" + "fmt" + "io" + "os" + "regexp" + "strconv" + "strings" + + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "gopkg.in/yaml.v2" + k8syaml "sigs.k8s.io/yaml" +) + +// Validator validates embedded cluster configuration files +type Validator struct { + apiClient *APIClient + verbose bool +} + +// NewValidator creates a new validator instance +func NewValidator(apiToken, apiOrigin, appID string) *Validator { + return &Validator{ + apiClient: NewAPIClient(apiToken, apiOrigin, appID), + verbose: false, + } +} + +// SetVerbose enables or disables verbose mode +func (v *Validator) SetVerbose(verbose bool) { + v.verbose = verbose + v.apiClient.SetVerbose(verbose) +} + +// ValidationError represents a validation error found during linting +type ValidationError struct { + Field string + Message string +} + +func (e ValidationError) Error() string { + return fmt.Sprintf("%s: %s", e.Field, e.Message) +} + +// ValidationWarning represents a validation warning found during linting +type ValidationWarning struct { + Field string + Message string +} + +func (w ValidationWarning) String() string { + return fmt.Sprintf("%s: %s", w.Field, w.Message) +} + +// ValidationResult contains both errors and warnings from validation +type ValidationResult struct { + Errors []error + Warnings []ValidationWarning +} + +// JSONOutput represents the JSON output format for all linted files +type JSONOutput struct { + Files []FileResult `json:"files"` +} + +// FileResult represents the validation result for a single file in JSON format +type FileResult struct { + Path string `json:"path"` + Valid bool `json:"valid"` + Errors []ValidationIssue `json:"errors,omitempty"` + Warnings []ValidationIssue `json:"warnings,omitempty"` +} + +// ValidationIssue represents a single validation error or warning in JSON format +type ValidationIssue struct { + Field string `json:"field"` + Message string `json:"message"` +} + +// ToJSON converts a ValidationResult to a FileResult for JSON output +func (r *ValidationResult) ToJSON(path string) FileResult { + result := FileResult{ + Path: path, + Valid: len(r.Errors) == 0, + Errors: []ValidationIssue{}, + Warnings: []ValidationIssue{}, + } + + for _, err := range r.Errors { + if ve, ok := err.(ValidationError); ok { + result.Errors = append(result.Errors, ValidationIssue(ve)) + } else { + result.Errors = append(result.Errors, ValidationIssue{ + Field: "", + Message: err.Error(), + }) + } + } + + for _, warning := range r.Warnings { + result.Warnings = append(result.Warnings, ValidationIssue(warning)) + } + + return result +} + +// ValidateFile validates a single configuration file +func (v *Validator) ValidateFile(path string) (*ValidationResult, error) { + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + // Validate YAML syntax first (before attempting to parse into structs) + // This provides better error messages for syntax errors + if err := v.validateYAMLSyntax(data); err != nil { + return nil, err + } + + // Parse the config using k8s yaml which properly handles the embedded types + var config ecv1beta1.Config + if err := k8syaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse YAML: %w", err) + } + + result := &ValidationResult{ + Errors: []error{}, + Warnings: []ValidationWarning{}, + } + + // Validate ports in unsupportedOverrides (returns warnings) + portWarnings := v.validatePorts(config.Spec.UnsupportedOverrides) + result.Warnings = append(result.Warnings, portWarnings...) + + // Validate custom domains if environment variables are set (returns errors) + if v.apiClient.isConfigured() { + if v.verbose { + v.apiClient.logConfiguration() + } + domainErrors, err := v.validateDomainsWithAPI(config.Spec.Domains) + if err != nil { + // Log the API error but continue validation + result.Errors = append(result.Errors, ValidationError{ + Field: "domains", + Message: fmt.Sprintf("failed to fetch custom domains from API: %v", err), + }) + } else { + result.Errors = append(result.Errors, domainErrors...) + } + } else if config.Spec.Domains.ReplicatedAppDomain != "" || + config.Spec.Domains.ProxyRegistryDomain != "" || + config.Spec.Domains.ReplicatedRegistryDomain != "" { + // Config has custom domains but API validation is not configured + missing := v.apiClient.getMissingConfig() + if len(missing) > 0 { + fmt.Fprintf(os.Stderr, "INFO: Skipping custom domain validation. Missing environment variable(s):\n") + for _, m := range missing { + fmt.Fprintf(os.Stderr, " - %s\n", m) + } + fmt.Fprintf(os.Stderr, "Set all three environment variables to enable custom domain validation.\n") + } + } + + return result, nil +} + +// validateYAMLSyntax validates that the YAML is syntactically correct +// Adapted from kots-lint/pkg/kots/lint.go:788-846 +func (v *Validator) validateYAMLSyntax(data []byte) error { + reader := bytes.NewReader(data) + decoder := yaml.NewDecoder(reader) + decoder.SetStrict(true) // Catches duplicate keys, wrong types, etc. + + for { + var doc interface{} + err := decoder.Decode(&doc) + + if err == nil { + continue // Document valid, check next + } + if err == io.EOF { + break // All documents checked + } + + // YAML syntax error found - extract line number from error message + lineNum := extractLineNumber(err.Error()) + if lineNum > 0 { + return fmt.Errorf("YAML syntax error at line %d: %v", lineNum, err) + } + return fmt.Errorf("YAML syntax error: %v", err) + } + + return nil +} + +// extractLineNumber extracts line number from YAML error messages +// YAML errors typically look like: "yaml: line 15: mapping values are not allowed in this context" +func extractLineNumber(errMsg string) int { + re := regexp.MustCompile(`line (\d+)`) + matches := re.FindStringSubmatch(errMsg) + if len(matches) > 1 { + if line, err := strconv.Atoi(matches[1]); err == nil { + return line + } + } + return 0 // Unknown line +} + +// validatePorts validates that ports in unsupportedOverrides are not already supported +func (v *Validator) validatePorts(overrides ecv1beta1.UnsupportedOverrides) []ValidationWarning { + var warnings []ValidationWarning + + // Default supported port range is 80-32767 + minSupportedPort := 80 + maxSupportedPort := 32767 + + for _, ext := range overrides.BuiltInExtensions { + if ext.Values == "" { + continue + } + + // Parse the YAML values + var values interface{} + if err := yaml.Unmarshal([]byte(ext.Values), &values); err != nil { + warnings = append(warnings, ValidationWarning{ + Field: fmt.Sprintf("unsupportedOverrides.builtInExtensions[%s]", ext.Name), + Message: fmt.Sprintf("failed to parse YAML values: %v", err), + }) + continue + } + + // Look for nodePort settings + ports := v.extractNodePorts(values, []string{}) + for _, portInfo := range ports { + port := portInfo.port + path := portInfo.path + + if port >= minSupportedPort && port <= maxSupportedPort { + warnings = append(warnings, ValidationWarning{ + Field: fmt.Sprintf("unsupportedOverrides.builtInExtensions[%s].%s", ext.Name, strings.Join(path, ".")), + Message: fmt.Sprintf("port %d is already supported (supported range: %d-%d) and should not be in unsupportedOverrides", + port, minSupportedPort, maxSupportedPort), + }) + } + } + } + + return warnings +} + +type portInfo struct { + port int + path []string +} + +// extractNodePorts recursively extracts nodePort values from the parsed YAML +func (v *Validator) extractNodePorts(data interface{}, path []string) []portInfo { + var ports []portInfo + + switch val := data.(type) { + case map[interface{}]interface{}: + for k, value := range val { + key, ok := k.(string) + if !ok { + continue + } + + newPath := append(append([]string{}, path...), key) + + // Check if this is a nodePort field + if key == "nodePort" { + if port := v.extractPortValue(value); port > 0 { + ports = append(ports, portInfo{port: port, path: newPath}) + } + } else { + // Recursively search in nested structures + ports = append(ports, v.extractNodePorts(value, newPath)...) + } + } + case map[string]interface{}: + for k, value := range val { + newPath := append(append([]string{}, path...), k) + + // Check if this is a nodePort field + if k == "nodePort" { + if port := v.extractPortValue(value); port > 0 { + ports = append(ports, portInfo{port: port, path: newPath}) + } + } else { + // Recursively search in nested structures + ports = append(ports, v.extractNodePorts(value, newPath)...) + } + } + case []interface{}: + for i, item := range val { + newPath := append(append([]string{}, path...), fmt.Sprintf("[%d]", i)) + ports = append(ports, v.extractNodePorts(item, newPath)...) + } + } + + return ports +} + +// extractPortValue extracts a port number from various types +func (v *Validator) extractPortValue(value interface{}) int { + switch v := value.(type) { + case int: + return v + case float64: + return int(v) + case string: + // Try to parse as integer + if port, err := strconv.Atoi(v); err == nil { + return port + } + } + return 0 +} + +// validateDomainsWithAPI validates custom domains by fetching allowed domains from the API +func (v *Validator) validateDomainsWithAPI(domains ecv1beta1.Domains) ([]error, error) { + // Fetch allowed custom domains from API + customDomains, err := v.apiClient.GetCustomDomains() + if err != nil { + return nil, err + } + + // Call the pure validation function + return v.validateDomains(domains, customDomains), nil +} + +// validateDomains validates custom domains against a list of allowed domains (pure function) +func (v *Validator) validateDomains(domains ecv1beta1.Domains, allowedDomains []string) []error { + var errors []error + + // Create a set of allowed domains for easy lookup + allowedSet := make(map[string]bool) + for _, domain := range allowedDomains { + allowedSet[domain] = true + } + + // Also add default domains as they're always allowed + allowedSet["replicated.app"] = true + allowedSet["proxy.replicated.com"] = true + allowedSet["registry.replicated.com"] = true + + // Check each configured domain + if domains.ReplicatedAppDomain != "" && !allowedSet[domains.ReplicatedAppDomain] { + errors = append(errors, ValidationError{ + Field: "domains.replicatedAppDomain", + Message: fmt.Sprintf("custom domain %q not found in app's configured domains", domains.ReplicatedAppDomain), + }) + } + + if domains.ProxyRegistryDomain != "" && !allowedSet[domains.ProxyRegistryDomain] { + errors = append(errors, ValidationError{ + Field: "domains.proxyRegistryDomain", + Message: fmt.Sprintf("custom domain %q not found in app's configured domains", domains.ProxyRegistryDomain), + }) + } + + if domains.ReplicatedRegistryDomain != "" && !allowedSet[domains.ReplicatedRegistryDomain] { + errors = append(errors, ValidationError{ + Field: "domains.replicatedRegistryDomain", + Message: fmt.Sprintf("custom domain %q not found in app's configured domains", domains.ReplicatedRegistryDomain), + }) + } + + return errors +} diff --git a/pkg/lint/validator_test.go b/pkg/lint/validator_test.go new file mode 100644 index 0000000000..294fc4caa3 --- /dev/null +++ b/pkg/lint/validator_test.go @@ -0,0 +1,554 @@ +package lint + +import ( + "os" + "path/filepath" + "testing" + + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidatePorts(t *testing.T) { + tests := []struct { + name string + yamlContent string + expectWarning bool + warningCount int + warningMsg string + }{ + { + name: "valid port outside supported range", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 50000`, + expectWarning: false, + }, + { + name: "port within supported range - should warn", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 8080`, + expectWarning: true, + warningCount: 1, + warningMsg: "port 8080 is already supported", + }, + { + name: "multiple ports mixed valid and invalid", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: openebs + values: | + apiServer: + service: + nodePort: 5000 + - name: adminconsole + values: | + service: + nodePort: 50000`, + expectWarning: true, + warningCount: 1, + warningMsg: "port 5000 is already supported", + }, + { + name: "port at boundary - 80", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 80`, + expectWarning: true, + warningCount: 1, + warningMsg: "port 80 is already supported", + }, + { + name: "port at boundary - 32767", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 32767`, + expectWarning: true, + warningCount: 1, + warningMsg: "port 32767 is already supported", + }, + { + name: "port outside boundary - 79", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 79`, + expectWarning: false, + }, + { + name: "port outside boundary - 32768", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: | + service: + nodePort: 32768`, + expectWarning: false, + }, + { + name: "nested nodePort in complex structure", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: monitoring + values: | + prometheus: + server: + service: + nodePort: 30090 + grafana: + service: + nodePort: 30091`, + expectWarning: true, + warningCount: 2, + warningMsg: "already supported", + }, + { + name: "empty values", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + unsupportedOverrides: + builtInExtensions: + - name: adminconsole + values: ""`, + expectWarning: false, + }, + { + name: "no unsupportedOverrides", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: {}`, + expectWarning: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "config.yaml") + err := os.WriteFile(tmpFile, []byte(tt.yamlContent), 0644) + require.NoError(t, err) + + // Create validator without API client + validator := NewValidator("", "", "") + + // Validate the file + result, err := validator.ValidateFile(tmpFile) + require.NoError(t, err) + + if tt.expectWarning { + assert.Len(t, result.Warnings, tt.warningCount, "Expected %d warnings but got %d", tt.warningCount, len(result.Warnings)) + if tt.warningMsg != "" && len(result.Warnings) > 0 { + assert.Contains(t, result.Warnings[0].String(), tt.warningMsg) + } + } else { + assert.Empty(t, result.Warnings, "Expected no warnings but got: %v", result.Warnings) + } + // Port validation should never produce errors, only warnings + assert.Empty(t, result.Errors, "Port validation should not produce errors") + }) + } +} + +func TestExtractNodePorts(t *testing.T) { + tests := []struct { + name string + data interface{} + expected []int + }{ + { + name: "simple nodePort", + data: map[interface{}]interface{}{ + "nodePort": 8080, + }, + expected: []int{8080}, + }, + { + name: "nested nodePort", + data: map[string]interface{}{ + "service": map[string]interface{}{ + "type": "NodePort", + "nodePort": 30000, + }, + }, + expected: []int{30000}, + }, + { + name: "multiple nodePorts", + data: map[string]interface{}{ + "service1": map[string]interface{}{ + "nodePort": 30001, + }, + "service2": map[string]interface{}{ + "nodePort": 30002, + }, + }, + expected: []int{30001, 30002}, + }, + { + name: "nodePort as string", + data: map[string]interface{}{ + "nodePort": "8080", + }, + expected: []int{8080}, + }, + { + name: "nodePort as float", + data: map[string]interface{}{ + "nodePort": float64(8080), + }, + expected: []int{8080}, + }, + { + name: "invalid nodePort string", + data: map[string]interface{}{ + "nodePort": "not-a-number", + }, + expected: []int{}, + }, + { + name: "array with nodePorts", + data: []interface{}{ + map[string]interface{}{ + "nodePort": 30001, + }, + map[string]interface{}{ + "nodePort": 30002, + }, + }, + expected: []int{30001, 30002}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validator := NewValidator("", "", "") + portInfos := validator.extractNodePorts(tt.data, []string{}) + + var ports []int + for _, pi := range portInfos { + ports = append(ports, pi.port) + } + + assert.ElementsMatch(t, tt.expected, ports, "Expected ports %v but got %v", tt.expected, ports) + }) + } +} + +func TestExtractPortValue(t *testing.T) { + tests := []struct { + name string + value interface{} + expected int + }{ + { + name: "int value", + value: 8080, + expected: 8080, + }, + { + name: "float64 value", + value: float64(8080), + expected: 8080, + }, + { + name: "string value", + value: "8080", + expected: 8080, + }, + { + name: "invalid string", + value: "not-a-port", + expected: 0, + }, + { + name: "nil value", + value: nil, + expected: 0, + }, + { + name: "boolean value", + value: true, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validator := NewValidator("", "", "") + result := validator.extractPortValue(tt.value) + assert.Equal(t, tt.expected, result, "Expected %d but got %d", tt.expected, result) + }) + } +} + +func TestValidateYAMLSyntax(t *testing.T) { + tests := []struct { + name string + yamlContent string + expectError bool + errorMsg string + }{ + { + name: "valid yaml", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: "1.0.0"`, + expectError: false, + }, + { + name: "valid multi-document yaml", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: "1.0.0" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test`, + expectError: false, + }, + { + name: "duplicate key - strict mode", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: "1.0.0" + version: "2.0.0"`, + expectError: true, + errorMsg: "line 5", + }, + { + name: "tab character mixed with spaces", + yamlContent: "apiVersion: embeddedcluster.replicated.com/v1beta1\nkind: Config\nspec:\n\tversion: \"1.0.0\"", + expectError: true, + errorMsg: "line", + }, + { + name: "unclosed quote", + yamlContent: `apiVersion: embeddedcluster.replicated.com/v1beta1 +kind: Config +spec: + version: "1.0.0`, + expectError: true, + errorMsg: "line", + }, + { + name: "empty yaml", + yamlContent: ``, + expectError: false, + }, + { + name: "only comments", + yamlContent: `# This is a comment +# Another comment`, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validator := NewValidator("", "", "") + err := validator.validateYAMLSyntax([]byte(tt.yamlContent)) + + if tt.expectError { + require.Error(t, err, "Expected YAML syntax error but got none") + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain: %s", tt.errorMsg) + } + } else { + assert.NoError(t, err, "Expected no error but got: %v", err) + } + }) + } +} + +func TestExtractLineNumber(t *testing.T) { + tests := []struct { + name string + errMsg string + expected int + }{ + { + name: "yaml error with line number", + errMsg: "yaml: line 15: mapping values are not allowed in this context", + expected: 15, + }, + { + name: "yaml error with different line", + errMsg: "yaml: line 42: found unexpected end of stream", + expected: 42, + }, + { + name: "error without line number", + errMsg: "yaml: invalid yaml", + expected: 0, + }, + { + name: "empty error message", + errMsg: "", + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractLineNumber(tt.errMsg) + assert.Equal(t, tt.expected, result, "Expected line %d but got %d", tt.expected, result) + }) + } +} + +func TestValidateDomains(t *testing.T) { + tests := []struct { + name string + configDomains ecv1beta1.Domains + allowedDomains []string + expectErrors int + errorFields []string + }{ + { + name: "valid custom domain", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "custom.example.com", + }, + allowedDomains: []string{"custom.example.com"}, + expectErrors: 0, + }, + { + name: "invalid custom domain", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "invalid.example.com", + }, + allowedDomains: []string{"custom.example.com"}, + expectErrors: 1, + errorFields: []string{"domains.replicatedAppDomain"}, + }, + { + name: "default domains always allowed", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "replicated.app", + ProxyRegistryDomain: "proxy.replicated.com", + ReplicatedRegistryDomain: "registry.replicated.com", + }, + allowedDomains: []string{}, // Empty list, but defaults should still be allowed + expectErrors: 0, + }, + { + name: "multiple invalid domains", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "bad1.example.com", + ProxyRegistryDomain: "bad2.example.com", + ReplicatedRegistryDomain: "bad3.example.com", + }, + allowedDomains: []string{}, + expectErrors: 3, + errorFields: []string{"domains.replicatedAppDomain", "domains.proxyRegistryDomain", "domains.replicatedRegistryDomain"}, + }, + { + name: "mixed valid and invalid domains", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "custom.example.com", + ProxyRegistryDomain: "invalid.example.com", + ReplicatedRegistryDomain: "registry.replicated.com", // Default + }, + allowedDomains: []string{"custom.example.com"}, + expectErrors: 1, + errorFields: []string{"domains.proxyRegistryDomain"}, + }, + { + name: "empty domain configuration", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "", + ProxyRegistryDomain: "", + ReplicatedRegistryDomain: "", + }, + allowedDomains: []string{"any.example.com"}, + expectErrors: 0, // No domains to validate + }, + { + name: "custom domain in allowed list", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "app.custom.io", + ProxyRegistryDomain: "proxy.custom.io", + }, + allowedDomains: []string{"app.custom.io", "proxy.custom.io", "registry.custom.io"}, + expectErrors: 0, + }, + { + name: "one valid, two invalid", + configDomains: ecv1beta1.Domains{ + ReplicatedAppDomain: "replicated.app", // Default - valid + ProxyRegistryDomain: "bad.com", + ReplicatedRegistryDomain: "alsobad.com", + }, + allowedDomains: []string{}, + expectErrors: 2, + errorFields: []string{"domains.proxyRegistryDomain", "domains.replicatedRegistryDomain"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validator := NewValidator("", "", "") + + errors := validator.validateDomains(tt.configDomains, tt.allowedDomains) + + assert.Len(t, errors, tt.expectErrors, "Expected %d errors but got %d", tt.expectErrors, len(errors)) + + // Check that expected error fields are present + if tt.expectErrors > 0 && len(tt.errorFields) > 0 { + for i, expectedField := range tt.errorFields { + if i < len(errors) { + if ve, ok := errors[i].(ValidationError); ok { + assert.Equal(t, expectedField, ve.Field, "Error field mismatch at index %d", i) + } + } + } + } + }) + } +} diff --git a/tests/dryrun/assets/bundle.airgap b/tests/dryrun/assets/bundle.airgap index 9644c4deb5..451871f219 100644 Binary files a/tests/dryrun/assets/bundle.airgap and b/tests/dryrun/assets/bundle.airgap differ diff --git a/tests/dryrun/assets/cluster-config.yaml b/tests/dryrun/assets/cluster-config.yaml index ec23869943..1a139cffc2 100644 --- a/tests/dryrun/assets/cluster-config.yaml +++ b/tests/dryrun/assets/cluster-config.yaml @@ -7,3 +7,19 @@ spec: domains: replicatedAppDomain: "fake-endpoint.com" proxyRegistryDomain: "fake-replicated-proxy.test.net" + unsupportedOverrides: + k0s: | + config: + metadata: + name: testing-overrides-k0s-name + spec: + telemetry: + enabled: false + api: + extraArgs: + test-key: test-value + workerProfiles: + - name: ip-forward + values: + allowedUnsafeSysctls: + - net.ipv4.ip_forward diff --git a/tests/dryrun/install_test.go b/tests/dryrun/install_test.go index de7a529054..5b9ed83c82 100644 --- a/tests/dryrun/install_test.go +++ b/tests/dryrun/install_test.go @@ -2,7 +2,10 @@ package dryrun import ( "context" + "crypto/x509" _ "embed" + "encoding/json" + "encoding/pem" "fmt" "os" "path/filepath" @@ -202,6 +205,30 @@ func testDefaultInstallationImpl(t *testing.T) { assert.Contains(t, k0sConfig.Spec.Images.Calico.CNI.Image, "fake-replicated-proxy.test.net/library") assert.Contains(t, k0sConfig.Spec.Images.Calico.Node.Image, "fake-replicated-proxy.test.net/library") assert.Contains(t, k0sConfig.Spec.Images.Calico.KubeControllers.Image, "fake-replicated-proxy.test.net/library") + + // validate unsupported overrides were applied + assert.Equal(t, "testing-overrides-k0s-name", k0sConfig.Name, "k0s config name should be set from unsupported-overrides") + + // telemetry + assert.NotNil(t, k0sConfig.Spec.Telemetry, "telemetry config should exist from unsupported-overrides") + require.NotNil(t, k0sConfig.Spec.Telemetry.Enabled, "telemetry enabled field should exist") + assert.False(t, *k0sConfig.Spec.Telemetry.Enabled, "telemetry should be enabled from unsupported-overrides") + + // api extraArgs + require.NotNil(t, k0sConfig.Spec.API, "api config should exist") + require.NotNil(t, k0sConfig.Spec.API.ExtraArgs, "api extraArgs should exist") + assert.Equal(t, "test-value", k0sConfig.Spec.API.ExtraArgs["test-key"], "api extraArgs should contain test-key from unsupported-overrides") + + // worker profiles + require.Len(t, k0sConfig.Spec.WorkerProfiles, 1, "workerProfiles should have one profile from unsupported-overrides") + assert.Equal(t, "ip-forward", k0sConfig.Spec.WorkerProfiles[0].Name, "workerProfile name should be set from unsupported-overrides") + require.NotNil(t, k0sConfig.Spec.WorkerProfiles[0].Config, "workerProfile config should exist") + + var profileConfig map[string]interface{} + err = json.Unmarshal(k0sConfig.Spec.WorkerProfiles[0].Config.Raw, &profileConfig) + require.NoError(t, err, "should be able to unmarshal workerProfile config") + sysctls := profileConfig["allowedUnsafeSysctls"].([]interface{}) + assert.Equal(t, "net.ipv4.ip_forward", sysctls[0], "allowedUnsafeSysctls should contain net.ipv4.ip_forward from unsupported-overrides") } func TestCustomDataDir(t *testing.T) { @@ -488,14 +515,18 @@ func TestCustomCidrInstallation(t *testing.T) { hcli := &helm.MockClient{} mock.InOrder( - // 4 addons - hcli.On("Install", mock.Anything, mock.Anything).Times(4).Return(nil, nil), + // 5 addons + hcli.On("Install", mock.Anything, mock.Anything).Times(5).Return(nil, nil), hcli.On("Close").Once().Return(nil), ) dr := dryrunInstall(t, &dryrun.Client{HelmClient: hcli}, "--cidr", "10.2.0.0/16", + "--airgap-bundle", airgapBundleFile(t), + "--http-proxy", "http://localhost:3128", + "--https-proxy", "https://localhost:3128", + "--no-proxy", "localhost,127.0.0.1,10.0.0.0/8", ) // --- validate commands --- // @@ -515,6 +546,96 @@ func TestCustomCidrInstallation(t *testing.T) { assert.Equal(t, "10.2.0.0/17", k0sConfig.Spec.Network.PodCIDR) assert.Equal(t, "10.2.128.0/17", k0sConfig.Spec.Network.ServiceCIDR) + // --- validate registry --- // + expectedRegistryIP := "10.2.128.11" // lower band index 10 + + kcli, err := dr.KubeClient() + require.NoError(t, err, "get kube client") + + var registrySecret corev1.Secret + err = kcli.Get(context.TODO(), types.NamespacedName{Name: "registry-tls", Namespace: "registry"}, ®istrySecret) + require.NoError(t, err, "get registry TLS secret") + + certData, ok := registrySecret.StringData["tls.crt"] + require.True(t, ok, "registry TLS secret must contain tls.crt") + + // parse certificate and verify it contains the expected IP + block, _ := pem.Decode([]byte(certData)) + require.NotNil(t, block, "failed to decode certificate PEM") + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err, "failed to parse certificate") + + // check if certificate contains the expected registry IP (convert to strings for comparison) + ipStrings := make([]string, len(cert.IPAddresses)) + for i, ip := range cert.IPAddresses { + ipStrings[i] = ip.String() + } + assert.Contains(t, ipStrings, expectedRegistryIP, "certificate should contain registry IP %s, found IPs: %v", expectedRegistryIP, cert.IPAddresses) + + // --- validate cidrs in NO_PROXY OS env var --- // + noProxy := dr.OSEnv["NO_PROXY"] + assert.Contains(t, noProxy, "10.2.0.0/17") + assert.Contains(t, noProxy, "10.2.128.0/17") + + // --- validate cidrs in NO_PROXY Helm value of operator chart --- // + assert.Equal(t, "Install", hcli.Calls[1].Method) + operatorOpts := hcli.Calls[1].Arguments[1].(helm.InstallOptions) + assert.Equal(t, "embedded-cluster-operator", operatorOpts.ReleaseName) + + found := false + for _, env := range operatorOpts.Values["extraEnv"].([]map[string]interface{}) { + if env["name"] == "NO_PROXY" { + assert.Equal(t, noProxy, env["value"]) + found = true + } + } + assert.True(t, found, "NO_PROXY env var not found in operator opts") + + // --- validate custom cidr was used for registry service cluster IP --- // + assert.Equal(t, "Install", hcli.Calls[2].Method) + registryOpts := hcli.Calls[2].Arguments[1].(helm.InstallOptions) + assert.Equal(t, "docker-registry", registryOpts.ReleaseName) + assertHelmValues(t, registryOpts.Values, map[string]interface{}{ + "service.clusterIP": expectedRegistryIP, + }) + + // --- validate cidrs in NO_PROXY Helm value of velero chart --- // + assert.Equal(t, "Install", hcli.Calls[3].Method) + veleroOpts := hcli.Calls[3].Arguments[1].(helm.InstallOptions) + assert.Equal(t, "velero", veleroOpts.ReleaseName) + found = false + + extraEnvVars, err := helm.GetValue(veleroOpts.Values, "configuration.extraEnvVars") + require.NoError(t, err) + + for _, env := range extraEnvVars.([]map[string]interface{}) { + if env["name"] == "NO_PROXY" { + assert.Equal(t, noProxy, env["value"]) + found = true + } + } + assert.True(t, found, "NO_PROXY env var not found in velero opts") + + // --- validate cidrs in NO_PROXY Helm value of admin console chart --- // + assert.Equal(t, "Install", hcli.Calls[4].Method) + adminConsoleOpts := hcli.Calls[4].Arguments[1].(helm.InstallOptions) + assert.Equal(t, "admin-console", adminConsoleOpts.ReleaseName) + + found = false + for _, env := range adminConsoleOpts.Values["extraEnv"].([]map[string]interface{}) { + if env["name"] == "NO_PROXY" { + assert.Equal(t, noProxy, env["value"]) + found = true + } + } + assert.True(t, found, "NO_PROXY env var not found in admin console opts") + + // --- validate custom cidrs in NO_PROXY in http-proxy.conf file --- // + proxyConfPath := "/etc/systemd/system/k0scontroller.service.d/http-proxy.conf" + proxyConfContent, err := os.ReadFile(proxyConfPath) + require.NoError(t, err, "failed to read http-proxy.conf file") + assert.Contains(t, string(proxyConfContent), fmt.Sprintf(`Environment="NO_PROXY=%s"`, noProxy), "http-proxy.conf should contain NO_PROXY with custom CIDRs") + t.Logf("%s: test complete", time.Now().Format(time.RFC3339)) } diff --git a/web/package-lock.json b/web/package-lock.json index 73fbd06634..80a8bfb368 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,35 +12,35 @@ "@tailwindcss/postcss": "^4.1.16", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.5", - "lucide-react": "^0.548.0", + "lucide-react": "^0.552.0", "openapi-fetch": "^0.15.0", "react": "^19.1.1", "react-dom": "^19.2.0", "react-markdown": "^10.1.0", - "react-router-dom": "^7.9.4", + "react-router-dom": "^7.9.5", "remark-gfm": "^4.0.1" }, "devDependencies": { - "@eslint/js": "^9.38.0", + "@eslint/js": "^9.39.0", "@faker-js/faker": "^10.1.0", "@netlify/functions": "^5.0.1", - "@netlify/vite-plugin": "^2.7.6", + "@netlify/vite-plugin": "^2.7.8", "@tailwindcss/vite": "^4.1.16", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.9.1", + "@types/node": "^24.9.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.1.0", "autoprefixer": "^10.4.21", - "eslint": "^9.38.0", + "eslint": "^9.39.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.4.0", - "jsdom": "^27.0.1", + "jsdom": "^27.1.0", "msw": "2.11.6", "openapi-backend": "^5.15.0", "openapi-typescript": "^7.10.1", @@ -53,6 +53,13 @@ "vitest": "^3.2.4" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.19", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.19.tgz", + "integrity": "sha512-Pp2gAQXPZ2o7lt4j0IMwNRXqQ3pagxtDj5wctL5U2Lz4oV0ocDNlkgx4DpxfyKav4S/bePuI+SMqcBSUHLy9kg==", + "dev": true, + "license": "MIT" + }, "node_modules/@adobe/css-tools": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", @@ -543,9 +550,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", - "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.15.tgz", + "integrity": "sha512-q0p6zkVq2lJnmzZVPR33doA51G7YOja+FBvRdp5ISIthL0MtFCgYHHhR563z9WFGxcOn0WfjSkPDJ5Qig3H3Sw==", "dev": true, "funding": [ { @@ -560,9 +567,6 @@ "license": "MIT-0", "engines": { "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" } }, "node_modules/@csstools/css-tokenizer": { @@ -660,9 +664,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -677,9 +681,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -694,9 +698,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -711,9 +715,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -728,9 +732,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -745,9 +749,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -762,9 +766,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -779,9 +783,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -796,9 +800,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -813,9 +817,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -830,9 +834,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -847,9 +851,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -864,9 +868,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -881,9 +885,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -898,9 +902,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -915,9 +919,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -932,9 +936,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -949,9 +953,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -966,9 +970,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -983,9 +987,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -1000,9 +1004,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -1017,9 +1021,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], @@ -1034,9 +1038,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -1051,9 +1055,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -1068,9 +1072,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -1085,9 +1089,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -1158,22 +1162,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0" + "@eslint/core": "^0.17.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1221,9 +1225,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", + "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", "dev": true, "license": "MIT", "engines": { @@ -1244,13 +1248,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.16.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -2197,13 +2201,13 @@ } }, "node_modules/@netlify/api": { - "version": "14.0.7", - "resolved": "https://registry.npmjs.org/@netlify/api/-/api-14.0.7.tgz", - "integrity": "sha512-smSD3MnyUwi+rdcvRlD2EAGEpzK9RRMiGxaXzKW7FGqIlZSEe08aPySuC0d5BXwl/7EmT43hOkBHH4KtP8DxcQ==", + "version": "14.0.9", + "resolved": "https://registry.npmjs.org/@netlify/api/-/api-14.0.9.tgz", + "integrity": "sha512-fKmMheaHDps5K8T3lyidFR+nCkpqEkkFLNz5YGptlaocz7LpgEZvmluv20XwqMEXQ3WBGZzXeJMns87Tdmtw2Q==", "dev": true, "license": "MIT", "dependencies": { - "@netlify/open-api": "^2.40.0", + "@netlify/open-api": "^2.42.0", "node-fetch": "^3.0.0", "p-wait-for": "^5.0.0", "picoquery": "^2.5.0" @@ -2220,14 +2224,14 @@ "license": "Apache 2" }, "node_modules/@netlify/blobs": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-10.2.1.tgz", - "integrity": "sha512-G7MAvZqhPLGUqZByS9n7g7TEwYDsneHpPi/ehzZVADzH0vF2DyeWUhG1ZkwvurgIuFuhmcHBlxoLlsvf9SNFqQ==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-10.3.1.tgz", + "integrity": "sha512-s3GokkzyZzZfGk5UjEcI1N/lDmUKYJ7K7WyHOBltqyVAO7Woo6D6eVH3vEeatN/FgSPA2WbDOmL1Tei6wku3kw==", "dev": true, "license": "MIT", "dependencies": { "@netlify/dev-utils": "4.3.0", - "@netlify/otel": "^4.3.1", + "@netlify/otel": "^4.3.2", "@netlify/runtime-utils": "2.2.0" }, "engines": { @@ -2402,21 +2406,21 @@ } }, "node_modules/@netlify/dev": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/@netlify/dev/-/dev-4.6.5.tgz", - "integrity": "sha512-KPORik3v1+lfDr74+OHwZCKxBHw8ybCl+2XURoM94+oGbjsGt1Tq1J3rqwTA3Px6HVrG3sxywJsovOPVEkKEaw==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/@netlify/dev/-/dev-4.6.7.tgz", + "integrity": "sha512-sJ2zPEQB93xZ+o+ABtKdKTtwNOUZaRPbXhZovxfAJwQMTQRjBj0+jnpI6O9htF/wdhOkqyOHa1TUOwVXAXcddg==", "dev": true, "license": "MIT", "dependencies": { - "@netlify/blobs": "10.2.1", + "@netlify/blobs": "10.3.1", "@netlify/config": "^23.2.0", "@netlify/dev-utils": "4.3.0", - "@netlify/edge-functions-dev": "1.0.1", - "@netlify/functions-dev": "1.0.2", + "@netlify/edge-functions-dev": "1.0.2", + "@netlify/functions-dev": "1.0.4", "@netlify/headers": "2.1.0", "@netlify/images": "1.3.0", "@netlify/redirects": "3.1.0", - "@netlify/runtime": "4.1.3", + "@netlify/runtime": "4.1.5", "@netlify/static": "3.1.0", "ulid": "^3.0.0" }, @@ -2495,9 +2499,9 @@ } }, "node_modules/@netlify/edge-bundler": { - "version": "14.8.0", - "resolved": "https://registry.npmjs.org/@netlify/edge-bundler/-/edge-bundler-14.8.0.tgz", - "integrity": "sha512-BzIZ9pn8yLaHVXUVPZQqbWgzXVvfbJsRjWSXNjYJHF7dU9vvr/q+seOOqG+epN+9Sbn9xb2KxTcfJasx7wgAmw==", + "version": "14.8.4", + "resolved": "https://registry.npmjs.org/@netlify/edge-bundler/-/edge-bundler-14.8.4.tgz", + "integrity": "sha512-fcSA0IYuuonTBI1prdTrobF42RQk8+M+9c+A1My/i+bONVfWgeNWFTYL4ZCDWrqK4gZe6Pn5/fe/ww9KquIXPg==", "dev": true, "license": "MIT", "dependencies": { @@ -2507,7 +2511,7 @@ "better-ajv-errors": "^1.2.0", "common-path-prefix": "^3.0.0", "env-paths": "^3.0.0", - "esbuild": "0.25.10", + "esbuild": "0.25.11", "execa": "^8.0.0", "find-up": "^7.0.0", "get-port": "^7.0.0", @@ -2696,14 +2700,14 @@ "license": "MIT" }, "node_modules/@netlify/edge-functions-dev": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@netlify/edge-functions-dev/-/edge-functions-dev-1.0.1.tgz", - "integrity": "sha512-Go35kPVVxVGBpSWfsLM8mPpdmSQyESaVhQJ85gGULDSHHI4MeHlsrqgbR2racdkjQU0Laqmsm8D95Qmw44Edlw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@netlify/edge-functions-dev/-/edge-functions-dev-1.0.2.tgz", + "integrity": "sha512-Twe78A7Jd0JpLw9g+XN2a4tDngM/6Qw/djt4h4Q4b3GYegEZG3/a3aKUWcHIkdimdund+LWzpIMcgUj5BvHHKw==", "dev": true, "license": "MIT", "dependencies": { "@netlify/dev-utils": "4.3.0", - "@netlify/edge-bundler": "^14.5.2", + "@netlify/edge-bundler": "^14.8.2", "@netlify/edge-functions": "3.0.1", "@netlify/edge-functions-bootstrap": "2.16.0", "@netlify/runtime-utils": "2.2.0", @@ -2727,16 +2731,16 @@ } }, "node_modules/@netlify/functions-dev": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@netlify/functions-dev/-/functions-dev-1.0.2.tgz", - "integrity": "sha512-gkZRVGetAAcAc2y/9G7cY77f2Z+IkwPzhnk77wYpaZyOW++z4SaTqfxM1IGgiAmUvIcfgZIkhjHyP/mUYEBLSQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@netlify/functions-dev/-/functions-dev-1.0.4.tgz", + "integrity": "sha512-LNutheCNiVTFMXYUi6fDl4/AA53H635k0fb14N5GNaojeUBP9R46bwBGBtm19RfvDjlyIEmYhsN4VlnqTqMbHg==", "dev": true, "license": "MIT", "dependencies": { - "@netlify/blobs": "10.2.1", + "@netlify/blobs": "10.3.1", "@netlify/dev-utils": "4.3.0", "@netlify/functions": "5.0.1", - "@netlify/zip-it-and-ship-it": "^14.1.3", + "@netlify/zip-it-and-ship-it": "^14.1.11", "cron-parser": "^4.9.0", "decache": "^4.6.2", "extract-zip": "^2.0.1", @@ -2819,9 +2823,9 @@ } }, "node_modules/@netlify/open-api": { - "version": "2.41.0", - "resolved": "https://registry.npmjs.org/@netlify/open-api/-/open-api-2.41.0.tgz", - "integrity": "sha512-e9eGmWs0AAJF5jxRlr6FJyfyftY3sj5IXyT26QF6vHXK5rzZNpJHhFenszKtuVFhUgScJeOfCOucvodqfKAzeg==", + "version": "2.42.0", + "resolved": "https://registry.npmjs.org/@netlify/open-api/-/open-api-2.42.0.tgz", + "integrity": "sha512-oHFCCo5FigIZcni/IioNKPwL9jC2++RUnHHfHFOhXKyLDGB8x93cU03N15RuZhG92+VM6dOzhM4WDEDeG6hXcQ==", "dev": true, "license": "MIT", "engines": { @@ -2829,9 +2833,9 @@ } }, "node_modules/@netlify/otel": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@netlify/otel/-/otel-4.3.1.tgz", - "integrity": "sha512-deFAOlU77Bw52YhUHcO9FFfikqGOfURsjwdgyJ24EP2xWO2CPgDgmwuCAZKNJeOCcS0qUA1xXyKhgb53n+hjxw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@netlify/otel/-/otel-4.3.2.tgz", + "integrity": "sha512-979piAUQd2UINkfy951JFUFWvscXl+t+a57uYX6ViDBlNOhV/UTZGF5THAv1es3qtng8SHFDmOiftO8D/rFEPA==", "dev": true, "license": "MIT", "dependencies": { @@ -2890,13 +2894,13 @@ } }, "node_modules/@netlify/runtime": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@netlify/runtime/-/runtime-4.1.3.tgz", - "integrity": "sha512-shnaT9MWv1Nkk6/EAXY0or/SjXnNiV+CPK1EXICQD/DPW+HAm/IZjeK61lN52N0dba96lZAqhFR7PlhGZl1/7Q==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@netlify/runtime/-/runtime-4.1.5.tgz", + "integrity": "sha512-1v0QLQN3AAErsLwc1RkEQD44Kp2V1FBnll/rQdyovNTV3LjWaiMl9uU6JlvNyN83mzhMKW5p1/wMePrPYwKBRg==", "dev": true, "license": "MIT", "dependencies": { - "@netlify/blobs": "^10.2.1", + "@netlify/blobs": "^10.3.1", "@netlify/cache": "3.3.0", "@netlify/runtime-utils": "2.2.0", "@netlify/types": "2.1.0" @@ -2949,13 +2953,13 @@ } }, "node_modules/@netlify/vite-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/@netlify/vite-plugin/-/vite-plugin-2.7.6.tgz", - "integrity": "sha512-oRpKPfi2oSs+TKW5ykA7rZ101DgYZ9VAIUkjQZwQlJKb4frU47Ok+3xA8OVf7kSBcb+bn1OQ/CneMcgp+Ryl4Q==", + "version": "2.7.8", + "resolved": "https://registry.npmjs.org/@netlify/vite-plugin/-/vite-plugin-2.7.8.tgz", + "integrity": "sha512-ebGgq3PxVANuw6HFCSN/Y4mDL19L9ptW9Syz1F/kH9L7cFOkpnsThK9CoEDIeERBLevLbHNZ4UsmsIZC6SLI9w==", "dev": true, "license": "MIT", "dependencies": { - "@netlify/dev": "4.6.5", + "@netlify/dev": "4.6.7", "@netlify/dev-utils": "^4.3.0", "dedent": "^1.7.0" }, @@ -2967,9 +2971,9 @@ } }, "node_modules/@netlify/zip-it-and-ship-it": { - "version": "14.1.11", - "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-14.1.11.tgz", - "integrity": "sha512-5Ed9XH1JVPL7pAdq9zpC2WHjqHhHkaghuV3r2bvTTpx9JrTdzZxPeNnjZRjJMkjQAi8xSped5hNFJuD0QYmOuw==", + "version": "14.1.12", + "resolved": "https://registry.npmjs.org/@netlify/zip-it-and-ship-it/-/zip-it-and-ship-it-14.1.12.tgz", + "integrity": "sha512-HdMcvyvDQBcqjzf8hUZQE+uqqiEJP3ucjBCl1FX5bly7ci1DT6qeZR1Zm6jrWAVl8y36UgAB2Nx8r4ulGg+p+w==", "dev": true, "license": "MIT", "dependencies": { @@ -2982,7 +2986,7 @@ "common-path-prefix": "^3.0.0", "copy-file": "^11.0.0", "es-module-lexer": "^1.0.0", - "esbuild": "0.25.10", + "esbuild": "0.25.11", "execa": "^8.0.0", "fast-glob": "^3.3.3", "filter-obj": "^6.0.0", @@ -4960,9 +4964,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", "dependencies": { @@ -7456,9 +7460,9 @@ "license": "CC0-1.0" }, "node_modules/cssstyle": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", - "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.2.tgz", + "integrity": "sha512-zDMqXh8Vs1CdRYZQ2M633m/SFgcjlu8RB8b/1h82i+6vpArF507NSYIWJHGlJaTWoS+imcnctmEz43txhbVkOw==", "dev": true, "license": "MIT", "dependencies": { @@ -8268,9 +8272,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8281,32 +8285,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/escalade": { @@ -8353,20 +8357,20 @@ } }, "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", + "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.0", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -8697,6 +8701,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -10739,14 +10756,15 @@ } }, "node_modules/jsdom": { - "version": "27.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", - "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "version": "27.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.1.0.tgz", + "integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/dom-selector": "^6.7.2", - "cssstyle": "^5.3.1", + "@acemir/cssom": "^0.9.19", + "@asamuzakjp/dom-selector": "^6.7.3", + "cssstyle": "^5.3.2", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", @@ -10754,7 +10772,6 @@ "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", - "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", @@ -10767,7 +10784,7 @@ "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -11435,9 +11452,9 @@ } }, "node_modules/lucide-react": { - "version": "0.548.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.548.0.tgz", - "integrity": "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==", + "version": "0.552.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz", + "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -12973,15 +12990,15 @@ } }, "node_modules/ofetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz", - "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.0.tgz", + "integrity": "sha512-A7llJ7eZyziA5xq9//3ZurA8OhFqtS99K5/V1sLBJ5j137CM/OAjlbA/TEJXBuOWwOfLqih+oH5U3ran4za1FQ==", "dev": true, "license": "MIT", "dependencies": { - "destr": "^2.0.3", - "node-fetch-native": "^1.6.4", - "ufo": "^1.5.4" + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" } }, "node_modules/omit.js": { @@ -13898,9 +13915,9 @@ } }, "node_modules/react-router": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", - "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", + "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -13920,12 +13937,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", - "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz", + "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", "license": "MIT", "dependencies": { - "react-router": "7.9.4" + "react-router": "7.9.5" }, "engines": { "node": ">=20.0.0" @@ -14317,13 +14334,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14491,9 +14501,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/set-function-length": { @@ -15252,11 +15262,11 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -15920,9 +15930,9 @@ } }, "node_modules/unstorage": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.1.tgz", - "integrity": "sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.2.tgz", + "integrity": "sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w==", "dev": true, "license": "MIT", "dependencies": { @@ -15932,7 +15942,7 @@ "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", - "ofetch": "^1.4.1", + "ofetch": "^1.5.0", "ufo": "^1.6.1" }, "peerDependencies": { diff --git a/web/package.json b/web/package.json index c902bb4bcf..b110fe5f36 100644 --- a/web/package.json +++ b/web/package.json @@ -23,35 +23,35 @@ "@tailwindcss/postcss": "^4.1.16", "@tailwindcss/typography": "^0.5.19", "@tanstack/react-query": "^5.90.5", - "lucide-react": "^0.548.0", + "lucide-react": "^0.552.0", "openapi-fetch": "^0.15.0", "react": "^19.1.1", "react-dom": "^19.2.0", "react-markdown": "^10.1.0", - "react-router-dom": "^7.9.4", + "react-router-dom": "^7.9.5", "remark-gfm": "^4.0.1" }, "devDependencies": { - "@eslint/js": "^9.38.0", + "@eslint/js": "^9.39.0", "@faker-js/faker": "^10.1.0", "@netlify/functions": "^5.0.1", - "@netlify/vite-plugin": "^2.7.6", + "@netlify/vite-plugin": "^2.7.8", "@tailwindcss/vite": "^4.1.16", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.9.1", + "@types/node": "^24.9.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.1.0", "autoprefixer": "^10.4.21", - "eslint": "^9.38.0", + "eslint": "^9.39.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react-hooks": "^6.1.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.4.0", - "jsdom": "^27.0.1", + "jsdom": "^27.1.0", "msw": "2.11.6", "openapi-backend": "^5.15.0", "openapi-typescript": "^7.10.1",