From e995d82721deb7899a93dda6e4225a7b7cebd7ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:58:38 +0100 Subject: [PATCH 01/48] build(deps-dev): bump ts-retry from 4.2.5 to 6.0.0 in /e2e/playwright (#2268) Bumps [ts-retry](https://github.com/franckLdx/ts-retry) from 4.2.5 to 6.0.0. - [Release notes](https://github.com/franckLdx/ts-retry/releases) - [Changelog](https://github.com/franckLdx/ts-retry/blob/master/changelog.md) - [Commits](https://github.com/franckLdx/ts-retry/compare/v4.2.5...v6.0.0) --- updated-dependencies: - dependency-name: ts-retry dependency-version: 6.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- e2e/playwright/package-lock.json | 11 ++++++----- e2e/playwright/package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/e2e/playwright/package-lock.json b/e2e/playwright/package-lock.json index 97121082b..4cc83d7e5 100644 --- a/e2e/playwright/package-lock.json +++ b/e2e/playwright/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@playwright/test": "^1.48.0", "@types/node": "^24.0.1", - "ts-retry": "^4.2.5" + "ts-retry": "^6.0.0" } }, "node_modules/@playwright/test": { @@ -88,10 +88,11 @@ } }, "node_modules/ts-retry": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/ts-retry/-/ts-retry-4.2.5.tgz", - "integrity": "sha512-dFBa4pxMBkt/bjzdBio8EwYfbAdycEAwe0KZgzlUKKwU9Wr1WErK7Hg9QLqJuDDYJXTW4KYZyXAyqYKOdO/ehA==", - "dev": true + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ts-retry/-/ts-retry-6.0.0.tgz", + "integrity": "sha512-WsVRE/P+VNYbiQC3E6TeIXBRCQj7vzjN4MlXd84AC88K7WwuWShN7A3Q/QSV/yd1hjO8qn2Cevdqny2HMwKUaA==", + "dev": true, + "license": "MIT" }, "node_modules/undici-types": { "version": "7.8.0", diff --git a/e2e/playwright/package.json b/e2e/playwright/package.json index 9cd1dd479..9475e6363 100644 --- a/e2e/playwright/package.json +++ b/e2e/playwright/package.json @@ -10,6 +10,6 @@ "devDependencies": { "@playwright/test": "^1.48.0", "@types/node": "^24.0.1", - "ts-retry": "^4.2.5" + "ts-retry": "^6.0.0" } } From 5be117d0e735076d305203138fc020a11b61fff4 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 17 Jun 2025 05:41:16 -0700 Subject: [PATCH 02/48] fix(openebs): ignore pre-release chart versions (#2322) --- pkg/helm/client.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 05c08f19d..f743f2a8d 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -185,6 +185,11 @@ func (h *HelmClient) AddRepo(repo *repo.Entry) error { } func (h *HelmClient) Latest(reponame, chart string) (string, error) { + stableConstraint, err := semver.NewConstraint(">0.0.0") // search only for stable versions + if err != nil { + return "", fmt.Errorf("create stable constraint: %w", err) + } + for _, repository := range h.repos { if repository.Name != reponame { continue @@ -207,14 +212,24 @@ func (h *HelmClient) Latest(reponame, chart string) (string, error) { versions, ok := repoidx.Entries[chart] if !ok { return "", fmt.Errorf("chart %s not found", chart) - } else if len(versions) == 0 { - return "", fmt.Errorf("chart %s has no versions", chart) } if len(versions) == 0 { return "", fmt.Errorf("chart %s has no versions", chart) } - return versions[0].Version, nil + + for _, version := range versions { + v, err := semver.NewVersion(version.Version) + if err != nil { + continue + } + + if stableConstraint.Check(v) { + return version.Version, nil + } + } + + return "", fmt.Errorf("no stable version found for chart %s", chart) } return "", fmt.Errorf("repository %s not found", reponame) } From 16cbc213aa3fdda519f27553b7a17eda51a1ffea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 13:44:27 +0100 Subject: [PATCH 03/48] build(deps-dev): bump globals from 15.11.0 to 16.2.0 in /web (#2270) Bumps [globals](https://github.com/sindresorhus/globals) from 15.11.0 to 16.2.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v15.11.0...v16.2.0) --- updated-dependencies: - dependency-name: globals dependency-version: 16.2.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 9 +++++---- web/package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 56799522b..60d2d4b40 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -29,7 +29,7 @@ "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^15.9.0", + "globals": "^16.2.0", "jsdom": "^26.1.0", "msw": "2.10.2", "postcss": "^8.5.5", @@ -4479,10 +4479,11 @@ } }, "node_modules/globals": { - "version": "15.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", - "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, diff --git a/web/package.json b/web/package.json index 4ef07cfa0..8916d9d83 100644 --- a/web/package.json +++ b/web/package.json @@ -33,7 +33,7 @@ "eslint": "^9.29.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^15.9.0", + "globals": "^16.2.0", "jsdom": "^26.1.0", "msw": "2.10.2", "postcss": "^8.5.5", From 15b0c80ea16aa57b7b1bb38c577d604dffd47d85 Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Tue, 17 Jun 2025 08:04:49 -0700 Subject: [PATCH 04/48] Stream infra install logs to the manager UI (#2306) --- api/docs/docs.go | 2 +- api/docs/swagger.json | 2 +- api/docs/swagger.yaml | 2 + api/internal/managers/infra/install.go | 55 ++-- api/internal/managers/infra/status.go | 10 +- api/internal/managers/infra/status_test.go | 32 +- api/internal/managers/infra/store.go | 35 ++ api/internal/managers/infra/store_mock.go | 15 + api/internal/managers/infra/store_test.go | 306 ++++++++++++++++++ api/internal/managers/infra/util.go | 18 ++ api/internal/managers/infra/util_test.go | 84 +++++ api/types/infra.go | 2 + .../components/wizard/InstallationStep.tsx | 13 +- .../wizard/installation/LogViewer.tsx | 8 +- .../wizard/tests/InstallationStep.test.tsx | 53 ++- web/src/test/setup.tsx | 5 + web/src/types/index.ts | 2 +- 17 files changed, 583 insertions(+), 61 deletions(-) create mode 100644 api/internal/managers/infra/store_test.go create mode 100644 api/internal/managers/infra/util_test.go diff --git a/api/docs/docs.go b/api/docs/docs.go index bd222dede..53dbd073b 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -6,7 +6,7 @@ import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, - "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"{{escape .Description}}","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"{{.Title}}","version":"{{.Version}}"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 68ba5f3bf..bc341b188 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1,5 +1,5 @@ { - "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"This is the API for the Embedded Cluster project.","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"Embedded Cluster API","version":"0.1"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index d3cdcdb6d..407f97e79 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -61,6 +61,8 @@ components: $ref: '#/components/schemas/types.InfraComponent' type: array uniqueItems: false + logs: + type: string status: $ref: '#/components/schemas/types.Status' type: object diff --git a/api/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index fba39c5f5..2cc8227f6 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -25,6 +25,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/support" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -144,7 +145,7 @@ func (m *infraManager) install(ctx context.Context, config *types.InstallationCo return fmt.Errorf("record installation: %w", err) } - if err := m.installAddOns(ctx, config, license, kcli, mcli, hcli); err != nil { + if err := m.installAddOns(ctx, license, kcli, mcli, hcli); err != nil { return fmt.Errorf("install addons: %w", err) } @@ -185,23 +186,27 @@ func (m *infraManager) installK0s(ctx context.Context, config *types.Installatio } }() - m.logger.Debug("creating k0s configuration file") + m.setStatusDesc(fmt.Sprintf("Installing %s", componentName)) + + logFn := m.logFn("k0s") + + logFn("creating k0s configuration file") k0sCfg, err := k0s.WriteK0sConfig(ctx, config.NetworkInterface, m.airgapBundle, config.PodCIDR, config.ServiceCIDR, m.endUserConfig, nil) if err != nil { return nil, fmt.Errorf("create config file: %w", err) } - m.logger.Debug("creating systemd unit files") + logFn("creating systemd unit files") if err := hostutils.CreateSystemdUnitFiles(ctx, m.logger, m.rc, false); err != nil { return nil, fmt.Errorf("create systemd unit files: %w", err) } - m.logger.Debug("installing k0s") + logFn("installing k0s") if err := k0s.Install(m.rc, config.NetworkInterface); err != nil { return nil, fmt.Errorf("install cluster: %w", err) } - m.logger.Debug("waiting for k0s to be ready") + logFn("waiting for k0s to be ready") if err := k0s.WaitForK0s(); err != nil { return nil, fmt.Errorf("wait for k0s: %w", err) } @@ -211,12 +216,14 @@ func (m *infraManager) installK0s(ctx context.Context, config *types.Installatio return nil, fmt.Errorf("create kube client: %w", err) } - m.logger.Debug("waiting for node to be ready") + m.setStatusDesc(fmt.Sprintf("Waiting for %s", componentName)) + + logFn("waiting for node to be ready") if err := m.waitForNode(ctx, kcli); err != nil { return nil, fmt.Errorf("wait for node: %w", err) } - m.logger.Debugf("adding insecure registry") + logFn("adding registry to containerd") registryIP, err := registry.GetRegistryClusterIP(config.ServiceCIDR) if err != nil { return nil, fmt.Errorf("get registry cluster IP: %w", err) @@ -229,11 +236,13 @@ func (m *infraManager) installK0s(ctx context.Context, config *types.Installatio } func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *kotsv1beta1.License) (*ecv1beta1.Installation, error) { + logFn := m.logFn("metadata") + // get the configured custom domains ecDomains := utils.GetDomains(m.releaseData) // record the installation - m.logger.Debugf("recording installation") + logFn("recording installation") in, err := kubeutils.RecordInstallation(ctx, kcli, kubeutils.RecordInstallationOptions{ IsAirgap: m.airgapBundle != "", License: license, @@ -246,6 +255,7 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien return nil, fmt.Errorf("record installation: %w", err) } + logFn("creating version metadata configmap") if err := ecmetadata.CreateVersionMetadataConfigmap(ctx, kcli); err != nil { return nil, fmt.Errorf("create version metadata configmap: %w", err) } @@ -253,14 +263,7 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien return in, nil } -func (m *infraManager) installAddOns( - ctx context.Context, - config *types.InstallationConfig, - license *kotsv1beta1.License, - kcli client.Client, - mcli metadata.Interface, - hcli helm.Client, -) error { +func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.License, kcli client.Client, mcli metadata.Interface, hcli helm.Client) error { // get the configured custom domains ecDomains := utils.GetDomains(m.releaseData) @@ -269,14 +272,25 @@ func (m *infraManager) installAddOns( go func() { for progress := range progressChan { + // capture progress in debug logs + m.logger.WithFields(logrus.Fields{"addon": progress.Name, "state": progress.Status.State, "description": progress.Status.Description}).Debugf("addon progress") + + // if in progress, update the overall status to reflect the current component + if progress.Status.State == types.StateRunning { + m.setStatusDesc(fmt.Sprintf("%s %s", progress.Status.Description, progress.Name)) + } + + // update the status for the current component if err := m.setComponentStatus(progress.Name, progress.Status.State, progress.Status.Description); err != nil { m.logger.Errorf("Failed to update addon status: %v", err) } } }() + logFn := m.logFn("addons") + addOns := addons.New( - addons.WithLogFunc(m.logger.Debugf), + addons.WithLogFunc(logFn), addons.WithKubernetesClient(kcli), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), @@ -284,7 +298,7 @@ func (m *infraManager) installAddOns( addons.WithProgressChannel(progressChan), ) - m.logger.Debugf("installing addons") + logFn("installing addons") if err := addOns.Install(ctx, addons.InstallOptions{ AdminConsolePwd: m.password, License: license, @@ -339,7 +353,10 @@ func (m *infraManager) installExtensions(ctx context.Context, hcli helm.Client) } }() - m.logger.Debugf("installing extensions") + m.setStatusDesc(fmt.Sprintf("Installing %s", componentName)) + + logFn := m.logFn("extensions") + logFn("installing extensions") if err := extensions.Install(ctx, hcli, nil); err != nil { return fmt.Errorf("install extensions: %w", err) } diff --git a/api/internal/managers/infra/status.go b/api/internal/managers/infra/status.go index 05db00980..c9a302e15 100644 --- a/api/internal/managers/infra/status.go +++ b/api/internal/managers/infra/status.go @@ -40,13 +40,11 @@ func (m *infraManager) setStatus(state types.State, description string) error { }) } +func (m *infraManager) setStatusDesc(description string) error { + return m.infraStore.SetStatusDesc(description) +} + func (m *infraManager) setComponentStatus(name string, state types.State, description string) error { - if state == types.StateRunning { - // update the overall status to reflect the current component - if err := m.setStatus(types.StateRunning, fmt.Sprintf("%s %s", description, name)); err != nil { - m.logger.Errorf("Failed to set status: %v", err) - } - } return m.infraStore.SetComponentStatus(name, &types.Status{ State: state, Description: description, diff --git a/api/internal/managers/infra/status_test.go b/api/internal/managers/infra/status_test.go index a6a4b7291..c744dc622 100644 --- a/api/internal/managers/infra/status_test.go +++ b/api/internal/managers/infra/status_test.go @@ -2,39 +2,25 @@ package infra import ( "testing" - "time" "github.com/stretchr/testify/assert" "github.com/replicatedhq/embedded-cluster/api/types" ) -func TestStatusSetAndGet(t *testing.T) { +func TestInfraWithLogs(t *testing.T) { manager := NewInfraManager() - // Test writing a status - statusToWrite := types.Status{ - State: types.StateRunning, - Description: "Installation in progress", - LastUpdated: time.Now().UTC().Truncate(time.Second), // Truncate to avoid time precision issues - } - - err := manager.SetStatus(statusToWrite) - assert.NoError(t, err) + // Add some logs through the internal logging mechanism + logFn := manager.logFn("test") + logFn("Test log message") + logFn("Another log message with arg: %s", "value") - // Test reading it back - readStatus, err := manager.GetStatus() + // Get the infra and verify logs are included + infra, err := manager.Get() assert.NoError(t, err) - assert.NotNil(t, readStatus) - - // Verify the values match - assert.Equal(t, statusToWrite.State, readStatus.State) - assert.Equal(t, statusToWrite.Description, readStatus.Description) - - // Compare time with string format to avoid precision issues - expectedTime := statusToWrite.LastUpdated.Format(time.RFC3339) - actualTime := readStatus.LastUpdated.Format(time.RFC3339) - assert.Equal(t, expectedTime, actualTime) + assert.Contains(t, infra.Logs, "[test] Test log message") + assert.Contains(t, infra.Logs, "[test] Another log message with arg: value") } func TestInstallDidRun(t *testing.T) { diff --git a/api/internal/managers/infra/store.go b/api/internal/managers/infra/store.go index fb4de7af9..078320d0c 100644 --- a/api/internal/managers/infra/store.go +++ b/api/internal/managers/infra/store.go @@ -8,13 +8,18 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) +const maxLogSize = 100 * 1024 // 100KB total log size + // Store provides methods for storing and retrieving infrastructure state type Store interface { Get() (*types.Infra, error) GetStatus() (*types.Status, error) SetStatus(status types.Status) error + SetStatusDesc(desc string) error RegisterComponent(name string) error SetComponentStatus(name string, status *types.Status) error + AddLogs(logs string) error + GetLogs() (string, error) } // memoryStore is an in-memory implementation of Store @@ -49,6 +54,18 @@ func (s *memoryStore) SetStatus(status types.Status) error { return nil } +func (s *memoryStore) SetStatusDesc(desc string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.infra.Status == nil { + return fmt.Errorf("status not set") + } + s.infra.Status.Description = desc + + return nil +} + func (s *memoryStore) RegisterComponent(name string) error { s.mu.Lock() defer s.mu.Unlock() @@ -78,3 +95,21 @@ func (s *memoryStore) SetComponentStatus(name string, status *types.Status) erro return fmt.Errorf("component %s not found", name) } + +func (s *memoryStore) AddLogs(logs string) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.infra.Logs += logs + "\n" + if len(s.infra.Logs) > maxLogSize { + s.infra.Logs = "... (truncated) " + s.infra.Logs[len(s.infra.Logs)-maxLogSize:] + } + + return nil +} + +func (s *memoryStore) GetLogs() (string, error) { + s.mu.RLock() + defer s.mu.RUnlock() + return s.infra.Logs, nil +} diff --git a/api/internal/managers/infra/store_mock.go b/api/internal/managers/infra/store_mock.go index 4c03580be..63f55a352 100644 --- a/api/internal/managers/infra/store_mock.go +++ b/api/internal/managers/infra/store_mock.go @@ -33,6 +33,11 @@ func (m *MockStore) SetStatus(status types.Status) error { return args.Error(0) } +func (m *MockStore) SetStatusDesc(desc string) error { + args := m.Called(desc) + return args.Error(0) +} + func (m *MockStore) RegisterComponent(name string) error { args := m.Called(name) return args.Error(0) @@ -42,3 +47,13 @@ func (m *MockStore) SetComponentStatus(name string, status *types.Status) error args := m.Called(name, status) return args.Error(0) } + +func (m *MockStore) AddLogs(logs string) error { + args := m.Called(logs) + return args.Error(0) +} + +func (m *MockStore) GetLogs() (string, error) { + args := m.Called() + return args.Get(0).(string), args.Error(1) +} diff --git a/api/internal/managers/infra/store_test.go b/api/internal/managers/infra/store_test.go new file mode 100644 index 000000000..6e1fd515a --- /dev/null +++ b/api/internal/managers/infra/store_test.go @@ -0,0 +1,306 @@ +package infra + +import ( + "strings" + "sync" + "testing" + "time" + + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newMemoryStore() Store { + infra := &types.Infra{ + Status: &types.Status{ + State: types.StatePending, + }, + Components: []types.InfraComponent{}, + Logs: "", + } + return NewMemoryStore(infra) +} + +func TestNewMemoryStore(t *testing.T) { + store := newMemoryStore() + + assert.NotNil(t, store) + infra, err := store.Get() + require.NoError(t, err) + assert.NotNil(t, infra) + assert.NotNil(t, infra.Status) + assert.Equal(t, types.StatePending, infra.Status.State) +} + +func TestMemoryStore_GetAndSetStatus(t *testing.T) { + store := newMemoryStore() + + // Test initial status + status, err := store.GetStatus() + require.NoError(t, err) + assert.Equal(t, types.StatePending, status.State) + + // Test setting status + newStatus := types.Status{ + State: types.StateRunning, + Description: "Installing components", + } + err = store.SetStatus(newStatus) + require.NoError(t, err) + + // Test getting updated status + status, err = store.GetStatus() + require.NoError(t, err) + assert.Equal(t, types.StateRunning, status.State) + assert.Equal(t, "Installing components", status.Description) +} + +func TestMemoryStore_SetStatusDesc(t *testing.T) { + store := newMemoryStore() + + // Test setting status description + err := store.SetStatusDesc("New description") + require.NoError(t, err) + + // Verify the description was updated + status, err := store.GetStatus() + require.NoError(t, err) + assert.Equal(t, "New description", status.Description) + assert.Equal(t, types.StatePending, status.State) // State should remain unchanged +} + +func TestMemoryStore_RegisterComponent(t *testing.T) { + store := newMemoryStore() + + // Test registering a component + err := store.RegisterComponent("k0s") + require.NoError(t, err) + + // Verify component was added + infra, err := store.Get() + require.NoError(t, err) + assert.Len(t, infra.Components, 1) + assert.Equal(t, "k0s", infra.Components[0].Name) + assert.Equal(t, types.StatePending, infra.Components[0].Status.State) + + // Test registering another component + err = store.RegisterComponent("addons") + require.NoError(t, err) + + infra, err = store.Get() + require.NoError(t, err) + assert.Len(t, infra.Components, 2) +} + +func TestMemoryStore_SetComponentStatus(t *testing.T) { + store := newMemoryStore() + + // Register a component first + err := store.RegisterComponent("k0s") + require.NoError(t, err) + + // Test setting component status + now := time.Now() + componentStatus := &types.Status{ + State: types.StateRunning, + Description: "Installing k0s", + LastUpdated: now, + } + err = store.SetComponentStatus("k0s", componentStatus) + require.NoError(t, err) + + // Verify the component status was updated + infra, err := store.Get() + require.NoError(t, err) + assert.Len(t, infra.Components, 1) + assert.Equal(t, types.StateRunning, infra.Components[0].Status.State) + assert.Equal(t, "Installing k0s", infra.Components[0].Status.Description) + assert.Equal(t, now, infra.Components[0].Status.LastUpdated) + + // Test setting status for non-existent component + err = store.SetComponentStatus("nonexistent", componentStatus) + assert.Error(t, err) + assert.Contains(t, err.Error(), "component nonexistent not found") +} + +func TestMemoryStore_AddLogs(t *testing.T) { + store := newMemoryStore() + + // Test adding logs + err := store.AddLogs("First log entry") + require.NoError(t, err) + + logs, err := store.GetLogs() + require.NoError(t, err) + assert.Equal(t, "First log entry\n", logs) + + // Test adding more logs + err = store.AddLogs("Second log entry") + require.NoError(t, err) + + logs, err = store.GetLogs() + require.NoError(t, err) + assert.Equal(t, "First log entry\nSecond log entry\n", logs) +} + +func TestMemoryStore_LogTruncation(t *testing.T) { + store := newMemoryStore() + + // Create a large log entry that exceeds maxLogSize + largeLog := strings.Repeat("a", maxLogSize+1000) + err := store.AddLogs(largeLog) + require.NoError(t, err) + + logs, err := store.GetLogs() + require.NoError(t, err) + + // Should be truncated and contain the truncation message + assert.True(t, len(logs) <= maxLogSize+50) // Allow some buffer for truncation message + assert.Contains(t, logs, "... (truncated)") +} + +func TestMemoryStore_GetLogs(t *testing.T) { + store := newMemoryStore() + + // Test getting logs when empty + logs, err := store.GetLogs() + require.NoError(t, err) + assert.Empty(t, logs) + + // Add some logs and test retrieval + err = store.AddLogs("Test log 1") + require.NoError(t, err) + err = store.AddLogs("Test log 2") + require.NoError(t, err) + + logs, err = store.GetLogs() + require.NoError(t, err) + assert.Equal(t, "Test log 1\nTest log 2\n", logs) +} + +func TestMemoryStore_Get(t *testing.T) { + store := newMemoryStore() + + // Test getting infra + infra, err := store.Get() + require.NoError(t, err) + assert.NotNil(t, infra) + assert.NotNil(t, infra.Status) + assert.Empty(t, infra.Components) + assert.Empty(t, infra.Logs) + + // Register a component and add logs + err = store.RegisterComponent("k0s") + require.NoError(t, err) + err = store.AddLogs("Test log") + require.NoError(t, err) + + // Test getting updated infra + infra, err = store.Get() + require.NoError(t, err) + assert.Len(t, infra.Components, 1) + assert.Equal(t, "Test log\n", infra.Logs) +} + +// Test concurrent access to ensure thread safety +func TestMemoryStore_ConcurrentAccess(t *testing.T) { + store := newMemoryStore() + var wg sync.WaitGroup + + // Register a component first + err := store.RegisterComponent("k0s") + require.NoError(t, err) + + numGoroutines := 10 + numOperations := 50 + + // Concurrent status operations + wg.Add(numGoroutines * 2) + for i := 0; i < numGoroutines; i++ { + // Concurrent status writes + go func(id int) { + defer wg.Done() + for j := 0; j < numOperations; j++ { + status := types.Status{ + State: types.StateRunning, + Description: "Concurrent test", + } + err := store.SetStatus(status) + assert.NoError(t, err) + } + }(i) + + // Concurrent status reads + go func(id int) { + defer wg.Done() + for j := 0; j < numOperations; j++ { + _, err := store.GetStatus() + assert.NoError(t, err) + } + }(i) + } + + // Concurrent log operations + wg.Add(numGoroutines * 2) + for i := 0; i < numGoroutines; i++ { + // Concurrent log writes + go func(id int) { + defer wg.Done() + for j := 0; j < numOperations; j++ { + err := store.AddLogs("Concurrent log") + assert.NoError(t, err) + } + }(i) + + // Concurrent log reads + go func(id int) { + defer wg.Done() + for j := 0; j < numOperations; j++ { + _, err := store.GetLogs() + assert.NoError(t, err) + } + }(i) + } + + // Concurrent component operations + wg.Add(numGoroutines * 2) + for i := 0; i < numGoroutines; i++ { + // Concurrent component status writes + go func(id int) { + defer wg.Done() + for j := 0; j < numOperations; j++ { + status := &types.Status{ + State: types.StateRunning, + Description: "Concurrent component test", + } + err := store.SetComponentStatus("k0s", status) + assert.NoError(t, err) + } + }(i) + + // Concurrent infra reads + go func(id int) { + defer wg.Done() + for j := 0; j < numOperations; j++ { + _, err := store.Get() + assert.NoError(t, err) + } + }(i) + } + + wg.Wait() +} + +func TestMemoryStore_StatusDescWithoutStatus(t *testing.T) { + store := &memoryStore{ + infra: &types.Infra{ + Status: nil, // No status set + }, + } + + // Test setting status description when status is nil + err := store.SetStatusDesc("Should fail") + assert.Error(t, err) + assert.Contains(t, err.Error(), "status not set") +} diff --git a/api/internal/managers/infra/util.go b/api/internal/managers/infra/util.go index 5e8405c5d..dc53eb60a 100644 --- a/api/internal/managers/infra/util.go +++ b/api/internal/managers/infra/util.go @@ -34,6 +34,7 @@ func (m *infraManager) getHelmClient() (helm.Client, error) { KubeConfig: m.rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, + LogFn: m.logFn("helm"), }) if err != nil { return nil, fmt.Errorf("create helm client: %w", err) @@ -54,3 +55,20 @@ func (m *infraManager) getEndUserConfigSpec() *ecv1beta1.ConfigSpec { } return &m.endUserConfig.Spec } + +// logFn creates a component-specific logging function that tags log entries with the +// component name and persists them to the infra store for client retrieval, +// as well as logs them to the structured logger. +func (m *infraManager) logFn(component string) func(format string, v ...interface{}) { + return func(format string, v ...interface{}) { + m.logger.WithField("component", component).Debugf(format, v...) + m.addLogs(component, format, v...) + } +} + +func (m *infraManager) addLogs(component string, format string, v ...interface{}) { + msg := fmt.Sprintf("[%s] %s", component, fmt.Sprintf(format, v...)) + if err := m.infraStore.AddLogs(msg); err != nil { + m.logger.WithField("error", err).Error("add log") + } +} diff --git a/api/internal/managers/infra/util_test.go b/api/internal/managers/infra/util_test.go new file mode 100644 index 000000000..02fa7bb3d --- /dev/null +++ b/api/internal/managers/infra/util_test.go @@ -0,0 +1,84 @@ +package infra + +import ( + "testing" + + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/stretchr/testify/assert" +) + +func TestInfraManager_logFn(t *testing.T) { + tests := []struct { + name string + component string + format string + args []interface{} + expected string + }{ + { + name: "simple log message", + component: "k0s", + format: "installing component", + args: []interface{}{}, + expected: "[k0s] installing component", + }, + { + name: "log message with arguments", + component: "addons", + format: "installing %s version %s", + args: []interface{}{"helm", "v3.12.0"}, + expected: "[addons] installing helm version v3.12.0", + }, + { + name: "log message with multiple arguments", + component: "helm", + format: "chart %s installed in namespace %s with values %v", + args: []interface{}{"test-chart", "default", map[string]string{"key": "value"}}, + expected: "[helm] chart test-chart installed in namespace default with values map[key:value]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock store + mockStore := &MockStore{} + mockStore.On("AddLogs", tt.expected).Return(nil) + + // Create a manager with the mock store + manager := &infraManager{ + infraStore: mockStore, + logger: logger.NewDiscardLogger(), + } + + // Call logFn and execute the returned function + logFunc := manager.logFn(tt.component) + logFunc(tt.format, tt.args...) + + // Verify the mock was called with expected arguments + mockStore.AssertExpectations(t) + }) + } +} + +func TestInfraManager_logFn_StoreError(t *testing.T) { + // Create a mock store that returns an error + mockStore := &MockStore{} + mockStore.On("AddLogs", "[test] error message").Return(assert.AnError) + + // Create a manager with the mock store + manager := &infraManager{ + infraStore: mockStore, + logger: logger.NewDiscardLogger(), + } + + // Call logFn and execute the returned function + logFunc := manager.logFn("test") + + // This should not panic even if AddLogs returns an error + assert.NotPanics(t, func() { + logFunc("error message") + }) + + // Verify the mock was called + mockStore.AssertExpectations(t) +} diff --git a/api/types/infra.go b/api/types/infra.go index 2e68d3d99..a5f41eadc 100644 --- a/api/types/infra.go +++ b/api/types/infra.go @@ -2,8 +2,10 @@ package types type Infra struct { Components []InfraComponent `json:"components"` + Logs string `json:"logs"` Status *Status `json:"status"` } + type InfraComponent struct { Name string `json:"name"` Status *Status `json:"status"` diff --git a/web/src/components/wizard/InstallationStep.tsx b/web/src/components/wizard/InstallationStep.tsx index 7bdee546e..88cef8057 100644 --- a/web/src/components/wizard/InstallationStep.tsx +++ b/web/src/components/wizard/InstallationStep.tsx @@ -7,7 +7,7 @@ import { useAuth } from "../../contexts/AuthContext"; import { InfraStatusResponse } from '../../types'; import { ChevronRight } from 'lucide-react'; import InstallationProgress from './installation/InstallationProgress'; -// import LogViewer from './installation/LogViewer'; +import LogViewer from './installation/LogViewer'; import StatusIndicator from './installation/StatusIndicator'; import ErrorMessage from './installation/ErrorMessage'; @@ -20,7 +20,7 @@ const InstallationStep: React.FC = ({ onNext }) => { const { prototypeSettings } = useConfig(); const [isInfraPolling, setIsInfraPolling] = useState(true); const [installComplete, setInstallComplete] = useState(false); - // const [showLogs, setShowLogs] = useState(false); + const [showLogs, setShowLogs] = useState(false); const themeColor = prototypeSettings.themeColor; // Query to poll infra status @@ -40,7 +40,7 @@ const InstallationStep: React.FC = ({ onNext }) => { return response.json() as Promise; }, enabled: isInfraPolling, - refetchInterval: 1000, + refetchInterval: 2000, }); // Handle infra status changes @@ -84,13 +84,12 @@ const InstallationStep: React.FC = ({ onNext }) => { ))} - {/* TODO (@salah): add support for installation logs */} - {/* setShowLogs(!showLogs)} - /> */} + /> {infraStatusError && } {infraStatusResponse?.status?.state === 'Failed' && } diff --git a/web/src/components/wizard/installation/LogViewer.tsx b/web/src/components/wizard/installation/LogViewer.tsx index 80186fcea..1c81ca8b7 100644 --- a/web/src/components/wizard/installation/LogViewer.tsx +++ b/web/src/components/wizard/installation/LogViewer.tsx @@ -23,10 +23,11 @@ const LogViewer: React.FC = ({ }, [logs, isExpanded]); return ( -
+
{isExpanded && ( -
+
{logs.map((log, index) => (
{log} diff --git a/web/src/components/wizard/tests/InstallationStep.test.tsx b/web/src/components/wizard/tests/InstallationStep.test.tsx index 0589e7668..bc1b54a18 100644 --- a/web/src/components/wizard/tests/InstallationStep.test.tsx +++ b/web/src/components/wizard/tests/InstallationStep.test.tsx @@ -1,6 +1,6 @@ import React from "react"; import { describe, it, expect, vi, beforeAll, afterEach, afterAll } from "vitest"; -import { screen, waitFor, within } from "@testing-library/react"; +import { screen, waitFor, within, fireEvent } from "@testing-library/react"; import { renderWithProviders } from "../../../test/setup.tsx"; import InstallationStep from "../InstallationStep.tsx"; import { MOCK_PROTOTYPE_SETTINGS } from "../../../test/testData.ts"; @@ -223,4 +223,55 @@ describe("InstallationStep", () => { // Next button should be disabled expect(screen.getByText("Next: Finish")).toBeDisabled(); }); + + it("verify log viewer", async () => { + const mockOnNext = vi.fn(); + server.use( + http.get("*/api/install/infra/status", () => { + return HttpResponse.json({ + status: { state: "Running", description: "Installing..." }, + components: [ + { name: "Runtime", status: { state: "Pending" } }, + { name: "Disaster Recovery", status: { state: "Pending" } } + ], + logs: "[k0s] creating k0s configuration file\n[k0s] creating systemd unit files" + }); + }) + ); + + renderWithProviders(, { + wrapperProps: { + authenticated: true, + preloadedState: { + prototypeSettings: MOCK_PROTOTYPE_SETTINGS, + config: mockConfig, + }, + }, + }); + + // Wait for log viewer to be available + await waitFor(() => { + expect(screen.getByTestId("log-viewer")).toBeInTheDocument(); + }); + + // Initially logs should be collapsed and not visible + expect(screen.queryByTestId("log-viewer-content")).not.toBeInTheDocument(); + + // Expand and verify logs + const toggleButton = screen.getByTestId("log-viewer-toggle"); + expect(toggleButton).toBeInTheDocument(); + fireEvent.click(toggleButton); + await waitFor(() => { + const logContent = screen.getByTestId("log-viewer-content"); + expect(logContent).toHaveTextContent("[k0s] creating k0s configuration file"); + expect(logContent).toHaveTextContent("[k0s] creating systemd unit files"); + }); + + // Click to collapse logs + expect(toggleButton).toBeInTheDocument(); + fireEvent.click(toggleButton); + await waitFor(() => { + expect(screen.queryByTestId("log-viewer-content")).not.toBeInTheDocument(); + }); + }); }); diff --git a/web/src/test/setup.tsx b/web/src/test/setup.tsx index 9e2adb2a0..103ffb09a 100644 --- a/web/src/test/setup.tsx +++ b/web/src/test/setup.tsx @@ -19,6 +19,11 @@ const mockLocalStorage = { }; Object.defineProperty(window, "localStorage", { value: mockLocalStorage }); +// Mock scrollIntoView for all tests (JSDOM does not implement it) +if (!window.HTMLElement.prototype.scrollIntoView) { + window.HTMLElement.prototype.scrollIntoView = vi.fn(); +} + interface PrototypeSettings { skipValidation: boolean; failPreflights: boolean; diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 36eb9c497..ea610af86 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -1,7 +1,7 @@ export interface InfraStatusResponse { components: InfraComponent[]; status: InfraStatus; - logs: string[]; + logs: string; } export interface InfraComponent { From 6c0d0110afbcb0282c1adaf0519d639b1e9d169c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:42:04 -0700 Subject: [PATCH 05/48] build(deps-dev): bump vitest from 0.32.4 to 3.2.3 in /web (#2271) Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.32.4 to 3.2.3. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.3/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.3 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 1584 +++++++---------------------------------- web/package.json | 2 +- 2 files changed, 254 insertions(+), 1332 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 60d2d4b40..c98c33e36 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -38,7 +38,7 @@ "typescript-eslint": "^8.34.0", "vite": "^5.4.2", "vite-plugin-static-copy": "^3.0.0", - "vitest": "^0.32.2" + "vitest": "^3.2.3" } }, "node_modules/@adobe/css-tools": { @@ -2228,20 +2228,13 @@ } }, "node_modules/@types/chai": { - "version": "4.3.20", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", - "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai-subset": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.6.tgz", - "integrity": "sha512-m8lERkkQj+uek18hXOZuec3W/fCRTrU4hrnXjH3qhHy96ytuPaPiWGgu7sJb7tZxZonO75vYAjCvpe/e4VUwRw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", "dev": true, "license": "MIT", - "peerDependencies": { - "@types/chai": "<5.2.0" + "dependencies": { + "@types/deep-eql": "*" } }, "node_modules/@types/cookie": { @@ -2251,6 +2244,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2711,177 +2711,120 @@ } }, "node_modules/@vitest/expect": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.4.tgz", - "integrity": "sha512-m7EPUqmGIwIeoU763N+ivkFjTzbaBn0n9evsTOcde03ugy2avPs3kZbYmw3DkcH1j5mxhMhdamJkLQ6dM1bk/A==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", + "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "0.32.4", - "@vitest/utils": "0.32.4", - "chai": "^4.3.7" + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.4.tgz", - "integrity": "sha512-cHOVCkiRazobgdKLnczmz2oaKK9GJOw6ZyRcaPdssO1ej+wzHVIkWiCiNacb3TTYPdzMddYkCgMjZ4r8C0JFCw==", + "node_modules/@vitest/mocker": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", + "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "0.32.4", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" + "@vitest/spy": "3.2.3", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@vitest/snapshot": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.4.tgz", - "integrity": "sha512-IRpyqn9t14uqsFlVI2d7DFMImGMs1Q9218of40bdQQgMePwVdmix33yMNnebXcTzDU5eiV3eUsoxxH5v0x/IQA==", + "node_modules/@vitest/pretty-format": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", + "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", "dev": true, "license": "MIT", "dependencies": { - "magic-string": "^0.30.0", - "pathe": "^1.1.1", - "pretty-format": "^29.5.0" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/@vitest/runner": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", + "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@vitest/utils": "3.2.3", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "node_modules/@vitest/snapshot": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", + "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@vitest/pretty-format": "3.2.3", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/spy": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.4.tgz", - "integrity": "sha512-oA7rCOqVOOpE6rEoXuCOADX7Lla1LIa4hljI2MSccbpec54q+oifhziZIJXxlE/CvI2E+ElhBHzVu0VEvJGQKQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", + "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^2.1.1" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.4.tgz", - "integrity": "sha512-Gwnl8dhd1uJ+HXrYyV0eRqfmk9ek1ASE/LWfTCuWMw+d07ogHqp4hEAV28NiecimK6UY9DpSEPh+pXBA5gtTBg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", + "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", "dev": true, "license": "MIT", "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" + "@vitest/pretty-format": "3.2.3", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2905,19 +2848,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -3042,13 +2972,13 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/autoprefixer": { @@ -3276,35 +3206,30 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -3496,13 +3421,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3587,10 +3505,11 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3611,14 +3530,11 @@ "license": "MIT" }, "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -3821,6 +3737,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4129,6 +4052,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -4155,6 +4088,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4377,16 +4320,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5718,19 +5651,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/local-pkg": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", - "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5771,14 +5691,11 @@ } }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -5889,26 +5806,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6260,20 +6157,20 @@ "license": "MIT" }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/picocolors": { @@ -6309,25 +6206,6 @@ "node": ">= 6" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7227,18 +7105,25 @@ } }, "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.10.0" + "js-tokens": "^9.0.1" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -7340,6 +7225,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -7386,9 +7278,19 @@ } }, "node_modules/tinypool": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", - "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -7396,9 +7298,9 @@ } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -7505,16 +7407,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -7565,13 +7457,6 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici-types": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", @@ -7706,1097 +7591,134 @@ } }, "node_modules/vite-node": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.4.tgz", - "integrity": "sha512-L2gIw+dCxO0LK14QnUMoqSYpa9XRGnTTTDjW2h19Mr+GR0EFj4vx52W41gFXfMLqpA00eK4ZjOVYo1Xk//LFEw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", + "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "mlly": "^1.4.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=v14.18.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/vite-node/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], + "node_modules/vite-plugin-static-copy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.0.0.tgz", + "integrity": "sha512-Uki9pPUQ4ZnoMEdIFabvoh9h6Bh9Q1m3iF7BrZvoiF30reREpJh2gZb4jOnW1/uYFzyRiLCmFSkM+8hwiq1vWQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "chokidar": "^3.5.3", + "fs-extra": "^11.3.0", + "p-map": "^7.0.3", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.13" + }, "engines": { - "node": ">=12" + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" } }, - "node_modules/vite-node/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/vitest": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", + "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.3", + "@vitest/mocker": "3.2.3", + "@vitest/pretty-format": "^3.2.3", + "@vitest/runner": "3.2.3", + "@vitest/snapshot": "3.2.3", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.0", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.3", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite-node/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/vite-node/node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/vite-node/node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-plugin-static-copy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.0.0.tgz", - "integrity": "sha512-Uki9pPUQ4ZnoMEdIFabvoh9h6Bh9Q1m3iF7BrZvoiF30reREpJh2gZb4jOnW1/uYFzyRiLCmFSkM+8hwiq1vWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.3", - "fs-extra": "^11.3.0", - "p-map": "^7.0.3", - "picocolors": "^1.1.1", - "tinyglobby": "^0.2.13" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0" - } - }, - "node_modules/vitest": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.4.tgz", - "integrity": "sha512-3czFm8RnrsWwIzVDu/Ca48Y/M+qh3vOnF16czJm98Q/AN1y3B6PBsyV8Re91Ty5s7txKNjEhpgtGPcfdbh2MZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^4.3.5", - "@types/chai-subset": "^1.3.3", - "@types/node": "*", - "@vitest/expect": "0.32.4", - "@vitest/runner": "0.32.4", - "@vitest/snapshot": "0.32.4", - "@vitest/spy": "0.32.4", - "@vitest/utils": "0.32.4", - "acorn": "^8.9.0", - "acorn-walk": "^8.2.0", - "cac": "^6.7.14", - "chai": "^4.3.7", - "debug": "^4.3.4", - "local-pkg": "^0.4.3", - "magic-string": "^0.30.0", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.3.3", - "strip-literal": "^1.0.1", - "tinybench": "^2.5.0", - "tinypool": "^0.5.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.32.4", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": ">=v14.18.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@vitest/browser": "*", - "@vitest/ui": "*", - "happy-dom": "*", - "jsdom": "*", - "playwright": "*", - "safaridriver": "*", - "webdriverio": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "playwright": { - "optional": true - }, - "safaridriver": { - "optional": true - }, - "webdriverio": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/vitest/node_modules/rollup": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", - "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.3", + "@vitest/ui": "3.2.3", + "happy-dom": "*", + "jsdom": "*" }, "peerDependenciesMeta": { - "@types/node": { + "@edge-runtime/vm": { "optional": true }, - "less": { + "@types/debug": { "optional": true }, - "lightningcss": { + "@types/node": { "optional": true }, - "sass": { + "@vitest/browser": { "optional": true }, - "stylus": { + "@vitest/ui": { "optional": true }, - "sugarss": { + "happy-dom": { "optional": true }, - "terser": { + "jsdom": { "optional": true } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/web/package.json b/web/package.json index 8916d9d83..8eb37ef57 100644 --- a/web/package.json +++ b/web/package.json @@ -42,6 +42,6 @@ "typescript-eslint": "^8.34.0", "vite": "^5.4.2", "vite-plugin-static-copy": "^3.0.0", - "vitest": "^0.32.2" + "vitest": "^3.2.3" } } From 81aa71f45cbd71225aa1304467bcab7bedbe4df7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:42:26 -0700 Subject: [PATCH 06/48] build(deps-dev): bump @faker-js/faker from 8.4.1 to 9.8.0 in /web (#2275) Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 8.4.1 to 9.8.0. - [Release notes](https://github.com/faker-js/faker/releases) - [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md) - [Commits](https://github.com/faker-js/faker/compare/v8.4.1...v9.8.0) --- updated-dependencies: - dependency-name: "@faker-js/faker" dependency-version: 9.8.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 12 ++++++------ web/package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index c98c33e36..2d252fc4a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@eslint/js": "^9.29.0", - "@faker-js/faker": "^8.0.2", + "@faker-js/faker": "^9.8.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", @@ -1054,9 +1054,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz", + "integrity": "sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==", "dev": true, "funding": [ { @@ -1066,8 +1066,8 @@ ], "license": "MIT", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0", - "npm": ">=6.14.13" + "node": ">=18.0.0", + "npm": ">=9.0.0" } }, "node_modules/@humanfs/core": { diff --git a/web/package.json b/web/package.json index 8eb37ef57..3a63b5c6e 100644 --- a/web/package.json +++ b/web/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@eslint/js": "^9.29.0", - "@faker-js/faker": "^8.0.2", + "@faker-js/faker": "^9.8.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", From 31478489cec2c6d71d530ee2acb9e166c9f6ce47 Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:00:03 -0400 Subject: [PATCH 07/48] feat(ui): hide validation from install wizard navigation (#2327) * consolidate validation into the setup step and update tests * remove uneeded goToPreviousStep function * run installation start after setup and validation * revert accidental changes to operator and e2e files * move validation back to its own component * minor changes to validation step * Revert unintended changes to helm charts and operator metadata * add newline to validation step * fex web unit test * revert to using validation as a separate step but hide it from navbar and only show setup step to user * complete reversion of setup and validation step * add test for step navigation * remove newline * Update web/src/components/wizard/StepNavigation.tsx Co-authored-by: Salah Al Saleh * fix variable naming * fix lint error --------- Co-authored-by: Salah Al Saleh --- web/src/components/wizard/StepNavigation.tsx | 16 +- .../wizard/tests/StepNavigation.test.tsx | 156 ++++++++++++++++++ 2 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 web/src/components/wizard/tests/StepNavigation.test.tsx diff --git a/web/src/components/wizard/StepNavigation.tsx b/web/src/components/wizard/StepNavigation.tsx index 635a7b7aa..7fd8235f1 100644 --- a/web/src/components/wizard/StepNavigation.tsx +++ b/web/src/components/wizard/StepNavigation.tsx @@ -13,20 +13,26 @@ const StepNavigation: React.FC = ({ currentStep }) => { const { prototypeSettings } = useConfig(); const themeColor = prototypeSettings.themeColor; + // Navigation steps (validation is hidden but still part of the wizard flow) const steps = [ { id: 'welcome', name: 'Welcome', icon: ClipboardList }, { id: 'setup', name: 'Setup', icon: Settings }, - { id: 'validation', name: 'Validation', icon: CheckCircle }, { id: 'installation', name: mode === 'upgrade' ? 'Upgrade' : 'Installation', icon: Download }, { id: 'completion', name: 'Completion', icon: CheckCircle }, ]; + // All wizard steps for progress calculation + const allSteps: WizardStep[] = ['welcome', 'setup', 'validation', 'installation', 'completion']; + const getStepStatus = (step: { id: string }) => { - const stepIndex = steps.findIndex((s) => s.id === step.id); - const currentIndex = steps.findIndex((s) => s.id === currentStep); + const stepIndex = allSteps.indexOf(step.id as WizardStep); + const currentStepIndex = allSteps.indexOf(currentStep); + + // Treat validation as part of setup for navigation purposes + const adjustedCurrentIndex = currentStep === 'validation' ? allSteps.indexOf('setup') : currentStepIndex; - if (stepIndex < currentIndex) return 'complete'; - if (stepIndex === currentIndex) return 'current'; + if (stepIndex < adjustedCurrentIndex) return 'complete'; + if (stepIndex === adjustedCurrentIndex || (step.id === 'setup' && currentStep === 'validation')) return 'current'; return 'upcoming'; }; diff --git a/web/src/components/wizard/tests/StepNavigation.test.tsx b/web/src/components/wizard/tests/StepNavigation.test.tsx new file mode 100644 index 000000000..1a55837a5 --- /dev/null +++ b/web/src/components/wizard/tests/StepNavigation.test.tsx @@ -0,0 +1,156 @@ +import React from "react"; +import { describe, it, expect } from "vitest"; +import { screen } from "@testing-library/react"; +import { renderWithProviders } from "../../../test/setup.tsx"; +import StepNavigation from "../StepNavigation.tsx"; +import { WizardStep } from "../../../types/index.ts"; + +describe("StepNavigation", () => { + const defaultPreloadedState = { + // Use generic settings instead of prototype-specific references + prototypeSettings: { + themeColor: "#316DE6", + }, + }; + + it("renders all navigation steps except validation", () => { + renderWithProviders(, { + wrapperProps: { + authenticated: true, + preloadedState: defaultPreloadedState, + }, + }); + + // Should show 4 steps (welcome, setup, installation, completion) + expect(screen.getByText("Welcome")).toBeInTheDocument(); + expect(screen.getByText("Setup")).toBeInTheDocument(); + expect(screen.getByText("Installation")).toBeInTheDocument(); + expect(screen.getByText("Completion")).toBeInTheDocument(); + + // Should NOT show validation step + expect(screen.queryByText("Validation")).not.toBeInTheDocument(); + }); + + it("shows 'current' status for the current step", () => { + renderWithProviders(, { + wrapperProps: { + authenticated: true, + preloadedState: defaultPreloadedState, + }, + }); + + const setupStep = screen.getByText("Setup").closest("div"); + expect(setupStep).toHaveStyle({ + border: "1px solid #316DE6", + }); + }); + + it("treats validation step as part of setup for navigation", () => { + renderWithProviders(, { + wrapperProps: { + authenticated: true, + preloadedState: defaultPreloadedState, + }, + }); + + // When currentStep is 'validation', setup should show as current + const setupStep = screen.getByText("Setup").closest("div"); + expect(setupStep).toHaveStyle({ + border: "1px solid #316DE6", + }); + + // Welcome should be complete + const welcomeStep = screen.getByText("Welcome").closest("div"); + expect(welcomeStep).toHaveStyle({ + backgroundColor: "#316DE61A", + color: "#316DE6", + }); + }); + + it("shows upcoming steps with default styling", () => { + renderWithProviders(, { + wrapperProps: { + authenticated: true, + preloadedState: defaultPreloadedState, + }, + }); + + // Setup, Installation, and Completion should be upcoming + const installationStep = screen.getByText("Installation").closest("div"); + const completionStep = screen.getByText("Completion").closest("div"); + + expect(installationStep).toHaveStyle({ + backgroundColor: "rgb(243 244 246)", // gray background + color: "rgb(107 114 128)", // gray text + }); + expect(completionStep).toHaveStyle({ + backgroundColor: "rgb(243 244 246)", + color: "rgb(107 114 128)", + }); + }); + + it("renders correct icons for each step", () => { + renderWithProviders(, { + wrapperProps: { + authenticated: true, + preloadedState: defaultPreloadedState, + }, + }); + + // Check that all step icons are rendered + const stepElements = screen.getAllByRole("listitem"); + expect(stepElements).toHaveLength(4); // welcome, setup, installation, completion + + // Each step should have an icon (svg element) + stepElements.forEach((step) => { + const icon = step.querySelector("svg"); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass("w-5", "h-5"); + }); + }); + + it("maintains proper step progression logic", () => { + // Test different current steps and their expected status + const testCases = [ + { currentStep: "welcome", setupStatus: "upcoming", installStatus: "upcoming" }, + { currentStep: "setup", setupStatus: "current", installStatus: "upcoming" }, + { currentStep: "validation", setupStatus: "current", installStatus: "upcoming" }, + { currentStep: "installation", setupStatus: "complete", installStatus: "current" }, + ]; + + testCases.forEach(({ currentStep, setupStatus, installStatus }) => { + const { unmount } = renderWithProviders( + , + { + wrapperProps: { + authenticated: true, + preloadedState: defaultPreloadedState, + }, + } + ); + + const setupStep = screen.getByText("Setup").closest("div"); + const installStep = screen.getByText("Installation").closest("div"); + + if (setupStatus === "current") { + expect(setupStep).toHaveStyle({ border: "1px solid #316DE6" }); + } else if (setupStatus === "complete") { + expect(setupStep).toHaveStyle({ + backgroundColor: "#316DE61A", + color: "#316DE6" + }); + } + + if (installStatus === "current") { + expect(installStep).toHaveStyle({ border: "1px solid #316DE6" }); + } else if (installStatus === "upcoming") { + expect(installStep).toHaveStyle({ + backgroundColor: "rgb(243 244 246)", + color: "rgb(107 114 128)" + }); + } + + unmount(); // Clean up for next iteration + }); + }); +}); From ae23f8e82159a2f0904710f8e29608fdee35555f Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 17 Jun 2025 14:47:38 -0700 Subject: [PATCH 08/48] chore: runtime config as argument (#2329) * chore: use runtime config * f * duplicated argument * f --- api/client/install.go | 4 +- api/controllers/install/controller.go | 3 - api/controllers/install/controller_test.go | 106 ++++++++---------- api/controllers/install/hostpreflight.go | 4 +- api/controllers/install/infra.go | 8 +- api/controllers/install/installation.go | 6 +- api/integration/hostpreflights_test.go | 2 - api/integration/install_test.go | 1 - api/internal/managers/infra/install.go | 46 ++++---- api/internal/managers/infra/manager.go | 13 +-- api/internal/managers/infra/manager_mock.go | 5 +- api/internal/managers/infra/util.go | 7 +- api/internal/managers/installation/config.go | 24 ++-- .../managers/installation/config_test.go | 7 +- api/internal/managers/installation/manager.go | 15 +-- .../managers/installation/manager_mock.go | 9 +- .../managers/preflight/hostpreflight.go | 41 +++---- .../managers/preflight/hostpreflight_test.go | 6 +- api/internal/managers/preflight/manager.go | 15 +-- .../managers/preflight/manager_mock.go | 9 +- cmd/installer/cli/install.go | 2 +- pkg-new/k0s/k0s.go | 4 +- pkg/dryrun/k0s.go | 2 +- 23 files changed, 138 insertions(+), 201 deletions(-) diff --git a/api/client/install.go b/api/client/install.go index 1c368d555..825f030ef 100644 --- a/api/client/install.go +++ b/api/client/install.go @@ -35,8 +35,8 @@ func (c *client) GetInstallationConfig() (*types.InstallationConfig, error) { return &config, nil } -func (c *client) ConfigureInstallation(cfg *types.InstallationConfig) (*types.Status, error) { - b, err := json.Marshal(cfg) +func (c *client) ConfigureInstallation(config *types.InstallationConfig) (*types.Status, error) { + b, err := json.Marshal(config) if err != nil { return nil, err } diff --git a/api/controllers/install/controller.go b/api/controllers/install/controller.go index 7e99a87f5..34736eff0 100644 --- a/api/controllers/install/controller.go +++ b/api/controllers/install/controller.go @@ -173,7 +173,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, if controller.installationManager == nil { controller.installationManager = installation.NewInstallationManager( - installation.WithRuntimeConfig(controller.rc), installation.WithLogger(controller.logger), installation.WithInstallation(controller.install.Steps.Installation), installation.WithLicenseFile(controller.licenseFile), @@ -185,7 +184,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, if controller.hostPreflightManager == nil { controller.hostPreflightManager = preflight.NewHostPreflightManager( - preflight.WithRuntimeConfig(controller.rc), preflight.WithLogger(controller.logger), preflight.WithMetricsReporter(controller.metricsReporter), preflight.WithHostPreflightStore(preflight.NewMemoryStore(controller.install.Steps.HostPreflight)), @@ -195,7 +193,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, if controller.infraManager == nil { controller.infraManager = infra.NewInfraManager( - infra.WithRuntimeConfig(controller.rc), infra.WithLogger(controller.logger), infra.WithInfra(controller.install.Steps.Infra), infra.WithPassword(controller.password), diff --git a/api/controllers/install/controller_test.go b/api/controllers/install/controller_test.go index b997e424f..1d5f592d9 100644 --- a/api/controllers/install/controller_test.go +++ b/api/controllers/install/controller_test.go @@ -37,7 +37,7 @@ func TestGetInstallationConfig(t *testing.T) { mock.InOrder( m.On("GetConfig").Return(config, nil), m.On("SetConfigDefaults", config).Return(nil), - m.On("ValidateConfig", config).Return(nil), + m.On("ValidateConfig", config, 9001).Return(nil), ) }, expectedErr: false, @@ -73,7 +73,7 @@ func TestGetInstallationConfig(t *testing.T) { mock.InOrder( m.On("GetConfig").Return(config, nil), m.On("SetConfigDefaults", config).Return(nil), - m.On("ValidateConfig", config).Return(errors.New("validation error")), + m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")), ) }, expectedErr: true, @@ -85,6 +85,7 @@ func TestGetInstallationConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{})) rc.SetDataDir(t.TempDir()) + rc.SetManagerPort(9001) mockManager := &installation.MockInstallationManager{} tt.setupMock(mockManager) @@ -114,7 +115,7 @@ func TestConfigureInstallation(t *testing.T) { tests := []struct { name string config *types.InstallationConfig - setupMock func(*installation.MockInstallationManager, *types.InstallationConfig) + setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, *types.InstallationConfig) expectedErr bool }{ { @@ -123,11 +124,11 @@ func TestConfigureInstallation(t *testing.T) { LocalArtifactMirrorPort: 9000, DataDirectory: t.TempDir(), }, - setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { mock.InOrder( - m.On("ValidateConfig", config).Return(nil), + m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", *config).Return(nil), - m.On("ConfigureHost", t.Context()).Return(nil), + m.On("ConfigureHost", t.Context(), rc).Return(nil), ) }, expectedErr: false, @@ -135,17 +136,17 @@ func TestConfigureInstallation(t *testing.T) { { name: "validate error", config: &types.InstallationConfig{}, - setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { - m.On("ValidateConfig", config).Return(errors.New("validation error")) + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { + m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")) }, expectedErr: true, }, { name: "set config error", config: &types.InstallationConfig{}, - setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { mock.InOrder( - m.On("ValidateConfig", config).Return(nil), + m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", *config).Return(errors.New("set config error")), ) }, @@ -157,16 +158,16 @@ func TestConfigureInstallation(t *testing.T) { GlobalCIDR: "10.0.0.0/16", DataDirectory: t.TempDir(), }, - setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { // Create a copy with expected CIDR values after computation configWithCIDRs := *config configWithCIDRs.PodCIDR = "10.0.0.0/17" configWithCIDRs.ServiceCIDR = "10.0.128.0/17" mock.InOrder( - m.On("ValidateConfig", config).Return(nil), + m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", configWithCIDRs).Return(nil), - m.On("ConfigureHost", t.Context()).Return(nil), + m.On("ConfigureHost", t.Context(), rc).Return(nil), ) }, expectedErr: false, @@ -177,13 +178,14 @@ func TestConfigureInstallation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{})) rc.SetDataDir(t.TempDir()) + rc.SetManagerPort(9001) mockManager := &installation.MockInstallationManager{} // Create a copy of the config to avoid modifying the original configCopy := *tt.config - tt.setupMock(mockManager, &configCopy) + tt.setupMock(mockManager, rc, &configCopy) controller, err := NewInstallController( WithRuntimeConfig(rc), @@ -273,15 +275,15 @@ func TestRunHostPreflights(t *testing.T) { tests := []struct { name string - setupMocks func(*preflight.MockHostPreflightManager) + setupMocks func(*preflight.MockHostPreflightManager, runtimeconfig.RuntimeConfig) expectedErr bool }{ { name: "successful run preflights", - setupMocks: func(pm *preflight.MockHostPreflightManager) { + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { mock.InOrder( - pm.On("PrepareHostPreflights", t.Context(), mock.Anything).Return(expectedHPF, nil), - pm.On("RunHostPreflights", t.Context(), mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", t.Context(), rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { return expectedHPF == opts.HostPreflightSpec })).Return(nil), ) @@ -290,19 +292,19 @@ func TestRunHostPreflights(t *testing.T) { }, { name: "prepare preflights error", - setupMocks: func(pm *preflight.MockHostPreflightManager) { + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { mock.InOrder( - pm.On("PrepareHostPreflights", t.Context(), mock.Anything).Return(nil, errors.New("prepare error")), + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(nil, errors.New("prepare error")), ) }, expectedErr: true, }, { name: "run preflights error", - setupMocks: func(pm *preflight.MockHostPreflightManager) { + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { mock.InOrder( - pm.On("PrepareHostPreflights", t.Context(), mock.Anything).Return(expectedHPF, nil), - pm.On("RunHostPreflights", t.Context(), mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", t.Context(), rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { return expectedHPF == opts.HostPreflightSpec })).Return(errors.New("run preflights error")), ) @@ -313,9 +315,6 @@ func TestRunHostPreflights(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockPreflightManager := &preflight.MockHostPreflightManager{} - tt.setupMocks(mockPreflightManager) - rc := runtimeconfig.New(nil) rc.SetDataDir(t.TempDir()) rc.SetProxySpec(&ecv1beta1.ProxySpec{ @@ -325,6 +324,9 @@ func TestRunHostPreflights(t *testing.T) { NoProxy: "no-proxy.com", }) + mockPreflightManager := &preflight.MockHostPreflightManager{} + tt.setupMocks(mockPreflightManager, rc) + controller, err := NewInstallController( WithRuntimeConfig(rc), WithHostPreflightManager(mockPreflightManager), @@ -566,29 +568,25 @@ func TestGetInstallationStatus(t *testing.T) { func TestSetupInfra(t *testing.T) { tests := []struct { name string - setupMocks func(*preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter) + setupMocks func(runtimeconfig.RuntimeConfig, *preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter) expectedErr bool }{ { name: "successful setup with passed preflights", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { preflightStatus := &types.Status{ State: types.StateSucceeded, } - config := &types.InstallationConfig{ - AdminConsolePort: 8000, - } mock.InOrder( pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), - im.On("GetConfig").Return(config, nil), - fm.On("Install", t.Context(), config).Return(nil), + fm.On("Install", t.Context(), rc).Return(nil), ) }, expectedErr: false, }, { name: "successful setup with failed preflights", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { preflightStatus := &types.Status{ State: types.StateFailed, } @@ -600,29 +598,25 @@ func TestSetupInfra(t *testing.T) { }, }, } - config := &types.InstallationConfig{ - AdminConsolePort: 8000, - } mock.InOrder( pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), pm.On("GetHostPreflightOutput", t.Context()).Return(preflightOutput, nil), r.On("ReportPreflightsFailed", t.Context(), preflightOutput).Return(nil), - im.On("GetConfig").Return(config, nil), - fm.On("Install", t.Context(), config).Return(nil), + fm.On("Install", t.Context(), rc).Return(nil), ) }, expectedErr: false, }, { name: "preflight status error", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { pm.On("GetHostPreflightStatus", t.Context()).Return(nil, errors.New("get preflight status error")) }, expectedErr: true, }, { name: "preflight not completed", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { preflightStatus := &types.Status{ State: types.StateRunning, } @@ -632,7 +626,7 @@ func TestSetupInfra(t *testing.T) { }, { name: "preflight output error", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { preflightStatus := &types.Status{ State: types.StateFailed, } @@ -643,32 +637,15 @@ func TestSetupInfra(t *testing.T) { }, expectedErr: true, }, - { - name: "get config error", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := &types.Status{ - State: types.StateSucceeded, - } - mock.InOrder( - pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), - im.On("GetConfig").Return(nil, errors.New("get config error")), - ) - }, - expectedErr: true, - }, { name: "install infra error", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { preflightStatus := &types.Status{ State: types.StateSucceeded, } - config := &types.InstallationConfig{ - AdminConsolePort: 8000, - } mock.InOrder( pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), - im.On("GetConfig").Return(config, nil), - fm.On("Install", t.Context(), config).Return(errors.New("install error")), + fm.On("Install", t.Context(), rc).Return(errors.New("install error")), ) }, expectedErr: true, @@ -677,13 +654,18 @@ func TestSetupInfra(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + rc := runtimeconfig.New(nil) + rc.SetDataDir(t.TempDir()) + rc.SetManagerPort(9001) + mockPreflightManager := &preflight.MockHostPreflightManager{} mockInstallationManager := &installation.MockInstallationManager{} mockInfraManager := &infra.MockInfraManager{} mockMetricsReporter := &metrics.MockReporter{} - tt.setupMocks(mockPreflightManager, mockInstallationManager, mockInfraManager, mockMetricsReporter) + tt.setupMocks(rc, mockPreflightManager, mockInstallationManager, mockInfraManager, mockMetricsReporter) controller, err := NewInstallController( + WithRuntimeConfig(rc), WithHostPreflightManager(mockPreflightManager), WithInstallationManager(mockInstallationManager), WithInfraManager(mockInfraManager), diff --git a/api/controllers/install/hostpreflight.go b/api/controllers/install/hostpreflight.go index 997b75cf3..835cae777 100644 --- a/api/controllers/install/hostpreflight.go +++ b/api/controllers/install/hostpreflight.go @@ -15,7 +15,7 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP ecDomains := utils.GetDomains(c.releaseData) // Prepare host preflights - hpf, err := c.hostPreflightManager.PrepareHostPreflights(ctx, preflight.PrepareHostPreflightOptions{ + hpf, err := c.hostPreflightManager.PrepareHostPreflights(ctx, c.rc, preflight.PrepareHostPreflightOptions{ ReplicatedAppURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), ProxyRegistryURL: netutils.MaybeAddHTTPS(ecDomains.ProxyRegistryDomain), HostPreflightSpec: c.releaseData.HostPreflights, @@ -28,7 +28,7 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP } // Run host preflights - return c.hostPreflightManager.RunHostPreflights(ctx, preflight.RunHostPreflightOptions{ + return c.hostPreflightManager.RunHostPreflights(ctx, c.rc, preflight.RunHostPreflightOptions{ HostPreflightSpec: hpf, }) } diff --git a/api/controllers/install/infra.go b/api/controllers/install/infra.go index f0c4e2e59..92bc09ae7 100644 --- a/api/controllers/install/infra.go +++ b/api/controllers/install/infra.go @@ -27,13 +27,7 @@ func (c *InstallController) SetupInfra(ctx context.Context) error { } } - // Get current installation config - config, err := c.installationManager.GetConfig() - if err != nil { - return fmt.Errorf("failed to read installation config: %w", err) - } - - if err := c.infraManager.Install(ctx, config); err != nil { + if err := c.infraManager.Install(ctx, c.rc); err != nil { return fmt.Errorf("install infra: %w", err) } diff --git a/api/controllers/install/installation.go b/api/controllers/install/installation.go index b1a122a70..8e950bf66 100644 --- a/api/controllers/install/installation.go +++ b/api/controllers/install/installation.go @@ -24,7 +24,7 @@ func (c *InstallController) GetInstallationConfig(ctx context.Context) (*types.I return nil, fmt.Errorf("set defaults: %w", err) } - if err := c.installationManager.ValidateConfig(config); err != nil { + if err := c.installationManager.ValidateConfig(config, c.rc.ManagerPort()); err != nil { return nil, fmt.Errorf("validate: %w", err) } @@ -32,7 +32,7 @@ func (c *InstallController) GetInstallationConfig(ctx context.Context) (*types.I } func (c *InstallController) ConfigureInstallation(ctx context.Context, config *types.InstallationConfig) error { - if err := c.installationManager.ValidateConfig(config); err != nil { + if err := c.installationManager.ValidateConfig(config, c.rc.ManagerPort()); err != nil { return fmt.Errorf("validate: %w", err) } @@ -70,7 +70,7 @@ func (c *InstallController) ConfigureInstallation(ctx context.Context, config *t return fmt.Errorf("set env vars: %w", err) } - if err := c.installationManager.ConfigureHost(ctx); err != nil { + if err := c.installationManager.ConfigureHost(ctx, c.rc); err != nil { return fmt.Errorf("configure: %w", err) } diff --git a/api/integration/hostpreflights_test.go b/api/integration/hostpreflights_test.go index 00e0d7c57..8b001e3d1 100644 --- a/api/integration/hostpreflights_test.go +++ b/api/integration/hostpreflights_test.go @@ -176,13 +176,11 @@ func TestPostRunHostPreflights(t *testing.T) { // Create a host preflights manager with the mock runner pfManager := preflight.NewHostPreflightManager( - preflight.WithRuntimeConfig(rc), preflight.WithPreflightRunner(runner), ) // Create an installation manager iManager := installation.NewInstallationManager( - installation.WithRuntimeConfig(rc), installation.WithInstallationStore(installation.NewMemoryStore(inst)), ) diff --git a/api/integration/install_test.go b/api/integration/install_test.go index 3adc99c21..5280d4176 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -933,7 +933,6 @@ func TestInstallWithAPIClient(t *testing.T) { // Create a config manager installationManager := installation.NewInstallationManager( - installation.WithRuntimeConfig(rc), installation.WithHostUtils(mockHostUtils), ) diff --git a/api/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index 2cc8227f6..2cd7e13bf 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -39,7 +39,7 @@ func AlreadyInstalledError() error { ) } -func (m *infraManager) Install(ctx context.Context, config *types.InstallationConfig) (finalErr error) { +func (m *infraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConfig) (finalErr error) { m.mu.Lock() defer m.mu.Unlock() @@ -59,16 +59,12 @@ func (m *infraManager) Install(ctx context.Context, config *types.InstallationCo return fmt.Errorf("install can only be run once") } - if config == nil { - return fmt.Errorf("installation config is required") - } - license, err := helpers.ParseLicense(m.licenseFile) if err != nil { return fmt.Errorf("parse license: %w", err) } - if err := m.initComponentsList(license); err != nil { + if err := m.initComponentsList(license, rc); err != nil { return fmt.Errorf("init components: %w", err) } @@ -76,16 +72,16 @@ func (m *infraManager) Install(ctx context.Context, config *types.InstallationCo return fmt.Errorf("set status: %w", err) } - // Run install in background - go m.install(context.Background(), config, license) + // Background context is used to avoid canceling the operation if the context is canceled + go m.install(context.Background(), license, rc) return nil } -func (m *infraManager) initComponentsList(license *kotsv1beta1.License) error { +func (m *infraManager) initComponentsList(license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) error { components := []types.InfraComponent{{Name: K0sComponentName}} - addOns := addons.GetAddOnsForInstall(m.rc, addons.InstallOptions{ + addOns := addons.GetAddOnsForInstall(rc, addons.InstallOptions{ IsAirgap: m.airgapBundle != "", DisasterRecoveryEnabled: license.Spec.IsDisasterRecoverySupported, }) @@ -103,7 +99,7 @@ func (m *infraManager) initComponentsList(license *kotsv1beta1.License) error { return nil } -func (m *infraManager) install(ctx context.Context, config *types.InstallationConfig, license *kotsv1beta1.License) (finalErr error) { +func (m *infraManager) install(ctx context.Context, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) (finalErr error) { defer func() { if r := recover(); r != nil { finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) @@ -119,7 +115,7 @@ func (m *infraManager) install(ctx context.Context, config *types.InstallationCo } }() - _, err := m.installK0s(ctx, config) + _, err := m.installK0s(ctx, rc) if err != nil { return fmt.Errorf("install k0s: %w", err) } @@ -134,18 +130,18 @@ func (m *infraManager) install(ctx context.Context, config *types.InstallationCo return fmt.Errorf("create metadata client: %w", err) } - hcli, err := m.getHelmClient() + hcli, err := m.getHelmClient(rc) if err != nil { return fmt.Errorf("create helm client: %w", err) } defer hcli.Close() - in, err := m.recordInstallation(ctx, kcli, license) + in, err := m.recordInstallation(ctx, kcli, license, rc) if err != nil { return fmt.Errorf("record installation: %w", err) } - if err := m.installAddOns(ctx, license, kcli, mcli, hcli); err != nil { + if err := m.installAddOns(ctx, license, kcli, mcli, hcli, rc); err != nil { return fmt.Errorf("install addons: %w", err) } @@ -164,7 +160,7 @@ func (m *infraManager) install(ctx context.Context, config *types.InstallationCo return nil } -func (m *infraManager) installK0s(ctx context.Context, config *types.InstallationConfig) (k0sCfg *k0sv1beta1.ClusterConfig, finalErr error) { +func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeConfig) (k0sCfg *k0sv1beta1.ClusterConfig, finalErr error) { componentName := K0sComponentName if err := m.setComponentStatus(componentName, types.StateRunning, "Installing"); err != nil { @@ -191,18 +187,18 @@ func (m *infraManager) installK0s(ctx context.Context, config *types.Installatio logFn := m.logFn("k0s") logFn("creating k0s configuration file") - k0sCfg, err := k0s.WriteK0sConfig(ctx, config.NetworkInterface, m.airgapBundle, config.PodCIDR, config.ServiceCIDR, m.endUserConfig, nil) + k0sCfg, err := k0s.WriteK0sConfig(ctx, rc.NetworkInterface(), m.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) if err != nil { return nil, fmt.Errorf("create config file: %w", err) } logFn("creating systemd unit files") - if err := hostutils.CreateSystemdUnitFiles(ctx, m.logger, m.rc, false); err != nil { + if err := hostutils.CreateSystemdUnitFiles(ctx, m.logger, rc, false); err != nil { return nil, fmt.Errorf("create systemd unit files: %w", err) } logFn("installing k0s") - if err := k0s.Install(m.rc, config.NetworkInterface); err != nil { + if err := k0s.Install(rc); err != nil { return nil, fmt.Errorf("install cluster: %w", err) } @@ -224,7 +220,7 @@ func (m *infraManager) installK0s(ctx context.Context, config *types.Installatio } logFn("adding registry to containerd") - registryIP, err := registry.GetRegistryClusterIP(config.ServiceCIDR) + registryIP, err := registry.GetRegistryClusterIP(rc.ServiceCIDR()) if err != nil { return nil, fmt.Errorf("get registry cluster IP: %w", err) } @@ -235,7 +231,7 @@ func (m *infraManager) installK0s(ctx context.Context, config *types.Installatio return k0sCfg, nil } -func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *kotsv1beta1.License) (*ecv1beta1.Installation, error) { +func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Client, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) (*ecv1beta1.Installation, error) { logFn := m.logFn("metadata") // get the configured custom domains @@ -248,7 +244,7 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien License: license, ConfigSpec: m.getECConfigSpec(), MetricsBaseURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), - RuntimeConfig: m.rc.Get(), + RuntimeConfig: rc.Get(), EndUserConfig: m.endUserConfig, }) if err != nil { @@ -263,7 +259,7 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien return in, nil } -func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.License, kcli client.Client, mcli metadata.Interface, hcli helm.Client) error { +func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.License, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig) error { // get the configured custom domains ecDomains := utils.GetDomains(m.releaseData) @@ -294,7 +290,7 @@ func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.L addons.WithKubernetesClient(kcli), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(m.rc), + addons.WithRuntimeConfig(rc), addons.WithProgressChannel(progressChan), ) @@ -312,7 +308,7 @@ func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.L EndUserConfigSpec: m.getEndUserConfigSpec(), KotsInstaller: func() error { opts := kotscli.InstallOptions{ - RuntimeConfig: m.rc, + RuntimeConfig: rc, AppSlug: license.Spec.AppSlug, LicenseFile: m.licenseFile, Namespace: runtimeconfig.KotsadmNamespace, diff --git a/api/internal/managers/infra/manager.go b/api/internal/managers/infra/manager.go index 7d673ff6a..aa777f267 100644 --- a/api/internal/managers/infra/manager.go +++ b/api/internal/managers/infra/manager.go @@ -17,14 +17,13 @@ var _ InfraManager = &infraManager{} // InfraManager provides methods for managing infrastructure setup type InfraManager interface { Get() (*types.Infra, error) - Install(ctx context.Context, config *types.InstallationConfig) error + Install(ctx context.Context, rc runtimeconfig.RuntimeConfig) error } // infraManager is an implementation of the InfraManager interface type infraManager struct { infra *types.Infra infraStore Store - rc runtimeconfig.RuntimeConfig password string tlsConfig types.TLSConfig licenseFile string @@ -38,12 +37,6 @@ type infraManager struct { type InfraManagerOption func(*infraManager) -func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) InfraManagerOption { - return func(c *infraManager) { - c.rc = rc - } -} - func WithLogger(logger logrus.FieldLogger) InfraManagerOption { return func(c *infraManager) { c.logger = logger @@ -112,10 +105,6 @@ func NewInfraManager(opts ...InfraManagerOption) *infraManager { opt(manager) } - if manager.rc == nil { - manager.rc = runtimeconfig.New(nil) - } - if manager.logger == nil { manager.logger = logger.NewDiscardLogger() } diff --git a/api/internal/managers/infra/manager_mock.go b/api/internal/managers/infra/manager_mock.go index 426a49500..44bd9e140 100644 --- a/api/internal/managers/infra/manager_mock.go +++ b/api/internal/managers/infra/manager_mock.go @@ -4,6 +4,7 @@ import ( "context" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/mock" ) @@ -14,8 +15,8 @@ type MockInfraManager struct { mock.Mock } -func (m *MockInfraManager) Install(ctx context.Context, config *types.InstallationConfig) error { - args := m.Called(ctx, config) +func (m *MockInfraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + args := m.Called(ctx, rc) return args.Error(0) } diff --git a/api/internal/managers/infra/util.go b/api/internal/managers/infra/util.go index dc53eb60a..50fbb3850 100644 --- a/api/internal/managers/infra/util.go +++ b/api/internal/managers/infra/util.go @@ -9,6 +9,7 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/versions" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -25,13 +26,13 @@ func (m *infraManager) waitForNode(ctx context.Context, kcli client.Client) erro return nil } -func (m *infraManager) getHelmClient() (helm.Client, error) { +func (m *infraManager) getHelmClient(rc runtimeconfig.RuntimeConfig) (helm.Client, error) { airgapChartsPath := "" if m.airgapBundle != "" { - airgapChartsPath = m.rc.EmbeddedClusterChartsSubDir() + airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: m.rc.PathToKubeConfig(), + KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, LogFn: m.logFn("helm"), diff --git a/api/internal/managers/installation/config.go b/api/internal/managers/installation/config.go index 25f7a0bcf..d06a7f02c 100644 --- a/api/internal/managers/installation/config.go +++ b/api/internal/managers/installation/config.go @@ -11,6 +11,7 @@ import ( newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) func (m *installationManager) GetConfig() (*types.InstallationConfig, error) { @@ -21,7 +22,7 @@ func (m *installationManager) SetConfig(config types.InstallationConfig) error { return m.installationStore.SetConfig(config) } -func (m *installationManager) ValidateConfig(config *types.InstallationConfig) error { +func (m *installationManager) ValidateConfig(config *types.InstallationConfig, managerPort int) error { var ve *types.APIError if err := m.validateGlobalCIDR(config); err != nil { @@ -40,11 +41,11 @@ func (m *installationManager) ValidateConfig(config *types.InstallationConfig) e ve = types.AppendFieldError(ve, "networkInterface", err) } - if err := m.validateAdminConsolePort(config); err != nil { + if err := m.validateAdminConsolePort(config, managerPort); err != nil { ve = types.AppendFieldError(ve, "adminConsolePort", err) } - if err := m.validateLocalArtifactMirrorPort(config); err != nil { + if err := m.validateLocalArtifactMirrorPort(config, managerPort); err != nil { ve = types.AppendFieldError(ve, "localArtifactMirrorPort", err) } @@ -97,7 +98,7 @@ func (m *installationManager) validateNetworkInterface(config *types.Installatio return nil } -func (m *installationManager) validateAdminConsolePort(config *types.InstallationConfig) error { +func (m *installationManager) validateAdminConsolePort(config *types.InstallationConfig, managerPort int) error { if config.AdminConsolePort == 0 { return errors.New("adminConsolePort is required") } @@ -111,14 +112,14 @@ func (m *installationManager) validateAdminConsolePort(config *types.Installatio return errors.New("adminConsolePort and localArtifactMirrorPort cannot be equal") } - if config.AdminConsolePort == m.rc.ManagerPort() { + if config.AdminConsolePort == managerPort { return errors.New("adminConsolePort cannot be the same as the manager port") } return nil } -func (m *installationManager) validateLocalArtifactMirrorPort(config *types.InstallationConfig) error { +func (m *installationManager) validateLocalArtifactMirrorPort(config *types.InstallationConfig, managerPort int) error { if config.LocalArtifactMirrorPort == 0 { return errors.New("localArtifactMirrorPort is required") } @@ -132,7 +133,7 @@ func (m *installationManager) validateLocalArtifactMirrorPort(config *types.Inst return errors.New("adminConsolePort and localArtifactMirrorPort cannot be equal") } - if config.LocalArtifactMirrorPort == m.rc.ManagerPort() { + if config.LocalArtifactMirrorPort == managerPort { return errors.New("localArtifactMirrorPort cannot be the same as the manager port") } @@ -202,7 +203,7 @@ func (m *installationManager) setCIDRDefaults(config *types.InstallationConfig) return nil } -func (m *installationManager) ConfigureHost(ctx context.Context) error { +func (m *installationManager) ConfigureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { m.mu.Lock() defer m.mu.Unlock() @@ -218,12 +219,13 @@ func (m *installationManager) ConfigureHost(ctx context.Context) error { return fmt.Errorf("set running status: %w", err) } - go m.configureHost(context.Background()) + // Background context is used to avoid canceling the operation if the context is canceled + go m.configureHost(context.Background(), rc) return nil } -func (m *installationManager) configureHost(ctx context.Context) (finalErr error) { +func (m *installationManager) configureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) (finalErr error) { defer func() { if r := recover(); r != nil { finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) @@ -243,7 +245,7 @@ func (m *installationManager) configureHost(ctx context.Context) (finalErr error LicenseFile: m.licenseFile, AirgapBundle: m.airgapBundle, } - if err := m.hostUtils.ConfigureHost(ctx, m.rc, opts); err != nil { + if err := m.hostUtils.ConfigureHost(ctx, rc, opts); err != nil { return fmt.Errorf("configure installation: %w", err) } diff --git a/api/internal/managers/installation/config_test.go b/api/internal/managers/installation/config_test.go index dc561acbc..428429f71 100644 --- a/api/internal/managers/installation/config_test.go +++ b/api/internal/managers/installation/config_test.go @@ -185,9 +185,9 @@ func TestValidateConfig(t *testing.T) { } rc.SetDataDir(t.TempDir()) - manager := NewInstallationManager(WithRuntimeConfig(rc)) + manager := NewInstallationManager() - err := manager.ValidateConfig(tt.config) + err := manager.ValidateConfig(tt.config, rc.ManagerPort()) if tt.expectedErr { assert.Error(t, err) @@ -443,7 +443,6 @@ func TestConfigureHost(t *testing.T) { // Create manager with mocks manager := NewInstallationManager( - WithRuntimeConfig(rc), WithHostUtils(mockHostUtils), WithInstallationStore(mockStore), WithLicenseFile("license.yaml"), @@ -451,7 +450,7 @@ func TestConfigureHost(t *testing.T) { ) // Run the test - err := manager.ConfigureHost(context.Background()) + err := manager.ConfigureHost(context.Background(), rc) // Assertions if tt.expectedErr { diff --git a/api/internal/managers/installation/manager.go b/api/internal/managers/installation/manager.go index 16b281c2f..cf3087f47 100644 --- a/api/internal/managers/installation/manager.go +++ b/api/internal/managers/installation/manager.go @@ -20,16 +20,15 @@ type InstallationManager interface { SetConfig(config types.InstallationConfig) error GetStatus() (*types.Status, error) SetStatus(status types.Status) error - ValidateConfig(config *types.InstallationConfig) error + ValidateConfig(config *types.InstallationConfig, managerPort int) error SetConfigDefaults(config *types.InstallationConfig) error - ConfigureHost(ctx context.Context) error + ConfigureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) error } // installationManager is an implementation of the InstallationManager interface type installationManager struct { installation *types.Installation installationStore InstallationStore - rc runtimeconfig.RuntimeConfig licenseFile string airgapBundle string netUtils utils.NetUtils @@ -40,12 +39,6 @@ type installationManager struct { type InstallationManagerOption func(*installationManager) -func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) InstallationManagerOption { - return func(c *installationManager) { - c.rc = rc - } -} - func WithLogger(logger logrus.FieldLogger) InstallationManagerOption { return func(c *installationManager) { c.logger = logger @@ -96,10 +89,6 @@ func NewInstallationManager(opts ...InstallationManagerOption) *installationMana opt(manager) } - if manager.rc == nil { - manager.rc = runtimeconfig.New(nil) - } - if manager.logger == nil { manager.logger = logger.NewDiscardLogger() } diff --git a/api/internal/managers/installation/manager_mock.go b/api/internal/managers/installation/manager_mock.go index 68754cf36..68bf6d7e0 100644 --- a/api/internal/managers/installation/manager_mock.go +++ b/api/internal/managers/installation/manager_mock.go @@ -4,6 +4,7 @@ import ( "context" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/mock" ) @@ -45,8 +46,8 @@ func (m *MockInstallationManager) SetStatus(status types.Status) error { } // ValidateConfig mocks the ValidateConfig method -func (m *MockInstallationManager) ValidateConfig(config *types.InstallationConfig) error { - args := m.Called(config) +func (m *MockInstallationManager) ValidateConfig(config *types.InstallationConfig, managerPort int) error { + args := m.Called(config, managerPort) return args.Error(0) } @@ -57,7 +58,7 @@ func (m *MockInstallationManager) SetConfigDefaults(config *types.InstallationCo } // ConfigureHost mocks the ConfigureHost method -func (m *MockInstallationManager) ConfigureHost(ctx context.Context) error { - args := m.Called(ctx) +func (m *MockInstallationManager) ConfigureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + args := m.Called(ctx, rc) return args.Error(0) } diff --git a/api/internal/managers/preflight/hostpreflight.go b/api/internal/managers/preflight/hostpreflight.go index b22b20bba..d1426fdfa 100644 --- a/api/internal/managers/preflight/hostpreflight.go +++ b/api/internal/managers/preflight/hostpreflight.go @@ -9,6 +9,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" troubleshootanalyze "github.com/replicatedhq/troubleshoot/pkg/analyze" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" ) @@ -28,15 +29,15 @@ type RunHostPreflightOptions struct { HostPreflightSpec *troubleshootv1beta2.HostPreflightSpec } -func (m *hostPreflightManager) PrepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { - hpf, err := m.prepareHostPreflights(ctx, opts) +func (m *hostPreflightManager) PrepareHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { + hpf, err := m.prepareHostPreflights(ctx, rc, opts) if err != nil { return nil, err } return hpf, nil } -func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, opts RunHostPreflightOptions) error { +func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) error { m.mu.Lock() defer m.mu.Unlock() @@ -48,8 +49,8 @@ func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, opts RunHo return fmt.Errorf("set running status: %w", err) } - // Run preflights in background - go m.runHostPreflights(context.Background(), opts) + // Background context is used to avoid canceling the operation if the context is canceled + go m.runHostPreflights(context.Background(), rc, opts) return nil } @@ -66,9 +67,9 @@ func (m *hostPreflightManager) GetHostPreflightTitles(ctx context.Context) ([]st return m.hostPreflightStore.GetTitles() } -func (m *hostPreflightManager) prepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { +func (m *hostPreflightManager) prepareHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { // Get node IP - nodeIP, err := m.netUtils.FirstValidAddress(m.rc.NetworkInterface()) + nodeIP, err := m.netUtils.FirstValidAddress(rc.NetworkInterface()) if err != nil { return nil, fmt.Errorf("determine node ip: %w", err) } @@ -78,21 +79,21 @@ func (m *hostPreflightManager) prepareHostPreflights(ctx context.Context, opts P HostPreflightSpec: opts.HostPreflightSpec, ReplicatedAppURL: opts.ReplicatedAppURL, ProxyRegistryURL: opts.ProxyRegistryURL, - AdminConsolePort: m.rc.AdminConsolePort(), - LocalArtifactMirrorPort: m.rc.LocalArtifactMirrorPort(), - DataDir: m.rc.EmbeddedClusterHomeDirectory(), - K0sDataDir: m.rc.EmbeddedClusterK0sSubDir(), - OpenEBSDataDir: m.rc.EmbeddedClusterOpenEBSLocalSubDir(), - Proxy: m.rc.ProxySpec(), - PodCIDR: m.rc.PodCIDR(), - ServiceCIDR: m.rc.ServiceCIDR(), + AdminConsolePort: rc.AdminConsolePort(), + LocalArtifactMirrorPort: rc.LocalArtifactMirrorPort(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + Proxy: rc.ProxySpec(), + PodCIDR: rc.PodCIDR(), + ServiceCIDR: rc.ServiceCIDR(), NodeIP: nodeIP, IsAirgap: opts.IsAirgap, TCPConnectionsRequired: opts.TCPConnectionsRequired, IsJoin: opts.IsJoin, IsUI: opts.IsUI, } - if cidr := m.rc.GlobalCIDR(); cidr != "" { + if cidr := rc.GlobalCIDR(); cidr != "" { prepareOpts.GlobalCIDR = &cidr } @@ -105,7 +106,7 @@ func (m *hostPreflightManager) prepareHostPreflights(ctx context.Context, opts P return hpf, nil } -func (m *hostPreflightManager) runHostPreflights(ctx context.Context, opts RunHostPreflightOptions) { +func (m *hostPreflightManager) runHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) { defer func() { if r := recover(); r != nil { if err := m.setFailedStatus(fmt.Sprintf("panic: %v: %s", r, string(debug.Stack()))); err != nil { @@ -115,7 +116,7 @@ func (m *hostPreflightManager) runHostPreflights(ctx context.Context, opts RunHo }() // Run the preflights using the shared core function - output, stderr, err := m.runner.Run(ctx, opts.HostPreflightSpec, m.rc) + output, stderr, err := m.runner.Run(ctx, opts.HostPreflightSpec, rc) if err != nil { errMsg := fmt.Sprintf("Host preflights failed to run: %v", err) if stderr != "" { @@ -127,11 +128,11 @@ func (m *hostPreflightManager) runHostPreflights(ctx context.Context, opts RunHo return } - if err := m.runner.SaveToDisk(output, m.rc.PathToEmbeddedClusterSupportFile("host-preflight-results.json")); err != nil { + if err := m.runner.SaveToDisk(output, rc.PathToEmbeddedClusterSupportFile("host-preflight-results.json")); err != nil { m.logger.WithField("error", err).Warn("save preflights output") } - if err := m.runner.CopyBundleTo(m.rc.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz")); err != nil { + if err := m.runner.CopyBundleTo(rc.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz")); err != nil { m.logger.WithField("error", err).Warn("copy preflight bundle to embedded-cluster support dir") } diff --git a/api/internal/managers/preflight/hostpreflight_test.go b/api/internal/managers/preflight/hostpreflight_test.go index f7223c645..f654ac535 100644 --- a/api/internal/managers/preflight/hostpreflight_test.go +++ b/api/internal/managers/preflight/hostpreflight_test.go @@ -215,14 +215,13 @@ func TestHostPreflightManager_PrepareHostPreflights(t *testing.T) { manager := NewHostPreflightManager( WithPreflightRunner(mockRunner), WithHostPreflightStore(mockStore), - WithRuntimeConfig(rc), WithLogger(logger.NewDiscardLogger()), WithMetricsReporter(mockMetrics), WithNetUtils(mockNetUtils), ) // Execute - hpf, err := manager.PrepareHostPreflights(context.Background(), tt.opts) + hpf, err := manager.PrepareHostPreflights(context.Background(), rc, tt.opts) // Assert if tt.expectedError != "" { @@ -457,13 +456,12 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { manager := NewHostPreflightManager( WithPreflightRunner(mockRunner), WithHostPreflightStore(NewMemoryStore(tt.initialState)), - WithRuntimeConfig(rc), WithLogger(logger.NewDiscardLogger()), WithMetricsReporter(mockMetrics), ) // Execute - err := manager.RunHostPreflights(context.Background(), tt.opts) + err := manager.RunHostPreflights(context.Background(), rc, tt.opts) // If there's an error we don't need to wait for async execution if tt.expectedError != "" { require.Error(t, err) diff --git a/api/internal/managers/preflight/manager.go b/api/internal/managers/preflight/manager.go index fa9b51a22..16bcc504d 100644 --- a/api/internal/managers/preflight/manager.go +++ b/api/internal/managers/preflight/manager.go @@ -16,8 +16,8 @@ import ( // HostPreflightManager provides methods for running host preflights type HostPreflightManager interface { - PrepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) - RunHostPreflights(ctx context.Context, opts RunHostPreflightOptions) error + PrepareHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) + RunHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) error GetHostPreflightStatus(ctx context.Context) (*types.Status, error) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) GetHostPreflightTitles(ctx context.Context) ([]string, error) @@ -27,7 +27,6 @@ type hostPreflightManager struct { hostPreflightStore HostPreflightStore runner preflights.PreflightsRunnerInterface netUtils utils.NetUtils - rc runtimeconfig.RuntimeConfig logger logrus.FieldLogger metricsReporter metrics.ReporterInterface mu sync.RWMutex @@ -35,12 +34,6 @@ type hostPreflightManager struct { type HostPreflightManagerOption func(*hostPreflightManager) -func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) HostPreflightManagerOption { - return func(m *hostPreflightManager) { - m.rc = rc - } -} - func WithLogger(logger logrus.FieldLogger) HostPreflightManagerOption { return func(m *hostPreflightManager) { m.logger = logger @@ -79,10 +72,6 @@ func NewHostPreflightManager(opts ...HostPreflightManagerOption) HostPreflightMa opt(manager) } - if manager.rc == nil { - manager.rc = runtimeconfig.New(nil) - } - if manager.logger == nil { manager.logger = logger.NewDiscardLogger() } diff --git a/api/internal/managers/preflight/manager_mock.go b/api/internal/managers/preflight/manager_mock.go index 4f087af8e..8abcac574 100644 --- a/api/internal/managers/preflight/manager_mock.go +++ b/api/internal/managers/preflight/manager_mock.go @@ -4,6 +4,7 @@ import ( "context" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/stretchr/testify/mock" ) @@ -16,8 +17,8 @@ type MockHostPreflightManager struct { } // PrepareHostPreflights mocks the PrepareHostPreflights method -func (m *MockHostPreflightManager) PrepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { - args := m.Called(ctx, opts) +func (m *MockHostPreflightManager) PrepareHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { + args := m.Called(ctx, rc, opts) if args.Get(0) == nil { return nil, args.Error(1) } @@ -25,8 +26,8 @@ func (m *MockHostPreflightManager) PrepareHostPreflights(ctx context.Context, op } // RunHostPreflights mocks the RunHostPreflights method -func (m *MockHostPreflightManager) RunHostPreflights(ctx context.Context, opts RunHostPreflightOptions) error { - args := m.Called(ctx, opts) +func (m *MockHostPreflightManager) RunHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) error { + args := m.Called(ctx, rc, opts) return args.Error(0) } diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index ebb4500fd..8f28cdd1c 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -800,7 +800,7 @@ func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runti } logrus.Debugf("installing k0s") - if err := k0s.Install(rc, flags.networkInterface); err != nil { + if err := k0s.Install(rc); err != nil { loading.ErrorClosef("Failed to install node") return nil, fmt.Errorf("install cluster: %w", err) } diff --git a/pkg-new/k0s/k0s.go b/pkg-new/k0s/k0s.go index d87c3fcd4..f455005e4 100644 --- a/pkg-new/k0s/k0s.go +++ b/pkg-new/k0s/k0s.go @@ -52,14 +52,14 @@ func (k *K0s) GetStatus(ctx context.Context) (*K0sStatus, error) { // Install runs the k0s install command and waits for it to finish. If no configuration // is found one is generated. -func Install(rc runtimeconfig.RuntimeConfig, networkInterface string) error { +func Install(rc runtimeconfig.RuntimeConfig) error { ourbin := rc.PathToEmbeddedClusterBinary("k0s") hstbin := runtimeconfig.K0sBinaryPath if err := helpers.MoveFile(ourbin, hstbin); err != nil { return fmt.Errorf("unable to move k0s binary: %w", err) } - nodeIP, err := netutils.FirstValidAddress(networkInterface) + nodeIP, err := netutils.FirstValidAddress(rc.NetworkInterface()) if err != nil { return fmt.Errorf("unable to find first valid address: %w", err) } diff --git a/pkg/dryrun/k0s.go b/pkg/dryrun/k0s.go index e729ac791..9e6c5edc5 100644 --- a/pkg/dryrun/k0s.go +++ b/pkg/dryrun/k0s.go @@ -17,7 +17,7 @@ func (c *K0s) GetStatus(ctx context.Context) (*k0s.K0sStatus, error) { return c.Status, nil } -func (c *K0s) Install(rc runtimeconfig.RuntimeConfig, networkInterface string) error { +func (c *K0s) Install(rc runtimeconfig.RuntimeConfig) error { return nil // TODO: implement } From 7f5fd8038be57f17cdbd2600cb0c1ae79ee89fd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:15:21 +0000 Subject: [PATCH 09/48] build(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#2332) Bumps the npm_and_yarn group with 2 updates in the /web directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [esbuild](https://github.com/evanw/esbuild). Updates `vite` from 5.4.8 to 5.4.19 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite) Updates `esbuild` from 0.21.5 to 0.25.5 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.5) Updates `vite` from 5.4.19 to 6.3.5 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.19/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.19/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 5.4.19 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: esbuild dependency-version: 0.25.5 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: vite dependency-version: 6.3.5 dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 584 +++++++++++++++++++++++++++--------------- web/package.json | 2 +- 2 files changed, 384 insertions(+), 202 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 2d252fc4a..e2cebe9a0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -36,7 +36,7 @@ "tailwindcss": "^3.4.1", "typescript": "^5.8.3", "typescript-eslint": "^8.34.0", - "vite": "^5.4.2", + "vite": "^6.3.5", "vite-plugin-static-copy": "^3.0.0", "vitest": "^3.2.3" } @@ -533,371 +533,428 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1589,208 +1646,280 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", + "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", + "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", + "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", + "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", + "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", + "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", + "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", + "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", + "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", + "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", + "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", + "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", + "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", + "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", + "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", + "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", + "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", + "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", + "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", + "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2252,10 +2381,11 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -3758,41 +3888,44 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escalade": { @@ -6638,12 +6771,13 @@ } }, "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", + "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -6653,22 +6787,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", + "@rollup/rollup-android-arm-eabi": "4.43.0", + "@rollup/rollup-android-arm64": "4.43.0", + "@rollup/rollup-darwin-arm64": "4.43.0", + "@rollup/rollup-darwin-x64": "4.43.0", + "@rollup/rollup-freebsd-arm64": "4.43.0", + "@rollup/rollup-freebsd-x64": "4.43.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", + "@rollup/rollup-linux-arm-musleabihf": "4.43.0", + "@rollup/rollup-linux-arm64-gnu": "4.43.0", + "@rollup/rollup-linux-arm64-musl": "4.43.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-musl": "4.43.0", + "@rollup/rollup-linux-s390x-gnu": "4.43.0", + "@rollup/rollup-linux-x64-gnu": "4.43.0", + "@rollup/rollup-linux-x64-musl": "4.43.0", + "@rollup/rollup-win32-arm64-msvc": "4.43.0", + "@rollup/rollup-win32-ia32-msvc": "4.43.0", + "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" } }, @@ -7532,20 +7670,24 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -7554,19 +7696,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -7587,6 +7735,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -7633,6 +7787,34 @@ "vite": "^5.0.0 || ^6.0.0" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", diff --git a/web/package.json b/web/package.json index 3a63b5c6e..e8dc22223 100644 --- a/web/package.json +++ b/web/package.json @@ -40,7 +40,7 @@ "tailwindcss": "^3.4.1", "typescript": "^5.8.3", "typescript-eslint": "^8.34.0", - "vite": "^5.4.2", + "vite": "^6.3.5", "vite-plugin-static-copy": "^3.0.0", "vitest": "^3.2.3" } From 04da9ad7fa17397428572588fb62731b850c8dae Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Wed, 18 Jun 2025 13:25:05 -0700 Subject: [PATCH 10/48] Add integration test for api infra setup (#2331) * Add integration test for api infra setup --- .github/workflows/ci.yaml | 18 + Makefile | 4 + api/Makefile | 6 +- api/client/client.go | 12 +- api/client/client_test.go | 20 +- api/client/install.go | 76 +-- api/controllers/install/controller.go | 38 +- api/controllers/install/controller_test.go | 135 ++-- api/controllers/install/hostpreflight.go | 2 +- api/controllers/install/infra.go | 2 +- api/controllers/install/installation.go | 22 +- api/controllers/install/status.go | 4 +- api/install.go | 6 +- api/integration/assets/license.yaml | 37 ++ api/integration/hostpreflights_test.go | 22 +- api/integration/install_test.go | 591 +++++++++++++++++- api/internal/managers/infra/install.go | 55 +- api/internal/managers/infra/manager.go | 75 ++- api/internal/managers/infra/manager_mock.go | 6 +- api/internal/managers/infra/status.go | 7 +- api/internal/managers/infra/status_test.go | 19 +- api/internal/managers/infra/util.go | 29 +- api/internal/managers/infra/util_test.go | 5 +- api/internal/managers/installation/config.go | 18 +- .../managers/installation/config_test.go | 79 +-- api/internal/managers/installation/manager.go | 24 +- .../managers/installation/manager_mock.go | 14 +- api/internal/managers/installation/status.go | 2 +- api/internal/managers/installation/store.go | 58 -- .../managers/installation/store_mock.go | 43 -- .../managers/preflight/hostpreflight.go | 8 +- .../managers/preflight/hostpreflight_test.go | 73 +-- api/internal/managers/preflight/manager.go | 9 +- .../managers/preflight/manager_mock.go | 6 +- api/internal/managers/preflight/store.go | 82 --- .../{managers => store}/infra/store.go | 75 ++- .../{managers => store}/infra/store_mock.go | 14 +- .../{managers => store}/infra/store_test.go | 18 +- api/internal/store/installation/store.go | 80 +++ api/internal/store/installation/store_mock.go | 43 ++ .../installation/store_test.go | 40 +- api/internal/store/preflight/store.go | 114 ++++ .../preflight/store_mock.go | 24 +- .../preflight/store_test.go | 66 +- api/internal/store/store.go | 87 +++ api/types/infra.go | 13 +- api/types/install.go | 20 +- api/types/installation.go | 12 +- api/types/preflight.go | 8 +- api/types/responses.go | 2 +- api/types/status.go | 12 +- api/types/status_test.go | 18 +- cmd/installer/cli/api.go | 2 +- cmd/installer/cli/install.go | 4 +- cmd/installer/cli/join.go | 2 +- cmd/installer/cli/restore.go | 3 +- go.mod | 1 + go.sum | 2 + operator/pkg/upgrade/upgrade.go | 2 +- .../hostutils}/containerd.go | 4 +- pkg-new/hostutils/interface.go | 5 + pkg-new/hostutils/mock.go | 6 + pkg-new/k0s/interface.go | 17 + pkg-new/k0s/k0s.go | 10 +- pkg-new/k0s/mock.go | 21 + pkg/dryrun/k0s.go | 12 +- pkg/support/hostbundle.go | 10 +- 67 files changed, 1608 insertions(+), 746 deletions(-) create mode 100644 api/integration/assets/license.yaml delete mode 100644 api/internal/managers/installation/store.go delete mode 100644 api/internal/managers/installation/store_mock.go delete mode 100644 api/internal/managers/preflight/store.go rename api/internal/{managers => store}/infra/store.go (50%) rename api/internal/{managers => store}/infra/store_mock.go (71%) rename api/internal/{managers => store}/infra/store_test.go (96%) create mode 100644 api/internal/store/installation/store.go create mode 100644 api/internal/store/installation/store_mock.go rename api/internal/{managers => store}/installation/store_test.go (79%) create mode 100644 api/internal/store/preflight/store.go rename api/internal/{managers => store}/preflight/store_mock.go (55%) rename api/internal/{managers => store}/preflight/store_test.go (80%) create mode 100644 api/internal/store/store.go rename {pkg/airgap => pkg-new/hostutils}/containerd.go (91%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6c446a6ce..c7f07db3f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -86,11 +86,29 @@ jobs: name: Integration tests runs-on: ubuntu-latest needs: + - int-tests-api - int-tests-kind steps: - name: Succeed if all tests passed run: echo "Integration tests succeeded" + int-tests-api: + name: Integration tests (api) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache-dependency-path: "**/*.sum" + - name: Run tests + run: | + make test-integration + int-tests-kind: name: Integration tests (kind) runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 746745253..21de5ec1f 100644 --- a/Makefile +++ b/Makefile @@ -277,6 +277,10 @@ unit-tests: envtest $(MAKE) -C operator test $(MAKE) -C utils unit-tests +.PHONY: test-integration +test-integration: static + $(MAKE) -C api test-integration + .PHONY: vet vet: go vet -tags $(GO_BUILD_TAGS) ./... diff --git a/api/Makefile b/api/Makefile index 6b0bdc047..ce38fe7f4 100644 --- a/api/Makefile +++ b/api/Makefile @@ -13,4 +13,8 @@ swag: .PHONY: unit-tests unit-tests: - go test -race -tags $(GO_BUILD_TAGS) -v ./... + go test -race -tags $(GO_BUILD_TAGS) -v $(shell go list ./... | grep -v '/integration') + +.PHONY: test-integration +test-integration: + go test -race -tags $(GO_BUILD_TAGS) -v ./integration diff --git a/api/client/client.go b/api/client/client.go index badf28227..3eb7e0537 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -11,12 +11,12 @@ import ( type Client interface { Authenticate(password string) error - GetInstallationConfig() (*types.InstallationConfig, error) - GetInstallationStatus() (*types.Status, error) - ConfigureInstallation(config *types.InstallationConfig) (*types.Status, error) - SetupInfra() (*types.Infra, error) - GetInfraStatus() (*types.Infra, error) - SetInstallStatus(status *types.Status) (*types.Status, error) + GetInstallationConfig() (types.InstallationConfig, error) + GetInstallationStatus() (types.Status, error) + ConfigureInstallation(config types.InstallationConfig) (types.Status, error) + SetupInfra() (types.Infra, error) + GetInfraStatus() (types.Infra, error) + SetInstallStatus(status types.Status) (types.Status, error) } type client struct { diff --git a/api/client/client_test.go b/api/client/client_test.go index 6f40fa513..ef4daed90 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -138,7 +138,7 @@ func TestGetInstallationConfig(t *testing.T) { c = New(errorServer.URL, WithToken("test-token")) config, err = c.GetInstallationConfig() assert.Error(t, err) - assert.Nil(t, config) + assert.Equal(t, types.InstallationConfig{}, config) apiErr, ok := err.(*types.APIError) require.True(t, ok, "Expected err to be of type *types.APIError") @@ -177,7 +177,7 @@ func TestConfigureInstallation(t *testing.T) { GlobalCIDR: "20.0.0.0/24", LocalArtifactMirrorPort: 9081, } - status, err := c.ConfigureInstallation(&config) + status, err := c.ConfigureInstallation(config) assert.NoError(t, err) assert.NotNil(t, status) assert.Equal(t, types.StateRunning, status.State) @@ -194,9 +194,9 @@ func TestConfigureInstallation(t *testing.T) { defer errorServer.Close() c = New(errorServer.URL, WithToken("test-token")) - status, err = c.ConfigureInstallation(&config) + status, err = c.ConfigureInstallation(config) assert.Error(t, err) - assert.Nil(t, status) + assert.Equal(t, types.Status{}, status) apiErr, ok := err.(*types.APIError) require.True(t, ok, "Expected err to be of type *types.APIError") @@ -216,7 +216,7 @@ func TestSetupInfra(t *testing.T) { // Return successful response w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.Infra{ - Status: &types.Status{ + Status: types.Status{ State: types.StateRunning, Description: "Installing infra", }, @@ -246,7 +246,7 @@ func TestSetupInfra(t *testing.T) { c = New(errorServer.URL, WithToken("test-token")) infra, err = c.SetupInfra() assert.Error(t, err) - assert.Nil(t, infra) + assert.Equal(t, types.Infra{}, infra) apiErr, ok := err.(*types.APIError) require.True(t, ok, "Expected err to be of type *types.APIError") @@ -266,7 +266,7 @@ func TestGetInfraStatus(t *testing.T) { // Return successful response w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(types.Infra{ - Status: &types.Status{ + Status: types.Status{ State: types.StateSucceeded, Description: "Installation successful", }, @@ -295,7 +295,7 @@ func TestGetInfraStatus(t *testing.T) { c = New(errorServer.URL, WithToken("test-token")) infra, err = c.GetInfraStatus() assert.Error(t, err) - assert.Nil(t, infra) + assert.Equal(t, types.Infra{}, infra) apiErr, ok := err.(*types.APIError) require.True(t, ok, "Expected err to be of type *types.APIError") @@ -325,7 +325,7 @@ func TestSetInstallStatus(t *testing.T) { // Test successful set c := New(server.URL, WithToken("test-token")) - status := &types.Status{ + status := types.Status{ State: types.StateSucceeded, Description: "Installation successful", } @@ -347,7 +347,7 @@ func TestSetInstallStatus(t *testing.T) { c = New(errorServer.URL, WithToken("test-token")) newStatus, err = c.SetInstallStatus(status) assert.Error(t, err) - assert.Nil(t, newStatus) + assert.Equal(t, types.Status{}, newStatus) apiErr, ok := err.(*types.APIError) require.True(t, ok, "Expected err to be of type *types.APIError") diff --git a/api/client/install.go b/api/client/install.go index 825f030ef..d9a036c96 100644 --- a/api/client/install.go +++ b/api/client/install.go @@ -8,174 +8,174 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -func (c *client) GetInstallationConfig() (*types.InstallationConfig, error) { +func (c *client) GetInstallationConfig() (types.InstallationConfig, error) { req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/config", nil) if err != nil { - return nil, err + return types.InstallationConfig{}, err } req.Header.Set("Content-Type", "application/json") setAuthorizationHeader(req, c.token) resp, err := c.httpClient.Do(req) if err != nil { - return nil, err + return types.InstallationConfig{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errorFromResponse(resp) + return types.InstallationConfig{}, errorFromResponse(resp) } var config types.InstallationConfig err = json.NewDecoder(resp.Body).Decode(&config) if err != nil { - return nil, err + return types.InstallationConfig{}, err } - return &config, nil + return config, nil } -func (c *client) ConfigureInstallation(config *types.InstallationConfig) (*types.Status, error) { +func (c *client) ConfigureInstallation(config types.InstallationConfig) (types.Status, error) { b, err := json.Marshal(config) if err != nil { - return nil, err + return types.Status{}, err } req, err := http.NewRequest("POST", c.apiURL+"/api/install/installation/configure", bytes.NewBuffer(b)) if err != nil { - return nil, err + return types.Status{}, err } req.Header.Set("Content-Type", "application/json") setAuthorizationHeader(req, c.token) resp, err := c.httpClient.Do(req) if err != nil { - return nil, err + return types.Status{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errorFromResponse(resp) + return types.Status{}, errorFromResponse(resp) } var status types.Status err = json.NewDecoder(resp.Body).Decode(&status) if err != nil { - return nil, err + return types.Status{}, err } - return &status, nil + return status, nil } -func (c *client) GetInstallationStatus() (*types.Status, error) { +func (c *client) GetInstallationStatus() (types.Status, error) { req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/status", nil) if err != nil { - return nil, err + return types.Status{}, err } req.Header.Set("Content-Type", "application/json") setAuthorizationHeader(req, c.token) resp, err := c.httpClient.Do(req) if err != nil { - return nil, err + return types.Status{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errorFromResponse(resp) + return types.Status{}, errorFromResponse(resp) } var status types.Status err = json.NewDecoder(resp.Body).Decode(&status) if err != nil { - return nil, err + return types.Status{}, err } - return &status, nil + return status, nil } -func (c *client) SetupInfra() (*types.Infra, error) { +func (c *client) SetupInfra() (types.Infra, error) { req, err := http.NewRequest("POST", c.apiURL+"/api/install/infra/setup", nil) if err != nil { - return nil, err + return types.Infra{}, err } req.Header.Set("Content-Type", "application/json") setAuthorizationHeader(req, c.token) resp, err := c.httpClient.Do(req) if err != nil { - return nil, err + return types.Infra{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errorFromResponse(resp) + return types.Infra{}, errorFromResponse(resp) } var infra types.Infra err = json.NewDecoder(resp.Body).Decode(&infra) if err != nil { - return nil, err + return types.Infra{}, err } - return &infra, nil + return infra, nil } -func (c *client) GetInfraStatus() (*types.Infra, error) { +func (c *client) GetInfraStatus() (types.Infra, error) { req, err := http.NewRequest("GET", c.apiURL+"/api/install/infra/status", nil) if err != nil { - return nil, err + return types.Infra{}, err } req.Header.Set("Content-Type", "application/json") setAuthorizationHeader(req, c.token) resp, err := c.httpClient.Do(req) if err != nil { - return nil, err + return types.Infra{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errorFromResponse(resp) + return types.Infra{}, errorFromResponse(resp) } var infra types.Infra err = json.NewDecoder(resp.Body).Decode(&infra) if err != nil { - return nil, err + return types.Infra{}, err } - return &infra, nil + return infra, nil } -func (c *client) SetInstallStatus(s *types.Status) (*types.Status, error) { +func (c *client) SetInstallStatus(s types.Status) (types.Status, error) { b, err := json.Marshal(s) if err != nil { - return nil, err + return types.Status{}, err } req, err := http.NewRequest("POST", c.apiURL+"/api/install/status", bytes.NewBuffer(b)) if err != nil { - return nil, err + return types.Status{}, err } req.Header.Set("Content-Type", "application/json") setAuthorizationHeader(req, c.token) resp, err := c.httpClient.Do(req) if err != nil { - return nil, err + return types.Status{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errorFromResponse(resp) + return types.Status{}, errorFromResponse(resp) } var status types.Status err = json.NewDecoder(resp.Body).Decode(&status) if err != nil { - return nil, err + return types.Status{}, err } - return &status, nil + return status, nil } diff --git a/api/controllers/install/controller.go b/api/controllers/install/controller.go index 34736eff0..46419f2ff 100644 --- a/api/controllers/install/controller.go +++ b/api/controllers/install/controller.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/managers/infra" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" + "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" @@ -19,17 +20,17 @@ import ( ) type Controller interface { - GetInstallationConfig(ctx context.Context) (*types.InstallationConfig, error) - ConfigureInstallation(ctx context.Context, config *types.InstallationConfig) error - GetInstallationStatus(ctx context.Context) (*types.Status, error) + GetInstallationConfig(ctx context.Context) (types.InstallationConfig, error) + ConfigureInstallation(ctx context.Context, config types.InstallationConfig) error + GetInstallationStatus(ctx context.Context) (types.Status, error) RunHostPreflights(ctx context.Context, opts RunHostPreflightsOptions) error - GetHostPreflightStatus(ctx context.Context) (*types.Status, error) + GetHostPreflightStatus(ctx context.Context) (types.Status, error) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) GetHostPreflightTitles(ctx context.Context) ([]string, error) SetupInfra(ctx context.Context) error - GetInfra(ctx context.Context) (*types.Infra, error) - SetStatus(ctx context.Context, status *types.Status) error - GetStatus(ctx context.Context) (*types.Status, error) + GetInfra(ctx context.Context) (types.Infra, error) + SetStatus(ctx context.Context, status types.Status) error + GetStatus(ctx context.Context) (types.Status, error) } type RunHostPreflightsOptions struct { @@ -39,7 +40,8 @@ type RunHostPreflightsOptions struct { var _ Controller = (*InstallController)(nil) type InstallController struct { - install *types.Install + install types.Install + store store.Store installationManager installation.InstallationManager hostPreflightManager preflight.HostPreflightManager infraManager infra.InfraManager @@ -144,10 +146,14 @@ func WithHostPreflightManager(hostPreflightManager preflight.HostPreflightManage } } -func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { - controller := &InstallController{ - install: types.NewInstall(), +func WithInfraManager(infraManager infra.InfraManager) InstallControllerOption { + return func(c *InstallController) { + c.infraManager = infraManager } +} + +func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { + controller := &InstallController{} for _, opt := range opts { opt(controller) @@ -171,10 +177,14 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, controller.netUtils = utils.NewNetUtils() } + if controller.store == nil { + controller.store = store.NewMemoryStore() + } + if controller.installationManager == nil { controller.installationManager = installation.NewInstallationManager( installation.WithLogger(controller.logger), - installation.WithInstallation(controller.install.Steps.Installation), + installation.WithInstallationStore(controller.store.InstallationStore()), installation.WithLicenseFile(controller.licenseFile), installation.WithAirgapBundle(controller.airgapBundle), installation.WithHostUtils(controller.hostUtils), @@ -186,7 +196,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, controller.hostPreflightManager = preflight.NewHostPreflightManager( preflight.WithLogger(controller.logger), preflight.WithMetricsReporter(controller.metricsReporter), - preflight.WithHostPreflightStore(preflight.NewMemoryStore(controller.install.Steps.HostPreflight)), + preflight.WithHostPreflightStore(controller.store.PreflightStore()), preflight.WithNetUtils(controller.netUtils), ) } @@ -194,7 +204,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, if controller.infraManager == nil { controller.infraManager = infra.NewInfraManager( infra.WithLogger(controller.logger), - infra.WithInfra(controller.install.Steps.Infra), + infra.WithInfraStore(controller.store.InfraStore()), infra.WithPassword(controller.password), infra.WithTLSConfig(controller.tlsConfig), infra.WithLicenseFile(controller.licenseFile), diff --git a/api/controllers/install/controller_test.go b/api/controllers/install/controller_test.go index 1d5f592d9..fd426d721 100644 --- a/api/controllers/install/controller_test.go +++ b/api/controllers/install/controller_test.go @@ -24,24 +24,24 @@ func TestGetInstallationConfig(t *testing.T) { name string setupMock func(*installation.MockInstallationManager) expectedErr bool - expectedValue *types.InstallationConfig + expectedValue types.InstallationConfig }{ { name: "successful get", setupMock: func(m *installation.MockInstallationManager) { - config := &types.InstallationConfig{ + config := types.InstallationConfig{ AdminConsolePort: 9000, GlobalCIDR: "10.0.0.1/16", } mock.InOrder( m.On("GetConfig").Return(config, nil), - m.On("SetConfigDefaults", config).Return(nil), + m.On("SetConfigDefaults", &config).Return(nil), m.On("ValidateConfig", config, 9001).Return(nil), ) }, expectedErr: false, - expectedValue: &types.InstallationConfig{ + expectedValue: types.InstallationConfig{ AdminConsolePort: 9000, GlobalCIDR: "10.0.0.1/16", }, @@ -52,32 +52,32 @@ func TestGetInstallationConfig(t *testing.T) { m.On("GetConfig").Return(nil, errors.New("read error")) }, expectedErr: true, - expectedValue: nil, + expectedValue: types.InstallationConfig{}, }, { name: "set defaults error", setupMock: func(m *installation.MockInstallationManager) { - config := &types.InstallationConfig{} + config := types.InstallationConfig{} mock.InOrder( m.On("GetConfig").Return(config, nil), - m.On("SetConfigDefaults", config).Return(errors.New("defaults error")), + m.On("SetConfigDefaults", &config).Return(errors.New("defaults error")), ) }, expectedErr: true, - expectedValue: nil, + expectedValue: types.InstallationConfig{}, }, { name: "validate error", setupMock: func(m *installation.MockInstallationManager) { - config := &types.InstallationConfig{} + config := types.InstallationConfig{} mock.InOrder( m.On("GetConfig").Return(config, nil), - m.On("SetConfigDefaults", config).Return(nil), + m.On("SetConfigDefaults", &config).Return(nil), m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")), ) }, expectedErr: true, - expectedValue: nil, + expectedValue: types.InstallationConfig{}, }, } @@ -100,7 +100,7 @@ func TestGetInstallationConfig(t *testing.T) { if tt.expectedErr { assert.Error(t, err) - assert.Nil(t, result) + assert.Equal(t, types.InstallationConfig{}, result) } else { assert.NoError(t, err) assert.Equal(t, tt.expectedValue, result) @@ -114,20 +114,20 @@ func TestGetInstallationConfig(t *testing.T) { func TestConfigureInstallation(t *testing.T) { tests := []struct { name string - config *types.InstallationConfig - setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, *types.InstallationConfig) + config types.InstallationConfig + setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, types.InstallationConfig) expectedErr bool }{ { name: "successful configure installation", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ LocalArtifactMirrorPort: 9000, DataDirectory: t.TempDir(), }, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), - m.On("SetConfig", *config).Return(nil), + m.On("SetConfig", config).Return(nil), m.On("ConfigureHost", t.Context(), rc).Return(nil), ) }, @@ -135,32 +135,32 @@ func TestConfigureInstallation(t *testing.T) { }, { name: "validate error", - config: &types.InstallationConfig{}, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { + config: types.InstallationConfig{}, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")) }, expectedErr: true, }, { name: "set config error", - config: &types.InstallationConfig{}, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { + config: types.InstallationConfig{}, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), - m.On("SetConfig", *config).Return(errors.New("set config error")), + m.On("SetConfig", config).Return(errors.New("set config error")), ) }, expectedErr: true, }, { name: "with global CIDR", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", DataDirectory: t.TempDir(), }, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config *types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { // Create a copy with expected CIDR values after computation - configWithCIDRs := *config + configWithCIDRs := config configWithCIDRs.PodCIDR = "10.0.0.0/17" configWithCIDRs.ServiceCIDR = "10.0.128.0/17" @@ -182,10 +182,7 @@ func TestConfigureInstallation(t *testing.T) { mockManager := &installation.MockInstallationManager{} - // Create a copy of the config to avoid modifying the original - configCopy := *tt.config - - tt.setupMock(mockManager, rc, &configCopy) + tt.setupMock(mockManager, rc, tt.config) controller, err := NewInstallController( WithRuntimeConfig(rc), @@ -247,11 +244,11 @@ func TestIntegrationComputeCIDRs(t *testing.T) { controller, err := NewInstallController() require.NoError(t, err) - config := &types.InstallationConfig{ + config := types.InstallationConfig{ GlobalCIDR: tt.globalCIDR, } - err = controller.computeCIDRs(config) + err = controller.computeCIDRs(&config) if tt.expectedErr { assert.Error(t, err) @@ -352,18 +349,18 @@ func TestGetHostPreflightStatus(t *testing.T) { name string setupMock func(*preflight.MockHostPreflightManager) expectedErr bool - expectedValue *types.Status + expectedValue types.Status }{ { name: "successful get status", setupMock: func(m *preflight.MockHostPreflightManager) { - status := &types.Status{ + status := types.Status{ State: types.StateFailed, } m.On("GetHostPreflightStatus", t.Context()).Return(status, nil) }, expectedErr: false, - expectedValue: &types.Status{ + expectedValue: types.Status{ State: types.StateFailed, }, }, @@ -373,7 +370,7 @@ func TestGetHostPreflightStatus(t *testing.T) { m.On("GetHostPreflightStatus", t.Context()).Return(nil, errors.New("get status error")) }, expectedErr: true, - expectedValue: nil, + expectedValue: types.Status{}, }, } @@ -389,7 +386,7 @@ func TestGetHostPreflightStatus(t *testing.T) { if tt.expectedErr { assert.Error(t, err) - assert.Nil(t, result) + assert.Equal(t, types.Status{}, result) } else { assert.NoError(t, err) assert.Equal(t, tt.expectedValue, result) @@ -517,18 +514,18 @@ func TestGetInstallationStatus(t *testing.T) { name string setupMock func(*installation.MockInstallationManager) expectedErr bool - expectedValue *types.Status + expectedValue types.Status }{ { name: "successful get status", setupMock: func(m *installation.MockInstallationManager) { - status := &types.Status{ + status := types.Status{ State: types.StateRunning, } m.On("GetStatus").Return(status, nil) }, expectedErr: false, - expectedValue: &types.Status{ + expectedValue: types.Status{ State: types.StateRunning, }, }, @@ -538,7 +535,7 @@ func TestGetInstallationStatus(t *testing.T) { m.On("GetStatus").Return(nil, errors.New("get status error")) }, expectedErr: true, - expectedValue: nil, + expectedValue: types.Status{}, }, } @@ -554,7 +551,7 @@ func TestGetInstallationStatus(t *testing.T) { if tt.expectedErr { assert.Error(t, err) - assert.Nil(t, result) + assert.Equal(t, types.Status{}, result) } else { assert.NoError(t, err) assert.Equal(t, tt.expectedValue, result) @@ -574,7 +571,7 @@ func TestSetupInfra(t *testing.T) { { name: "successful setup with passed preflights", setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := &types.Status{ + preflightStatus := types.Status{ State: types.StateSucceeded, } mock.InOrder( @@ -587,7 +584,7 @@ func TestSetupInfra(t *testing.T) { { name: "successful setup with failed preflights", setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := &types.Status{ + preflightStatus := types.Status{ State: types.StateFailed, } preflightOutput := &types.HostPreflightsOutput{ @@ -617,7 +614,7 @@ func TestSetupInfra(t *testing.T) { { name: "preflight not completed", setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := &types.Status{ + preflightStatus := types.Status{ State: types.StateRunning, } pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil) @@ -627,7 +624,7 @@ func TestSetupInfra(t *testing.T) { { name: "preflight output error", setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := &types.Status{ + preflightStatus := types.Status{ State: types.StateFailed, } mock.InOrder( @@ -640,7 +637,7 @@ func TestSetupInfra(t *testing.T) { { name: "install infra error", setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := &types.Status{ + preflightStatus := types.Status{ State: types.StateSucceeded, } mock.InOrder( @@ -694,37 +691,37 @@ func TestGetInfra(t *testing.T) { name string setupMock func(*infra.MockInfraManager) expectedErr bool - expectedValue *types.Infra + expectedValue types.Infra }{ { name: "successful get infra", setupMock: func(m *infra.MockInfraManager) { - infra := &types.Infra{ + infra := types.Infra{ Components: []types.InfraComponent{ { Name: infra.K0sComponentName, - Status: &types.Status{ + Status: types.Status{ State: types.StateRunning, }, }, }, - Status: &types.Status{ + Status: types.Status{ State: types.StateRunning, }, } m.On("Get").Return(infra, nil) }, expectedErr: false, - expectedValue: &types.Infra{ + expectedValue: types.Infra{ Components: []types.InfraComponent{ { Name: infra.K0sComponentName, - Status: &types.Status{ + Status: types.Status{ State: types.StateRunning, }, }, }, - Status: &types.Status{ + Status: types.Status{ State: types.StateRunning, }, }, @@ -735,7 +732,7 @@ func TestGetInfra(t *testing.T) { m.On("Get").Return(nil, errors.New("get infra error")) }, expectedErr: true, - expectedValue: nil, + expectedValue: types.Infra{}, }, } @@ -751,7 +748,7 @@ func TestGetInfra(t *testing.T) { if tt.expectedErr { assert.Error(t, err) - assert.Nil(t, result) + assert.Equal(t, types.Infra{}, result) } else { assert.NoError(t, err) assert.Equal(t, tt.expectedValue, result) @@ -765,24 +762,24 @@ func TestGetInfra(t *testing.T) { func TestGetStatus(t *testing.T) { tests := []struct { name string - install *types.Install - expectedValue *types.Status + install types.Install + expectedValue types.Status }{ { name: "successful get status", - install: &types.Install{ - Status: &types.Status{ + install: types.Install{ + Status: types.Status{ State: types.StateFailed, }, }, - expectedValue: &types.Status{ + expectedValue: types.Status{ State: types.StateFailed, }, }, { - name: "nil status", - install: &types.Install{}, - expectedValue: nil, + name: "empty status", + install: types.Install{}, + expectedValue: types.Status{}, }, } @@ -803,19 +800,19 @@ func TestGetStatus(t *testing.T) { func TestSetStatus(t *testing.T) { tests := []struct { name string - status *types.Status + status types.Status expectedErr bool }{ { name: "successful set status", - status: &types.Status{ + status: types.Status{ State: types.StateFailed, }, expectedErr: false, }, { name: "nil status", - status: nil, + status: types.Status{}, expectedErr: false, }, } @@ -844,12 +841,6 @@ func getTestReleaseData() *release.ReleaseData { } } -func WithInfraManager(infraManager infra.InfraManager) InstallControllerOption { - return func(c *InstallController) { - c.infraManager = infraManager - } -} - type testEnvSetter struct { env map[string]string } diff --git a/api/controllers/install/hostpreflight.go b/api/controllers/install/hostpreflight.go index 835cae777..dc46a4d50 100644 --- a/api/controllers/install/hostpreflight.go +++ b/api/controllers/install/hostpreflight.go @@ -33,7 +33,7 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP }) } -func (c *InstallController) GetHostPreflightStatus(ctx context.Context) (*types.Status, error) { +func (c *InstallController) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { return c.hostPreflightManager.GetHostPreflightStatus(ctx) } diff --git a/api/controllers/install/infra.go b/api/controllers/install/infra.go index 92bc09ae7..2acb4297a 100644 --- a/api/controllers/install/infra.go +++ b/api/controllers/install/infra.go @@ -34,6 +34,6 @@ func (c *InstallController) SetupInfra(ctx context.Context) error { return nil } -func (c *InstallController) GetInfra(ctx context.Context) (*types.Infra, error) { +func (c *InstallController) GetInfra(ctx context.Context) (types.Infra, error) { return c.infraManager.Get() } diff --git a/api/controllers/install/installation.go b/api/controllers/install/installation.go index 8e950bf66..1902a03f2 100644 --- a/api/controllers/install/installation.go +++ b/api/controllers/install/installation.go @@ -10,37 +10,33 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/netutils" ) -func (c *InstallController) GetInstallationConfig(ctx context.Context) (*types.InstallationConfig, error) { +func (c *InstallController) GetInstallationConfig(ctx context.Context) (types.InstallationConfig, error) { config, err := c.installationManager.GetConfig() if err != nil { - return nil, err + return types.InstallationConfig{}, err } - if config == nil { - return nil, fmt.Errorf("installation config is nil") - } - - if err := c.installationManager.SetConfigDefaults(config); err != nil { - return nil, fmt.Errorf("set defaults: %w", err) + if err := c.installationManager.SetConfigDefaults(&config); err != nil { + return types.InstallationConfig{}, fmt.Errorf("set defaults: %w", err) } if err := c.installationManager.ValidateConfig(config, c.rc.ManagerPort()); err != nil { - return nil, fmt.Errorf("validate: %w", err) + return types.InstallationConfig{}, fmt.Errorf("validate: %w", err) } return config, nil } -func (c *InstallController) ConfigureInstallation(ctx context.Context, config *types.InstallationConfig) error { +func (c *InstallController) ConfigureInstallation(ctx context.Context, config types.InstallationConfig) error { if err := c.installationManager.ValidateConfig(config, c.rc.ManagerPort()); err != nil { return fmt.Errorf("validate: %w", err) } - if err := c.computeCIDRs(config); err != nil { + if err := c.computeCIDRs(&config); err != nil { return fmt.Errorf("compute cidrs: %w", err) } - if err := c.installationManager.SetConfig(*config); err != nil { + if err := c.installationManager.SetConfig(config); err != nil { return fmt.Errorf("write: %w", err) } @@ -90,6 +86,6 @@ func (c *InstallController) computeCIDRs(config *types.InstallationConfig) error return nil } -func (c *InstallController) GetInstallationStatus(ctx context.Context) (*types.Status, error) { +func (c *InstallController) GetInstallationStatus(ctx context.Context) (types.Status, error) { return c.installationManager.GetStatus() } diff --git a/api/controllers/install/status.go b/api/controllers/install/status.go index f8359e3ae..442ee8aea 100644 --- a/api/controllers/install/status.go +++ b/api/controllers/install/status.go @@ -6,13 +6,13 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -func (c *InstallController) SetStatus(ctx context.Context, status *types.Status) error { +func (c *InstallController) SetStatus(ctx context.Context, status types.Status) error { c.mu.Lock() defer c.mu.Unlock() c.install.Status = status return nil } -func (c *InstallController) GetStatus(ctx context.Context) (*types.Status, error) { +func (c *InstallController) GetStatus(ctx context.Context) (types.Status, error) { return c.install.Status, nil } diff --git a/api/install.go b/api/install.go index f9dedc01c..c55819443 100644 --- a/api/install.go +++ b/api/install.go @@ -44,7 +44,7 @@ func (a *API) postInstallConfigureInstallation(w http.ResponseWriter, r *http.Re return } - if err := a.installController.ConfigureInstallation(r.Context(), &config); err != nil { + if err := a.installController.ConfigureInstallation(r.Context(), config); err != nil { a.logError(r, err, "failed to set installation config") a.jsonError(w, r, err) return @@ -199,13 +199,13 @@ func (a *API) setInstallStatus(w http.ResponseWriter, r *http.Request) { return } - if err := types.ValidateStatus(&status); err != nil { + if err := types.ValidateStatus(status); err != nil { a.logError(r, err, "invalid install status") a.jsonError(w, r, err) return } - if err := a.installController.SetStatus(r.Context(), &status); err != nil { + if err := a.installController.SetStatus(r.Context(), status); err != nil { a.logError(r, err, "failed to set install status") a.jsonError(w, r, err) return diff --git a/api/integration/assets/license.yaml b/api/integration/assets/license.yaml new file mode 100644 index 000000000..ec35c3b0d --- /dev/null +++ b/api/integration/assets/license.yaml @@ -0,0 +1,37 @@ +apiVersion: kots.io/v1beta1 +kind: License +metadata: + name: dryrun-install +spec: + appSlug: fake-app-slug + channelID: fake-channel-id + channelName: fake-channel-name + channels: + - channelID: fake-channel-id + channelName: fake-channel-name + channelSlug: fake-channel-slug + endpoint: https://fake-endpoint.com + isDefault: true + replicatedProxyDomain: fake-replicated-proxy.test.net + customerEmail: salah@replicated.com + customerName: Salah EC Dev + endpoint: https://fake-endpoint.com + entitlements: + expires_at: + description: License Expiration + signature: {} + title: Expiration + value: "" + valueType: String + isDisasterRecoverySupported: true + isEmbeddedClusterDownloadEnabled: true + isEmbeddedClusterMultiNodeEnabled: true + isKotsInstallEnabled: true + isNewKotsUiEnabled: true + isSnapshotSupported: true + isSupportBundleUploadSupported: true + licenseID: fake-license-id + licenseSequence: 4 + licenseType: dev + replicatedProxyDomain: fake-replicated-proxy.test.net + signature: ZmFrZS1zaWduYXR1cmU= diff --git a/api/integration/hostpreflights_test.go b/api/integration/hostpreflights_test.go index 8b001e3d1..7cd868b7b 100644 --- a/api/integration/hostpreflights_test.go +++ b/api/integration/hostpreflights_test.go @@ -13,6 +13,8 @@ import ( "github.com/replicatedhq/embedded-cluster/api/controllers/install" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" + installationstore "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" + preflightstore "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" @@ -48,7 +50,7 @@ func TestGetHostPreflightsStatus(t *testing.T) { "Some Preflight", "Another Preflight", }, - Status: &types.Status{ + Status: types.Status{ State: types.StateFailed, Description: "A preflight failed", }, @@ -56,7 +58,9 @@ func TestGetHostPreflightsStatus(t *testing.T) { runner := &preflights.MockPreflightRunner{} // Create a host preflights manager manager := preflight.NewHostPreflightManager( - preflight.WithHostPreflightStore(preflight.NewMemoryStore(&hpf)), + preflight.WithHostPreflightStore( + preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf)), + ), preflight.WithPreflightRunner(runner), ) // Create an install controller @@ -172,7 +176,7 @@ func TestPostRunHostPreflights(t *testing.T) { runner := &preflights.MockPreflightRunner{} // Creeate the installation struct - inst := types.NewInstallation() + inst := types.Installation{} // Create a host preflights manager with the mock runner pfManager := preflight.NewHostPreflightManager( @@ -181,7 +185,7 @@ func TestPostRunHostPreflights(t *testing.T) { // Create an installation manager iManager := installation.NewInstallationManager( - installation.WithInstallationStore(installation.NewMemoryStore(inst)), + installation.WithInstallationStore(installationstore.NewMemoryStore(installationstore.WithInstallation(inst))), ) // Create an install controller with the mocked manager @@ -261,7 +265,7 @@ func TestPostRunHostPreflights(t *testing.T) { require.NoError(t, err) // The state should eventually be set to succeeded in a goroutine - var preflightsStatus *types.Status + var preflightsStatus types.Status if !assert.Eventually(t, func() bool { preflightsStatus, err = installController.GetHostPreflightStatus(t.Context()) require.NoError(t, err, "GetHostPreflightStatus should succeed") @@ -439,7 +443,7 @@ func TestPostRunHostPreflights(t *testing.T) { require.NoError(t, err) // The state should eventually be set to failed in a goroutine - var preflightsStatus *types.Status + var preflightsStatus types.Status if !assert.Eventually(t, func() bool { preflightsStatus, err = installController.GetHostPreflightStatus(t.Context()) require.NoError(t, err, "GetHostPreflightStatus should succeed") @@ -456,13 +460,13 @@ func TestPostRunHostPreflights(t *testing.T) { // Test we get a conflict error if preflights are already running t.Run("Preflights already running errror", func(t *testing.T) { // Create a host preflights manager with the failing mock runner - hp := types.NewHostPreflights() - hp.Status = &types.Status{ + hp := types.HostPreflights{} + hp.Status = types.Status{ State: types.StateRunning, Description: "Preflights running", } manager := preflight.NewHostPreflightManager( - preflight.WithHostPreflightStore(preflight.NewMemoryStore(hp)), + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hp))), ) // Create an install controller with the failing manager diff --git a/api/integration/install_test.go b/api/integration/install_test.go index 5280d4176..b9913d361 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -3,28 +3,53 @@ package integration import ( "bytes" "context" + _ "embed" "encoding/json" + "errors" "net" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" "time" "github.com/gorilla/mux" + k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "github.com/replicatedhq/embedded-cluster/api" - "github.com/replicatedhq/embedded-cluster/api/client" + apiclient "github.com/replicatedhq/embedded-cluster/api/client" "github.com/replicatedhq/embedded-cluster/api/controllers/install" + "github.com/replicatedhq/embedded-cluster/api/internal/managers/infra" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" + preflightstore "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" + "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" + "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" + "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + metadatafake "k8s.io/client-go/metadata/fake" + client "sigs.k8s.io/controller-runtime/pkg/client" + clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" +) + +var ( + //go:embed assets/license.yaml + licenseData string ) // Mock implementation of the install.Controller interface @@ -41,33 +66,33 @@ type mockInstallController struct { readStatusError error } -func (m *mockInstallController) GetInstallationConfig(ctx context.Context) (*types.InstallationConfig, error) { +func (m *mockInstallController) GetInstallationConfig(ctx context.Context) (types.InstallationConfig, error) { if m.getInstallationConfigError != nil { - return nil, m.getInstallationConfigError + return types.InstallationConfig{}, m.getInstallationConfigError } - return &types.InstallationConfig{}, nil + return types.InstallationConfig{}, nil } -func (m *mockInstallController) ConfigureInstallation(ctx context.Context, config *types.InstallationConfig) error { +func (m *mockInstallController) ConfigureInstallation(ctx context.Context, config types.InstallationConfig) error { return m.configureInstallationError } -func (m *mockInstallController) GetInstallationStatus(ctx context.Context) (*types.Status, error) { +func (m *mockInstallController) GetInstallationStatus(ctx context.Context) (types.Status, error) { if m.readStatusError != nil { - return nil, m.readStatusError + return types.Status{}, m.readStatusError } - return &types.Status{}, nil + return types.Status{}, nil } func (m *mockInstallController) RunHostPreflights(ctx context.Context, opts install.RunHostPreflightsOptions) error { return m.runHostPreflightsError } -func (m *mockInstallController) GetHostPreflightStatus(ctx context.Context) (*types.Status, error) { +func (m *mockInstallController) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { if m.getHostPreflightStatusError != nil { - return nil, m.getHostPreflightStatusError + return types.Status{}, m.getHostPreflightStatusError } - return &types.Status{}, nil + return types.Status{}, nil } func (m *mockInstallController) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) { @@ -88,19 +113,19 @@ func (m *mockInstallController) SetupInfra(ctx context.Context) error { return m.setupInfraError } -func (m *mockInstallController) GetInfra(ctx context.Context) (*types.Infra, error) { +func (m *mockInstallController) GetInfra(ctx context.Context) (types.Infra, error) { if m.getInfraError != nil { - return nil, m.getInfraError + return types.Infra{}, m.getInfraError } - return &types.Infra{}, nil + return types.Infra{}, nil } -func (m *mockInstallController) SetStatus(ctx context.Context, status *types.Status) error { +func (m *mockInstallController) SetStatus(ctx context.Context, status types.Status) error { return m.setStatusError } -func (m *mockInstallController) GetStatus(ctx context.Context) (*types.Status, error) { - return nil, m.readStatusError +func (m *mockInstallController) GetStatus(ctx context.Context) (types.Status, error) { + return types.Status{}, m.readStatusError } func TestConfigureInstallation(t *testing.T) { @@ -673,7 +698,7 @@ func TestGetInstallStatus(t *testing.T) { State: types.StatePending, Description: "Installation in progress", } - err = installController.SetStatus(t.Context(), &initialStatus) + err = installController.SetStatus(t.Context(), initialStatus) require.NoError(t, err) // Create the API with the install controller @@ -980,7 +1005,7 @@ func TestInstallWithAPIClient(t *testing.T) { defer server.Close() // Create client with the predefined token - c := client.New(server.URL, client.WithToken("TOKEN")) + c := apiclient.New(server.URL, apiclient.WithToken("TOKEN")) require.NoError(t, err, "API client login should succeed") // Test GetInstallationConfig @@ -1018,12 +1043,12 @@ func TestInstallWithAPIClient(t *testing.T) { } // Configure the installation using the client - status, err := c.ConfigureInstallation(&config) + status, err := c.ConfigureInstallation(config) require.NoError(t, err, "ConfigureInstallation should succeed with valid config") assert.NotNil(t, status, "Status should not be nil") // Verify the status was set correctly - var installStatus *types.Status + var installStatus types.Status if !assert.Eventually(t, func() bool { installStatus, err = c.GetInstallationStatus() require.NoError(t, err, "GetInstallationStatus should succeed") @@ -1047,7 +1072,7 @@ func TestInstallWithAPIClient(t *testing.T) { // Test ConfigureInstallation validation error t.Run("ConfigureInstallation validation error", func(t *testing.T) { // Create an invalid config (port conflict) - config := &types.InstallationConfig{ + config := types.InstallationConfig{ DataDirectory: "/tmp/new-dir", AdminConsolePort: 8080, LocalArtifactMirrorPort: 8080, // Same as AdminConsolePort @@ -1074,7 +1099,7 @@ func TestInstallWithAPIClient(t *testing.T) { // Test SetInstallStatus t.Run("SetInstallStatus", func(t *testing.T) { // Create a status - status := &types.Status{ + status := types.Status{ State: types.StateFailed, Description: "Installation failed", } @@ -1087,6 +1112,526 @@ func TestInstallWithAPIClient(t *testing.T) { }) } +// Test the setupInfra endpoint runs infrastructure setup correctly +func TestPostSetupInfra(t *testing.T) { + // Create schemes + scheme := runtime.NewScheme() + require.NoError(t, ecv1beta1.AddToScheme(scheme)) + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, apiextensionsv1.AddToScheme(scheme)) + + metascheme := metadatafake.NewTestScheme() + require.NoError(t, metav1.AddMetaToScheme(metascheme)) + require.NoError(t, corev1.AddToScheme(metascheme)) + + t.Run("Success", func(t *testing.T) { + // Create mocks + k0sMock := &k0s.MockK0s{} + helmMock := &helm.MockClient{} + hostutilsMock := &hostutils.MockHostUtils{} + fakeKcli := clientfake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(testControllerNode(t)). + WithStatusSubresource(&ecv1beta1.Installation{}, &apiextensionsv1.CustomResourceDefinition{}). + WithInterceptorFuncs(testInterceptorFuncs(t)). + Build() + fakeMcli := metadatafake.NewSimpleMetadataClient(metascheme) + + // Create a runtime config + rc := runtimeconfig.New(nil) + rc.SetDataDir(t.TempDir()) + rc.SetNetworkSpec(ecv1beta1.NetworkSpec{ + NetworkInterface: "eth0", + ServiceCIDR: "10.96.0.0/12", + PodCIDR: "10.244.0.0/16", + }) + + // Create host preflights with successful status + hpf := types.HostPreflights{} + hpf.Status = types.Status{ + State: types.StateSucceeded, + Description: "Host preflights succeeded", + } + + // Create host preflights manager + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + + // Create license file + licenseFile := filepath.Join(t.TempDir(), "license.yaml") + require.NoError(t, os.WriteFile(licenseFile, []byte(licenseData), 0644)) + + // Create infra manager with mocks + infraManager := infra.NewInfraManager( + infra.WithK0s(k0sMock), + infra.WithKubeClient(fakeKcli), + infra.WithMetadataClient(fakeMcli), + infra.WithHelmClient(helmMock), + infra.WithLicenseFile(licenseFile), + infra.WithHostUtils(hostutilsMock), + infra.WithKotsInstaller(func() error { + return nil + }), + infra.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{ + DefaultDomains: release.Domains{ + ReplicatedAppDomain: "replicated.example.com", + ProxyRegistryDomain: "some-proxy.example.com", + }, + }, + }), + ) + + // Setup mock expectations + k0sConfig := &k0sv1beta1.ClusterConfig{ + Spec: &k0sv1beta1.ClusterSpec{ + Network: &k0sv1beta1.Network{ + PodCIDR: "10.244.0.0/16", + ServiceCIDR: "10.96.0.0/12", + }, + }, + } + 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), + hostutilsMock.On("CreateSystemdUnitFiles", mock.Anything, mock.Anything, rc, false).Return(nil), + k0sMock.On("Install", rc).Return(nil), + k0sMock.On("WaitForK0s").Return(nil), + hostutilsMock.On("AddInsecureRegistry", mock.Anything).Return(nil), + helmMock.On("Install", mock.Anything, mock.Anything).Times(4).Return(nil, nil), // 4 addons + helmMock.On("Close").Return(nil), + ) + + // Create an install controller with the mocked managers + installController, err := install.NewInstallController( + install.WithHostPreflightManager(pfManager), + install.WithInfraManager(infraManager), + install.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{ + DefaultDomains: release.Domains{ + ReplicatedAppDomain: "replicated.example.com", + ProxyRegistryDomain: "some-proxy.example.com", + }, + }, + }), + install.WithRuntimeConfig(rc), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + req.Header.Set("Authorization", "Bearer TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusOK, rec.Code) + + t.Logf("Response body: %s", rec.Body.String()) + + // Parse the response body + var infra types.Infra + err = json.NewDecoder(rec.Body).Decode(&infra) + require.NoError(t, err) + + // Verify that the status is not pending. We cannot check for an end state here because the hots config is async + // so the state might have moved from running to a final state before we get the response. + assert.NotEqual(t, types.StatePending, infra.Status.State) + + // Helper function to get infra status + getInfraStatus := func() types.Infra { + // Create a request to get infra status + req := httptest.NewRequest(http.MethodGet, "/install/infra/status", nil) + req.Header.Set("Authorization", "Bearer TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusOK, rec.Code) + + // Parse the response body + var infra types.Infra + err = json.NewDecoder(rec.Body).Decode(&infra) + require.NoError(t, err) + + // Log the infra status + t.Logf("Infra Status: %s, Description: %s", infra.Status.State, infra.Status.Description) + + return infra + } + + // The status should eventually be set to succeeded in a goroutine + assert.Eventually(t, func() bool { + infra := getInfraStatus() + + // Fail the test if the status is Failed + if infra.Status.State == types.StateFailed { + t.Fatalf("Infrastructure setup failed: %s", infra.Status.Description) + } + + return infra.Status.State == types.StateSucceeded + }, 30*time.Second, 500*time.Millisecond, "Infrastructure setup did not succeed in time") + + // Verify that the mock expectations were met + k0sMock.AssertExpectations(t) + hostutilsMock.AssertExpectations(t) + helmMock.AssertExpectations(t) + + // Verify installation was created + gotInst, err := kubeutils.GetLatestInstallation(t.Context(), fakeKcli) + require.NoError(t, err) + assert.NotNil(t, gotInst) + assert.Equal(t, ecv1beta1.InstallationStateInstalled, gotInst.Status.State) + + // Verify version metadata configmap was created + var gotConfigmap corev1.ConfigMap + err = fakeKcli.Get(t.Context(), client.ObjectKey{Namespace: "embedded-cluster", Name: "version-metadata-0-0-0"}, &gotConfigmap) + require.NoError(t, err) + + // Verify kotsadm namespace and kotsadm-password secret were created + var gotKotsadmNamespace corev1.Namespace + err = fakeKcli.Get(t.Context(), client.ObjectKey{Name: runtimeconfig.KotsadmNamespace}, &gotKotsadmNamespace) + require.NoError(t, err) + + var gotKotsadmPasswordSecret corev1.Secret + err = fakeKcli.Get(t.Context(), client.ObjectKey{Namespace: runtimeconfig.KotsadmNamespace, Name: "kotsadm-password"}, &gotKotsadmPasswordSecret) + require.NoError(t, err) + assert.NotEmpty(t, gotKotsadmPasswordSecret.Data["passwordBcrypt"]) + + // Get infra status again and verify more details + infra = getInfraStatus() + assert.Contains(t, infra.Logs, "[k0s]") + assert.Contains(t, infra.Logs, "[metadata]") + assert.Contains(t, infra.Logs, "[addons]") + assert.Contains(t, infra.Logs, "[extensions]") + assert.Len(t, infra.Components, 6) + }) + + // Test authorization + t.Run("Authorization error", func(t *testing.T) { + // Create the API + apiInstance, err := api.New( + "password", + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + req.Header.Set("Authorization", "Bearer NOT_A_TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusUnauthorized, rec.Code) + assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) + + // Parse the response body + var apiError types.APIError + err = json.NewDecoder(rec.Body).Decode(&apiError) + require.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, apiError.StatusCode) + }) + + // Test preflight checks not completed + t.Run("Preflight checks not completed", func(t *testing.T) { + // Create host preflights with running status (not completed) + hpf := types.HostPreflights{} + hpf.Status = types.Status{ + State: types.StateRunning, + Description: "Host preflights running", + } + + // Create managers + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + + // Create an install controller + installController, err := install.NewInstallController( + install.WithHostPreflightManager(pfManager), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + req.Header.Set("Authorization", "Bearer TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusInternalServerError, rec.Code) + + t.Logf("Response body: %s", rec.Body.String()) + + // Parse the response body + var apiError types.APIError + err = json.NewDecoder(rec.Body).Decode(&apiError) + require.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, apiError.StatusCode) + assert.Contains(t, apiError.Message, "host preflight checks did not complete") + }) + + // Test k0s already installed error + t.Run("K0s already installed", func(t *testing.T) { + // Create mocks + k0sMock := &k0s.MockK0s{} + + // Create a runtime config + rc := runtimeconfig.New(nil) + rc.SetDataDir(t.TempDir()) + rc.SetNetworkSpec(ecv1beta1.NetworkSpec{ + NetworkInterface: "eth0", + }) + + // Create host preflights with successful status + hpf := types.HostPreflights{} + hpf.Status = types.Status{ + State: types.StateSucceeded, + Description: "Host preflights succeeded", + } + + // Create managers + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + infraManager := infra.NewInfraManager( + infra.WithK0s(k0sMock), + ) + + // Setup k0s mock to return already installed + k0sMock.On("IsInstalled").Return(true, nil) + + // Create an install controller + installController, err := install.NewInstallController( + install.WithHostPreflightManager(pfManager), + install.WithInfraManager(infraManager), + install.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{}, + }), + install.WithRuntimeConfig(rc), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + req.Header.Set("Authorization", "Bearer TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusInternalServerError, rec.Code) + assert.Contains(t, rec.Body.String(), "installation is detected") + + // Verify that the mock expectations were met + k0sMock.AssertExpectations(t) + }) + + // Test k0s install error + t.Run("K0s install error", func(t *testing.T) { + // Create mocks + k0sMock := &k0s.MockK0s{} + hostutilsMock := &hostutils.MockHostUtils{} + + // Create a runtime config + rc := runtimeconfig.New(nil) + rc.SetDataDir(t.TempDir()) + rc.SetNetworkSpec(ecv1beta1.NetworkSpec{ + NetworkInterface: "eth0", + ServiceCIDR: "10.96.0.0/12", + PodCIDR: "10.244.0.0/16", + }) + + // Create host preflights with successful status + hpf := types.HostPreflights{} + hpf.Status = types.Status{ + State: types.StateSucceeded, + Description: "Host preflights succeeded", + } + + // Create license file + licenseFile := filepath.Join(t.TempDir(), "license.yaml") + require.NoError(t, os.WriteFile(licenseFile, []byte(licenseData), 0644)) + + // Create managers + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + infraManager := infra.NewInfraManager( + infra.WithK0s(k0sMock), + infra.WithHostUtils(hostutilsMock), + infra.WithLicenseFile(licenseFile), + ) + + // Setup k0s mock expectations with failure + 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), + hostutilsMock.On("CreateSystemdUnitFiles", mock.Anything, mock.Anything, rc, false).Return(nil), + k0sMock.On("Install", mock.Anything).Return(errors.New("failed to install k0s")), + ) + + // Create an install controller + installController, err := install.NewInstallController( + install.WithHostPreflightManager(pfManager), + install.WithInfraManager(infraManager), + install.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{}, + }), + install.WithRuntimeConfig(rc), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + req.Header.Set("Authorization", "Bearer TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusOK, rec.Code) + + // The status should eventually be set to failed due to k0s install error + assert.Eventually(t, func() bool { + // Create a request to get infra status + req := httptest.NewRequest(http.MethodGet, "/install/infra/status", nil) + req.Header.Set("Authorization", "Bearer TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + assert.Equal(t, http.StatusOK, rec.Code) + + // Parse the response body + var infra types.Infra + err = json.NewDecoder(rec.Body).Decode(&infra) + require.NoError(t, err) + + t.Logf("Infra Status: %s, Description: %s", infra.Status.State, infra.Status.Description) + return infra.Status.State == types.StateFailed && strings.Contains(infra.Status.Description, "failed to install k0s") + }, 10*time.Second, 100*time.Millisecond, "Infrastructure setup did not fail in time") + + // Verify that the mock expectations were met + k0sMock.AssertExpectations(t) + hostutilsMock.AssertExpectations(t) + }) +} + +func testControllerNode(t *testing.T) *corev1.Node { + hostname, err := os.Hostname() + require.NoError(t, err) + return &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.ToLower(hostname), + Labels: map[string]string{ + "node-role.kubernetes.io/control-plane": "", + }, + }, + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + }, + }, + }, + } +} + +func testInterceptorFuncs(t *testing.T) interceptor.Funcs { + return interceptor.Funcs{ + Create: func(ctx context.Context, cli client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + if crd, ok := obj.(*apiextensionsv1.CustomResourceDefinition); ok { + err := cli.Create(ctx, obj, opts...) + if err != nil { + return err + } + // Update status to ready after creation + crd.Status.Conditions = []apiextensionsv1.CustomResourceDefinitionCondition{ + {Type: apiextensionsv1.Established, Status: apiextensionsv1.ConditionTrue}, + {Type: apiextensionsv1.NamesAccepted, Status: apiextensionsv1.ConditionTrue}, + } + return cli.Status().Update(ctx, crd) + } + return cli.Create(ctx, obj, opts...) + }, + } +} + type testEnvSetter struct { env map[string]string } diff --git a/api/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index 2cd7e13bf..57b870f5b 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -10,13 +10,10 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" - "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" ecmetadata "github.com/replicatedhq/embedded-cluster/pkg-new/metadata" "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/extensions" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" @@ -43,7 +40,7 @@ func (m *infraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConf m.mu.Lock() defer m.mu.Unlock() - installed, err := k0s.IsInstalled() + installed, err := m.k0scli.IsInstalled() if err != nil { return err } @@ -120,17 +117,17 @@ func (m *infraManager) install(ctx context.Context, license *kotsv1beta1.License return fmt.Errorf("install k0s: %w", err) } - kcli, err := kubeutils.KubeClient() + kcli, err := m.kubeClient() if err != nil { return fmt.Errorf("create kube client: %w", err) } - mcli, err := kubeutils.MetadataClient() + mcli, err := m.metadataClient() if err != nil { return fmt.Errorf("create metadata client: %w", err) } - hcli, err := m.getHelmClient(rc) + hcli, err := m.helmClient(rc) if err != nil { return fmt.Errorf("create helm client: %w", err) } @@ -153,7 +150,7 @@ func (m *infraManager) install(ctx context.Context, license *kotsv1beta1.License return fmt.Errorf("update installation: %w", err) } - if err = support.CreateHostSupportBundle(); err != nil { + if err = support.CreateHostSupportBundle(ctx, kcli); err != nil { m.logger.Warnf("Unable to create host support bundle: %v", err) } @@ -187,27 +184,27 @@ func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeC logFn := m.logFn("k0s") logFn("creating k0s configuration file") - k0sCfg, err := k0s.WriteK0sConfig(ctx, rc.NetworkInterface(), m.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) + k0sCfg, err := m.k0scli.WriteK0sConfig(ctx, rc.NetworkInterface(), m.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) if err != nil { return nil, fmt.Errorf("create config file: %w", err) } logFn("creating systemd unit files") - if err := hostutils.CreateSystemdUnitFiles(ctx, m.logger, rc, false); err != nil { + if err := m.hostUtils.CreateSystemdUnitFiles(ctx, m.logger, rc, false); err != nil { return nil, fmt.Errorf("create systemd unit files: %w", err) } logFn("installing k0s") - if err := k0s.Install(rc); err != nil { + if err := m.k0scli.Install(rc); err != nil { return nil, fmt.Errorf("install cluster: %w", err) } logFn("waiting for k0s to be ready") - if err := k0s.WaitForK0s(); err != nil { + if err := m.k0scli.WaitForK0s(); err != nil { return nil, fmt.Errorf("wait for k0s: %w", err) } - kcli, err := kubeutils.KubeClient() + kcli, err := m.kubeClient() if err != nil { return nil, fmt.Errorf("create kube client: %w", err) } @@ -224,7 +221,7 @@ func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeC if err != nil { return nil, fmt.Errorf("get registry cluster IP: %w", err) } - if err := airgap.AddInsecureRegistry(fmt.Sprintf("%s:5000", registryIP)); err != nil { + if err := m.hostUtils.AddInsecureRegistry(fmt.Sprintf("%s:5000", registryIP)); err != nil { return nil, fmt.Errorf("add insecure registry: %w", err) } @@ -260,9 +257,6 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien } func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.License, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig) error { - // get the configured custom domains - ecDomains := utils.GetDomains(m.releaseData) - progressChan := make(chan addontypes.AddOnProgress) defer close(progressChan) @@ -294,8 +288,20 @@ func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.L addons.WithProgressChannel(progressChan), ) + opts := m.getAddonInstallOpts(license, rc) + logFn("installing addons") - if err := addOns.Install(ctx, addons.InstallOptions{ + if err := addOns.Install(ctx, opts); err != nil { + return err + } + + return nil +} + +func (m *infraManager) getAddonInstallOpts(license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) addons.InstallOptions { + ecDomains := utils.GetDomains(m.releaseData) + + opts := addons.InstallOptions{ AdminConsolePwd: m.password, License: license, IsAirgap: m.airgapBundle != "", @@ -306,7 +312,12 @@ func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.L IsMultiNodeEnabled: license.Spec.IsEmbeddedClusterMultiNodeEnabled, EmbeddedConfigSpec: m.getECConfigSpec(), EndUserConfigSpec: m.getEndUserConfigSpec(), - KotsInstaller: func() error { + } + + if m.kotsInstaller != nil { // used for testing + opts.KotsInstaller = m.kotsInstaller + } else { + opts.KotsInstaller = func() error { opts := kotscli.InstallOptions{ RuntimeConfig: rc, AppSlug: license.Spec.AppSlug, @@ -319,12 +330,10 @@ func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.L // Stdout: stdout, } return kotscli.Install(opts) - }, - }); err != nil { - return err + } } - return nil + return opts } func (m *infraManager) installExtensions(ctx context.Context, hcli helm.Client) (finalErr error) { diff --git a/api/internal/managers/infra/manager.go b/api/internal/managers/infra/manager.go index aa777f267..d21583081 100644 --- a/api/internal/managers/infra/manager.go +++ b/api/internal/managers/infra/manager.go @@ -4,26 +4,31 @@ import ( "context" "sync" + "github.com/replicatedhq/embedded-cluster/api/internal/store/infra" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" + "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" + "k8s.io/client-go/metadata" + "sigs.k8s.io/controller-runtime/pkg/client" ) var _ InfraManager = &infraManager{} // InfraManager provides methods for managing infrastructure setup type InfraManager interface { - Get() (*types.Infra, error) + Get() (types.Infra, error) Install(ctx context.Context, rc runtimeconfig.RuntimeConfig) error } // infraManager is an implementation of the InfraManager interface type infraManager struct { - infra *types.Infra - infraStore Store + infraStore infra.Store password string tlsConfig types.TLSConfig licenseFile string @@ -32,6 +37,12 @@ type infraManager struct { releaseData *release.ReleaseData endUserConfig *ecv1beta1.Config logger logrus.FieldLogger + k0scli k0s.K0sInterface + kcli client.Client + mcli metadata.Interface + hcli helm.Client + hostUtils hostutils.HostUtilsInterface + kotsInstaller func() error mu sync.RWMutex } @@ -43,13 +54,7 @@ func WithLogger(logger logrus.FieldLogger) InfraManagerOption { } } -func WithInfra(infra *types.Infra) InfraManagerOption { - return func(c *infraManager) { - c.infra = infra - } -} - -func WithInfraStore(store Store) InfraManagerOption { +func WithInfraStore(store infra.Store) InfraManagerOption { return func(c *infraManager) { c.infraStore = store } @@ -97,6 +102,42 @@ func WithEndUserConfig(endUserConfig *ecv1beta1.Config) InfraManagerOption { } } +func WithK0s(k0s k0s.K0sInterface) InfraManagerOption { + return func(c *infraManager) { + c.k0scli = k0s + } +} + +func WithKubeClient(kcli client.Client) InfraManagerOption { + return func(c *infraManager) { + c.kcli = kcli + } +} + +func WithMetadataClient(mcli metadata.Interface) InfraManagerOption { + return func(c *infraManager) { + c.mcli = mcli + } +} + +func WithHelmClient(hcli helm.Client) InfraManagerOption { + return func(c *infraManager) { + c.hcli = hcli + } +} + +func WithHostUtils(hostUtils hostutils.HostUtilsInterface) InfraManagerOption { + return func(c *infraManager) { + c.hostUtils = hostUtils + } +} + +func WithKotsInstaller(kotsInstaller func() error) InfraManagerOption { + return func(c *infraManager) { + c.kotsInstaller = kotsInstaller + } +} + // NewInfraManager creates a new InfraManager with the provided options func NewInfraManager(opts ...InfraManagerOption) *infraManager { manager := &infraManager{} @@ -109,17 +150,21 @@ func NewInfraManager(opts ...InfraManagerOption) *infraManager { manager.logger = logger.NewDiscardLogger() } - if manager.infra == nil { - manager.infra = &types.Infra{} + if manager.infraStore == nil { + manager.infraStore = infra.NewMemoryStore() } - if manager.infraStore == nil { - manager.infraStore = NewMemoryStore(manager.infra) + if manager.k0scli == nil { + manager.k0scli = k0s.New() + } + + if manager.hostUtils == nil { + manager.hostUtils = hostutils.New() } return manager } -func (m *infraManager) Get() (*types.Infra, error) { +func (m *infraManager) Get() (types.Infra, error) { return m.infraStore.Get() } diff --git a/api/internal/managers/infra/manager_mock.go b/api/internal/managers/infra/manager_mock.go index 44bd9e140..12345c5db 100644 --- a/api/internal/managers/infra/manager_mock.go +++ b/api/internal/managers/infra/manager_mock.go @@ -20,10 +20,10 @@ func (m *MockInfraManager) Install(ctx context.Context, rc runtimeconfig.Runtime return args.Error(0) } -func (m *MockInfraManager) Get() (*types.Infra, error) { +func (m *MockInfraManager) Get() (types.Infra, error) { args := m.Called() if args.Get(0) == nil { - return nil, args.Error(1) + return types.Infra{}, args.Error(1) } - return args.Get(0).(*types.Infra), args.Error(1) + return args.Get(0).(types.Infra), args.Error(1) } diff --git a/api/internal/managers/infra/status.go b/api/internal/managers/infra/status.go index c9a302e15..afeaeba93 100644 --- a/api/internal/managers/infra/status.go +++ b/api/internal/managers/infra/status.go @@ -7,7 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -func (m *infraManager) GetStatus() (*types.Status, error) { +func (m *infraManager) GetStatus() (types.Status, error) { return m.infraStore.GetStatus() } @@ -20,9 +20,6 @@ func (m *infraManager) installDidRun() (bool, error) { if err != nil { return false, fmt.Errorf("get status: %w", err) } - if currStatus == nil { - return false, nil - } if currStatus.State == "" { return false, nil } @@ -45,7 +42,7 @@ func (m *infraManager) setStatusDesc(description string) error { } func (m *infraManager) setComponentStatus(name string, state types.State, description string) error { - return m.infraStore.SetComponentStatus(name, &types.Status{ + return m.infraStore.SetComponentStatus(name, types.Status{ State: state, Description: description, LastUpdated: time.Now(), diff --git a/api/internal/managers/infra/status_test.go b/api/internal/managers/infra/status_test.go index c744dc622..7c4bca268 100644 --- a/api/internal/managers/infra/status_test.go +++ b/api/internal/managers/infra/status_test.go @@ -26,19 +26,19 @@ func TestInfraWithLogs(t *testing.T) { func TestInstallDidRun(t *testing.T) { tests := []struct { name string - currentStatus *types.Status + currentStatus types.Status expectedResult bool expectedErr bool }{ { name: "nil status", - currentStatus: nil, + currentStatus: types.Status{}, expectedResult: false, expectedErr: false, }, { name: "empty state", - currentStatus: &types.Status{ + currentStatus: types.Status{ State: "", }, expectedResult: false, @@ -46,7 +46,7 @@ func TestInstallDidRun(t *testing.T) { }, { name: "pending state", - currentStatus: &types.Status{ + currentStatus: types.Status{ State: types.StatePending, }, expectedResult: false, @@ -54,7 +54,7 @@ func TestInstallDidRun(t *testing.T) { }, { name: "running state", - currentStatus: &types.Status{ + currentStatus: types.Status{ State: types.StateRunning, }, expectedResult: true, @@ -62,7 +62,7 @@ func TestInstallDidRun(t *testing.T) { }, { name: "failed state", - currentStatus: &types.Status{ + currentStatus: types.Status{ State: types.StateFailed, }, expectedResult: true, @@ -70,7 +70,7 @@ func TestInstallDidRun(t *testing.T) { }, { name: "succeeded state", - currentStatus: &types.Status{ + currentStatus: types.Status{ State: types.StateSucceeded, }, expectedResult: true, @@ -81,9 +81,8 @@ func TestInstallDidRun(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { manager := NewInfraManager() - if tt.currentStatus != nil { - manager.SetStatus(*tt.currentStatus) - } + manager.SetStatus(tt.currentStatus) + result, err := manager.installDidRun() if tt.expectedErr { diff --git a/api/internal/managers/infra/util.go b/api/internal/managers/infra/util.go index 50fbb3850..46e2b8ca5 100644 --- a/api/internal/managers/infra/util.go +++ b/api/internal/managers/infra/util.go @@ -11,6 +11,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/versions" + "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -26,7 +27,33 @@ func (m *infraManager) waitForNode(ctx context.Context, kcli client.Client) erro return nil } -func (m *infraManager) getHelmClient(rc runtimeconfig.RuntimeConfig) (helm.Client, error) { +func (m *infraManager) kubeClient() (client.Client, error) { + if m.kcli != nil { + return m.kcli, nil + } + kcli, err := kubeutils.KubeClient() + if err != nil { + return nil, fmt.Errorf("create kube client: %w", err) + } + return kcli, nil +} + +func (m *infraManager) metadataClient() (metadata.Interface, error) { + if m.mcli != nil { + return m.mcli, nil + } + mcli, err := kubeutils.MetadataClient() + if err != nil { + return nil, fmt.Errorf("create metadata client: %w", err) + } + return mcli, nil +} + +func (m *infraManager) helmClient(rc runtimeconfig.RuntimeConfig) (helm.Client, error) { + if m.hcli != nil { + return m.hcli, nil + } + airgapChartsPath := "" if m.airgapBundle != "" { airgapChartsPath = rc.EmbeddedClusterChartsSubDir() diff --git a/api/internal/managers/infra/util_test.go b/api/internal/managers/infra/util_test.go index 02fa7bb3d..b7f3d396d 100644 --- a/api/internal/managers/infra/util_test.go +++ b/api/internal/managers/infra/util_test.go @@ -3,6 +3,7 @@ package infra import ( "testing" + infrastore "github.com/replicatedhq/embedded-cluster/api/internal/store/infra" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/stretchr/testify/assert" ) @@ -41,7 +42,7 @@ func TestInfraManager_logFn(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a mock store - mockStore := &MockStore{} + mockStore := &infrastore.MockStore{} mockStore.On("AddLogs", tt.expected).Return(nil) // Create a manager with the mock store @@ -62,7 +63,7 @@ func TestInfraManager_logFn(t *testing.T) { func TestInfraManager_logFn_StoreError(t *testing.T) { // Create a mock store that returns an error - mockStore := &MockStore{} + mockStore := &infrastore.MockStore{} mockStore.On("AddLogs", "[test] error message").Return(assert.AnError) // Create a manager with the mock store diff --git a/api/internal/managers/installation/config.go b/api/internal/managers/installation/config.go index d06a7f02c..e3c6127b1 100644 --- a/api/internal/managers/installation/config.go +++ b/api/internal/managers/installation/config.go @@ -14,7 +14,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) -func (m *installationManager) GetConfig() (*types.InstallationConfig, error) { +func (m *installationManager) GetConfig() (types.InstallationConfig, error) { return m.installationStore.GetConfig() } @@ -22,7 +22,7 @@ func (m *installationManager) SetConfig(config types.InstallationConfig) error { return m.installationStore.SetConfig(config) } -func (m *installationManager) ValidateConfig(config *types.InstallationConfig, managerPort int) error { +func (m *installationManager) ValidateConfig(config types.InstallationConfig, managerPort int) error { var ve *types.APIError if err := m.validateGlobalCIDR(config); err != nil { @@ -56,7 +56,7 @@ func (m *installationManager) ValidateConfig(config *types.InstallationConfig, m return ve.ErrorOrNil() } -func (m *installationManager) validateGlobalCIDR(config *types.InstallationConfig) error { +func (m *installationManager) validateGlobalCIDR(config types.InstallationConfig) error { if config.GlobalCIDR != "" { if err := netutils.ValidateCIDR(config.GlobalCIDR, 16, true); err != nil { return err @@ -69,7 +69,7 @@ func (m *installationManager) validateGlobalCIDR(config *types.InstallationConfi return nil } -func (m *installationManager) validatePodCIDR(config *types.InstallationConfig) error { +func (m *installationManager) validatePodCIDR(config types.InstallationConfig) error { if config.GlobalCIDR != "" { return nil } @@ -79,7 +79,7 @@ func (m *installationManager) validatePodCIDR(config *types.InstallationConfig) return nil } -func (m *installationManager) validateServiceCIDR(config *types.InstallationConfig) error { +func (m *installationManager) validateServiceCIDR(config types.InstallationConfig) error { if config.GlobalCIDR != "" { return nil } @@ -89,7 +89,7 @@ func (m *installationManager) validateServiceCIDR(config *types.InstallationConf return nil } -func (m *installationManager) validateNetworkInterface(config *types.InstallationConfig) error { +func (m *installationManager) validateNetworkInterface(config types.InstallationConfig) error { if config.NetworkInterface == "" { return errors.New("networkInterface is required") } @@ -98,7 +98,7 @@ func (m *installationManager) validateNetworkInterface(config *types.Installatio return nil } -func (m *installationManager) validateAdminConsolePort(config *types.InstallationConfig, managerPort int) error { +func (m *installationManager) validateAdminConsolePort(config types.InstallationConfig, managerPort int) error { if config.AdminConsolePort == 0 { return errors.New("adminConsolePort is required") } @@ -119,7 +119,7 @@ func (m *installationManager) validateAdminConsolePort(config *types.Installatio return nil } -func (m *installationManager) validateLocalArtifactMirrorPort(config *types.InstallationConfig, managerPort int) error { +func (m *installationManager) validateLocalArtifactMirrorPort(config types.InstallationConfig, managerPort int) error { if config.LocalArtifactMirrorPort == 0 { return errors.New("localArtifactMirrorPort is required") } @@ -140,7 +140,7 @@ func (m *installationManager) validateLocalArtifactMirrorPort(config *types.Inst return nil } -func (m *installationManager) validateDataDirectory(config *types.InstallationConfig) error { +func (m *installationManager) validateDataDirectory(config types.InstallationConfig) error { if config.DataDirectory == "" { return errors.New("dataDirectory is required") } diff --git a/api/internal/managers/installation/config_test.go b/api/internal/managers/installation/config_test.go index 428429f71..242ce105a 100644 --- a/api/internal/managers/installation/config_test.go +++ b/api/internal/managers/installation/config_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" @@ -21,12 +22,12 @@ func TestValidateConfig(t *testing.T) { tests := []struct { name string rc runtimeconfig.RuntimeConfig - config *types.InstallationConfig + config types.InstallationConfig expectedErr bool }{ { name: "valid config with global CIDR", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -37,7 +38,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "valid config with pod and service CIDRs", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ PodCIDR: "10.0.0.0/17", ServiceCIDR: "10.0.128.0/17", NetworkInterface: "eth0", @@ -49,7 +50,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "missing network interface", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", AdminConsolePort: 8800, LocalArtifactMirrorPort: 8888, @@ -59,7 +60,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "missing global CIDR and pod/service CIDRs", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ NetworkInterface: "eth0", AdminConsolePort: 8800, LocalArtifactMirrorPort: 8888, @@ -69,7 +70,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "missing pod CIDR when no global CIDR", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ ServiceCIDR: "10.0.128.0/17", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -80,7 +81,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "missing service CIDR when no global CIDR", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ PodCIDR: "10.0.0.0/17", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -91,7 +92,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "invalid global CIDR", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/24", // Not a /16 NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -102,7 +103,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "missing admin console port", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", LocalArtifactMirrorPort: 8888, @@ -112,7 +113,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "missing local artifact mirror port", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -122,7 +123,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "missing data directory", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -132,7 +133,7 @@ func TestValidateConfig(t *testing.T) { }, { name: "same ports for admin console and artifact mirror", - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -148,7 +149,7 @@ func TestValidateConfig(t *testing.T) { rc.SetManagerPort(8800) return rc }(), - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -164,7 +165,7 @@ func TestValidateConfig(t *testing.T) { rc.SetManagerPort(8888) return rc }(), - config: &types.InstallationConfig{ + config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", AdminConsolePort: 8800, @@ -205,13 +206,13 @@ func TestSetConfigDefaults(t *testing.T) { tests := []struct { name string - inputConfig *types.InstallationConfig - expectedConfig *types.InstallationConfig + inputConfig types.InstallationConfig + expectedConfig types.InstallationConfig }{ { name: "empty config", - inputConfig: &types.InstallationConfig{}, - expectedConfig: &types.InstallationConfig{ + inputConfig: types.InstallationConfig{}, + expectedConfig: types.InstallationConfig{ AdminConsolePort: ecv1beta1.DefaultAdminConsolePort, DataDirectory: ecv1beta1.DefaultDataDir, LocalArtifactMirrorPort: ecv1beta1.DefaultLocalArtifactMirrorPort, @@ -221,11 +222,11 @@ func TestSetConfigDefaults(t *testing.T) { }, { name: "partial config", - inputConfig: &types.InstallationConfig{ + inputConfig: types.InstallationConfig{ AdminConsolePort: 9000, DataDirectory: "/custom/dir", }, - expectedConfig: &types.InstallationConfig{ + expectedConfig: types.InstallationConfig{ AdminConsolePort: 9000, DataDirectory: "/custom/dir", LocalArtifactMirrorPort: ecv1beta1.DefaultLocalArtifactMirrorPort, @@ -235,11 +236,11 @@ func TestSetConfigDefaults(t *testing.T) { }, { name: "config with pod and service CIDRs", - inputConfig: &types.InstallationConfig{ + inputConfig: types.InstallationConfig{ PodCIDR: "10.1.0.0/17", ServiceCIDR: "10.1.128.0/17", }, - expectedConfig: &types.InstallationConfig{ + expectedConfig: types.InstallationConfig{ AdminConsolePort: ecv1beta1.DefaultAdminConsolePort, DataDirectory: ecv1beta1.DefaultDataDir, LocalArtifactMirrorPort: ecv1beta1.DefaultLocalArtifactMirrorPort, @@ -250,10 +251,10 @@ func TestSetConfigDefaults(t *testing.T) { }, { name: "config with global CIDR", - inputConfig: &types.InstallationConfig{ + inputConfig: types.InstallationConfig{ GlobalCIDR: "192.168.0.0/16", }, - expectedConfig: &types.InstallationConfig{ + expectedConfig: types.InstallationConfig{ AdminConsolePort: ecv1beta1.DefaultAdminConsolePort, DataDirectory: ecv1beta1.DefaultDataDir, LocalArtifactMirrorPort: ecv1beta1.DefaultLocalArtifactMirrorPort, @@ -263,11 +264,11 @@ func TestSetConfigDefaults(t *testing.T) { }, { name: "config with proxy settings", - inputConfig: &types.InstallationConfig{ + inputConfig: types.InstallationConfig{ HTTPProxy: "http://proxy.example.com:3128", HTTPSProxy: "https://proxy.example.com:3128", }, - expectedConfig: &types.InstallationConfig{ + expectedConfig: types.InstallationConfig{ AdminConsolePort: ecv1beta1.DefaultAdminConsolePort, DataDirectory: ecv1beta1.DefaultDataDir, LocalArtifactMirrorPort: ecv1beta1.DefaultLocalArtifactMirrorPort, @@ -283,7 +284,7 @@ func TestSetConfigDefaults(t *testing.T) { t.Run(tt.name, func(t *testing.T) { manager := NewInstallationManager(WithNetUtils(mockNetUtils)) - err := manager.SetConfigDefaults(tt.inputConfig) + err := manager.SetConfigDefaults(&tt.inputConfig) assert.NoError(t, err) assert.NotNil(t, tt.inputConfig) @@ -298,8 +299,8 @@ func TestSetConfigDefaults(t *testing.T) { manager := NewInstallationManager(WithNetUtils(failingMockNetUtils)) - config := &types.InstallationConfig{} - err := manager.SetConfigDefaults(config) + config := types.InstallationConfig{} + err := manager.SetConfigDefaults(&config) assert.NoError(t, err) // Network interface should remain empty when detection fails @@ -339,7 +340,7 @@ func TestConfigureHost(t *testing.T) { tests := []struct { name string rc runtimeconfig.RuntimeConfig - setupMocks func(*hostutils.MockHostUtils, *MockInstallationStore) + setupMocks func(*hostutils.MockHostUtils, *installation.MockStore) expectedErr bool }{ { @@ -354,9 +355,9 @@ func TestConfigureHost(t *testing.T) { }) return rc }(), - setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { + setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { mock.InOrder( - im.On("GetStatus").Return(&types.Status{State: types.StatePending}, nil), + im.On("GetStatus").Return(types.Status{State: types.StatePending}, nil), im.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { return status.State == types.StateRunning })).Return(nil), hum.On("ConfigureHost", mock.Anything, mock.MatchedBy(func(rc runtimeconfig.RuntimeConfig) bool { @@ -381,8 +382,8 @@ func TestConfigureHost(t *testing.T) { }) return rc }(), - setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { - im.On("GetStatus").Return(&types.Status{State: types.StateRunning}, nil) + setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { + im.On("GetStatus").Return(types.Status{State: types.StateRunning}, nil) }, expectedErr: true, }, @@ -394,9 +395,9 @@ func TestConfigureHost(t *testing.T) { }) return rc }(), - setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { + setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { mock.InOrder( - im.On("GetStatus").Return(&types.Status{State: types.StatePending}, nil), + im.On("GetStatus").Return(types.Status{State: types.StatePending}, nil), im.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { return status.State == types.StateRunning })).Return(nil), hum.On("ConfigureHost", mock.Anything, mock.MatchedBy(func(rc runtimeconfig.RuntimeConfig) bool { @@ -420,9 +421,9 @@ func TestConfigureHost(t *testing.T) { }) return rc }(), - setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { + setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { mock.InOrder( - im.On("GetStatus").Return(&types.Status{State: types.StatePending}, nil), + im.On("GetStatus").Return(types.Status{State: types.StatePending}, nil), im.On("SetStatus", mock.Anything).Return(errors.New("failed to set status")), ) }, @@ -436,7 +437,7 @@ func TestConfigureHost(t *testing.T) { // Create mocks mockHostUtils := &hostutils.MockHostUtils{} - mockStore := &MockInstallationStore{} + mockStore := &installation.MockStore{} // Setup mocks tt.setupMocks(mockHostUtils, mockStore) diff --git a/api/internal/managers/installation/manager.go b/api/internal/managers/installation/manager.go index cf3087f47..bfb0329ec 100644 --- a/api/internal/managers/installation/manager.go +++ b/api/internal/managers/installation/manager.go @@ -4,6 +4,7 @@ import ( "context" "sync" + "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" @@ -16,19 +17,18 @@ var _ InstallationManager = &installationManager{} // InstallationManager provides methods for validating and setting defaults for installation configuration type InstallationManager interface { - GetConfig() (*types.InstallationConfig, error) + GetConfig() (types.InstallationConfig, error) SetConfig(config types.InstallationConfig) error - GetStatus() (*types.Status, error) + GetStatus() (types.Status, error) SetStatus(status types.Status) error - ValidateConfig(config *types.InstallationConfig, managerPort int) error + ValidateConfig(config types.InstallationConfig, managerPort int) error SetConfigDefaults(config *types.InstallationConfig) error ConfigureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) error } // installationManager is an implementation of the InstallationManager interface type installationManager struct { - installation *types.Installation - installationStore InstallationStore + installationStore installation.Store licenseFile string airgapBundle string netUtils utils.NetUtils @@ -45,13 +45,7 @@ func WithLogger(logger logrus.FieldLogger) InstallationManagerOption { } } -func WithInstallation(installation *types.Installation) InstallationManagerOption { - return func(c *installationManager) { - c.installation = installation - } -} - -func WithInstallationStore(installationStore InstallationStore) InstallationManagerOption { +func WithInstallationStore(installationStore installation.Store) InstallationManagerOption { return func(c *installationManager) { c.installationStore = installationStore } @@ -93,12 +87,8 @@ func NewInstallationManager(opts ...InstallationManagerOption) *installationMana manager.logger = logger.NewDiscardLogger() } - if manager.installation == nil { - manager.installation = types.NewInstallation() - } - if manager.installationStore == nil { - manager.installationStore = NewMemoryStore(manager.installation) + manager.installationStore = installation.NewMemoryStore() } if manager.netUtils == nil { diff --git a/api/internal/managers/installation/manager_mock.go b/api/internal/managers/installation/manager_mock.go index 68bf6d7e0..eb3817849 100644 --- a/api/internal/managers/installation/manager_mock.go +++ b/api/internal/managers/installation/manager_mock.go @@ -16,12 +16,12 @@ type MockInstallationManager struct { } // GetConfig mocks the GetConfig method -func (m *MockInstallationManager) GetConfig() (*types.InstallationConfig, error) { +func (m *MockInstallationManager) GetConfig() (types.InstallationConfig, error) { args := m.Called() if args.Get(0) == nil { - return nil, args.Error(1) + return types.InstallationConfig{}, args.Error(1) } - return args.Get(0).(*types.InstallationConfig), args.Error(1) + return args.Get(0).(types.InstallationConfig), args.Error(1) } // SetConfig mocks the SetConfig method @@ -31,12 +31,12 @@ func (m *MockInstallationManager) SetConfig(config types.InstallationConfig) err } // GetStatus mocks the GetStatus method -func (m *MockInstallationManager) GetStatus() (*types.Status, error) { +func (m *MockInstallationManager) GetStatus() (types.Status, error) { args := m.Called() if args.Get(0) == nil { - return nil, args.Error(1) + return types.Status{}, args.Error(1) } - return args.Get(0).(*types.Status), args.Error(1) + return args.Get(0).(types.Status), args.Error(1) } // SetStatus mocks the SetStatus method @@ -46,7 +46,7 @@ func (m *MockInstallationManager) SetStatus(status types.Status) error { } // ValidateConfig mocks the ValidateConfig method -func (m *MockInstallationManager) ValidateConfig(config *types.InstallationConfig, managerPort int) error { +func (m *MockInstallationManager) ValidateConfig(config types.InstallationConfig, managerPort int) error { args := m.Called(config, managerPort) return args.Error(0) } diff --git a/api/internal/managers/installation/status.go b/api/internal/managers/installation/status.go index 9557a29a3..611b413f9 100644 --- a/api/internal/managers/installation/status.go +++ b/api/internal/managers/installation/status.go @@ -6,7 +6,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -func (m *installationManager) GetStatus() (*types.Status, error) { +func (m *installationManager) GetStatus() (types.Status, error) { return m.installationStore.GetStatus() } diff --git a/api/internal/managers/installation/store.go b/api/internal/managers/installation/store.go deleted file mode 100644 index 7159ed6e6..000000000 --- a/api/internal/managers/installation/store.go +++ /dev/null @@ -1,58 +0,0 @@ -package installation - -import ( - "sync" - - "github.com/replicatedhq/embedded-cluster/api/types" -) - -// TODO (@team): discuss the idea of having a generic store interface that can be used for all stores -type InstallationStore interface { - GetConfig() (*types.InstallationConfig, error) - SetConfig(cfg types.InstallationConfig) error - GetStatus() (*types.Status, error) - SetStatus(status types.Status) error -} - -var _ InstallationStore = &MemoryStore{} - -type MemoryStore struct { - mu sync.RWMutex - installation *types.Installation -} - -func NewMemoryStore(installation *types.Installation) *MemoryStore { - return &MemoryStore{ - installation: installation, - } -} - -func (s *MemoryStore) GetConfig() (*types.InstallationConfig, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.installation.Config, nil -} - -func (s *MemoryStore) SetConfig(cfg types.InstallationConfig) error { - s.mu.Lock() - defer s.mu.Unlock() - s.installation.Config = &cfg - - return nil -} - -func (s *MemoryStore) GetStatus() (*types.Status, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.installation.Status, nil -} - -func (s *MemoryStore) SetStatus(status types.Status) error { - s.mu.Lock() - defer s.mu.Unlock() - s.installation.Status = &status - - return nil -} diff --git a/api/internal/managers/installation/store_mock.go b/api/internal/managers/installation/store_mock.go deleted file mode 100644 index 871e15192..000000000 --- a/api/internal/managers/installation/store_mock.go +++ /dev/null @@ -1,43 +0,0 @@ -package installation - -import ( - "github.com/replicatedhq/embedded-cluster/api/types" - "github.com/stretchr/testify/mock" -) - -var _ InstallationStore = (*MockInstallationStore)(nil) - -// MockInstallationStore is a mock implementation of the InstallationStore interface -type MockInstallationStore struct { - mock.Mock -} - -// GetConfig mocks the GetConfig method -func (m *MockInstallationStore) GetConfig() (*types.InstallationConfig, error) { - args := m.Called() - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*types.InstallationConfig), args.Error(1) -} - -// SetConfig mocks the SetConfig method -func (m *MockInstallationStore) SetConfig(cfg types.InstallationConfig) error { - args := m.Called(cfg) - return args.Error(0) -} - -// GetStatus mocks the GetStatus method -func (m *MockInstallationStore) GetStatus() (*types.Status, error) { - args := m.Called() - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*types.Status), args.Error(1) -} - -// SetStatus mocks the SetStatus method -func (m *MockInstallationStore) SetStatus(status types.Status) error { - args := m.Called(status) - return args.Error(0) -} diff --git a/api/internal/managers/preflight/hostpreflight.go b/api/internal/managers/preflight/hostpreflight.go index d1426fdfa..9ed9557f3 100644 --- a/api/internal/managers/preflight/hostpreflight.go +++ b/api/internal/managers/preflight/hostpreflight.go @@ -55,7 +55,7 @@ func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, rc runtime return nil } -func (m *hostPreflightManager) GetHostPreflightStatus(ctx context.Context) (*types.Status, error) { +func (m *hostPreflightManager) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { return m.hostPreflightStore.GetStatus() } @@ -168,7 +168,7 @@ func (m *hostPreflightManager) setRunningStatus(hpf *troubleshootv1beta2.HostPre return fmt.Errorf("reset output: %w", err) } - if err := m.hostPreflightStore.SetStatus(&types.Status{ + if err := m.hostPreflightStore.SetStatus(types.Status{ State: types.StateRunning, Description: "Running host preflights", LastUpdated: time.Now(), @@ -182,7 +182,7 @@ func (m *hostPreflightManager) setRunningStatus(hpf *troubleshootv1beta2.HostPre func (m *hostPreflightManager) setFailedStatus(description string) error { m.logger.Error(description) - return m.hostPreflightStore.SetStatus(&types.Status{ + return m.hostPreflightStore.SetStatus(types.Status{ State: types.StateFailed, Description: description, LastUpdated: time.Now(), @@ -194,7 +194,7 @@ func (m *hostPreflightManager) setCompletedStatus(state types.State, description return fmt.Errorf("set output: %w", err) } - return m.hostPreflightStore.SetStatus(&types.Status{ + return m.hostPreflightStore.SetStatus(types.Status{ State: state, Description: description, LastUpdated: time.Now(), diff --git a/api/internal/managers/preflight/hostpreflight_test.go b/api/internal/managers/preflight/hostpreflight_test.go index f654ac535..4cc700629 100644 --- a/api/internal/managers/preflight/hostpreflight_test.go +++ b/api/internal/managers/preflight/hostpreflight_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + preflightstore "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" @@ -202,7 +203,7 @@ func TestHostPreflightManager_PrepareHostPreflights(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Setup mocks mockRunner := &preflights.MockPreflightRunner{} - mockStore := &MockHostPreflightStore{} + mockStore := &preflightstore.MockStore{} mockMetrics := &metrics.MockReporter{} mockNetUtils := &utils.MockNetUtils{} @@ -247,7 +248,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { tests := []struct { name string opts RunHostPreflightOptions - initialState *types.HostPreflights + initialState types.HostPreflights setupMocks func(*preflights.MockPreflightRunner, *metrics.MockReporter, runtimeconfig.RuntimeConfig) expectedFinalState types.State // This is the expected error message returned by the RunHostPreflights method, synchronously @@ -258,8 +259,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { opts: RunHostPreflightOptions{ HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, }, - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, @@ -279,8 +280,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { opts: RunHostPreflightOptions{ HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, }, - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, @@ -306,8 +307,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { }, { name: "execution with preflight warnings", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, @@ -335,8 +336,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { }, { name: "execution with both failures and warnings", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, @@ -368,8 +369,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { }, { name: "runner execution fails", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, @@ -384,8 +385,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { }, { name: "SaveToDisk fails but execution continues", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, @@ -405,8 +406,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { }, { name: "CopyBundleTo fails but execution continues", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, @@ -426,8 +427,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { }, { name: "error - preflights already running", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StateRunning, }, }, @@ -455,7 +456,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { // Create manager using builder pattern manager := NewHostPreflightManager( WithPreflightRunner(mockRunner), - WithHostPreflightStore(NewMemoryStore(tt.initialState)), + WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(tt.initialState))), WithLogger(logger.NewDiscardLogger()), WithMetricsReporter(mockMetrics), ) @@ -490,27 +491,27 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { func TestHostPreflightManager_GetHostPreflightStatus(t *testing.T) { tests := []struct { name string - setupMocks func(*MockHostPreflightStore) - expectedStatus *types.Status + setupMocks func(*preflightstore.MockStore) + expectedStatus types.Status expectedError string }{ { name: "success", - setupMocks: func(store *MockHostPreflightStore) { - store.On("GetStatus").Return(&types.Status{ + setupMocks: func(store *preflightstore.MockStore) { + store.On("GetStatus").Return(types.Status{ State: types.StateSucceeded, Description: "Host preflights passed", LastUpdated: time.Now(), }, nil) }, - expectedStatus: &types.Status{ + expectedStatus: types.Status{ State: types.StateSucceeded, Description: "Host preflights passed", }, }, { name: "error from store", - setupMocks: func(store *MockHostPreflightStore) { + setupMocks: func(store *preflightstore.MockStore) { store.On("GetStatus").Return(nil, fmt.Errorf("store error")) }, expectedError: "store error", @@ -520,7 +521,7 @@ func TestHostPreflightManager_GetHostPreflightStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mocks - mockStore := &MockHostPreflightStore{} + mockStore := &preflightstore.MockStore{} tt.setupMocks(mockStore) // Create manager using builder pattern @@ -535,7 +536,7 @@ func TestHostPreflightManager_GetHostPreflightStatus(t *testing.T) { if tt.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, status) + assert.Equal(t, types.Status{}, status) } else { require.NoError(t, err) assert.Equal(t, tt.expectedStatus.State, status.State) @@ -551,13 +552,13 @@ func TestHostPreflightManager_GetHostPreflightStatus(t *testing.T) { func TestHostPreflightManager_GetHostPreflightOutput(t *testing.T) { tests := []struct { name string - setupMocks func(*MockHostPreflightStore) + setupMocks func(*preflightstore.MockStore) expectedOutput *types.HostPreflightsOutput expectedError string }{ { name: "success", - setupMocks: func(store *MockHostPreflightStore) { + setupMocks: func(store *preflightstore.MockStore) { output := &types.HostPreflightsOutput{} store.On("GetOutput").Return(output, nil) }, @@ -565,7 +566,7 @@ func TestHostPreflightManager_GetHostPreflightOutput(t *testing.T) { }, { name: "error from store", - setupMocks: func(store *MockHostPreflightStore) { + setupMocks: func(store *preflightstore.MockStore) { store.On("GetOutput").Return(nil, fmt.Errorf("store error")) }, expectedError: "store error", @@ -575,7 +576,7 @@ func TestHostPreflightManager_GetHostPreflightOutput(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mocks - mockStore := &MockHostPreflightStore{} + mockStore := &preflightstore.MockStore{} tt.setupMocks(mockStore) // Create manager using builder pattern @@ -605,13 +606,13 @@ func TestHostPreflightManager_GetHostPreflightOutput(t *testing.T) { func TestHostPreflightManager_GetHostPreflightTitles(t *testing.T) { tests := []struct { name string - setupMocks func(*MockHostPreflightStore) + setupMocks func(*preflightstore.MockStore) expectedTitles []string expectedError string }{ { name: "success", - setupMocks: func(store *MockHostPreflightStore) { + setupMocks: func(store *preflightstore.MockStore) { titles := []string{"Memory Check", "Disk Space Check", "Network Check"} store.On("GetTitles").Return(titles, nil) }, @@ -619,14 +620,14 @@ func TestHostPreflightManager_GetHostPreflightTitles(t *testing.T) { }, { name: "success with empty titles", - setupMocks: func(store *MockHostPreflightStore) { + setupMocks: func(store *preflightstore.MockStore) { store.On("GetTitles").Return([]string{}, nil) }, expectedTitles: []string{}, }, { name: "error from store", - setupMocks: func(store *MockHostPreflightStore) { + setupMocks: func(store *preflightstore.MockStore) { store.On("GetTitles").Return(nil, fmt.Errorf("store error")) }, expectedError: "store error", @@ -636,7 +637,7 @@ func TestHostPreflightManager_GetHostPreflightTitles(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup mocks - mockStore := &MockHostPreflightStore{} + mockStore := &preflightstore.MockStore{} tt.setupMocks(mockStore) // Create manager using builder pattern diff --git a/api/internal/managers/preflight/manager.go b/api/internal/managers/preflight/manager.go index 16bcc504d..d5ba90439 100644 --- a/api/internal/managers/preflight/manager.go +++ b/api/internal/managers/preflight/manager.go @@ -4,6 +4,7 @@ import ( "context" "sync" + "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" @@ -18,13 +19,13 @@ import ( type HostPreflightManager interface { PrepareHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) RunHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) error - GetHostPreflightStatus(ctx context.Context) (*types.Status, error) + GetHostPreflightStatus(ctx context.Context) (types.Status, error) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) GetHostPreflightTitles(ctx context.Context) ([]string, error) } type hostPreflightManager struct { - hostPreflightStore HostPreflightStore + hostPreflightStore preflight.Store runner preflights.PreflightsRunnerInterface netUtils utils.NetUtils logger logrus.FieldLogger @@ -46,7 +47,7 @@ func WithMetricsReporter(metricsReporter metrics.ReporterInterface) HostPrefligh } } -func WithHostPreflightStore(hostPreflightStore HostPreflightStore) HostPreflightManagerOption { +func WithHostPreflightStore(hostPreflightStore preflight.Store) HostPreflightManagerOption { return func(m *hostPreflightManager) { m.hostPreflightStore = hostPreflightStore } @@ -77,7 +78,7 @@ func NewHostPreflightManager(opts ...HostPreflightManagerOption) HostPreflightMa } if manager.hostPreflightStore == nil { - manager.hostPreflightStore = NewMemoryStore(types.NewHostPreflights()) + manager.hostPreflightStore = preflight.NewMemoryStore() } if manager.runner == nil { diff --git a/api/internal/managers/preflight/manager_mock.go b/api/internal/managers/preflight/manager_mock.go index 8abcac574..6d659ccfa 100644 --- a/api/internal/managers/preflight/manager_mock.go +++ b/api/internal/managers/preflight/manager_mock.go @@ -32,12 +32,12 @@ func (m *MockHostPreflightManager) RunHostPreflights(ctx context.Context, rc run } // GetHostPreflightStatus mocks the GetHostPreflightStatus method -func (m *MockHostPreflightManager) GetHostPreflightStatus(ctx context.Context) (*types.Status, error) { +func (m *MockHostPreflightManager) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { args := m.Called(ctx) if args.Get(0) == nil { - return nil, args.Error(1) + return types.Status{}, args.Error(1) } - return args.Get(0).(*types.Status), args.Error(1) + return args.Get(0).(types.Status), args.Error(1) } // GetHostPreflightOutput mocks the GetHostPreflightOutput method diff --git a/api/internal/managers/preflight/store.go b/api/internal/managers/preflight/store.go deleted file mode 100644 index 7f10bd5e7..000000000 --- a/api/internal/managers/preflight/store.go +++ /dev/null @@ -1,82 +0,0 @@ -package preflight - -import ( - "sync" - - "github.com/replicatedhq/embedded-cluster/api/types" -) - -type HostPreflightStore interface { - GetTitles() ([]string, error) - SetTitles(titles []string) error - GetOutput() (*types.HostPreflightsOutput, error) - SetOutput(output *types.HostPreflightsOutput) error - GetStatus() (*types.Status, error) - SetStatus(status *types.Status) error - IsRunning() bool -} - -var _ HostPreflightStore = &MemoryStore{} - -type MemoryStore struct { - mu sync.RWMutex - hostPreflight *types.HostPreflights -} - -func NewMemoryStore(hostPreflight *types.HostPreflights) *MemoryStore { - return &MemoryStore{ - hostPreflight: hostPreflight, - } -} - -func (s *MemoryStore) GetTitles() ([]string, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.hostPreflight.Titles, nil -} - -func (s *MemoryStore) SetTitles(titles []string) error { - s.mu.Lock() - defer s.mu.Unlock() - s.hostPreflight.Titles = titles - - return nil -} - -func (s *MemoryStore) GetOutput() (*types.HostPreflightsOutput, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.hostPreflight.Output, nil -} - -func (s *MemoryStore) SetOutput(output *types.HostPreflightsOutput) error { - s.mu.Lock() - defer s.mu.Unlock() - s.hostPreflight.Output = output - - return nil -} - -func (s *MemoryStore) GetStatus() (*types.Status, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.hostPreflight.Status, nil -} - -func (s *MemoryStore) SetStatus(status *types.Status) error { - s.mu.Lock() - defer s.mu.Unlock() - s.hostPreflight.Status = status - - return nil -} - -func (s *MemoryStore) IsRunning() bool { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.hostPreflight.Status.State == types.StateRunning -} diff --git a/api/internal/managers/infra/store.go b/api/internal/store/infra/store.go similarity index 50% rename from api/internal/managers/infra/store.go rename to api/internal/store/infra/store.go index 078320d0c..079f63ed4 100644 --- a/api/internal/managers/infra/store.go +++ b/api/internal/store/infra/store.go @@ -6,73 +6,100 @@ import ( "time" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/tiendc/go-deepcopy" ) const maxLogSize = 100 * 1024 // 100KB total log size +var _ Store = &MemoryStore{} + // Store provides methods for storing and retrieving infrastructure state type Store interface { - Get() (*types.Infra, error) - GetStatus() (*types.Status, error) + Get() (types.Infra, error) + GetStatus() (types.Status, error) SetStatus(status types.Status) error SetStatusDesc(desc string) error RegisterComponent(name string) error - SetComponentStatus(name string, status *types.Status) error + SetComponentStatus(name string, status types.Status) error AddLogs(logs string) error GetLogs() (string, error) } -// memoryStore is an in-memory implementation of Store -type memoryStore struct { - infra *types.Infra +// MemoryStore is an in-memory implementation of Store +type MemoryStore struct { + infra types.Infra mu sync.RWMutex } +type StoreOption func(*MemoryStore) + +func WithInfra(infra types.Infra) StoreOption { + return func(s *MemoryStore) { + s.infra = infra + } +} + // NewMemoryStore creates a new memory store -func NewMemoryStore(infra *types.Infra) Store { - return &memoryStore{ - infra: infra, +func NewMemoryStore(opts ...StoreOption) Store { + s := &MemoryStore{} + + for _, opt := range opts { + opt(s) } + + return s } -func (s *memoryStore) Get() (*types.Infra, error) { +func (s *MemoryStore) Get() (types.Infra, error) { s.mu.RLock() defer s.mu.RUnlock() - return s.infra, nil + + var infra types.Infra + if err := deepcopy.Copy(&infra, &s.infra); err != nil { + return types.Infra{}, err + } + + return infra, nil } -func (s *memoryStore) GetStatus() (*types.Status, error) { +func (s *MemoryStore) GetStatus() (types.Status, error) { s.mu.RLock() defer s.mu.RUnlock() - return s.infra.Status, nil + + var status types.Status + if err := deepcopy.Copy(&status, &s.infra.Status); err != nil { + return types.Status{}, err + } + + return status, nil } -func (s *memoryStore) SetStatus(status types.Status) error { +func (s *MemoryStore) SetStatus(status types.Status) error { s.mu.Lock() defer s.mu.Unlock() - s.infra.Status = &status + s.infra.Status = status return nil } -func (s *memoryStore) SetStatusDesc(desc string) error { +func (s *MemoryStore) SetStatusDesc(desc string) error { s.mu.Lock() defer s.mu.Unlock() - if s.infra.Status == nil { - return fmt.Errorf("status not set") + if s.infra.Status.State == "" { + return fmt.Errorf("state not set") } - s.infra.Status.Description = desc + s.infra.Status.Description = desc return nil } -func (s *memoryStore) RegisterComponent(name string) error { +func (s *MemoryStore) RegisterComponent(name string) error { s.mu.Lock() defer s.mu.Unlock() s.infra.Components = append(s.infra.Components, types.InfraComponent{ Name: name, - Status: &types.Status{ + Status: types.Status{ State: types.StatePending, Description: "", LastUpdated: time.Now(), @@ -82,7 +109,7 @@ func (s *memoryStore) RegisterComponent(name string) error { return nil } -func (s *memoryStore) SetComponentStatus(name string, status *types.Status) error { +func (s *MemoryStore) SetComponentStatus(name string, status types.Status) error { s.mu.Lock() defer s.mu.Unlock() @@ -96,7 +123,7 @@ func (s *memoryStore) SetComponentStatus(name string, status *types.Status) erro return fmt.Errorf("component %s not found", name) } -func (s *memoryStore) AddLogs(logs string) error { +func (s *MemoryStore) AddLogs(logs string) error { s.mu.Lock() defer s.mu.Unlock() @@ -108,7 +135,7 @@ func (s *memoryStore) AddLogs(logs string) error { return nil } -func (s *memoryStore) GetLogs() (string, error) { +func (s *MemoryStore) GetLogs() (string, error) { s.mu.RLock() defer s.mu.RUnlock() return s.infra.Logs, nil diff --git a/api/internal/managers/infra/store_mock.go b/api/internal/store/infra/store_mock.go similarity index 71% rename from api/internal/managers/infra/store_mock.go rename to api/internal/store/infra/store_mock.go index 63f55a352..ac0717f5f 100644 --- a/api/internal/managers/infra/store_mock.go +++ b/api/internal/store/infra/store_mock.go @@ -12,20 +12,20 @@ type MockStore struct { mock.Mock } -func (m *MockStore) Get() (*types.Infra, error) { +func (m *MockStore) Get() (types.Infra, error) { args := m.Called() if args.Get(0) == nil { - return nil, args.Error(1) + return types.Infra{}, args.Error(1) } - return args.Get(0).(*types.Infra), args.Error(1) + return args.Get(0).(types.Infra), args.Error(1) } -func (m *MockStore) GetStatus() (*types.Status, error) { +func (m *MockStore) GetStatus() (types.Status, error) { args := m.Called() if args.Get(0) == nil { - return nil, args.Error(1) + return types.Status{}, args.Error(1) } - return args.Get(0).(*types.Status), args.Error(1) + return args.Get(0).(types.Status), args.Error(1) } func (m *MockStore) SetStatus(status types.Status) error { @@ -43,7 +43,7 @@ func (m *MockStore) RegisterComponent(name string) error { return args.Error(0) } -func (m *MockStore) SetComponentStatus(name string, status *types.Status) error { +func (m *MockStore) SetComponentStatus(name string, status types.Status) error { args := m.Called(name, status) return args.Error(0) } diff --git a/api/internal/managers/infra/store_test.go b/api/internal/store/infra/store_test.go similarity index 96% rename from api/internal/managers/infra/store_test.go rename to api/internal/store/infra/store_test.go index 6e1fd515a..d4c346c75 100644 --- a/api/internal/managers/infra/store_test.go +++ b/api/internal/store/infra/store_test.go @@ -12,14 +12,14 @@ import ( ) func newMemoryStore() Store { - infra := &types.Infra{ - Status: &types.Status{ + infra := types.Infra{ + Status: types.Status{ State: types.StatePending, }, Components: []types.InfraComponent{}, Logs: "", } - return NewMemoryStore(infra) + return NewMemoryStore(WithInfra(infra)) } func TestNewMemoryStore(t *testing.T) { @@ -102,7 +102,7 @@ func TestMemoryStore_SetComponentStatus(t *testing.T) { // Test setting component status now := time.Now() - componentStatus := &types.Status{ + componentStatus := types.Status{ State: types.StateRunning, Description: "Installing k0s", LastUpdated: now, @@ -270,7 +270,7 @@ func TestMemoryStore_ConcurrentAccess(t *testing.T) { go func(id int) { defer wg.Done() for j := 0; j < numOperations; j++ { - status := &types.Status{ + status := types.Status{ State: types.StateRunning, Description: "Concurrent component test", } @@ -293,14 +293,12 @@ func TestMemoryStore_ConcurrentAccess(t *testing.T) { } func TestMemoryStore_StatusDescWithoutStatus(t *testing.T) { - store := &memoryStore{ - infra: &types.Infra{ - Status: nil, // No status set - }, + store := &MemoryStore{ + infra: types.Infra{}, } // Test setting status description when status is nil err := store.SetStatusDesc("Should fail") assert.Error(t, err) - assert.Contains(t, err.Error(), "status not set") + assert.Contains(t, err.Error(), "state not set") } diff --git a/api/internal/store/installation/store.go b/api/internal/store/installation/store.go new file mode 100644 index 000000000..5ec7cedfd --- /dev/null +++ b/api/internal/store/installation/store.go @@ -0,0 +1,80 @@ +package installation + +import ( + "sync" + + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/tiendc/go-deepcopy" +) + +var _ Store = &MemoryStore{} + +type Store interface { + GetConfig() (types.InstallationConfig, error) + SetConfig(cfg types.InstallationConfig) error + GetStatus() (types.Status, error) + SetStatus(status types.Status) error +} + +type MemoryStore struct { + mu sync.RWMutex + installation types.Installation +} + +type StoreOption func(*MemoryStore) + +func WithInstallation(installation types.Installation) StoreOption { + return func(s *MemoryStore) { + s.installation = installation + } +} + +func NewMemoryStore(opts ...StoreOption) *MemoryStore { + s := &MemoryStore{} + + for _, opt := range opts { + opt(s) + } + + return s +} + +func (s *MemoryStore) GetConfig() (types.InstallationConfig, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var config types.InstallationConfig + if err := deepcopy.Copy(&config, &s.installation.Config); err != nil { + return types.InstallationConfig{}, err + } + + return config, nil +} + +func (s *MemoryStore) SetConfig(cfg types.InstallationConfig) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.installation.Config = cfg + return nil +} + +func (s *MemoryStore) GetStatus() (types.Status, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var status types.Status + if err := deepcopy.Copy(&status, &s.installation.Status); err != nil { + return types.Status{}, err + } + + return status, nil +} + +func (s *MemoryStore) SetStatus(status types.Status) error { + s.mu.Lock() + defer s.mu.Unlock() + s.installation.Status = status + + return nil +} diff --git a/api/internal/store/installation/store_mock.go b/api/internal/store/installation/store_mock.go new file mode 100644 index 000000000..8339f8501 --- /dev/null +++ b/api/internal/store/installation/store_mock.go @@ -0,0 +1,43 @@ +package installation + +import ( + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/stretchr/testify/mock" +) + +var _ Store = (*MockStore)(nil) + +// MockStore is a mock implementation of the InstallationStore interface +type MockStore struct { + mock.Mock +} + +// GetConfig mocks the GetConfig method +func (m *MockStore) GetConfig() (types.InstallationConfig, error) { + args := m.Called() + if args.Get(0) == nil { + return types.InstallationConfig{}, args.Error(1) + } + return args.Get(0).(types.InstallationConfig), args.Error(1) +} + +// SetConfig mocks the SetConfig method +func (m *MockStore) SetConfig(cfg types.InstallationConfig) error { + args := m.Called(cfg) + return args.Error(0) +} + +// GetStatus mocks the GetStatus method +func (m *MockStore) GetStatus() (types.Status, error) { + args := m.Called() + if args.Get(0) == nil { + return types.Status{}, args.Error(1) + } + return args.Get(0).(types.Status), args.Error(1) +} + +// SetStatus mocks the SetStatus method +func (m *MockStore) SetStatus(status types.Status) error { + args := m.Called(status) + return args.Error(0) +} diff --git a/api/internal/managers/installation/store_test.go b/api/internal/store/installation/store_test.go similarity index 79% rename from api/internal/managers/installation/store_test.go rename to api/internal/store/installation/store_test.go index 3eca603d0..9abc067cb 100644 --- a/api/internal/managers/installation/store_test.go +++ b/api/internal/store/installation/store_test.go @@ -10,8 +10,8 @@ import ( ) func TestNewMemoryStore(t *testing.T) { - inst := types.NewInstallation() - store := NewMemoryStore(inst) + inst := types.Installation{} + store := NewMemoryStore(WithInstallation(inst)) assert.NotNil(t, store) assert.NotNil(t, store.installation) @@ -19,32 +19,32 @@ func TestNewMemoryStore(t *testing.T) { } func TestMemoryStore_GetConfig(t *testing.T) { - inst := &types.Installation{ - Config: &types.InstallationConfig{ + inst := types.Installation{ + Config: types.InstallationConfig{ AdminConsolePort: 8080, DataDirectory: "/some/dir", }, } - store := NewMemoryStore(inst) + store := NewMemoryStore(WithInstallation(inst)) config, err := store.GetConfig() require.NoError(t, err) assert.NotNil(t, config) - assert.Equal(t, &types.InstallationConfig{ + assert.Equal(t, types.InstallationConfig{ AdminConsolePort: 8080, DataDirectory: "/some/dir", }, config) } func TestMemoryStore_SetConfig(t *testing.T) { - inst := &types.Installation{ - Config: &types.InstallationConfig{ + inst := types.Installation{ + Config: types.InstallationConfig{ AdminConsolePort: 1000, DataDirectory: "/a/different/dir", }, } - store := NewMemoryStore(inst) + store := NewMemoryStore(WithInstallation(inst)) expectedConfig := types.InstallationConfig{ AdminConsolePort: 8080, DataDirectory: "/some/dir", @@ -57,35 +57,35 @@ func TestMemoryStore_SetConfig(t *testing.T) { // Verify the config was stored actualConfig, err := store.GetConfig() require.NoError(t, err) - assert.Equal(t, &expectedConfig, actualConfig) + assert.Equal(t, expectedConfig, actualConfig) } func TestMemoryStore_GetStatus(t *testing.T) { - inst := &types.Installation{ - Status: &types.Status{ + inst := types.Installation{ + Status: types.Status{ State: "failed", Description: "Failure", }, } - store := NewMemoryStore(inst) + store := NewMemoryStore(WithInstallation(inst)) status, err := store.GetStatus() require.NoError(t, err) assert.NotNil(t, status) - assert.Equal(t, &types.Status{ + assert.Equal(t, types.Status{ State: "failed", Description: "Failure", }, status) } func TestMemoryStore_SetStatus(t *testing.T) { - inst := &types.Installation{ - Status: &types.Status{ + inst := types.Installation{ + Status: types.Status{ State: "failed", Description: "Failure", }, } - store := NewMemoryStore(inst) + store := NewMemoryStore(WithInstallation(inst)) expectedStatus := types.Status{ State: "running", Description: "Running", @@ -98,13 +98,13 @@ func TestMemoryStore_SetStatus(t *testing.T) { // Verify the status was stored actualStatus, err := store.GetStatus() require.NoError(t, err) - assert.Equal(t, &expectedStatus, actualStatus) + assert.Equal(t, expectedStatus, actualStatus) } // Useful to test concurrent access with -race flag func TestMemoryStore_ConcurrentAccess(t *testing.T) { - inst := types.NewInstallation() - store := NewMemoryStore(inst) + inst := types.Installation{} + store := NewMemoryStore(WithInstallation(inst)) var wg sync.WaitGroup // Test concurrent reads and writes diff --git a/api/internal/store/preflight/store.go b/api/internal/store/preflight/store.go new file mode 100644 index 000000000..d413d72ee --- /dev/null +++ b/api/internal/store/preflight/store.go @@ -0,0 +1,114 @@ +package preflight + +import ( + "sync" + + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/tiendc/go-deepcopy" +) + +var _ Store = &MemoryStore{} + +type Store interface { + GetTitles() ([]string, error) + SetTitles(titles []string) error + GetOutput() (*types.HostPreflightsOutput, error) + SetOutput(output *types.HostPreflightsOutput) error + GetStatus() (types.Status, error) + SetStatus(status types.Status) error + IsRunning() bool +} + +type MemoryStore struct { + mu sync.RWMutex + hostPreflight types.HostPreflights +} + +type StoreOption func(*MemoryStore) + +func WithHostPreflight(hostPreflight types.HostPreflights) StoreOption { + return func(s *MemoryStore) { + s.hostPreflight = hostPreflight + } +} + +func NewMemoryStore(opts ...StoreOption) *MemoryStore { + s := &MemoryStore{} + + for _, opt := range opts { + opt(s) + } + + return s +} + +func (s *MemoryStore) GetTitles() ([]string, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var titles []string + if err := deepcopy.Copy(&titles, &s.hostPreflight.Titles); err != nil { + return nil, err + } + + return titles, nil +} + +func (s *MemoryStore) SetTitles(titles []string) error { + s.mu.Lock() + defer s.mu.Unlock() + s.hostPreflight.Titles = titles + + return nil +} + +func (s *MemoryStore) GetOutput() (*types.HostPreflightsOutput, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + if s.hostPreflight.Output == nil { + return nil, nil + } + + var output *types.HostPreflightsOutput + if err := deepcopy.Copy(&output, &s.hostPreflight.Output); err != nil { + return nil, err + } + + return output, nil +} + +func (s *MemoryStore) SetOutput(output *types.HostPreflightsOutput) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.hostPreflight.Output = output + return nil +} + +func (s *MemoryStore) GetStatus() (types.Status, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + var status types.Status + if err := deepcopy.Copy(&status, &s.hostPreflight.Status); err != nil { + return types.Status{}, err + } + + return status, nil +} + +func (s *MemoryStore) SetStatus(status types.Status) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.hostPreflight.Status = status + return nil +} + +func (s *MemoryStore) IsRunning() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.hostPreflight.Status.State == types.StateRunning +} diff --git a/api/internal/managers/preflight/store_mock.go b/api/internal/store/preflight/store_mock.go similarity index 55% rename from api/internal/managers/preflight/store_mock.go rename to api/internal/store/preflight/store_mock.go index fc4ec22ac..59b0ea951 100644 --- a/api/internal/managers/preflight/store_mock.go +++ b/api/internal/store/preflight/store_mock.go @@ -5,15 +5,15 @@ import ( "github.com/stretchr/testify/mock" ) -var _ HostPreflightStore = (*MockHostPreflightStore)(nil) +var _ Store = (*MockStore)(nil) -// MockHostPreflightStore is a mock implementation of the HostPreflightStore interface -type MockHostPreflightStore struct { +// MockStore is a mock implementation of the Store interface +type MockStore struct { mock.Mock } // GetTitles mocks the GetTitles method -func (m *MockHostPreflightStore) GetTitles() ([]string, error) { +func (m *MockStore) GetTitles() ([]string, error) { args := m.Called() if args.Get(0) == nil { return nil, args.Error(1) @@ -22,13 +22,13 @@ func (m *MockHostPreflightStore) GetTitles() ([]string, error) { } // SetTitles mocks the SetTitles method -func (m *MockHostPreflightStore) SetTitles(titles []string) error { +func (m *MockStore) SetTitles(titles []string) error { args := m.Called(titles) return args.Error(0) } // GetOutput mocks the GetOutput method -func (m *MockHostPreflightStore) GetOutput() (*types.HostPreflightsOutput, error) { +func (m *MockStore) GetOutput() (*types.HostPreflightsOutput, error) { args := m.Called() if args.Get(0) == nil { return nil, args.Error(1) @@ -37,28 +37,28 @@ func (m *MockHostPreflightStore) GetOutput() (*types.HostPreflightsOutput, error } // SetOutput mocks the SetOutput method -func (m *MockHostPreflightStore) SetOutput(output *types.HostPreflightsOutput) error { +func (m *MockStore) SetOutput(output *types.HostPreflightsOutput) error { args := m.Called(output) return args.Error(0) } // GetStatus mocks the GetStatus method -func (m *MockHostPreflightStore) GetStatus() (*types.Status, error) { +func (m *MockStore) GetStatus() (types.Status, error) { args := m.Called() if args.Get(0) == nil { - return nil, args.Error(1) + return types.Status{}, args.Error(1) } - return args.Get(0).(*types.Status), args.Error(1) + return args.Get(0).(types.Status), args.Error(1) } // SetStatus mocks the SetStatus method -func (m *MockHostPreflightStore) SetStatus(status *types.Status) error { +func (m *MockStore) SetStatus(status types.Status) error { args := m.Called(status) return args.Error(0) } // IsRunning mocks the IsRunning method -func (m *MockHostPreflightStore) IsRunning() bool { +func (m *MockStore) IsRunning() bool { args := m.Called() return args.Bool(0) } diff --git a/api/internal/managers/preflight/store_test.go b/api/internal/store/preflight/store_test.go similarity index 80% rename from api/internal/managers/preflight/store_test.go rename to api/internal/store/preflight/store_test.go index f3f37e5f9..6e3d391ac 100644 --- a/api/internal/managers/preflight/store_test.go +++ b/api/internal/store/preflight/store_test.go @@ -11,8 +11,8 @@ import ( ) func TestNewMemoryStore(t *testing.T) { - hostPreflight := types.NewHostPreflights() - store := NewMemoryStore(hostPreflight) + hostPreflight := types.HostPreflights{} + store := NewMemoryStore(WithHostPreflight(hostPreflight)) assert.NotNil(t, store) assert.NotNil(t, store.hostPreflight) @@ -20,10 +20,10 @@ func TestNewMemoryStore(t *testing.T) { } func TestMemoryStore_GetTitles(t *testing.T) { - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Titles: []string{"Memory Check", "Disk Space Check", "Network Check"}, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) titles, err := store.GetTitles() @@ -33,10 +33,10 @@ func TestMemoryStore_GetTitles(t *testing.T) { } func TestMemoryStore_GetTitles_Empty(t *testing.T) { - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Titles: []string{}, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) titles, err := store.GetTitles() @@ -46,10 +46,10 @@ func TestMemoryStore_GetTitles_Empty(t *testing.T) { } func TestMemoryStore_SetTitles(t *testing.T) { - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Titles: []string{"Old Title"}, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) expectedTitles := []string{"CPU Check", "RAM Check", "Storage Check"} err := store.SetTitles(expectedTitles) @@ -64,10 +64,10 @@ func TestMemoryStore_SetTitles(t *testing.T) { func TestMemoryStore_GetOutput(t *testing.T) { output := &types.HostPreflightsOutput{} - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Output: output, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) result, err := store.GetOutput() @@ -76,10 +76,10 @@ func TestMemoryStore_GetOutput(t *testing.T) { } func TestMemoryStore_GetOutput_Nil(t *testing.T) { - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Output: nil, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) result, err := store.GetOutput() @@ -88,10 +88,10 @@ func TestMemoryStore_GetOutput_Nil(t *testing.T) { } func TestMemoryStore_SetOutput(t *testing.T) { - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Output: nil, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) expectedOutput := &types.HostPreflightsOutput{} err := store.SetOutput(expectedOutput) @@ -105,10 +105,10 @@ func TestMemoryStore_SetOutput(t *testing.T) { } func TestMemoryStore_SetOutput_Nil(t *testing.T) { - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Output: &types.HostPreflightsOutput{}, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) err := store.SetOutput(nil) @@ -121,15 +121,15 @@ func TestMemoryStore_SetOutput_Nil(t *testing.T) { } func TestMemoryStore_GetStatus(t *testing.T) { - status := &types.Status{ + status := types.Status{ State: types.StateRunning, Description: "Running host preflights", LastUpdated: time.Now(), } - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Status: status, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) result, err := store.GetStatus() @@ -139,14 +139,14 @@ func TestMemoryStore_GetStatus(t *testing.T) { } func TestMemoryStore_SetStatus(t *testing.T) { - hostPreflight := &types.HostPreflights{ - Status: &types.Status{ + hostPreflight := types.HostPreflights{ + Status: types.Status{ State: types.StateFailed, Description: "Failed", }, } - store := NewMemoryStore(hostPreflight) - expectedStatus := &types.Status{ + store := NewMemoryStore(WithHostPreflight(hostPreflight)) + expectedStatus := types.Status{ State: types.StateSucceeded, Description: "Host preflights passed", LastUpdated: time.Now(), @@ -165,12 +165,12 @@ func TestMemoryStore_SetStatus(t *testing.T) { func TestMemoryStore_IsRunning(t *testing.T) { tests := []struct { name string - status *types.Status + status types.Status expectedBool bool }{ { name: "is running when state is running", - status: &types.Status{ + status: types.Status{ State: types.StateRunning, Description: "Running host preflights", }, @@ -178,7 +178,7 @@ func TestMemoryStore_IsRunning(t *testing.T) { }, { name: "is not running when state is succeeded", - status: &types.Status{ + status: types.Status{ State: types.StateSucceeded, Description: "Host preflights passed", }, @@ -186,7 +186,7 @@ func TestMemoryStore_IsRunning(t *testing.T) { }, { name: "is not running when state is failed", - status: &types.Status{ + status: types.Status{ State: types.StateFailed, Description: "Host preflights failed", }, @@ -194,7 +194,7 @@ func TestMemoryStore_IsRunning(t *testing.T) { }, { name: "is not running when state is pending", - status: &types.Status{ + status: types.Status{ State: types.StatePending, Description: "Pending host preflights", }, @@ -204,10 +204,10 @@ func TestMemoryStore_IsRunning(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - hostPreflight := &types.HostPreflights{ + hostPreflight := types.HostPreflights{ Status: tt.status, } - store := NewMemoryStore(hostPreflight) + store := NewMemoryStore(WithHostPreflight(hostPreflight)) result := store.IsRunning() @@ -218,8 +218,8 @@ func TestMemoryStore_IsRunning(t *testing.T) { // Useful to test concurrent access with -race flag func TestMemoryStore_ConcurrentAccess(t *testing.T) { - hostPreflight := types.NewHostPreflights() - store := NewMemoryStore(hostPreflight) + hostPreflight := types.HostPreflights{} + store := NewMemoryStore(WithHostPreflight(hostPreflight)) var wg sync.WaitGroup // Test concurrent reads and writes @@ -279,7 +279,7 @@ func TestMemoryStore_ConcurrentAccess(t *testing.T) { go func(id int) { defer wg.Done() for j := 0; j < numOperations; j++ { - status := &types.Status{ + status := types.Status{ State: types.StateRunning, Description: "Running", LastUpdated: time.Now(), diff --git a/api/internal/store/store.go b/api/internal/store/store.go new file mode 100644 index 000000000..28f8d2dfd --- /dev/null +++ b/api/internal/store/store.go @@ -0,0 +1,87 @@ +package store + +import ( + "github.com/replicatedhq/embedded-cluster/api/internal/store/infra" + "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" +) + +var _ Store = &MemoryStore{} + +// Store is the global interface that combines all substores +type Store interface { + // PreflightStore provides access to host preflight operations + PreflightStore() preflight.Store + + // InstallationStore provides access to installation operations + InstallationStore() installation.Store + + // InfraStore provides access to infrastructure operations + InfraStore() infra.Store +} + +// StoreOption is a function that configures a store +type StoreOption func(*MemoryStore) + +// WithPreflightStore sets the preflight store +func WithPreflightStore(store preflight.Store) StoreOption { + return func(s *MemoryStore) { + s.preflightStore = store + } +} + +// WithInstallationStore sets the installation store +func WithInstallationStore(store installation.Store) StoreOption { + return func(s *MemoryStore) { + s.installationStore = store + } +} + +// WithInfraStore sets the infra store +func WithInfraStore(store infra.Store) StoreOption { + return func(s *MemoryStore) { + s.infraStore = store + } +} + +// MemoryStore is an in-memory implementation of the global Store interface +type MemoryStore struct { + preflightStore preflight.Store + installationStore installation.Store + infraStore infra.Store +} + +// NewMemoryStore creates a new memory store with the given options +func NewMemoryStore(opts ...StoreOption) Store { + s := &MemoryStore{} + + for _, opt := range opts { + opt(s) + } + + if s.preflightStore == nil { + s.preflightStore = preflight.NewMemoryStore() + } + + if s.installationStore == nil { + s.installationStore = installation.NewMemoryStore() + } + + if s.infraStore == nil { + s.infraStore = infra.NewMemoryStore() + } + + return s +} + +func (s *MemoryStore) PreflightStore() preflight.Store { + return s.preflightStore +} + +func (s *MemoryStore) InstallationStore() installation.Store { + return s.installationStore +} + +func (s *MemoryStore) InfraStore() infra.Store { + return s.infraStore +} diff --git a/api/types/infra.go b/api/types/infra.go index a5f41eadc..22db09a25 100644 --- a/api/types/infra.go +++ b/api/types/infra.go @@ -3,17 +3,10 @@ package types type Infra struct { Components []InfraComponent `json:"components"` Logs string `json:"logs"` - Status *Status `json:"status"` + Status Status `json:"status"` } type InfraComponent struct { - Name string `json:"name"` - Status *Status `json:"status"` -} - -func NewInfra() *Infra { - return &Infra{ - Components: []InfraComponent{}, - Status: NewStatus(), - } + Name string `json:"name"` + Status Status `json:"status"` } diff --git a/api/types/install.go b/api/types/install.go index 5cb37d9e0..573c67336 100644 --- a/api/types/install.go +++ b/api/types/install.go @@ -3,24 +3,12 @@ package types // Install represents the install workflow state type Install struct { Steps InstallSteps `json:"steps"` - Status *Status `json:"status"` + Status Status `json:"status"` } // InstallSteps represents the steps of the install workflow type InstallSteps struct { - Installation *Installation `json:"installation"` - HostPreflight *HostPreflights `json:"hostPreflight"` - Infra *Infra `json:"infra"` -} - -// NewInstall initializes a new install workflow state -func NewInstall() *Install { - return &Install{ - Steps: InstallSteps{ - Installation: NewInstallation(), - HostPreflight: NewHostPreflights(), - Infra: NewInfra(), - }, - Status: NewStatus(), - } + Installation Installation `json:"installation"` + HostPreflight HostPreflights `json:"hostPreflight"` + Infra Infra `json:"infra"` } diff --git a/api/types/installation.go b/api/types/installation.go index 3b2dca16f..19d7ef5cd 100644 --- a/api/types/installation.go +++ b/api/types/installation.go @@ -1,8 +1,8 @@ package types type Installation struct { - Config *InstallationConfig `json:"config"` - Status *Status `json:"status"` + Config InstallationConfig `json:"config"` + Status Status `json:"status"` } // InstallationConfig represents the configuration for an installation @@ -18,11 +18,3 @@ type InstallationConfig struct { ServiceCIDR string `json:"serviceCidr"` GlobalCIDR string `json:"globalCidr"` } - -// NewInstallation initializes a new installation state -func NewInstallation() *Installation { - return &Installation{ - Config: &InstallationConfig{}, - Status: NewStatus(), - } -} diff --git a/api/types/preflight.go b/api/types/preflight.go index 1948d2ced..e5efc6dbc 100644 --- a/api/types/preflight.go +++ b/api/types/preflight.go @@ -8,7 +8,7 @@ type PostInstallRunHostPreflightsRequest struct { type HostPreflights struct { Titles []string `json:"titles"` Output *HostPreflightsOutput `json:"output"` - Status *Status `json:"status"` + Status Status `json:"status"` } type HostPreflightsOutput struct { @@ -23,12 +23,6 @@ type HostPreflightsRecord struct { Message string `json:"message"` } -func NewHostPreflights() *HostPreflights { - return &HostPreflights{ - Status: NewStatus(), - } -} - // HasFail returns true if any of the preflight checks failed. func (o HostPreflightsOutput) HasFail() bool { return len(o.Fail) > 0 diff --git a/api/types/responses.go b/api/types/responses.go index 44db4cdf1..fd560eb78 100644 --- a/api/types/responses.go +++ b/api/types/responses.go @@ -4,5 +4,5 @@ package types type InstallHostPreflightsStatusResponse struct { Titles []string `json:"titles"` Output *HostPreflightsOutput `json:"output,omitempty"` - Status *Status `json:"status,omitempty"` + Status Status `json:"status,omitempty"` } diff --git a/api/types/status.go b/api/types/status.go index 19a769e42..bf9d8af5b 100644 --- a/api/types/status.go +++ b/api/types/status.go @@ -21,19 +21,9 @@ const ( StateFailed State = "Failed" ) -func NewStatus() *Status { - return &Status{ - State: StatePending, - } -} - -func ValidateStatus(status *Status) error { +func ValidateStatus(status Status) error { var ve *APIError - if status == nil { - return NewBadRequestError(errors.New("a status is required")) - } - switch status.State { case StatePending, StateRunning, StateSucceeded, StateFailed: // valid states diff --git a/api/types/status_test.go b/api/types/status_test.go index 8032d18de..b6f3476a0 100644 --- a/api/types/status_test.go +++ b/api/types/status_test.go @@ -10,12 +10,12 @@ import ( func TestValidateStatus(t *testing.T) { tests := []struct { name string - status *Status + status Status expectedErr bool }{ { name: "valid status - pending", - status: &Status{ + status: Status{ State: StatePending, Description: "Installation pending", LastUpdated: time.Now(), @@ -24,7 +24,7 @@ func TestValidateStatus(t *testing.T) { }, { name: "valid status - running", - status: &Status{ + status: Status{ State: StateRunning, Description: "Installation in progress", LastUpdated: time.Now(), @@ -33,7 +33,7 @@ func TestValidateStatus(t *testing.T) { }, { name: "valid status - succeeded", - status: &Status{ + status: Status{ State: StateSucceeded, Description: "Installation completed successfully", LastUpdated: time.Now(), @@ -42,7 +42,7 @@ func TestValidateStatus(t *testing.T) { }, { name: "valid status - failed", - status: &Status{ + status: Status{ State: StateFailed, Description: "Installation failed", LastUpdated: time.Now(), @@ -50,13 +50,13 @@ func TestValidateStatus(t *testing.T) { expectedErr: false, }, { - name: "nil status", - status: nil, + name: "empty status", + status: Status{}, expectedErr: true, }, { name: "invalid state", - status: &Status{ + status: Status{ State: "Invalid", Description: "Invalid state", LastUpdated: time.Now(), @@ -65,7 +65,7 @@ func TestValidateStatus(t *testing.T) { }, { name: "missing description", - status: &Status{ + status: Status{ State: StateRunning, Description: "", LastUpdated: time.Now(), diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go index 5401dac0c..21bf02119 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -198,7 +198,7 @@ func markUIInstallComplete(password string, managerPort int, installErr error) e description = "Installation succeeded" } - _, err := apiClient.SetInstallStatus(&apitypes.Status{ + _, err := apiClient.SetInstallStatus(apitypes.Status{ State: state, Description: description, LastUpdated: time.Now(), diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 8f28cdd1c..853ddfbc2 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -486,7 +486,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run if err != nil { return fmt.Errorf("unable to get registry cluster IP: %w", err) } - if err := airgap.AddInsecureRegistry(fmt.Sprintf("%s:5000", registryIP)); err != nil { + if err := hostutils.AddInsecureRegistry(fmt.Sprintf("%s:5000", registryIP)); err != nil { return fmt.Errorf("unable to add insecure registry: %w", err) } @@ -519,7 +519,7 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run return fmt.Errorf("unable to update installation: %w", err) } - if err = support.CreateHostSupportBundle(); err != nil { + if err = support.CreateHostSupportBundle(ctx, kcli); err != nil { logrus.Warnf("Unable to create host support bundle: %v", err) } diff --git a/cmd/installer/cli/join.go b/cmd/installer/cli/join.go index 2b25a5e51..974cce330 100644 --- a/cmd/installer/cli/join.go +++ b/cmd/installer/cli/join.go @@ -390,7 +390,7 @@ func installAndJoinCluster(ctx context.Context, rc runtimeconfig.RuntimeConfig, } if jcmd.AirgapRegistryAddress != "" { - if err := airgap.AddInsecureRegistry(jcmd.AirgapRegistryAddress); err != nil { + if err := hostutils.AddInsecureRegistry(jcmd.AirgapRegistryAddress); err != nil { return fmt.Errorf("unable to add insecure registry: %w", err) } } diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index 8b0a40acb..5b08701af 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -25,7 +25,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" "github.com/replicatedhq/embedded-cluster/pkg/addons" addontypes "github.com/replicatedhq/embedded-cluster/pkg/addons/types" - "github.com/replicatedhq/embedded-cluster/pkg/airgap" "github.com/replicatedhq/embedded-cluster/pkg/constants" "github.com/replicatedhq/embedded-cluster/pkg/disasterrecovery" "github.com/replicatedhq/embedded-cluster/pkg/helm" @@ -665,7 +664,7 @@ func runRestoreRegistry(ctx context.Context, flags InstallCmdFlags, backupToRest return fmt.Errorf("unable to read registry address from backup") } - if err := airgap.AddInsecureRegistry(registryAddress); err != nil { + if err := hostutils.AddInsecureRegistry(registryAddress); err != nil { return fmt.Errorf("failed to add insecure registry: %w", err) } diff --git a/go.mod b/go.mod index 41030bb9d..ac40c8e57 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/swaggo/http-swagger/v2 v2.0.2 github.com/swaggo/swag/v2 v2.0.0-rc4 + github.com/tiendc/go-deepcopy v1.6.1 github.com/urfave/cli/v2 v2.27.7 github.com/vmware-tanzu/velero v1.16.1 go.uber.org/multierr v1.11.0 diff --git a/go.sum b/go.sum index 5031b3bb9..c4a311753 100644 --- a/go.sum +++ b/go.sum @@ -1563,6 +1563,8 @@ github.com/sylabs/sif/v2 v2.20.2 h1:HGEPzauCHhIosw5o6xmT3jczuKEuaFzSfdjAsH33vYw= github.com/sylabs/sif/v2 v2.20.2/go.mod h1:WyYryGRaR4Wp21SAymm5pK0p45qzZCSRiZMFvUZiuhc= github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tiendc/go-deepcopy v1.6.1 h1:uVRTItFeNHkMcLueHS7OCsxgxT9P8MzGB/taUa2Y4Tk= +github.com/tiendc/go-deepcopy v1.6.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= diff --git a/operator/pkg/upgrade/upgrade.go b/operator/pkg/upgrade/upgrade.go index c431a8242..59a5baeda 100644 --- a/operator/pkg/upgrade/upgrade.go +++ b/operator/pkg/upgrade/upgrade.go @@ -67,7 +67,7 @@ func Upgrade(ctx context.Context, cli client.Client, hcli helm.Client, rc runtim return fmt.Errorf("upgrade extensions: %w", err) } - err = support.CreateHostSupportBundle() + err = support.CreateHostSupportBundle(ctx, cli) if err != nil { slog.Error("Failed to upgrade host support bundle", "error", err) } diff --git a/pkg/airgap/containerd.go b/pkg-new/hostutils/containerd.go similarity index 91% rename from pkg/airgap/containerd.go rename to pkg-new/hostutils/containerd.go index a730a100f..42b522405 100644 --- a/pkg/airgap/containerd.go +++ b/pkg-new/hostutils/containerd.go @@ -1,4 +1,4 @@ -package airgap +package hostutils import ( "fmt" @@ -17,7 +17,7 @@ const registryConfigTemplate = ` // AddInsecureRegistry adds a registry to the list of registries that // are allowed to be accessed over HTTP. -func AddInsecureRegistry(registry string) error { +func (h *HostUtils) AddInsecureRegistry(registry string) error { parentDir := runtimeconfig.K0sContainerdConfigPath contents := fmt.Sprintf(registryConfigTemplate, registry) diff --git a/pkg-new/hostutils/interface.go b/pkg-new/hostutils/interface.go index 4ded1db18..9feb63a9f 100644 --- a/pkg-new/hostutils/interface.go +++ b/pkg-new/hostutils/interface.go @@ -27,6 +27,7 @@ type HostUtilsInterface interface { MaterializeFiles(rc runtimeconfig.RuntimeConfig, airgapBundle string) error CreateSystemdUnitFiles(ctx context.Context, logger logrus.FieldLogger, rc runtimeconfig.RuntimeConfig, isWorker bool) error WriteLocalArtifactMirrorDropInFile(rc runtimeconfig.RuntimeConfig) error + AddInsecureRegistry(registry string) error } // Convenience functions @@ -67,3 +68,7 @@ func CreateSystemdUnitFiles(ctx context.Context, logger logrus.FieldLogger, rc r func WriteLocalArtifactMirrorDropInFile(rc runtimeconfig.RuntimeConfig) error { return h.WriteLocalArtifactMirrorDropInFile(rc) } + +func AddInsecureRegistry(registry string) error { + return h.AddInsecureRegistry(registry) +} diff --git a/pkg-new/hostutils/mock.go b/pkg-new/hostutils/mock.go index a2b6d7660..9154441ec 100644 --- a/pkg-new/hostutils/mock.go +++ b/pkg-new/hostutils/mock.go @@ -68,3 +68,9 @@ func (m *MockHostUtils) WriteLocalArtifactMirrorDropInFile(rc runtimeconfig.Runt args := m.Called(rc) return args.Error(0) } + +// AddInsecureRegistry mocks the AddInsecureRegistry method +func (m *MockHostUtils) AddInsecureRegistry(registry string) error { + args := m.Called(registry) + return args.Error(0) +} diff --git a/pkg-new/k0s/interface.go b/pkg-new/k0s/interface.go index 19e65f888..021647817 100644 --- a/pkg-new/k0s/interface.go +++ b/pkg-new/k0s/interface.go @@ -4,6 +4,8 @@ import ( "context" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) var ( @@ -33,7 +35,10 @@ type K0sVars struct { type K0sInterface interface { GetStatus(ctx context.Context) (*K0sStatus, error) + Install(rc runtimeconfig.RuntimeConfig) 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) + PatchK0sConfig(path string, patch string) error WaitForK0s() error } @@ -41,10 +46,22 @@ func GetStatus(ctx context.Context) (*K0sStatus, error) { return _k0s.GetStatus(ctx) } +func Install(rc runtimeconfig.RuntimeConfig) error { + return _k0s.Install(rc) +} + 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 PatchK0sConfig(path string, patch string) error { + return _k0s.PatchK0sConfig(path, patch) +} + func WaitForK0s() error { return _k0s.WaitForK0s() } diff --git a/pkg-new/k0s/k0s.go b/pkg-new/k0s/k0s.go index f455005e4..3edc1d65b 100644 --- a/pkg-new/k0s/k0s.go +++ b/pkg-new/k0s/k0s.go @@ -30,6 +30,10 @@ var _ K0sInterface = (*K0s)(nil) type K0s struct { } +func New() *K0s { + return &K0s{} +} + // GetStatus calls the k0s status command and returns information about system init, PID, k0s role, // kubeconfig and similar. func (k *K0s) GetStatus(ctx context.Context) (*K0sStatus, error) { @@ -52,7 +56,7 @@ func (k *K0s) GetStatus(ctx context.Context) (*K0sStatus, error) { // Install runs the k0s install command and waits for it to finish. If no configuration // is found one is generated. -func Install(rc runtimeconfig.RuntimeConfig) error { +func (k *K0s) Install(rc runtimeconfig.RuntimeConfig) error { ourbin := rc.PathToEmbeddedClusterBinary("k0s") hstbin := runtimeconfig.K0sBinaryPath if err := helpers.MoveFile(ourbin, hstbin); err != nil { @@ -131,7 +135,7 @@ 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 WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle string, podCIDR string, serviceCIDR string, eucfg *ecv1beta1.Config, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, 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) @@ -190,7 +194,7 @@ func applyUnsupportedOverrides(cfg *k0sv1beta1.ClusterConfig, eucfg *ecv1beta1.C } // PatchK0sConfig patches the created k0s config with the unsupported overrides passed in. -func PatchK0sConfig(path string, patch string) error { +func (k *K0s) PatchK0sConfig(path string, patch string) error { if len(patch) == 0 { return nil } diff --git a/pkg-new/k0s/mock.go b/pkg-new/k0s/mock.go index 210971aea..9f6627ecb 100644 --- a/pkg-new/k0s/mock.go +++ b/pkg-new/k0s/mock.go @@ -3,6 +3,9 @@ package k0s import ( "context" + k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/mock" ) @@ -22,12 +25,30 @@ func (m *MockK0s) GetStatus(ctx context.Context) (*K0sStatus, error) { return args.Get(0).(*K0sStatus), args.Error(1) } +// Install mocks the Install method +func (m *MockK0s) Install(rc runtimeconfig.RuntimeConfig) error { + args := m.Called(rc) + return args.Error(0) +} + // IsInstalled mocks the IsInstalled method func (m *MockK0s) IsInstalled() (bool, error) { args := m.Called() 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) + return args.Get(0).(*k0sv1beta1.ClusterConfig), args.Error(1) +} + +// PatchK0sConfig mocks the PatchK0sConfig method +func (m *MockK0s) PatchK0sConfig(path string, patch string) error { + args := m.Called(path, patch) + return args.Error(0) +} + // WaitForK0s mocks the WaitForK0s method func (m *MockK0s) WaitForK0s() error { args := m.Called() diff --git a/pkg/dryrun/k0s.go b/pkg/dryrun/k0s.go index 9e6c5edc5..997e391a0 100644 --- a/pkg/dryrun/k0s.go +++ b/pkg/dryrun/k0s.go @@ -3,6 +3,8 @@ package dryrun import ( "context" + k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) @@ -18,13 +20,21 @@ func (c *K0s) GetStatus(ctx context.Context) (*k0s.K0sStatus, error) { } func (c *K0s) Install(rc runtimeconfig.RuntimeConfig) error { - return nil // TODO: implement + return k0s.New().Install(rc) // actual implementation accounts for dryrun } 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) PatchK0sConfig(path string, patch string) error { + return k0s.New().PatchK0sConfig(path, patch) // actual implementation accounts for dryrun +} + func (c *K0s) WaitForK0s() error { return nil } diff --git a/pkg/support/hostbundle.go b/pkg/support/hostbundle.go index eeef21142..b66740dcf 100644 --- a/pkg/support/hostbundle.go +++ b/pkg/support/hostbundle.go @@ -6,13 +6,13 @@ import ( _ "embed" "fmt" - "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" sb "github.com/replicatedhq/troubleshoot/pkg/supportbundle" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" serializer "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" ) var ( @@ -24,7 +24,7 @@ func GetRemoteHostSupportBundleSpec() []byte { return _hostSupportBundleRemote } -func CreateHostSupportBundle() error { +func CreateHostSupportBundle(ctx context.Context, kcli client.Client) error { specFile := GetRemoteHostSupportBundleSpec() var b bytes.Buffer @@ -61,12 +61,6 @@ func CreateHostSupportBundle() error { }, } - ctx := context.Background() - kcli, err := kubeutils.KubeClient() - if err != nil { - return fmt.Errorf("unable to create kube client: %w", err) - } - err = kcli.Create(ctx, configMap) if err != nil && !errors.IsAlreadyExists(err) { return fmt.Errorf("unable to create config map: %w", err) From 9825d66d5082f1e38a51ca71d08df664c9a5d51b Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Wed, 18 Jun 2025 14:37:31 -0700 Subject: [PATCH 11/48] Some API cleanup (#2340) --- api/client/client_test.go | 6 ---- api/integration/auth_controller_test.go | 3 +- api/integration/install_test.go | 7 +---- .../managers/installation/config_test.go | 3 -- .../managers/installation/status_test.go | 4 --- api/internal/store/infra/store.go | 28 +++++++++---------- api/internal/store/infra/store_test.go | 6 +--- api/internal/store/installation/store.go | 20 ++++++------- api/internal/store/installation/store_test.go | 3 -- api/internal/store/preflight/store.go | 26 ++++++++--------- api/internal/store/preflight/store_test.go | 2 -- api/internal/store/store.go | 22 +++++++-------- 12 files changed, 51 insertions(+), 79 deletions(-) diff --git a/api/client/client_test.go b/api/client/client_test.go index ef4daed90..696c03b22 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -121,7 +121,6 @@ func TestGetInstallationConfig(t *testing.T) { c := New(server.URL, WithToken("test-token")) config, err := c.GetInstallationConfig() assert.NoError(t, err) - assert.NotNil(t, config) assert.Equal(t, "10.0.0.0/24", config.GlobalCIDR) assert.Equal(t, 8080, config.AdminConsolePort) @@ -179,7 +178,6 @@ func TestConfigureInstallation(t *testing.T) { } status, err := c.ConfigureInstallation(config) assert.NoError(t, err) - assert.NotNil(t, status) assert.Equal(t, types.StateRunning, status.State) assert.Equal(t, "Configuring installation", status.Description) @@ -228,8 +226,6 @@ func TestSetupInfra(t *testing.T) { c := New(server.URL, WithToken("test-token")) infra, err := c.SetupInfra() assert.NoError(t, err) - assert.NotNil(t, infra) - assert.NotNil(t, infra.Status) assert.Equal(t, types.StateRunning, infra.Status.State) assert.Equal(t, "Installing infra", infra.Status.Description) @@ -278,7 +274,6 @@ func TestGetInfraStatus(t *testing.T) { c := New(server.URL, WithToken("test-token")) infra, err := c.GetInfraStatus() assert.NoError(t, err) - assert.NotNil(t, infra) assert.Equal(t, types.StateSucceeded, infra.Status.State) assert.Equal(t, "Installation successful", infra.Status.Description) @@ -331,7 +326,6 @@ func TestSetInstallStatus(t *testing.T) { } newStatus, err := c.SetInstallStatus(status) assert.NoError(t, err) - assert.NotNil(t, newStatus) assert.Equal(t, status, newStatus) // Test error response diff --git a/api/integration/auth_controller_test.go b/api/integration/auth_controller_test.go index a625ba311..8380dd62e 100644 --- a/api/integration/auth_controller_test.go +++ b/api/integration/auth_controller_test.go @@ -162,9 +162,8 @@ func TestAPIClientLogin(t *testing.T) { require.NoError(t, err, "API client login should succeed with correct password") // Verify we can make authenticated requests after login - status, err := c.GetInstallationStatus() + _, err = c.GetInstallationStatus() require.NoError(t, err, "API client should be able to get installation status after successful login") - assert.NotNil(t, status, "Installation status should not be nil") }) // Test failed login with incorrect password diff --git a/api/integration/install_test.go b/api/integration/install_test.go index b9913d361..2bc72727e 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -1012,7 +1012,6 @@ func TestInstallWithAPIClient(t *testing.T) { t.Run("GetInstallationConfig", func(t *testing.T) { config, err := c.GetInstallationConfig() require.NoError(t, err, "GetInstallationConfig should succeed") - assert.NotNil(t, config, "InstallationConfig should not be nil") // Verify values assert.Equal(t, "/tmp/test-data-for-client", config.DataDirectory) @@ -1026,7 +1025,6 @@ func TestInstallWithAPIClient(t *testing.T) { t.Run("GetInstallationStatus", func(t *testing.T) { status, err := c.GetInstallationStatus() require.NoError(t, err, "GetInstallationStatus should succeed") - assert.NotNil(t, status, "InstallationStatus should not be nil") assert.Equal(t, types.StatePending, status.State) assert.Equal(t, "Installation pending", status.Description) }) @@ -1043,9 +1041,8 @@ func TestInstallWithAPIClient(t *testing.T) { } // Configure the installation using the client - status, err := c.ConfigureInstallation(config) + _, err = c.ConfigureInstallation(config) require.NoError(t, err, "ConfigureInstallation should succeed with valid config") - assert.NotNil(t, status, "Status should not be nil") // Verify the status was set correctly var installStatus types.Status @@ -1107,7 +1104,6 @@ func TestInstallWithAPIClient(t *testing.T) { // Set the status using the client newStatus, err := c.SetInstallStatus(status) require.NoError(t, err, "SetInstallStatus should succeed") - assert.NotNil(t, newStatus, "Install should not be nil") assert.Equal(t, status, newStatus, "Install status should match the one set") }) } @@ -1300,7 +1296,6 @@ func TestPostSetupInfra(t *testing.T) { // Verify installation was created gotInst, err := kubeutils.GetLatestInstallation(t.Context(), fakeKcli) require.NoError(t, err) - assert.NotNil(t, gotInst) assert.Equal(t, ecv1beta1.InstallationStateInstalled, gotInst.Status.State) // Verify version metadata configmap was created diff --git a/api/internal/managers/installation/config_test.go b/api/internal/managers/installation/config_test.go index 242ce105a..6c5822fdc 100644 --- a/api/internal/managers/installation/config_test.go +++ b/api/internal/managers/installation/config_test.go @@ -286,8 +286,6 @@ func TestSetConfigDefaults(t *testing.T) { err := manager.SetConfigDefaults(&tt.inputConfig) assert.NoError(t, err) - - assert.NotNil(t, tt.inputConfig) assert.Equal(t, tt.expectedConfig, tt.inputConfig) }) } @@ -326,7 +324,6 @@ func TestConfigSetAndGet(t *testing.T) { // Test reading it back readConfig, err := manager.GetConfig() assert.NoError(t, err) - assert.NotNil(t, readConfig) // Verify the values match assert.Equal(t, configToWrite.AdminConsolePort, readConfig.AdminConsolePort) diff --git a/api/internal/managers/installation/status_test.go b/api/internal/managers/installation/status_test.go index aee4f8a74..d799de72d 100644 --- a/api/internal/managers/installation/status_test.go +++ b/api/internal/managers/installation/status_test.go @@ -25,7 +25,6 @@ func TestStatusSetAndGet(t *testing.T) { // Test reading it back readStatus, err := manager.GetStatus() assert.NoError(t, err) - assert.NotNil(t, readStatus) // Verify the values match assert.Equal(t, statusToWrite.State, readStatus.State) @@ -46,7 +45,6 @@ func TestSetRunningStatus(t *testing.T) { status, err := manager.GetStatus() assert.NoError(t, err) - assert.NotNil(t, status) assert.Equal(t, types.StateRunning, status.State) assert.Equal(t, description, status.Description) @@ -62,7 +60,6 @@ func TestSetFailedStatus(t *testing.T) { status, err := manager.GetStatus() assert.NoError(t, err) - assert.NotNil(t, status) assert.Equal(t, types.StateFailed, status.State) assert.Equal(t, description, status.Description) @@ -96,7 +93,6 @@ func TestSetCompletedStatus(t *testing.T) { status, err := manager.GetStatus() assert.NoError(t, err) - assert.NotNil(t, status) assert.Equal(t, tt.state, status.State) assert.Equal(t, tt.description, status.Description) diff --git a/api/internal/store/infra/store.go b/api/internal/store/infra/store.go index 079f63ed4..4f70322d3 100644 --- a/api/internal/store/infra/store.go +++ b/api/internal/store/infra/store.go @@ -11,7 +11,7 @@ import ( const maxLogSize = 100 * 1024 // 100KB total log size -var _ Store = &MemoryStore{} +var _ Store = &memoryStore{} // Store provides methods for storing and retrieving infrastructure state type Store interface { @@ -25,23 +25,23 @@ type Store interface { GetLogs() (string, error) } -// MemoryStore is an in-memory implementation of Store -type MemoryStore struct { +// memoryStore is an in-memory implementation of Store +type memoryStore struct { infra types.Infra mu sync.RWMutex } -type StoreOption func(*MemoryStore) +type StoreOption func(*memoryStore) func WithInfra(infra types.Infra) StoreOption { - return func(s *MemoryStore) { + return func(s *memoryStore) { s.infra = infra } } // NewMemoryStore creates a new memory store func NewMemoryStore(opts ...StoreOption) Store { - s := &MemoryStore{} + s := &memoryStore{} for _, opt := range opts { opt(s) @@ -50,7 +50,7 @@ func NewMemoryStore(opts ...StoreOption) Store { return s } -func (s *MemoryStore) Get() (types.Infra, error) { +func (s *memoryStore) Get() (types.Infra, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -62,7 +62,7 @@ func (s *MemoryStore) Get() (types.Infra, error) { return infra, nil } -func (s *MemoryStore) GetStatus() (types.Status, error) { +func (s *memoryStore) GetStatus() (types.Status, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -74,14 +74,14 @@ func (s *MemoryStore) GetStatus() (types.Status, error) { return status, nil } -func (s *MemoryStore) SetStatus(status types.Status) error { +func (s *memoryStore) SetStatus(status types.Status) error { s.mu.Lock() defer s.mu.Unlock() s.infra.Status = status return nil } -func (s *MemoryStore) SetStatusDesc(desc string) error { +func (s *memoryStore) SetStatusDesc(desc string) error { s.mu.Lock() defer s.mu.Unlock() @@ -93,7 +93,7 @@ func (s *MemoryStore) SetStatusDesc(desc string) error { return nil } -func (s *MemoryStore) RegisterComponent(name string) error { +func (s *memoryStore) RegisterComponent(name string) error { s.mu.Lock() defer s.mu.Unlock() @@ -109,7 +109,7 @@ func (s *MemoryStore) RegisterComponent(name string) error { return nil } -func (s *MemoryStore) SetComponentStatus(name string, status types.Status) error { +func (s *memoryStore) SetComponentStatus(name string, status types.Status) error { s.mu.Lock() defer s.mu.Unlock() @@ -123,7 +123,7 @@ func (s *MemoryStore) SetComponentStatus(name string, status types.Status) error return fmt.Errorf("component %s not found", name) } -func (s *MemoryStore) AddLogs(logs string) error { +func (s *memoryStore) AddLogs(logs string) error { s.mu.Lock() defer s.mu.Unlock() @@ -135,7 +135,7 @@ func (s *MemoryStore) AddLogs(logs string) error { return nil } -func (s *MemoryStore) GetLogs() (string, error) { +func (s *memoryStore) GetLogs() (string, error) { s.mu.RLock() defer s.mu.RUnlock() return s.infra.Logs, nil diff --git a/api/internal/store/infra/store_test.go b/api/internal/store/infra/store_test.go index d4c346c75..d4591da81 100644 --- a/api/internal/store/infra/store_test.go +++ b/api/internal/store/infra/store_test.go @@ -28,8 +28,6 @@ func TestNewMemoryStore(t *testing.T) { assert.NotNil(t, store) infra, err := store.Get() require.NoError(t, err) - assert.NotNil(t, infra) - assert.NotNil(t, infra.Status) assert.Equal(t, types.StatePending, infra.Status.State) } @@ -185,8 +183,6 @@ func TestMemoryStore_Get(t *testing.T) { // Test getting infra infra, err := store.Get() require.NoError(t, err) - assert.NotNil(t, infra) - assert.NotNil(t, infra.Status) assert.Empty(t, infra.Components) assert.Empty(t, infra.Logs) @@ -293,7 +289,7 @@ func TestMemoryStore_ConcurrentAccess(t *testing.T) { } func TestMemoryStore_StatusDescWithoutStatus(t *testing.T) { - store := &MemoryStore{ + store := &memoryStore{ infra: types.Infra{}, } diff --git a/api/internal/store/installation/store.go b/api/internal/store/installation/store.go index 5ec7cedfd..d1d18b177 100644 --- a/api/internal/store/installation/store.go +++ b/api/internal/store/installation/store.go @@ -7,7 +7,7 @@ import ( "github.com/tiendc/go-deepcopy" ) -var _ Store = &MemoryStore{} +var _ Store = &memoryStore{} type Store interface { GetConfig() (types.InstallationConfig, error) @@ -16,21 +16,21 @@ type Store interface { SetStatus(status types.Status) error } -type MemoryStore struct { +type memoryStore struct { mu sync.RWMutex installation types.Installation } -type StoreOption func(*MemoryStore) +type StoreOption func(*memoryStore) func WithInstallation(installation types.Installation) StoreOption { - return func(s *MemoryStore) { + return func(s *memoryStore) { s.installation = installation } } -func NewMemoryStore(opts ...StoreOption) *MemoryStore { - s := &MemoryStore{} +func NewMemoryStore(opts ...StoreOption) *memoryStore { + s := &memoryStore{} for _, opt := range opts { opt(s) @@ -39,7 +39,7 @@ func NewMemoryStore(opts ...StoreOption) *MemoryStore { return s } -func (s *MemoryStore) GetConfig() (types.InstallationConfig, error) { +func (s *memoryStore) GetConfig() (types.InstallationConfig, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -51,7 +51,7 @@ func (s *MemoryStore) GetConfig() (types.InstallationConfig, error) { return config, nil } -func (s *MemoryStore) SetConfig(cfg types.InstallationConfig) error { +func (s *memoryStore) SetConfig(cfg types.InstallationConfig) error { s.mu.Lock() defer s.mu.Unlock() @@ -59,7 +59,7 @@ func (s *MemoryStore) SetConfig(cfg types.InstallationConfig) error { return nil } -func (s *MemoryStore) GetStatus() (types.Status, error) { +func (s *memoryStore) GetStatus() (types.Status, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -71,7 +71,7 @@ func (s *MemoryStore) GetStatus() (types.Status, error) { return status, nil } -func (s *MemoryStore) SetStatus(status types.Status) error { +func (s *memoryStore) SetStatus(status types.Status) error { s.mu.Lock() defer s.mu.Unlock() s.installation.Status = status diff --git a/api/internal/store/installation/store_test.go b/api/internal/store/installation/store_test.go index 9abc067cb..643304813 100644 --- a/api/internal/store/installation/store_test.go +++ b/api/internal/store/installation/store_test.go @@ -14,7 +14,6 @@ func TestNewMemoryStore(t *testing.T) { store := NewMemoryStore(WithInstallation(inst)) assert.NotNil(t, store) - assert.NotNil(t, store.installation) assert.Equal(t, inst, store.installation) } @@ -30,7 +29,6 @@ func TestMemoryStore_GetConfig(t *testing.T) { config, err := store.GetConfig() require.NoError(t, err) - assert.NotNil(t, config) assert.Equal(t, types.InstallationConfig{ AdminConsolePort: 8080, DataDirectory: "/some/dir", @@ -72,7 +70,6 @@ func TestMemoryStore_GetStatus(t *testing.T) { status, err := store.GetStatus() require.NoError(t, err) - assert.NotNil(t, status) assert.Equal(t, types.Status{ State: "failed", Description: "Failure", diff --git a/api/internal/store/preflight/store.go b/api/internal/store/preflight/store.go index d413d72ee..3741b629c 100644 --- a/api/internal/store/preflight/store.go +++ b/api/internal/store/preflight/store.go @@ -7,7 +7,7 @@ import ( "github.com/tiendc/go-deepcopy" ) -var _ Store = &MemoryStore{} +var _ Store = &memoryStore{} type Store interface { GetTitles() ([]string, error) @@ -19,21 +19,21 @@ type Store interface { IsRunning() bool } -type MemoryStore struct { +type memoryStore struct { mu sync.RWMutex hostPreflight types.HostPreflights } -type StoreOption func(*MemoryStore) +type StoreOption func(*memoryStore) func WithHostPreflight(hostPreflight types.HostPreflights) StoreOption { - return func(s *MemoryStore) { + return func(s *memoryStore) { s.hostPreflight = hostPreflight } } -func NewMemoryStore(opts ...StoreOption) *MemoryStore { - s := &MemoryStore{} +func NewMemoryStore(opts ...StoreOption) *memoryStore { + s := &memoryStore{} for _, opt := range opts { opt(s) @@ -42,7 +42,7 @@ func NewMemoryStore(opts ...StoreOption) *MemoryStore { return s } -func (s *MemoryStore) GetTitles() ([]string, error) { +func (s *memoryStore) GetTitles() ([]string, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -54,7 +54,7 @@ func (s *MemoryStore) GetTitles() ([]string, error) { return titles, nil } -func (s *MemoryStore) SetTitles(titles []string) error { +func (s *memoryStore) SetTitles(titles []string) error { s.mu.Lock() defer s.mu.Unlock() s.hostPreflight.Titles = titles @@ -62,7 +62,7 @@ func (s *MemoryStore) SetTitles(titles []string) error { return nil } -func (s *MemoryStore) GetOutput() (*types.HostPreflightsOutput, error) { +func (s *memoryStore) GetOutput() (*types.HostPreflightsOutput, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -78,7 +78,7 @@ func (s *MemoryStore) GetOutput() (*types.HostPreflightsOutput, error) { return output, nil } -func (s *MemoryStore) SetOutput(output *types.HostPreflightsOutput) error { +func (s *memoryStore) SetOutput(output *types.HostPreflightsOutput) error { s.mu.Lock() defer s.mu.Unlock() @@ -86,7 +86,7 @@ func (s *MemoryStore) SetOutput(output *types.HostPreflightsOutput) error { return nil } -func (s *MemoryStore) GetStatus() (types.Status, error) { +func (s *memoryStore) GetStatus() (types.Status, error) { s.mu.RLock() defer s.mu.RUnlock() @@ -98,7 +98,7 @@ func (s *MemoryStore) GetStatus() (types.Status, error) { return status, nil } -func (s *MemoryStore) SetStatus(status types.Status) error { +func (s *memoryStore) SetStatus(status types.Status) error { s.mu.Lock() defer s.mu.Unlock() @@ -106,7 +106,7 @@ func (s *MemoryStore) SetStatus(status types.Status) error { return nil } -func (s *MemoryStore) IsRunning() bool { +func (s *memoryStore) IsRunning() bool { s.mu.RLock() defer s.mu.RUnlock() diff --git a/api/internal/store/preflight/store_test.go b/api/internal/store/preflight/store_test.go index 6e3d391ac..48f83b422 100644 --- a/api/internal/store/preflight/store_test.go +++ b/api/internal/store/preflight/store_test.go @@ -15,7 +15,6 @@ func TestNewMemoryStore(t *testing.T) { store := NewMemoryStore(WithHostPreflight(hostPreflight)) assert.NotNil(t, store) - assert.NotNil(t, store.hostPreflight) assert.Equal(t, hostPreflight, store.hostPreflight) } @@ -134,7 +133,6 @@ func TestMemoryStore_GetStatus(t *testing.T) { result, err := store.GetStatus() require.NoError(t, err) - assert.NotNil(t, result) assert.Equal(t, status, result) } diff --git a/api/internal/store/store.go b/api/internal/store/store.go index 28f8d2dfd..3f04f135d 100644 --- a/api/internal/store/store.go +++ b/api/internal/store/store.go @@ -6,7 +6,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" ) -var _ Store = &MemoryStore{} +var _ Store = &memoryStore{} // Store is the global interface that combines all substores type Store interface { @@ -21,31 +21,31 @@ type Store interface { } // StoreOption is a function that configures a store -type StoreOption func(*MemoryStore) +type StoreOption func(*memoryStore) // WithPreflightStore sets the preflight store func WithPreflightStore(store preflight.Store) StoreOption { - return func(s *MemoryStore) { + return func(s *memoryStore) { s.preflightStore = store } } // WithInstallationStore sets the installation store func WithInstallationStore(store installation.Store) StoreOption { - return func(s *MemoryStore) { + return func(s *memoryStore) { s.installationStore = store } } // WithInfraStore sets the infra store func WithInfraStore(store infra.Store) StoreOption { - return func(s *MemoryStore) { + return func(s *memoryStore) { s.infraStore = store } } -// MemoryStore is an in-memory implementation of the global Store interface -type MemoryStore struct { +// memoryStore is an in-memory implementation of the global Store interface +type memoryStore struct { preflightStore preflight.Store installationStore installation.Store infraStore infra.Store @@ -53,7 +53,7 @@ type MemoryStore struct { // NewMemoryStore creates a new memory store with the given options func NewMemoryStore(opts ...StoreOption) Store { - s := &MemoryStore{} + s := &memoryStore{} for _, opt := range opts { opt(s) @@ -74,14 +74,14 @@ func NewMemoryStore(opts ...StoreOption) Store { return s } -func (s *MemoryStore) PreflightStore() preflight.Store { +func (s *memoryStore) PreflightStore() preflight.Store { return s.preflightStore } -func (s *MemoryStore) InstallationStore() installation.Store { +func (s *memoryStore) InstallationStore() installation.Store { return s.installationStore } -func (s *MemoryStore) InfraStore() infra.Store { +func (s *memoryStore) InfraStore() infra.Store { return s.infraStore } From 5c52173fa644edea3aa6dc4026063d5774d99e4e Mon Sep 17 00:00:00 2001 From: Steven Crespo <96719548+screspod@users.noreply.github.com> Date: Thu, 19 Jun 2025 07:00:58 -0700 Subject: [PATCH 12/48] bug: Fix mix of camel case and title case in error messages (#2314) * Port error message camel to title case formatter to frontend * Add tests * Update error message formatter to use a map * f --- api/integration/install_test.go | 10 +-- api/types/errors.go | 64 +----------------- .../components/wizard/setup/LinuxSetup.tsx | 56 +++++++++++++--- .../wizard/tests/LinuxSetup.test.tsx | 66 +++++++++++++++++++ 4 files changed, 119 insertions(+), 77 deletions(-) create mode 100644 web/src/components/wizard/tests/LinuxSetup.test.tsx diff --git a/api/integration/install_test.go b/api/integration/install_test.go index 2bc72727e..e19a51a47 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -418,7 +418,7 @@ func TestConfigureInstallationValidation(t *testing.T) { var apiError types.APIError err = json.NewDecoder(rec.Body).Decode(&apiError) require.NoError(t, err) - assert.Contains(t, apiError.Error(), "Service CIDR is required when globalCidr is not set") + assert.Contains(t, apiError.Error(), "serviceCidr is required when globalCidr is not set") // Also verify the field name is correct assert.Equal(t, "serviceCidr", apiError.Errors[0].Field) } @@ -1085,12 +1085,8 @@ func TestInstallWithAPIClient(t *testing.T) { apiErr, ok := err.(*types.APIError) require.True(t, ok, "Error should be of type *types.APIError") assert.Equal(t, http.StatusBadRequest, apiErr.StatusCode) - // Error message should contain both variants of the port conflict message - assert.True(t, - strings.Contains(apiErr.Error(), "Admin Console Port and localArtifactMirrorPort cannot be equal") && - strings.Contains(apiErr.Error(), "adminConsolePort and Local Artifact Mirror Port cannot be equal"), - "Error message should contain both variants of the port conflict message", - ) + // Error message should contain the same port conflict message for both fields + assert.Equal(t, 2, strings.Count(apiErr.Error(), "adminConsolePort and localArtifactMirrorPort cannot be equal")) }) // Test SetInstallStatus diff --git a/api/types/errors.go b/api/types/errors.go index fef75b973..3f33d5e4c 100644 --- a/api/types/errors.go +++ b/api/types/errors.go @@ -6,11 +6,6 @@ import ( "errors" "fmt" "net/http" - "regexp" - "strings" - - "golang.org/x/text/cases" - "golang.org/x/text/language" ) type APIError struct { @@ -105,64 +100,11 @@ func AppendFieldError(apiErr *APIError, field string, err error) *APIError { if apiErr == nil { apiErr = NewBadRequestError(errors.New("field errors")) } - return AppendError(apiErr, newFieldError(field, err)) -} - -func camelCaseToWords(s string) string { - // Handle special cases - specialCases := map[string]string{ - "cidr": "CIDR", - "Cidr": "CIDR", - "CIDR": "CIDR", - } - - // Check if the entire string is a special case - if replacement, ok := specialCases[strings.ToLower(s)]; ok { - return replacement - } - - // Split on capital letters - re := regexp.MustCompile(`([a-z])([A-Z])`) - words := re.ReplaceAllString(s, "$1 $2") - - // Split the words and handle special cases - wordList := strings.Split(strings.ToLower(words), " ") - for i, word := range wordList { - if replacement, ok := specialCases[word]; ok { - wordList[i] = replacement - } else { - // Capitalize other words - c := cases.Title(language.English) - wordList[i] = c.String(word) - } - } - - return strings.Join(wordList, " ") -} - -func newFieldError(field string, err error) *APIError { - msg := err.Error() - - // Try different patterns to replace the field name - patterns := []string{ - field, // exact match - strings.ToLower(field), // lowercase - strings.ToUpper(field), // uppercase - "cidr", // special case for CIDR - } - - for _, pattern := range patterns { - if strings.Contains(msg, pattern) { - msg = strings.Replace(msg, pattern, camelCaseToWords(field), 1) - break - } - } - - return &APIError{ - Message: msg, + return AppendError(apiErr, &APIError{ + Message: err.Error(), Field: field, err: err, - } + }) } // JSON writes the APIError as JSON to the provided http.ResponseWriter diff --git a/web/src/components/wizard/setup/LinuxSetup.tsx b/web/src/components/wizard/setup/LinuxSetup.tsx index 3bfa4db12..f6c3187d5 100644 --- a/web/src/components/wizard/setup/LinuxSetup.tsx +++ b/web/src/components/wizard/setup/LinuxSetup.tsx @@ -4,6 +4,27 @@ import Select from "../../common/Select"; import { useBranding } from "../../../contexts/BrandingContext"; import { ChevronDown, ChevronRight } from "lucide-react"; +/** + * Maps internal field names to user-friendly display names. + * Used for: + * - Input IDs: + * - Labels: + * - Error formatting: formatErrorMessage("adminConsolePort invalid") -> "Admin Console Port invalid" + */ +const fieldNames = { + adminConsolePort: "Admin Console Port", + dataDirectory: "Data Directory", + localArtifactMirrorPort: "Local Artifact Mirror Port", + httpProxy: "HTTP Proxy", + httpsProxy: "HTTPS Proxy", + noProxy: "Proxy Bypass List", + networkInterface: "Network Interface", + podCidr: "Pod CIDR", + serviceCidr: "Service CIDR", + globalCidr: "Reserved Network Range (CIDR)", + cidr: "CIDR", +} + interface LinuxSetupProps { config: { dataDirectory?: string; @@ -43,7 +64,7 @@ const LinuxSetup: React.FC = ({ const getFieldError = (fieldName: string) => { const fieldError = fieldErrors.find((err) => err.field === fieldName); - return fieldError?.message; + return fieldError ? formatErrorMessage(fieldError.message) : undefined; }; return ( @@ -52,7 +73,7 @@ const LinuxSetup: React.FC = ({

System Configuration

= ({ = ({ = ({
= ({ = ({ = ({
= ({ ); }; +/** + * Formats error messages by replacing technical field names with more user-friendly display names. + * Example: "adminConsolePort" becomes "Admin Console Port". + * + * @param message - The error message to format + * @returns The formatted error message with replaced field names + */ +export function formatErrorMessage(message: string) { + let finalMsg = message + for (const [field, fieldName] of Object.entries(fieldNames)) { + // Case-insensitive regex that matches whole words only + // Example: "podCidr", "PodCidr", "PODCIDR" all become "Pod CIDR" + finalMsg = finalMsg.replace(new RegExp(`\\b${field}\\b`, 'gi'), fieldName) + } + return finalMsg +} + export default LinuxSetup; diff --git a/web/src/components/wizard/tests/LinuxSetup.test.tsx b/web/src/components/wizard/tests/LinuxSetup.test.tsx new file mode 100644 index 000000000..cfb7d3386 --- /dev/null +++ b/web/src/components/wizard/tests/LinuxSetup.test.tsx @@ -0,0 +1,66 @@ +import { describe, it, expect } from "vitest"; +import { formatErrorMessage } from "../setup/LinuxSetup"; + +describe("formatErrorMessage", () => { + it("handles empty string", () => { + expect(formatErrorMessage("")).toBe(""); + }); + + it("replaces field names with their proper format", () => { + expect(formatErrorMessage("adminConsolePort")).toBe("Admin Console Port"); + expect(formatErrorMessage("dataDirectory")).toBe("Data Directory"); + expect(formatErrorMessage("localArtifactMirrorPort")).toBe("Local Artifact Mirror Port"); + expect(formatErrorMessage("httpProxy")).toBe("HTTP Proxy"); + expect(formatErrorMessage("httpsProxy")).toBe("HTTPS Proxy"); + expect(formatErrorMessage("noProxy")).toBe("Proxy Bypass List"); + expect(formatErrorMessage("networkInterface")).toBe("Network Interface"); + expect(formatErrorMessage("podCidr")).toBe("Pod CIDR"); + expect(formatErrorMessage("serviceCidr")).toBe("Service CIDR"); + expect(formatErrorMessage("globalCidr")).toBe("Reserved Network Range (CIDR)"); + expect(formatErrorMessage("cidr")).toBe("CIDR"); + }); + + it("handles multiple field names in one message", () => { + expect(formatErrorMessage("podCidr and serviceCidr are required")).toBe("Pod CIDR and Service CIDR are required"); + expect(formatErrorMessage("httpProxy and httpsProxy must be set")).toBe("HTTP Proxy and HTTPS Proxy must be set"); + }); + + it("preserves non-field words", () => { + expect(formatErrorMessage("The podCidr is invalid")).toBe("The Pod CIDR is invalid"); + expect(formatErrorMessage("Please set the httpProxy")).toBe("Please set the HTTP Proxy"); + }); + + it("handles case insensitivity correctly", () => { + expect(formatErrorMessage("PodCidr")).toBe("Pod CIDR"); + expect(formatErrorMessage("HTTPPROXY")).toBe("HTTP Proxy"); + expect(formatErrorMessage("cidr")).toBe("CIDR"); + expect(formatErrorMessage("Cidr")).toBe("CIDR"); + expect(formatErrorMessage("CIDR")).toBe("CIDR"); + }); + + it("handles real-world error messages", () => { + expect(formatErrorMessage("The podCidr 10.0.0.0/24 overlaps with serviceCidr 10.0.0.0/16")).toBe( + "The Pod CIDR 10.0.0.0/24 overlaps with Service CIDR 10.0.0.0/16" + ); + expect(formatErrorMessage("httpProxy and httpsProxy cannot be empty when noProxy is set")).toBe( + "HTTP Proxy and HTTPS Proxy cannot be empty when Proxy Bypass List is set" + ); + expect(formatErrorMessage("adminConsolePort must be between 1024 and 65535")).toBe( + "Admin Console Port must be between 1024 and 65535" + ); + expect(formatErrorMessage("dataDirectory /var/lib/k0s is not writable")).toBe( + "Data Directory /var/lib/k0s is not writable" + ); + expect(formatErrorMessage("globalCidr must be a valid CIDR block")).toBe( + "Reserved Network Range (CIDR) must be a valid CIDR block" + ); + }); + + it("handles special characters and formatting", () => { + expect(formatErrorMessage("admin_console_port and localArtifactMirrorPort cannot be equal.")).toBe( + "admin_console_port and Local Artifact Mirror Port cannot be equal." + ); + expect(formatErrorMessage("httpProxy: invalid URL format")).toBe("HTTP Proxy: invalid URL format"); + expect(formatErrorMessage("podCidr: 192.168.0.0/24 (invalid)")).toBe("Pod CIDR: 192.168.0.0/24 (invalid)"); + }); +}); From 827dd9d7bdf681a0c0ca2b4eca46d78513ffd616 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Fri, 20 Jun 2025 07:41:10 -0600 Subject: [PATCH 13/48] feat: add preflight to check for XFS filesystem with ftype=0 (#2338) * feat: add preflight check for XFS with ftype=0 Signed-off-by: Evans Mungai * Update pkg-new/preflights/host-preflight.yaml Co-authored-by: Alex Parker <7272359+ajp-io@users.noreply.github.com> * Update XFS preflight check with more descriptive message Signed-off-by: Evans Mungai --------- Signed-off-by: Evans Mungai Co-authored-by: Alex Parker <7272359+ajp-io@users.noreply.github.com> --- pkg-new/preflights/host-preflight.yaml | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg-new/preflights/host-preflight.yaml b/pkg-new/preflights/host-preflight.yaml index 8e279747d..a3750c70b 100644 --- a/pkg-new/preflights/host-preflight.yaml +++ b/pkg-new/preflights/host-preflight.yaml @@ -172,6 +172,20 @@ spec: dir="{{ .DataDir }}" while [ "$dir" != "/" ]; do find "$dir" -maxdepth 0 ! -perm -111; dir=$(dirname "$dir"); done find "/" -maxdepth 0 ! -perm -111 + - run: + collectorName: 'xfs_info-data-dir' + command: 'sh' + args: + - '-c' + - > + # Get filesystem type + fstype=$(findmnt -n -o FSTYPE --target "{{ .DataDir }}") + if [ "$fstype" = "xfs" ]; then + echo "Filesystem is XFS. Running xfs_info..." + xfs_info "{{ .DataDir }}" + else + echo "Filesystem is not XFS (detected: $fstype). Skipping xfs_info." + fi analyzers: - cpu: checkName: CPU @@ -937,7 +951,7 @@ spec: The node IP {{ .NodeIP }} cannot be within the Pod CIDR range {{ .PodCIDR.CIDR }}. Use --pod-cidr to specify a different Pod CIDR, or use --network-interface to specify a different network interface. {{- end }} - pass: - when: "false" + when: "false" message: The node IP {{ .NodeIP }} is not within the Pod CIDR range {{ .PodCIDR.CIDR }}. - subnetContainsIP: checkName: Node IP in Service CIDR Check @@ -1194,3 +1208,14 @@ spec: - fail: message: >- The following directories lack execute permissions: {{ `{{ .Dirs | trim | splitList "\n" | join ", " }}` }}. + - textAnalyze: + checkName: Check filesystem on data directory path + fileName: host-collectors/run-host/xfs_info-data-dir.txt + regex: 'ftype=0' + outcomes: + - fail: + when: "true" + message: "The XFS filesystem at {{ .DataDir }} is configured with ftype=0, which is not supported. Reformat the filesystem with ftype=1, or choose a different data directory on a supported filesystem." + - pass: + when: "false" + message: "The filesystem at {{ .DataDir }} is either not XFS or is XFS with ftype=1." From d3131d948b69065108fb3ebb8e632228481af211 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Fri, 20 Jun 2025 10:50:09 -0600 Subject: [PATCH 14/48] feat: collect network interface statistics, configurations and parameters (#2328) * chore: collect network interface statistics Signed-off-by: Evans Mungai * Collect PCI device information Signed-off-by: Evans Mungai * Add ethtool info to host support bundle Signed-off-by: Evans Mungai * Fix support bundle spec indentation Signed-off-by: Evans Mungai --------- Signed-off-by: Evans Mungai --- .../support/host-support-bundle.tmpl.yaml | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cmd/installer/goods/support/host-support-bundle.tmpl.yaml b/cmd/installer/goods/support/host-support-bundle.tmpl.yaml index fb93075f7..3214058a3 100644 --- a/cmd/installer/goods/support/host-support-bundle.tmpl.yaml +++ b/cmd/installer/goods/support/host-support-bundle.tmpl.yaml @@ -173,6 +173,53 @@ spec: collectorName: "ip-route-table" command: "ip" args: ["route"] + - run: + collectorName: "ip-address-stats" + command: "ip" + args: ["-s", "-s", "address"] + - run: + collectorName: "lspci" + command: "lspci" + args: ["-vvv", "-D"] + - run: + collectorName: "ethool-info" + command: "sh" + args: + - -c + - > + interfaces=$(ls /sys/class/net); + for iface in $interfaces; do + echo "=============================================="; + echo "Interface: $iface"; + echo "=============================================="; + + echo + echo "--- Basic Info ---" + ethtool "$iface" + + echo + echo "--- Features (Offloads) ---" + ethtool -k "$iface" + + echo + echo "--- Pause Parameters ---" + ethtool -a "$iface" + + echo + echo "--- Ring Parameters ---" + ethtool -g "$iface" + + echo + echo "--- Coalesce Settings ---" + ethtool -c "$iface" + + echo + echo "--- Driver Info ---" + ethtool -i "$iface" + + echo + echo + done - run: collectorName: "sysctl" command: "sysctl" From 7f3cb9c2ba8354ae9e88e85aecdab28bb749f6c9 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Fri, 20 Jun 2025 11:20:04 -0700 Subject: [PATCH 15/48] chore: install state machine with final states (#2334) * chore: install state machine with final states --- api/README.md | 8 +- api/api.go | 8 +- api/controllers/install/controller.go | 48 +- api/controllers/install/controller_test.go | 195 ++++--- api/controllers/install/hostpreflight.go | 57 +- api/controllers/install/infra.go | 83 ++- api/controllers/install/installation.go | 47 +- api/controllers/install/statemachine.go | 62 +++ api/controllers/install/statemachine_test.go | 140 +++++ api/integration/hostpreflights_test.go | 19 +- api/integration/install_test.go | 49 +- api/internal/managers/infra/install.go | 75 ++- api/internal/managers/infra/manager.go | 6 +- api/internal/managers/infra/status.go | 15 - api/internal/managers/infra/status_test.go | 74 --- api/internal/managers/installation/config.go | 24 +- .../managers/installation/config_test.go | 28 +- api/internal/managers/installation/manager.go | 8 +- api/internal/managers/installation/status.go | 8 - .../managers/preflight/hostpreflight.go | 71 +-- .../managers/preflight/hostpreflight_test.go | 26 +- api/internal/managers/preflight/manager.go | 2 - api/internal/statemachine/statemachine.go | 143 +++++ .../statemachine/statemachine_test.go | 503 ++++++++++++++++++ api/internal/store/preflight/store.go | 8 - api/internal/store/preflight/store_mock.go | 6 - api/internal/store/preflight/store_test.go | 64 +-- cmd/installer/cli/api.go | 4 +- cmd/installer/cli/install.go | 18 +- cmd/installer/cli/install_runpreflights.go | 8 +- cmd/installer/kotscli/kotscli.go | 14 +- pkg-new/hostutils/initialize.go | 12 +- 32 files changed, 1338 insertions(+), 495 deletions(-) create mode 100644 api/controllers/install/statemachine.go create mode 100644 api/controllers/install/statemachine_test.go create mode 100644 api/internal/statemachine/statemachine.go create mode 100644 api/internal/statemachine/statemachine_test.go diff --git a/api/README.md b/api/README.md index edc2325a9..c85b61869 100644 --- a/api/README.md +++ b/api/README.md @@ -12,9 +12,15 @@ The root directory contains the main API setup files and request handlers. #### `/controllers` Contains the business logic for different API endpoints. Each controller package focuses on a specific domain of functionality or workflow (e.g., authentication, console, install, upgrade, join, etc.) and implements the core business logic for that domain or workflow. Controllers can utilize multiple managers with each manager handling a specific subdomain of functionality. +#### `/internal` +Contains shared utilities and helper packages that provide common functionality used across different parts of the API. This includes both general-purpose utilities and domain-specific helpers. + #### `/internal/managers` Each manager is responsible for a specific subdomain of functionality and provides a clean, thread-safe interface for controllers to interact with. For example, the Preflight Manager manages system requirement checks and validation. +#### `/internal/statemachine` +The statemachine is used by controllers to capture workflow state and enforce valid transitions. + #### `/types` Defines the core data structures and types used throughout the API. This includes: - Request and response types @@ -30,7 +36,7 @@ Contains Swagger-generated API documentation. This includes: - API operation descriptions #### `/pkg` -Contains shared utilities and helper packages that provide common functionality used across different parts of the API. This includes both general-purpose utilities and domain-specific helpers. +Contains helper packages that can be used by packages external to the API. #### `/client` Provides a client library for interacting with the API. The client package implements a clean interface for making API calls and handling responses, making it easy to integrate with the API from other parts of the system. diff --git a/api/api.go b/api/api.go index 241406246..56928f762 100644 --- a/api/api.go +++ b/api/api.go @@ -48,7 +48,7 @@ type API struct { rc runtimeconfig.RuntimeConfig releaseData *release.ReleaseData tlsConfig types.TLSConfig - licenseFile string + license []byte airgapBundle string configValues string endUserConfig *ecv1beta1.Config @@ -113,9 +113,9 @@ func WithTLSConfig(tlsConfig types.TLSConfig) APIOption { } } -func WithLicenseFile(licenseFile string) APIOption { +func WithLicense(license []byte) APIOption { return func(a *API) { - a.licenseFile = licenseFile + a.license = license } } @@ -188,7 +188,7 @@ func New(password string, opts ...APIOption) (*API, error) { install.WithReleaseData(api.releaseData), install.WithPassword(password), install.WithTLSConfig(api.tlsConfig), - install.WithLicenseFile(api.licenseFile), + install.WithLicense(api.license), install.WithAirgapBundle(api.airgapBundle), install.WithConfigValues(api.configValues), install.WithEndUserConfig(api.endUserConfig), diff --git a/api/controllers/install/controller.go b/api/controllers/install/controller.go index 46419f2ff..512d0ee15 100644 --- a/api/controllers/install/controller.go +++ b/api/controllers/install/controller.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/managers/infra" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" @@ -40,24 +41,26 @@ type RunHostPreflightsOptions struct { var _ Controller = (*InstallController)(nil) type InstallController struct { - install types.Install - store store.Store installationManager installation.InstallationManager hostPreflightManager preflight.HostPreflightManager infraManager infra.InfraManager - rc runtimeconfig.RuntimeConfig - logger logrus.FieldLogger hostUtils hostutils.HostUtilsInterface netUtils utils.NetUtils metricsReporter metrics.ReporterInterface releaseData *release.ReleaseData password string tlsConfig types.TLSConfig - licenseFile string + license []byte airgapBundle string configValues string endUserConfig *ecv1beta1.Config - mu sync.RWMutex + + install types.Install + store store.Store + rc runtimeconfig.RuntimeConfig + stateMachine statemachine.Interface + logger logrus.FieldLogger + mu sync.RWMutex } type InstallControllerOption func(*InstallController) @@ -110,9 +113,9 @@ func WithTLSConfig(tlsConfig types.TLSConfig) InstallControllerOption { } } -func WithLicenseFile(licenseFile string) InstallControllerOption { +func WithLicense(license []byte) InstallControllerOption { return func(c *InstallController) { - c.licenseFile = licenseFile + c.license = license } } @@ -152,19 +155,22 @@ func WithInfraManager(infraManager infra.InfraManager) InstallControllerOption { } } -func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { - controller := &InstallController{} - - for _, opt := range opts { - opt(controller) +func WithStateMachine(stateMachine statemachine.Interface) InstallControllerOption { + return func(c *InstallController) { + c.stateMachine = stateMachine } +} - if controller.rc == nil { - controller.rc = runtimeconfig.New(nil) +func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { + controller := &InstallController{ + store: store.NewMemoryStore(), + rc: runtimeconfig.New(nil), + logger: logger.NewDiscardLogger(), + stateMachine: NewStateMachine(), } - if controller.logger == nil { - controller.logger = logger.NewDiscardLogger() + for _, opt := range opts { + opt(controller) } if controller.hostUtils == nil { @@ -177,15 +183,11 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, controller.netUtils = utils.NewNetUtils() } - if controller.store == nil { - controller.store = store.NewMemoryStore() - } - if controller.installationManager == nil { controller.installationManager = installation.NewInstallationManager( installation.WithLogger(controller.logger), installation.WithInstallationStore(controller.store.InstallationStore()), - installation.WithLicenseFile(controller.licenseFile), + installation.WithLicense(controller.license), installation.WithAirgapBundle(controller.airgapBundle), installation.WithHostUtils(controller.hostUtils), installation.WithNetUtils(controller.netUtils), @@ -207,7 +209,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, infra.WithInfraStore(controller.store.InfraStore()), infra.WithPassword(controller.password), infra.WithTLSConfig(controller.tlsConfig), - infra.WithLicenseFile(controller.licenseFile), + infra.WithLicense(controller.license), infra.WithAirgapBundle(controller.airgapBundle), infra.WithConfigValues(controller.configValues), infra.WithReleaseData(controller.releaseData), diff --git a/api/controllers/install/controller_test.go b/api/controllers/install/controller_test.go index fd426d721..37c75b6c0 100644 --- a/api/controllers/install/controller_test.go +++ b/api/controllers/install/controller_test.go @@ -3,6 +3,7 @@ package install import ( "errors" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -11,6 +12,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/managers/infra" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/metrics" @@ -113,10 +115,12 @@ func TestGetInstallationConfig(t *testing.T) { func TestConfigureInstallation(t *testing.T) { tests := []struct { - name string - config types.InstallationConfig - setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, types.InstallationConfig) - expectedErr bool + name string + config types.InstallationConfig + currentState statemachine.State + expectedState statemachine.State + setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, types.InstallationConfig) + expectedErr bool }{ { name: "successful configure installation", @@ -124,26 +128,32 @@ func TestConfigureInstallation(t *testing.T) { LocalArtifactMirrorPort: 9000, DataDirectory: t.TempDir(), }, + currentState: StateNew, + expectedState: StateHostConfigured, setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", config).Return(nil), - m.On("ConfigureHost", t.Context(), rc).Return(nil), + m.On("ConfigureHost", mock.Anything, rc).Return(nil), ) }, expectedErr: false, }, { - name: "validate error", - config: types.InstallationConfig{}, + name: "validate error", + config: types.InstallationConfig{}, + currentState: StateNew, + expectedState: StateNew, setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")) }, expectedErr: true, }, { - name: "set config error", - config: types.InstallationConfig{}, + name: "set config error", + config: types.InstallationConfig{}, + currentState: StateNew, + expectedState: StateNew, setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), @@ -152,12 +162,31 @@ func TestConfigureInstallation(t *testing.T) { }, expectedErr: true, }, + { + name: "configure host error", + config: types.InstallationConfig{ + LocalArtifactMirrorPort: 9000, + DataDirectory: t.TempDir(), + }, + currentState: StateNew, + expectedState: StateInstallationConfigured, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(nil), + m.On("SetConfig", config).Return(nil), + m.On("ConfigureHost", mock.Anything, rc).Return(errors.New("configure host error")), + ) + }, + expectedErr: false, + }, { name: "with global CIDR", config: types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", DataDirectory: t.TempDir(), }, + currentState: StateNew, + expectedState: StateHostConfigured, setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { // Create a copy with expected CIDR values after computation configWithCIDRs := config @@ -167,11 +196,23 @@ func TestConfigureInstallation(t *testing.T) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", configWithCIDRs).Return(nil), - m.On("ConfigureHost", t.Context(), rc).Return(nil), + m.On("ConfigureHost", mock.Anything, rc).Return(nil), ) }, expectedErr: false, }, + { + name: "invalid state transition", + config: types.InstallationConfig{ + LocalArtifactMirrorPort: 9000, + DataDirectory: t.TempDir(), + }, + currentState: StateInfrastructureInstalling, + expectedState: StateInfrastructureInstalling, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { + }, + expectedErr: true, + }, } for _, tt := range tests { @@ -180,12 +221,15 @@ func TestConfigureInstallation(t *testing.T) { rc.SetDataDir(t.TempDir()) rc.SetManagerPort(9001) + sm := NewStateMachine(WithCurrentState(tt.currentState)) + mockManager := &installation.MockInstallationManager{} tt.setupMock(mockManager, rc, tt.config) controller, err := NewInstallController( WithRuntimeConfig(rc), + WithStateMachine(sm), WithInstallationManager(mockManager), ) require.NoError(t, err) @@ -195,8 +239,14 @@ func TestConfigureInstallation(t *testing.T) { assert.Error(t, err) } else { assert.NoError(t, err) + + assert.NotEqual(t, tt.currentState, sm.CurrentState(), "state should have changed and should not be %s", tt.currentState) } + assert.Eventually(t, func() bool { + return sm.CurrentState() == tt.expectedState + }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + mockManager.AssertExpectations(t) }) } @@ -271,16 +321,20 @@ func TestRunHostPreflights(t *testing.T) { } tests := []struct { - name string - setupMocks func(*preflight.MockHostPreflightManager, runtimeconfig.RuntimeConfig) - expectedErr bool + name string + currentState statemachine.State + expectedState statemachine.State + setupMocks func(*preflight.MockHostPreflightManager, runtimeconfig.RuntimeConfig) + expectedErr bool }{ { - name: "successful run preflights", + name: "successful run preflights", + currentState: StateHostConfigured, + expectedState: StatePreflightsSucceeded, setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { mock.InOrder( pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), - pm.On("RunHostPreflights", t.Context(), rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { return expectedHPF == opts.HostPreflightSpec })).Return(nil), ) @@ -288,7 +342,9 @@ func TestRunHostPreflights(t *testing.T) { expectedErr: false, }, { - name: "prepare preflights error", + name: "prepare preflights error", + currentState: StateHostConfigured, + expectedState: StateHostConfigured, setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { mock.InOrder( pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(nil, errors.New("prepare error")), @@ -297,15 +353,25 @@ func TestRunHostPreflights(t *testing.T) { expectedErr: true, }, { - name: "run preflights error", + name: "run preflights error", + currentState: StateHostConfigured, + expectedState: StatePreflightsFailed, setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { mock.InOrder( pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), - pm.On("RunHostPreflights", t.Context(), rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { return expectedHPF == opts.HostPreflightSpec })).Return(errors.New("run preflights error")), ) }, + expectedErr: false, + }, + { + name: "invalid state transition", + currentState: StateInfrastructureInstalling, + expectedState: StateInfrastructureInstalling, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { + }, expectedErr: true, }, } @@ -321,11 +387,14 @@ func TestRunHostPreflights(t *testing.T) { NoProxy: "no-proxy.com", }) + sm := NewStateMachine(WithCurrentState(tt.currentState)) + mockPreflightManager := &preflight.MockHostPreflightManager{} tt.setupMocks(mockPreflightManager, rc) controller, err := NewInstallController( WithRuntimeConfig(rc), + WithStateMachine(sm), WithHostPreflightManager(mockPreflightManager), WithReleaseData(getTestReleaseData()), ) @@ -337,8 +406,14 @@ func TestRunHostPreflights(t *testing.T) { require.Error(t, err) } else { require.NoError(t, err) + + assert.NotEqual(t, sm.CurrentState(), tt.currentState, "state should have changed and should not be %s", tt.currentState) } + assert.Eventually(t, func() bool { + return sm.CurrentState() == tt.expectedState + }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + mockPreflightManager.AssertExpectations(t) }) } @@ -564,29 +639,28 @@ func TestGetInstallationStatus(t *testing.T) { func TestSetupInfra(t *testing.T) { tests := []struct { - name string - setupMocks func(runtimeconfig.RuntimeConfig, *preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter) - expectedErr bool + name string + currentState statemachine.State + expectedState statemachine.State + setupMocks func(runtimeconfig.RuntimeConfig, *preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter) + expectedErr bool }{ { - name: "successful setup with passed preflights", + name: "successful setup with passed preflights", + currentState: StatePreflightsSucceeded, + expectedState: StateSucceeded, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := types.Status{ - State: types.StateSucceeded, - } mock.InOrder( - pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), - fm.On("Install", t.Context(), rc).Return(nil), + fm.On("Install", mock.Anything, rc).Return(nil), ) }, expectedErr: false, }, { - name: "successful setup with failed preflights", + name: "successful setup with failed preflights", + currentState: StatePreflightsFailed, + expectedState: StateSucceeded, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := types.Status{ - State: types.StateFailed, - } preflightOutput := &types.HostPreflightsOutput{ Fail: []types.HostPreflightsRecord{ { @@ -596,54 +670,40 @@ func TestSetupInfra(t *testing.T) { }, } mock.InOrder( - pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), pm.On("GetHostPreflightOutput", t.Context()).Return(preflightOutput, nil), - r.On("ReportPreflightsFailed", t.Context(), preflightOutput).Return(nil), - fm.On("Install", t.Context(), rc).Return(nil), + r.On("ReportPreflightsBypassed", t.Context(), preflightOutput).Return(nil), + fm.On("Install", mock.Anything, rc).Return(nil), ) }, expectedErr: false, }, { - name: "preflight status error", + name: "preflight output error", + currentState: StatePreflightsFailed, + expectedState: StatePreflightsFailed, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - pm.On("GetHostPreflightStatus", t.Context()).Return(nil, errors.New("get preflight status error")) - }, - expectedErr: true, - }, - { - name: "preflight not completed", - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := types.Status{ - State: types.StateRunning, - } - pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil) + mock.InOrder( + pm.On("GetHostPreflightOutput", t.Context()).Return(nil, errors.New("get output error")), + ) }, expectedErr: true, }, { - name: "preflight output error", + name: "install infra error", + currentState: StatePreflightsSucceeded, + expectedState: StateFailed, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := types.Status{ - State: types.StateFailed, - } mock.InOrder( - pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), - pm.On("GetHostPreflightOutput", t.Context()).Return(nil, errors.New("get output error")), + fm.On("Install", mock.Anything, rc).Return(errors.New("install error")), ) }, - expectedErr: true, + expectedErr: false, }, { - name: "install infra error", + name: "invalid state transition", + currentState: StateInstallationConfigured, + expectedState: StateInstallationConfigured, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := types.Status{ - State: types.StateSucceeded, - } - mock.InOrder( - pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil), - fm.On("Install", t.Context(), rc).Return(errors.New("install error")), - ) }, expectedErr: true, }, @@ -651,6 +711,8 @@ func TestSetupInfra(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + sm := NewStateMachine(WithCurrentState(tt.currentState)) + rc := runtimeconfig.New(nil) rc.SetDataDir(t.TempDir()) rc.SetManagerPort(9001) @@ -663,6 +725,7 @@ func TestSetupInfra(t *testing.T) { controller, err := NewInstallController( WithRuntimeConfig(rc), + WithStateMachine(sm), WithHostPreflightManager(mockPreflightManager), WithInstallationManager(mockInstallationManager), WithInfraManager(mockInfraManager), @@ -673,11 +736,17 @@ func TestSetupInfra(t *testing.T) { err = controller.SetupInfra(t.Context()) if tt.expectedErr { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) + + assert.NotEqual(t, sm.CurrentState(), tt.currentState, "state should have changed and should not be %s", tt.currentState) } + assert.Eventually(t, func() bool { + return sm.CurrentState() == tt.expectedState + }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + mockPreflightManager.AssertExpectations(t) mockInstallationManager.AssertExpectations(t) mockInfraManager.AssertExpectations(t) diff --git a/api/controllers/install/hostpreflight.go b/api/controllers/install/hostpreflight.go index dc46a4d50..4a74e2691 100644 --- a/api/controllers/install/hostpreflight.go +++ b/api/controllers/install/hostpreflight.go @@ -3,6 +3,7 @@ package install import ( "context" "fmt" + "runtime/debug" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" @@ -11,6 +12,15 @@ import ( ) func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostPreflightsOptions) error { + lock, err := c.stateMachine.AcquireLock() + if err != nil { + return types.NewConflictError(err) + } + + if err := c.stateMachine.ValidateTransition(lock, StatePreflightsRunning); err != nil { + return types.NewConflictError(err) + } + // Get the configured custom domains ecDomains := utils.GetDomains(c.releaseData) @@ -24,13 +34,52 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP IsUI: opts.IsUI, }) if err != nil { + lock.Release() return fmt.Errorf("failed to prepare host preflights: %w", err) } - // Run host preflights - return c.hostPreflightManager.RunHostPreflights(ctx, c.rc, preflight.RunHostPreflightOptions{ - HostPreflightSpec: hpf, - }) + err = c.stateMachine.Transition(lock, StatePreflightsRunning) + if err != nil { + return fmt.Errorf("failed to transition states: %w", err) + } + + go func() { + // Background context is used to avoid canceling the operation if the context is canceled + ctx := context.Background() + + defer lock.Release() + + defer func() { + if r := recover(); r != nil { + c.logger.Errorf("panic running host preflights: %v: %s", r, string(debug.Stack())) + + err := c.stateMachine.Transition(lock, StatePreflightsFailed) + if err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } + }() + + err := c.hostPreflightManager.RunHostPreflights(ctx, c.rc, preflight.RunHostPreflightOptions{ + HostPreflightSpec: hpf, + }) + + if err != nil { + c.logger.Errorf("failed to run host preflights: %w", err) + + err = c.stateMachine.Transition(lock, StatePreflightsFailed) + if err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } else { + err = c.stateMachine.Transition(lock, StatePreflightsSucceeded) + if err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } + }() + + return nil } func (c *InstallController) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { diff --git a/api/controllers/install/infra.go b/api/controllers/install/infra.go index 2acb4297a..ec3341d74 100644 --- a/api/controllers/install/infra.go +++ b/api/controllers/install/infra.go @@ -3,32 +3,91 @@ package install import ( "context" "fmt" + "runtime/debug" "github.com/replicatedhq/embedded-cluster/api/types" ) func (c *InstallController) SetupInfra(ctx context.Context) error { - preflightStatus, err := c.GetHostPreflightStatus(ctx) + if c.stateMachine.CurrentState() == StatePreflightsFailed { + err := c.bypassPreflights(ctx) + if err != nil { + return fmt.Errorf("bypass preflights: %w", err) + } + } + + lock, err := c.stateMachine.AcquireLock() if err != nil { - return fmt.Errorf("get install host preflight status: %w", err) + return types.NewConflictError(err) } - if preflightStatus.State != types.StateFailed && preflightStatus.State != types.StateSucceeded { - return fmt.Errorf("host preflight checks did not complete") + err = c.stateMachine.Transition(lock, StateInfrastructureInstalling) + if err != nil { + lock.Release() + return types.NewConflictError(err) } - if preflightStatus.State == types.StateFailed && c.metricsReporter != nil { - preflightOutput, err := c.GetHostPreflightOutput(ctx) + go func() { + // Background context is used to avoid canceling the operation if the context is canceled + ctx := context.Background() + + defer lock.Release() + + defer func() { + if r := recover(); r != nil { + c.logger.Errorf("panic installing infrastructure: %v: %s", r, string(debug.Stack())) + + err := c.stateMachine.Transition(lock, StateFailed) + if err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } + }() + + err := c.infraManager.Install(ctx, c.rc) + if err != nil { - return fmt.Errorf("get install host preflight output: %w", err) - } - if preflightOutput != nil { - c.metricsReporter.ReportPreflightsFailed(ctx, preflightOutput) + c.logger.Errorf("failed to install infrastructure: %w", err) + + err := c.stateMachine.Transition(lock, StateFailed) + if err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } else { + err = c.stateMachine.Transition(lock, StateSucceeded) + if err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } } + }() + + return nil +} + +func (c *InstallController) bypassPreflights(ctx context.Context) error { + lock, err := c.stateMachine.AcquireLock() + if err != nil { + return types.NewConflictError(err) + } + defer lock.Release() + + if err := c.stateMachine.ValidateTransition(lock, StatePreflightsFailedBypassed); err != nil { + return types.NewConflictError(err) + } + + // TODO (@ethan): we have already sent the preflight output when we sent the failed event. + // We should evaluate if we should send it again. + preflightOutput, err := c.GetHostPreflightOutput(ctx) + if err != nil { + return fmt.Errorf("get install host preflight output: %w", err) + } + if preflightOutput != nil { + c.metricsReporter.ReportPreflightsBypassed(ctx, preflightOutput) } - if err := c.infraManager.Install(ctx, c.rc); err != nil { - return fmt.Errorf("install infra: %w", err) + err = c.stateMachine.Transition(lock, StatePreflightsFailedBypassed) + if err != nil { + return types.NewConflictError(err) } return nil diff --git a/api/controllers/install/installation.go b/api/controllers/install/installation.go index 1902a03f2..71b2e80c7 100644 --- a/api/controllers/install/installation.go +++ b/api/controllers/install/installation.go @@ -28,6 +28,48 @@ func (c *InstallController) GetInstallationConfig(ctx context.Context) (types.In } func (c *InstallController) ConfigureInstallation(ctx context.Context, config types.InstallationConfig) error { + err := c.configureInstallation(ctx, config) + if err != nil { + return err + } + + go func() { + // Background context is used to avoid canceling the operation if the context is canceled + ctx := context.Background() + + lock, err := c.stateMachine.AcquireLock() + if err != nil { + c.logger.Error("failed to acquire lock", "error", err) + return + } + defer lock.Release() + + err = c.installationManager.ConfigureHost(ctx, c.rc) + + if err != nil { + c.logger.Error("failed to configure host", "error", err) + } else { + err = c.stateMachine.Transition(lock, StateHostConfigured) + if err != nil { + c.logger.Error("failed to transition states", "error", err) + } + } + }() + + return nil +} + +func (c *InstallController) configureInstallation(ctx context.Context, config types.InstallationConfig) error { + lock, err := c.stateMachine.AcquireLock() + if err != nil { + return types.NewConflictError(err) + } + defer lock.Release() + + if err := c.stateMachine.ValidateTransition(lock, StateInstallationConfigured); err != nil { + return types.NewConflictError(err) + } + if err := c.installationManager.ValidateConfig(config, c.rc.ManagerPort()); err != nil { return fmt.Errorf("validate: %w", err) } @@ -66,8 +108,9 @@ func (c *InstallController) ConfigureInstallation(ctx context.Context, config ty return fmt.Errorf("set env vars: %w", err) } - if err := c.installationManager.ConfigureHost(ctx, c.rc); err != nil { - return fmt.Errorf("configure: %w", err) + err = c.stateMachine.Transition(lock, StateInstallationConfigured) + if err != nil { + return fmt.Errorf("failed to transition states: %w", err) } return nil diff --git a/api/controllers/install/statemachine.go b/api/controllers/install/statemachine.go new file mode 100644 index 000000000..10b3928c2 --- /dev/null +++ b/api/controllers/install/statemachine.go @@ -0,0 +1,62 @@ +package install + +import "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" + +const ( + // StateNew is the initial state of the install process + StateNew statemachine.State = "New" + // StateInstallationConfigured is the state of the install process when the installation is configured + StateInstallationConfigured statemachine.State = "InstallationConfigured" + // StateHostConfigured is the state of the install process when the host is configured + StateHostConfigured statemachine.State = "HostConfigured" + // StatePreflightsRunning is the state of the install process when the preflights are running + StatePreflightsRunning statemachine.State = "PreflightsRunning" + // StatePreflightsSucceeded is the state of the install process when the preflights have succeeded + StatePreflightsSucceeded statemachine.State = "PreflightsSucceeded" + // StatePreflightsFailed is the state of the install process when the preflights have failed + StatePreflightsFailed statemachine.State = "PreflightsFailed" + // StatePreflightsFailedBypassed is the state of the install process when the preflights have failed bypassed + StatePreflightsFailedBypassed statemachine.State = "PreflightsFailedBypassed" + // StateInfrastructureInstalling is the state of the install process when the infrastructure is being installed + StateInfrastructureInstalling statemachine.State = "InfrastructureInstalling" + // StateSucceeded is the final state of the install process when the install has succeeded + StateSucceeded statemachine.State = "Succeeded" + // StateFailed is the final state of the install process when the install has failed + StateFailed statemachine.State = "Failed" +) + +var validStateTransitions = map[statemachine.State][]statemachine.State{ + StateNew: {StateInstallationConfigured}, + StateInstallationConfigured: {StateHostConfigured, StateInstallationConfigured}, + StateHostConfigured: {StatePreflightsRunning, StateInstallationConfigured}, + StatePreflightsRunning: {StatePreflightsSucceeded, StatePreflightsFailed}, + StatePreflightsSucceeded: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured}, + StatePreflightsFailed: {StatePreflightsFailedBypassed, StatePreflightsRunning, StateInstallationConfigured}, + StatePreflightsFailedBypassed: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured}, + StateInfrastructureInstalling: {StateSucceeded, StateFailed}, + StateSucceeded: {}, + StateFailed: {}, +} + +type StateMachineOptions struct { + CurrentState statemachine.State +} + +type StateMachineOption func(*StateMachineOptions) + +func WithCurrentState(currentState statemachine.State) StateMachineOption { + return func(o *StateMachineOptions) { + o.CurrentState = currentState + } +} + +// NewStateMachine creates a new state machine starting in the New state +func NewStateMachine(opts ...StateMachineOption) statemachine.Interface { + options := &StateMachineOptions{ + CurrentState: StateNew, + } + for _, opt := range opts { + opt(options) + } + return statemachine.New(options.CurrentState, validStateTransitions) +} diff --git a/api/controllers/install/statemachine_test.go b/api/controllers/install/statemachine_test.go new file mode 100644 index 000000000..48ab5c882 --- /dev/null +++ b/api/controllers/install/statemachine_test.go @@ -0,0 +1,140 @@ +package install + +import ( + "slices" + "testing" + + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" + "github.com/stretchr/testify/assert" +) + +func TestStateMachineTransitions(t *testing.T) { + tests := []struct { + name string + startState statemachine.State + validTransitions []statemachine.State + }{ + { + name: `State "New" can transition to "InstallationConfigured"`, + startState: StateNew, + validTransitions: []statemachine.State{ + StateInstallationConfigured, + }, + }, + { + name: `State "InstallationConfigured" can transition to "HostConfigured" or "InstallationConfigured"`, + startState: StateInstallationConfigured, + validTransitions: []statemachine.State{ + StateHostConfigured, + StateInstallationConfigured, + }, + }, + { + name: `State "HostConfigured" can transition to "PreflightsRunning" or "InstallationConfigured"`, + startState: StateHostConfigured, + validTransitions: []statemachine.State{ + StatePreflightsRunning, + StateInstallationConfigured, + }, + }, + { + name: `State "PreflightsRunning" can transition to "PreflightsSucceeded" or "PreflightsFailed"`, + startState: StatePreflightsRunning, + validTransitions: []statemachine.State{ + StatePreflightsSucceeded, + StatePreflightsFailed, + }, + }, + { + name: `State "PreflightsSucceeded" can transition to "InfrastructureInstalling", "PreflightsRunning" or "InstallationConfigured"`, + startState: StatePreflightsSucceeded, + validTransitions: []statemachine.State{ + StateInfrastructureInstalling, + StatePreflightsRunning, + StateInstallationConfigured, + }, + }, + { + name: `State "PreflightsFailed" can transition to "PreflightsFailedBypassed" , "PreflightsRunning" or "InstallationConfigured"`, + startState: StatePreflightsFailed, + validTransitions: []statemachine.State{ + StatePreflightsFailedBypassed, + StatePreflightsRunning, + StateInstallationConfigured, + }, + }, + { + name: `State "PreflightsFailedBypassed" can transition to "InfrastructureInstalling", "PreflightsRunning" or "InstallationConfigured"`, + startState: StatePreflightsFailedBypassed, + validTransitions: []statemachine.State{ + StateInfrastructureInstalling, + StatePreflightsRunning, + StateInstallationConfigured, + }, + }, + { + name: `State "InfrastructureInstalling" can transition to "Succeeded" or "Failed"`, + startState: StateInfrastructureInstalling, + validTransitions: []statemachine.State{ + StateSucceeded, + StateFailed, + }, + }, + { + name: `State "Succeeded" can not transition to any other state`, + startState: StateSucceeded, + validTransitions: []statemachine.State{}, + }, + { + name: `State "Failed" can not transition to any other state`, + startState: StateFailed, + validTransitions: []statemachine.State{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for nextState := range validStateTransitions { + sm := NewStateMachine(WithCurrentState(tt.startState)) + + lock, err := sm.AcquireLock() + if err != nil { + t.Fatalf("failed to acquire lock: %v", err) + } + defer lock.Release() + + err = sm.Transition(lock, nextState) + if !slices.Contains(tt.validTransitions, nextState) { + assert.Error(t, err, "expected error for transition from %s to %s", tt.startState, nextState) + } else { + assert.NoError(t, err, "unexpected error for transition from %s to %s", tt.startState, nextState) + + // Verify state has changed + assert.Equal(t, nextState, sm.CurrentState(), "state should change after commit") + } + } + }) + } +} + +func TestIsFinalState(t *testing.T) { + finalStates := []statemachine.State{ + StateSucceeded, + StateFailed, + } + + for state := range validStateTransitions { + var isFinal bool + if slices.Contains(finalStates, state) { + isFinal = true + } + + sm := NewStateMachine(WithCurrentState(state)) + + if isFinal { + assert.True(t, sm.IsFinalState(), "expected state %s to be final", state) + } else { + assert.False(t, sm.IsFinalState(), "expected state %s to not be final", state) + } + } +} diff --git a/api/integration/hostpreflights_test.go b/api/integration/hostpreflights_test.go index 7cd868b7b..7270808a5 100644 --- a/api/integration/hostpreflights_test.go +++ b/api/integration/hostpreflights_test.go @@ -64,7 +64,9 @@ func TestGetHostPreflightsStatus(t *testing.T) { preflight.WithPreflightRunner(runner), ) // Create an install controller - installController, err := install.NewInstallController(install.WithHostPreflightManager(manager)) + installController, err := install.NewInstallController( + install.WithHostPreflightManager(manager), + ) require.NoError(t, err) // Create the API with the install controller @@ -190,6 +192,9 @@ func TestPostRunHostPreflights(t *testing.T) { // Create an install controller with the mocked manager installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine( + install.WithCurrentState(install.StateHostConfigured), + )), install.WithHostPreflightManager(pfManager), install.WithInstallationManager(iManager), // Mock the release data used by the preflight runner @@ -291,6 +296,9 @@ func TestPostRunHostPreflights(t *testing.T) { // Create an install controller installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine( + install.WithCurrentState(install.StateHostConfigured), + )), install.WithHostPreflightManager(manager), install.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, @@ -346,6 +354,9 @@ func TestPostRunHostPreflights(t *testing.T) { // Create an install controller with the failing manager installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine( + install.WithCurrentState(install.StateHostConfigured), + )), install.WithHostPreflightManager(manager), install.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, @@ -402,6 +413,9 @@ func TestPostRunHostPreflights(t *testing.T) { // Create an install controller with the failing manager installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine( + install.WithCurrentState(install.StateHostConfigured), + )), install.WithHostPreflightManager(manager), install.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, @@ -471,6 +485,9 @@ func TestPostRunHostPreflights(t *testing.T) { // Create an install controller with the failing manager installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine( + install.WithCurrentState(install.StatePreflightsRunning), + )), install.WithHostPreflightManager(manager), install.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, diff --git a/api/integration/install_test.go b/api/integration/install_test.go index e19a51a47..d829e89f8 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -10,7 +10,6 @@ import ( "net/http" "net/http/httptest" "os" - "path/filepath" "strings" "testing" "time" @@ -49,7 +48,7 @@ import ( var ( //go:embed assets/license.yaml - licenseData string + licenseData []byte ) // Mock implementation of the install.Controller interface @@ -277,6 +276,7 @@ func TestConfigureInstallation(t *testing.T) { // Create an install controller with the config manager installController, err := install.NewInstallController( install.WithRuntimeConfig(rc), + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateHostConfigured))), install.WithHostUtils(tc.mockHostUtils), install.WithNetUtils(tc.mockNetUtils), ) @@ -371,6 +371,7 @@ func TestConfigureInstallationValidation(t *testing.T) { // Create an install controller with the config manager installController, err := install.NewInstallController( install.WithRuntimeConfig(rc), + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateHostConfigured))), ) require.NoError(t, err) @@ -431,6 +432,7 @@ func TestConfigureInstallationBadRequest(t *testing.T) { // Create an install controller with the config manager installController, err := install.NewInstallController( install.WithRuntimeConfig(rc), + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateHostConfigured))), ) require.NoError(t, err) @@ -1150,17 +1152,13 @@ func TestPostSetupInfra(t *testing.T) { preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), ) - // Create license file - licenseFile := filepath.Join(t.TempDir(), "license.yaml") - require.NoError(t, os.WriteFile(licenseFile, []byte(licenseData), 0644)) - // Create infra manager with mocks infraManager := infra.NewInfraManager( infra.WithK0s(k0sMock), infra.WithKubeClient(fakeKcli), infra.WithMetadataClient(fakeMcli), infra.WithHelmClient(helmMock), - infra.WithLicenseFile(licenseFile), + infra.WithLicense(licenseData), infra.WithHostUtils(hostutilsMock), infra.WithKotsInstaller(func() error { return nil @@ -1198,6 +1196,8 @@ func TestPostSetupInfra(t *testing.T) { // Create an install controller with the mocked managers installController, err := install.NewInstallController( + install.WithRuntimeConfig(rc), + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsSucceeded))), install.WithHostPreflightManager(pfManager), install.WithInfraManager(infraManager), install.WithReleaseData(&release.ReleaseData{ @@ -1209,7 +1209,6 @@ func TestPostSetupInfra(t *testing.T) { }, }, }), - install.WithRuntimeConfig(rc), ) require.NoError(t, err) @@ -1367,6 +1366,7 @@ func TestPostSetupInfra(t *testing.T) { // Create an install controller installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsRunning))), install.WithHostPreflightManager(pfManager), ) require.NoError(t, err) @@ -1392,7 +1392,7 @@ func TestPostSetupInfra(t *testing.T) { router.ServeHTTP(rec, req) // Check the response - assert.Equal(t, http.StatusInternalServerError, rec.Code) + assert.Equal(t, http.StatusConflict, rec.Code) t.Logf("Response body: %s", rec.Body.String()) @@ -1400,15 +1400,12 @@ func TestPostSetupInfra(t *testing.T) { var apiError types.APIError err = json.NewDecoder(rec.Body).Decode(&apiError) require.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, apiError.StatusCode) - assert.Contains(t, apiError.Message, "host preflight checks did not complete") + assert.Equal(t, http.StatusConflict, apiError.StatusCode) + assert.Contains(t, apiError.Message, "invalid transition from PreflightsRunning to InfrastructureInstalling") }) // Test k0s already installed error t.Run("K0s already installed", func(t *testing.T) { - // Create mocks - k0sMock := &k0s.MockK0s{} - // Create a runtime config rc := runtimeconfig.New(nil) rc.SetDataDir(t.TempDir()) @@ -1427,22 +1424,16 @@ func TestPostSetupInfra(t *testing.T) { pfManager := preflight.NewHostPreflightManager( preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), ) - infraManager := infra.NewInfraManager( - infra.WithK0s(k0sMock), - ) - - // Setup k0s mock to return already installed - k0sMock.On("IsInstalled").Return(true, nil) // Create an install controller installController, err := install.NewInstallController( + install.WithRuntimeConfig(rc), + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateSucceeded))), install.WithHostPreflightManager(pfManager), - install.WithInfraManager(infraManager), install.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{}, }), - install.WithRuntimeConfig(rc), ) require.NoError(t, err) @@ -1467,11 +1458,8 @@ func TestPostSetupInfra(t *testing.T) { router.ServeHTTP(rec, req) // Check the response - assert.Equal(t, http.StatusInternalServerError, rec.Code) - assert.Contains(t, rec.Body.String(), "installation is detected") - - // Verify that the mock expectations were met - k0sMock.AssertExpectations(t) + assert.Equal(t, http.StatusConflict, rec.Code) + assert.Contains(t, rec.Body.String(), "invalid transition from Succeeded to InfrastructureInstalling") }) // Test k0s install error @@ -1496,10 +1484,6 @@ func TestPostSetupInfra(t *testing.T) { Description: "Host preflights succeeded", } - // Create license file - licenseFile := filepath.Join(t.TempDir(), "license.yaml") - require.NoError(t, os.WriteFile(licenseFile, []byte(licenseData), 0644)) - // Create managers pfManager := preflight.NewHostPreflightManager( preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), @@ -1507,7 +1491,7 @@ func TestPostSetupInfra(t *testing.T) { infraManager := infra.NewInfraManager( infra.WithK0s(k0sMock), infra.WithHostUtils(hostutilsMock), - infra.WithLicenseFile(licenseFile), + infra.WithLicense(licenseData), ) // Setup k0s mock expectations with failure @@ -1528,6 +1512,7 @@ func TestPostSetupInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, }), install.WithRuntimeConfig(rc), + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsSucceeded))), ) require.NoError(t, err) diff --git a/api/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index 57b870f5b..5bea965ad 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -16,7 +16,6 @@ import ( addontypes "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/extensions" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -25,6 +24,7 @@ import ( "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" + kyaml "sigs.k8s.io/yaml" ) const K0sComponentName = "Runtime" @@ -37,40 +37,39 @@ func AlreadyInstalledError() error { } func (m *infraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConfig) (finalErr error) { - m.mu.Lock() - defer m.mu.Unlock() - installed, err := m.k0scli.IsInstalled() if err != nil { - return err + return fmt.Errorf("check if k0s is installed: %w", err) } if installed { return AlreadyInstalledError() } - didRun, err := m.installDidRun() - if err != nil { - return fmt.Errorf("check if install did run: %w", err) - } - if didRun { - return fmt.Errorf("install can only be run once") + if err := m.setStatus(types.StateRunning, ""); err != nil { + return fmt.Errorf("set status: %w", err) } - license, err := helpers.ParseLicense(m.licenseFile) - if err != nil { - return fmt.Errorf("parse license: %w", err) - } + defer func() { + if r := recover(); r != nil { + finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) - if err := m.initComponentsList(license, rc); err != nil { - return fmt.Errorf("init components: %w", err) - } + if err := m.setStatus(types.StateFailed, "Installation failed to run: panic"); err != nil { + m.logger.WithField("error", err).Error("set failed status") + } + } + }() - if err := m.setStatus(types.StateRunning, ""); err != nil { - return fmt.Errorf("set status: %w", err) - } + err = m.install(ctx, rc) - // Background context is used to avoid canceling the operation if the context is canceled - go m.install(context.Background(), license, rc) + if err != nil { + if err := m.setStatus(types.StateFailed, err.Error()); err != nil { + m.logger.WithField("error", err).Error("set failed status") + } + } else { + if err := m.setStatus(types.StateSucceeded, "Installation complete"); err != nil { + m.logger.WithField("error", err).Error("set succeeded status") + } + } return nil } @@ -96,21 +95,15 @@ func (m *infraManager) initComponentsList(license *kotsv1beta1.License, rc runti return nil } -func (m *infraManager) install(ctx context.Context, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) (finalErr error) { - defer func() { - if r := recover(); r != nil { - finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) - } - if finalErr != nil { - if err := m.setStatus(types.StateFailed, finalErr.Error()); err != nil { - m.logger.WithField("error", err).Error("set failed status") - } - } else { - if err := m.setStatus(types.StateSucceeded, "Installation complete"); err != nil { - m.logger.WithField("error", err).Error("set succeeded status") - } - } - }() +func (m *infraManager) install(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + license := &kotsv1beta1.License{} + if err := kyaml.Unmarshal(m.license, license); err != nil { + return fmt.Errorf("parse license: %w", err) + } + + if err := m.initComponentsList(license, rc); err != nil { + return fmt.Errorf("init components: %w", err) + } _, err := m.installK0s(ctx, rc) if err != nil { @@ -138,7 +131,7 @@ func (m *infraManager) install(ctx context.Context, license *kotsv1beta1.License return fmt.Errorf("record installation: %w", err) } - if err := m.installAddOns(ctx, license, kcli, mcli, hcli, rc); err != nil { + if err := m.installAddOns(ctx, kcli, mcli, hcli, license, rc); err != nil { return fmt.Errorf("install addons: %w", err) } @@ -256,7 +249,7 @@ func (m *infraManager) recordInstallation(ctx context.Context, kcli client.Clien return in, nil } -func (m *infraManager) installAddOns(ctx context.Context, license *kotsv1beta1.License, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig) error { +func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mcli metadata.Interface, hcli helm.Client, license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) error { progressChan := make(chan addontypes.AddOnProgress) defer close(progressChan) @@ -321,7 +314,7 @@ func (m *infraManager) getAddonInstallOpts(license *kotsv1beta1.License, rc runt opts := kotscli.InstallOptions{ RuntimeConfig: rc, AppSlug: license.Spec.AppSlug, - LicenseFile: m.licenseFile, + License: m.license, Namespace: runtimeconfig.KotsadmNamespace, AirgapBundle: m.airgapBundle, ConfigValuesFile: m.configValues, diff --git a/api/internal/managers/infra/manager.go b/api/internal/managers/infra/manager.go index d21583081..4d8103a33 100644 --- a/api/internal/managers/infra/manager.go +++ b/api/internal/managers/infra/manager.go @@ -31,7 +31,7 @@ type infraManager struct { infraStore infra.Store password string tlsConfig types.TLSConfig - licenseFile string + license []byte airgapBundle string configValues string releaseData *release.ReleaseData @@ -72,9 +72,9 @@ func WithTLSConfig(tlsConfig types.TLSConfig) InfraManagerOption { } } -func WithLicenseFile(licenseFile string) InfraManagerOption { +func WithLicense(license []byte) InfraManagerOption { return func(c *infraManager) { - c.licenseFile = licenseFile + c.license = license } } diff --git a/api/internal/managers/infra/status.go b/api/internal/managers/infra/status.go index afeaeba93..a7b303b1f 100644 --- a/api/internal/managers/infra/status.go +++ b/api/internal/managers/infra/status.go @@ -1,7 +1,6 @@ package infra import ( - "fmt" "time" "github.com/replicatedhq/embedded-cluster/api/types" @@ -15,20 +14,6 @@ func (m *infraManager) SetStatus(status types.Status) error { return m.infraStore.SetStatus(status) } -func (m *infraManager) installDidRun() (bool, error) { - currStatus, err := m.GetStatus() - if err != nil { - return false, fmt.Errorf("get status: %w", err) - } - if currStatus.State == "" { - return false, nil - } - if currStatus.State == types.StatePending { - return false, nil - } - return true, nil -} - func (m *infraManager) setStatus(state types.State, description string) error { return m.SetStatus(types.Status{ State: state, diff --git a/api/internal/managers/infra/status_test.go b/api/internal/managers/infra/status_test.go index 7c4bca268..c003433a8 100644 --- a/api/internal/managers/infra/status_test.go +++ b/api/internal/managers/infra/status_test.go @@ -4,8 +4,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - - "github.com/replicatedhq/embedded-cluster/api/types" ) func TestInfraWithLogs(t *testing.T) { @@ -22,75 +20,3 @@ func TestInfraWithLogs(t *testing.T) { assert.Contains(t, infra.Logs, "[test] Test log message") assert.Contains(t, infra.Logs, "[test] Another log message with arg: value") } - -func TestInstallDidRun(t *testing.T) { - tests := []struct { - name string - currentStatus types.Status - expectedResult bool - expectedErr bool - }{ - { - name: "nil status", - currentStatus: types.Status{}, - expectedResult: false, - expectedErr: false, - }, - { - name: "empty state", - currentStatus: types.Status{ - State: "", - }, - expectedResult: false, - expectedErr: false, - }, - { - name: "pending state", - currentStatus: types.Status{ - State: types.StatePending, - }, - expectedResult: false, - expectedErr: false, - }, - { - name: "running state", - currentStatus: types.Status{ - State: types.StateRunning, - }, - expectedResult: true, - expectedErr: false, - }, - { - name: "failed state", - currentStatus: types.Status{ - State: types.StateFailed, - }, - expectedResult: true, - expectedErr: false, - }, - { - name: "succeeded state", - currentStatus: types.Status{ - State: types.StateSucceeded, - }, - expectedResult: true, - expectedErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - manager := NewInfraManager() - manager.SetStatus(tt.currentStatus) - - result, err := manager.installDidRun() - - if tt.expectedErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedResult, result) - } - }) - } -} diff --git a/api/internal/managers/installation/config.go b/api/internal/managers/installation/config.go index e3c6127b1..ece4ec1cf 100644 --- a/api/internal/managers/installation/config.go +++ b/api/internal/managers/installation/config.go @@ -203,29 +203,11 @@ func (m *installationManager) setCIDRDefaults(config *types.InstallationConfig) return nil } -func (m *installationManager) ConfigureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { - m.mu.Lock() - defer m.mu.Unlock() - - running, err := m.isRunning() - if err != nil { - return fmt.Errorf("check if installation is running: %w", err) - } - if running { - return fmt.Errorf("installation configuration is already running") - } - +func (m *installationManager) ConfigureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) (finalErr error) { if err := m.setRunningStatus("Configuring installation"); err != nil { return fmt.Errorf("set running status: %w", err) } - // Background context is used to avoid canceling the operation if the context is canceled - go m.configureHost(context.Background(), rc) - - return nil -} - -func (m *installationManager) configureHost(ctx context.Context, rc runtimeconfig.RuntimeConfig) (finalErr error) { defer func() { if r := recover(); r != nil { finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) @@ -242,11 +224,11 @@ func (m *installationManager) configureHost(ctx context.Context, rc runtimeconfi }() opts := hostutils.InitForInstallOptions{ - LicenseFile: m.licenseFile, + License: m.license, AirgapBundle: m.airgapBundle, } if err := m.hostUtils.ConfigureHost(ctx, rc, opts); err != nil { - return fmt.Errorf("configure installation: %w", err) + return fmt.Errorf("configure host: %w", err) } return nil diff --git a/api/internal/managers/installation/config_test.go b/api/internal/managers/installation/config_test.go index 6c5822fdc..b433f42dd 100644 --- a/api/internal/managers/installation/config_test.go +++ b/api/internal/managers/installation/config_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -354,7 +353,6 @@ func TestConfigureHost(t *testing.T) { }(), setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { mock.InOrder( - im.On("GetStatus").Return(types.Status{State: types.StatePending}, nil), im.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { return status.State == types.StateRunning })).Return(nil), hum.On("ConfigureHost", mock.Anything, mock.MatchedBy(func(rc runtimeconfig.RuntimeConfig) bool { @@ -363,7 +361,7 @@ func TestConfigureHost(t *testing.T) { rc.ServiceCIDR() == "10.1.0.0/16" }), hostutils.InitForInstallOptions{ - LicenseFile: "license.yaml", + License: []byte("metadata:\n name: test-license"), AirgapBundle: "bundle.tar", }).Return(nil), im.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { return status.State == types.StateSucceeded })).Return(nil), @@ -371,19 +369,6 @@ func TestConfigureHost(t *testing.T) { }, expectedErr: false, }, - { - name: "already running", - rc: func() runtimeconfig.RuntimeConfig { - rc := runtimeconfig.New(&ecv1beta1.RuntimeConfigSpec{ - DataDir: "/var/lib/embedded-cluster", - }) - return rc - }(), - setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { - im.On("GetStatus").Return(types.Status{State: types.StateRunning}, nil) - }, - expectedErr: true, - }, { name: "configure installation fails", rc: func() runtimeconfig.RuntimeConfig { @@ -394,21 +379,20 @@ func TestConfigureHost(t *testing.T) { }(), setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { mock.InOrder( - im.On("GetStatus").Return(types.Status{State: types.StatePending}, nil), im.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { return status.State == types.StateRunning })).Return(nil), hum.On("ConfigureHost", mock.Anything, mock.MatchedBy(func(rc runtimeconfig.RuntimeConfig) bool { return rc.EmbeddedClusterHomeDirectory() == "/var/lib/embedded-cluster" }), hostutils.InitForInstallOptions{ - LicenseFile: "license.yaml", + License: []byte("metadata:\n name: test-license"), AirgapBundle: "bundle.tar", }, ).Return(errors.New("configuration failed")), im.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { return status.State == types.StateFailed })).Return(nil), ) }, - expectedErr: false, + expectedErr: true, }, { name: "set running status fails", @@ -420,7 +404,6 @@ func TestConfigureHost(t *testing.T) { }(), setupMocks: func(hum *hostutils.MockHostUtils, im *installation.MockStore) { mock.InOrder( - im.On("GetStatus").Return(types.Status{State: types.StatePending}, nil), im.On("SetStatus", mock.Anything).Return(errors.New("failed to set status")), ) }, @@ -443,7 +426,7 @@ func TestConfigureHost(t *testing.T) { manager := NewInstallationManager( WithHostUtils(mockHostUtils), WithInstallationStore(mockStore), - WithLicenseFile("license.yaml"), + WithLicense([]byte("metadata:\n name: test-license")), WithAirgapBundle("bundle.tar"), ) @@ -455,9 +438,6 @@ func TestConfigureHost(t *testing.T) { assert.Error(t, err) } else { assert.NoError(t, err) - - // Wait a bit for the goroutine to complete - time.Sleep(200 * time.Millisecond) } // Verify all mock expectations were met diff --git a/api/internal/managers/installation/manager.go b/api/internal/managers/installation/manager.go index bfb0329ec..bc462389c 100644 --- a/api/internal/managers/installation/manager.go +++ b/api/internal/managers/installation/manager.go @@ -2,7 +2,6 @@ package installation import ( "context" - "sync" "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" @@ -29,12 +28,11 @@ type InstallationManager interface { // installationManager is an implementation of the InstallationManager interface type installationManager struct { installationStore installation.Store - licenseFile string + license []byte airgapBundle string netUtils utils.NetUtils hostUtils hostutils.HostUtilsInterface logger logrus.FieldLogger - mu sync.RWMutex } type InstallationManagerOption func(*installationManager) @@ -51,9 +49,9 @@ func WithInstallationStore(installationStore installation.Store) InstallationMan } } -func WithLicenseFile(licenseFile string) InstallationManagerOption { +func WithLicense(license []byte) InstallationManagerOption { return func(c *installationManager) { - c.licenseFile = licenseFile + c.license = license } } diff --git a/api/internal/managers/installation/status.go b/api/internal/managers/installation/status.go index 611b413f9..684b8aeae 100644 --- a/api/internal/managers/installation/status.go +++ b/api/internal/managers/installation/status.go @@ -14,14 +14,6 @@ func (m *installationManager) SetStatus(status types.Status) error { return m.installationStore.SetStatus(status) } -func (m *installationManager) isRunning() (bool, error) { - status, err := m.GetStatus() - if err != nil { - return false, err - } - return status.State == types.StateRunning, nil -} - func (m *installationManager) setRunningStatus(description string) error { return m.SetStatus(types.Status{ State: types.StateRunning, diff --git a/api/internal/managers/preflight/hostpreflight.go b/api/internal/managers/preflight/hostpreflight.go index 9ed9557f3..e0496fccd 100644 --- a/api/internal/managers/preflight/hostpreflight.go +++ b/api/internal/managers/preflight/hostpreflight.go @@ -30,44 +30,6 @@ type RunHostPreflightOptions struct { } func (m *hostPreflightManager) PrepareHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { - hpf, err := m.prepareHostPreflights(ctx, rc, opts) - if err != nil { - return nil, err - } - return hpf, nil -} - -func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.hostPreflightStore.IsRunning() { - return types.NewConflictError(fmt.Errorf("host preflights are already running")) - } - - if err := m.setRunningStatus(opts.HostPreflightSpec); err != nil { - return fmt.Errorf("set running status: %w", err) - } - - // Background context is used to avoid canceling the operation if the context is canceled - go m.runHostPreflights(context.Background(), rc, opts) - - return nil -} - -func (m *hostPreflightManager) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { - return m.hostPreflightStore.GetStatus() -} - -func (m *hostPreflightManager) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) { - return m.hostPreflightStore.GetOutput() -} - -func (m *hostPreflightManager) GetHostPreflightTitles(ctx context.Context) ([]string, error) { - return m.hostPreflightStore.GetTitles() -} - -func (m *hostPreflightManager) prepareHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { // Get node IP nodeIP, err := m.netUtils.FirstValidAddress(rc.NetworkInterface()) if err != nil { @@ -106,15 +68,21 @@ func (m *hostPreflightManager) prepareHostPreflights(ctx context.Context, rc run return hpf, nil } -func (m *hostPreflightManager) runHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) { +func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts RunHostPreflightOptions) (finalErr error) { defer func() { if r := recover(); r != nil { - if err := m.setFailedStatus(fmt.Sprintf("panic: %v: %s", r, string(debug.Stack()))); err != nil { + finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) + + if err := m.setFailedStatus("Host preflights failed to run: panic"); err != nil { m.logger.WithField("error", err).Error("set failed status") } } }() + if err := m.setRunningStatus(opts.HostPreflightSpec); err != nil { + return fmt.Errorf("set running status: %w", err) + } + // Run the preflights using the shared core function output, stderr, err := m.runner.Run(ctx, opts.HostPreflightSpec, rc) if err != nil { @@ -122,8 +90,9 @@ func (m *hostPreflightManager) runHostPreflights(ctx context.Context, rc runtime if stderr != "" { errMsg += fmt.Sprintf(" (stderr: %s)", stderr) } + m.logger.Error(errMsg) if err := m.setFailedStatus(errMsg); err != nil { - m.logger.WithField("error", err).Error("set failed status") + return fmt.Errorf("set failed status: %w", err) } return } @@ -145,13 +114,27 @@ func (m *hostPreflightManager) runHostPreflights(ctx context.Context, rc runtime // Set final status based on results if output.HasFail() { if err := m.setCompletedStatus(types.StateFailed, "Host preflights failed", output); err != nil { - m.logger.WithField("error", err).Error("set failed status") + return fmt.Errorf("set failed status: %w", err) } } else { if err := m.setCompletedStatus(types.StateSucceeded, "Host preflights passed", output); err != nil { - m.logger.WithField("error", err).Error("set succeeded status") + return fmt.Errorf("set succeeded status: %w", err) } } + + return nil +} + +func (m *hostPreflightManager) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { + return m.hostPreflightStore.GetStatus() +} + +func (m *hostPreflightManager) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) { + return m.hostPreflightStore.GetOutput() +} + +func (m *hostPreflightManager) GetHostPreflightTitles(ctx context.Context) ([]string, error) { + return m.hostPreflightStore.GetTitles() } func (m *hostPreflightManager) setRunningStatus(hpf *troubleshootv1beta2.HostPreflightSpec) error { @@ -180,8 +163,6 @@ func (m *hostPreflightManager) setRunningStatus(hpf *troubleshootv1beta2.HostPre } func (m *hostPreflightManager) setFailedStatus(description string) error { - m.logger.Error(description) - return m.hostPreflightStore.SetStatus(types.Status{ State: types.StateFailed, Description: description, diff --git a/api/internal/managers/preflight/hostpreflight_test.go b/api/internal/managers/preflight/hostpreflight_test.go index 4cc700629..6fea05ab5 100644 --- a/api/internal/managers/preflight/hostpreflight_test.go +++ b/api/internal/managers/preflight/hostpreflight_test.go @@ -425,20 +425,6 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { }, expectedFinalState: types.StateSucceeded, }, - { - name: "error - preflights already running", - initialState: types.HostPreflights{ - Status: types.Status{ - State: types.StateRunning, - }, - }, - opts: RunHostPreflightOptions{ - HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, - }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { - }, - expectedError: "host preflights are already running", - }, } for _, tt := range tests { @@ -467,19 +453,13 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { if tt.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) - mockRunner.AssertExpectations(t) - mockMetrics.AssertExpectations(t) - return } else { require.NoError(t, err) } - // Use assert.Eventually to wait for async execution to complete - assert.Eventually(t, func() bool { - status, err := manager.GetHostPreflightStatus(t.Context()) - require.NoError(t, err) - return tt.expectedFinalState == status.State - }, 2*time.Second, 50*time.Millisecond, "Async execution should complete within timeout") + status, err := manager.GetHostPreflightStatus(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.expectedFinalState, status.State) // Additional verification that calls were made in the correct order mockRunner.AssertExpectations(t) diff --git a/api/internal/managers/preflight/manager.go b/api/internal/managers/preflight/manager.go index d5ba90439..e2c8285f8 100644 --- a/api/internal/managers/preflight/manager.go +++ b/api/internal/managers/preflight/manager.go @@ -2,7 +2,6 @@ package preflight import ( "context" - "sync" "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" @@ -30,7 +29,6 @@ type hostPreflightManager struct { netUtils utils.NetUtils logger logrus.FieldLogger metricsReporter metrics.ReporterInterface - mu sync.RWMutex } type HostPreflightManagerOption func(*hostPreflightManager) diff --git a/api/internal/statemachine/statemachine.go b/api/internal/statemachine/statemachine.go new file mode 100644 index 000000000..32aaed390 --- /dev/null +++ b/api/internal/statemachine/statemachine.go @@ -0,0 +1,143 @@ +package statemachine + +import ( + "fmt" + "slices" + "sync" +) + +// State represents the possible states of the install process +type State string + +var ( + _ Interface = &stateMachine{} +) + +// Interface is the interface for the state machine +type Interface interface { + // CurrentState returns the current state + CurrentState() State + // IsFinalState checks if the current state is a final state + IsFinalState() bool + // ValidateTransition checks if a transition from the current state to a new state is valid + ValidateTransition(lock Lock, newState State) error + // Transition attempts to transition to a new state and returns an error if the transition is + // invalid. + Transition(lock Lock, nextState State) error + // AcquireLock acquires a lock on the state machine. + AcquireLock() (Lock, error) +} + +type Lock interface { + // Release releases the lock. + Release() +} + +// stateMachine manages the state transitions for the install process +type stateMachine struct { + currentState State + validStateTransitions map[State][]State + lock *lock + mu sync.RWMutex +} + +// New creates a new state machine starting in the given state with the given valid state +// transitions. +func New(currentState State, validStateTransitions map[State][]State) *stateMachine { + return &stateMachine{ + currentState: currentState, + validStateTransitions: validStateTransitions, + } +} + +func (sm *stateMachine) CurrentState() State { + sm.mu.RLock() + defer sm.mu.RUnlock() + + return sm.currentState +} + +func (sm *stateMachine) IsFinalState() bool { + sm.mu.RLock() + defer sm.mu.RUnlock() + + return len(sm.validStateTransitions[sm.currentState]) == 0 +} + +func (sm *stateMachine) AcquireLock() (Lock, error) { + sm.mu.Lock() + defer sm.mu.Unlock() + + if sm.lock != nil { + return nil, fmt.Errorf("lock already acquired") + } + + sm.lock = &lock{ + release: func() { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.lock = nil + }, + } + + return sm.lock, nil +} + +func (sm *stateMachine) ValidateTransition(lock Lock, nextState State) error { + sm.mu.RLock() + defer sm.mu.RUnlock() + + if sm.lock == nil { + return fmt.Errorf("lock not acquired") + } else if sm.lock != lock { + return fmt.Errorf("lock mismatch") + } + + if !sm.isValidTransition(sm.currentState, nextState) { + return fmt.Errorf("invalid transition from %s to %s", sm.currentState, nextState) + } + + return nil +} + +func (sm *stateMachine) Transition(lock Lock, nextState State) error { + sm.mu.Lock() + defer sm.mu.Unlock() + + if sm.lock == nil { + return fmt.Errorf("lock not acquired") + } else if sm.lock != lock { + return fmt.Errorf("lock mismatch") + } + + if !sm.isValidTransition(sm.currentState, nextState) { + return fmt.Errorf("invalid transition from %s to %s", sm.currentState, nextState) + } + + sm.currentState = nextState + + return nil +} + +func (sm *stateMachine) isValidTransition(currentState State, newState State) bool { + validTransitions, ok := sm.validStateTransitions[currentState] + if !ok { + return false + } + return slices.Contains(validTransitions, newState) +} + +type lock struct { + release func() + mu sync.Mutex +} + +func (l *lock) Release() { + l.mu.Lock() + defer l.mu.Unlock() + + if l.release != nil { + l.release() + l.release = nil + } +} diff --git a/api/internal/statemachine/statemachine_test.go b/api/internal/statemachine/statemachine_test.go new file mode 100644 index 000000000..cc60301f1 --- /dev/null +++ b/api/internal/statemachine/statemachine_test.go @@ -0,0 +1,503 @@ +package statemachine + +import ( + "slices" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + // StateNew is the initial state of the install process + StateNew State = "New" + // StateInstallationConfigured is the state of the install process when the installation is configured + StateInstallationConfigured State = "InstallationConfigured" + // StatePreflightsRunning is the state of the install process when the preflights are running + StatePreflightsRunning State = "PreflightsRunning" + // StatePreflightsSucceeded is the state of the install process when the preflights have succeeded + StatePreflightsSucceeded State = "PreflightsSucceeded" + // StatePreflightsFailed is the state of the install process when the preflights have failed + StatePreflightsFailed State = "PreflightsFailed" + // StatePreflightsFailedBypassed is the state of the install process when the preflights have failed bypassed + StatePreflightsFailedBypassed State = "PreflightsFailedBypassed" + // StateInfrastructureInstalling is the state of the install process when the infrastructure is being installed + StateInfrastructureInstalling State = "InfrastructureInstalling" + // StateSucceeded is the final state of the install process when the install has succeeded + StateSucceeded State = "Succeeded" + // StateFailed is the final state of the install process when the install has failed + StateFailed State = "Failed" +) + +var validStateTransitions = map[State][]State{ + StateNew: {StateInstallationConfigured}, + StateInstallationConfigured: {StatePreflightsRunning}, + StatePreflightsRunning: {StatePreflightsSucceeded, StatePreflightsFailed}, + StatePreflightsSucceeded: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured}, + StatePreflightsFailed: {StatePreflightsFailedBypassed, StatePreflightsRunning, StateInstallationConfigured}, + StatePreflightsFailedBypassed: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured}, + StateInfrastructureInstalling: {StateSucceeded, StateFailed}, + StateSucceeded: {}, + StateFailed: {}, +} + +func TestLockAcquisitionAndRelease(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + // Test valid lock acquisition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock) + + // Test transition with lock + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) + + // Release lock + lock.Release() + + // Test double lock acquisition + lock, err = sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock) + + err = sm.Transition(lock, StatePreflightsRunning) + assert.NoError(t, err) + + // Release lock + lock.Release() + assert.Equal(t, StatePreflightsRunning, sm.CurrentState()) +} + +func TestDoubleLockAcquisition(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock1, err := sm.AcquireLock() + assert.NoError(t, err) + + // Try to acquire second lock while first is held + lock2, err := sm.AcquireLock() + assert.Error(t, err, "second lock acquisition should fail while first is held") + assert.Nil(t, lock2) + assert.Contains(t, err.Error(), "lock already acquired") + + // Release first lock + lock1.Release() + + // Now second lock should work + lock2, err = sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock2) + + // Release second lock + lock2.Release() +} + +func TestLockReleaseAfterTransition(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + + // Release lock after transition + lock.Release() + + // State should remain changed + assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) +} + +func TestDoubleLockRelease(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + // Release lock + lock.Release() + + // Acquire another lock + lock2, err := sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock2) + + // Second release should not actually do anything + lock.Release() + + // Should not be able to acquire lock after as the other lock is still held + nilLock, err := sm.AcquireLock() + assert.Error(t, err, "should not be able to acquire lock after as the other lock is still held") + assert.Nil(t, nilLock) + + // Release the second lock + lock2.Release() + + // Should be able to acquire lock after the other lock is released + lock3, err := sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock3) + + lock3.Release() +} + +func TestConcurrentLockBlocking(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + // Start first lock acquisition + lock1, err := sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock1) + + // Try to acquire second lock while first is held + lock2, err := sm.AcquireLock() + assert.Error(t, err, "second lock should fail while first is held") + assert.Nil(t, lock2) + assert.Contains(t, err.Error(), "lock already acquired") + + // Release first lock + lock1.Release() + + // Now second lock should work + lock2, err = sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock2) + + lock2.Release() +} + +func TestRaceConditionMultipleGoroutines(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + var wg sync.WaitGroup + successCount := 0 + var mu sync.Mutex + + // Start multiple goroutines trying to acquire lock simultaneously + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + lock, err := sm.AcquireLock() + if err == nil && lock != nil { + err = sm.Transition(lock, StateInstallationConfigured) + if err == nil { + mu.Lock() + successCount++ + mu.Unlock() + + // Release the lock + lock.Release() + } else { + lock.Release() + } + } + }() + } + + wg.Wait() + + // Only one transition should succeed + assert.Equal(t, 1, successCount, "only one transition should succeed") + assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) +} + +func TestRaceConditionReadWrite(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + var wg sync.WaitGroup + + // Start a goroutine that continuously reads the current state + readDone := make(chan bool) + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 1000; i++ { + _ = sm.CurrentState() + _ = sm.IsFinalState() + } + readDone <- true + }() + + // Start a goroutine that performs transitions + wg.Add(1) + go func() { + defer wg.Done() + + // Wait for reads to start + <-readDone + + lock, err := sm.AcquireLock() + if err == nil && lock != nil { + err = sm.Transition(lock, StateInstallationConfigured) + if err == nil { + lock.Release() + } else { + lock.Release() + } + } + + lock, err = sm.AcquireLock() + if err == nil && lock != nil { + err = sm.Transition(lock, StatePreflightsRunning) + if err == nil { + lock.Release() + } else { + lock.Release() + } + } + }() + + wg.Wait() + + // Final state should be consistent + finalState := sm.CurrentState() + assert.True(t, finalState == StateInstallationConfigured || finalState == StatePreflightsRunning, + "final state should be one of the expected states") +} + +func TestIsFinalState(t *testing.T) { + finalStates := []State{ + StateSucceeded, + StateFailed, + } + + for state := range validStateTransitions { + var isFinal bool + if slices.Contains(finalStates, state) { + isFinal = true + } + + sm := New(state, validStateTransitions) + + if isFinal { + assert.True(t, sm.IsFinalState(), "expected state %s to be final", state) + } else { + assert.False(t, sm.IsFinalState(), "expected state %s to not be final", state) + } + } +} + +func TestFinalStateTransitionBlocking(t *testing.T) { + finalStates := []State{StateSucceeded, StateFailed} + + for _, finalState := range finalStates { + t.Run(string(finalState), func(t *testing.T) { + sm := New(finalState, validStateTransitions) + + // Try to transition from final state + lock, err := sm.AcquireLock() + if err != nil { + t.Fatalf("failed to acquire lock: %v", err) + } + + err = sm.Transition(lock, StateNew) + assert.Error(t, err, "should not be able to transition from final state %s", finalState) + assert.Contains(t, err.Error(), "invalid transition") + + // Release the lock + lock.Release() + + // State should remain unchanged + assert.Equal(t, finalState, sm.CurrentState()) + }) + } +} + +func TestMultiStateTransitionWithLock(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + // Acquire lock and transition through multiple states + lock, err := sm.AcquireLock() + assert.NoError(t, err) + assert.NotNil(t, lock) + + // Transition 1: New -> StateInstallationConfigured + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) + + // Transition 2: StateInstallationConfigured -> StatePreflightsRunning + err = sm.Transition(lock, StatePreflightsRunning) + assert.NoError(t, err) + assert.Equal(t, StatePreflightsRunning, sm.CurrentState()) + + // Transition 3: StatePreflightsRunning -> StatePreflightsSucceeded + err = sm.Transition(lock, StatePreflightsSucceeded) + assert.NoError(t, err) + assert.Equal(t, StatePreflightsSucceeded, sm.CurrentState()) + + // Transition 4: StatePreflightsSucceeded -> StateInfrastructureInstalling + err = sm.Transition(lock, StateInfrastructureInstalling) + assert.NoError(t, err) + assert.Equal(t, StateInfrastructureInstalling, sm.CurrentState()) + + // Release the lock + lock.Release() + + // State should be the final state in the transition chain + assert.Equal(t, StateInfrastructureInstalling, sm.CurrentState(), "state should be the final transitioned state after lock release") +} + +func TestInvalidTransition(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + // Try invalid transition + err = sm.Transition(lock, StateSucceeded) + assert.Error(t, err, "should not be able to transition directly from New to Succeeded") + assert.Contains(t, err.Error(), "invalid transition") + + // State should remain unchanged + assert.Equal(t, StateNew, sm.CurrentState()) + + lock.Release() +} + +func TestTransitionWithoutLock(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + err := sm.Transition(nil, StateInstallationConfigured) + assert.Error(t, err, "transition should be invalid") + assert.Contains(t, err.Error(), "lock not acquired") +} + +func TestValidateTransitionWithoutLock(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + err := sm.ValidateTransition(nil, StateInstallationConfigured) + assert.Error(t, err, "transition should be invalid") + assert.Contains(t, err.Error(), "lock not acquired") +} + +func TestTransitionWithNilLock(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(nil, StateInstallationConfigured) + assert.Error(t, err, "transition should be invalid") + assert.Contains(t, err.Error(), "lock mismatch") + + lock.Release() +} + +func TestValidateTransitionWithNilLock(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.ValidateTransition(nil, StateInstallationConfigured) + assert.Error(t, err, "transition should be invalid") + assert.Contains(t, err.Error(), "lock mismatch") + + lock.Release() +} + +func TestTransitionWithWrongLock(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock, err := sm.AcquireLock() + assert.NoError(t, err) + lock.Release() + + lock2, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.Error(t, err, "transition should be invalid") + assert.Contains(t, err.Error(), "lock mismatch") + + lock2.Release() +} + +func TestValidateTransitionWithWrongLock(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + lock, err := sm.AcquireLock() + assert.NoError(t, err) + lock.Release() + + lock2, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.ValidateTransition(lock, StateInstallationConfigured) + assert.Error(t, err, "transition should be invalid") + assert.Contains(t, err.Error(), "lock mismatch") + + lock2.Release() +} + +func TestValidateTransitionWithNonExistentState(t *testing.T) { + sm := New(StateNew, validStateTransitions) + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + // Test with a state that doesn't exist in the transition map + nonExistentState := State("NonExistentState") + err = sm.ValidateTransition(lock, nonExistentState) + assert.Error(t, err, "transition to non-existent state should be invalid") + assert.Contains(t, err.Error(), "invalid transition") + assert.Contains(t, err.Error(), string(StateNew)) + assert.Contains(t, err.Error(), string(nonExistentState)) + + lock.Release() +} + +func TestValidateTransitionStateConsistency(t *testing.T) { + sm := New(StateNew, validStateTransitions) + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + // Validate a transition + err = sm.ValidateTransition(lock, StateInstallationConfigured) + assert.NoError(t, err, "transition should be valid") + + // State should remain unchanged after validation + assert.Equal(t, StateNew, sm.CurrentState(), "state should not change after validation") + + // Actually perform the transition + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err, "transition should succeed") + assert.Equal(t, StateInstallationConfigured, sm.CurrentState(), "state should change after transition") + + lock.Release() +} + +func TestValidateTransitionEdgeCases(t *testing.T) { + // Test with empty transition map + emptyTransitions := make(map[State][]State) + sm := New(StateNew, emptyTransitions) + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + // Any transition should be invalid with empty transition map + err = sm.ValidateTransition(lock, StateInstallationConfigured) + assert.Error(t, err, "transition should be invalid with empty transition map") + assert.Contains(t, err.Error(), "invalid transition") + + lock.Release() + + // Test with state that has no valid transitions (final state) + finalStateTransitions := map[State][]State{ + StateSucceeded: {}, + StateFailed: {}, + } + sm = New(StateSucceeded, finalStateTransitions) + lock, err = sm.AcquireLock() + assert.NoError(t, err) + + // Any transition from final state should be invalid + err = sm.ValidateTransition(lock, StateNew) + assert.Error(t, err, "transition from final state should be invalid") + assert.Contains(t, err.Error(), "invalid transition") + + lock.Release() +} diff --git a/api/internal/store/preflight/store.go b/api/internal/store/preflight/store.go index 3741b629c..16b246773 100644 --- a/api/internal/store/preflight/store.go +++ b/api/internal/store/preflight/store.go @@ -16,7 +16,6 @@ type Store interface { SetOutput(output *types.HostPreflightsOutput) error GetStatus() (types.Status, error) SetStatus(status types.Status) error - IsRunning() bool } type memoryStore struct { @@ -105,10 +104,3 @@ func (s *memoryStore) SetStatus(status types.Status) error { s.hostPreflight.Status = status return nil } - -func (s *memoryStore) IsRunning() bool { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.hostPreflight.Status.State == types.StateRunning -} diff --git a/api/internal/store/preflight/store_mock.go b/api/internal/store/preflight/store_mock.go index 59b0ea951..2790e8900 100644 --- a/api/internal/store/preflight/store_mock.go +++ b/api/internal/store/preflight/store_mock.go @@ -56,9 +56,3 @@ func (m *MockStore) SetStatus(status types.Status) error { args := m.Called(status) return args.Error(0) } - -// IsRunning mocks the IsRunning method -func (m *MockStore) IsRunning() bool { - args := m.Called() - return args.Bool(0) -} diff --git a/api/internal/store/preflight/store_test.go b/api/internal/store/preflight/store_test.go index 48f83b422..d8d6f4c38 100644 --- a/api/internal/store/preflight/store_test.go +++ b/api/internal/store/preflight/store_test.go @@ -160,60 +160,6 @@ func TestMemoryStore_SetStatus(t *testing.T) { assert.Equal(t, expectedStatus, actualStatus) } -func TestMemoryStore_IsRunning(t *testing.T) { - tests := []struct { - name string - status types.Status - expectedBool bool - }{ - { - name: "is running when state is running", - status: types.Status{ - State: types.StateRunning, - Description: "Running host preflights", - }, - expectedBool: true, - }, - { - name: "is not running when state is succeeded", - status: types.Status{ - State: types.StateSucceeded, - Description: "Host preflights passed", - }, - expectedBool: false, - }, - { - name: "is not running when state is failed", - status: types.Status{ - State: types.StateFailed, - Description: "Host preflights failed", - }, - expectedBool: false, - }, - { - name: "is not running when state is pending", - status: types.Status{ - State: types.StatePending, - Description: "Pending host preflights", - }, - expectedBool: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hostPreflight := types.HostPreflights{ - Status: tt.status, - } - store := NewMemoryStore(WithHostPreflight(hostPreflight)) - - result := store.IsRunning() - - assert.Equal(t, tt.expectedBool, result) - }) - } -} - // Useful to test concurrent access with -race flag func TestMemoryStore_ConcurrentAccess(t *testing.T) { hostPreflight := types.HostPreflights{} @@ -271,7 +217,7 @@ func TestMemoryStore_ConcurrentAccess(t *testing.T) { } // Concurrent status operations - wg.Add(numGoroutines * 3) + wg.Add(numGoroutines * 2) for i := 0; i < numGoroutines; i++ { // Concurrent writes go func(id int) { @@ -295,14 +241,6 @@ func TestMemoryStore_ConcurrentAccess(t *testing.T) { assert.NoError(t, err) } }(i) - - // Concurrent IsRunning calls - go func(id int) { - defer wg.Done() - for j := 0; j < numOperations; j++ { - store.IsRunning() - } - }(i) } wg.Wait() diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go index 21bf02119..96f7629e6 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -35,7 +35,7 @@ type apiConfig struct { Password string TLSConfig apitypes.TLSConfig ManagerPort int - LicenseFile string + License []byte AirgapBundle string ConfigValues string ReleaseData *release.ReleaseData @@ -87,7 +87,7 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, api.WithMetricsReporter(config.MetricsReporter), api.WithReleaseData(config.ReleaseData), api.WithTLSConfig(config.TLSConfig), - api.WithLicenseFile(config.LicenseFile), + api.WithLicense(config.License), api.WithAirgapBundle(config.AirgapBundle), api.WithConfigValues(config.ConfigValues), api.WithEndUserConfig(config.EndUserConfig), diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 853ddfbc2..06f0e9773 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -72,6 +72,7 @@ type InstallCmdFlags struct { // TODO: move to substruct license *kotsv1beta1.License + licenseBytes []byte tlsCert tls.Certificate tlsCertBytes []byte tlsKeyBytes []byte @@ -239,6 +240,12 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. // license file can be empty for restore if flags.licenseFile != "" { + b, err := os.ReadFile(flags.licenseFile) + if err != nil { + return fmt.Errorf("unable 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 { @@ -417,7 +424,7 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc Hostname: flags.hostname, }, ManagerPort: flags.managerPort, - LicenseFile: flags.licenseFile, + License: flags.licenseBytes, AirgapBundle: flags.airgapBundle, ConfigValues: flags.configValues, ReleaseData: release.GetReleaseData(), @@ -558,7 +565,7 @@ func getAddonInstallOpts(flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, opts := kotscli.InstallOptions{ RuntimeConfig: rc, AppSlug: flags.license.Spec.AppSlug, - LicenseFile: flags.licenseFile, + License: flags.licenseBytes, Namespace: runtimeconfig.KotsadmNamespace, AirgapBundle: flags.airgapBundle, ConfigValuesFile: flags.configValues, @@ -765,8 +772,13 @@ func initializeInstall(ctx context.Context, flags InstallCmdFlags, rc runtimecon spinner := spinner.Start() spinner.Infof("Initializing") + licenseBytes, err := os.ReadFile(flags.licenseFile) + if err != nil { + return fmt.Errorf("unable to read license file: %w", err) + } + if err := hostutils.ConfigureHost(ctx, rc, hostutils.InitForInstallOptions{ - LicenseFile: flags.licenseFile, + License: licenseBytes, AirgapBundle: flags.airgapBundle, }); err != nil { spinner.ErrorClosef("Initialization failed") diff --git a/cmd/installer/cli/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index 8409c1eb9..fee57aa4b 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" @@ -64,9 +65,14 @@ func runInstallRunPreflights(ctx context.Context, name string, flags InstallCmdF return err } + licenseBytes, err := os.ReadFile(flags.licenseFile) + if err != nil { + return fmt.Errorf("unable to read license file: %w", err) + } + logrus.Debugf("configuring host") if err := hostutils.ConfigureHost(ctx, rc, hostutils.InitForInstallOptions{ - LicenseFile: flags.licenseFile, + License: licenseBytes, AirgapBundle: flags.airgapBundle, }); err != nil { return fmt.Errorf("configure host: %w", err) diff --git a/cmd/installer/kotscli/kotscli.go b/cmd/installer/kotscli/kotscli.go index 5d4dc6206..a77557c5c 100644 --- a/cmd/installer/kotscli/kotscli.go +++ b/cmd/installer/kotscli/kotscli.go @@ -25,7 +25,7 @@ var ( type InstallOptions struct { RuntimeConfig runtimeconfig.RuntimeConfig AppSlug string - LicenseFile string + License []byte Namespace string AirgapBundle string ConfigValuesFile string @@ -53,12 +53,22 @@ func Install(opts InstallOptions) error { upstreamURI = fmt.Sprintf("%s/%s", upstreamURI, channelSlug) } + licenseFile, err := os.CreateTemp("", "license") + if err != nil { + return fmt.Errorf("unable to create temp file: %w", err) + } + defer os.Remove(licenseFile.Name()) + + if _, err := licenseFile.Write(opts.License); err != nil { + return fmt.Errorf("unable to write license to temp file: %w", err) + } + maskfn := MaskKotsOutputForOnline() installArgs := []string{ "install", upstreamURI, "--license-file", - opts.LicenseFile, + licenseFile.Name(), "--namespace", opts.Namespace, "--app-version-label", diff --git a/pkg-new/hostutils/initialize.go b/pkg-new/hostutils/initialize.go index 27ba75281..024735b08 100644 --- a/pkg-new/hostutils/initialize.go +++ b/pkg-new/hostutils/initialize.go @@ -6,12 +6,11 @@ import ( "os" "path/filepath" - "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) type InitForInstallOptions struct { - LicenseFile string + License []byte AirgapBundle string } @@ -33,11 +32,10 @@ func (h *HostUtils) ConfigureHost(ctx context.Context, rc runtimeconfig.RuntimeC return fmt.Errorf("materialize files: %w", err) } - if opts.LicenseFile != "" { - h.logger.Debugf("copy license file to %s", rc.EmbeddedClusterHomeDirectory()) - if err := helpers.CopyFile(opts.LicenseFile, filepath.Join(rc.EmbeddedClusterHomeDirectory(), "license.yaml"), 0400); err != nil { - // We have decided not to report this error - h.logger.Warnf("unable to copy license file to %s: %v", rc.EmbeddedClusterHomeDirectory(), err) + if opts.License != nil { + h.logger.Debugf("write license file to %s", rc.EmbeddedClusterHomeDirectory()) + if err := os.WriteFile(filepath.Join(rc.EmbeddedClusterHomeDirectory(), "license.yaml"), opts.License, 0400); err != nil { + h.logger.Warnf("unable to write license file to %s: %v", rc.EmbeddedClusterHomeDirectory(), err) } } From a2872e83da87976a15ba813bb2e2b7cd14993ea7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 12:05:25 +0000 Subject: [PATCH 16/48] build(deps): bump the security group across 2 directories with 8 updates (#2348) Bumps the security group with 2 updates in the /e2e/playwright directory: [@playwright/test](https://github.com/microsoft/playwright) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Bumps the security group with 7 updates in the /web directory: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.0.1` | `24.0.3` | | [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.80.7` | `5.80.10` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.515.0` | `0.519.0` | | [postcss](https://github.com/postcss/postcss) | `8.5.5` | `8.5.6` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.34.0` | `8.34.1` | | [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy) | `3.0.0` | `3.0.2` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `3.2.3` | `3.2.4` | Updates `@playwright/test` from 1.53.0 to 1.53.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.53.0...v1.53.1) Updates `@types/node` from 24.0.1 to 24.0.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/node` from 24.0.1 to 24.0.3 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@tanstack/react-query` from 5.80.7 to 5.80.10 - [Release notes](https://github.com/TanStack/query/releases) - [Commits](https://github.com/TanStack/query/commits/v5.80.10/packages/react-query) Updates `lucide-react` from 0.515.0 to 0.519.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.519.0/packages/lucide-react) Updates `postcss` from 8.5.5 to 8.5.6 - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.5...8.5.6) Updates `typescript-eslint` from 8.34.0 to 8.34.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.34.1/packages/typescript-eslint) Updates `vite-plugin-static-copy` from 3.0.0 to 3.0.2 - [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases) - [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md) - [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@3.0.0...vite-plugin-static-copy@3.0.2) Updates `vitest` from 3.2.3 to 3.2.4 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.4/packages/vitest) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-version: 1.53.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: security - dependency-name: "@types/node" dependency-version: 24.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: security - dependency-name: "@types/node" dependency-version: 24.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: security - dependency-name: "@tanstack/react-query" dependency-version: 5.80.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: lucide-react dependency-version: 0.519.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: postcss dependency-version: 8.5.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: security - dependency-name: typescript-eslint dependency-version: 8.34.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: security - dependency-name: vite-plugin-static-copy dependency-version: 3.0.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: security - dependency-name: vitest dependency-version: 3.2.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: security ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- e2e/playwright/package-lock.json | 28 ++-- web/package-lock.json | 264 +++++++++++++++---------------- web/package.json | 14 +- 3 files changed, 153 insertions(+), 153 deletions(-) diff --git a/e2e/playwright/package-lock.json b/e2e/playwright/package-lock.json index 4cc83d7e5..9c71019ee 100644 --- a/e2e/playwright/package-lock.json +++ b/e2e/playwright/package-lock.json @@ -15,13 +15,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz", - "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz", + "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.53.0" + "playwright": "1.53.1" }, "bin": { "playwright": "cli.js" @@ -31,9 +31,9 @@ } }, "node_modules/@types/node": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", - "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "dependencies": { @@ -56,13 +56,13 @@ } }, "node_modules/playwright": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz", - "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz", + "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.53.0" + "playwright-core": "1.53.1" }, "bin": { "playwright": "cli.js" @@ -75,9 +75,9 @@ } }, "node_modules/playwright-core": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz", - "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz", + "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/web/package-lock.json b/web/package-lock.json index e2cebe9a0..97e90920c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.80.7", - "lucide-react": "^0.515.0", + "@tanstack/react-query": "^5.80.10", + "lucide-react": "^0.519.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.22.3" @@ -21,7 +21,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", - "@types/node": "^24.0.1", + "@types/node": "^24.0.3", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.5.2", @@ -32,13 +32,13 @@ "globals": "^16.2.0", "jsdom": "^26.1.0", "msw": "2.10.2", - "postcss": "^8.5.5", + "postcss": "^8.5.6", "tailwindcss": "^3.4.1", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0", + "typescript-eslint": "^8.34.1", "vite": "^6.3.5", - "vite-plugin-static-copy": "^3.0.0", - "vitest": "^3.2.3" + "vite-plugin-static-copy": "^3.0.2", + "vitest": "^3.2.4" } }, "node_modules/@adobe/css-tools": { @@ -1945,9 +1945,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.7.tgz", - "integrity": "sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==", + "version": "5.80.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.10.tgz", + "integrity": "sha512-mUNQOtzxkjL6jLbyChZoSBP6A5gQDVRUiPvW+/zw/9ftOAz+H754zCj3D8PwnzPKyHzGkQ9JbH48ukhym9LK1Q==", "license": "MIT", "funding": { "type": "github", @@ -1955,12 +1955,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.7.tgz", - "integrity": "sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==", + "version": "5.80.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.10.tgz", + "integrity": "sha512-6zM098J8sLy9oU60XAdzUlAH4wVzoMVsWUWiiE/Iz4fd67PplxeyL4sw/MPcVJJVhbwGGXCsHn9GrQt2mlAzig==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.80.7" + "@tanstack/query-core": "5.80.10" }, "funding": { "type": "github", @@ -2467,9 +2467,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "24.0.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", - "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "dependencies": { @@ -2550,17 +2550,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", - "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", + "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/type-utils": "8.34.0", - "@typescript-eslint/utils": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/type-utils": "8.34.1", + "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2574,7 +2574,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.0", + "@typescript-eslint/parser": "^8.34.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2590,16 +2590,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", - "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", + "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4" }, "engines": { @@ -2615,14 +2615,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", - "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", + "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.0", - "@typescript-eslint/types": "^8.34.0", + "@typescript-eslint/tsconfig-utils": "^8.34.1", + "@typescript-eslint/types": "^8.34.1", "debug": "^4.3.4" }, "engines": { @@ -2637,14 +2637,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", - "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0" + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2655,9 +2655,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", - "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", + "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", "dev": true, "license": "MIT", "engines": { @@ -2672,14 +2672,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", - "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", + "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.0", - "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/utils": "8.34.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2696,9 +2696,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", - "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", + "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", "dev": true, "license": "MIT", "engines": { @@ -2710,16 +2710,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", - "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.34.0", - "@typescript-eslint/tsconfig-utils": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0", + "@typescript-eslint/project-service": "8.34.1", + "@typescript-eslint/tsconfig-utils": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2778,16 +2778,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", - "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", + "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.34.0", - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/typescript-estree": "8.34.0" + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2802,14 +2802,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", - "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.34.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2841,15 +2841,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", - "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.3", - "@vitest/utils": "3.2.3", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -2858,13 +2858,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", - "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.3", + "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2885,9 +2885,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", - "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -2898,13 +2898,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", - "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.3", + "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -2913,13 +2913,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", - "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.3", + "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2928,9 +2928,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", - "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "dev": true, "license": "MIT", "dependencies": { @@ -2941,14 +2941,14 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", - "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.3", - "loupe": "^3.1.3", + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, "funding": { @@ -5841,9 +5841,9 @@ } }, "node_modules/lucide-react": { - "version": "0.515.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.515.0.tgz", - "integrity": "sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw==", + "version": "0.519.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.519.0.tgz", + "integrity": "sha512-cLJyjRKBJFzaZ/+1oIeQaH7XUdxKOYU3uANcGSrKdIZWElmNbRAm8RXKiTJS7AWLCBOS8b7A497Al/kCHozd+A==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6350,9 +6350,9 @@ } }, "node_modules/postcss": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", - "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -7573,15 +7573,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.34.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz", - "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.1.tgz", + "integrity": "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.34.0", - "@typescript-eslint/parser": "8.34.0", - "@typescript-eslint/utils": "8.34.0" + "@typescript-eslint/eslint-plugin": "8.34.1", + "@typescript-eslint/parser": "8.34.1", + "@typescript-eslint/utils": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7745,9 +7745,9 @@ } }, "node_modules/vite-node": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", - "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { @@ -7768,9 +7768,9 @@ } }, "node_modules/vite-plugin-static-copy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.0.0.tgz", - "integrity": "sha512-Uki9pPUQ4ZnoMEdIFabvoh9h6Bh9Q1m3iF7BrZvoiF30reREpJh2gZb4jOnW1/uYFzyRiLCmFSkM+8hwiq1vWQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.0.2.tgz", + "integrity": "sha512-/seLvhUg44s1oU9RhjTZZy/0NPbfNctozdysKcvPovxxXZdI5l19mGq6Ri3IaTf1Dy/qChS4BSR7ayxeu8o9aQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7778,7 +7778,7 @@ "fs-extra": "^11.3.0", "p-map": "^7.0.3", "picocolors": "^1.1.1", - "tinyglobby": "^0.2.13" + "tinyglobby": "^0.2.14" }, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -7816,20 +7816,20 @@ } }, "node_modules/vitest": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", - "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.3", - "@vitest/mocker": "3.2.3", - "@vitest/pretty-format": "^3.2.3", - "@vitest/runner": "3.2.3", - "@vitest/snapshot": "3.2.3", - "@vitest/spy": "3.2.3", - "@vitest/utils": "3.2.3", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -7840,10 +7840,10 @@ "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", - "tinypool": "^1.1.0", + "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.3", + "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -7859,8 +7859,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.3", - "@vitest/ui": "3.2.3", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/web/package.json b/web/package.json index e8dc22223..f76ed4a6a 100644 --- a/web/package.json +++ b/web/package.json @@ -13,8 +13,8 @@ }, "dependencies": { "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.80.7", - "lucide-react": "^0.515.0", + "@tanstack/react-query": "^5.80.10", + "lucide-react": "^0.519.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.22.3" @@ -25,7 +25,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", - "@types/node": "^24.0.1", + "@types/node": "^24.0.3", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.5.2", @@ -36,12 +36,12 @@ "globals": "^16.2.0", "jsdom": "^26.1.0", "msw": "2.10.2", - "postcss": "^8.5.5", + "postcss": "^8.5.6", "tailwindcss": "^3.4.1", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0", + "typescript-eslint": "^8.34.1", "vite": "^6.3.5", - "vite-plugin-static-copy": "^3.0.0", - "vitest": "^3.2.3" + "vite-plugin-static-copy": "^3.0.2", + "vitest": "^3.2.4" } } From f78d15e84e09965099fa5dc31fe5b16a30822eb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 12:06:29 +0000 Subject: [PATCH 17/48] build(deps-dev): bump @testing-library/react from 14.3.1 to 16.3.0 in /web (#2350) build(deps-dev): bump @testing-library/react in /web --- updated-dependencies: - dependency-name: "@testing-library/react" dependency-version: 16.3.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 1161 +++-------------------------------------- web/package.json | 2 +- 2 files changed, 75 insertions(+), 1088 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 97e90920c..a21d794a1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -19,7 +19,7 @@ "@eslint/js": "^9.29.0", "@faker-js/faker": "^9.8.0", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.4.3", "@types/node": "^24.0.3", "@types/react": "^18.3.5", @@ -2170,128 +2170,31 @@ } }, "node_modules/@testing-library/react": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", - "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/react/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/react/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" }, - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@testing-library/user-event": { @@ -2313,7 +2216,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3084,23 +2988,6 @@ "dequal": "^2.0.3" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3149,22 +3036,6 @@ "postcss": "^8.1.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3246,56 +3117,6 @@ "node": ">=8" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3669,81 +3490,12 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3781,21 +3533,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3826,47 +3563,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -3874,19 +3570,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -4343,22 +4026,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -4423,16 +4090,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4453,45 +4110,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4557,19 +4175,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4594,61 +4199,6 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4766,298 +4316,23 @@ "node": ">=8" } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5066,40 +4341,52 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, @@ -5855,6 +5142,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -5869,16 +5157,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6081,67 +5359,6 @@ "node": ">= 6" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6339,16 +5556,6 @@ "node": ">= 6" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -6512,6 +5719,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6527,6 +5735,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6537,6 +5746,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -6620,7 +5830,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.17.0", @@ -6697,27 +5908,6 @@ "node": ">=8" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6839,24 +6029,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6895,40 +6067,6 @@ "semver": "bin/semver.js" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6948,82 +6086,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -7107,20 +6169,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -7975,67 +7023,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/web/package.json b/web/package.json index f76ed4a6a..46706ecb4 100644 --- a/web/package.json +++ b/web/package.json @@ -23,7 +23,7 @@ "@eslint/js": "^9.29.0", "@faker-js/faker": "^9.8.0", "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.4.3", "@types/node": "^24.0.3", "@types/react": "^18.3.5", From c5987d7b7bc431f1c26dd3a621fb176ccb0635d3 Mon Sep 17 00:00:00 2001 From: Kyle Squizzato Date: Sat, 21 Jun 2025 06:42:56 -0700 Subject: [PATCH 18/48] fix(e2e): Set sshArgs to mostly defaults (#2347) --- e2e/cluster/cmx/cluster.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/e2e/cluster/cmx/cluster.go b/e2e/cluster/cmx/cluster.go index edc2bf183..cfd25b340 100644 --- a/e2e/cluster/cmx/cluster.go +++ b/e2e/cluster/cmx/cluster.go @@ -504,9 +504,6 @@ func copyFileFromNode(node Node, src, dst string) error { func sshArgs() []string { return []string{ "-o", "StrictHostKeyChecking=no", - "-o", "ServerAliveInterval=30", - "-o", "ServerAliveCountMax=10", - "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", } } From 120ce00577be3d3e0ef5acd40f5be41d9f913730 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 12:07:15 +0000 Subject: [PATCH 19/48] build(deps): bump golang.org/x/crypto from 0.38.0 to 0.39.0 (#2356) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.39.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index ac40c8e57..e8f1b116d 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/urfave/cli/v2 v2.27.7 github.com/vmware-tanzu/velero v1.16.1 go.uber.org/multierr v1.11.0 - golang.org/x/crypto v0.38.0 + golang.org/x/crypto v0.39.0 golang.org/x/term v0.32.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -296,8 +296,8 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/tools v0.33.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.218.0 // indirect @@ -364,7 +364,7 @@ require ( golang.org/x/net v0.40.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 + golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index c4a311753..4e3be7bdc 100644 --- a/go.sum +++ b/go.sum @@ -1711,8 +1711,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1775,8 +1775,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1897,8 +1897,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2043,8 +2043,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 1b2f8e13e0ea550cd7df7d0899deaafab4ae8050 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 12:07:49 +0000 Subject: [PATCH 20/48] build(deps): bump helm.sh/helm/v3 from 3.17.3 to 3.18.3 (#2357) --- updated-dependencies: - dependency-name: helm.sh/helm/v3 dependency-version: 3.18.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 57 +++++++++++++------------ go.sum | 131 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 96 insertions(+), 92 deletions(-) diff --git a/go.mod b/go.mod index e8f1b116d..9d841605b 100644 --- a/go.mod +++ b/go.mod @@ -49,14 +49,14 @@ require ( gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools v2.2.0+incompatible - helm.sh/helm/v3 v3.17.3 - k8s.io/api v0.32.3 - k8s.io/apimachinery v0.32.3 - k8s.io/cli-runtime v0.32.3 - k8s.io/client-go v0.32.3 - k8s.io/kubectl v0.32.3 + helm.sh/helm/v3 v3.18.3 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/cli-runtime v0.33.1 + k8s.io/client-go v0.33.1 + k8s.io/kubectl v0.33.1 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 - oras.land/oras-go/v2 v2.5.0 + oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/yaml v1.4.0 ) @@ -67,7 +67,7 @@ replace ( ) require ( - cel.dev/expr v0.18.0 // indirect + cel.dev/expr v0.19.1 // indirect cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.14.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect @@ -78,7 +78,7 @@ require ( 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/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // 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.25.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect @@ -132,7 +132,7 @@ require ( github.com/containers/storage v1.57.2 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect - github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/distribution/distribution/v3 v3.0.0 // indirect github.com/docker/cli v27.5.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -197,7 +197,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 // indirect github.com/k0sproject/dig v0.4.0 // indirect github.com/k0sproject/version v0.6.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -221,7 +221,7 @@ require ( github.com/moby/sys/capability v0.4.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/moby/sys/user v0.3.0 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/oklog/ulid v1.3.1 // indirect @@ -236,12 +236,12 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/proglottis/gpgme v0.1.4 // indirect - github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/rubenv/sql-migrate v1.7.1 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect github.com/segmentio/ksuid v1.0.4 // indirect @@ -280,14 +280,14 @@ require ( github.com/zitadel/logging v0.6.1 // indirect github.com/zitadel/oidc/v3 v3.31.0 // indirect github.com/zitadel/schema v1.3.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.18 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.18 // indirect - go.etcd.io/etcd/client/v3 v3.5.18 // indirect + go.etcd.io/etcd/api/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/v3 v3.5.21 // indirect go.mongodb.org/mongo-driver v1.17.3 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect @@ -306,14 +306,15 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.69.4 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - k8s.io/apiserver v0.32.3 // indirect - k8s.io/component-base v0.32.3 // indirect + k8s.io/apiserver v0.33.1 // indirect + k8s.io/component-base v0.33.1 // indirect k8s.io/kubelet v0.32.3 // indirect - k8s.io/metrics v0.32.3 // indirect + k8s.io/metrics v0.33.1 // indirect oras.land/oras-go v1.2.6 // indirect periph.io/x/host/v3 v3.8.5 // indirect - sigs.k8s.io/kustomize/api v0.18.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect + sigs.k8s.io/kustomize/api v0.19.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect ) require ( @@ -330,10 +331,10 @@ require ( github.com/go-openapi/swag v0.23.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic-models v0.6.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -368,9 +369,9 @@ require ( golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/apiextensions-apiserver v0.32.3 + k8s.io/apiextensions-apiserver v0.33.1 k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect ) diff --git a/go.sum b/go.sum index 4e3be7bdc..62f7d392d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -637,8 +637,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7Um github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0/go.mod h1:Wjo+24QJVhhl/L7jy6w9yzFF2yDOf3cKECAa8ecf9vE= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -843,8 +843,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -1053,8 +1053,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -1147,8 +1147,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= @@ -1161,8 +1161,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -1243,8 +1243,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= @@ -1345,8 +1345,8 @@ github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1439,8 +1439,8 @@ github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glE github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -1479,8 +1479,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= @@ -1617,12 +1617,12 @@ github.com/zitadel/oidc/v3 v3.31.0 h1:XQcTVHTYpSkNxjGccEb6pRfrGJdUhkTgXOIzSqRXdo github.com/zitadel/oidc/v3 v3.31.0/go.mod h1:DyE/XClysRK/ozFaZSqlYamKVnTh4l6Ln25ihSNI03w= github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= -go.etcd.io/etcd/api/v3 v3.5.18 h1:Q4oDAKnmwqTo5lafvB+afbgCDF7E35E4EYV2g+FNGhs= -go.etcd.io/etcd/api/v3 v3.5.18/go.mod h1:uY03Ob2H50077J7Qq0DeehjM/A9S8PhVfbQ1mSaMopU= -go.etcd.io/etcd/client/pkg/v3 v3.5.18 h1:mZPOYw4h8rTk7TeJ5+3udUkfVGBqc+GCjOJYd68QgNM= -go.etcd.io/etcd/client/pkg/v3 v3.5.18/go.mod h1:BxVf2o5wXG9ZJV+/Cu7QNUiJYk4A29sAhoI5tIRsCu4= -go.etcd.io/etcd/client/v3 v3.5.18 h1:nvvYmNHGumkDjZhTHgVU36A9pykGa2K4lAJ0yY7hcXA= -go.etcd.io/etcd/client/v3 v3.5.18/go.mod h1:kmemwOsPU9broExyhYsBxX4spCTDX3yLgPMWtpBXG6E= +go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= +go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= +go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= +go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= +go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= +go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1642,8 +1642,8 @@ go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//sn go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= 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.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= @@ -1656,10 +1656,10 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7Z go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= @@ -1685,8 +1685,8 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= 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= @@ -2439,8 +2439,8 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= -helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= +helm.sh/helm/v3 v3.18.3 h1:+cvyGKgs7Jt7BN3Klmb4SsG4IkVpA7GAZVGvMz6VO4I= +helm.sh/helm/v3 v3.18.3/go.mod h1:wUc4n3txYBocM7S9RjTeZBN9T/b5MjffpcSsWEjSIpw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2449,30 +2449,30 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= -k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= -k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= -k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= -k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= -k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= +k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= +k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= +k8s.io/cli-runtime v0.33.1 h1:TvpjEtF71ViFmPeYMj1baZMJR4iWUEplklsUQ7D3quA= +k8s.io/cli-runtime v0.33.1/go.mod h1:9dz5Q4Uh8io4OWCLiEf/217DXwqNgiTS/IOuza99VZE= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= +k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= -k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= -k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.33.1 h1:OJUXa6FV5bap6iRy345ezEjU9dTLxqv1zFTVqmeHb6A= +k8s.io/kubectl v0.33.1/go.mod h1:Z07pGqXoP4NgITlPRrnmiM3qnoo1QrK1zjw85Aiz8J0= k8s.io/kubelet v0.32.3 h1:B9HzW4yB67flx8tN2FYuDwZvxnmK3v5EjxxFvOYjmc8= k8s.io/kubelet v0.32.3/go.mod h1:yyAQSCKC+tjSlaFw4HQG7Jein+vo+GeKBGdXdQGvL1U= -k8s.io/metrics v0.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4= -k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU= +k8s.io/metrics v0.33.1 h1:Ypd5ITCf+fM+LDNFk7hESXTc3vh02CQYGiwRoVRaGsM= +k8s.io/metrics v0.33.1/go.mod h1:wK8cFTK5ykBdhL0Wy4RZwLH28XM7j/Klc+NQrMRWVxg= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= @@ -2511,8 +2511,8 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8= -oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= -oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII= periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= @@ -2523,11 +2523,14 @@ sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+ sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= -sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= -sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= -sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From 8e034e4264f0e2dd4653759c1c59bf3dcd0a14c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 12:08:25 +0000 Subject: [PATCH 21/48] build(deps): bump golang.org/x/text from 0.25.0 to 0.26.0 (#2358) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.26.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From ccbcad49de4a75b67d679ce6580da105497b8cd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 06:05:34 +0000 Subject: [PATCH 22/48] build(deps): bump the security group across 2 directories with 12 updates (#2360) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2 dependency-version: 1.36.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.29.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: github.com/aws/aws-sdk-go-v2/credentials dependency-version: 1.17.70 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: github.com/aws/aws-sdk-go-v2/feature/s3/manager dependency-version: 1.17.81 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: github.com/aws/aws-sdk-go-v2/service/s3 dependency-version: 1.81.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: security - dependency-name: k8s.io/api dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/apimachinery dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/cli-runtime dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/client-go dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/kubectl dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/apiextensions-apiserver dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/api dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: k8s.io/apimachinery dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: security - dependency-name: github.com/go-logr/logr dependency-version: 1.4.3 dependency-type: indirect update-type: version-update:semver-patch dependency-group: security ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 54 +++++++++++++------------- go.sum | 108 +++++++++++++++++++++++++-------------------------- kinds/go.mod | 2 +- kinds/go.sum | 4 +- 4 files changed, 84 insertions(+), 84 deletions(-) diff --git a/go.mod b/go.mod index 9d841605b..c3947a332 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/apparentlymart/go-cidr v1.1.0 github.com/aws/aws-sdk-go v1.55.7 - github.com/aws/aws-sdk-go-v2 v1.36.4 - github.com/aws/aws-sdk-go-v2/config v1.29.16 - github.com/aws/aws-sdk-go-v2/credentials v1.17.69 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.79 - github.com/aws/aws-sdk-go-v2/service/s3 v1.80.2 + github.com/aws/aws-sdk-go-v2 v1.36.5 + github.com/aws/aws-sdk-go-v2/config v1.29.17 + github.com/aws/aws-sdk-go-v2/credentials v1.17.70 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.81 + github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0 github.com/bombsimon/logrusr/v4 v4.1.0 github.com/canonical/lxd v0.0.0-20241030172432-dee0d04b56ee github.com/containers/image/v5 v5.34.3 @@ -50,11 +50,11 @@ require ( gopkg.in/yaml.v3 v3.0.1 gotest.tools v2.2.0+incompatible helm.sh/helm/v3 v3.18.3 - k8s.io/api v0.33.1 - k8s.io/apimachinery v0.33.1 - k8s.io/cli-runtime v0.33.1 - k8s.io/client-go v0.33.1 - k8s.io/kubectl v0.33.1 + k8s.io/api v0.33.2 + k8s.io/apimachinery v0.33.2 + k8s.io/cli-runtime v0.33.2 + k8s.io/client-go v0.33.2 + k8s.io/kubectl v0.33.2 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.20.4 @@ -93,20 +93,20 @@ require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.35 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 // indirect - github.com/aws/smithy-go v1.22.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect + github.com/aws/smithy-go v1.22.4 // indirect 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 @@ -306,10 +306,10 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.69.4 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - k8s.io/apiserver v0.33.1 // indirect - k8s.io/component-base v0.33.1 // indirect + k8s.io/apiserver v0.33.2 // indirect + k8s.io/component-base v0.33.2 // indirect k8s.io/kubelet v0.32.3 // indirect - k8s.io/metrics v0.33.1 // indirect + k8s.io/metrics v0.33.2 // indirect oras.land/oras-go v1.2.6 // indirect periph.io/x/host/v3 v3.8.5 // indirect sigs.k8s.io/kustomize/api v0.19.0 // indirect @@ -369,7 +369,7 @@ require ( golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/apiextensions-apiserver v0.33.1 + k8s.io/apiextensions-apiserver v0.33.2 k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect diff --git a/go.sum b/go.sum index 62f7d392d..19061fd91 100644 --- a/go.sum +++ b/go.sum @@ -703,44 +703,44 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= -github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= -github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= -github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= -github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.79 h1:mGo6WGWry+s5GEf2GLfw3zkHad109FQmtvBV3VYQ8mA= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.79/go.mod h1:siwnpWxHYFSSge7Euw9lGMgQBgvRyym352mCuGNHsMQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= +github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= +github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= +github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= +github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.81 h1:E5ff1vZlAudg24j5lF6F6/gBpln2LjWxGdQDBSLfVe4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.81/go.mod h1:hHBLCuhHI4Aokvs5vdVoCDBzmFy86yxs5J7LEPQwQEM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.35 h1:th/m+Q18CkajTw1iqx2cKkLCij/uz8NMwJFPK91p2ug= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.35/go.mod h1:dkJuf0a1Bc8HAA0Zm2MoTGm/WDC18Td9vSbrQ1+VqE8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.3 h1:VHPZakq2L7w+RLzV54LmQavbvheFaR2u1NomJRSEfcU= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.3/go.mod h1:DX1e/lkbsAt0MkY3NgLYuH4jQvRfw8MYxTe9feR7aXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.16 h1:2HuI7vWKhFWsBhIr2Zq8KfFZT6xqaId2XXnXZjkbEuc= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.16/go.mod h1:BrwWnsfbFtFeRjdx0iM1ymvlqDX1Oz68JsQaibX/wG8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.80.2 h1:T6Wu+8E2LeTUqzqQ/Bh1EoFNj1u4jUyveMgmTlu9fDU= -github.com/aws/aws-sdk-go-v2/service/s3 v1.80.2/go.mod h1:chSY8zfqmS0OnhZoO/hpPx/BHfAIL80m77HwhRLYScY= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= +github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0 h1:1GmCadhKR3J2sMVKs2bAYq9VnwYeCqfRyZzD4RASGlA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= +github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= +github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -2449,30 +2449,30 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/cli-runtime v0.33.1 h1:TvpjEtF71ViFmPeYMj1baZMJR4iWUEplklsUQ7D3quA= -k8s.io/cli-runtime v0.33.1/go.mod h1:9dz5Q4Uh8io4OWCLiEf/217DXwqNgiTS/IOuza99VZE= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y= +k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.33.1 h1:OJUXa6FV5bap6iRy345ezEjU9dTLxqv1zFTVqmeHb6A= -k8s.io/kubectl v0.33.1/go.mod h1:Z07pGqXoP4NgITlPRrnmiM3qnoo1QrK1zjw85Aiz8J0= +k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y= +k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI= k8s.io/kubelet v0.32.3 h1:B9HzW4yB67flx8tN2FYuDwZvxnmK3v5EjxxFvOYjmc8= k8s.io/kubelet v0.32.3/go.mod h1:yyAQSCKC+tjSlaFw4HQG7Jein+vo+GeKBGdXdQGvL1U= -k8s.io/metrics v0.33.1 h1:Ypd5ITCf+fM+LDNFk7hESXTc3vh02CQYGiwRoVRaGsM= -k8s.io/metrics v0.33.1/go.mod h1:wK8cFTK5ykBdhL0Wy4RZwLH28XM7j/Klc+NQrMRWVxg= +k8s.io/metrics v0.33.2 h1:gNCBmtnUMDMCRg9Ly5ehxP3OdKISMsOnh1vzk01iCgE= +k8s.io/metrics v0.33.2/go.mod h1:yxoAosKGRsZisv3BGekC5W6T1J8XSV+PoUEevACRv7c= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= diff --git a/kinds/go.mod b/kinds/go.mod index f00d89650..2267e8ee6 100644 --- a/kinds/go.mod +++ b/kinds/go.mod @@ -23,7 +23,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect diff --git a/kinds/go.sum b/kinds/go.sum index 6b1b56bd9..ec6337111 100644 --- a/kinds/go.sum +++ b/kinds/go.sum @@ -16,8 +16,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= From c452bb8b42585fbf628e2c47c7d3530f33a17138 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:06:12 +0000 Subject: [PATCH 23/48] build(deps): bump the k8s-io group across 1 directory with 4 updates (#2361) Bumps the k8s-io group with 3 updates in the /kinds directory: [k8s.io/api](https://github.com/kubernetes/api), [k8s.io/client-go](https://github.com/kubernetes/client-go) and [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver). Updates `k8s.io/api` from 0.32.3 to 0.33.2 - [Commits](https://github.com/kubernetes/api/compare/v0.32.3...v0.33.2) Updates `k8s.io/apimachinery` from 0.32.3 to 0.33.2 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.3...v0.33.2) Updates `k8s.io/client-go` from 0.32.3 to 0.33.2 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.32.3...v0.33.2) Updates `k8s.io/apiextensions-apiserver` from 0.32.3 to 0.33.2 - [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases) - [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.32.3...v0.33.2) Updates `k8s.io/api` from 0.32.3 to 0.33.2 - [Commits](https://github.com/kubernetes/api/compare/v0.32.3...v0.33.2) Updates `k8s.io/apimachinery` from 0.32.3 to 0.33.2 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.3...v0.33.2) --- updated-dependencies: - dependency-name: k8s.io/api dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: k8s-io - dependency-name: k8s.io/apimachinery dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: k8s-io - dependency-name: k8s.io/client-go dependency-version: 0.33.2 dependency-type: indirect update-type: version-update:semver-minor dependency-group: k8s-io - dependency-name: k8s.io/apiextensions-apiserver dependency-version: 0.33.2 dependency-type: indirect update-type: version-update:semver-minor dependency-group: k8s-io - dependency-name: k8s.io/api dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: k8s-io - dependency-name: k8s.io/apimachinery dependency-version: 0.33.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: k8s-io ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- kinds/go.mod | 16 +++++++--------- kinds/go.sum | 29 +++++++++++++++-------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/kinds/go.mod b/kinds/go.mod index 2267e8ee6..414d0992a 100644 --- a/kinds/go.mod +++ b/kinds/go.mod @@ -9,8 +9,8 @@ require ( github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.32.3 - k8s.io/apimachinery v0.32.3 + k8s.io/api v0.33.2 + k8s.io/apimachinery v0.33.2 sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/yaml v1.4.0 ) @@ -25,8 +25,6 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -37,7 +35,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/vishvananda/netlink v1.3.0 // indirect github.com/vishvananda/netns v0.0.5 // indirect @@ -45,16 +42,17 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/tools v0.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect helm.sh/helm/v3 v3.17.3 // indirect - k8s.io/apiextensions-apiserver v0.32.3 // indirect - k8s.io/client-go v0.32.3 // indirect + k8s.io/apiextensions-apiserver v0.33.2 // indirect + k8s.io/client-go v0.33.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect ) diff --git a/kinds/go.sum b/kinds/go.sum index ec6337111..fbbd81aba 100644 --- a/kinds/go.sum +++ b/kinds/go.sum @@ -26,8 +26,6 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -104,8 +102,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -145,14 +143,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg= helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= @@ -161,7 +159,10 @@ sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+ sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From 8fa788bbb64d6c60bdfde6e7bb77361a12e0d4d7 Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Mon, 23 Jun 2025 05:39:00 -0700 Subject: [PATCH 24/48] Fix potential deadlocks in the statemachine (#2346) --- api/README.md | 2 +- api/controllers/install/hostpreflight.go | 41 ++++++++++++---------- api/controllers/install/infra.go | 44 +++++++++++++----------- api/internal/managers/infra/install.go | 21 +++++------ 4 files changed, 57 insertions(+), 51 deletions(-) diff --git a/api/README.md b/api/README.md index c85b61869..83dcc6223 100644 --- a/api/README.md +++ b/api/README.md @@ -16,7 +16,7 @@ Contains the business logic for different API endpoints. Each controller package Contains shared utilities and helper packages that provide common functionality used across different parts of the API. This includes both general-purpose utilities and domain-specific helpers. #### `/internal/managers` -Each manager is responsible for a specific subdomain of functionality and provides a clean, thread-safe interface for controllers to interact with. For example, the Preflight Manager manages system requirement checks and validation. +Each manager is responsible for a specific subdomain of functionality and provides a clean interface for controllers to interact with. For example, the Preflight Manager manages system requirement checks and validation. #### `/internal/statemachine` The statemachine is used by controllers to capture workflow state and enforce valid transitions. diff --git a/api/controllers/install/hostpreflight.go b/api/controllers/install/hostpreflight.go index 4a74e2691..71481ae02 100644 --- a/api/controllers/install/hostpreflight.go +++ b/api/controllers/install/hostpreflight.go @@ -11,12 +11,21 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/netutils" ) -func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostPreflightsOptions) error { +func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostPreflightsOptions) (finalErr error) { lock, err := c.stateMachine.AcquireLock() if err != nil { return types.NewConflictError(err) } + defer func() { + if r := recover(); r != nil { + finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) + } + if finalErr != nil { + lock.Release() + } + }() + if err := c.stateMachine.ValidateTransition(lock, StatePreflightsRunning); err != nil { return types.NewConflictError(err) } @@ -34,7 +43,6 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP IsUI: opts.IsUI, }) if err != nil { - lock.Release() return fmt.Errorf("failed to prepare host preflights: %w", err) } @@ -43,7 +51,7 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP return fmt.Errorf("failed to transition states: %w", err) } - go func() { + go func() (finalErr error) { // Background context is used to avoid canceling the operation if the context is canceled ctx := context.Background() @@ -51,10 +59,16 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP defer func() { if r := recover(); r != nil { - c.logger.Errorf("panic running host preflights: %v: %s", r, string(debug.Stack())) + finalErr = fmt.Errorf("panic running host preflights: %v: %s", r, string(debug.Stack())) + } + if finalErr != nil { + c.logger.Error(finalErr) - err := c.stateMachine.Transition(lock, StatePreflightsFailed) - if err != nil { + if err := c.stateMachine.Transition(lock, StatePreflightsFailed); err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } else { + if err := c.stateMachine.Transition(lock, StatePreflightsSucceeded); err != nil { c.logger.Errorf("failed to transition states: %w", err) } } @@ -63,20 +77,11 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP err := c.hostPreflightManager.RunHostPreflights(ctx, c.rc, preflight.RunHostPreflightOptions{ HostPreflightSpec: hpf, }) - if err != nil { - c.logger.Errorf("failed to run host preflights: %w", err) - - err = c.stateMachine.Transition(lock, StatePreflightsFailed) - if err != nil { - c.logger.Errorf("failed to transition states: %w", err) - } - } else { - err = c.stateMachine.Transition(lock, StatePreflightsSucceeded) - if err != nil { - c.logger.Errorf("failed to transition states: %w", err) - } + return fmt.Errorf("failed to run host preflights: %w", err) } + + return nil }() return nil diff --git a/api/controllers/install/infra.go b/api/controllers/install/infra.go index ec3341d74..3a391e6ba 100644 --- a/api/controllers/install/infra.go +++ b/api/controllers/install/infra.go @@ -8,7 +8,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -func (c *InstallController) SetupInfra(ctx context.Context) error { +func (c *InstallController) SetupInfra(ctx context.Context) (finalErr error) { if c.stateMachine.CurrentState() == StatePreflightsFailed { err := c.bypassPreflights(ctx) if err != nil { @@ -21,13 +21,21 @@ func (c *InstallController) SetupInfra(ctx context.Context) error { return types.NewConflictError(err) } + defer func() { + if r := recover(); r != nil { + finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) + } + if finalErr != nil { + lock.Release() + } + }() + err = c.stateMachine.Transition(lock, StateInfrastructureInstalling) if err != nil { - lock.Release() return types.NewConflictError(err) } - go func() { + go func() (finalErr error) { // Background context is used to avoid canceling the operation if the context is canceled ctx := context.Background() @@ -35,30 +43,26 @@ func (c *InstallController) SetupInfra(ctx context.Context) error { defer func() { if r := recover(); r != nil { - c.logger.Errorf("panic installing infrastructure: %v: %s", r, string(debug.Stack())) + finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) + } + if finalErr != nil { + c.logger.Error(finalErr) - err := c.stateMachine.Transition(lock, StateFailed) - if err != nil { + if err := c.stateMachine.Transition(lock, StateFailed); err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } else { + if err := c.stateMachine.Transition(lock, StateSucceeded); err != nil { c.logger.Errorf("failed to transition states: %w", err) } } }() - err := c.infraManager.Install(ctx, c.rc) - - if err != nil { - c.logger.Errorf("failed to install infrastructure: %w", err) - - err := c.stateMachine.Transition(lock, StateFailed) - if err != nil { - c.logger.Errorf("failed to transition states: %w", err) - } - } else { - err = c.stateMachine.Transition(lock, StateSucceeded) - if err != nil { - c.logger.Errorf("failed to transition states: %w", err) - } + if err := c.infraManager.Install(ctx, c.rc); err != nil { + return fmt.Errorf("failed to install infrastructure: %w", err) } + + return nil }() return nil diff --git a/api/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index 5bea965ad..41eee9013 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -52,23 +52,20 @@ func (m *infraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConf defer func() { if r := recover(); r != nil { finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) - - if err := m.setStatus(types.StateFailed, "Installation failed to run: panic"); err != nil { + } + if finalErr != nil { + if err := m.setStatus(types.StateFailed, finalErr.Error()); err != nil { m.logger.WithField("error", err).Error("set failed status") } + } else { + if err := m.setStatus(types.StateSucceeded, "Installation complete"); err != nil { + m.logger.WithField("error", err).Error("set succeeded status") + } } }() - err = m.install(ctx, rc) - - if err != nil { - if err := m.setStatus(types.StateFailed, err.Error()); err != nil { - m.logger.WithField("error", err).Error("set failed status") - } - } else { - if err := m.setStatus(types.StateSucceeded, "Installation complete"); err != nil { - m.logger.WithField("error", err).Error("set succeeded status") - } + if err := m.install(ctx, rc); err != nil { + return err } return nil From 725a109531521561592aaa12a3f77e810122907a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Antunes?= Date: Mon, 23 Jun 2025 16:42:00 +0100 Subject: [PATCH 25/48] chore: validate state machine lock in tests (#2362) --- api/controllers/install/controller_test.go | 3 + api/internal/statemachine/statemachine.go | 9 +++ .../statemachine/statemachine_test.go | 65 ++++++++++++------- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/api/controllers/install/controller_test.go b/api/controllers/install/controller_test.go index 37c75b6c0..f7434cbab 100644 --- a/api/controllers/install/controller_test.go +++ b/api/controllers/install/controller_test.go @@ -246,6 +246,7 @@ func TestConfigureInstallation(t *testing.T) { assert.Eventually(t, func() bool { return sm.CurrentState() == tt.expectedState }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after configuration") mockManager.AssertExpectations(t) }) @@ -413,6 +414,7 @@ func TestRunHostPreflights(t *testing.T) { assert.Eventually(t, func() bool { return sm.CurrentState() == tt.expectedState }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after running preflights") mockPreflightManager.AssertExpectations(t) }) @@ -746,6 +748,7 @@ func TestSetupInfra(t *testing.T) { assert.Eventually(t, func() bool { return sm.CurrentState() == tt.expectedState }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after running infra setup") mockPreflightManager.AssertExpectations(t) mockInstallationManager.AssertExpectations(t) diff --git a/api/internal/statemachine/statemachine.go b/api/internal/statemachine/statemachine.go index 32aaed390..2aee4362a 100644 --- a/api/internal/statemachine/statemachine.go +++ b/api/internal/statemachine/statemachine.go @@ -26,6 +26,8 @@ type Interface interface { Transition(lock Lock, nextState State) error // AcquireLock acquires a lock on the state machine. AcquireLock() (Lock, error) + // IsLockAcquired checks if a lock already exists on the state machine. + IsLockAcquired() bool } type Lock interface { @@ -83,6 +85,13 @@ func (sm *stateMachine) AcquireLock() (Lock, error) { return sm.lock, nil } +func (sm *stateMachine) IsLockAcquired() bool { + sm.mu.RLock() + defer sm.mu.RUnlock() + + return sm.lock != nil +} + func (sm *stateMachine) ValidateTransition(lock Lock, nextState State) error { sm.mu.RLock() defer sm.mu.RUnlock() diff --git a/api/internal/statemachine/statemachine_test.go b/api/internal/statemachine/statemachine_test.go index cc60301f1..953d6d62f 100644 --- a/api/internal/statemachine/statemachine_test.go +++ b/api/internal/statemachine/statemachine_test.go @@ -43,11 +43,13 @@ var validStateTransitions = map[State][]State{ func TestLockAcquisitionAndRelease(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) // Test valid lock acquisition lock, err := sm.AcquireLock() assert.NoError(t, err) assert.NotNil(t, lock) + assert.True(t, sm.IsLockAcquired()) // Test transition with lock err = sm.Transition(lock, StateInstallationConfigured) @@ -56,11 +58,13 @@ func TestLockAcquisitionAndRelease(t *testing.T) { // Release lock lock.Release() + assert.False(t, sm.IsLockAcquired()) // Test double lock acquisition lock, err = sm.AcquireLock() assert.NoError(t, err) assert.NotNil(t, lock) + assert.True(t, sm.IsLockAcquired()) err = sm.Transition(lock, StatePreflightsRunning) assert.NoError(t, err) @@ -68,43 +72,54 @@ func TestLockAcquisitionAndRelease(t *testing.T) { // Release lock lock.Release() assert.Equal(t, StatePreflightsRunning, sm.CurrentState()) + assert.False(t, sm.IsLockAcquired()) } func TestDoubleLockAcquisition(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) lock1, err := sm.AcquireLock() assert.NoError(t, err) + assert.True(t, sm.IsLockAcquired()) // Try to acquire second lock while first is held lock2, err := sm.AcquireLock() assert.Error(t, err, "second lock acquisition should fail while first is held") assert.Nil(t, lock2) assert.Contains(t, err.Error(), "lock already acquired") + assert.True(t, sm.IsLockAcquired()) // Release first lock lock1.Release() + assert.False(t, sm.IsLockAcquired()) // Now second lock should work lock2, err = sm.AcquireLock() assert.NoError(t, err) assert.NotNil(t, lock2) + assert.True(t, sm.IsLockAcquired()) // Release second lock lock2.Release() + assert.False(t, sm.IsLockAcquired()) } func TestLockReleaseAfterTransition(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) lock, err := sm.AcquireLock() assert.NoError(t, err) + assert.True(t, sm.IsLockAcquired()) err = sm.Transition(lock, StateInstallationConfigured) assert.NoError(t, err) + assert.True(t, sm.IsLockAcquired()) // Release lock after transition lock.Release() + assert.False(t, sm.IsLockAcquired()) // State should remain changed assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) @@ -112,64 +127,49 @@ func TestLockReleaseAfterTransition(t *testing.T) { func TestDoubleLockRelease(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) lock, err := sm.AcquireLock() assert.NoError(t, err) + assert.True(t, sm.IsLockAcquired()) // Release lock lock.Release() + assert.False(t, sm.IsLockAcquired()) // Acquire another lock lock2, err := sm.AcquireLock() assert.NoError(t, err) assert.NotNil(t, lock2) + assert.True(t, sm.IsLockAcquired()) // Second release should not actually do anything lock.Release() + assert.True(t, sm.IsLockAcquired()) // Should not be able to acquire lock after as the other lock is still held nilLock, err := sm.AcquireLock() assert.Error(t, err, "should not be able to acquire lock after as the other lock is still held") assert.Nil(t, nilLock) + assert.True(t, sm.IsLockAcquired()) // Release the second lock lock2.Release() + assert.False(t, sm.IsLockAcquired()) // Should be able to acquire lock after the other lock is released lock3, err := sm.AcquireLock() assert.NoError(t, err) assert.NotNil(t, lock3) + assert.True(t, sm.IsLockAcquired()) lock3.Release() -} - -func TestConcurrentLockBlocking(t *testing.T) { - sm := New(StateNew, validStateTransitions) - - // Start first lock acquisition - lock1, err := sm.AcquireLock() - assert.NoError(t, err) - assert.NotNil(t, lock1) - - // Try to acquire second lock while first is held - lock2, err := sm.AcquireLock() - assert.Error(t, err, "second lock should fail while first is held") - assert.Nil(t, lock2) - assert.Contains(t, err.Error(), "lock already acquired") - - // Release first lock - lock1.Release() - - // Now second lock should work - lock2, err = sm.AcquireLock() - assert.NoError(t, err) - assert.NotNil(t, lock2) - - lock2.Release() + assert.False(t, sm.IsLockAcquired()) } func TestRaceConditionMultipleGoroutines(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) var wg sync.WaitGroup successCount := 0 @@ -203,10 +203,13 @@ func TestRaceConditionMultipleGoroutines(t *testing.T) { // Only one transition should succeed assert.Equal(t, 1, successCount, "only one transition should succeed") assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) + // There should be no lock acquired at the end + assert.False(t, sm.IsLockAcquired()) } func TestRaceConditionReadWrite(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) var wg sync.WaitGroup @@ -257,6 +260,8 @@ func TestRaceConditionReadWrite(t *testing.T) { finalState := sm.CurrentState() assert.True(t, finalState == StateInstallationConfigured || finalState == StatePreflightsRunning, "final state should be one of the expected states") + // There should be no lock acquired at the end + assert.False(t, sm.IsLockAcquired()) } func TestIsFinalState(t *testing.T) { @@ -309,11 +314,13 @@ func TestFinalStateTransitionBlocking(t *testing.T) { func TestMultiStateTransitionWithLock(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) // Acquire lock and transition through multiple states lock, err := sm.AcquireLock() assert.NoError(t, err) assert.NotNil(t, lock) + assert.True(t, sm.IsLockAcquired()) // Transition 1: New -> StateInstallationConfigured err = sm.Transition(lock, StateInstallationConfigured) @@ -335,8 +342,10 @@ func TestMultiStateTransitionWithLock(t *testing.T) { assert.NoError(t, err) assert.Equal(t, StateInfrastructureInstalling, sm.CurrentState()) + assert.True(t, sm.IsLockAcquired()) // Release the lock lock.Release() + assert.False(t, sm.IsLockAcquired()) // State should be the final state in the transition chain assert.Equal(t, StateInfrastructureInstalling, sm.CurrentState(), "state should be the final transitioned state after lock release") @@ -344,9 +353,11 @@ func TestMultiStateTransitionWithLock(t *testing.T) { func TestInvalidTransition(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) lock, err := sm.AcquireLock() assert.NoError(t, err) + assert.True(t, sm.IsLockAcquired()) // Try invalid transition err = sm.Transition(lock, StateSucceeded) @@ -356,12 +367,15 @@ func TestInvalidTransition(t *testing.T) { // State should remain unchanged assert.Equal(t, StateNew, sm.CurrentState()) + assert.True(t, sm.IsLockAcquired()) lock.Release() + assert.False(t, sm.IsLockAcquired()) } func TestTransitionWithoutLock(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) err := sm.Transition(nil, StateInstallationConfigured) assert.Error(t, err, "transition should be invalid") assert.Contains(t, err.Error(), "lock not acquired") @@ -370,6 +384,7 @@ func TestTransitionWithoutLock(t *testing.T) { func TestValidateTransitionWithoutLock(t *testing.T) { sm := New(StateNew, validStateTransitions) + assert.False(t, sm.IsLockAcquired()) err := sm.ValidateTransition(nil, StateInstallationConfigured) assert.Error(t, err, "transition should be invalid") assert.Contains(t, err.Error(), "lock not acquired") From d52e8a9bdde268112b2c91d3fb43c17493e58ae1 Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:27:27 -0400 Subject: [PATCH 26/48] feat: allow users to bypass host preflights in install wizard (#2335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add ignorehostpreflight to api installation config * continue adding ignorehostpreflights flag to API * add integration tests * add modal to confirm skipping of preflights when ignorehostpreflights flag is passed * revert files updated by buildtools * update swagger docs * fix unit tests * restructure unit test * debug issue with unit test * debug issue with unit test * remove test * Update web/src/components/wizard/ValidationStep.tsx Co-authored-by: Alex Parker <7272359+ajp-io@users.noreply.github.com> * store ignorehostpreflight flag value in preflight response instead of installation config * adding preflight validation to be used during infra setup * continue adding preflight validation to infra setup * update web to send request to ignore preflights when we start the installation * add additional validationstep tests * update swagger docs * cleanup unused code * add design doc * simply preflight validatio and move to setupinfra on install controller * consistently use allowignorehostpreflights in api * simplify response for setupinfra * update design doc * revert buildtools files * cleanup * address feedback * address feedback * fix integration test * fix failing test * improve error handling * returning bool for setupinfra no longer necessary * improve setupinfra tests * remove NewHostPreflights function that's not being used * use mock manager with real controller for integration tests * conditional render modal only when needed * remove uneeded comment * Update api/integration/infra_test.go Co-authored-by: João Antunes * remove modal test * remove unneeded validation step test * consolidate tests * change api error handling to use returned handler from the API * update tests to assert expected error from API * update tests * f * fix tests * fix lint * make property name consistent * revert uneeded changes --------- Co-authored-by: Alex Parker <7272359+ajp-io@users.noreply.github.com> Co-authored-by: João Antunes --- api/api.go | 34 +- api/controllers/install/controller.go | 21 +- api/controllers/install/controller_test.go | 107 ++- api/controllers/install/infra.go | 18 +- api/docs/docs.go | 4 +- api/docs/swagger.json | 4 +- api/docs/swagger.yaml | 14 + api/install.go | 20 +- api/integration/hostpreflights_test.go | 83 +++ api/integration/install_test.go | 248 ++++++- api/types/errors.go | 8 + api/types/infra.go | 5 + api/types/preflight.go | 7 +- api/types/responses.go | 7 +- cmd/installer/cli/api.go | 26 +- cmd/installer/cli/install.go | 13 +- web/src/components/common/Modal.tsx | 47 ++ web/src/components/wizard/ValidationStep.tsx | 94 ++- .../wizard/preflight/LinuxPreflightCheck.tsx | 5 +- .../wizard/tests/LinuxPreflightCheck.test.tsx | 83 ++- .../wizard/tests/ValidationStep.test.tsx | 683 ++++++++++++++++++ 21 files changed, 1416 insertions(+), 115 deletions(-) create mode 100644 web/src/components/common/Modal.tsx create mode 100644 web/src/components/wizard/tests/ValidationStep.test.tsx diff --git a/api/api.go b/api/api.go index 56928f762..f483bc431 100644 --- a/api/api.go +++ b/api/api.go @@ -42,19 +42,20 @@ import ( // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ type API struct { - authController auth.Controller - consoleController console.Controller - installController install.Controller - rc runtimeconfig.RuntimeConfig - releaseData *release.ReleaseData - tlsConfig types.TLSConfig - license []byte - airgapBundle string - configValues string - endUserConfig *ecv1beta1.Config - logger logrus.FieldLogger - hostUtils hostutils.HostUtilsInterface - metricsReporter metrics.ReporterInterface + authController auth.Controller + consoleController console.Controller + installController install.Controller + rc runtimeconfig.RuntimeConfig + releaseData *release.ReleaseData + tlsConfig types.TLSConfig + license []byte + airgapBundle string + configValues string + endUserConfig *ecv1beta1.Config + allowIgnoreHostPreflights bool + logger logrus.FieldLogger + hostUtils hostutils.HostUtilsInterface + metricsReporter metrics.ReporterInterface } type APIOption func(*API) @@ -137,6 +138,12 @@ func WithEndUserConfig(endUserConfig *ecv1beta1.Config) APIOption { } } +func WithAllowIgnoreHostPreflights(allowIgnoreHostPreflights bool) APIOption { + return func(a *API) { + a.allowIgnoreHostPreflights = allowIgnoreHostPreflights + } +} + func New(password string, opts ...APIOption) (*API, error) { api := &API{} @@ -192,6 +199,7 @@ func New(password string, opts ...APIOption) (*API, error) { install.WithAirgapBundle(api.airgapBundle), install.WithConfigValues(api.configValues), install.WithEndUserConfig(api.endUserConfig), + install.WithAllowIgnoreHostPreflights(api.allowIgnoreHostPreflights), ) if err != nil { return nil, fmt.Errorf("new install controller: %w", err) diff --git a/api/controllers/install/controller.go b/api/controllers/install/controller.go index 512d0ee15..e3265388a 100644 --- a/api/controllers/install/controller.go +++ b/api/controllers/install/controller.go @@ -28,7 +28,7 @@ type Controller interface { GetHostPreflightStatus(ctx context.Context) (types.Status, error) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) GetHostPreflightTitles(ctx context.Context) ([]string, error) - SetupInfra(ctx context.Context) error + SetupInfra(ctx context.Context, ignoreHostPreflights bool) error GetInfra(ctx context.Context) (types.Infra, error) SetStatus(ctx context.Context, status types.Status) error GetStatus(ctx context.Context) (types.Status, error) @@ -55,12 +55,13 @@ type InstallController struct { configValues string endUserConfig *ecv1beta1.Config - install types.Install - store store.Store - rc runtimeconfig.RuntimeConfig - stateMachine statemachine.Interface - logger logrus.FieldLogger - mu sync.RWMutex + install types.Install + store store.Store + rc runtimeconfig.RuntimeConfig + stateMachine statemachine.Interface + logger logrus.FieldLogger + mu sync.RWMutex + allowIgnoreHostPreflights bool } type InstallControllerOption func(*InstallController) @@ -137,6 +138,12 @@ func WithEndUserConfig(endUserConfig *ecv1beta1.Config) InstallControllerOption } } +func WithAllowIgnoreHostPreflights(allowIgnoreHostPreflights bool) InstallControllerOption { + return func(c *InstallController) { + c.allowIgnoreHostPreflights = allowIgnoreHostPreflights + } +} + func WithInstallationManager(installationManager installation.InstallationManager) InstallControllerOption { return func(c *InstallController) { c.installationManager = installationManager diff --git a/api/controllers/install/controller_test.go b/api/controllers/install/controller_test.go index f7434cbab..fe9bf04f4 100644 --- a/api/controllers/install/controller_test.go +++ b/api/controllers/install/controller_test.go @@ -641,27 +641,33 @@ func TestGetInstallationStatus(t *testing.T) { func TestSetupInfra(t *testing.T) { tests := []struct { - name string - currentState statemachine.State - expectedState statemachine.State - setupMocks func(runtimeconfig.RuntimeConfig, *preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter) - expectedErr bool + name string + clientIgnoreHostPreflights bool // From HTTP request + serverAllowIgnoreHostPreflights bool // From CLI flag + currentState statemachine.State + expectedState statemachine.State + setupMocks func(runtimeconfig.RuntimeConfig, *preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter) + expectedErr error }{ { - name: "successful setup with passed preflights", - currentState: StatePreflightsSucceeded, - expectedState: StateSucceeded, + name: "successful setup with passed preflights", + clientIgnoreHostPreflights: false, + serverAllowIgnoreHostPreflights: true, + currentState: StatePreflightsSucceeded, + expectedState: StateSucceeded, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { mock.InOrder( fm.On("Install", mock.Anything, rc).Return(nil), ) }, - expectedErr: false, + expectedErr: nil, }, { - name: "successful setup with failed preflights", - currentState: StatePreflightsFailed, - expectedState: StateSucceeded, + name: "successful setup with failed preflights - ignored with CLI flag", + clientIgnoreHostPreflights: true, + serverAllowIgnoreHostPreflights: true, + currentState: StatePreflightsFailed, + expectedState: StateSucceeded, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { preflightOutput := &types.HostPreflightsOutput{ Fail: []types.HostPreflightsRecord{ @@ -677,37 +683,73 @@ func TestSetupInfra(t *testing.T) { fm.On("Install", mock.Anything, rc).Return(nil), ) }, - expectedErr: false, + expectedErr: nil, }, { - name: "preflight output error", - currentState: StatePreflightsFailed, - expectedState: StatePreflightsFailed, + name: "failed setup with failed preflights - not ignored", + clientIgnoreHostPreflights: false, + serverAllowIgnoreHostPreflights: true, + currentState: StatePreflightsFailed, + expectedState: StatePreflightsFailed, + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + }, + expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), + }, + { + name: "preflight output error", + clientIgnoreHostPreflights: true, + serverAllowIgnoreHostPreflights: true, + currentState: StatePreflightsFailed, + expectedState: StatePreflightsFailed, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { mock.InOrder( pm.On("GetHostPreflightOutput", t.Context()).Return(nil, errors.New("get output error")), ) }, - expectedErr: true, + expectedErr: errors.New("any error"), // Just check that an error occurs, don't care about exact message }, { - name: "install infra error", - currentState: StatePreflightsSucceeded, - expectedState: StateFailed, + name: "install infra error", + clientIgnoreHostPreflights: false, + serverAllowIgnoreHostPreflights: true, + currentState: StatePreflightsSucceeded, + expectedState: StateFailed, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { mock.InOrder( fm.On("Install", mock.Anything, rc).Return(errors.New("install error")), ) }, - expectedErr: false, + expectedErr: nil, }, { - name: "invalid state transition", - currentState: StateInstallationConfigured, - expectedState: StateInstallationConfigured, + name: "invalid state transition", + clientIgnoreHostPreflights: false, + serverAllowIgnoreHostPreflights: true, + currentState: StateInstallationConfigured, + expectedState: StateInstallationConfigured, setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { }, - expectedErr: true, + expectedErr: errors.New("invalid transition"), // Just check that an error occurs, don't care about exact message + }, + { + name: "failed preflights with ignore flag but CLI flag disabled", + clientIgnoreHostPreflights: true, + serverAllowIgnoreHostPreflights: false, + currentState: StatePreflightsFailed, + expectedState: StatePreflightsFailed, + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + }, + expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), + }, + { + name: "failed preflights without ignore flag and CLI flag disabled", + clientIgnoreHostPreflights: false, + serverAllowIgnoreHostPreflights: false, + currentState: StatePreflightsFailed, + expectedState: StatePreflightsFailed, + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + }, + expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), }, } @@ -732,13 +774,24 @@ func TestSetupInfra(t *testing.T) { WithInstallationManager(mockInstallationManager), WithInfraManager(mockInfraManager), WithMetricsReporter(mockMetricsReporter), + WithAllowIgnoreHostPreflights(tt.serverAllowIgnoreHostPreflights), ) require.NoError(t, err) - err = controller.SetupInfra(t.Context()) + err = controller.SetupInfra(t.Context(), tt.clientIgnoreHostPreflights) - if tt.expectedErr { + if tt.expectedErr != nil { require.Error(t, err) + + // Check for specific error types + var expectedAPIErr *types.APIError + if errors.As(tt.expectedErr, &expectedAPIErr) { + // For API errors, check the exact type and status code + var actualAPIErr *types.APIError + require.True(t, errors.As(err, &actualAPIErr), "expected error to be of type *types.APIError, got %T", err) + assert.Equal(t, expectedAPIErr.StatusCode, actualAPIErr.StatusCode, "status codes should match") + assert.Contains(t, actualAPIErr.Error(), expectedAPIErr.Unwrap().Error(), "error messages should contain expected content") + } } else { require.NoError(t, err) diff --git a/api/controllers/install/infra.go b/api/controllers/install/infra.go index 3a391e6ba..7d2f47f80 100644 --- a/api/controllers/install/infra.go +++ b/api/controllers/install/infra.go @@ -2,15 +2,21 @@ package install import ( "context" + "errors" "fmt" "runtime/debug" "github.com/replicatedhq/embedded-cluster/api/types" ) -func (c *InstallController) SetupInfra(ctx context.Context) (finalErr error) { +var ( + ErrPreflightChecksFailed = errors.New("preflight checks failed") + ErrPreflightChecksNotComplete = errors.New("preflight checks not complete") +) + +func (c *InstallController) SetupInfra(ctx context.Context, ignoreHostPreflights bool) (finalErr error) { if c.stateMachine.CurrentState() == StatePreflightsFailed { - err := c.bypassPreflights(ctx) + err := c.bypassPreflights(ctx, ignoreHostPreflights) if err != nil { return fmt.Errorf("bypass preflights: %w", err) } @@ -68,7 +74,11 @@ func (c *InstallController) SetupInfra(ctx context.Context) (finalErr error) { return nil } -func (c *InstallController) bypassPreflights(ctx context.Context) error { +func (c *InstallController) bypassPreflights(ctx context.Context, ignoreHostPreflights bool) error { + if !ignoreHostPreflights || !c.allowIgnoreHostPreflights { + return types.NewBadRequestError(ErrPreflightChecksFailed) + } + lock, err := c.stateMachine.AcquireLock() if err != nil { return types.NewConflictError(err) @@ -85,6 +95,8 @@ func (c *InstallController) bypassPreflights(ctx context.Context) error { if err != nil { return fmt.Errorf("get install host preflight output: %w", err) } + + // Report that preflights were bypassed if preflightOutput != nil { c.metricsReporter.ReportPreflightsBypassed(ctx, preflightOutput) } diff --git a/api/docs/docs.go b/api/docs/docs.go index 53dbd073b..edc316d4d 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -6,10 +6,10 @@ import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, - "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"{{escape .Description}}","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"{{.Title}}","version":"{{.Version}}"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, - "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.json b/api/docs/swagger.json index bc341b188..82e788854 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1,8 +1,8 @@ { - "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"This is the API for the Embedded Cluster project.","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"Embedded Cluster API","version":"0.1"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, - "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 407f97e79..df73c34e6 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -73,8 +73,15 @@ components: status: $ref: '#/components/schemas/types.Status' type: object + types.InfraSetupRequest: + properties: + ignoreHostPreflights: + type: boolean + type: object types.InstallHostPreflightsStatusResponse: properties: + allowIgnoreHostPreflights: + type: boolean output: $ref: '#/components/schemas/types.HostPreflightsOutput' status: @@ -232,6 +239,13 @@ paths: /install/infra/setup: post: description: Setup infra components + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/types.InfraSetupRequest' + description: Infra Setup Request + required: true responses: "200": content: diff --git a/api/install.go b/api/install.go index c55819443..05e27798b 100644 --- a/api/install.go +++ b/api/install.go @@ -134,9 +134,10 @@ func (a *API) getInstallHostPreflightsStatus(w http.ResponseWriter, r *http.Requ } response := types.InstallHostPreflightsStatusResponse{ - Titles: titles, - Output: output, - Status: status, + Titles: titles, + Output: output, + Status: status, + AllowIgnoreHostPreflights: a.allowIgnoreHostPreflights, } a.json(w, r, http.StatusOK, response) @@ -148,11 +149,20 @@ func (a *API) getInstallHostPreflightsStatus(w http.ResponseWriter, r *http.Requ // @Description Setup infra components // @Tags install // @Security bearerauth +// @Accept json // @Produce json -// @Success 200 {object} types.Infra +// @Param request body types.InfraSetupRequest true "Infra Setup Request" +// @Success 200 {object} types.Infra // @Router /install/infra/setup [post] func (a *API) postInstallSetupInfra(w http.ResponseWriter, r *http.Request) { - err := a.installController.SetupInfra(r.Context()) + // Parse request body + var req types.InfraSetupRequest + if err := a.bindJSON(w, r, &req); err != nil { + return + } + + // Setup infrastructure with preflight validation handled internally + err := a.installController.SetupInfra(r.Context(), req.IgnoreHostPreflights) if err != nil { a.logError(r, err, "failed to setup infra") a.jsonError(w, r, err) diff --git a/api/integration/hostpreflights_test.go b/api/integration/hostpreflights_test.go index 7270808a5..a2709a91e 100644 --- a/api/integration/hostpreflights_test.go +++ b/api/integration/hostpreflights_test.go @@ -167,6 +167,89 @@ func TestGetHostPreflightsStatus(t *testing.T) { }) } +// Test the getHostPreflightsStatus endpoint returns AllowIgnoreHostPreflights flag correctly +func TestGetHostPreflightsStatusWithIgnoreFlag(t *testing.T) { + tests := []struct { + name string + allowIgnoreHostPreflights bool + expectedAllowIgnore bool + }{ + { + name: "allow ignore host preflights true", + allowIgnoreHostPreflights: true, + expectedAllowIgnore: true, + }, + { + name: "allow ignore host preflights false", + allowIgnoreHostPreflights: false, + expectedAllowIgnore: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hpf := types.HostPreflights{ + Output: &types.HostPreflightsOutput{ + Pass: []types.HostPreflightsRecord{ + { + Title: "Some Preflight", + Message: "All good", + }, + }, + }, + Titles: []string{"Some Preflight"}, + Status: types.Status{ + State: types.StateSucceeded, + Description: "All preflights passed", + }, + } + runner := &preflights.MockPreflightRunner{} + // Create a host preflights manager + manager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + preflight.WithPreflightRunner(runner), + ) + // Create an install controller + installController, err := install.NewInstallController(install.WithHostPreflightManager(manager)) + require.NoError(t, err) + + // Create the API with allow ignore host preflights flag + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithAllowIgnoreHostPreflights(tt.allowIgnoreHostPreflights), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + // Create a router and register the API routes + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request + req := httptest.NewRequest(http.MethodGet, "/install/host-preflights/status", nil) + req.Header.Set("Authorization", "Bearer TOKEN") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response + require.Equal(t, http.StatusOK, rec.Code, "expected status ok, got %d with body %s", rec.Code, rec.Body.String()) + assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) + + // Parse the response body + var status types.InstallHostPreflightsStatusResponse + err = json.NewDecoder(rec.Body).Decode(&status) + require.NoError(t, err) + + // Verify the flag is present and correctly set by the handler + assert.Equal(t, tt.expectedAllowIgnore, status.AllowIgnoreHostPreflights) + }) + } +} + // Test the postRunHostPreflights endpoint runs host preflights correctly func TestPostRunHostPreflights(t *testing.T) { // Create a runtime config diff --git a/api/integration/install_test.go b/api/integration/install_test.go index d829e89f8..f78d214bf 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -108,7 +108,7 @@ func (m *mockInstallController) GetHostPreflightTitles(ctx context.Context) ([]s return []string{}, nil } -func (m *mockInstallController) SetupInfra(ctx context.Context) error { +func (m *mockInstallController) SetupInfra(ctx context.Context, ignoreHostPreflights bool) error { return m.setupInfraError } @@ -1225,9 +1225,16 @@ func TestPostSetupInfra(t *testing.T) { router := mux.NewRouter() apiInstance.RegisterRoutes(router) - // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + // Create a request with proper JSON body + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") + req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() // Serve the request @@ -1331,9 +1338,16 @@ func TestPostSetupInfra(t *testing.T) { router := mux.NewRouter() apiInstance.RegisterRoutes(router) - // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + // Create a request with proper JSON body + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer NOT_A_TOKEN") + req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() // Serve the request @@ -1350,6 +1364,185 @@ func TestPostSetupInfra(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, apiError.StatusCode) }) + // Test preflight bypass with CLI flag allowing it - should succeed + t.Run("Preflight bypass allowed by CLI flag", func(t *testing.T) { + // Create host preflights with failed status + hpf := types.HostPreflights{} + hpf.Status = types.Status{ + State: types.StateFailed, + Description: "Host preflights failed", + } + + // Create managers + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + + // Create an install controller with CLI flag allowing bypass + installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsFailed))), + install.WithHostPreflightManager(pfManager), + install.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request with ignoreHostPreflights=true + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: true, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req.Header.Set("Authorization", "Bearer TOKEN") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response - should succeed because CLI flag allows bypass + assert.Equal(t, http.StatusOK, rec.Code) + + t.Logf("Response body: %s", rec.Body.String()) + }) + + // Test preflight bypass with CLI flag NOT allowing it - should fail + t.Run("Preflight bypass denied by CLI flag", func(t *testing.T) { + // Create host preflights with failed status + hpf := types.HostPreflights{} + hpf.Status = types.Status{ + State: types.StateFailed, + Description: "Host preflights failed", + } + + // Create managers + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + + // Create an install controller with CLI flag NOT allowing bypass + installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsFailed))), + install.WithHostPreflightManager(pfManager), + install.WithAllowIgnoreHostPreflights(false), // CLI flag does NOT allow bypass + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request with ignoreHostPreflights=true + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: true, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req.Header.Set("Authorization", "Bearer TOKEN") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response - should fail because CLI flag does NOT allow bypass + assert.Equal(t, http.StatusBadRequest, rec.Code) + + t.Logf("Response body: %s", rec.Body.String()) + + // Parse the response body + var apiError types.APIError + err = json.NewDecoder(rec.Body).Decode(&apiError) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, apiError.StatusCode) + assert.Contains(t, apiError.Message, "preflight checks failed") + }) + + // Test client not requesting bypass but preflights failed - should fail + t.Run("Client not requesting bypass with failed preflights", func(t *testing.T) { + // Create host preflights with failed status + hpf := types.HostPreflights{} + hpf.Status = types.Status{ + State: types.StateFailed, + Description: "Host preflights failed", + } + + // Create managers + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + + // Create an install controller with CLI flag allowing bypass + installController, err := install.NewInstallController( + install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsFailed))), + install.WithHostPreflightManager(pfManager), + install.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + "password", + api.WithInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // Create a request with ignoreHostPreflights=false (client not requesting bypass) + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req.Header.Set("Authorization", "Bearer TOKEN") + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + // Serve the request + router.ServeHTTP(rec, req) + + // Check the response - should fail because client is not requesting bypass + assert.Equal(t, http.StatusBadRequest, rec.Code) + + t.Logf("Response body: %s", rec.Body.String()) + + // Parse the response body + var apiError types.APIError + err = json.NewDecoder(rec.Body).Decode(&apiError) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, apiError.StatusCode) + assert.Contains(t, apiError.Message, "preflight checks failed") + }) + // Test preflight checks not completed t.Run("Preflight checks not completed", func(t *testing.T) { // Create host preflights with running status (not completed) @@ -1383,9 +1576,16 @@ func TestPostSetupInfra(t *testing.T) { router := mux.NewRouter() apiInstance.RegisterRoutes(router) - // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + // Create a request with proper JSON body + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") + req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() // Serve the request @@ -1393,15 +1593,7 @@ func TestPostSetupInfra(t *testing.T) { // Check the response assert.Equal(t, http.StatusConflict, rec.Code) - - t.Logf("Response body: %s", rec.Body.String()) - - // Parse the response body - var apiError types.APIError - err = json.NewDecoder(rec.Body).Decode(&apiError) - require.NoError(t, err) - assert.Equal(t, http.StatusConflict, apiError.StatusCode) - assert.Contains(t, apiError.Message, "invalid transition from PreflightsRunning to InfrastructureInstalling") + assert.Contains(t, rec.Body.String(), "invalid transition") }) // Test k0s already installed error @@ -1449,9 +1641,16 @@ func TestPostSetupInfra(t *testing.T) { router := mux.NewRouter() apiInstance.RegisterRoutes(router) - // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + // Create a request with proper JSON body + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") + req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() // Serve the request @@ -1459,7 +1658,7 @@ func TestPostSetupInfra(t *testing.T) { // Check the response assert.Equal(t, http.StatusConflict, rec.Code) - assert.Contains(t, rec.Body.String(), "invalid transition from Succeeded to InfrastructureInstalling") + assert.Contains(t, rec.Body.String(), "invalid transition") }) // Test k0s install error @@ -1528,9 +1727,16 @@ func TestPostSetupInfra(t *testing.T) { router := mux.NewRouter() apiInstance.RegisterRoutes(router) - // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", nil) + // Create a request with proper JSON body + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") + req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() // Serve the request diff --git a/api/types/errors.go b/api/types/errors.go index 3f33d5e4c..031cd8f21 100644 --- a/api/types/errors.go +++ b/api/types/errors.go @@ -63,6 +63,14 @@ func NewConflictError(err error) *APIError { } } +func NewForbiddenError(err error) *APIError { + return &APIError{ + StatusCode: http.StatusForbidden, + Message: err.Error(), + err: err, + } +} + func NewUnauthorizedError(err error) *APIError { return &APIError{ StatusCode: http.StatusUnauthorized, diff --git a/api/types/infra.go b/api/types/infra.go index 22db09a25..ef251c552 100644 --- a/api/types/infra.go +++ b/api/types/infra.go @@ -1,5 +1,10 @@ package types +// InfraSetupRequest represents a request to set up infrastructure +type InfraSetupRequest struct { + IgnoreHostPreflights bool `json:"ignoreHostPreflights"` +} + type Infra struct { Components []InfraComponent `json:"components"` Logs string `json:"logs"` diff --git a/api/types/preflight.go b/api/types/preflight.go index e5efc6dbc..158c7fa86 100644 --- a/api/types/preflight.go +++ b/api/types/preflight.go @@ -6,9 +6,10 @@ type PostInstallRunHostPreflightsRequest struct { // HostPreflights represents the host preflight checks state type HostPreflights struct { - Titles []string `json:"titles"` - Output *HostPreflightsOutput `json:"output"` - Status Status `json:"status"` + Titles []string `json:"titles"` + Output *HostPreflightsOutput `json:"output"` + Status Status `json:"status"` + AllowIgnoreHostPreflights bool `json:"allowIgnoreHostPreflights"` } type HostPreflightsOutput struct { diff --git a/api/types/responses.go b/api/types/responses.go index fd560eb78..ebb71c9d2 100644 --- a/api/types/responses.go +++ b/api/types/responses.go @@ -2,7 +2,8 @@ package types // InstallHostPreflightsStatusResponse represents the response when polling install host preflights status type InstallHostPreflightsStatusResponse struct { - Titles []string `json:"titles"` - Output *HostPreflightsOutput `json:"output,omitempty"` - Status Status `json:"status,omitempty"` + Titles []string `json:"titles"` + Output *HostPreflightsOutput `json:"output,omitempty"` + Status Status `json:"status,omitempty"` + AllowIgnoreHostPreflights bool `json:"allowIgnoreHostPreflights"` } diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go index 96f7629e6..58c0cf11d 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -29,18 +29,19 @@ import ( // apiConfig holds the configuration for the API server type apiConfig struct { - RuntimeConfig runtimeconfig.RuntimeConfig - Logger logrus.FieldLogger - MetricsReporter metrics.ReporterInterface - Password string - TLSConfig apitypes.TLSConfig - ManagerPort int - License []byte - AirgapBundle string - ConfigValues string - ReleaseData *release.ReleaseData - EndUserConfig *ecv1beta1.Config - WebAssetsFS fs.FS + RuntimeConfig runtimeconfig.RuntimeConfig + Logger logrus.FieldLogger + MetricsReporter metrics.ReporterInterface + Password string + TLSConfig apitypes.TLSConfig + ManagerPort int + License []byte + AirgapBundle string + ConfigValues string + ReleaseData *release.ReleaseData + EndUserConfig *ecv1beta1.Config + WebAssetsFS fs.FS + AllowIgnoreHostPreflights bool } func startAPI(ctx context.Context, cert tls.Certificate, config apiConfig) error { @@ -91,6 +92,7 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, api.WithAirgapBundle(config.AirgapBundle), api.WithConfigValues(config.ConfigValues), api.WithEndUserConfig(config.EndUserConfig), + api.WithAllowIgnoreHostPreflights(config.AllowIgnoreHostPreflights), ) if err != nil { return fmt.Errorf("new api: %w", err) diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 06f0e9773..7d0acbbd0 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -423,12 +423,13 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc KeyBytes: flags.tlsKeyBytes, Hostname: flags.hostname, }, - ManagerPort: flags.managerPort, - License: flags.licenseBytes, - AirgapBundle: flags.airgapBundle, - ConfigValues: flags.configValues, - ReleaseData: release.GetReleaseData(), - EndUserConfig: eucfg, + ManagerPort: flags.managerPort, + License: flags.licenseBytes, + AirgapBundle: flags.airgapBundle, + ConfigValues: flags.configValues, + ReleaseData: release.GetReleaseData(), + EndUserConfig: eucfg, + AllowIgnoreHostPreflights: flags.ignoreHostPreflights, } if err := startAPI(ctx, flags.tlsCert, apiConfig); err != nil { diff --git a/web/src/components/common/Modal.tsx b/web/src/components/common/Modal.tsx new file mode 100644 index 000000000..bff81af56 --- /dev/null +++ b/web/src/components/common/Modal.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { X } from 'lucide-react'; + +interface ModalProps { + onClose: () => void; + title: string; + children: React.ReactNode; + footer?: React.ReactNode; +} + +export const Modal: React.FC = ({ onClose, title, children, footer }) => { + return ( +
+
+ {/* Background overlay */} +
+ + {/* Modal panel */} +
+
+
+

+ {title} +

+ +
+
+ {children} +
+
+ {footer && ( +
+ {footer} +
+ )} +
+
+
+ ); +}; \ No newline at end of file diff --git a/web/src/components/wizard/ValidationStep.tsx b/web/src/components/wizard/ValidationStep.tsx index 317d53877..7ea7fa0a7 100644 --- a/web/src/components/wizard/ValidationStep.tsx +++ b/web/src/components/wizard/ValidationStep.tsx @@ -1,8 +1,9 @@ import React from "react"; import Card from "../common/Card"; import Button from "../common/Button"; +import { Modal } from "../common/Modal"; import { useWizardMode } from "../../contexts/WizardModeContext"; -import { ChevronLeft, ChevronRight } from "lucide-react"; +import { ChevronLeft, ChevronRight, AlertTriangle } from "lucide-react"; import LinuxPreflightCheck from "./preflight/LinuxPreflightCheck"; import { useMutation } from "@tanstack/react-query"; import { useAuth } from "../../contexts/AuthContext"; @@ -16,38 +17,83 @@ const ValidationStep: React.FC = ({ onNext, onBack }) => { const { text } = useWizardMode(); const [preflightComplete, setPreflightComplete] = React.useState(false); const [preflightSuccess, setPreflightSuccess] = React.useState(false); + const [allowIgnoreHostPreflights, setAllowIgnoreHostPreflights] = React.useState(false); + const [showPreflightModal, setShowPreflightModal] = React.useState(false); const [error, setError] = React.useState(null); const { token } = useAuth(); - const handlePreflightComplete = (success: boolean) => { + const handlePreflightComplete = (success: boolean, allowIgnore: boolean) => { setPreflightComplete(true); setPreflightSuccess(success); + setAllowIgnoreHostPreflights(allowIgnore); }; const { mutate: startInstallation } = useMutation({ - mutationFn: async () => { + mutationFn: async ({ ignoreHostPreflights }: { ignoreHostPreflights: boolean }) => { const response = await fetch("/api/install/infra/setup", { method: "POST", headers: { + "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, + body: JSON.stringify({ + ignoreHostPreflights: ignoreHostPreflights + }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); - throw errorData; + throw new Error(errorData.message || "Failed to start installation"); } return response.json(); }, onSuccess: () => { + setError(null); // Clear any previous errors onNext(); }, onError: (err: Error) => { setError(err.message || "Failed to start installation"); - return err; }, }); + const handleNextClick = () => { + // If preflights passed, proceed normally + if (preflightSuccess) { + startInstallation({ ignoreHostPreflights: false }); // No need to ignore preflights + return; + } + + // If preflights failed and button is enabled (allowIgnoreHostPreflights is true), show warning modal + if (allowIgnoreHostPreflights) { + setShowPreflightModal(true); + } + // Note: If allowIgnoreHostPreflights is false, button should be disabled (handled in canProceed) + }; + + const handleCancelProceed = () => { + setShowPreflightModal(false); + }; + + const handleConfirmProceed = () => { + setShowPreflightModal(false); + startInstallation({ ignoreHostPreflights: true }); // User confirmed they want to ignore preflight failures + }; + + const canProceed = () => { + // If preflights haven't completed yet, disable button + if (!preflightComplete) { + return false; + } + + // If preflights passed, always allow proceeding + if (preflightSuccess) { + return true; + } + + // If preflights failed, only allow proceeding if CLI flag was used + return allowIgnoreHostPreflights; + }; + return (
@@ -66,13 +112,47 @@ const ValidationStep: React.FC = ({ onNext, onBack }) => { Back
+ + {showPreflightModal && ( + + + +
+ } + > +
+
+ +
+
+

+ Some preflight checks have failed. Continuing with the installation is likely to cause errors. Are you sure you want to proceed? +

+
+
+ + )}
); }; diff --git a/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx b/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx index 1a445a882..17c98066e 100644 --- a/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx +++ b/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx @@ -6,7 +6,7 @@ import Button from "../../common/Button"; import { useAuth } from "../../../contexts/AuthContext"; interface LinuxPreflightCheckProps { - onComplete: (success: boolean) => void; + onComplete: (success: boolean, allowIgnoreHostPreflights: boolean) => void; } interface PreflightResult { @@ -30,6 +30,7 @@ interface PreflightResponse { titles: string[]; output?: PreflightOutput; status?: PreflightStatus; + allowIgnoreHostPreflights?: boolean; } interface InstallationStatusResponse { @@ -132,7 +133,7 @@ const LinuxPreflightCheck: React.FC = ({ onComplete }) useEffect(() => { if (preflightResponse?.status?.state === "Succeeded" || preflightResponse?.status?.state === "Failed") { setIsPreflightsPolling(false); - onComplete(!hasFailures(preflightResponse.output)); + onComplete(!hasFailures(preflightResponse.output), preflightResponse.allowIgnoreHostPreflights ?? false); } }, [preflightResponse, onComplete]); diff --git a/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx b/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx index cc72c2156..30d01b101 100644 --- a/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx +++ b/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { screen, waitFor, fireEvent } from "@testing-library/react"; import { renderWithProviders } from "../../../test/setup.tsx"; import LinuxPreflightCheck from "../preflight/LinuxPreflightCheck"; @@ -26,12 +25,14 @@ const server = setupServer( return new HttpResponse(null, { status: 401 }); } return HttpResponse.json({ + titles: ["Test"], output: { pass: [{ title: "CPU Check", message: "CPU requirements met" }], warn: [{ title: "Memory Warning", message: "Memory is below recommended" }], fail: [{ title: "Disk Space", message: "Insufficient disk space" }], }, status: { state: "Failed" }, + allowIgnoreHostPreflights: false, }); }), @@ -152,7 +153,7 @@ describe("LinuxPreflightCheck", () => { await waitFor(() => { expect(screen.getByText("Host validation successful!")).toBeInTheDocument(); }); - expect(mockOnComplete).toHaveBeenCalledWith(true); + expect(mockOnComplete).toHaveBeenCalledWith(true, false); // success: true, allowIgnore: false (default) }); it("handles installation status error", async () => { @@ -243,4 +244,82 @@ describe("LinuxPreflightCheck", () => { expect(screen.getByText("Validating host requirements...")).toBeInTheDocument(); }); }); + + it("receives allowIgnoreHostPreflights field in preflight response", async () => { + // Mock preflight status endpoint with allowIgnoreHostPreflights: true + server.use( + http.get("*/api/install/host-preflights/status", ({ request }) => { + const authHeader = request.headers.get("Authorization"); + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return new HttpResponse(null, { status: 401 }); + } + return HttpResponse.json({ + titles: ["Test"], + output: { + pass: [{ title: "CPU Check", message: "CPU requirements met" }], + warn: [], + fail: [{ title: "Disk Space", message: "Insufficient disk space" }], + }, + status: { state: "Failed" }, + allowIgnoreHostPreflights: true, // Test that this field is properly received + }); + }) + ); + + renderWithProviders(, { + wrapperProps: { + preloadedState: { + prototypeSettings: MOCK_PROTOTYPE_SETTINGS, + }, + authToken: TEST_TOKEN, + }, + }); + + await waitFor(() => { + expect(screen.getByText("Host Requirements Not Met")).toBeInTheDocument(); + expect(screen.getByText("Disk Space")).toBeInTheDocument(); + }); + + // The component should call onComplete with BOTH success status AND allowIgnoreHostPreflights flag + expect(mockOnComplete).toHaveBeenCalledWith(false, true); // success: false, allowIgnore: true + }); + + it("passes allowIgnoreHostPreflights false to onComplete callback", async () => { + // Mock preflight status endpoint with allowIgnoreHostPreflights: false + server.use( + http.get("*/api/install/host-preflights/status", ({ request }) => { + const authHeader = request.headers.get("Authorization"); + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return new HttpResponse(null, { status: 401 }); + } + return HttpResponse.json({ + titles: ["Test"], + output: { + pass: [{ title: "CPU Check", message: "CPU requirements met" }], + warn: [], + fail: [{ title: "Disk Space", message: "Insufficient disk space" }], + }, + status: { state: "Failed" }, + allowIgnoreHostPreflights: false, // Test that this field is properly received + }); + }) + ); + + renderWithProviders(, { + wrapperProps: { + preloadedState: { + prototypeSettings: MOCK_PROTOTYPE_SETTINGS, + }, + authToken: TEST_TOKEN, + }, + }); + + await waitFor(() => { + expect(screen.getByText("Host Requirements Not Met")).toBeInTheDocument(); + expect(screen.getByText("Disk Space")).toBeInTheDocument(); + }); + + // The component should call onComplete with success: false, allowIgnore: false + expect(mockOnComplete).toHaveBeenCalledWith(false, false); + }); }); diff --git a/web/src/components/wizard/tests/ValidationStep.test.tsx b/web/src/components/wizard/tests/ValidationStep.test.tsx new file mode 100644 index 000000000..8536eea87 --- /dev/null +++ b/web/src/components/wizard/tests/ValidationStep.test.tsx @@ -0,0 +1,683 @@ +import { describe, it, expect, vi, beforeAll, afterEach, afterAll } from 'vitest'; +import React from 'react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { renderWithProviders } from '../../../test/setup.tsx'; +import ValidationStep from '../ValidationStep'; + +const server = setupServer( + // Mock installation status endpoint + http.get('*/api/install/installation/status', () => { + return HttpResponse.json({ + state: 'Succeeded', + description: 'Installation initialized', + lastUpdated: '2024-01-01T00:00:00Z', + }); + }), + + // Mock start installation endpoint + http.post('*/api/install/infra/setup', () => { + return HttpResponse.json({ success: true }); + }) +); + +describe('ValidationStep', () => { + const mockOnNext = vi.fn(); + const mockOnBack = vi.fn(); + + beforeAll(() => { + server.listen(); + }); + + afterEach(() => { + server.resetHandlers(); + vi.clearAllMocks(); + }); + + afterAll(() => { + server.close(); + }); + + it('enables Start Installation button when allowIgnoreHostPreflights is true and preflights fail', async () => { + // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: true + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Failed' }, + output: { + fail: [ + { title: 'Disk Space', message: 'Not enough disk space available' } + ], + warn: [], + pass: [] + }, + allowIgnoreHostPreflights: true + }); + }) + ); + + renderWithProviders( + , + { + wrapperProps: { + authenticated: true + } + } + ); + + // Wait for preflights to complete and show failures + await waitFor(() => { + expect(screen.getByText('Host Requirements Not Met')).toBeInTheDocument(); + }); + + // Button should be enabled when CLI flag allows ignoring failures + const nextButton = screen.getByText('Next: Start Installation'); + expect(nextButton).not.toBeDisabled(); + }); + + it('disables Start Installation button when allowIgnoreHostPreflights is false and preflights fail', async () => { + // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: false + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Failed' }, + output: { + fail: [ + { title: 'Disk Space', message: 'Not enough disk space available' } + ], + warn: [], + pass: [] + }, + allowIgnoreHostPreflights: false + }); + }) + ); + + renderWithProviders( + , + { + wrapperProps: { + authenticated: true + } + } + ); + + // Wait for preflights to complete and show failures + await waitFor(() => { + expect(screen.getByText('Host Requirements Not Met')).toBeInTheDocument(); + }); + + // Button should be disabled when CLI flag doesn't allow ignoring failures + const nextButton = screen.getByText('Next: Start Installation'); + expect(nextButton).toBeDisabled(); + + // Try to click disabled button - nothing should happen + fireEvent.click(nextButton); + + // No modal should appear and onNext should not be called + expect(screen.queryByText('Proceed with Failed Checks?')).not.toBeInTheDocument(); + expect(mockOnNext).not.toHaveBeenCalled(); + }); + + it('shows modal when Start Installation clicked and allowIgnoreHostPreflights is true and preflights fail', async () => { + // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: true + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Failed' }, + output: { + fail: [ + { title: 'Disk Space', message: 'Not enough disk space available' } + ], + warn: [], + pass: [] + }, + allowIgnoreHostPreflights: true + }); + }) + ); + + renderWithProviders( + , + { + wrapperProps: { + authenticated: true + } + } + ); + + // Wait for preflights to complete and show failures + await waitFor(() => { + expect(screen.getByText('Host Requirements Not Met')).toBeInTheDocument(); + }); + + // Button should be enabled + const nextButton = screen.getByText('Next: Start Installation'); + expect(nextButton).not.toBeDisabled(); + + // Click Next button - should show modal with warning + fireEvent.click(nextButton); + + // Modal SHOULD appear when allowIgnoreHostPreflights is true and preflights fail + await waitFor(() => { + expect(screen.getByText('Proceed with Failed Checks?')).toBeInTheDocument(); + }); + + // User can choose to continue anyway + const continueButton = screen.getByText('Continue Anyway'); + fireEvent.click(continueButton); + + // Should proceed to next step after confirming + await waitFor(() => { + expect(mockOnNext).toHaveBeenCalledTimes(1); + }); + }); + + it('proceeds automatically when allowIgnoreHostPreflights is true and preflights pass', async () => { + // Mock preflight status endpoint - returns success with allowIgnoreHostPreflights: true + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Succeeded' }, + output: { + fail: [], + warn: [], + pass: [ + { title: 'Disk Space', message: 'Sufficient disk space available' } + ] + }, + allowIgnoreHostPreflights: true + }); + }) + ); + + renderWithProviders( + , + { + wrapperProps: { + authenticated: true + } + } + ); + + // Wait for preflights to complete and show success + await waitFor(() => { + expect(screen.getByText('Host validation successful!')).toBeInTheDocument(); + }); + + // Button should be enabled + const nextButton = screen.getByText('Next: Start Installation'); + expect(nextButton).not.toBeDisabled(); + + // Click Next button - should proceed directly without modal (normal success case) + fireEvent.click(nextButton); + + // No modal should appear when preflights pass + expect(screen.queryByText('Proceed with Failed Checks?')).not.toBeInTheDocument(); + + // Should proceed to next step + await waitFor(() => { + expect(mockOnNext).toHaveBeenCalledTimes(1); + }); + }); + + it('proceeds normally when allowIgnoreHostPreflights is false and preflights pass', async () => { + // Mock preflight status endpoint - returns success with allowIgnoreHostPreflights: false + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Succeeded' }, + output: { + fail: [], + warn: [], + pass: [ + { title: 'Disk Space', message: 'Sufficient disk space available' } + ] + }, + allowIgnoreHostPreflights: false + }); + }) + ); + + renderWithProviders( + , + { + wrapperProps: { + authenticated: true + } + } + ); + + // Wait for preflights to complete and show success + await waitFor(() => { + expect(screen.getByText('Host validation successful!')).toBeInTheDocument(); + }); + + // Button should be enabled + const nextButton = screen.getByText('Next: Start Installation'); + expect(nextButton).not.toBeDisabled(); + + // Click Next button - should proceed directly without modal (normal success case) + fireEvent.click(nextButton); + + // No modal should appear when preflights pass + expect(screen.queryByText('Proceed with Failed Checks?')).not.toBeInTheDocument(); + + // Should proceed to next step + await waitFor(() => { + expect(mockOnNext).toHaveBeenCalledTimes(1); + }); + }); + + // Verify ignoreHostPreflights parameter is sent + it('sends ignoreHostPreflights parameter when starting installation with failed preflights', async () => { + // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: true + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Failed' }, + output: { + fail: [{ title: 'Disk Space', message: 'Not enough disk space available' }], + warn: [], + pass: [] + }, + allowIgnoreHostPreflights: true + }); + }), + // Mock infra setup endpoint to capture request body + http.post('*/api/install/infra/setup', async ({ request }) => { + const body = await request.json(); + + // Verify the request includes ignoreHostPreflights parameter + expect(body).toHaveProperty('ignoreHostPreflights', true); + + return HttpResponse.json({ success: true }); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for preflights to complete and show failures + await waitFor(() => { + expect(screen.getByText('Host Requirements Not Met')).toBeInTheDocument(); + }); + + // Click Start Installation button + const nextButton = screen.getByText('Next: Start Installation'); + fireEvent.click(nextButton); + + // Confirm in modal + await waitFor(() => { + expect(screen.getByText('Proceed with Failed Checks?')).toBeInTheDocument(); + }); + + const continueButton = screen.getByText('Continue Anyway'); + fireEvent.click(continueButton); + + // Should proceed to next step + await waitFor(() => { + expect(mockOnNext).toHaveBeenCalledTimes(1); + }); + }); + + it('sends ignoreHostPreflights false when starting installation with passed preflights', async () => { + // Mock preflight status endpoint - returns success + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Succeeded' }, + output: { + fail: [], + warn: [], + pass: [{ title: 'Disk Space', message: 'Sufficient disk space available' }] + }, + allowIgnoreHostPreflights: false + }); + }), + // Mock infra setup endpoint to capture request body + http.post('*/api/install/infra/setup', async ({ request }) => { + const body = await request.json(); + + // Verify the request includes ignoreHostPreflights parameter as false + expect(body).toHaveProperty('ignoreHostPreflights', false); + + return HttpResponse.json({ success: true }); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for preflights to complete and show success + await waitFor(() => { + expect(screen.getByText('Host validation successful!')).toBeInTheDocument(); + }); + + // Click Start Installation button + const nextButton = screen.getByText('Next: Start Installation'); + fireEvent.click(nextButton); + + // Should proceed to next step + await waitFor(() => { + expect(mockOnNext).toHaveBeenCalledTimes(1); + }); + }); +}); + +// Additional robust frontend tests for error handling and edge cases +describe('ValidationStep - Error Handling & Edge Cases', () => { + beforeAll(() => server.listen()); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + const mockOnNext = vi.fn(); + const mockOnBack = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('handles API error responses gracefully when starting installation', async () => { + // Mock preflight status endpoint - returns success + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Succeeded' }, + output: { fail: [], warn: [], pass: [{ title: 'Test', message: 'Pass' }] }, + allowIgnoreHostPreflights: false + }); + }), + // Mock infra setup endpoint to return API error + http.post('*/api/install/infra/setup', () => { + return HttpResponse.json( + { + statusCode: 400, + message: 'Preflight checks failed. Cannot proceed with installation.' + }, + { status: 400 } + ); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for success state + await waitFor(() => { + expect(screen.getByText('Host validation successful!')).toBeInTheDocument(); + }); + + // Click Start Installation + const nextButton = screen.getByText('Next: Start Installation'); + fireEvent.click(nextButton); + + // Should show error message instead of proceeding + await waitFor(() => { + expect(screen.getByText(/Preflight checks failed. Cannot proceed with installation./)).toBeInTheDocument(); + }); + + // Should NOT proceed to next step + expect(mockOnNext).not.toHaveBeenCalled(); + }); + + it('handles network failure during installation start', async () => { + // Mock preflight status endpoint - returns success + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Succeeded' }, + output: { fail: [], warn: [], pass: [{ title: 'Test', message: 'Pass' }] }, + allowIgnoreHostPreflights: false + }); + }), + // Mock infra setup endpoint to return network error + http.post('*/api/install/infra/setup', () => { + return HttpResponse.error(); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for success state + await waitFor(() => { + expect(screen.getByText('Host validation successful!')).toBeInTheDocument(); + }); + + // Click Start Installation + const nextButton = screen.getByText('Next: Start Installation'); + fireEvent.click(nextButton); + + // Should show network error message (matches actual fetch error) + await waitFor(() => { + expect(screen.getByText(/Failed to fetch/)).toBeInTheDocument(); + }); + + // Should NOT proceed to next step + expect(mockOnNext).not.toHaveBeenCalled(); + }); + + it('handles button states during API interactions', async () => { + // Mock preflight status endpoint - returns success + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Succeeded' }, + output: { fail: [], warn: [], pass: [{ title: 'Test', message: 'Pass' }] }, + allowIgnoreHostPreflights: false + }); + }), + // Mock successful infra setup + http.post('*/api/install/infra/setup', () => { + return HttpResponse.json({ success: true }); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for success state + await waitFor(() => { + expect(screen.getByText('Host validation successful!')).toBeInTheDocument(); + }); + + // Button should be enabled initially + const nextButton = screen.getByText('Next: Start Installation'); + expect(nextButton).not.toBeDisabled(); + + // Click Start Installation - should succeed + fireEvent.click(nextButton); + + // Should proceed to next step + await waitFor(() => { + expect(mockOnNext).toHaveBeenCalledTimes(1); + }); + }); + + it('handles error when ignoring preflights without CLI flag', async () => { + // Mock preflight status endpoint - returns failures + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Failed' }, + output: { + fail: [{ title: 'Disk Space', message: 'Not enough disk space' }], + warn: [], + pass: [] + }, + allowIgnoreHostPreflights: true + }); + }), + // Mock infra setup endpoint to return CLI flag error + http.post('*/api/install/infra/setup', () => { + return HttpResponse.json( + { + statusCode: 400, + message: 'preflight checks failed' + }, + { status: 400 } + ); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for failed state + await waitFor(() => { + expect(screen.getByText('Host Requirements Not Met')).toBeInTheDocument(); + }); + + // Click Start Installation button (should show modal) + const nextButton = screen.getByText('Next: Start Installation'); + fireEvent.click(nextButton); + + // Confirm in modal + await waitFor(() => { + expect(screen.getByText('Proceed with Failed Checks?')).toBeInTheDocument(); + }); + + const continueButton = screen.getByText('Continue Anyway'); + fireEvent.click(continueButton); + + // Should show the specific API error message + await waitFor(() => { + expect(screen.getByText(/preflight checks failed/)).toBeInTheDocument(); + }); + + // Should NOT proceed to next step + expect(mockOnNext).not.toHaveBeenCalled(); + }); + + it('clears previous errors when new installation attempt succeeds', async () => { + let shouldFail = true; + + // Mock preflight status endpoint - returns success + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Succeeded' }, + output: { fail: [], warn: [], pass: [{ title: 'Test', message: 'Pass' }] }, + allowIgnoreHostPreflights: false + }); + }), + // Mock infra setup endpoint that fails first, succeeds second + http.post('*/api/install/infra/setup', () => { + if (shouldFail) { + shouldFail = false; // Succeed on next attempt + return HttpResponse.json( + { statusCode: 500, message: 'Internal server error' }, + { status: 500 } + ); + } + return HttpResponse.json({ success: true }); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for success state + await waitFor(() => { + expect(screen.getByText('Host validation successful!')).toBeInTheDocument(); + }); + + // First attempt - should fail + const nextButton = screen.getByText('Next: Start Installation'); + fireEvent.click(nextButton); + + await waitFor(() => { + expect(screen.getByText(/Internal server error/)).toBeInTheDocument(); + }); + + // Second attempt - should succeed and clear error + fireEvent.click(nextButton); + + await waitFor(() => { + expect(mockOnNext).toHaveBeenCalledTimes(1); + }); + + // Error message should be gone + expect(screen.queryByText(/Internal server error/)).not.toBeInTheDocument(); + }); + + it('properly handles modal cancellation flow', async () => { + // Mock preflight status endpoint - returns failures + server.use( + http.get('*/api/install/host-preflights/status', () => { + return HttpResponse.json({ + titles: ['Host Check'], + status: { state: 'Failed' }, + output: { + fail: [{ title: 'Disk Space', message: 'Not enough disk space' }], + warn: [], + pass: [] + }, + allowIgnoreHostPreflights: true + }); + }) + ); + + renderWithProviders( + , + { wrapperProps: { authenticated: true } } + ); + + // Wait for failed state + await waitFor(() => { + expect(screen.getByText('Host Requirements Not Met')).toBeInTheDocument(); + }); + + // Click Start Installation button (should show modal) + const nextButton = screen.getByText('Next: Start Installation'); + fireEvent.click(nextButton); + + // Modal should appear + await waitFor(() => { + expect(screen.getByText('Proceed with Failed Checks?')).toBeInTheDocument(); + }); + + // Cancel the modal + const cancelButton = screen.getByText('Cancel'); + fireEvent.click(cancelButton); + + // Modal should disappear + await waitFor(() => { + expect(screen.queryByText('Proceed with Failed Checks?')).not.toBeInTheDocument(); + }); + + // Should NOT proceed to next step + expect(mockOnNext).not.toHaveBeenCalled(); + + // Button should still be available for another attempt + expect(screen.getByText('Next: Start Installation')).toBeInTheDocument(); + }); +}); \ No newline at end of file From c34c2bf4efff3901bc17d7cd7f140f389f09acd6 Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Tue, 24 Jun 2025 08:56:39 -0700 Subject: [PATCH 27/48] Bootstrap API Support for Kubernetes and Linux Installation Targets (#2365) * Bootstrap API Support for Kubernetes and Linux Installation Targets * swagger * fix swagger spacing * f * f * feedback * f * f --- api/api.go | 235 ++-------------- api/auth.go | 76 ----- api/client/client_test.go | 10 +- api/client/install.go | 12 +- api/console.go | 28 -- api/controllers/console/controller.go | 10 +- .../{ => linux}/install/controller.go | 0 .../{ => linux}/install/controller_test.go | 0 .../{ => linux}/install/hostpreflight.go | 0 api/controllers/{ => linux}/install/infra.go | 0 .../{ => linux}/install/installation.go | 0 .../{ => linux}/install/statemachine.go | 0 .../{ => linux}/install/statemachine_test.go | 0 api/controllers/{ => linux}/install/status.go | 0 api/docs/docs.go | 4 +- api/docs/swagger.json | 4 +- api/docs/swagger.yaml | 67 +++-- api/handlers.go | 64 +++++ api/health.go | 22 -- api/install.go | 245 ---------------- api/integration/auth_controller_test.go | 30 +- api/integration/console_test.go | 12 +- api/integration/hostpreflights_test.go | 138 +++++---- api/integration/install_test.go | 263 ++++++++++-------- api/internal/handlers/auth/auth.go | 85 ++++++ api/internal/handlers/auth/login.go | 47 ++++ api/internal/handlers/console/console.go | 81 ++++++ api/internal/handlers/health/health.go | 52 ++++ api/internal/handlers/linux/install.go | 253 +++++++++++++++++ api/internal/handlers/linux/linux.go | 90 ++++++ api/internal/handlers/utils/utils.go | 62 +++++ .../handlers/utils/utils_test.go} | 7 +- api/routes.go | 60 ++++ api/types/api.go | 20 ++ api/types/responses.go | 5 + cmd/installer/cli/api.go | 58 ++-- cmd/installer/cli/api_test.go | 2 +- cmd/installer/cli/install.go | 2 +- .../components/wizard/InstallationStep.tsx | 2 +- web/src/components/wizard/SetupStep.tsx | 4 +- web/src/components/wizard/ValidationStep.tsx | 2 +- .../wizard/preflight/LinuxPreflightCheck.tsx | 6 +- .../wizard/tests/InstallationStep.test.tsx | 10 +- .../wizard/tests/LinuxPreflightCheck.test.tsx | 22 +- .../wizard/tests/SetupStep.test.tsx | 10 +- .../wizard/tests/ValidationStep.test.tsx | 44 +-- web/src/contexts/AuthContext.tsx | 2 +- 47 files changed, 1250 insertions(+), 896 deletions(-) delete mode 100644 api/auth.go delete mode 100644 api/console.go rename api/controllers/{ => linux}/install/controller.go (100%) rename api/controllers/{ => linux}/install/controller_test.go (100%) rename api/controllers/{ => linux}/install/hostpreflight.go (100%) rename api/controllers/{ => linux}/install/infra.go (100%) rename api/controllers/{ => linux}/install/installation.go (100%) rename api/controllers/{ => linux}/install/statemachine.go (100%) rename api/controllers/{ => linux}/install/statemachine_test.go (100%) rename api/controllers/{ => linux}/install/status.go (100%) create mode 100644 api/handlers.go delete mode 100644 api/health.go delete mode 100644 api/install.go create mode 100644 api/internal/handlers/auth/auth.go create mode 100644 api/internal/handlers/auth/login.go create mode 100644 api/internal/handlers/console/console.go create mode 100644 api/internal/handlers/health/health.go create mode 100644 api/internal/handlers/linux/install.go create mode 100644 api/internal/handlers/linux/linux.go create mode 100644 api/internal/handlers/utils/utils.go rename api/{api_test.go => internal/handlers/utils/utils_test.go} (94%) create mode 100644 api/routes.go create mode 100644 api/types/api.go diff --git a/api/api.go b/api/api.go index f483bc431..2b594b4ed 100644 --- a/api/api.go +++ b/api/api.go @@ -1,26 +1,16 @@ package api import ( - "encoding/json" - "errors" "fmt" - "net/http" - "strings" - "github.com/gorilla/mux" "github.com/replicatedhq/embedded-cluster/api/controllers/auth" "github.com/replicatedhq/embedded-cluster/api/controllers/console" - "github.com/replicatedhq/embedded-cluster/api/controllers/install" - "github.com/replicatedhq/embedded-cluster/api/docs" + linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" - "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" - httpSwagger "github.com/swaggo/http-swagger/v2" ) // @title Embedded Cluster API @@ -42,20 +32,16 @@ import ( // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ type API struct { - authController auth.Controller - consoleController console.Controller - installController install.Controller - rc runtimeconfig.RuntimeConfig - releaseData *release.ReleaseData - tlsConfig types.TLSConfig - license []byte - airgapBundle string - configValues string - endUserConfig *ecv1beta1.Config - allowIgnoreHostPreflights bool - logger logrus.FieldLogger - hostUtils hostutils.HostUtilsInterface - metricsReporter metrics.ReporterInterface + cfg types.APIConfig + + logger logrus.FieldLogger + metricsReporter metrics.ReporterInterface + + authController auth.Controller + consoleController console.Controller + linuxInstallController linuxinstall.Controller + + handlers Handlers } type APIOption func(*API) @@ -72,15 +58,9 @@ func WithConsoleController(consoleController console.Controller) APIOption { } } -func WithInstallController(installController install.Controller) APIOption { - return func(a *API) { - a.installController = installController - } -} - -func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) APIOption { +func WithLinuxInstallController(linuxInstallController linuxinstall.Controller) APIOption { return func(a *API) { - a.rc = rc + a.linuxInstallController = linuxInstallController } } @@ -90,69 +70,23 @@ func WithLogger(logger logrus.FieldLogger) APIOption { } } -func WithHostUtils(hostUtils hostutils.HostUtilsInterface) APIOption { - return func(a *API) { - a.hostUtils = hostUtils - } -} - func WithMetricsReporter(metricsReporter metrics.ReporterInterface) APIOption { return func(a *API) { a.metricsReporter = metricsReporter } } -func WithReleaseData(releaseData *release.ReleaseData) APIOption { - return func(a *API) { - a.releaseData = releaseData - } -} - -func WithTLSConfig(tlsConfig types.TLSConfig) APIOption { - return func(a *API) { - a.tlsConfig = tlsConfig - } -} - -func WithLicense(license []byte) APIOption { - return func(a *API) { - a.license = license - } -} - -func WithAirgapBundle(airgapBundle string) APIOption { - return func(a *API) { - a.airgapBundle = airgapBundle - } -} - -func WithConfigValues(configValues string) APIOption { - return func(a *API) { - a.configValues = configValues - } -} - -func WithEndUserConfig(endUserConfig *ecv1beta1.Config) APIOption { - return func(a *API) { - a.endUserConfig = endUserConfig - } -} - -func WithAllowIgnoreHostPreflights(allowIgnoreHostPreflights bool) APIOption { - return func(a *API) { - a.allowIgnoreHostPreflights = allowIgnoreHostPreflights +func New(cfg types.APIConfig, opts ...APIOption) (*API, error) { + api := &API{ + cfg: cfg, } -} - -func New(password string, opts ...APIOption) (*API, error) { - api := &API{} for _, opt := range opts { opt(api) } - if api.rc == nil { - api.rc = runtimeconfig.New(nil) + if api.cfg.RuntimeConfig == nil { + api.cfg.RuntimeConfig = runtimeconfig.New(nil) } if api.logger == nil { @@ -163,138 +97,9 @@ func New(password string, opts ...APIOption) (*API, error) { api.logger = l } - if api.hostUtils == nil { - api.hostUtils = hostutils.New( - hostutils.WithLogger(api.logger), - ) - } - - if api.authController == nil { - authController, err := auth.NewAuthController(password) - if err != nil { - return nil, fmt.Errorf("new auth controller: %w", err) - } - api.authController = authController - } - - if api.consoleController == nil { - consoleController, err := console.NewConsoleController() - if err != nil { - return nil, fmt.Errorf("new console controller: %w", err) - } - api.consoleController = consoleController - } - - // TODO (@team): discuss which of these should / should not be pointers - if api.installController == nil { - installController, err := install.NewInstallController( - install.WithRuntimeConfig(api.rc), - install.WithLogger(api.logger), - install.WithHostUtils(api.hostUtils), - install.WithMetricsReporter(api.metricsReporter), - install.WithReleaseData(api.releaseData), - install.WithPassword(password), - install.WithTLSConfig(api.tlsConfig), - install.WithLicense(api.license), - install.WithAirgapBundle(api.airgapBundle), - install.WithConfigValues(api.configValues), - install.WithEndUserConfig(api.endUserConfig), - install.WithAllowIgnoreHostPreflights(api.allowIgnoreHostPreflights), - ) - if err != nil { - return nil, fmt.Errorf("new install controller: %w", err) - } - api.installController = installController + if err := api.InitHandlers(api.cfg); err != nil { + return nil, fmt.Errorf("init handlers: %w", err) } return api, nil } - -func (a *API) RegisterRoutes(router *mux.Router) { - router.HandleFunc("/health", a.getHealth).Methods("GET") - - // Hack to fix issue - // https://github.com/swaggo/swag/issues/1588#issuecomment-2797801240 - router.HandleFunc("/swagger/doc.json", func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write([]byte(docs.SwaggerInfo.ReadDoc())) - }).Methods("GET") - router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) - - router.HandleFunc("/auth/login", a.postAuthLogin).Methods("POST") - - authenticatedRouter := router.PathPrefix("/").Subrouter() - authenticatedRouter.Use(a.authMiddleware) - - installRouter := authenticatedRouter.PathPrefix("/install").Subrouter() - installRouter.HandleFunc("/installation/config", a.getInstallInstallationConfig).Methods("GET") - installRouter.HandleFunc("/installation/configure", a.postInstallConfigureInstallation).Methods("POST") - installRouter.HandleFunc("/installation/status", a.getInstallInstallationStatus).Methods("GET") - - installRouter.HandleFunc("/host-preflights/run", a.postInstallRunHostPreflights).Methods("POST") - installRouter.HandleFunc("/host-preflights/status", a.getInstallHostPreflightsStatus).Methods("GET") - - installRouter.HandleFunc("/infra/setup", a.postInstallSetupInfra).Methods("POST") - installRouter.HandleFunc("/infra/status", a.getInstallInfraStatus).Methods("GET") - - // TODO (@salah): remove this once the cli isn't responsible for setting the install status - // and the ui isn't polling for it to know if the entire install is complete - installRouter.HandleFunc("/status", a.getInstallStatus).Methods("GET") - installRouter.HandleFunc("/status", a.setInstallStatus).Methods("POST") - - consoleRouter := authenticatedRouter.PathPrefix("/console").Subrouter() - consoleRouter.HandleFunc("/available-network-interfaces", a.getListAvailableNetworkInterfaces).Methods("GET") -} - -func (a *API) bindJSON(w http.ResponseWriter, r *http.Request, v any) error { - if err := json.NewDecoder(r.Body).Decode(v); err != nil { - a.logError(r, err, fmt.Sprintf("failed to decode %s %s request", strings.ToLower(r.Method), r.URL.Path)) - a.jsonError(w, r, types.NewBadRequestError(err)) - return err - } - - return nil -} - -func (a *API) json(w http.ResponseWriter, r *http.Request, code int, payload any) { - response, err := json.Marshal(payload) - if err != nil { - a.logError(r, err, "failed to encode response") - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - _, _ = w.Write(response) -} - -func (a *API) jsonError(w http.ResponseWriter, r *http.Request, err error) { - var apiErr *types.APIError - if !errors.As(err, &apiErr) { - apiErr = types.NewInternalServerError(err) - } - - response, err := json.Marshal(apiErr) - if err != nil { - a.logError(r, err, "failed to encode response") - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(apiErr.StatusCode) - _, _ = w.Write(response) -} - -func (a *API) logError(r *http.Request, err error, args ...any) { - a.logger.WithFields(logrusFieldsFromRequest(r)).WithError(err).Error(args...) -} - -func logrusFieldsFromRequest(r *http.Request) logrus.Fields { - return logrus.Fields{ - "method": r.Method, - "path": r.URL.Path, - } -} diff --git a/api/auth.go b/api/auth.go deleted file mode 100644 index dec938a32..000000000 --- a/api/auth.go +++ /dev/null @@ -1,76 +0,0 @@ -package api - -import ( - "errors" - "net/http" - "strings" - - "github.com/replicatedhq/embedded-cluster/api/controllers/auth" - "github.com/replicatedhq/embedded-cluster/api/types" -) - -func (a *API) authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("Authorization") - if token == "" { - err := errors.New("authorization header is required") - a.logError(r, err, "failed to authenticate") - a.jsonError(w, r, types.NewUnauthorizedError(err)) - return - } - - if !strings.HasPrefix(token, "Bearer ") { - err := errors.New("authorization header must start with Bearer ") - a.logError(r, err, "failed to authenticate") - a.jsonError(w, r, types.NewUnauthorizedError(err)) - return - } - - token = token[len("Bearer "):] - - err := a.authController.ValidateToken(r.Context(), token) - if err != nil { - a.logError(r, err, "failed to validate token") - a.jsonError(w, r, types.NewUnauthorizedError(err)) - return - } - - next.ServeHTTP(w, r) - }) -} - -// postAuthLogin handler to authenticate a user -// -// @Summary Authenticate a user -// @Description Authenticate a user -// @Tags auth -// @Accept json -// @Produce json -// @Param request body types.AuthRequest true "Auth Request" -// @Success 200 {object} types.AuthResponse -// @Failure 401 {object} types.APIError -// @Router /auth/login [post] -func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) { - var request types.AuthRequest - if err := a.bindJSON(w, r, &request); err != nil { - return - } - - token, err := a.authController.Authenticate(r.Context(), request.Password) - if errors.Is(err, auth.ErrInvalidPassword) { - a.jsonError(w, r, types.NewUnauthorizedError(err)) - return - } - - if err != nil { - a.logError(r, err, "failed to authenticate") - a.jsonError(w, r, types.NewInternalServerError(err)) - return - } - - response := types.AuthResponse{ - Token: token, - } - - a.json(w, r, http.StatusOK, response) -} diff --git a/api/client/client_test.go b/api/client/client_test.go index 696c03b22..84f8df824 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -103,7 +103,7 @@ func TestGetInstallationConfig(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) - assert.Equal(t, "/api/install/installation/config", r.URL.Path) + assert.Equal(t, "/api/linux/install/installation/config", r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) @@ -150,7 +150,7 @@ func TestConfigureInstallation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method and path assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/api/install/installation/configure", r.URL.Path) + assert.Equal(t, "/api/linux/install/installation/configure", r.URL.Path) // Check headers assert.Equal(t, "application/json", r.Header.Get("Content-Type")) @@ -206,7 +206,7 @@ func TestSetupInfra(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/api/install/infra/setup", r.URL.Path) + assert.Equal(t, "/api/linux/install/infra/setup", r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) @@ -254,7 +254,7 @@ func TestGetInfraStatus(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) - assert.Equal(t, "/api/install/infra/status", r.URL.Path) + assert.Equal(t, "/api/linux/install/infra/status", r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) @@ -302,7 +302,7 @@ func TestSetInstallStatus(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) - assert.Equal(t, "/api/install/status", r.URL.Path) + assert.Equal(t, "/api/linux/install/status", r.URL.Path) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) diff --git a/api/client/install.go b/api/client/install.go index d9a036c96..30f1b7b9d 100644 --- a/api/client/install.go +++ b/api/client/install.go @@ -9,7 +9,7 @@ import ( ) func (c *client) GetInstallationConfig() (types.InstallationConfig, error) { - req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/config", nil) + req, err := http.NewRequest("GET", c.apiURL+"/api/linux/install/installation/config", nil) if err != nil { return types.InstallationConfig{}, err } @@ -41,7 +41,7 @@ func (c *client) ConfigureInstallation(config types.InstallationConfig) (types.S return types.Status{}, err } - req, err := http.NewRequest("POST", c.apiURL+"/api/install/installation/configure", bytes.NewBuffer(b)) + req, err := http.NewRequest("POST", c.apiURL+"/api/linux/install/installation/configure", bytes.NewBuffer(b)) if err != nil { return types.Status{}, err } @@ -68,7 +68,7 @@ func (c *client) ConfigureInstallation(config types.InstallationConfig) (types.S } func (c *client) GetInstallationStatus() (types.Status, error) { - req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/status", nil) + req, err := http.NewRequest("GET", c.apiURL+"/api/linux/install/installation/status", nil) if err != nil { return types.Status{}, err } @@ -95,7 +95,7 @@ func (c *client) GetInstallationStatus() (types.Status, error) { } func (c *client) SetupInfra() (types.Infra, error) { - req, err := http.NewRequest("POST", c.apiURL+"/api/install/infra/setup", nil) + req, err := http.NewRequest("POST", c.apiURL+"/api/linux/install/infra/setup", nil) if err != nil { return types.Infra{}, err } @@ -122,7 +122,7 @@ func (c *client) SetupInfra() (types.Infra, error) { } func (c *client) GetInfraStatus() (types.Infra, error) { - req, err := http.NewRequest("GET", c.apiURL+"/api/install/infra/status", nil) + req, err := http.NewRequest("GET", c.apiURL+"/api/linux/install/infra/status", nil) if err != nil { return types.Infra{}, err } @@ -154,7 +154,7 @@ func (c *client) SetInstallStatus(s types.Status) (types.Status, error) { return types.Status{}, err } - req, err := http.NewRequest("POST", c.apiURL+"/api/install/status", bytes.NewBuffer(b)) + req, err := http.NewRequest("POST", c.apiURL+"/api/linux/install/status", bytes.NewBuffer(b)) if err != nil { return types.Status{}, err } diff --git a/api/console.go b/api/console.go deleted file mode 100644 index 3e7c3a2bc..000000000 --- a/api/console.go +++ /dev/null @@ -1,28 +0,0 @@ -package api - -import ( - "net/http" -) - -type getListAvailableNetworkInterfacesResponse struct { - NetworkInterfaces []string `json:"networkInterfaces"` -} - -func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.Request) { - interfaces, err := a.consoleController.ListAvailableNetworkInterfaces() - if err != nil { - a.logError(r, err, "failed to list available network interfaces") - a.jsonError(w, r, err) - return - } - - a.logger.WithFields(logrusFieldsFromRequest(r)). - WithField("interfaces", interfaces). - Info("got available network interfaces") - - response := getListAvailableNetworkInterfacesResponse{ - NetworkInterfaces: interfaces, - } - - a.json(w, r, http.StatusOK, response) -} diff --git a/api/controllers/console/controller.go b/api/controllers/console/controller.go index 66ede9f4e..d1eb315de 100644 --- a/api/controllers/console/controller.go +++ b/api/controllers/console/controller.go @@ -11,14 +11,14 @@ type Controller interface { var _ Controller = (*ConsoleController)(nil) type ConsoleController struct { - utils.NetUtils + netUtils utils.NetUtils } type ConsoleControllerOption func(*ConsoleController) func WithNetUtils(netUtils utils.NetUtils) ConsoleControllerOption { return func(c *ConsoleController) { - c.NetUtils = netUtils + c.netUtils = netUtils } } @@ -29,13 +29,13 @@ func NewConsoleController(opts ...ConsoleControllerOption) (*ConsoleController, opt(controller) } - if controller.NetUtils == nil { - controller.NetUtils = utils.NewNetUtils() + if controller.netUtils == nil { + controller.netUtils = utils.NewNetUtils() } return controller, nil } func (c *ConsoleController) ListAvailableNetworkInterfaces() ([]string, error) { - return c.NetUtils.ListValidNetworkInterfaces() + return c.netUtils.ListValidNetworkInterfaces() } diff --git a/api/controllers/install/controller.go b/api/controllers/linux/install/controller.go similarity index 100% rename from api/controllers/install/controller.go rename to api/controllers/linux/install/controller.go diff --git a/api/controllers/install/controller_test.go b/api/controllers/linux/install/controller_test.go similarity index 100% rename from api/controllers/install/controller_test.go rename to api/controllers/linux/install/controller_test.go diff --git a/api/controllers/install/hostpreflight.go b/api/controllers/linux/install/hostpreflight.go similarity index 100% rename from api/controllers/install/hostpreflight.go rename to api/controllers/linux/install/hostpreflight.go diff --git a/api/controllers/install/infra.go b/api/controllers/linux/install/infra.go similarity index 100% rename from api/controllers/install/infra.go rename to api/controllers/linux/install/infra.go diff --git a/api/controllers/install/installation.go b/api/controllers/linux/install/installation.go similarity index 100% rename from api/controllers/install/installation.go rename to api/controllers/linux/install/installation.go diff --git a/api/controllers/install/statemachine.go b/api/controllers/linux/install/statemachine.go similarity index 100% rename from api/controllers/install/statemachine.go rename to api/controllers/linux/install/statemachine.go diff --git a/api/controllers/install/statemachine_test.go b/api/controllers/linux/install/statemachine_test.go similarity index 100% rename from api/controllers/install/statemachine_test.go rename to api/controllers/linux/install/statemachine_test.go diff --git a/api/controllers/install/status.go b/api/controllers/linux/install/status.go similarity index 100% rename from api/controllers/install/status.go rename to api/controllers/linux/install/status.go diff --git a/api/docs/docs.go b/api/docs/docs.go index edc316d4d..efc372c7d 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -6,10 +6,10 @@ import "github.com/swaggo/swag/v2" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, - "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.GetListAvailableNetworkInterfacesResponse":{"properties":{"networkInterfaces":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"{{escape .Description}}","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"{{.Title}}","version":"{{.Version}}"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, - "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","operationId":"postAuthLogin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/console/available-network-interfaces":{"get":{"description":"List available network interfaces","operationId":"getConsoleListAvailableNetworkInterfaces","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.GetListAvailableNetworkInterfacesResponse"}}},"description":"OK"}},"summary":"List available network interfaces","tags":["console"]}},"/health":{"get":{"description":"get the health of the API","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/linux/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","operationId":"postLinuxInstallRunHostPreflights","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["linux-install"]}},"/linux/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","operationId":"getLinuxInstallHostPreflightsStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["linux-install"]}},"/linux/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postLinuxInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["linux-install"]}},"/linux/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getLinuxInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["linux-install"]}},"/linux/install/installation/config":{"get":{"description":"get the installation config","operationId":"getLinuxInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["linux-install"]}},"/linux/install/installation/configure":{"post":{"description":"configure the installation for install","operationId":"postLinuxInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["linux-install"]}},"/linux/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getLinuxInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["linux-install"]}},"/linux/install/status":{"get":{"description":"Get the current status of the install workflow","operationId":"getLinuxInstallStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["linux-install"]},"post":{"description":"Set the status of the install workflow","operationId":"postLinuxInstallSetStatus","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["linux-install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 82e788854..ff60253c1 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1,8 +1,8 @@ { - "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.GetListAvailableNetworkInterfacesResponse":{"properties":{"networkInterfaces":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightsOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightsRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightsRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.Infra":{"properties":{"components":{"items":{"$ref":"#/components/schemas/types.InfraComponent"},"type":"array","uniqueItems":false},"logs":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraComponent":{"properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/types.Status"}},"type":"object"},"types.InfraSetupRequest":{"properties":{"ignoreHostPreflights":{"type":"boolean"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"allowIgnoreHostPreflights":{"type":"boolean"},"output":{"$ref":"#/components/schemas/types.HostPreflightsOutput"},"status":{"$ref":"#/components/schemas/types.Status"},"titles":{"items":{"type":"string"},"type":"array","uniqueItems":false}},"type":"object"},"types.InstallationConfig":{"properties":{"adminConsolePort":{"type":"integer"},"dataDirectory":{"type":"string"},"globalCidr":{"type":"string"},"httpProxy":{"type":"string"},"httpsProxy":{"type":"string"},"localArtifactMirrorPort":{"type":"integer"},"networkInterface":{"type":"string"},"noProxy":{"type":"string"},"podCidr":{"type":"string"},"serviceCidr":{"type":"string"}},"type":"object"},"types.PostInstallRunHostPreflightsRequest":{"properties":{"isUi":{"type":"boolean"}},"type":"object"},"types.State":{"type":"string","x-enum-varnames":["StatePending","StateRunning","StateSucceeded","StateFailed"]},"types.Status":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.State"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, "info": {"contact":{"email":"support@replicated.com","name":"API Support","url":"https://github.com/replicatedhq/embedded-cluster/issues"},"description":"This is the API for the Embedded Cluster project.","license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"},"termsOfService":"http://swagger.io/terms/","title":"Embedded Cluster API","version":"0.1"}, "externalDocs": {"description":"OpenAPI","url":"https://swagger.io/resources/open-api/"}, - "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["install"]}},"/install/infra/setup":{"post":{"description":"Setup infra components","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["install"]}},"/install/infra/status":{"get":{"description":"Get the current status of the infra","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["install"]}},"/install/installation/config":{"get":{"description":"get the installation config","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["install"]}},"/install/installation/configure":{"post":{"description":"configure the installation for install","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["install"]}},"/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","operationId":"postAuthLogin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/console/available-network-interfaces":{"get":{"description":"List available network interfaces","operationId":"getConsoleListAvailableNetworkInterfaces","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.GetListAvailableNetworkInterfacesResponse"}}},"description":"OK"}},"summary":"List available network interfaces","tags":["console"]}},"/health":{"get":{"description":"get the health of the API","operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/linux/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","operationId":"postLinuxInstallRunHostPreflights","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.PostInstallRunHostPreflightsRequest"}}},"description":"Post Install Run Host Preflights Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["linux-install"]}},"/linux/install/host-preflights/status":{"get":{"description":"Get the current status and results of host preflight checks for install","operationId":"getLinuxInstallHostPreflightsStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallHostPreflightsStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status for install","tags":["linux-install"]}},"/linux/install/infra/setup":{"post":{"description":"Setup infra components","operationId":"postLinuxInstallSetupInfra","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InfraSetupRequest"}}},"description":"Infra Setup Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup infra components","tags":["linux-install"]}},"/linux/install/infra/status":{"get":{"description":"Get the current status of the infra","operationId":"getLinuxInstallInfraStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Infra"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the infra","tags":["linux-install"]}},"/linux/install/installation/config":{"get":{"description":"get the installation config","operationId":"getLinuxInstallInstallationConfig","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation config","tags":["linux-install"]}},"/linux/install/installation/configure":{"post":{"description":"configure the installation for install","operationId":"postLinuxInstallConfigureInstallation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationConfig"}}},"description":"Installation config","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Configure the installation for install","tags":["linux-install"]}},"/linux/install/installation/status":{"get":{"description":"Get the current status of the installation configuration for install","operationId":"getLinuxInstallInstallationStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get installation configuration status for install","tags":["linux-install"]}},"/linux/install/status":{"get":{"description":"Get the current status of the install workflow","operationId":"getLinuxInstallStatus","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["linux-install"]},"post":{"description":"Set the status of the install workflow","operationId":"postLinuxInstallSetStatus","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["linux-install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index df73c34e6..f2b37d253 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -24,6 +24,14 @@ components: token: type: string type: object + types.GetListAvailableNetworkInterfacesResponse: + properties: + networkInterfaces: + items: + type: string + type: array + uniqueItems: false + type: object types.Health: properties: status: @@ -161,6 +169,7 @@ paths: /auth/login: post: description: Authenticate a user + operationId: postAuthLogin requestBody: content: application/json: @@ -184,9 +193,24 @@ paths: summary: Authenticate a user tags: - auth + /console/available-network-interfaces: + get: + description: List available network interfaces + operationId: getConsoleListAvailableNetworkInterfaces + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/types.GetListAvailableNetworkInterfacesResponse' + description: OK + summary: List available network interfaces + tags: + - console /health: get: description: get the health of the API + operationId: getHealth responses: "200": content: @@ -197,10 +221,11 @@ paths: summary: Get the health of the API tags: - health - /install/host-preflights/run: + /linux/install/host-preflights/run: post: description: Run install host preflight checks using installation config and client-provided data + operationId: postLinuxInstallRunHostPreflights requestBody: content: application/json: @@ -219,11 +244,12 @@ paths: - bearerauth: [] summary: Run install host preflight checks tags: - - install - /install/host-preflights/status: + - linux-install + /linux/install/host-preflights/status: get: description: Get the current status and results of host preflight checks for install + operationId: getLinuxInstallHostPreflightsStatus responses: "200": content: @@ -235,10 +261,11 @@ paths: - bearerauth: [] summary: Get host preflight status for install tags: - - install - /install/infra/setup: + - linux-install + /linux/install/infra/setup: post: description: Setup infra components + operationId: postLinuxInstallSetupInfra requestBody: content: application/json: @@ -257,10 +284,11 @@ paths: - bearerauth: [] summary: Setup infra components tags: - - install - /install/infra/status: + - linux-install + /linux/install/infra/status: get: description: Get the current status of the infra + operationId: getLinuxInstallInfraStatus responses: "200": content: @@ -272,10 +300,11 @@ paths: - bearerauth: [] summary: Get the status of the infra tags: - - install - /install/installation/config: + - linux-install + /linux/install/installation/config: get: description: get the installation config + operationId: getLinuxInstallInstallationConfig responses: "200": content: @@ -287,10 +316,11 @@ paths: - bearerauth: [] summary: Get the installation config tags: - - install - /install/installation/configure: + - linux-install + /linux/install/installation/configure: post: description: configure the installation for install + operationId: postLinuxInstallConfigureInstallation requestBody: content: application/json: @@ -309,10 +339,11 @@ paths: - bearerauth: [] summary: Configure the installation for install tags: - - install - /install/installation/status: + - linux-install + /linux/install/installation/status: get: description: Get the current status of the installation configuration for install + operationId: getLinuxInstallInstallationStatus responses: "200": content: @@ -324,10 +355,11 @@ paths: - bearerauth: [] summary: Get installation configuration status for install tags: - - install - /install/status: + - linux-install + /linux/install/status: get: description: Get the current status of the install workflow + operationId: getLinuxInstallStatus responses: "200": content: @@ -339,9 +371,10 @@ paths: - bearerauth: [] summary: Get the status of the install workflow tags: - - install + - linux-install post: description: Set the status of the install workflow + operationId: postLinuxInstallSetStatus requestBody: content: application/json: @@ -360,6 +393,6 @@ paths: - bearerauth: [] summary: Set the status of the install workflow tags: - - install + - linux-install servers: - url: /api diff --git a/api/handlers.go b/api/handlers.go new file mode 100644 index 000000000..cfc7e8fd5 --- /dev/null +++ b/api/handlers.go @@ -0,0 +1,64 @@ +package api + +import ( + "fmt" + + authhandler "github.com/replicatedhq/embedded-cluster/api/internal/handlers/auth" + consolehandler "github.com/replicatedhq/embedded-cluster/api/internal/handlers/console" + healthhandler "github.com/replicatedhq/embedded-cluster/api/internal/handlers/health" + linuxhandler "github.com/replicatedhq/embedded-cluster/api/internal/handlers/linux" + "github.com/replicatedhq/embedded-cluster/api/types" +) + +type Handlers struct { + auth *authhandler.Handler + console *consolehandler.Handler + health *healthhandler.Handler + linux *linuxhandler.Handler +} + +func (a *API) InitHandlers(cfg types.APIConfig) error { + // Auth handler + authHandler, err := authhandler.New( + cfg.Password, + authhandler.WithLogger(a.logger), + authhandler.WithAuthController(a.authController), + ) + if err != nil { + return fmt.Errorf("new auth handler: %w", err) + } + a.handlers.auth = authHandler + + // Console handler + consoleHandler, err := consolehandler.New( + consolehandler.WithLogger(a.logger), + consolehandler.WithConsoleController(a.consoleController), + ) + if err != nil { + return fmt.Errorf("new console handler: %w", err) + } + a.handlers.console = consoleHandler + + // Health handler + healthHandler, err := healthhandler.New( + healthhandler.WithLogger(a.logger), + ) + if err != nil { + return fmt.Errorf("new health handler: %w", err) + } + a.handlers.health = healthHandler + + // Linux handler + linuxHandler, err := linuxhandler.New( + cfg, + linuxhandler.WithLogger(a.logger), + linuxhandler.WithMetricsReporter(a.metricsReporter), + linuxhandler.WithInstallController(a.linuxInstallController), + ) + if err != nil { + return fmt.Errorf("new linux handler: %w", err) + } + a.handlers.linux = linuxHandler + + return nil +} diff --git a/api/health.go b/api/health.go deleted file mode 100644 index e38709b18..000000000 --- a/api/health.go +++ /dev/null @@ -1,22 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/replicatedhq/embedded-cluster/api/types" -) - -// getHealth handler to get the health of the API -// -// @Summary Get the health of the API -// @Description get the health of the API -// @Tags health -// @Produce json -// @Success 200 {object} types.Health -// @Router /health [get] -func (a *API) getHealth(w http.ResponseWriter, r *http.Request) { - response := types.Health{ - Status: types.HealthStatusOK, - } - a.json(w, r, http.StatusOK, response) -} diff --git a/api/install.go b/api/install.go deleted file mode 100644 index 05e27798b..000000000 --- a/api/install.go +++ /dev/null @@ -1,245 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/replicatedhq/embedded-cluster/api/controllers/install" - "github.com/replicatedhq/embedded-cluster/api/types" -) - -// getInstallInstallationConfig handler to get the installation config -// -// @Summary Get the installation config -// @Description get the installation config -// @Tags install -// @Security bearerauth -// @Produce json -// @Success 200 {object} types.InstallationConfig -// @Router /install/installation/config [get] -func (a *API) getInstallInstallationConfig(w http.ResponseWriter, r *http.Request) { - config, err := a.installController.GetInstallationConfig(r.Context()) - if err != nil { - a.logError(r, err, "failed to get installation config") - a.jsonError(w, r, err) - return - } - - a.json(w, r, http.StatusOK, config) -} - -// postInstallConfigureInstallation handler to configure the installation for install -// -// @Summary Configure the installation for install -// @Description configure the installation for install -// @Tags install -// @Security bearerauth -// @Accept json -// @Produce json -// @Param installationConfig body types.InstallationConfig true "Installation config" -// @Success 200 {object} types.Status -// @Router /install/installation/configure [post] -func (a *API) postInstallConfigureInstallation(w http.ResponseWriter, r *http.Request) { - var config types.InstallationConfig - if err := a.bindJSON(w, r, &config); err != nil { - return - } - - if err := a.installController.ConfigureInstallation(r.Context(), config); err != nil { - a.logError(r, err, "failed to set installation config") - a.jsonError(w, r, err) - return - } - - a.getInstallInstallationStatus(w, r) -} - -// getInstallInstallationStatus handler to get the status of the installation configuration for install -// -// @Summary Get installation configuration status for install -// @Description Get the current status of the installation configuration for install -// @Tags install -// @Security bearerauth -// @Produce json -// @Success 200 {object} types.Status -// @Router /install/installation/status [get] -func (a *API) getInstallInstallationStatus(w http.ResponseWriter, r *http.Request) { - status, err := a.installController.GetInstallationStatus(r.Context()) - if err != nil { - a.logError(r, err, "failed to get installation status") - a.jsonError(w, r, err) - return - } - - a.json(w, r, http.StatusOK, status) -} - -// postInstallRunHostPreflights handler to run install host preflight checks -// -// @Summary Run install host preflight checks -// @Description Run install host preflight checks using installation config and client-provided data -// @Tags install -// @Security bearerauth -// @Accept json -// @Produce json -// @Param request body types.PostInstallRunHostPreflightsRequest true "Post Install Run Host Preflights Request" -// @Success 200 {object} types.InstallHostPreflightsStatusResponse -// @Router /install/host-preflights/run [post] -func (a *API) postInstallRunHostPreflights(w http.ResponseWriter, r *http.Request) { - var req types.PostInstallRunHostPreflightsRequest - if err := a.bindJSON(w, r, &req); err != nil { - return - } - - err := a.installController.RunHostPreflights(r.Context(), install.RunHostPreflightsOptions{ - IsUI: req.IsUI, - }) - if err != nil { - a.logError(r, err, "failed to run install host preflights") - a.jsonError(w, r, err) - return - } - - a.getInstallHostPreflightsStatus(w, r) -} - -// getInstallHostPreflightsStatus handler to get host preflight status for install -// -// @Summary Get host preflight status for install -// @Description Get the current status and results of host preflight checks for install -// @Tags install -// @Security bearerauth -// @Produce json -// @Success 200 {object} types.InstallHostPreflightsStatusResponse -// @Router /install/host-preflights/status [get] -func (a *API) getInstallHostPreflightsStatus(w http.ResponseWriter, r *http.Request) { - titles, err := a.installController.GetHostPreflightTitles(r.Context()) - if err != nil { - a.logError(r, err, "failed to get install host preflight titles") - a.jsonError(w, r, err) - return - } - - output, err := a.installController.GetHostPreflightOutput(r.Context()) - if err != nil { - a.logError(r, err, "failed to get install host preflight output") - a.jsonError(w, r, err) - return - } - - status, err := a.installController.GetHostPreflightStatus(r.Context()) - if err != nil { - a.logError(r, err, "failed to get install host preflight status") - a.jsonError(w, r, err) - return - } - - response := types.InstallHostPreflightsStatusResponse{ - Titles: titles, - Output: output, - Status: status, - AllowIgnoreHostPreflights: a.allowIgnoreHostPreflights, - } - - a.json(w, r, http.StatusOK, response) -} - -// postInstallSetupInfra handler to setup infra components -// -// @Summary Setup infra components -// @Description Setup infra components -// @Tags install -// @Security bearerauth -// @Accept json -// @Produce json -// @Param request body types.InfraSetupRequest true "Infra Setup Request" -// @Success 200 {object} types.Infra -// @Router /install/infra/setup [post] -func (a *API) postInstallSetupInfra(w http.ResponseWriter, r *http.Request) { - // Parse request body - var req types.InfraSetupRequest - if err := a.bindJSON(w, r, &req); err != nil { - return - } - - // Setup infrastructure with preflight validation handled internally - err := a.installController.SetupInfra(r.Context(), req.IgnoreHostPreflights) - if err != nil { - a.logError(r, err, "failed to setup infra") - a.jsonError(w, r, err) - return - } - - a.getInstallInfraStatus(w, r) -} - -// getInstallInfraStatus handler to get the status of the infra -// -// @Summary Get the status of the infra -// @Description Get the current status of the infra -// @Tags install -// @Security bearerauth -// @Produce json -// @Success 200 {object} types.Infra -// @Router /install/infra/status [get] -func (a *API) getInstallInfraStatus(w http.ResponseWriter, r *http.Request) { - infra, err := a.installController.GetInfra(r.Context()) - if err != nil { - a.logError(r, err, "failed to get install infra status") - a.jsonError(w, r, err) - return - } - - a.json(w, r, http.StatusOK, infra) -} - -// postInstallSetInstallStatus handler to set the status of the install workflow -// -// @Summary Set the status of the install workflow -// @Description Set the status of the install workflow -// @Tags install -// @Security bearerauth -// @Accept json -// @Produce json -// @Param status body types.Status true "Status" -// @Success 200 {object} types.Status -// @Router /install/status [post] -func (a *API) setInstallStatus(w http.ResponseWriter, r *http.Request) { - var status types.Status - if err := a.bindJSON(w, r, &status); err != nil { - return - } - - if err := types.ValidateStatus(status); err != nil { - a.logError(r, err, "invalid install status") - a.jsonError(w, r, err) - return - } - - if err := a.installController.SetStatus(r.Context(), status); err != nil { - a.logError(r, err, "failed to set install status") - a.jsonError(w, r, err) - return - } - - a.getInstallStatus(w, r) -} - -// getInstallStatus handler to get the status of the install workflow -// -// @Summary Get the status of the install workflow -// @Description Get the current status of the install workflow -// @Tags install -// @Security bearerauth -// @Produce json -// @Success 200 {object} types.Status -// @Router /install/status [get] -func (a *API) getInstallStatus(w http.ResponseWriter, r *http.Request) { - status, err := a.installController.GetStatus(r.Context()) - if err != nil { - a.logError(r, err, "failed to get install status") - a.jsonError(w, r, err) - return - } - - a.json(w, r, http.StatusOK, status) -} diff --git a/api/integration/auth_controller_test.go b/api/integration/auth_controller_test.go index 8380dd62e..df903cca6 100644 --- a/api/integration/auth_controller_test.go +++ b/api/integration/auth_controller_test.go @@ -11,7 +11,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api" "github.com/replicatedhq/embedded-cluster/api/client" "github.com/replicatedhq/embedded-cluster/api/controllers/auth" - "github.com/replicatedhq/embedded-cluster/api/controllers/install" + linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/pkg/utils" @@ -21,15 +21,17 @@ import ( ) func TestAuthLoginAndTokenValidation(t *testing.T) { - password := "test-password" + cfg := types.APIConfig{ + Password: "test-password", + } // Create an auth controller - authController, err := auth.NewAuthController(password) + authController, err := auth.NewAuthController(cfg.Password) require.NoError(t, err) // Create an install controller - installController, err := install.NewInstallController( - install.WithInstallationManager(installation.NewInstallationManager( + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithInstallationManager(installation.NewInstallationManager( installation.WithNetUtils(&utils.MockNetUtils{}), )), ) @@ -37,9 +39,9 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { // Create the API with the auth controller apiInstance, err := api.New( - password, + cfg, api.WithAuthController(authController), - api.WithInstallController(installController), + api.WithLinuxInstallController(installController), api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -52,7 +54,7 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { t.Run("successful login", func(t *testing.T) { // Create login request with correct password loginReq := types.AuthRequest{ - Password: password, + Password: cfg.Password, } loginReqJSON, err := json.Marshal(loginReq) require.NoError(t, err) @@ -110,7 +112,7 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { // Test access to protected route without token t.Run("access protected route without token", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/installation/config", nil) rec := httptest.NewRecorder() // Serve the request @@ -122,7 +124,7 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { // Test access to protected route with invalid token t.Run("access protected route with invalid token", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"invalid-token") rec := httptest.NewRecorder() @@ -135,11 +137,13 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { } func TestAPIClientLogin(t *testing.T) { - password := "test-password" + cfg := types.APIConfig{ + Password: "test-password", + } // Create the API with the auth controller apiInstance, err := api.New( - password, + cfg, api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -158,7 +162,7 @@ func TestAPIClientLogin(t *testing.T) { c := client.New(server.URL) // Login with the client - err := c.Authenticate(password) + err := c.Authenticate(cfg.Password) require.NoError(t, err, "API client login should succeed with correct password") // Verify we can make authenticated requests after login diff --git a/api/integration/console_test.go b/api/integration/console_test.go index 169e78911..92e7bc99e 100644 --- a/api/integration/console_test.go +++ b/api/integration/console_test.go @@ -28,7 +28,9 @@ func TestConsoleListAvailableNetworkInterfaces(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", + types.APIConfig{ + Password: "password", + }, api.WithConsoleController(consoleController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), @@ -74,7 +76,9 @@ func TestConsoleListAvailableNetworkInterfacesUnauthorized(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", + types.APIConfig{ + Password: "password", + }, api.WithConsoleController(consoleController), api.WithAuthController(&staticAuthController{"VALID_TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), @@ -116,7 +120,9 @@ func TestConsoleListAvailableNetworkInterfacesError(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", + types.APIConfig{ + Password: "password", + }, api.WithConsoleController(consoleController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/hostpreflights_test.go b/api/integration/hostpreflights_test.go index a2709a91e..938bbbc15 100644 --- a/api/integration/hostpreflights_test.go +++ b/api/integration/hostpreflights_test.go @@ -10,7 +10,7 @@ import ( "github.com/gorilla/mux" "github.com/replicatedhq/embedded-cluster/api" - "github.com/replicatedhq/embedded-cluster/api/controllers/install" + linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" installationstore "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" @@ -64,15 +64,17 @@ func TestGetHostPreflightsStatus(t *testing.T) { preflight.WithPreflightRunner(runner), ) // Create an install controller - installController, err := install.NewInstallController( - install.WithHostPreflightManager(manager), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithHostPreflightManager(manager), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -85,7 +87,7 @@ func TestGetHostPreflightsStatus(t *testing.T) { // Test successful get t.Run("Success", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/host-preflights/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/host-preflights/status", nil) req.Header.Set("Authorization", "Bearer TOKEN") rec := httptest.NewRecorder() @@ -110,7 +112,7 @@ func TestGetHostPreflightsStatus(t *testing.T) { // Test authorization t.Run("Authorization error", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/host-preflights/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/host-preflights/status", nil) req.Header.Set("Authorization", "Bearer NOT_A_TOKEN") rec := httptest.NewRecorder() @@ -137,8 +139,10 @@ func TestGetHostPreflightsStatus(t *testing.T) { // Create the API with the mock controller apiInstance, err := api.New( - "password", - api.WithInstallController(mockController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -148,7 +152,7 @@ func TestGetHostPreflightsStatus(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/host-preflights/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/host-preflights/status", nil) req.Header.Set("Authorization", "Bearer TOKEN") rec := httptest.NewRecorder() @@ -210,15 +214,17 @@ func TestGetHostPreflightsStatusWithIgnoreFlag(t *testing.T) { preflight.WithPreflightRunner(runner), ) // Create an install controller - installController, err := install.NewInstallController(install.WithHostPreflightManager(manager)) + installController, err := linuxinstall.NewInstallController(linuxinstall.WithHostPreflightManager(manager)) require.NoError(t, err) // Create the API with allow ignore host preflights flag apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + AllowIgnoreHostPreflights: tt.allowIgnoreHostPreflights, + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithAllowIgnoreHostPreflights(tt.allowIgnoreHostPreflights), api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -228,7 +234,7 @@ func TestGetHostPreflightsStatusWithIgnoreFlag(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/host-preflights/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/host-preflights/status", nil) req.Header.Set("Authorization", "Bearer TOKEN") rec := httptest.NewRecorder() @@ -274,14 +280,14 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller with the mocked manager - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine( - install.WithCurrentState(install.StateHostConfigured), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), )), - install.WithHostPreflightManager(pfManager), - install.WithInstallationManager(iManager), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithInstallationManager(iManager), // Mock the release data used by the preflight runner - install.WithReleaseData(&release.ReleaseData{ + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{ DefaultDomains: release.Domains{ @@ -290,7 +296,7 @@ func TestPostRunHostPreflights(t *testing.T) { }, }, }), - install.WithRuntimeConfig(rc), + linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) @@ -322,8 +328,10 @@ func TestPostRunHostPreflights(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -334,7 +342,7 @@ func TestPostRunHostPreflights(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) + req := httptest.NewRequest(http.MethodPost, "/linux/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -378,23 +386,25 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine( - install.WithCurrentState(install.StateHostConfigured), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), )), - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + linuxinstall.WithHostPreflightManager(manager), + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{}, }), - install.WithRuntimeConfig(rc), + linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -405,7 +415,7 @@ func TestPostRunHostPreflights(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) + req := httptest.NewRequest(http.MethodPost, "/linux/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) req.Header.Set("Authorization", "Bearer NOT_A_TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -436,23 +446,25 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller with the failing manager - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine( - install.WithCurrentState(install.StateHostConfigured), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), )), - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + linuxinstall.WithHostPreflightManager(manager), + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{}, }), - install.WithRuntimeConfig(rc), + linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -462,7 +474,7 @@ func TestPostRunHostPreflights(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) + req := httptest.NewRequest(http.MethodPost, "/linux/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -495,23 +507,25 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller with the failing manager - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine( - install.WithCurrentState(install.StateHostConfigured), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), )), - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + linuxinstall.WithHostPreflightManager(manager), + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{}, }), - install.WithRuntimeConfig(rc), + linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -521,7 +535,7 @@ func TestPostRunHostPreflights(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) + req := httptest.NewRequest(http.MethodPost, "/linux/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -567,23 +581,25 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller with the failing manager - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine( - install.WithCurrentState(install.StatePreflightsRunning), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StatePreflightsRunning), )), - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + linuxinstall.WithHostPreflightManager(manager), + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{}, }), - install.WithRuntimeConfig(rc), + linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -593,7 +609,7 @@ func TestPostRunHostPreflights(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) + req := httptest.NewRequest(http.MethodPost, "/linux/install/host-preflights/run", bytes.NewBuffer([]byte(`{"isUi": true}`))) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() diff --git a/api/integration/install_test.go b/api/integration/install_test.go index f78d214bf..d88fc45ad 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -18,7 +18,8 @@ import ( k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "github.com/replicatedhq/embedded-cluster/api" apiclient "github.com/replicatedhq/embedded-cluster/api/client" - "github.com/replicatedhq/embedded-cluster/api/controllers/install" + "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" + linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/internal/managers/infra" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" @@ -51,7 +52,7 @@ var ( licenseData []byte ) -// Mock implementation of the install.Controller interface +// Mock implementation of the linuxinstall.Controller interface type mockInstallController struct { configureInstallationError error getInstallationConfigError error @@ -83,7 +84,7 @@ func (m *mockInstallController) GetInstallationStatus(ctx context.Context) (type return types.Status{}, nil } -func (m *mockInstallController) RunHostPreflights(ctx context.Context, opts install.RunHostPreflightsOptions) error { +func (m *mockInstallController) RunHostPreflights(ctx context.Context, opts linuxinstall.RunHostPreflightsOptions) error { return m.runHostPreflightsError } @@ -274,18 +275,20 @@ func TestConfigureInstallation(t *testing.T) { rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{})) // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateHostConfigured))), - install.WithHostUtils(tc.mockHostUtils), - install.WithNetUtils(tc.mockNetUtils), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured))), + linuxinstall.WithHostUtils(tc.mockHostUtils), + linuxinstall.WithNetUtils(tc.mockNetUtils), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -300,7 +303,7 @@ func TestConfigureInstallation(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/installation/configure", bytes.NewReader(configJSON)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/installation/configure", bytes.NewReader(configJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+tc.token) rec := httptest.NewRecorder() @@ -369,16 +372,18 @@ func TestConfigureInstallationValidation(t *testing.T) { rc.SetDataDir(t.TempDir()) // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateHostConfigured))), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured))), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -402,7 +407,7 @@ func TestConfigureInstallationValidation(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/installation/configure", bytes.NewReader(configJSON)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/installation/configure", bytes.NewReader(configJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -430,15 +435,17 @@ func TestConfigureInstallationBadRequest(t *testing.T) { rc.SetDataDir(t.TempDir()) // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateHostConfigured))), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured))), ) require.NoError(t, err) apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -448,7 +455,7 @@ func TestConfigureInstallationBadRequest(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request with invalid JSON - req := httptest.NewRequest(http.MethodPost, "/install/installation/configure", + req := httptest.NewRequest(http.MethodPost, "/linux/install/installation/configure", bytes.NewReader([]byte(`{"dataDirectory": "/tmp/data", "adminConsolePort": "not-a-number"}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") @@ -472,8 +479,10 @@ func TestConfigureInstallationControllerError(t *testing.T) { // Create the API with the mock controller apiInstance, err := api.New( - "password", - api.WithInstallController(mockController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -491,7 +500,7 @@ func TestConfigureInstallationControllerError(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/installation/configure", bytes.NewReader(configJSON)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/installation/configure", bytes.NewReader(configJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -514,9 +523,9 @@ func TestGetInstallationConfig(t *testing.T) { installationManager := installation.NewInstallationManager() // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithInstallationManager(installationManager), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithInstallationManager(installationManager), ) require.NoError(t, err) @@ -533,8 +542,10 @@ func TestGetInstallationConfig(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -547,7 +558,7 @@ func TestGetInstallationConfig(t *testing.T) { // Test successful get t.Run("Success", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -586,16 +597,18 @@ func TestGetInstallationConfig(t *testing.T) { ) // Create an install controller with the empty config manager - emptyInstallController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithInstallationManager(emptyInstallationManager), + emptyInstallController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithInstallationManager(emptyInstallationManager), ) require.NoError(t, err) // Create the API with the install controller emptyAPI, err := api.New( - "password", - api.WithInstallController(emptyInstallController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(emptyInstallController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -606,7 +619,7 @@ func TestGetInstallationConfig(t *testing.T) { emptyAPI.RegisterRoutes(emptyRouter) // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -633,7 +646,7 @@ func TestGetInstallationConfig(t *testing.T) { // Test authorization t.Run("Authorization error", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"NOT_A_TOKEN") rec := httptest.NewRecorder() @@ -659,8 +672,10 @@ func TestGetInstallationConfig(t *testing.T) { // Create the API with the mock controller apiInstance, err := api.New( - "password", - api.WithInstallController(mockController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -670,7 +685,7 @@ func TestGetInstallationConfig(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -689,10 +704,10 @@ func TestGetInstallationConfig(t *testing.T) { }) } -// Test the getInstallStatus endpoint returns install status correctly +// Test the getLinuxInstallStatus endpoint returns install status correctly func TestGetInstallStatus(t *testing.T) { // Create an install controller with the config manager - installController, err := install.NewInstallController() + installController, err := linuxinstall.NewInstallController() require.NoError(t, err) // Set some initial status @@ -705,8 +720,10 @@ func TestGetInstallStatus(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -719,7 +736,7 @@ func TestGetInstallStatus(t *testing.T) { // Test successful get t.Run("Success", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/status", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -743,7 +760,7 @@ func TestGetInstallStatus(t *testing.T) { // Test authorization t.Run("Authorization error", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/status", nil) req.Header.Set("Authorization", "Bearer "+"NOT_A_TOKEN") rec := httptest.NewRecorder() @@ -769,8 +786,10 @@ func TestGetInstallStatus(t *testing.T) { // Create the API with the mock controller apiInstance, err := api.New( - "password", - api.WithInstallController(mockController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -780,7 +799,7 @@ func TestGetInstallStatus(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodGet, "/install/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/status", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -799,16 +818,18 @@ func TestGetInstallStatus(t *testing.T) { }) } -// Test the setInstallStatus endpoint sets install status correctly +// Test the setLinuxInstallStatus endpoint sets install status correctly func TestSetInstallStatus(t *testing.T) { // Create an install controller with the config manager - installController, err := install.NewInstallController() + installController, err := linuxinstall.NewInstallController() require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -832,7 +853,7 @@ func TestSetInstallStatus(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/status", bytes.NewReader(statusJSON)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/status", bytes.NewReader(statusJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -866,7 +887,7 @@ func TestSetInstallStatus(t *testing.T) { // Test that the endpoint properly handles validation errors t.Run("Validation error", func(t *testing.T) { // Create a request with invalid JSON - req := httptest.NewRequest(http.MethodPost, "/install/status", + req := httptest.NewRequest(http.MethodPost, "/linux/install/status", bytes.NewReader([]byte(`{"state": "INVALID_STATE"}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") @@ -884,7 +905,7 @@ func TestSetInstallStatus(t *testing.T) { // Test authorization errors t.Run("Authorization error", func(t *testing.T) { // Create a request with invalid JSON - req := httptest.NewRequest(http.MethodPost, "/install/status", + req := httptest.NewRequest(http.MethodPost, "/linux/install/status", bytes.NewReader([]byte(`{}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"NOT_A_TOKEN") @@ -912,8 +933,10 @@ func TestSetInstallStatus(t *testing.T) { // Create the API with the mock controller apiInstance, err := api.New( - "password", - api.WithInstallController(mockController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -931,7 +954,7 @@ func TestSetInstallStatus(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/status", bytes.NewReader(statusJSON)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/status", bytes.NewReader(statusJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -964,9 +987,9 @@ func TestInstallWithAPIClient(t *testing.T) { ) // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithInstallationManager(installationManager), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithInstallationManager(installationManager), ) require.NoError(t, err) @@ -991,9 +1014,11 @@ func TestInstallWithAPIClient(t *testing.T) { // Create the API with controllers apiInstance, err := api.New( - password, + types.APIConfig{ + Password: password, + }, api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithInstallController(installController), + api.WithLinuxInstallController(installController), api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -1195,12 +1220,12 @@ func TestPostSetupInfra(t *testing.T) { ) // Create an install controller with the mocked managers - installController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsSucceeded))), - install.WithHostPreflightManager(pfManager), - install.WithInfraManager(infraManager), - install.WithReleaseData(&release.ReleaseData{ + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsSucceeded))), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithInfraManager(infraManager), + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{ DefaultDomains: release.Domains{ @@ -1214,8 +1239,10 @@ func TestPostSetupInfra(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1232,7 +1259,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1257,7 +1284,7 @@ func TestPostSetupInfra(t *testing.T) { // Helper function to get infra status getInfraStatus := func() types.Infra { // Create a request to get infra status - req := httptest.NewRequest(http.MethodGet, "/install/infra/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/infra/status", nil) req.Header.Set("Authorization", "Bearer TOKEN") rec := httptest.NewRecorder() @@ -1328,7 +1355,9 @@ func TestPostSetupInfra(t *testing.T) { t.Run("Authorization error", func(t *testing.T) { // Create the API apiInstance, err := api.New( - "password", + types.APIConfig{ + Password: "password", + }, api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1345,7 +1374,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer NOT_A_TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1388,8 +1417,10 @@ func TestPostSetupInfra(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1405,7 +1436,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1443,8 +1474,10 @@ func TestPostSetupInfra(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1460,7 +1493,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1505,8 +1538,10 @@ func TestPostSetupInfra(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1522,7 +1557,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1558,16 +1593,18 @@ func TestPostSetupInfra(t *testing.T) { ) // Create an install controller - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsRunning))), - install.WithHostPreflightManager(pfManager), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsRunning))), + linuxinstall.WithHostPreflightManager(pfManager), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1583,7 +1620,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1618,11 +1655,11 @@ func TestPostSetupInfra(t *testing.T) { ) // Create an install controller - installController, err := install.NewInstallController( - install.WithRuntimeConfig(rc), - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StateSucceeded))), - install.WithHostPreflightManager(pfManager), - install.WithReleaseData(&release.ReleaseData{ + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateSucceeded))), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{}, }), @@ -1631,8 +1668,10 @@ func TestPostSetupInfra(t *testing.T) { // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1648,7 +1687,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1703,22 +1742,24 @@ func TestPostSetupInfra(t *testing.T) { ) // Create an install controller - installController, err := install.NewInstallController( - install.WithHostPreflightManager(pfManager), - install.WithInfraManager(infraManager), - install.WithReleaseData(&release.ReleaseData{ + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithInfraManager(infraManager), + linuxinstall.WithReleaseData(&release.ReleaseData{ EmbeddedClusterConfig: &ecv1beta1.Config{}, ChannelRelease: &release.ChannelRelease{}, }), - install.WithRuntimeConfig(rc), - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsSucceeded))), + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsSucceeded))), ) require.NoError(t, err) // Create the API with the install controller apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -1734,7 +1775,7 @@ func TestPostSetupInfra(t *testing.T) { reqBodyBytes, err := json.Marshal(requestBody) require.NoError(t, err) - req := httptest.NewRequest(http.MethodPost, "/install/infra/setup", bytes.NewReader(reqBodyBytes)) + req := httptest.NewRequest(http.MethodPost, "/linux/install/infra/setup", bytes.NewReader(reqBodyBytes)) req.Header.Set("Authorization", "Bearer TOKEN") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() @@ -1748,7 +1789,7 @@ func TestPostSetupInfra(t *testing.T) { // The status should eventually be set to failed due to k0s install error assert.Eventually(t, func() bool { // Create a request to get infra status - req := httptest.NewRequest(http.MethodGet, "/install/infra/status", nil) + req := httptest.NewRequest(http.MethodGet, "/linux/install/infra/status", nil) req.Header.Set("Authorization", "Bearer TOKEN") rec := httptest.NewRecorder() diff --git a/api/internal/handlers/auth/auth.go b/api/internal/handlers/auth/auth.go new file mode 100644 index 000000000..5f1cc8568 --- /dev/null +++ b/api/internal/handlers/auth/auth.go @@ -0,0 +1,85 @@ +package auth + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/replicatedhq/embedded-cluster/api/controllers/auth" + "github.com/replicatedhq/embedded-cluster/api/internal/handlers/utils" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/sirupsen/logrus" +) + +type Handler struct { + logger logrus.FieldLogger + authController auth.Controller +} + +type Option func(*Handler) + +func WithAuthController(controller auth.Controller) Option { + return func(h *Handler) { + h.authController = controller + } +} + +func WithLogger(logger logrus.FieldLogger) Option { + return func(h *Handler) { + h.logger = logger + } +} + +func New(password string, opts ...Option) (*Handler, error) { + h := &Handler{} + + for _, opt := range opts { + opt(h) + } + + if h.logger == nil { + h.logger = logger.NewDiscardLogger() + } + + if h.authController == nil { + authController, err := auth.NewAuthController(password) + if err != nil { + return nil, fmt.Errorf("new auth controller: %w", err) + } + h.authController = authController + } + + return h, nil +} + +func (h *Handler) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + err := errors.New("authorization header is required") + utils.LogError(r, err, h.logger, "failed to authenticate") + utils.JSONError(w, r, types.NewUnauthorizedError(err), h.logger) + return + } + + if !strings.HasPrefix(token, "Bearer ") { + err := errors.New("authorization header must start with Bearer ") + utils.LogError(r, err, h.logger, "failed to authenticate") + utils.JSONError(w, r, types.NewUnauthorizedError(err), h.logger) + return + } + + token = token[len("Bearer "):] + + err := h.authController.ValidateToken(r.Context(), token) + if err != nil { + utils.LogError(r, err, h.logger, "failed to validate token") + utils.JSONError(w, r, types.NewUnauthorizedError(err), h.logger) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/api/internal/handlers/auth/login.go b/api/internal/handlers/auth/login.go new file mode 100644 index 000000000..34e5e3f17 --- /dev/null +++ b/api/internal/handlers/auth/login.go @@ -0,0 +1,47 @@ +package auth + +import ( + "errors" + "net/http" + + "github.com/replicatedhq/embedded-cluster/api/controllers/auth" + "github.com/replicatedhq/embedded-cluster/api/internal/handlers/utils" + "github.com/replicatedhq/embedded-cluster/api/types" +) + +// PostLogin handler to authenticate a user +// +// @ID postAuthLogin +// @Summary Authenticate a user +// @Description Authenticate a user +// @Tags auth +// @Accept json +// @Produce json +// @Param request body types.AuthRequest true "Auth Request" +// @Success 200 {object} types.AuthResponse +// @Failure 401 {object} types.APIError +// @Router /auth/login [post] +func (h *Handler) PostLogin(w http.ResponseWriter, r *http.Request) { + var request types.AuthRequest + if err := utils.BindJSON(w, r, &request, h.logger); err != nil { + return + } + + token, err := h.authController.Authenticate(r.Context(), request.Password) + if errors.Is(err, auth.ErrInvalidPassword) { + utils.JSONError(w, r, types.NewUnauthorizedError(err), h.logger) + return + } + + if err != nil { + utils.LogError(r, err, h.logger, "failed to authenticate") + utils.JSONError(w, r, types.NewInternalServerError(err), h.logger) + return + } + + response := types.AuthResponse{ + Token: token, + } + + utils.JSON(w, r, http.StatusOK, response, h.logger) +} diff --git a/api/internal/handlers/console/console.go b/api/internal/handlers/console/console.go new file mode 100644 index 000000000..c9035a25c --- /dev/null +++ b/api/internal/handlers/console/console.go @@ -0,0 +1,81 @@ +package console + +import ( + "fmt" + "net/http" + + "github.com/replicatedhq/embedded-cluster/api/controllers/console" + "github.com/replicatedhq/embedded-cluster/api/internal/handlers/utils" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/sirupsen/logrus" +) + +type Handler struct { + logger logrus.FieldLogger + consoleController console.Controller +} + +type Option func(*Handler) + +func WithConsoleController(controller console.Controller) Option { + return func(h *Handler) { + h.consoleController = controller + } +} + +func WithLogger(logger logrus.FieldLogger) Option { + return func(h *Handler) { + h.logger = logger + } +} + +func New(opts ...Option) (*Handler, error) { + h := &Handler{} + + for _, opt := range opts { + opt(h) + } + + if h.logger == nil { + h.logger = logger.NewDiscardLogger() + } + + if h.consoleController == nil { + consoleController, err := console.NewConsoleController() + if err != nil { + return nil, fmt.Errorf("new console controller: %w", err) + } + h.consoleController = consoleController + } + + return h, nil +} + +// GetListAvailableNetworkInterfaces handler to list available network interfaces +// +// @ID getConsoleListAvailableNetworkInterfaces +// @Summary List available network interfaces +// @Description List available network interfaces +// @Tags console +// @Produce json +// @Success 200 {object} types.GetListAvailableNetworkInterfacesResponse +// @Router /console/available-network-interfaces [get] +func (h *Handler) GetListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.Request) { + interfaces, err := h.consoleController.ListAvailableNetworkInterfaces() + if err != nil { + utils.LogError(r, err, h.logger, "failed to list available network interfaces") + utils.JSONError(w, r, err, h.logger) + return + } + + h.logger.WithFields(utils.LogrusFieldsFromRequest(r)). + WithField("interfaces", interfaces). + Info("got available network interfaces") + + response := types.GetListAvailableNetworkInterfacesResponse{ + NetworkInterfaces: interfaces, + } + + utils.JSON(w, r, http.StatusOK, response, h.logger) +} diff --git a/api/internal/handlers/health/health.go b/api/internal/handlers/health/health.go new file mode 100644 index 000000000..7f033c767 --- /dev/null +++ b/api/internal/handlers/health/health.go @@ -0,0 +1,52 @@ +package health + +import ( + "net/http" + + "github.com/replicatedhq/embedded-cluster/api/internal/handlers/utils" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/sirupsen/logrus" +) + +type Handler struct { + logger logrus.FieldLogger +} + +type Option func(*Handler) + +func WithLogger(logger logrus.FieldLogger) Option { + return func(h *Handler) { + h.logger = logger + } +} + +func New(opts ...Option) (*Handler, error) { + h := &Handler{} + + for _, opt := range opts { + opt(h) + } + + if h.logger == nil { + h.logger = logger.NewDiscardLogger() + } + + return h, nil +} + +// GetHealth handler to get the health of the API +// +// @ID getHealth +// @Summary Get the health of the API +// @Description get the health of the API +// @Tags health +// @Produce json +// @Success 200 {object} types.Health +// @Router /health [get] +func (h *Handler) GetHealth(w http.ResponseWriter, r *http.Request) { + response := types.Health{ + Status: types.HealthStatusOK, + } + utils.JSON(w, r, http.StatusOK, response, h.logger) +} diff --git a/api/internal/handlers/linux/install.go b/api/internal/handlers/linux/install.go new file mode 100644 index 000000000..efcb3687b --- /dev/null +++ b/api/internal/handlers/linux/install.go @@ -0,0 +1,253 @@ +package linux + +import ( + "net/http" + + "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" + "github.com/replicatedhq/embedded-cluster/api/internal/handlers/utils" + "github.com/replicatedhq/embedded-cluster/api/types" +) + +// GetInstallationConfig handler to get the installation config +// +// @ID getLinuxInstallInstallationConfig +// @Summary Get the installation config +// @Description get the installation config +// @Tags linux-install +// @Security bearerauth +// @Produce json +// @Success 200 {object} types.InstallationConfig +// @Router /linux/install/installation/config [get] +func (h *Handler) GetInstallationConfig(w http.ResponseWriter, r *http.Request) { + config, err := h.installController.GetInstallationConfig(r.Context()) + if err != nil { + utils.LogError(r, err, h.logger, "failed to get installation config") + utils.JSONError(w, r, err, h.logger) + return + } + + utils.JSON(w, r, http.StatusOK, config, h.logger) +} + +// PostConfigureInstallation handler to configure the installation for install +// +// @ID postLinuxInstallConfigureInstallation +// @Summary Configure the installation for install +// @Description configure the installation for install +// @Tags linux-install +// @Security bearerauth +// @Accept json +// @Produce json +// @Param installationConfig body types.InstallationConfig true "Installation config" +// @Success 200 {object} types.Status +// @Router /linux/install/installation/configure [post] +func (h *Handler) PostConfigureInstallation(w http.ResponseWriter, r *http.Request) { + var config types.InstallationConfig + if err := utils.BindJSON(w, r, &config, h.logger); err != nil { + return + } + + if err := h.installController.ConfigureInstallation(r.Context(), config); err != nil { + utils.LogError(r, err, h.logger, "failed to set installation config") + utils.JSONError(w, r, err, h.logger) + return + } + + h.GetInstallationStatus(w, r) +} + +// GetInstallationStatus handler to get the status of the installation configuration for install +// +// @ID getLinuxInstallInstallationStatus +// @Summary Get installation configuration status for install +// @Description Get the current status of the installation configuration for install +// @Tags linux-install +// @Security bearerauth +// @Produce json +// @Success 200 {object} types.Status +// @Router /linux/install/installation/status [get] +func (h *Handler) GetInstallationStatus(w http.ResponseWriter, r *http.Request) { + status, err := h.installController.GetInstallationStatus(r.Context()) + if err != nil { + utils.LogError(r, err, h.logger, "failed to get installation status") + utils.JSONError(w, r, err, h.logger) + return + } + + utils.JSON(w, r, http.StatusOK, status, h.logger) +} + +// PostRunHostPreflights handler to run install host preflight checks +// +// @ID postLinuxInstallRunHostPreflights +// @Summary Run install host preflight checks +// @Description Run install host preflight checks using installation config and client-provided data +// @Tags linux-install +// @Security bearerauth +// @Accept json +// @Produce json +// @Param request body types.PostInstallRunHostPreflightsRequest true "Post Install Run Host Preflights Request" +// @Success 200 {object} types.InstallHostPreflightsStatusResponse +// @Router /linux/install/host-preflights/run [post] +func (h *Handler) PostRunHostPreflights(w http.ResponseWriter, r *http.Request) { + var req types.PostInstallRunHostPreflightsRequest + if err := utils.BindJSON(w, r, &req, h.logger); err != nil { + return + } + + err := h.installController.RunHostPreflights(r.Context(), install.RunHostPreflightsOptions{ + IsUI: req.IsUI, + }) + if err != nil { + utils.LogError(r, err, h.logger, "failed to run install host preflights") + utils.JSONError(w, r, err, h.logger) + return + } + + h.GetHostPreflightsStatus(w, r) +} + +// GetHostPreflightsStatus handler to get host preflight status for install +// +// @ID getLinuxInstallHostPreflightsStatus +// @Summary Get host preflight status for install +// @Description Get the current status and results of host preflight checks for install +// @Tags linux-install +// @Security bearerauth +// @Produce json +// @Success 200 {object} types.InstallHostPreflightsStatusResponse +// @Router /linux/install/host-preflights/status [get] +func (h *Handler) GetHostPreflightsStatus(w http.ResponseWriter, r *http.Request) { + titles, err := h.installController.GetHostPreflightTitles(r.Context()) + if err != nil { + utils.LogError(r, err, h.logger, "failed to get install host preflight titles") + utils.JSONError(w, r, err, h.logger) + return + } + + output, err := h.installController.GetHostPreflightOutput(r.Context()) + if err != nil { + utils.LogError(r, err, h.logger, "failed to get install host preflight output") + utils.JSONError(w, r, err, h.logger) + return + } + + status, err := h.installController.GetHostPreflightStatus(r.Context()) + if err != nil { + utils.LogError(r, err, h.logger, "failed to get install host preflight status") + utils.JSONError(w, r, err, h.logger) + return + } + + response := types.InstallHostPreflightsStatusResponse{ + Titles: titles, + Output: output, + Status: status, + AllowIgnoreHostPreflights: h.cfg.AllowIgnoreHostPreflights, + } + + utils.JSON(w, r, http.StatusOK, response, h.logger) +} + +// PostSetupInfra handler to setup infra components +// +// @ID postLinuxInstallSetupInfra +// @Summary Setup infra components +// @Description Setup infra components +// @Tags linux-install +// @Security bearerauth +// @Accept json +// @Produce json +// @Param request body types.InfraSetupRequest true "Infra Setup Request" +// @Success 200 {object} types.Infra +// @Router /linux/install/infra/setup [post] +func (h *Handler) PostSetupInfra(w http.ResponseWriter, r *http.Request) { + var req types.InfraSetupRequest + if err := utils.BindJSON(w, r, &req, h.logger); err != nil { + return + } + + err := h.installController.SetupInfra(r.Context(), req.IgnoreHostPreflights) + if err != nil { + utils.LogError(r, err, h.logger, "failed to setup infra") + utils.JSONError(w, r, err, h.logger) + return + } + + h.GetInfraStatus(w, r) +} + +// GetInfraStatus handler to get the status of the infra +// +// @ID getLinuxInstallInfraStatus +// @Summary Get the status of the infra +// @Description Get the current status of the infra +// @Tags linux-install +// @Security bearerauth +// @Produce json +// @Success 200 {object} types.Infra +// @Router /linux/install/infra/status [get] +func (h *Handler) GetInfraStatus(w http.ResponseWriter, r *http.Request) { + infra, err := h.installController.GetInfra(r.Context()) + if err != nil { + utils.LogError(r, err, h.logger, "failed to get install infra status") + utils.JSONError(w, r, err, h.logger) + return + } + + utils.JSON(w, r, http.StatusOK, infra, h.logger) +} + +// PostSetStatus handler to set the status of the install workflow +// +// @ID postLinuxInstallSetStatus +// @Summary Set the status of the install workflow +// @Description Set the status of the install workflow +// @Tags linux-install +// @Security bearerauth +// @Accept json +// @Produce json +// @Param status body types.Status true "Status" +// @Success 200 {object} types.Status +// @Router /linux/install/status [post] +func (h *Handler) PostSetStatus(w http.ResponseWriter, r *http.Request) { + var status types.Status + if err := utils.BindJSON(w, r, &status, h.logger); err != nil { + return + } + + if err := types.ValidateStatus(status); err != nil { + utils.LogError(r, err, h.logger, "invalid install status") + utils.JSONError(w, r, err, h.logger) + return + } + + if err := h.installController.SetStatus(r.Context(), status); err != nil { + utils.LogError(r, err, h.logger, "failed to set install status") + utils.JSONError(w, r, err, h.logger) + return + } + + h.GetStatus(w, r) +} + +// GetStatus handler to get the status of the install workflow +// +// @ID getLinuxInstallStatus +// @Summary Get the status of the install workflow +// @Description Get the current status of the install workflow +// @Tags linux-install +// @Security bearerauth +// @Produce json +// @Success 200 {object} types.Status +// @Router /linux/install/status [get] +func (h *Handler) GetStatus(w http.ResponseWriter, r *http.Request) { + status, err := h.installController.GetStatus(r.Context()) + if err != nil { + utils.LogError(r, err, h.logger, "failed to get install status") + utils.JSONError(w, r, err, h.logger) + return + } + + utils.JSON(w, r, http.StatusOK, status, h.logger) +} diff --git a/api/internal/handlers/linux/linux.go b/api/internal/handlers/linux/linux.go new file mode 100644 index 000000000..976c5f616 --- /dev/null +++ b/api/internal/handlers/linux/linux.go @@ -0,0 +1,90 @@ +package linux + +import ( + "fmt" + + "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" + "github.com/sirupsen/logrus" +) + +type Handler struct { + cfg types.APIConfig + installController install.Controller + logger logrus.FieldLogger + hostUtils hostutils.HostUtilsInterface + metricsReporter metrics.ReporterInterface +} + +type Option func(*Handler) + +func WithInstallController(controller install.Controller) Option { + return func(h *Handler) { + h.installController = controller + } +} + +func WithLogger(logger logrus.FieldLogger) Option { + return func(h *Handler) { + h.logger = logger + } +} + +func WithHostUtils(hostUtils hostutils.HostUtilsInterface) Option { + return func(h *Handler) { + h.hostUtils = hostUtils + } +} + +func WithMetricsReporter(metricsReporter metrics.ReporterInterface) Option { + return func(h *Handler) { + h.metricsReporter = metricsReporter + } +} + +func New(cfg types.APIConfig, opts ...Option) (*Handler, error) { + h := &Handler{ + cfg: cfg, + } + + for _, opt := range opts { + opt(h) + } + + if h.logger == nil { + h.logger = logger.NewDiscardLogger() + } + + if h.hostUtils == nil { + h.hostUtils = hostutils.New( + hostutils.WithLogger(h.logger), + ) + } + + // TODO (@team): discuss which of these should / should not be pointers + if h.installController == nil { + installController, err := install.NewInstallController( + install.WithRuntimeConfig(h.cfg.RuntimeConfig), + install.WithLogger(h.logger), + install.WithHostUtils(h.hostUtils), + install.WithMetricsReporter(h.metricsReporter), + install.WithReleaseData(h.cfg.ReleaseData), + install.WithPassword(h.cfg.Password), + install.WithTLSConfig(h.cfg.TLSConfig), + install.WithLicense(h.cfg.License), + install.WithAirgapBundle(h.cfg.AirgapBundle), + install.WithConfigValues(h.cfg.ConfigValues), + install.WithEndUserConfig(h.cfg.EndUserConfig), + install.WithAllowIgnoreHostPreflights(h.cfg.AllowIgnoreHostPreflights), + ) + if err != nil { + return nil, fmt.Errorf("new install controller: %w", err) + } + h.installController = installController + } + + return h, nil +} diff --git a/api/internal/handlers/utils/utils.go b/api/internal/handlers/utils/utils.go new file mode 100644 index 000000000..57fc071a5 --- /dev/null +++ b/api/internal/handlers/utils/utils.go @@ -0,0 +1,62 @@ +package utils + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/sirupsen/logrus" +) + +// Shared helper functions for all handler packages + +func BindJSON(w http.ResponseWriter, r *http.Request, v any, logger logrus.FieldLogger) error { + if err := json.NewDecoder(r.Body).Decode(v); err != nil { + LogError(r, err, logger, fmt.Sprintf("failed to decode %s %s request", strings.ToLower(r.Method), r.URL.Path)) + JSONError(w, r, types.NewBadRequestError(err), logger) + return err + } + return nil +} + +func JSON(w http.ResponseWriter, r *http.Request, code int, payload any, logger logrus.FieldLogger) { + response, err := json.Marshal(payload) + if err != nil { + LogError(r, err, logger, "failed to encode response") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _, _ = w.Write(response) +} + +func JSONError(w http.ResponseWriter, r *http.Request, err error, logger logrus.FieldLogger) { + var apiErr *types.APIError + if !errors.As(err, &apiErr) { + apiErr = types.NewInternalServerError(err) + } + response, err := json.Marshal(apiErr) + if err != nil { + LogError(r, err, logger, "failed to encode response") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(apiErr.StatusCode) + _, _ = w.Write(response) +} + +func LogError(r *http.Request, err error, logger logrus.FieldLogger, args ...any) { + logger.WithFields(LogrusFieldsFromRequest(r)).WithError(err).Error(args...) +} + +func LogrusFieldsFromRequest(r *http.Request) logrus.Fields { + return logrus.Fields{ + "method": r.Method, + "path": r.URL.Path, + } +} diff --git a/api/api_test.go b/api/internal/handlers/utils/utils_test.go similarity index 94% rename from api/api_test.go rename to api/internal/handlers/utils/utils_test.go index 8a35376cc..3d6e26ca2 100644 --- a/api/api_test.go +++ b/api/internal/handlers/utils/utils_test.go @@ -1,4 +1,4 @@ -package api +package utils import ( "encoding/json" @@ -84,10 +84,7 @@ func TestAPI_jsonError(t *testing.T) { rec := httptest.NewRecorder() // Call the JSON method - api := &API{ - logger: logger.NewDiscardLogger(), - } - api.jsonError(rec, httptest.NewRequest("GET", "/api/test", nil), tt.apiErr) + JSONError(rec, httptest.NewRequest("GET", "/api/test", nil), tt.apiErr, logger.NewDiscardLogger()) // Check status code assert.Equal(t, tt.wantCode, rec.Code, "Status code should match") diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 000000000..74a60e125 --- /dev/null +++ b/api/routes.go @@ -0,0 +1,60 @@ +package api + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/replicatedhq/embedded-cluster/api/docs" + httpSwagger "github.com/swaggo/http-swagger/v2" +) + +func (a *API) RegisterRoutes(router *mux.Router) { + router.HandleFunc("/health", a.handlers.health.GetHealth).Methods("GET") + + // Hack to fix issue + // https://github.com/swaggo/swag/issues/1588#issuecomment-2797801240 + router.HandleFunc("/swagger/doc.json", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write([]byte(docs.SwaggerInfo.ReadDoc())) + }).Methods("GET") + router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) + + router.HandleFunc("/auth/login", a.handlers.auth.PostLogin).Methods("POST") + + authenticatedRouter := router.PathPrefix("/").Subrouter() + authenticatedRouter.Use(a.handlers.auth.Middleware) + + a.registerLinuxRoutes(authenticatedRouter) + a.registerKubernetesRoutes(authenticatedRouter) + a.registerConsoleRoutes(authenticatedRouter) +} + +func (a *API) registerLinuxRoutes(router *mux.Router) { + linuxRouter := router.PathPrefix("/linux").Subrouter() + + installRouter := linuxRouter.PathPrefix("/install").Subrouter() + installRouter.HandleFunc("/installation/config", a.handlers.linux.GetInstallationConfig).Methods("GET") + installRouter.HandleFunc("/installation/configure", a.handlers.linux.PostConfigureInstallation).Methods("POST") + installRouter.HandleFunc("/installation/status", a.handlers.linux.GetInstallationStatus).Methods("GET") + + installRouter.HandleFunc("/host-preflights/run", a.handlers.linux.PostRunHostPreflights).Methods("POST") + installRouter.HandleFunc("/host-preflights/status", a.handlers.linux.GetHostPreflightsStatus).Methods("GET") + + installRouter.HandleFunc("/infra/setup", a.handlers.linux.PostSetupInfra).Methods("POST") + installRouter.HandleFunc("/infra/status", a.handlers.linux.GetInfraStatus).Methods("GET") + + // TODO (@salah): remove this once the cli isn't responsible for setting the install status + // and the ui isn't polling for it to know if the entire install is complete + installRouter.HandleFunc("/status", a.handlers.linux.GetStatus).Methods("GET") + installRouter.HandleFunc("/status", a.handlers.linux.PostSetStatus).Methods("POST") +} + +func (a *API) registerKubernetesRoutes(router *mux.Router) { + // kubernetesRouter := router.PathPrefix("/kubernetes").Subrouter() +} + +func (a *API) registerConsoleRoutes(router *mux.Router) { + consoleRouter := router.PathPrefix("/console").Subrouter() + consoleRouter.HandleFunc("/available-network-interfaces", a.handlers.console.GetListAvailableNetworkInterfaces).Methods("GET") +} diff --git a/api/types/api.go b/api/types/api.go new file mode 100644 index 000000000..93b10c617 --- /dev/null +++ b/api/types/api.go @@ -0,0 +1,20 @@ +package types + +import ( + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" +) + +// APIConfig holds the configuration for the API server +type APIConfig struct { + RuntimeConfig runtimeconfig.RuntimeConfig + Password string + TLSConfig TLSConfig + License []byte + AirgapBundle string + ConfigValues string + ReleaseData *release.ReleaseData + EndUserConfig *ecv1beta1.Config + AllowIgnoreHostPreflights bool +} diff --git a/api/types/responses.go b/api/types/responses.go index ebb71c9d2..1bbc55b42 100644 --- a/api/types/responses.go +++ b/api/types/responses.go @@ -7,3 +7,8 @@ type InstallHostPreflightsStatusResponse struct { Status Status `json:"status,omitempty"` AllowIgnoreHostPreflights bool `json:"allowIgnoreHostPreflights"` } + +// GetListAvailableNetworkInterfacesResponse represents the response when listing available network interfaces +type GetListAvailableNetworkInterfacesResponse struct { + NetworkInterfaces []string `json:"networkInterfaces"` +} diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go index 58c0cf11d..884037230 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -27,8 +27,8 @@ import ( "github.com/sirupsen/logrus" ) -// apiConfig holds the configuration for the API server -type apiConfig struct { +// apiOptions holds the configuration options for the API server +type apiOptions struct { RuntimeConfig runtimeconfig.RuntimeConfig Logger logrus.FieldLogger MetricsReporter metrics.ReporterInterface @@ -40,25 +40,25 @@ type apiConfig struct { ConfigValues string ReleaseData *release.ReleaseData EndUserConfig *ecv1beta1.Config - WebAssetsFS fs.FS AllowIgnoreHostPreflights bool + WebAssetsFS fs.FS } -func startAPI(ctx context.Context, cert tls.Certificate, config apiConfig) error { - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.ManagerPort)) +func startAPI(ctx context.Context, cert tls.Certificate, opts apiOptions) error { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", opts.ManagerPort)) if err != nil { return fmt.Errorf("unable to create listener: %w", err) } go func() { - if err := serveAPI(ctx, listener, cert, config); err != nil { + if err := serveAPI(ctx, listener, cert, opts); err != nil { if !errors.Is(err, http.ErrServerClosed) { logrus.Errorf("api error: %v", err) } } }() - addr := fmt.Sprintf("localhost:%d", config.ManagerPort) + addr := fmt.Sprintf("localhost:%d", opts.ManagerPort) if err := waitForAPI(ctx, addr); err != nil { return fmt.Errorf("unable to wait for api: %w", err) } @@ -66,42 +66,46 @@ func startAPI(ctx context.Context, cert tls.Certificate, config apiConfig) error return nil } -func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, config apiConfig) error { +func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, opts apiOptions) error { router := mux.NewRouter() - if config.ReleaseData == nil { + if opts.ReleaseData == nil { return fmt.Errorf("release not found") } - if config.ReleaseData.Application == nil { + if opts.ReleaseData.Application == nil { return fmt.Errorf("application not found") } - logger, err := loggerFromConfig(config) + logger, err := loggerFromOptions(opts) if err != nil { return fmt.Errorf("new api logger: %w", err) } + cfg := apitypes.APIConfig{ + RuntimeConfig: opts.RuntimeConfig, + Password: opts.Password, + TLSConfig: opts.TLSConfig, + License: opts.License, + AirgapBundle: opts.AirgapBundle, + ConfigValues: opts.ConfigValues, + ReleaseData: opts.ReleaseData, + EndUserConfig: opts.EndUserConfig, + AllowIgnoreHostPreflights: opts.AllowIgnoreHostPreflights, + } + api, err := api.New( - config.Password, + cfg, api.WithLogger(logger), - api.WithRuntimeConfig(config.RuntimeConfig), - api.WithMetricsReporter(config.MetricsReporter), - api.WithReleaseData(config.ReleaseData), - api.WithTLSConfig(config.TLSConfig), - api.WithLicense(config.License), - api.WithAirgapBundle(config.AirgapBundle), - api.WithConfigValues(config.ConfigValues), - api.WithEndUserConfig(config.EndUserConfig), - api.WithAllowIgnoreHostPreflights(config.AllowIgnoreHostPreflights), + api.WithMetricsReporter(opts.MetricsReporter), ) if err != nil { return fmt.Errorf("new api: %w", err) } webServer, err := web.New(web.InitialState{ - Title: config.ReleaseData.Application.Spec.Title, - Icon: config.ReleaseData.Application.Spec.Icon, - }, web.WithLogger(logger), web.WithAssetsFS(config.WebAssetsFS)) + Title: opts.ReleaseData.Application.Spec.Title, + Icon: opts.ReleaseData.Application.Spec.Icon, + }, web.WithLogger(logger), web.WithAssetsFS(opts.WebAssetsFS)) if err != nil { return fmt.Errorf("new web server: %w", err) } @@ -125,9 +129,9 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, return server.ServeTLS(listener, "", "") } -func loggerFromConfig(config apiConfig) (logrus.FieldLogger, error) { - if config.Logger != nil { - return config.Logger, nil +func loggerFromOptions(opts apiOptions) (logrus.FieldLogger, error) { + if opts.Logger != nil { + return opts.Logger, nil } logger, err := apilogger.NewLogger() if err != nil { diff --git a/cmd/installer/cli/api_test.go b/cmd/installer/cli/api_test.go index 4d17bf90b..3f0510410 100644 --- a/cmd/installer/cli/api_test.go +++ b/cmd/installer/cli/api_test.go @@ -54,7 +54,7 @@ func Test_serveAPI(t *testing.T) { portInt, err := strconv.Atoi(port) require.NoError(t, err) - config := apiConfig{ + config := apiOptions{ Logger: apilogger.NewDiscardLogger(), Password: "password", ManagerPort: portInt, diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 7d0acbbd0..e79f58f9c 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -413,7 +413,7 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc return fmt.Errorf("process overrides file: %w", err) } - apiConfig := apiConfig{ + apiConfig := apiOptions{ // TODO (@salah): implement reporting in api // MetricsReporter: installReporter, RuntimeConfig: rc, diff --git a/web/src/components/wizard/InstallationStep.tsx b/web/src/components/wizard/InstallationStep.tsx index 88cef8057..9fdadadde 100644 --- a/web/src/components/wizard/InstallationStep.tsx +++ b/web/src/components/wizard/InstallationStep.tsx @@ -27,7 +27,7 @@ const InstallationStep: React.FC = ({ onNext }) => { const { data: infraStatusResponse, error: infraStatusError } = useQuery({ queryKey: ["infraStatus"], queryFn: async () => { - const response = await fetch("/api/install/infra/status", { + const response = await fetch("/api/linux/install/infra/status", { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, diff --git a/web/src/components/wizard/SetupStep.tsx b/web/src/components/wizard/SetupStep.tsx index b1908898e..e7df99ef7 100644 --- a/web/src/components/wizard/SetupStep.tsx +++ b/web/src/components/wizard/SetupStep.tsx @@ -33,7 +33,7 @@ const SetupStep: React.FC = ({ onNext }) => { const { isLoading: isConfigLoading } = useQuery({ queryKey: ["installConfig"], queryFn: async () => { - const response = await fetch("/api/install/installation/config", { + const response = await fetch("/api/linux/install/installation/config", { headers: { Authorization: `Bearer ${token}`, }, @@ -76,7 +76,7 @@ const SetupStep: React.FC = ({ onNext }) => { // Mutation for submitting the configuration const { mutate: submitConfig, error: submitError } = useMutation({ mutationFn: async (configData: typeof config) => { - const response = await fetch("/api/install/installation/configure", { + const response = await fetch("/api/linux/install/installation/configure", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/web/src/components/wizard/ValidationStep.tsx b/web/src/components/wizard/ValidationStep.tsx index 7ea7fa0a7..37a86e8fc 100644 --- a/web/src/components/wizard/ValidationStep.tsx +++ b/web/src/components/wizard/ValidationStep.tsx @@ -30,7 +30,7 @@ const ValidationStep: React.FC = ({ onNext, onBack }) => { const { mutate: startInstallation } = useMutation({ mutationFn: async ({ ignoreHostPreflights }: { ignoreHostPreflights: boolean }) => { - const response = await fetch("/api/install/infra/setup", { + const response = await fetch("/api/linux/install/infra/setup", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx b/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx index 17c98066e..004531334 100644 --- a/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx +++ b/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx @@ -66,7 +66,7 @@ const LinuxPreflightCheck: React.FC = ({ onComplete }) // Mutation to run preflight checks const { mutate: runPreflights, error: preflightsRunError } = useMutation({ mutationFn: async () => { - const response = await fetch("/api/install/host-preflights/run", { + const response = await fetch("/api/linux/install/host-preflights/run", { method: "POST", headers: { Authorization: `Bearer ${token}`, @@ -91,7 +91,7 @@ const LinuxPreflightCheck: React.FC = ({ onComplete }) const { data: installationStatus } = useQuery({ queryKey: ["installationStatus"], queryFn: async () => { - const response = await fetch("/api/install/installation/status", { + const response = await fetch("/api/linux/install/installation/status", { headers: { ...(localStorage.getItem("auth") && { Authorization: `Bearer ${localStorage.getItem("auth")}`, @@ -112,7 +112,7 @@ const LinuxPreflightCheck: React.FC = ({ onComplete }) const { data: preflightResponse } = useQuery({ queryKey: ["preflightStatus"], queryFn: async () => { - const response = await fetch("/api/install/host-preflights/status", { + const response = await fetch("/api/linux/install/host-preflights/status", { headers: { ...(localStorage.getItem("auth") && { Authorization: `Bearer ${localStorage.getItem("auth")}`, diff --git a/web/src/components/wizard/tests/InstallationStep.test.tsx b/web/src/components/wizard/tests/InstallationStep.test.tsx index bc1b54a18..c086647a7 100644 --- a/web/src/components/wizard/tests/InstallationStep.test.tsx +++ b/web/src/components/wizard/tests/InstallationStep.test.tsx @@ -9,7 +9,7 @@ import { http, HttpResponse } from "msw"; const server = setupServer( // Mock installation status endpoint - http.get("*/api/install/infra/status", () => { + http.get("*/api/linux/install/infra/status", () => { return HttpResponse.json({ status: { state: "Running", description: "Installing..." }, components: [ @@ -84,7 +84,7 @@ describe("InstallationStep", () => { it("shows progress as components complete", async () => { const mockOnNext = vi.fn(); server.use( - http.get("*/api/install/infra/status", ({ request }) => { + http.get("*/api/linux/install/infra/status", ({ request }) => { expect(request.headers.get("Authorization")).toBe("Bearer test-token"); return HttpResponse.json({ status: { state: "InProgress", description: "Installing components..." }, @@ -130,7 +130,7 @@ describe("InstallationStep", () => { it("enables next button when installation succeeds", async () => { const mockOnNext = vi.fn(); server.use( - http.get("*/api/install/infra/status", ({ request }) => { + http.get("*/api/linux/install/infra/status", ({ request }) => { expect(request.headers.get("Authorization")).toBe("Bearer test-token"); return HttpResponse.json({ status: { state: "Succeeded", description: "Installation complete" }, @@ -176,7 +176,7 @@ describe("InstallationStep", () => { it("shows error message when installation fails", async () => { const mockOnNext = vi.fn(); server.use( - http.get("*/api/install/infra/status", ({ request }) => { + http.get("*/api/linux/install/infra/status", ({ request }) => { expect(request.headers.get("Authorization")).toBe("Bearer test-token"); return HttpResponse.json({ status: { @@ -227,7 +227,7 @@ describe("InstallationStep", () => { it("verify log viewer", async () => { const mockOnNext = vi.fn(); server.use( - http.get("*/api/install/infra/status", () => { + http.get("*/api/linux/install/infra/status", () => { return HttpResponse.json({ status: { state: "Running", description: "Installing..." }, components: [ diff --git a/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx b/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx index 30d01b101..6dad078dd 100644 --- a/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx +++ b/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx @@ -10,7 +10,7 @@ const TEST_TOKEN = "test-auth-token"; const server = setupServer( // Mock installation status endpoint - http.get("*/api/install/installation/status", ({ request }) => { + http.get("*/api/linux/install/installation/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -19,7 +19,7 @@ const server = setupServer( }), // Mock preflight status endpoint - http.get("*/api/install/host-preflights/status", ({ request }) => { + http.get("*/api/linux/install/host-preflights/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -37,7 +37,7 @@ const server = setupServer( }), // Mock preflight run endpoint - http.post("*/api/install/host-preflights/run", ({ request }) => { + http.post("*/api/linux/install/host-preflights/run", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -59,7 +59,7 @@ describe("LinuxPreflightCheck", () => { it("shows initializing state when installation status is polling", async () => { server.use( - http.get("*/api/install/installation/status", ({ request }) => { + http.get("*/api/linux/install/installation/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -83,7 +83,7 @@ describe("LinuxPreflightCheck", () => { it("shows validating state when preflights are polling", async () => { server.use( - http.get("*/api/install/host-preflights/status", ({ request }) => { + http.get("*/api/linux/install/host-preflights/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -127,7 +127,7 @@ describe("LinuxPreflightCheck", () => { it("shows success state when all preflights pass", async () => { server.use( - http.get("*/api/install/host-preflights/status", ({ request }) => { + http.get("*/api/linux/install/host-preflights/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -158,7 +158,7 @@ describe("LinuxPreflightCheck", () => { it("handles installation status error", async () => { server.use( - http.get("*/api/install/installation/status", ({ request }) => { + http.get("*/api/linux/install/installation/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -168,7 +168,7 @@ describe("LinuxPreflightCheck", () => { description: "Failed to configure the host", }); }), - http.get("*/api/install/host-preflights/status", ({ request }) => { + http.get("*/api/linux/install/host-preflights/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -197,7 +197,7 @@ describe("LinuxPreflightCheck", () => { it("handles preflight run error", async () => { server.use( - http.post("*/api/install/host-preflights/run", ({ request }) => { + http.post("*/api/linux/install/host-preflights/run", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -248,7 +248,7 @@ describe("LinuxPreflightCheck", () => { it("receives allowIgnoreHostPreflights field in preflight response", async () => { // Mock preflight status endpoint with allowIgnoreHostPreflights: true server.use( - http.get("*/api/install/host-preflights/status", ({ request }) => { + http.get("*/api/linux/install/host-preflights/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); @@ -287,7 +287,7 @@ describe("LinuxPreflightCheck", () => { it("passes allowIgnoreHostPreflights false to onComplete callback", async () => { // Mock preflight status endpoint with allowIgnoreHostPreflights: false server.use( - http.get("*/api/install/host-preflights/status", ({ request }) => { + http.get("*/api/linux/install/host-preflights/status", ({ request }) => { const authHeader = request.headers.get("Authorization"); if (!authHeader || !authHeader.startsWith("Bearer ")) { return new HttpResponse(null, { status: 401 }); diff --git a/web/src/components/wizard/tests/SetupStep.test.tsx b/web/src/components/wizard/tests/SetupStep.test.tsx index 2ced28379..4e2cef767 100644 --- a/web/src/components/wizard/tests/SetupStep.test.tsx +++ b/web/src/components/wizard/tests/SetupStep.test.tsx @@ -9,7 +9,7 @@ import { MOCK_INSTALL_CONFIG, MOCK_NETWORK_INTERFACES, MOCK_PROTOTYPE_SETTINGS } const server = setupServer( // Mock install config endpoint - http.get("*/api/install/installation/config", () => { + http.get("*/api/linux/install/installation/config", () => { return HttpResponse.json({ config: MOCK_INSTALL_CONFIG }); }), @@ -19,7 +19,7 @@ const server = setupServer( }), // Mock config submission endpoint - http.post("*/api/install/installation/configure", () => { + http.post("*/api/linux/install/installation/configure", () => { return HttpResponse.json({ success: true }); }) ); @@ -122,7 +122,7 @@ describe("SetupStep", () => { }); }), // Mock config submission endpoint to return an error - http.post("*/api/install/installation/configure", () => { + http.post("*/api/linux/install/installation/configure", () => { return new HttpResponse(JSON.stringify({ message: "Invalid configuration" }), { status: 400 }); }) ); @@ -188,7 +188,7 @@ describe("SetupStep", () => { // Mock all required API endpoints server.use( // Mock install config endpoint - http.get("*/api/install/installation/config", ({ request }) => { + http.get("*/api/linux/install/installation/config", ({ request }) => { // Verify auth header expect(request.headers.get("Authorization")).toBe("Bearer test-token"); return HttpResponse.json({ @@ -212,7 +212,7 @@ describe("SetupStep", () => { }); }), // Mock config submission endpoint - http.post("*/api/install/installation/configure", async ({ request }) => { + http.post("*/api/linux/install/installation/configure", async ({ request }) => { // Verify auth header expect(request.headers.get("Authorization")).toBe("Bearer test-token"); const body = await request.json(); diff --git a/web/src/components/wizard/tests/ValidationStep.test.tsx b/web/src/components/wizard/tests/ValidationStep.test.tsx index 8536eea87..404396716 100644 --- a/web/src/components/wizard/tests/ValidationStep.test.tsx +++ b/web/src/components/wizard/tests/ValidationStep.test.tsx @@ -8,7 +8,7 @@ import ValidationStep from '../ValidationStep'; const server = setupServer( // Mock installation status endpoint - http.get('*/api/install/installation/status', () => { + http.get('*/api/linux/install/installation/status', () => { return HttpResponse.json({ state: 'Succeeded', description: 'Installation initialized', @@ -17,7 +17,7 @@ const server = setupServer( }), // Mock start installation endpoint - http.post('*/api/install/infra/setup', () => { + http.post('*/api/linux/install/infra/setup', () => { return HttpResponse.json({ success: true }); }) ); @@ -42,7 +42,7 @@ describe('ValidationStep', () => { it('enables Start Installation button when allowIgnoreHostPreflights is true and preflights fail', async () => { // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: true server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Failed' }, @@ -80,7 +80,7 @@ describe('ValidationStep', () => { it('disables Start Installation button when allowIgnoreHostPreflights is false and preflights fail', async () => { // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: false server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Failed' }, @@ -125,7 +125,7 @@ describe('ValidationStep', () => { it('shows modal when Start Installation clicked and allowIgnoreHostPreflights is true and preflights fail', async () => { // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: true server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Failed' }, @@ -180,7 +180,7 @@ describe('ValidationStep', () => { it('proceeds automatically when allowIgnoreHostPreflights is true and preflights pass', async () => { // Mock preflight status endpoint - returns success with allowIgnoreHostPreflights: true server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Succeeded' }, @@ -229,7 +229,7 @@ describe('ValidationStep', () => { it('proceeds normally when allowIgnoreHostPreflights is false and preflights pass', async () => { // Mock preflight status endpoint - returns success with allowIgnoreHostPreflights: false server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Succeeded' }, @@ -279,7 +279,7 @@ describe('ValidationStep', () => { it('sends ignoreHostPreflights parameter when starting installation with failed preflights', async () => { // Mock preflight status endpoint - returns failures with allowIgnoreHostPreflights: true server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Failed' }, @@ -292,7 +292,7 @@ describe('ValidationStep', () => { }); }), // Mock infra setup endpoint to capture request body - http.post('*/api/install/infra/setup', async ({ request }) => { + http.post('*/api/linux/install/infra/setup', async ({ request }) => { const body = await request.json(); // Verify the request includes ignoreHostPreflights parameter @@ -333,7 +333,7 @@ describe('ValidationStep', () => { it('sends ignoreHostPreflights false when starting installation with passed preflights', async () => { // Mock preflight status endpoint - returns success server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Succeeded' }, @@ -346,7 +346,7 @@ describe('ValidationStep', () => { }); }), // Mock infra setup endpoint to capture request body - http.post('*/api/install/infra/setup', async ({ request }) => { + http.post('*/api/linux/install/infra/setup', async ({ request }) => { const body = await request.json(); // Verify the request includes ignoreHostPreflights parameter as false @@ -393,7 +393,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { it('handles API error responses gracefully when starting installation', async () => { // Mock preflight status endpoint - returns success server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Succeeded' }, @@ -402,7 +402,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { }); }), // Mock infra setup endpoint to return API error - http.post('*/api/install/infra/setup', () => { + http.post('*/api/linux/install/infra/setup', () => { return HttpResponse.json( { statusCode: 400, @@ -439,7 +439,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { it('handles network failure during installation start', async () => { // Mock preflight status endpoint - returns success server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Succeeded' }, @@ -448,7 +448,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { }); }), // Mock infra setup endpoint to return network error - http.post('*/api/install/infra/setup', () => { + http.post('*/api/linux/install/infra/setup', () => { return HttpResponse.error(); }) ); @@ -479,7 +479,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { it('handles button states during API interactions', async () => { // Mock preflight status endpoint - returns success server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Succeeded' }, @@ -488,7 +488,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { }); }), // Mock successful infra setup - http.post('*/api/install/infra/setup', () => { + http.post('*/api/linux/install/infra/setup', () => { return HttpResponse.json({ success: true }); }) ); @@ -519,7 +519,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { it('handles error when ignoring preflights without CLI flag', async () => { // Mock preflight status endpoint - returns failures server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Failed' }, @@ -532,7 +532,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { }); }), // Mock infra setup endpoint to return CLI flag error - http.post('*/api/install/infra/setup', () => { + http.post('*/api/linux/install/infra/setup', () => { return HttpResponse.json( { statusCode: 400, @@ -579,7 +579,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { // Mock preflight status endpoint - returns success server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Succeeded' }, @@ -588,7 +588,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { }); }), // Mock infra setup endpoint that fails first, succeeds second - http.post('*/api/install/infra/setup', () => { + http.post('*/api/linux/install/infra/setup', () => { if (shouldFail) { shouldFail = false; // Succeed on next attempt return HttpResponse.json( @@ -632,7 +632,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { it('properly handles modal cancellation flow', async () => { // Mock preflight status endpoint - returns failures server.use( - http.get('*/api/install/host-preflights/status', () => { + http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ titles: ['Host Check'], status: { state: 'Failed' }, diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 49c54e052..9c93dabee 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -37,7 +37,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children useEffect(() => { if (token) { // Make a request to any authenticated endpoint to check token validity - fetch("/api/install/installation/config", { + fetch("/api/linux/install/installation/config", { headers: { Authorization: `Bearer ${token}`, }, From 3c85d28b3043e2fcad9f09777c6482717f105d86 Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Tue, 24 Jun 2025 10:15:18 -0700 Subject: [PATCH 28/48] Unexport certain API types and functions (#2366) --- api/api.go | 32 ++++++++++++++++---------------- api/handlers.go | 9 ++++----- api/routes.go | 8 ++++---- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/api/api.go b/api/api.go index 2b594b4ed..36f564f17 100644 --- a/api/api.go +++ b/api/api.go @@ -31,7 +31,7 @@ import ( // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ -type API struct { +type api struct { cfg types.APIConfig logger logrus.FieldLogger @@ -41,43 +41,43 @@ type API struct { consoleController console.Controller linuxInstallController linuxinstall.Controller - handlers Handlers + handlers handlers } -type APIOption func(*API) +type apiOption func(*api) -func WithAuthController(authController auth.Controller) APIOption { - return func(a *API) { +func WithAuthController(authController auth.Controller) apiOption { + return func(a *api) { a.authController = authController } } -func WithConsoleController(consoleController console.Controller) APIOption { - return func(a *API) { +func WithConsoleController(consoleController console.Controller) apiOption { + return func(a *api) { a.consoleController = consoleController } } -func WithLinuxInstallController(linuxInstallController linuxinstall.Controller) APIOption { - return func(a *API) { +func WithLinuxInstallController(linuxInstallController linuxinstall.Controller) apiOption { + return func(a *api) { a.linuxInstallController = linuxInstallController } } -func WithLogger(logger logrus.FieldLogger) APIOption { - return func(a *API) { +func WithLogger(logger logrus.FieldLogger) apiOption { + return func(a *api) { a.logger = logger } } -func WithMetricsReporter(metricsReporter metrics.ReporterInterface) APIOption { - return func(a *API) { +func WithMetricsReporter(metricsReporter metrics.ReporterInterface) apiOption { + return func(a *api) { a.metricsReporter = metricsReporter } } -func New(cfg types.APIConfig, opts ...APIOption) (*API, error) { - api := &API{ +func New(cfg types.APIConfig, opts ...apiOption) (*api, error) { + api := &api{ cfg: cfg, } @@ -97,7 +97,7 @@ func New(cfg types.APIConfig, opts ...APIOption) (*API, error) { api.logger = l } - if err := api.InitHandlers(api.cfg); err != nil { + if err := api.initHandlers(); err != nil { return nil, fmt.Errorf("init handlers: %w", err) } diff --git a/api/handlers.go b/api/handlers.go index cfc7e8fd5..3864eb4b0 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -7,20 +7,19 @@ import ( consolehandler "github.com/replicatedhq/embedded-cluster/api/internal/handlers/console" healthhandler "github.com/replicatedhq/embedded-cluster/api/internal/handlers/health" linuxhandler "github.com/replicatedhq/embedded-cluster/api/internal/handlers/linux" - "github.com/replicatedhq/embedded-cluster/api/types" ) -type Handlers struct { +type handlers struct { auth *authhandler.Handler console *consolehandler.Handler health *healthhandler.Handler linux *linuxhandler.Handler } -func (a *API) InitHandlers(cfg types.APIConfig) error { +func (a *api) initHandlers() error { // Auth handler authHandler, err := authhandler.New( - cfg.Password, + a.cfg.Password, authhandler.WithLogger(a.logger), authhandler.WithAuthController(a.authController), ) @@ -50,7 +49,7 @@ func (a *API) InitHandlers(cfg types.APIConfig) error { // Linux handler linuxHandler, err := linuxhandler.New( - cfg, + a.cfg, linuxhandler.WithLogger(a.logger), linuxhandler.WithMetricsReporter(a.metricsReporter), linuxhandler.WithInstallController(a.linuxInstallController), diff --git a/api/routes.go b/api/routes.go index 74a60e125..f036efd7a 100644 --- a/api/routes.go +++ b/api/routes.go @@ -8,7 +8,7 @@ import ( httpSwagger "github.com/swaggo/http-swagger/v2" ) -func (a *API) RegisterRoutes(router *mux.Router) { +func (a *api) RegisterRoutes(router *mux.Router) { router.HandleFunc("/health", a.handlers.health.GetHealth).Methods("GET") // Hack to fix issue @@ -30,7 +30,7 @@ func (a *API) RegisterRoutes(router *mux.Router) { a.registerConsoleRoutes(authenticatedRouter) } -func (a *API) registerLinuxRoutes(router *mux.Router) { +func (a *api) registerLinuxRoutes(router *mux.Router) { linuxRouter := router.PathPrefix("/linux").Subrouter() installRouter := linuxRouter.PathPrefix("/install").Subrouter() @@ -50,11 +50,11 @@ func (a *API) registerLinuxRoutes(router *mux.Router) { installRouter.HandleFunc("/status", a.handlers.linux.PostSetStatus).Methods("POST") } -func (a *API) registerKubernetesRoutes(router *mux.Router) { +func (a *api) registerKubernetesRoutes(router *mux.Router) { // kubernetesRouter := router.PathPrefix("/kubernetes").Subrouter() } -func (a *API) registerConsoleRoutes(router *mux.Router) { +func (a *api) registerConsoleRoutes(router *mux.Router) { consoleRouter := router.PathPrefix("/console").Subrouter() consoleRouter.HandleFunc("/available-network-interfaces", a.handlers.console.GetListAvailableNetworkInterfaces).Methods("GET") } From 4e416be8b36e6b7f7ff4048d8b0da18376734fd7 Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Tue, 24 Jun 2025 12:12:31 -0700 Subject: [PATCH 29/48] Introduce KubernetesInstallation Type (#2367) * Introduce KubernetesInstallation Type * generate * f * f --- .../v1beta1/kubernetes_installation_types.go | 88 ++++++++++++ kinds/apis/v1beta1/zz_generated.deepcopy.go | 66 +++++++++ pkg/kubernetesinstallation/installation.go | 128 ++++++++++++++++++ pkg/kubernetesinstallation/interface.go | 25 ++++ pkg/kubernetesinstallation/mock.go | 82 +++++++++++ 5 files changed, 389 insertions(+) create mode 100644 kinds/apis/v1beta1/kubernetes_installation_types.go create mode 100644 pkg/kubernetesinstallation/installation.go create mode 100644 pkg/kubernetesinstallation/interface.go create mode 100644 pkg/kubernetesinstallation/mock.go diff --git a/kinds/apis/v1beta1/kubernetes_installation_types.go b/kinds/apis/v1beta1/kubernetes_installation_types.go new file mode 100644 index 000000000..5e4e3da0c --- /dev/null +++ b/kinds/apis/v1beta1/kubernetes_installation_types.go @@ -0,0 +1,88 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type KubernetesInstallationState string + +// What follows is a list of all valid states for an KubernetesInstallation object. +const ( + KubernetesInstallationStateEnqueued KubernetesInstallationState = "Enqueued" + KubernetesInstallationStateInstalling KubernetesInstallationState = "Installing" + KubernetesInstallationStateInstalled KubernetesInstallationState = "Installed" + KubernetesInstallationStateAddonsInstalling KubernetesInstallationState = "AddonsInstalling" + KubernetesInstallationStateAddonsInstalled KubernetesInstallationState = "AddonsInstalled" + KubernetesInstallationStateObsolete KubernetesInstallationState = "Obsolete" + KubernetesInstallationStateFailed KubernetesInstallationState = "Failed" + KubernetesInstallationStateUnknown KubernetesInstallationState = "Unknown" +) + +// KubernetesInstallationSpec defines the desired state of KubernetesInstallation. +type KubernetesInstallationSpec struct { + // ClusterID holds the cluster id, generated during the installation. + ClusterID string `json:"clusterID,omitempty"` + // MetricsBaseURL holds the base URL for the metrics server. + MetricsBaseURL string `json:"metricsBaseURL,omitempty"` + // Config holds the configuration used at installation time. + Config *ConfigSpec `json:"config,omitempty"` + // BinaryName holds the name of the binary used to install the cluster. + // this will follow the pattern 'appslug-channelslug' + BinaryName string `json:"binaryName,omitempty"` + // LicenseInfo holds information about the license used to install the cluster. + LicenseInfo *LicenseInfo `json:"licenseInfo,omitempty"` + // Proxy holds the proxy configuration. + Proxy *ProxySpec `json:"proxy,omitempty"` + // AdminConsole holds the Admin Console configuration. + AdminConsole AdminConsoleSpec `json:"adminConsole,omitempty"` + // Manager holds the Manager configuration. + Manager ManagerSpec `json:"manager,omitempty"` + // HighAvailability indicates if the installation is high availability. + HighAvailability bool `json:"highAvailability,omitempty"` + // AirGap indicates if the installation is airgapped. + AirGap bool `json:"airGap,omitempty"` +} + +// KubernetesInstallationStatus defines the observed state of KubernetesInstallation +type KubernetesInstallationStatus struct { + // State holds the current state of the installation. + State KubernetesInstallationState `json:"state,omitempty"` + // Reason holds the reason for the current state. + Reason string `json:"reason,omitempty"` +} + +// KubernetesInstallation is the Schema for the kubernetes installations API +type KubernetesInstallation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec KubernetesInstallationSpec `json:"spec,omitempty"` + Status KubernetesInstallationStatus `json:"status,omitempty"` +} + +func GetDefaultKubernetesInstallationSpec() KubernetesInstallationSpec { + c := KubernetesInstallationSpec{} + kubernetesInstallationSpecSetDefaults(&c) + return c +} + +func kubernetesInstallationSpecSetDefaults(c *KubernetesInstallationSpec) { + adminConsoleSpecSetDefaults(&c.AdminConsole) + managerSpecSetDefaults(&c.Manager) +} diff --git a/kinds/apis/v1beta1/zz_generated.deepcopy.go b/kinds/apis/v1beta1/zz_generated.deepcopy.go index 39ca1dca3..881191eef 100644 --- a/kinds/apis/v1beta1/zz_generated.deepcopy.go +++ b/kinds/apis/v1beta1/zz_generated.deepcopy.go @@ -437,6 +437,72 @@ func (in *InstallationStatus) DeepCopy() *InstallationStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesInstallation) DeepCopyInto(out *KubernetesInstallation) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesInstallation. +func (in *KubernetesInstallation) DeepCopy() *KubernetesInstallation { + if in == nil { + return nil + } + out := new(KubernetesInstallation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesInstallationSpec) DeepCopyInto(out *KubernetesInstallationSpec) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(ConfigSpec) + (*in).DeepCopyInto(*out) + } + if in.LicenseInfo != nil { + in, out := &in.LicenseInfo, &out.LicenseInfo + *out = new(LicenseInfo) + **out = **in + } + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(ProxySpec) + **out = **in + } + out.AdminConsole = in.AdminConsole + out.Manager = in.Manager +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesInstallationSpec. +func (in *KubernetesInstallationSpec) DeepCopy() *KubernetesInstallationSpec { + if in == nil { + return nil + } + out := new(KubernetesInstallationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesInstallationStatus) DeepCopyInto(out *KubernetesInstallationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesInstallationStatus. +func (in *KubernetesInstallationStatus) DeepCopy() *KubernetesInstallationStatus { + if in == nil { + return nil + } + out := new(KubernetesInstallationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LicenseInfo) DeepCopyInto(out *LicenseInfo) { *out = *in diff --git a/pkg/kubernetesinstallation/installation.go b/pkg/kubernetesinstallation/installation.go new file mode 100644 index 000000000..89e992161 --- /dev/null +++ b/pkg/kubernetesinstallation/installation.go @@ -0,0 +1,128 @@ +package kubernetesinstallation + +import ( + "os" + + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" +) + +var _ Installation = &kubernetesInstallation{} + +type Option func(*kubernetesInstallation) + +type EnvSetter interface { + Setenv(key string, val string) error +} + +type kubernetesInstallation struct { + installation *ecv1beta1.KubernetesInstallation + envSetter EnvSetter +} + +type osEnvSetter struct{} + +func (o *osEnvSetter) Setenv(key string, val string) error { + return os.Setenv(key, val) +} + +func WithEnvSetter(envSetter EnvSetter) Option { + return func(rc *kubernetesInstallation) { + rc.envSetter = envSetter + } +} + +// New creates a new KubernetesInstallation instance +func New(installation *ecv1beta1.KubernetesInstallation, opts ...Option) Installation { + if installation == nil { + installation = &ecv1beta1.KubernetesInstallation{ + Spec: ecv1beta1.GetDefaultKubernetesInstallationSpec(), + } + } + + ki := &kubernetesInstallation{installation: installation} + for _, opt := range opts { + opt(ki) + } + + if ki.envSetter == nil { + ki.envSetter = &osEnvSetter{} + } + + return ki +} + +// Get returns the KubernetesInstallation. +func (ki *kubernetesInstallation) Get() *ecv1beta1.KubernetesInstallation { + return ki.installation +} + +// Set sets the KubernetesInstallation. +func (ki *kubernetesInstallation) Set(installation *ecv1beta1.KubernetesInstallation) { + if installation == nil { + return + } + ki.installation = installation +} + +// GetSpec returns the spec for the KubernetesInstallation. +func (ki *kubernetesInstallation) GetSpec() ecv1beta1.KubernetesInstallationSpec { + return ki.installation.Spec +} + +// SetSpec sets the spec for the KubernetesInstallation. +func (ki *kubernetesInstallation) SetSpec(spec ecv1beta1.KubernetesInstallationSpec) { + ki.installation.Spec = spec +} + +// GetStatus returns the status for the KubernetesInstallation. +func (ki *kubernetesInstallation) GetStatus() ecv1beta1.KubernetesInstallationStatus { + return ki.installation.Status +} + +// SetStatus sets the status for the KubernetesInstallation. +func (ki *kubernetesInstallation) SetStatus(status ecv1beta1.KubernetesInstallationStatus) { + ki.installation.Status = status +} + +// SetEnv sets the environment variables for the KubernetesInstallation. +func (ki *kubernetesInstallation) SetEnv() error { + return nil +} + +// AdminConsolePort returns the configured port for the admin console or the default if not +// configured. +func (ki *kubernetesInstallation) AdminConsolePort() int { + if ki.installation.Spec.AdminConsole.Port > 0 { + return ki.installation.Spec.AdminConsole.Port + } + return ecv1beta1.DefaultAdminConsolePort +} + +// ManagerPort returns the configured port for the manager or the default if not +// configured. +func (ki *kubernetesInstallation) ManagerPort() int { + if ki.installation.Spec.Manager.Port > 0 { + return ki.installation.Spec.Manager.Port + } + return ecv1beta1.DefaultManagerPort +} + +// ProxySpec returns the configured proxy spec or nil if not configured. +func (ki *kubernetesInstallation) ProxySpec() *ecv1beta1.ProxySpec { + return ki.installation.Spec.Proxy +} + +// SetAdminConsolePort sets the port for the admin console. +func (ki *kubernetesInstallation) SetAdminConsolePort(port int) { + ki.installation.Spec.AdminConsole.Port = port +} + +// SetManagerPort sets the port for the manager. +func (ki *kubernetesInstallation) SetManagerPort(port int) { + ki.installation.Spec.Manager.Port = port +} + +// SetProxySpec sets the proxy spec for the kubernetes installation. +func (ki *kubernetesInstallation) SetProxySpec(proxySpec *ecv1beta1.ProxySpec) { + ki.installation.Spec.Proxy = proxySpec +} diff --git a/pkg/kubernetesinstallation/interface.go b/pkg/kubernetesinstallation/interface.go new file mode 100644 index 000000000..2539c0d0e --- /dev/null +++ b/pkg/kubernetesinstallation/interface.go @@ -0,0 +1,25 @@ +package kubernetesinstallation + +import ( + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" +) + +// Installation defines the interface for managing kubernetes installation +type Installation interface { + Get() *ecv1beta1.KubernetesInstallation + Set(installation *ecv1beta1.KubernetesInstallation) + + GetSpec() ecv1beta1.KubernetesInstallationSpec + SetSpec(spec ecv1beta1.KubernetesInstallationSpec) + + GetStatus() ecv1beta1.KubernetesInstallationStatus + SetStatus(status ecv1beta1.KubernetesInstallationStatus) + + AdminConsolePort() int + ManagerPort() int + ProxySpec() *ecv1beta1.ProxySpec + + SetAdminConsolePort(port int) + SetManagerPort(port int) + SetProxySpec(proxySpec *ecv1beta1.ProxySpec) +} diff --git a/pkg/kubernetesinstallation/mock.go b/pkg/kubernetesinstallation/mock.go new file mode 100644 index 000000000..31cdf3e9f --- /dev/null +++ b/pkg/kubernetesinstallation/mock.go @@ -0,0 +1,82 @@ +package kubernetesinstallation + +import ( + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/stretchr/testify/mock" +) + +var _ Installation = (*MockInstallation)(nil) + +// MockInstallation is a mock implementation of the KubernetesInstallation interface +type MockInstallation struct { + mock.Mock +} + +// Get mocks the Get method +func (m *MockInstallation) Get() *ecv1beta1.KubernetesInstallation { + args := m.Called() + if args.Get(0) == nil { + return nil + } + return args.Get(0).(*ecv1beta1.KubernetesInstallation) +} + +// Set mocks the Set method +func (m *MockInstallation) Set(installation *ecv1beta1.KubernetesInstallation) { + m.Called(installation) +} + +// GetSpec mocks the GetSpec method +func (m *MockInstallation) GetSpec() ecv1beta1.KubernetesInstallationSpec { + args := m.Called() + return args.Get(0).(ecv1beta1.KubernetesInstallationSpec) +} + +// SetSpec mocks the SetSpec method +func (m *MockInstallation) SetSpec(spec ecv1beta1.KubernetesInstallationSpec) { + m.Called(spec) +} + +// GetStatus mocks the GetStatus method +func (m *MockInstallation) GetStatus() ecv1beta1.KubernetesInstallationStatus { + args := m.Called() + return args.Get(0).(ecv1beta1.KubernetesInstallationStatus) +} + +// SetStatus mocks the SetStatus method +func (m *MockInstallation) SetStatus(status ecv1beta1.KubernetesInstallationStatus) { + m.Called(status) +} + +// AdminConsolePort mocks the AdminConsolePort method +func (m *MockInstallation) AdminConsolePort() int { + args := m.Called() + return args.Int(0) +} + +// ManagerPort mocks the ManagerPort method +func (m *MockInstallation) ManagerPort() int { + args := m.Called() + return args.Int(0) +} + +// ProxySpec mocks the ProxySpec method +func (m *MockInstallation) ProxySpec() *ecv1beta1.ProxySpec { + args := m.Called() + return args.Get(0).(*ecv1beta1.ProxySpec) +} + +// SetAdminConsolePort mocks the SetAdminConsolePort method +func (m *MockInstallation) SetAdminConsolePort(port int) { + m.Called(port) +} + +// SetManagerPort mocks the SetManagerPort method +func (m *MockInstallation) SetManagerPort(port int) { + m.Called(port) +} + +// SetProxySpec mocks the SetProxySpec method +func (m *MockInstallation) SetProxySpec(proxySpec *ecv1beta1.ProxySpec) { + m.Called(proxySpec) +} From e801e7e216046b13bb93a2fb5a863da6d472711b Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 24 Jun 2025 14:55:59 -0700 Subject: [PATCH 30/48] chore(api): utils package should be internal (#2368) --- api/controllers/console/controller.go | 2 +- api/controllers/linux/install/controller.go | 2 +- api/controllers/linux/install/hostpreflight.go | 2 +- api/integration/auth_controller_test.go | 2 +- api/integration/console_test.go | 2 +- api/integration/install_test.go | 2 +- api/internal/managers/infra/install.go | 2 +- api/internal/managers/installation/config_test.go | 2 +- api/internal/managers/installation/manager.go | 2 +- api/internal/managers/preflight/hostpreflight_test.go | 2 +- api/internal/managers/preflight/manager.go | 2 +- api/{pkg => internal}/utils/domains.go | 0 api/{pkg => internal}/utils/netutils.go | 0 api/{pkg => internal}/utils/netutils_mock.go | 0 14 files changed, 11 insertions(+), 11 deletions(-) rename api/{pkg => internal}/utils/domains.go (100%) rename api/{pkg => internal}/utils/netutils.go (100%) rename api/{pkg => internal}/utils/netutils_mock.go (100%) diff --git a/api/controllers/console/controller.go b/api/controllers/console/controller.go index d1eb315de..c2bea48e2 100644 --- a/api/controllers/console/controller.go +++ b/api/controllers/console/controller.go @@ -1,7 +1,7 @@ package console import ( - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" ) type Controller interface { diff --git a/api/controllers/linux/install/controller.go b/api/controllers/linux/install/controller.go index e3265388a..08368bc6f 100644 --- a/api/controllers/linux/install/controller.go +++ b/api/controllers/linux/install/controller.go @@ -9,8 +9,8 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" "github.com/replicatedhq/embedded-cluster/api/internal/store" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" diff --git a/api/controllers/linux/install/hostpreflight.go b/api/controllers/linux/install/hostpreflight.go index 71481ae02..cc4e1e174 100644 --- a/api/controllers/linux/install/hostpreflight.go +++ b/api/controllers/linux/install/hostpreflight.go @@ -6,7 +6,7 @@ import ( "runtime/debug" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg/netutils" ) diff --git a/api/integration/auth_controller_test.go b/api/integration/auth_controller_test.go index df903cca6..02aef1ffd 100644 --- a/api/integration/auth_controller_test.go +++ b/api/integration/auth_controller_test.go @@ -13,8 +13,8 @@ import ( "github.com/replicatedhq/embedded-cluster/api/controllers/auth" linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/api/integration/console_test.go b/api/integration/console_test.go index 92e7bc99e..f16b6b5e0 100644 --- a/api/integration/console_test.go +++ b/api/integration/console_test.go @@ -9,8 +9,8 @@ import ( "github.com/gorilla/mux" "github.com/replicatedhq/embedded-cluster/api" "github.com/replicatedhq/embedded-cluster/api/controllers/console" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/api/integration/install_test.go b/api/integration/install_test.go index d88fc45ad..645fb9e4e 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -24,8 +24,8 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" preflightstore "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" diff --git a/api/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index 41eee9013..9ac224aea 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -6,7 +6,7 @@ import ( "runtime/debug" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" diff --git a/api/internal/managers/installation/config_test.go b/api/internal/managers/installation/config_test.go index b433f42dd..ddf0f23b5 100644 --- a/api/internal/managers/installation/config_test.go +++ b/api/internal/managers/installation/config_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" diff --git a/api/internal/managers/installation/manager.go b/api/internal/managers/installation/manager.go index bc462389c..1410e8a05 100644 --- a/api/internal/managers/installation/manager.go +++ b/api/internal/managers/installation/manager.go @@ -4,8 +4,8 @@ import ( "context" "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" diff --git a/api/internal/managers/preflight/hostpreflight_test.go b/api/internal/managers/preflight/hostpreflight_test.go index 6fea05ab5..158dc15d0 100644 --- a/api/internal/managers/preflight/hostpreflight_test.go +++ b/api/internal/managers/preflight/hostpreflight_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" preflightstore "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" diff --git a/api/internal/managers/preflight/manager.go b/api/internal/managers/preflight/manager.go index e2c8285f8..82f9e7e75 100644 --- a/api/internal/managers/preflight/manager.go +++ b/api/internal/managers/preflight/manager.go @@ -4,8 +4,8 @@ import ( "context" "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" + "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" "github.com/replicatedhq/embedded-cluster/pkg/metrics" diff --git a/api/pkg/utils/domains.go b/api/internal/utils/domains.go similarity index 100% rename from api/pkg/utils/domains.go rename to api/internal/utils/domains.go diff --git a/api/pkg/utils/netutils.go b/api/internal/utils/netutils.go similarity index 100% rename from api/pkg/utils/netutils.go rename to api/internal/utils/netutils.go diff --git a/api/pkg/utils/netutils_mock.go b/api/internal/utils/netutils_mock.go similarity index 100% rename from api/pkg/utils/netutils_mock.go rename to api/internal/utils/netutils_mock.go From b2401d12a4c43aace231bce50128599f8a535ee7 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 24 Jun 2025 18:57:36 -0700 Subject: [PATCH 31/48] chore(web): text on welcome string reflects install target (#2369) * chore(web): text on welcome string reflects install target * f * f --- Makefile | 2 + cmd/installer/cli/api.go | 48 ++-------------- cmd/installer/cli/install.go | 55 +++++++++++++------ .../components/wizard/setup/LinuxSetup.tsx | 2 +- .../wizard/tests/SetupStep.test.tsx | 9 ++- web/src/contexts/ConfigContext.tsx | 4 +- web/src/contexts/WizardModeContext.tsx | 15 ++--- web/src/global.d.ts | 1 + web/src/test/setup.tsx | 6 +- web/src/test/testData.ts | 4 +- web/static.go | 5 +- 11 files changed, 69 insertions(+), 82 deletions(-) diff --git a/Makefile b/Makefile index 21de5ec1f..7e57e0118 100644 --- a/Makefile +++ b/Makefile @@ -343,6 +343,7 @@ create-node%: DISTRO = debian-bookworm create-node%: NODE_PORT = 30000 create-node%: MANAGER_NODE_PORT = 30080 create-node%: K0S_DATA_DIR = /var/lib/embedded-cluster/k0s +create-node%: ENABLE_V3 = 0 create-node%: @docker run -d \ --name node$* \ @@ -356,6 +357,7 @@ create-node%: $(if $(filter node0,node$*),-p $(MANAGER_NODE_PORT):$(MANAGER_NODE_PORT)) \ $(if $(filter node0,node$*),-p 30003:30003) \ -e EC_PUBLIC_ADDRESS=localhost \ + -e ENABLE_V3=$(ENABLE_V3) \ replicated/ec-distro:$(DISTRO) @$(MAKE) ssh-node$* diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go index 884037230..5640fa5d0 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -14,7 +14,6 @@ import ( "github.com/gorilla/mux" "github.com/replicatedhq/embedded-cluster/api" - apiclient "github.com/replicatedhq/embedded-cluster/api/client" apilogger "github.com/replicatedhq/embedded-cluster/api/pkg/logger" apitypes "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" @@ -29,6 +28,7 @@ import ( // apiOptions holds the configuration options for the API server type apiOptions struct { + InstallTarget string RuntimeConfig runtimeconfig.RuntimeConfig Logger logrus.FieldLogger MetricsReporter metrics.ReporterInterface @@ -103,8 +103,9 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, } webServer, err := web.New(web.InitialState{ - Title: opts.ReleaseData.Application.Spec.Title, - Icon: opts.ReleaseData.Application.Spec.Icon, + Title: opts.ReleaseData.Application.Spec.Title, + Icon: opts.ReleaseData.Application.Spec.Icon, + InstallTarget: opts.InstallTarget, }, web.WithLogger(logger), web.WithAssetsFS(opts.WebAssetsFS)) if err != nil { return fmt.Errorf("new web server: %w", err) @@ -123,7 +124,7 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, go func() { <-ctx.Done() logrus.Debugf("Shutting down API") - server.Shutdown(context.Background()) + _ = server.Shutdown(context.Background()) }() return server.ServeTLS(listener, "", "") @@ -177,45 +178,6 @@ func waitForAPI(ctx context.Context, addr string) error { } } -func markUIInstallComplete(password string, managerPort int, installErr error) error { - httpClient := &http.Client{ - Transport: &http.Transport{ - Proxy: nil, // This is a local client so no proxy is needed - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - }, - } - apiClient := apiclient.New( - fmt.Sprintf("https://localhost:%d", managerPort), - apiclient.WithHTTPClient(httpClient), - ) - if err := apiClient.Authenticate(password); err != nil { - return fmt.Errorf("unable to authenticate: %w", err) - } - - var state apitypes.State - var description string - if installErr != nil { - state = apitypes.StateFailed - description = fmt.Sprintf("Installation failed: %v", installErr) - } else { - state = apitypes.StateSucceeded - description = "Installation succeeded" - } - - _, err := apiClient.SetInstallStatus(apitypes.Status{ - State: state, - Description: description, - LastUpdated: time.Now(), - }) - if err != nil { - return fmt.Errorf("unable to set install status: %w", err) - } - - return nil -} - func getManagerURL(hostname string, port int) string { if hostname != "" { return fmt.Sprintf("https://%s:%v", hostname, port) diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index e79f58f9c..80c53faa8 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -7,6 +7,7 @@ import ( "fmt" "io/fs" "os" + "slices" "strings" "syscall" "time" @@ -65,6 +66,7 @@ type InstallCmdFlags struct { // guided UI flags enableManagerExperience bool + target string managerPort int tlsCertFile string tlsKeyFile string @@ -152,6 +154,13 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { } func addInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { + cmd.Flags().StringVar(&flags.target, "target", "linux", "The target platform to install to. Valid options are 'linux' or 'kubernetes'.") + if os.Getenv("ENABLE_V3") != "1" { + if err := cmd.Flags().MarkHidden("target"); err != nil { + return err + } + } + cmd.Flags().StringVar(&flags.airgapBundle, "airgap-bundle", "", "Path to the air gap bundle. If set, the installation will complete without internet access.") cmd.Flags().StringVar(&flags.dataDir, "data-dir", ecv1beta1.DefaultDataDir, "Path to the data directory") cmd.Flags().IntVar(&flags.localArtifactMirrorPort, "local-artifact-mirror-port", ecv1beta1.DefaultLocalArtifactMirrorPort, "Port on which the Local Artifact Mirror will be served") @@ -204,26 +213,33 @@ func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) err } func addManagerExperienceFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { - cmd.Flags().BoolVar(&flags.enableManagerExperience, "manager-experience", false, "Run the browser-based installation experience.") + // If the ENABLE_V3 environment variable is set, default to the new manager experience and do + // not hide the new flags. + enableV3 := os.Getenv("ENABLE_V3") == "1" + + cmd.Flags().BoolVar(&flags.enableManagerExperience, "manager-experience", enableV3, "Run the browser-based installation experience.") + if err := cmd.Flags().MarkHidden("manager-experience"); err != nil { + return err + } + cmd.Flags().IntVar(&flags.managerPort, "manager-port", ecv1beta1.DefaultManagerPort, "Port on which the Manager will be served") cmd.Flags().StringVar(&flags.tlsCertFile, "tls-cert", "", "Path to the TLS certificate file") cmd.Flags().StringVar(&flags.tlsKeyFile, "tls-key", "", "Path to the TLS key file") cmd.Flags().StringVar(&flags.hostname, "hostname", "", "Hostname to use for TLS configuration") - if err := cmd.Flags().MarkHidden("manager-experience"); err != nil { - return err - } - if err := cmd.Flags().MarkHidden("manager-port"); err != nil { - return err - } - if err := cmd.Flags().MarkHidden("tls-cert"); err != nil { - return err - } - if err := cmd.Flags().MarkHidden("tls-key"); err != nil { - return err - } - if err := cmd.Flags().MarkHidden("hostname"); err != nil { - return err + if !enableV3 { + if err := cmd.Flags().MarkHidden("manager-port"); err != nil { + return err + } + if err := cmd.Flags().MarkHidden("tls-cert"); err != nil { + return err + } + if err := cmd.Flags().MarkHidden("tls-key"); err != nil { + return err + } + if err := cmd.Flags().MarkHidden("hostname"); err != nil { + return err + } } return nil @@ -238,6 +254,10 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. // this does not return an error - it returns the previous umask _ = syscall.Umask(0o022) + if !slices.Contains([]string{"linux", "kubernetes"}, flags.target) { + return fmt.Errorf(`invalid target (must be one of: "linux", "kubernetes")`) + } + // license file can be empty for restore if flags.licenseFile != "" { b, err := os.ReadFile(flags.licenseFile) @@ -414,10 +434,11 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc } apiConfig := apiOptions{ + InstallTarget: flags.target, + RuntimeConfig: rc, // TODO (@salah): implement reporting in api // MetricsReporter: installReporter, - RuntimeConfig: rc, - Password: flags.adminConsolePassword, + Password: flags.adminConsolePassword, TLSConfig: apitypes.TLSConfig{ CertBytes: flags.tlsCertBytes, KeyBytes: flags.tlsKeyBytes, diff --git a/web/src/components/wizard/setup/LinuxSetup.tsx b/web/src/components/wizard/setup/LinuxSetup.tsx index f6c3187d5..198a288ed 100644 --- a/web/src/components/wizard/setup/LinuxSetup.tsx +++ b/web/src/components/wizard/setup/LinuxSetup.tsx @@ -37,7 +37,7 @@ interface LinuxSetupProps { globalCidr?: string; }; prototypeSettings: { - clusterMode: string; + installTarget: string; availableNetworkInterfaces?: Array<{ name: string; }>; diff --git a/web/src/components/wizard/tests/SetupStep.test.tsx b/web/src/components/wizard/tests/SetupStep.test.tsx index 4e2cef767..0b31971af 100644 --- a/web/src/components/wizard/tests/SetupStep.test.tsx +++ b/web/src/components/wizard/tests/SetupStep.test.tsx @@ -26,7 +26,6 @@ const server = setupServer( describe("SetupStep", () => { const mockOnNext = vi.fn(); - const mockOnBack = vi.fn(); beforeAll(() => { server.listen(); @@ -45,8 +44,8 @@ describe("SetupStep", () => { server.close(); }); - it("renders the linux setup form when it's embedded", async () => { - renderWithProviders(, { + it("renders the linux setup form when the install target is linux", async () => { + renderWithProviders(, { wrapperProps: { authenticated: true, preloadedState: { @@ -127,7 +126,7 @@ describe("SetupStep", () => { }) ); - renderWithProviders(, { + renderWithProviders(, { wrapperProps: { authenticated: true, preloadedState: { @@ -230,7 +229,7 @@ describe("SetupStep", () => { }) ); - renderWithProviders(, { + renderWithProviders(, { wrapperProps: { authenticated: true, preloadedState: { diff --git a/web/src/contexts/ConfigContext.tsx b/web/src/contexts/ConfigContext.tsx index eb4eaf76b..f20606853 100644 --- a/web/src/contexts/ConfigContext.tsx +++ b/web/src/contexts/ConfigContext.tsx @@ -18,7 +18,7 @@ interface PrototypeSettings { failPreflights: boolean; failInstallation: boolean; failHostPreflights: boolean; - clusterMode: 'existing' | 'embedded'; + installTarget: 'linux' | 'kubernetes'; themeColor: string; skipNodeValidation: boolean; useSelfSignedCert: boolean; @@ -47,7 +47,7 @@ const defaultPrototypeSettings: PrototypeSettings = { failPreflights: false, failInstallation: false, failHostPreflights: false, - clusterMode: 'embedded', + installTarget: 'linux', themeColor: '#316DE6', skipNodeValidation: false, useSelfSignedCert: false, diff --git a/web/src/contexts/WizardModeContext.tsx b/web/src/contexts/WizardModeContext.tsx index 0acc1d33e..d4a4d7945 100644 --- a/web/src/contexts/WizardModeContext.tsx +++ b/web/src/contexts/WizardModeContext.tsx @@ -1,5 +1,4 @@ import React, { createContext, useContext } from "react"; -import { useConfig } from "./ConfigContext"; import { useBranding } from "./BrandingContext"; export type WizardMode = "install" | "upgrade"; @@ -19,13 +18,13 @@ interface WizardText { nextButtonText: string; } -const getTextVariations = (isEmbedded: boolean, title: string): Record => ({ +const getTextVariations = (isLinux: boolean, title: string): Record => ({ install: { title: title || "", subtitle: "Installation Wizard", welcomeTitle: `Welcome to ${title}`, welcomeDescription: `This wizard will guide you through installing ${title} on your ${ - isEmbedded ? "Linux machine" : "Kubernetes cluster" + isLinux ? "Linux machine" : "Kubernetes cluster" }.`, setupTitle: "Setup", setupDescription: "Configure the host settings for this installation.", @@ -41,7 +40,7 @@ const getTextVariations = (isEmbedded: boolean, title: string): Record = ({ children, mode }) => { - const { prototypeSettings } = useConfig(); + // __INITIAL_STATE__ is a global variable that can be set by the server-side rendering process + // as a way to pass initial data to the client. + const initialState = window.__INITIAL_STATE__ || {}; const { title } = useBranding(); - const isEmbedded = prototypeSettings.clusterMode === "embedded"; - const text = getTextVariations(isEmbedded, title)[mode]; + const isLinux = initialState.installTarget === "linux"; + const text = getTextVariations(isLinux, title)[mode]; return {children}; }; diff --git a/web/src/global.d.ts b/web/src/global.d.ts index ab361cdbe..e67927c53 100644 --- a/web/src/global.d.ts +++ b/web/src/global.d.ts @@ -10,5 +10,6 @@ declare global { interface InitialState { icon?: string; title?: string; + installTarget?: string; } } diff --git a/web/src/test/setup.tsx b/web/src/test/setup.tsx index 103ffb09a..cb6f4a59a 100644 --- a/web/src/test/setup.tsx +++ b/web/src/test/setup.tsx @@ -29,7 +29,7 @@ interface PrototypeSettings { failPreflights: boolean; failInstallation: boolean; failHostPreflights: boolean; - clusterMode: "existing" | "embedded"; + installTarget: "linux" | "kubernetes"; themeColor: string; skipNodeValidation: boolean; useSelfSignedCert: boolean; @@ -91,7 +91,7 @@ const MockProvider = ({ children, queryClient, contexts }: MockProviderProps) => return ( - + {children} @@ -129,7 +129,7 @@ export const renderWithProviders = ( failPreflights: false, failInstallation: false, failHostPreflights: false, - clusterMode: "embedded", + installTarget: "linux", themeColor: "#316DE6", skipNodeValidation: false, useSelfSignedCert: false, diff --git a/web/src/test/testData.ts b/web/src/test/testData.ts index 328d35e23..f71fb0153 100644 --- a/web/src/test/testData.ts +++ b/web/src/test/testData.ts @@ -2,7 +2,7 @@ export const MOCK_INSTALL_CONFIG = { adminConsolePort: 8800, localArtifactMirrorPort: 8801, networkInterface: "eth0", - clusterMode: "embedded", + installTarget: "linux", }; export const MOCK_NETWORK_INTERFACES = { @@ -13,7 +13,7 @@ export const MOCK_NETWORK_INTERFACES = { }; export const MOCK_PROTOTYPE_SETTINGS = { - clusterMode: "embedded", + installTarget: "linux", title: "Test Cluster", description: "Test cluster configuration", }; \ No newline at end of file diff --git a/web/static.go b/web/static.go index 987f7363d..e785308f5 100644 --- a/web/static.go +++ b/web/static.go @@ -28,8 +28,9 @@ func init() { } type InitialState struct { - Title string `json:"title"` - Icon string `json:"icon"` + Title string `json:"title"` + Icon string `json:"icon"` + InstallTarget string `json:"installTarget"` } type Web struct { From fd07258658221791d8c808af61a9f40ea689e0ba Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:28:40 -0400 Subject: [PATCH 32/48] feat: Host Support Bundle Improvements (#2236) * add support for templating based on airgap and proxy for support bundle * add curl commands for comparison with the http collectors * update unit test * update spec * update tests to validate content of actual support bundle template * capture stderr from curl collectors --- cmd/installer/cli/join.go | 3 +- .../support/host-support-bundle.tmpl.yaml | 44 +++ pkg-new/hostutils/files.go | 4 +- pkg/support/materialize.go | 40 ++- pkg/support/materialize_test.go | 257 ++++++++++++++++++ 5 files changed, 339 insertions(+), 9 deletions(-) create mode 100644 pkg/support/materialize_test.go diff --git a/cmd/installer/cli/join.go b/cmd/installer/cli/join.go index 974cce330..c643e7b2f 100644 --- a/cmd/installer/cli/join.go +++ b/cmd/installer/cli/join.go @@ -341,7 +341,8 @@ func materializeFilesForJoin(ctx context.Context, rc runtimeconfig.RuntimeConfig if err := materializer.Materialize(); err != nil { return fmt.Errorf("materialize binaries: %w", err) } - if err := support.MaterializeSupportBundleSpec(rc); err != nil { + + if err := support.MaterializeSupportBundleSpec(rc, jcmd.InstallationSpec.AirGap); err != nil { return fmt.Errorf("materialize support bundle spec: %w", err) } diff --git a/cmd/installer/goods/support/host-support-bundle.tmpl.yaml b/cmd/installer/goods/support/host-support-bundle.tmpl.yaml index 3214058a3..6b8a750cc 100644 --- a/cmd/installer/goods/support/host-support-bundle.tmpl.yaml +++ b/cmd/installer/goods/support/host-support-bundle.tmpl.yaml @@ -173,6 +173,50 @@ spec: collectorName: "ip-route-table" command: "ip" args: ["route"] + - run: + collectorName: "ip-neighbor-show" + command: "ip" + args: ["-s", "-d", "neigh", "show"] + # HTTP connectivity checks (only run for online installations) + - http: + collectorName: http-replicated-app + get: + url: '{{ .ReplicatedAppURL }}/healthz' + timeout: 5s + proxy: '{{ .HTTPSProxy }}' + exclude: '{{ or .IsAirgap (eq .ReplicatedAppURL "") }}' + - http: + collectorName: http-proxy-replicated-com + get: + url: '{{ .ProxyRegistryURL }}/v2/' + timeout: 5s + proxy: '{{ .HTTPSProxy }}' + exclude: '{{ or .IsAirgap (eq .ProxyRegistryURL "") }}' + # Curl-based connectivity checks (for comparison with HTTP collectors) + - run: + collectorName: curl-replicated-app + command: sh + args: + - -c + - | + if [ -n "{{ .HTTPSProxy }}" ]; then + curl --connect-timeout 5 --max-time 10 -v --proxy "{{ .HTTPSProxy }}" "{{ .ReplicatedAppURL }}/healthz" 2>&1 + else + curl --connect-timeout 5 --max-time 10 -v "{{ .ReplicatedAppURL }}" 2>&1 + fi + exclude: '{{ or .IsAirgap (eq .ReplicatedAppURL "") }}' + - run: + collectorName: curl-proxy-replicated-com + command: sh + args: + - -c + - | + if [ -n "{{ .HTTPSProxy }}" ]; then + curl --connect-timeout 5 --max-time 10 -v --proxy "{{ .HTTPSProxy }}" "{{ .ProxyRegistryURL }}/v2/" 2>&1 + else + curl --connect-timeout 5 --max-time 10 -v "{{ .ProxyRegistryURL }}/v2/" 2>&1 + fi + exclude: '{{ or .IsAirgap (eq .ProxyRegistryURL "") }}' - run: collectorName: "ip-address-stats" command: "ip" diff --git a/pkg-new/hostutils/files.go b/pkg-new/hostutils/files.go index 3aaa4a8a9..32378eb3b 100644 --- a/pkg-new/hostutils/files.go +++ b/pkg-new/hostutils/files.go @@ -15,7 +15,9 @@ func (h *HostUtils) MaterializeFiles(rc runtimeconfig.RuntimeConfig, airgapBundl if err := materializer.Materialize(); err != nil { return fmt.Errorf("materialize binaries: %w", err) } - if err := support.MaterializeSupportBundleSpec(rc); err != nil { + + isAirgap := airgapBundle != "" + if err := support.MaterializeSupportBundleSpec(rc, isAirgap); err != nil { return fmt.Errorf("materialize support bundle spec: %w", err) } diff --git a/pkg/support/materialize.go b/pkg/support/materialize.go index 98722ffbb..88e048a17 100644 --- a/pkg/support/materialize.go +++ b/pkg/support/materialize.go @@ -6,21 +6,47 @@ import ( "os" "text/template" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) type TemplateData struct { - DataDir string - K0sDataDir string - OpenEBSDataDir string + DataDir string + K0sDataDir string + OpenEBSDataDir string + IsAirgap bool + ReplicatedAppURL string + ProxyRegistryURL string + HTTPProxy string + HTTPSProxy string + NoProxy string } -func MaterializeSupportBundleSpec(rc runtimeconfig.RuntimeConfig) error { +func MaterializeSupportBundleSpec(rc runtimeconfig.RuntimeConfig, isAirgap bool) error { + var embCfgSpec *ecv1beta1.ConfigSpec + if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { + embCfgSpec = &embCfg.Spec + } + domains := runtimeconfig.GetDomains(embCfgSpec) + data := TemplateData{ - DataDir: rc.EmbeddedClusterHomeDirectory(), - K0sDataDir: rc.EmbeddedClusterK0sSubDir(), - OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + IsAirgap: isAirgap, + ReplicatedAppURL: netutils.MaybeAddHTTPS(domains.ReplicatedAppDomain), + ProxyRegistryURL: netutils.MaybeAddHTTPS(domains.ProxyRegistryDomain), } + + // Add proxy configuration if available + if proxy := rc.ProxySpec(); proxy != nil { + data.HTTPProxy = proxy.HTTPProxy + data.HTTPSProxy = proxy.HTTPSProxy + data.NoProxy = proxy.NoProxy + } + path := rc.PathToEmbeddedClusterSupportFile("host-support-bundle.tmpl.yaml") tmpl, err := os.ReadFile(path) if err != nil { diff --git a/pkg/support/materialize_test.go b/pkg/support/materialize_test.go new file mode 100644 index 000000000..d20f95363 --- /dev/null +++ b/pkg/support/materialize_test.go @@ -0,0 +1,257 @@ +package support + +import ( + "os" + "path/filepath" + "strings" + "testing" + + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMaterializeSupportBundleSpec(t *testing.T) { + tests := []struct { + name string + isAirgap bool + proxySpec *ecv1beta1.ProxySpec + expectedInFile []string + notInFile []string + validateFunc func(t *testing.T, content string) + }{ + { + name: "airgap installation - HTTP collectors excluded", + isAirgap: true, + proxySpec: &ecv1beta1.ProxySpec{ + HTTPSProxy: "https://proxy:8080", + HTTPProxy: "http://proxy:8080", + NoProxy: "localhost,127.0.0.1", + }, + expectedInFile: []string{ + // Core collectors should always be present + "k8s-api-healthz-6443", + "free", + "embedded-cluster-path-usage", + // HTTP collectors are present in template (but will be excluded) + "http-replicated-app", + "curl-replicated-app", + }, + notInFile: []string{ + // Template variables should be substituted + "{{ .ReplicatedAppURL }}", + "{{ .ProxyRegistryURL }}", + "{{ .HTTPSProxy }}", + }, + validateFunc: func(t *testing.T, content string) { + // Validate that HTTP collectors have exclude: 'true' for airgap + assert.Contains(t, content, "collectorName: http-replicated-app") + + // Check that the http-replicated-app collector block has exclude: 'true' + httpCollectorStart := strings.Index(content, "collectorName: http-replicated-app") + require.Greater(t, httpCollectorStart, -1, "http-replicated-app collector should be present") + + // Find the next collector to limit our search scope + nextCollectorStart := strings.Index(content[httpCollectorStart+1:], "collectorName:") + var httpCollectorBlock string + if nextCollectorStart > -1 { + httpCollectorBlock = content[httpCollectorStart : httpCollectorStart+1+nextCollectorStart] + } else { + httpCollectorBlock = content[httpCollectorStart:] + } + + assert.Contains(t, httpCollectorBlock, "exclude: 'true'", + "http-replicated-app collector should be excluded in airgap mode") + + // Also validate curl-replicated-app is excluded + curlCollectorStart := strings.Index(content, "collectorName: curl-replicated-app") + require.Greater(t, curlCollectorStart, -1, "curl-replicated-app collector should be present") + + nextCurlCollectorStart := strings.Index(content[curlCollectorStart+1:], "collectorName:") + var curlCollectorBlock string + if nextCurlCollectorStart > -1 { + curlCollectorBlock = content[curlCollectorStart : curlCollectorStart+1+nextCurlCollectorStart] + } else { + curlCollectorBlock = content[curlCollectorStart:] + } + + assert.Contains(t, curlCollectorBlock, "exclude: 'true'", + "curl-replicated-app collector should be excluded in airgap mode") + }, + }, + { + name: "online installation with proxy - HTTP collectors included", + isAirgap: false, + proxySpec: &ecv1beta1.ProxySpec{ + HTTPSProxy: "https://proxy:8080", + HTTPProxy: "http://proxy:8080", + NoProxy: "localhost,127.0.0.1", + }, + expectedInFile: []string{ + // Core collectors + "k8s-api-healthz-6443", + "free", + "embedded-cluster-path-usage", + // HTTP collectors are included for online + "http-replicated-app", + "curl-replicated-app", + // URLs and proxy settings + "https://replicated.app/healthz", + "https://proxy.replicated.com/v2/", + "proxy: 'https://proxy:8080'", + }, + notInFile: []string{ + // Template variables should be substituted + "{{ .ReplicatedAppURL }}", + "{{ .HTTPSProxy }}", + }, + validateFunc: func(t *testing.T, content string) { + // Validate that HTTP collectors have exclude: 'false' for online + assert.Contains(t, content, "collectorName: http-replicated-app") + + // Check that the http-replicated-app collector block has exclude: 'false' + httpCollectorStart := strings.Index(content, "collectorName: http-replicated-app") + require.Greater(t, httpCollectorStart, -1, "http-replicated-app collector should be present") + + // Find the next collector to limit our search scope + nextCollectorStart := strings.Index(content[httpCollectorStart+1:], "collectorName:") + var httpCollectorBlock string + if nextCollectorStart > -1 { + httpCollectorBlock = content[httpCollectorStart : httpCollectorStart+1+nextCollectorStart] + } else { + httpCollectorBlock = content[httpCollectorStart:] + } + + assert.Contains(t, httpCollectorBlock, "exclude: 'false'", + "http-replicated-app collector should not be excluded in online mode") + }, + }, + { + name: "online installation without proxy - HTTP collectors included, no proxy config", + isAirgap: false, + proxySpec: nil, + expectedInFile: []string{ + // Core collectors + "k8s-api-healthz-6443", + "embedded-cluster-path-usage", + // HTTP collectors included + "http-replicated-app", + "curl-replicated-app", + // URLs populated + "https://replicated.app/healthz", + "https://proxy.replicated.com/v2/", + }, + notInFile: []string{ + // No proxy settings when proxy not configured + "proxy: 'https://proxy:8080'", + "proxy: 'http://proxy:8080'", + // Template variables should be substituted + "{{ .HTTPSProxy }}", + "{{ .HTTPProxy }}", + }, + validateFunc: func(t *testing.T, content string) { + // Validate that HTTP collectors have exclude: 'false' for online + httpCollectorStart := strings.Index(content, "collectorName: http-replicated-app") + require.Greater(t, httpCollectorStart, -1, "http-replicated-app collector should be present") + + nextCollectorStart := strings.Index(content[httpCollectorStart+1:], "collectorName:") + var httpCollectorBlock string + if nextCollectorStart > -1 { + httpCollectorBlock = content[httpCollectorStart : httpCollectorStart+1+nextCollectorStart] + } else { + httpCollectorBlock = content[httpCollectorStart:] + } + + assert.Contains(t, httpCollectorBlock, "exclude: 'false'", + "http-replicated-app collector should not be excluded in online mode") + + // Verify proxy is empty/not set in the collector block + assert.Contains(t, httpCollectorBlock, "proxy: ''", + "proxy should be empty when no proxy is configured") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory for the test + tempDir := t.TempDir() + + // Create the support subdirectory + supportDir := filepath.Join(tempDir, "support") + err := os.MkdirAll(supportDir, 0755) + require.NoError(t, err) + + // Copy the actual customer template to the test directory + actualTemplatePath := filepath.Join("../../cmd/installer/goods/support/host-support-bundle.tmpl.yaml") + templateContent, err := os.ReadFile(actualTemplatePath) + require.NoError(t, err, "Should be able to read the actual customer template") + + // Write the actual template to the test directory + templatePath := filepath.Join(supportDir, "host-support-bundle.tmpl.yaml") + err = os.WriteFile(templatePath, templateContent, 0644) + require.NoError(t, err) + + // Create mock RuntimeConfig + mockRC := &runtimeconfig.MockRuntimeConfig{} + mockRC.On("EmbeddedClusterHomeDirectory").Return(tempDir) + mockRC.On("EmbeddedClusterK0sSubDir").Return(filepath.Join(tempDir, "k0s")) + mockRC.On("EmbeddedClusterOpenEBSLocalSubDir").Return(filepath.Join(tempDir, "openebs")) + mockRC.On("PathToEmbeddedClusterSupportFile", "host-support-bundle.tmpl.yaml").Return(templatePath) + mockRC.On("PathToEmbeddedClusterSupportFile", "host-support-bundle.yaml").Return( + filepath.Join(supportDir, "host-support-bundle.yaml")) + mockRC.On("ProxySpec").Return(tt.proxySpec) + + // Call the function under test + err = MaterializeSupportBundleSpec(mockRC, tt.isAirgap) + require.NoError(t, err) + + // Verify the file was created + outputFile := filepath.Join(supportDir, "host-support-bundle.yaml") + _, err = os.Stat(outputFile) + require.NoError(t, err, "Support bundle spec file should be created") + + // Read the generated file content + content, err := os.ReadFile(outputFile) + require.NoError(t, err) + contentStr := string(content) + + // Verify expected content is present + for _, expected := range tt.expectedInFile { + assert.Contains(t, contentStr, expected, + "Expected %q to be in the generated support bundle spec", expected) + } + + // Verify unwanted content is not present + for _, notExpected := range tt.notInFile { + assert.NotContains(t, contentStr, notExpected, + "Expected %q to NOT be in the generated support bundle spec", notExpected) + } + + // Verify that key template variables were properly substituted + assert.Contains(t, contentStr, tempDir, "Data directory should be substituted") + assert.Contains(t, contentStr, filepath.Join(tempDir, "k0s"), "K0s data directory should be substituted") + assert.Contains(t, contentStr, filepath.Join(tempDir, "openebs"), "OpenEBS data directory should be substituted") + + // Verify the YAML structure is valid + assert.Contains(t, contentStr, "apiVersion: troubleshoot.sh/v1beta2") + assert.Contains(t, contentStr, "kind: SupportBundle") + assert.Contains(t, contentStr, "hostCollectors:") + assert.Contains(t, contentStr, "hostAnalyzers:") + + // Verify key collectors that should always be present + assert.Contains(t, contentStr, "ipv4Interfaces", "Basic network collector should be present") + assert.Contains(t, contentStr, "memory", "Memory collector should be present") + assert.Contains(t, contentStr, "filesystem-write-latency-etcd", "Performance collector should be present") + + // Run the specific validation function for this test case + if tt.validateFunc != nil { + tt.validateFunc(t, contentStr) + } + + // Assert all mock expectations were met + mockRC.AssertExpectations(t) + }) + } +} From 5e18495e5b2fc7cef5af5a597e9cb745d240507e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Antunes?= Date: Wed, 25 Jun 2025 20:26:14 +0100 Subject: [PATCH 33/48] chore: add statemachine event handlers (#2363) * chore: try out state machine event handlers chore: tests chore: keep the PR focused * chore: address PR feedback * chore: address feedback * chore: address lock holding concern --- api/internal/statemachine/event_handler.go | 78 +++++ api/internal/statemachine/interface.go | 34 ++ api/internal/statemachine/statemachine.go | 96 ++++-- .../statemachine/statemachine_test.go | 306 ++++++++++++++++++ 4 files changed, 481 insertions(+), 33 deletions(-) create mode 100644 api/internal/statemachine/event_handler.go create mode 100644 api/internal/statemachine/interface.go diff --git a/api/internal/statemachine/event_handler.go b/api/internal/statemachine/event_handler.go new file mode 100644 index 000000000..d616bfbea --- /dev/null +++ b/api/internal/statemachine/event_handler.go @@ -0,0 +1,78 @@ +package statemachine + +import ( + "context" + "fmt" + "runtime/debug" + "time" +) + +var ( + _ EventHandler = &eventHandler{} +) + +// EventHandler is an interface for handling state transition events in the state machine. +type EventHandler interface { + // TriggerHandler triggers the event handler for a state transition. + TriggerHandler(ctx context.Context, fromState, toState State) error +} + +// EventHandlerFunc is a function that handles state transition events. Used to report state changes. +type EventHandlerFunc func(ctx context.Context, fromState, toState State) + +// EventHandlerOption is a configurable state machine option. +type EventHandlerOption func(*eventHandler) + +// WithHandlerTimeout sets the timeout for the event handler to complete. +func WithHandlerTimeout(timeout time.Duration) EventHandlerOption { + return func(eh *eventHandler) { + eh.timeout = timeout + } +} + +// NewEventHandler creates a new event handler with the provided function and options. +func NewEventHandler(handler EventHandlerFunc, options ...EventHandlerOption) EventHandler { + eh := &eventHandler{ + handler: handler, + timeout: 5 * time.Second, // Default timeout + } + + for _, option := range options { + option(eh) + } + + return eh +} + +// eventHandler is a struct that implements the EventHandler interface. It contains a handler function that is called when a state transition occurs, and it supports a timeout for the handler to complete. +type eventHandler struct { + handler EventHandlerFunc + timeout time.Duration // Timeout for the handler to complete, default is 5 seconds +} + +// TriggerHandler triggers the event handler for a state transition. The trigger is blocking and will wait for the handler to complete or timeout. +func (eh *eventHandler) TriggerHandler(ctx context.Context, fromState, toState State) error { + ctx, cancel := context.WithTimeout(ctx, eh.timeout) + defer cancel() + done := make(chan error, 1) + + go func() { + defer func() { + if r := recover(); r != nil { + // Capture panic but don't affect the transition + err := fmt.Errorf("event handler panic from %s to %s: %v: %s\n", fromState, toState, r, debug.Stack()) + done <- err + } + close(done) + }() + eh.handler(ctx, fromState, toState) + }() + + select { + case err := <-done: + return err + case <-ctx.Done(): + err := fmt.Errorf("event handler for transition from %s to %s timed out after %s", fromState, toState, eh.timeout) + return err + } +} diff --git a/api/internal/statemachine/interface.go b/api/internal/statemachine/interface.go new file mode 100644 index 000000000..93e749483 --- /dev/null +++ b/api/internal/statemachine/interface.go @@ -0,0 +1,34 @@ +package statemachine + +// State represents the possible states of the install process +type State string + +var ( + _ Interface = &stateMachine{} +) + +// Interface is the interface for the state machine +type Interface interface { + // CurrentState returns the current state + CurrentState() State + // IsFinalState checks if the current state is a final state + IsFinalState() bool + // ValidateTransition checks if a transition from the current state to a new state is valid + ValidateTransition(lock Lock, newState State) error + // Transition attempts to transition to a new state and returns an error if the transition is + // invalid. + Transition(lock Lock, nextState State) error + // AcquireLock acquires a lock on the state machine. + AcquireLock() (Lock, error) + // IsLockAcquired checks if a lock already exists on the state machine. + IsLockAcquired() bool + // RegisterEventHandler registers a blocking event handler for reporting events in the state machine. + RegisterEventHandler(targetState State, handler EventHandlerFunc, options ...EventHandlerOption) + // UnregisterEventHandler unregisters a blocking event handler for reporting events in the state machine. + UnregisterEventHandler(targetState State) +} + +type Lock interface { + // Release releases the lock. + Release() +} diff --git a/api/internal/statemachine/statemachine.go b/api/internal/statemachine/statemachine.go index 2aee4362a..312393975 100644 --- a/api/internal/statemachine/statemachine.go +++ b/api/internal/statemachine/statemachine.go @@ -1,54 +1,48 @@ package statemachine import ( + "context" "fmt" "slices" "sync" -) - -// State represents the possible states of the install process -type State string -var ( - _ Interface = &stateMachine{} + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/sirupsen/logrus" ) -// Interface is the interface for the state machine -type Interface interface { - // CurrentState returns the current state - CurrentState() State - // IsFinalState checks if the current state is a final state - IsFinalState() bool - // ValidateTransition checks if a transition from the current state to a new state is valid - ValidateTransition(lock Lock, newState State) error - // Transition attempts to transition to a new state and returns an error if the transition is - // invalid. - Transition(lock Lock, nextState State) error - // AcquireLock acquires a lock on the state machine. - AcquireLock() (Lock, error) - // IsLockAcquired checks if a lock already exists on the state machine. - IsLockAcquired() bool -} - -type Lock interface { - // Release releases the lock. - Release() -} - // stateMachine manages the state transitions for the install process type stateMachine struct { currentState State validStateTransitions map[State][]State lock *lock mu sync.RWMutex + eventHandlers map[State][]EventHandler + logger logrus.FieldLogger } +// StateMachineOption is a configurable state machine option. +type StateMachineOption func(*stateMachine) + // New creates a new state machine starting in the given state with the given valid state -// transitions. -func New(currentState State, validStateTransitions map[State][]State) *stateMachine { - return &stateMachine{ +// transitions and options. +func New(currentState State, validStateTransitions map[State][]State, opts ...StateMachineOption) *stateMachine { + sm := &stateMachine{ currentState: currentState, validStateTransitions: validStateTransitions, + logger: logger.NewDiscardLogger(), + eventHandlers: make(map[State][]EventHandler), + } + + for _, opt := range opts { + opt(sm) + } + + return sm +} + +func WithLogger(logger logrus.FieldLogger) StateMachineOption { + return func(sm *stateMachine) { + sm.logger = logger } } @@ -109,9 +103,13 @@ func (sm *stateMachine) ValidateTransition(lock Lock, nextState State) error { return nil } -func (sm *stateMachine) Transition(lock Lock, nextState State) error { +func (sm *stateMachine) Transition(lock Lock, nextState State) (finalError error) { sm.mu.Lock() - defer sm.mu.Unlock() + defer func() { + if finalError != nil { + sm.mu.Unlock() + } + }() if sm.lock == nil { return fmt.Errorf("lock not acquired") @@ -123,11 +121,43 @@ func (sm *stateMachine) Transition(lock Lock, nextState State) error { return fmt.Errorf("invalid transition from %s to %s", sm.currentState, nextState) } + fromState := sm.currentState sm.currentState = nextState + // Trigger event handlers after successful transition + handlers, exists := sm.eventHandlers[nextState] + safeHandlers := make([]EventHandler, len(handlers)) + copy(safeHandlers, handlers) // Copy to avoid holding the lock while calling handlers + + // We can release the lock here since the transition is successful and there will be no further operations to the state machine internal state + sm.mu.Unlock() + + if !exists || len(safeHandlers) == 0 { + return nil + } + + for _, handler := range safeHandlers { + err := handler.TriggerHandler(context.Background(), fromState, nextState) + if err != nil { + sm.logger.WithFields(logrus.Fields{"fromState": fromState, "toState": nextState}).Errorf("event handler error: %v", err) + } + } + return nil } +func (sm *stateMachine) RegisterEventHandler(targetState State, handler EventHandlerFunc, options ...EventHandlerOption) { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.eventHandlers[targetState] = append(sm.eventHandlers[targetState], NewEventHandler(handler, options...)) +} + +func (sm *stateMachine) UnregisterEventHandler(targetState State) { + sm.mu.Lock() + defer sm.mu.Unlock() + delete(sm.eventHandlers, targetState) +} + func (sm *stateMachine) isValidTransition(currentState State, newState State) bool { validTransitions, ok := sm.validStateTransitions[currentState] if !ok { diff --git a/api/internal/statemachine/statemachine_test.go b/api/internal/statemachine/statemachine_test.go index 953d6d62f..4365cafab 100644 --- a/api/internal/statemachine/statemachine_test.go +++ b/api/internal/statemachine/statemachine_test.go @@ -1,11 +1,14 @@ package statemachine import ( + "context" "slices" "sync" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) const ( @@ -516,3 +519,306 @@ func TestValidateTransitionEdgeCases(t *testing.T) { lock.Release() } + +func TestEventHandlerRegistrationAndTriggering(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + // Create a mock handler + mockHandler := &MockEventHandler{} + mockHandler.On("Handle", mock.Anything, StateNew, StateInstallationConfigured).Return() + + // Register event handler + handler := func(ctx context.Context, from, to State) { + mockHandler.Handle(ctx, from, to) + } + + sm.RegisterEventHandler(StateInstallationConfigured, handler) + + // Perform transition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + + lock.Release() + + // Use assert.Eventually to verify handler was called with correct parameters + assert.Eventually(t, func() bool { + return mockHandler.AssertCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + }, time.Second, time.Millisecond*50) + + mockHandler.AssertExpectations(t) +} + +func TestEventHandlerMultipleHandlers(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + // Create mock handlers + mockHandler1 := &MockEventHandler{} + mockHandler1.On("Handle", mock.Anything, StateNew, StateInstallationConfigured).Return() + + mockHandler2 := &MockEventHandler{} + mockHandler2.On("Handle", mock.Anything, StateNew, StateInstallationConfigured).Return() + + // Register multiple handlers for the same state + handler1 := func(ctx context.Context, from, to State) { + mockHandler1.Handle(ctx, from, to) + } + + handler2 := func(ctx context.Context, from, to State) { + mockHandler2.Handle(ctx, from, to) + } + + sm.RegisterEventHandler(StateInstallationConfigured, handler1) + sm.RegisterEventHandler(StateInstallationConfigured, handler2) + + // Perform transition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + + lock.Release() + + // Use assert.Eventually to verify handler was called with correct parameters + assert.Eventually(t, func() bool { + return mockHandler1.AssertCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + }, time.Second, time.Millisecond*50, "mockHandler1 was not called") + + assert.Eventually(t, func() bool { + return mockHandler2.AssertCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + }, time.Second, time.Millisecond*50, "mockHandler2 was not called") + + mockHandler1.AssertExpectations(t) + mockHandler2.AssertExpectations(t) +} + +func TestEventHandlerUnregistration(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + mockHandler := &MockEventHandler{} + + handler := func(ctx context.Context, from, to State) { + mockHandler.Handle(ctx, from, to) + } + + sm.RegisterEventHandler(StateInstallationConfigured, handler) + + // Unregister handlers + sm.UnregisterEventHandler(StateInstallationConfigured) + + // Perform transition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + + lock.Release() + + // Use assert.Eventually to wait for the state to change + assert.Eventually(t, func() bool { + return sm.currentState == StateInstallationConfigured + }, time.Second, time.Millisecond*50, "failed to transition to StateInstallationConfigured") + // Verify that the handler was not called + mockHandler.AssertNotCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + mockHandler.AssertExpectations(t) +} + +func TestEventHandlerPanicRecovery(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + mockPanicHandler := &MockEventHandler{} + mockPanicHandler.On("Handle", mock.Anything, StateNew, StateInstallationConfigured).Return() + + // Register panicking handler + panicHandler := func(ctx context.Context, from, to State) { + mockPanicHandler.Handle(ctx, from, to) + panic("test panic") + } + + mockNormalHandler := &MockEventHandler{} + mockNormalHandler.On("Handle", mock.Anything, StateNew, StateInstallationConfigured).Return() + + // Register normal handler + normalHandler := func(ctx context.Context, from, to State) { + mockNormalHandler.Handle(ctx, from, to) + } + + sm.RegisterEventHandler(StateInstallationConfigured, panicHandler) + sm.RegisterEventHandler(StateInstallationConfigured, normalHandler) + + // Perform transition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + + lock.Release() + + // Use assert.Eventually to verify handler was called with correct parameters + assert.Eventually(t, func() bool { + return mockPanicHandler.AssertCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + }, time.Second, time.Millisecond*50, "mockPanicHandler was not called") + + assert.Eventually(t, func() bool { + return mockNormalHandler.AssertCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + }, time.Second, time.Millisecond*50, "mockNormalHandler was not called") + + mockPanicHandler.AssertExpectations(t) + mockNormalHandler.AssertExpectations(t) + // Verify state machine is still in correct state + assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) +} + +func TestEventHandlerContextTimeout(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + mockHandler := &MockEventHandler{} + mockHandler.On("Handle", mock.Anything, StateNew, StateInstallationConfigured).Return() + + handler := func(ctx context.Context, from, to State) { + mockHandler.Handle(ctx, from, to) + } + + sm.RegisterEventHandler(StateInstallationConfigured, handler, WithHandlerTimeout(time.Millisecond)) + + // Perform transition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + + lock.Release() + + // Verify handler was called and context was cancelled + assert.Eventually(t, func() bool { + return mockHandler.AssertCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + }, time.Second, time.Millisecond*50, "mockHandler was not called") + + mockHandler.AssertExpectations(t) + // State machine correctly transitioned + assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) +} + +func TestEventHandlerDifferentStates(t *testing.T) { + tests := []struct { + name string + registerState State + transitionToState State + shouldTrigger bool + }{ + { + name: "handler for target state should trigger", + registerState: StateInstallationConfigured, + transitionToState: StateInstallationConfigured, + shouldTrigger: true, + }, + { + name: "handler for different state should not trigger", + registerState: StatePreflightsRunning, + transitionToState: StateInstallationConfigured, + shouldTrigger: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + mockHandler := &MockEventHandler{} + if tt.shouldTrigger { + mockHandler.On("Handle", mock.Anything, StateNew, tt.transitionToState).Return() + } + + handler := func(ctx context.Context, from, to State) { + mockHandler.Handle(ctx, from, to) + } + + sm.RegisterEventHandler(tt.registerState, handler) + + // Perform transition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, tt.transitionToState) + assert.NoError(t, err) + + lock.Release() + + if tt.shouldTrigger { + assert.Eventually(t, func() bool { + return mockHandler.AssertCalled(t, "Handle", mock.Anything, StateNew, tt.transitionToState) + }, time.Second, time.Millisecond*50, "mockHandler was not called") + mockHandler.AssertExpectations(t) + } else { + // Use assert.Eventually to wait for the state to change, then verify no calls + assert.Eventually(t, func() bool { + return sm.CurrentState() == tt.transitionToState + }, time.Second, time.Millisecond*50, "failed to transition to target state") + mockHandler.AssertNotCalled(t, "Handle", mock.Anything, StateNew, tt.transitionToState) + mockHandler.AssertExpectations(t) + } + }) + } +} + +func TestEventHandlerConcurrentRegistration(t *testing.T) { + sm := New(StateNew, validStateTransitions) + + numHandlers := 10 + mockHandlers := make([]*MockEventHandler, numHandlers) + var wg sync.WaitGroup + wg.Add(numHandlers) + + // Initialize mock handlers + for i := 0; i < numHandlers; i++ { + mockHandlers[i] = &MockEventHandler{} + mockHandlers[i].On("Handle", mock.Anything, StateNew, StateInstallationConfigured).Return() + } + + // Register handlers concurrently + for i := 0; i < numHandlers; i++ { + i := i // capture loop variable + go func() { + defer wg.Done() + handler := func(ctx context.Context, from, to State) { + mockHandlers[i].Handle(ctx, from, to) + } + sm.RegisterEventHandler(StateInstallationConfigured, handler) + }() + } + + wg.Wait() + + // Perform transition + lock, err := sm.AcquireLock() + assert.NoError(t, err) + + err = sm.Transition(lock, StateInstallationConfigured) + assert.NoError(t, err) + + lock.Release() + + // Verify all handlers were called using assert.Eventually + for i := 0; i < numHandlers; i++ { + i := i // capture loop variable + assert.Eventually(t, func() bool { + return mockHandlers[i].AssertCalled(t, "Handle", mock.Anything, StateNew, StateInstallationConfigured) + }, time.Second, time.Millisecond*50, "mockHandler %d was not called", i) + mockHandlers[i].AssertExpectations(t) + } +} + +// MockEventHandler is a mock for event handler testing +type MockEventHandler struct { + mock.Mock +} + +func (m *MockEventHandler) Handle(ctx context.Context, from, to State) { + m.Called(ctx, from, to) +} From e3fb92eaa92c1702dec0a1004d2f08a3011eb4a2 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Wed, 25 Jun 2025 13:26:19 -0700 Subject: [PATCH 34/48] feat(v3): customize install command help output based on targets (#2371) * feat(v3): customize install command help output based on targets * f * feedback --- cmd/installer/cli/cidr.go | 13 ++-- cmd/installer/cli/cidr_test.go | 2 +- cmd/installer/cli/flags.go | 130 ++++++++++++++++++++++++++++++++ cmd/installer/cli/install.go | 126 ++++++++++++++++++++++++------- cmd/installer/cli/proxy_test.go | 2 +- 5 files changed, 239 insertions(+), 34 deletions(-) create mode 100644 cmd/installer/cli/flags.go diff --git a/cmd/installer/cli/cidr.go b/cmd/installer/cli/cidr.go index c477b9f88..aa7a62352 100644 --- a/cmd/installer/cli/cidr.go +++ b/cmd/installer/cli/cidr.go @@ -7,18 +7,19 @@ import ( 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" ) -func addCIDRFlags(cmd *cobra.Command) error { - cmd.Flags().String("pod-cidr", k0sv1beta1.DefaultNetwork().PodCIDR, "IP address range for Pods") - if err := cmd.Flags().MarkHidden("pod-cidr"); err != nil { +func addCIDRFlags(flagSet *pflag.FlagSet) error { + flagSet.String("pod-cidr", k0sv1beta1.DefaultNetwork().PodCIDR, "IP address range for Pods") + if err := flagSet.MarkHidden("pod-cidr"); err != nil { return err } - cmd.Flags().String("service-cidr", k0sv1beta1.DefaultNetwork().ServiceCIDR, "IP address range for Services") - if err := cmd.Flags().MarkHidden("service-cidr"); err != nil { + flagSet.String("service-cidr", k0sv1beta1.DefaultNetwork().ServiceCIDR, "IP address range for Services") + if err := flagSet.MarkHidden("service-cidr"); err != nil { return err } - cmd.Flags().String("cidr", ecv1beta1.DefaultNetworkCIDR, "CIDR block of available private IP addresses (/16 or larger)") + flagSet.String("cidr", ecv1beta1.DefaultNetworkCIDR, "CIDR block of available private IP addresses (/16 or larger)") return nil } diff --git a/cmd/installer/cli/cidr_test.go b/cmd/installer/cli/cidr_test.go index aba77aa76..7acc19cbb 100644 --- a/cmd/installer/cli/cidr_test.go +++ b/cmd/installer/cli/cidr_test.go @@ -83,7 +83,7 @@ func Test_getCIDRConfig(t *testing.T) { req := require.New(t) cmd := &cobra.Command{} - addCIDRFlags(cmd) + addCIDRFlags(cmd.Flags()) test.setFlags(cmd.Flags()) diff --git a/cmd/installer/cli/flags.go b/cmd/installer/cli/flags.go new file mode 100644 index 000000000..ed3ba3efd --- /dev/null +++ b/cmd/installer/cli/flags.go @@ -0,0 +1,130 @@ +package cli + +import ( + "os" + "text/template" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + flagAnnotationTarget = "replicated.com/target" + flagAnnotationTargetValueLinux = "linux" + flagAnnotationTargetValueKubernetes = "kubernetes" +) + +const ( + defaultUsageTemplateV3 = `Usage:{{if .Runnable}} +{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} +{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: +{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} +{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} +{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} +{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}{{if (usesTargetFlagMenu .LocalFlags)}} + +Common Flags: + +{{(commonFlags .LocalFlags).FlagUsages | trimTrailingWhitespaces}} + +Linux‑Specific Flags: + (Valid only with --target=linux) + +{{(linuxFlags .LocalFlags).FlagUsages | trimTrailingWhitespaces}} + +Kubernetes‑Specific Flags: + (Valid only with --target=kubernetes) + +{{(kubernetesFlags .LocalFlags).FlagUsages | trimTrailingWhitespaces}}{{else}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} +{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` +) + +func init() { + cobra.AddTemplateFuncs(template.FuncMap{ + // usesTargetFlagMenu returns true if the target flag is present and the ENABLE_V3 environment variable is set. + "usesTargetFlagMenu": func(flagSet *pflag.FlagSet) bool { + if os.Getenv("ENABLE_V3") == "1" { + return flagSet.Lookup("target") != nil + } + return false + }, + // commonFlags returns a flag set with all flags that do not have a target annotation. + "commonFlags": func(flagSet *pflag.FlagSet) *pflag.FlagSet { + return filterFlagSetNoTarget(flagSet) + }, + // linuxFlags returns a flag set with all flags that have the target annotation set to linux. + "linuxFlags": func(flagSet *pflag.FlagSet) *pflag.FlagSet { + return filterFlagSetByTarget(flagSet, flagAnnotationTargetValueLinux) + }, + // kubernetesFlags returns a flag set with all flags that have the target annotation set to kubernetes. + "kubernetesFlags": func(flagSet *pflag.FlagSet) *pflag.FlagSet { + return filterFlagSetByTarget(flagSet, flagAnnotationTargetValueKubernetes) + }, + }) +} + +func mustSetFlagTargetLinux(flags *pflag.FlagSet, name string) { + mustSetFlagTarget(flags, name, flagAnnotationTargetValueLinux) +} + +func mustSetFlagTargetKubernetes(flags *pflag.FlagSet, name string) { + mustSetFlagTarget(flags, name, flagAnnotationTargetValueKubernetes) +} + +func mustSetFlagTarget(flags *pflag.FlagSet, name string, target string) { + err := flags.SetAnnotation(name, flagAnnotationTarget, []string{target}) + if err != nil { + panic(err) + } +} + +func filterFlagSetByTarget(flags *pflag.FlagSet, target string) *pflag.FlagSet { + if flags == nil { + return nil + } + next := pflag.NewFlagSet(flags.Name(), pflag.ContinueOnError) + flags.VisitAll(func(flag *pflag.Flag) { + for _, t := range flag.Annotations[flagAnnotationTarget] { + if t == target { + next.AddFlag(flag) + break + } + } + }) + return next +} + +func filterFlagSetNoTarget(flags *pflag.FlagSet) *pflag.FlagSet { + if flags == nil { + return nil + } + next := pflag.NewFlagSet(flags.Name(), pflag.ContinueOnError) + flags.VisitAll(func(flag *pflag.Flag) { + if len(flag.Annotations[flagAnnotationTarget]) == 0 { + next.AddFlag(flag) + } + }) + return next +} diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 80c53faa8..14b124e41 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -90,9 +90,15 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { ctx, cancel := context.WithCancel(ctx) rc := runtimeconfig.New(nil) + short := fmt.Sprintf("Install %s", name) + if os.Getenv("ENABLE_V3") == "1" { + short = fmt.Sprintf("Install %s onto Linux or Kubernetes", name) + } + cmd := &cobra.Command{ - Use: "install", - Short: fmt.Sprintf("Install %s", name), + Use: "install", + Short: short, + Example: installCmdExample(name), PostRun: func(cmd *cobra.Command, args []string) { rc.Cleanup() cancel() // Cancel context when command completes @@ -138,6 +144,8 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { }, } + cmd.SetUsageTemplate(defaultUsageTemplateV3) + if err := addInstallFlags(cmd, &flags); err != nil { panic(err) } @@ -153,6 +161,88 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { return cmd } +const ( + installCmdExampleText = ` + # Install on a Linux host + %s install \ + --target linux \ + --data-dir /opt/embedded-cluster \ + --license ./license.yaml \ + --yes + + # Install in a Kubernetes cluster + %s install \ + --target kubernetes \ + --kubeconfig ./kubeconfig \ + --airgap-bundle ./replicated.airgap \ + --license ./license.yaml +` +) + +func installCmdExample(name string) string { + if os.Getenv("ENABLE_V3") != "1" { + return "" + } + + return fmt.Sprintf(installCmdExampleText, name, name) +} + +func newLinuxInstallFlags(flags *InstallCmdFlags) *pflag.FlagSet { + flagSet := pflag.NewFlagSet("linux", pflag.ExitOnError) + + flagSet.StringVar(&flags.dataDir, "data-dir", ecv1beta1.DefaultDataDir, "Path to the data directory") + flagSet.IntVar(&flags.localArtifactMirrorPort, "local-artifact-mirror-port", ecv1beta1.DefaultLocalArtifactMirrorPort, "Port on which the Local Artifact Mirror will be served") + flagSet.StringVar(&flags.networkInterface, "network-interface", "", "The network interface to use for the cluster") + + flagSet.StringSlice("private-ca", []string{}, "Path to a trusted private CA certificate file") + if err := flagSet.MarkHidden("private-ca"); err != nil { + panic(err) + } + if err := flagSet.MarkDeprecated("private-ca", "This flag is no longer used and will be removed in a future version. The CA bundle will be automatically detected from the host."); err != nil { + panic(err) + } + + flagSet.BoolVar(&flags.skipHostPreflights, "skip-host-preflights", false, "Skip host preflight checks. This is not recommended and has been deprecated.") + if err := flagSet.MarkHidden("skip-host-preflights"); err != nil { + panic(err) + } + if err := flagSet.MarkDeprecated("skip-host-preflights", "This flag is deprecated and will be removed in a future version. Use --ignore-host-preflights instead."); err != nil { + panic(err) + } + flagSet.BoolVar(&flags.ignoreHostPreflights, "ignore-host-preflights", false, "Allow bypassing host preflight failures") + + if err := addCIDRFlags(flagSet); err != nil { + panic(err) + } + + flagSet.VisitAll(func(flag *pflag.Flag) { + mustSetFlagTargetLinux(flagSet, flag.Name) + }) + + return flagSet +} + +func newKubernetesInstallFlags(flags *InstallCmdFlags) *pflag.FlagSet { + // If the ENABLE_V3 environment variable is set, do not hide the new flags. + enableV3 := os.Getenv("ENABLE_V3") == "1" + + flagSet := pflag.NewFlagSet("kubernetes", pflag.ExitOnError) + + flagSet.String("kubeconfig", "", "Path to the kubeconfig file") + + if !enableV3 { + if err := flagSet.MarkHidden("kubeconfig"); err != nil { + panic(err) + } + } + + flagSet.VisitAll(func(flag *pflag.Flag) { + mustSetFlagTargetKubernetes(flagSet, flag.Name) + }) + + return flagSet +} + func addInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { cmd.Flags().StringVar(&flags.target, "target", "linux", "The target platform to install to. Valid options are 'linux' or 'kubernetes'.") if os.Getenv("ENABLE_V3") != "1" { @@ -162,40 +252,24 @@ func addInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { } cmd.Flags().StringVar(&flags.airgapBundle, "airgap-bundle", "", "Path to the air gap bundle. If set, the installation will complete without internet access.") - cmd.Flags().StringVar(&flags.dataDir, "data-dir", ecv1beta1.DefaultDataDir, "Path to the data directory") - cmd.Flags().IntVar(&flags.localArtifactMirrorPort, "local-artifact-mirror-port", ecv1beta1.DefaultLocalArtifactMirrorPort, "Port on which the Local Artifact Mirror will be served") - cmd.Flags().StringVar(&flags.networkInterface, "network-interface", "", "The network interface to use for the cluster") - cmd.Flags().BoolVarP(&flags.assumeYes, "yes", "y", false, "Assume yes to all prompts.") - cmd.Flags().SetNormalizeFunc(normalizeNoPromptToYes) cmd.Flags().StringVar(&flags.overrides, "overrides", "", "File with an EmbeddedClusterConfig object to override the default configuration") if err := cmd.Flags().MarkHidden("overrides"); err != nil { return err } - cmd.Flags().StringSlice("private-ca", []string{}, "Path to a trusted private CA certificate file") - if err := cmd.Flags().MarkHidden("private-ca"); err != nil { - return err - } - if err := cmd.Flags().MarkDeprecated("private-ca", "This flag is no longer used and will be removed in a future version. The CA bundle will be automatically detected from the host."); err != nil { - return err - } - if err := addProxyFlags(cmd); err != nil { return err } - if err := addCIDRFlags(cmd); err != nil { - return err - } - cmd.Flags().BoolVar(&flags.skipHostPreflights, "skip-host-preflights", false, "Skip host preflight checks. This is not recommended and has been deprecated.") - if err := cmd.Flags().MarkHidden("skip-host-preflights"); err != nil { - return err - } - if err := cmd.Flags().MarkDeprecated("skip-host-preflights", "This flag is deprecated and will be removed in a future version. Use --ignore-host-preflights instead."); err != nil { - return err - } - cmd.Flags().BoolVar(&flags.ignoreHostPreflights, "ignore-host-preflights", false, "Allow bypassing host preflight failures") + cmd.Flags().BoolVarP(&flags.assumeYes, "yes", "y", false, "Assume yes to all prompts.") + cmd.Flags().SetNormalizeFunc(normalizeNoPromptToYes) + + linuxFlagSet := newLinuxInstallFlags(flags) + cmd.Flags().AddFlagSet(linuxFlagSet) + + kubernetesFlagSet := newKubernetesInstallFlags(flags) + cmd.Flags().AddFlagSet(kubernetesFlagSet) return nil } diff --git a/cmd/installer/cli/proxy_test.go b/cmd/installer/cli/proxy_test.go index 2ff870141..23ac640f4 100644 --- a/cmd/installer/cli/proxy_test.go +++ b/cmd/installer/cli/proxy_test.go @@ -151,7 +151,7 @@ func Test_getProxySpecFromFlags(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &cobra.Command{} - addCIDRFlags(cmd) + addCIDRFlags(cmd.Flags()) addProxyFlags(cmd) cmd.Flags().String("network-interface", "", "The network interface to use for the cluster") From 970352f57ce4570e0d950f2aa1fe323da77ac1af Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Wed, 25 Jun 2025 14:49:01 -0700 Subject: [PATCH 35/48] feat(kubernetes): support for kubeconfig flag and from env (#2374) --- cmd/installer/cli/cidr.go | 16 +- cmd/installer/cli/cidr_test.go | 2 +- cmd/installer/cli/flags.go | 14 ++ cmd/installer/cli/install.go | 264 ++++++++++++++------- cmd/installer/cli/install_runpreflights.go | 5 +- cmd/installer/cli/proxy.go | 11 +- cmd/installer/cli/proxy_test.go | 4 +- cmd/installer/cli/restore.go | 4 +- 8 files changed, 209 insertions(+), 111 deletions(-) diff --git a/cmd/installer/cli/cidr.go b/cmd/installer/cli/cidr.go index aa7a62352..95b4d2f4d 100644 --- a/cmd/installer/cli/cidr.go +++ b/cmd/installer/cli/cidr.go @@ -10,18 +10,14 @@ import ( "github.com/spf13/pflag" ) -func addCIDRFlags(flagSet *pflag.FlagSet) error { - flagSet.String("pod-cidr", k0sv1beta1.DefaultNetwork().PodCIDR, "IP address range for Pods") - if err := flagSet.MarkHidden("pod-cidr"); err != nil { - return err - } - flagSet.String("service-cidr", k0sv1beta1.DefaultNetwork().ServiceCIDR, "IP address range for Services") - if err := flagSet.MarkHidden("service-cidr"); err != nil { - return err - } +func mustAddCIDRFlags(flagSet *pflag.FlagSet) { flagSet.String("cidr", ecv1beta1.DefaultNetworkCIDR, "CIDR block of available private IP addresses (/16 or larger)") - return nil + flagSet.String("pod-cidr", k0sv1beta1.DefaultNetwork().PodCIDR, "IP address range for Pods") + mustMarkFlagHidden(flagSet, "pod-cidr") + + flagSet.String("service-cidr", k0sv1beta1.DefaultNetwork().ServiceCIDR, "IP address range for Services") + mustMarkFlagHidden(flagSet, "service-cidr") } func validateCIDRFlags(cmd *cobra.Command) error { diff --git a/cmd/installer/cli/cidr_test.go b/cmd/installer/cli/cidr_test.go index 7acc19cbb..2cc36e67f 100644 --- a/cmd/installer/cli/cidr_test.go +++ b/cmd/installer/cli/cidr_test.go @@ -83,7 +83,7 @@ func Test_getCIDRConfig(t *testing.T) { req := require.New(t) cmd := &cobra.Command{} - addCIDRFlags(cmd.Flags()) + mustAddCIDRFlags(cmd.Flags()) test.setFlags(cmd.Flags()) diff --git a/cmd/installer/cli/flags.go b/cmd/installer/cli/flags.go index ed3ba3efd..627bdc85f 100644 --- a/cmd/installer/cli/flags.go +++ b/cmd/installer/cli/flags.go @@ -100,6 +100,20 @@ func mustSetFlagTarget(flags *pflag.FlagSet, name string, target string) { } } +func mustMarkFlagHidden(flags *pflag.FlagSet, name string) { + err := flags.MarkHidden(name) + if err != nil { + panic(err) + } +} + +func mustMarkFlagDeprecated(flags *pflag.FlagSet, name string, deprecationMessage string) { + err := flags.MarkDeprecated(name, deprecationMessage) + if err != nil { + panic(err) + } +} + func filterFlagSetByTarget(flags *pflag.FlagSet, target string) *pflag.FlagSet { if flags == nil { return nil diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 14b124e41..942b9be44 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -45,25 +45,32 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" + helmcli "helm.sh/helm/v3/pkg/cli" "k8s.io/client-go/metadata" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" ) type InstallCmdFlags struct { - adminConsolePassword string - adminConsolePort int - airgapBundle string - isAirgap bool + adminConsolePassword string + adminConsolePort int + airgapBundle string + isAirgap bool + licenseFile string + assumeYes bool + overrides string + configValues string + + // linux flags dataDir string - licenseFile string localArtifactMirrorPort int - assumeYes bool - overrides string skipHostPreflights bool ignoreHostPreflights bool - configValues string networkInterface string + // kubernetes flags + kubernetesEnvSettings *helmcli.EnvSettings + // guided UI flags enableManagerExperience bool target string @@ -72,12 +79,17 @@ type InstallCmdFlags struct { tlsKeyFile string hostname string - // TODO: move to substruct + installConfig +} + +type installConfig struct { license *kotsv1beta1.License licenseBytes []byte tlsCert tls.Certificate tlsCertBytes []byte tlsKeyBytes []byte + + kubernetesRestConfig *rest.Config } // 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. @@ -146,9 +158,8 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { cmd.SetUsageTemplate(defaultUsageTemplateV3) - if err := addInstallFlags(cmd, &flags); err != nil { - panic(err) - } + mustAddInstallFlags(cmd, &flags) + if err := addInstallAdminConsoleFlags(cmd, &flags); err != nil { panic(err) } @@ -187,33 +198,79 @@ func installCmdExample(name string) string { return fmt.Sprintf(installCmdExampleText, name, name) } +func mustAddInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) { + enableV3 := os.Getenv("ENABLE_V3") == "1" + + normalizeFuncs := []func(f *pflag.FlagSet, name string) pflag.NormalizedName{} + + commonFlagSet := newCommonInstallFlags(flags, enableV3) + cmd.Flags().AddFlagSet(commonFlagSet) + if fn := commonFlagSet.GetNormalizeFunc(); fn != nil { + normalizeFuncs = append(normalizeFuncs, fn) + } + + linuxFlagSet := newLinuxInstallFlags(flags) + cmd.Flags().AddFlagSet(linuxFlagSet) + if fn := linuxFlagSet.GetNormalizeFunc(); fn != nil { + normalizeFuncs = append(normalizeFuncs, fn) + } + + kubernetesFlagSet := newKubernetesInstallFlags(flags, enableV3) + cmd.Flags().AddFlagSet(kubernetesFlagSet) + if fn := kubernetesFlagSet.GetNormalizeFunc(); fn != nil { + normalizeFuncs = append(normalizeFuncs, fn) + } + + cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { + result := pflag.NormalizedName(strings.ToLower(name)) + for _, fn := range normalizeFuncs { + if fn != nil { + result = fn(f, string(result)) + } + } + return result + }) +} + +func newCommonInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { + flagSet := pflag.NewFlagSet("common", pflag.ContinueOnError) + + flagSet.StringVar(&flags.target, "target", "linux", "The target platform to install to. Valid options are 'linux' or 'kubernetes'.") + if !enableV3 { + mustMarkFlagHidden(flagSet, "target") + } + + flagSet.StringVar(&flags.airgapBundle, "airgap-bundle", "", "Path to the air gap bundle. If set, the installation will complete without internet access.") + + flagSet.StringVar(&flags.overrides, "overrides", "", "File with an EmbeddedClusterConfig object to override the default configuration") + mustMarkFlagHidden(flagSet, "overrides") + + mustAddProxyFlags(flagSet) + + flagSet.BoolVarP(&flags.assumeYes, "yes", "y", false, "Assume yes to all prompts.") + flagSet.SetNormalizeFunc(normalizeNoPromptToYes) + + return flagSet +} + func newLinuxInstallFlags(flags *InstallCmdFlags) *pflag.FlagSet { - flagSet := pflag.NewFlagSet("linux", pflag.ExitOnError) + flagSet := pflag.NewFlagSet("linux", pflag.ContinueOnError) flagSet.StringVar(&flags.dataDir, "data-dir", ecv1beta1.DefaultDataDir, "Path to the data directory") flagSet.IntVar(&flags.localArtifactMirrorPort, "local-artifact-mirror-port", ecv1beta1.DefaultLocalArtifactMirrorPort, "Port on which the Local Artifact Mirror will be served") flagSet.StringVar(&flags.networkInterface, "network-interface", "", "The network interface to use for the cluster") flagSet.StringSlice("private-ca", []string{}, "Path to a trusted private CA certificate file") - if err := flagSet.MarkHidden("private-ca"); err != nil { - panic(err) - } - if err := flagSet.MarkDeprecated("private-ca", "This flag is no longer used and will be removed in a future version. The CA bundle will be automatically detected from the host."); err != nil { - panic(err) - } + mustMarkFlagHidden(flagSet, "private-ca") + mustMarkFlagDeprecated(flagSet, "private-ca", "This flag is no longer used and will be removed in a future version. The CA bundle will be automatically detected from the host.") flagSet.BoolVar(&flags.skipHostPreflights, "skip-host-preflights", false, "Skip host preflight checks. This is not recommended and has been deprecated.") - if err := flagSet.MarkHidden("skip-host-preflights"); err != nil { - panic(err) - } - if err := flagSet.MarkDeprecated("skip-host-preflights", "This flag is deprecated and will be removed in a future version. Use --ignore-host-preflights instead."); err != nil { - panic(err) - } + mustMarkFlagHidden(flagSet, "skip-host-preflights") + mustMarkFlagDeprecated(flagSet, "skip-host-preflights", "This flag is deprecated and will be removed in a future version. Use --ignore-host-preflights instead.") + flagSet.BoolVar(&flags.ignoreHostPreflights, "ignore-host-preflights", false, "Allow bypassing host preflight failures") - if err := addCIDRFlags(flagSet); err != nil { - panic(err) - } + mustAddCIDRFlags(flagSet) flagSet.VisitAll(func(flag *pflag.Flag) { mustSetFlagTargetLinux(flagSet, flag.Name) @@ -222,56 +279,44 @@ func newLinuxInstallFlags(flags *InstallCmdFlags) *pflag.FlagSet { return flagSet } -func newKubernetesInstallFlags(flags *InstallCmdFlags) *pflag.FlagSet { - // If the ENABLE_V3 environment variable is set, do not hide the new flags. - enableV3 := os.Getenv("ENABLE_V3") == "1" - - flagSet := pflag.NewFlagSet("kubernetes", pflag.ExitOnError) - - flagSet.String("kubeconfig", "", "Path to the kubeconfig file") +func newKubernetesInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { + flagSet := pflag.NewFlagSet("kubernetes", pflag.ContinueOnError) - if !enableV3 { - if err := flagSet.MarkHidden("kubeconfig"); err != nil { - panic(err) - } - } + addKubernetesCLIFlags(flagSet, flags) flagSet.VisitAll(func(flag *pflag.Flag) { + if !enableV3 { + mustMarkFlagHidden(flagSet, flag.Name) + } mustSetFlagTargetKubernetes(flagSet, flag.Name) }) return flagSet } -func addInstallFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { - cmd.Flags().StringVar(&flags.target, "target", "linux", "The target platform to install to. Valid options are 'linux' or 'kubernetes'.") - if os.Getenv("ENABLE_V3") != "1" { - if err := cmd.Flags().MarkHidden("target"); err != nil { - return err - } - } - - cmd.Flags().StringVar(&flags.airgapBundle, "airgap-bundle", "", "Path to the air gap bundle. If set, the installation will complete without internet access.") - - cmd.Flags().StringVar(&flags.overrides, "overrides", "", "File with an EmbeddedClusterConfig object to override the default configuration") - if err := cmd.Flags().MarkHidden("overrides"); err != nil { - return err - } - - if err := addProxyFlags(cmd); err != nil { - return err - } - - cmd.Flags().BoolVarP(&flags.assumeYes, "yes", "y", false, "Assume yes to all prompts.") - cmd.Flags().SetNormalizeFunc(normalizeNoPromptToYes) - - linuxFlagSet := newLinuxInstallFlags(flags) - cmd.Flags().AddFlagSet(linuxFlagSet) - - kubernetesFlagSet := newKubernetesInstallFlags(flags) - cmd.Flags().AddFlagSet(kubernetesFlagSet) - - return nil +func addKubernetesCLIFlags(flagSet *pflag.FlagSet, flags *InstallCmdFlags) { + // From helm + // https://github.com/helm/helm/blob/v3.18.3/pkg/cli/environment.go#L145-L163 + + s := helmcli.New() + + flagSet.StringVar(&s.KubeConfig, "kubeconfig", "", "Path to the kubeconfig file") + flagSet.StringVar(&s.KubeContext, "kube-context", s.KubeContext, "Name of the kubeconfig context to use") + flagSet.StringVar(&s.KubeToken, "kube-token", s.KubeToken, "Bearer token used for authentication") + flagSet.StringVar(&s.KubeAsUser, "kube-as-user", s.KubeAsUser, "Username to impersonate for the operation") + flagSet.StringArrayVar(&s.KubeAsGroups, "kube-as-group", s.KubeAsGroups, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.") + flagSet.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "The address and the port for the Kubernetes API server") + flagSet.StringVar(&s.KubeCaFile, "kube-ca-file", s.KubeCaFile, "The certificate authority file for the Kubernetes API server connection") + flagSet.StringVar(&s.KubeTLSServerName, "kube-tls-server-name", s.KubeTLSServerName, "Server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used") + // flagSet.BoolVar(&s.Debug, "helm-debug", s.Debug, "enable verbose output") + flagSet.BoolVar(&s.KubeInsecureSkipTLSVerify, "kube-insecure-skip-tls-verify", s.KubeInsecureSkipTLSVerify, "If true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure") + // flagSet.StringVar(&s.RegistryConfig, "helm-registry-config", s.RegistryConfig, "Path to the Helm registry config file") + // flagSet.StringVar(&s.RepositoryConfig, "helm-repository-config", s.RepositoryConfig, "Path to the file containing Helm repository names and URLs") + // flagSet.StringVar(&s.RepositoryCache, "helm-repository-cache", s.RepositoryCache, "Path to the directory containing cached Helm repository indexes") + flagSet.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "Kubernetes API client-side default throttling limit") + flagSet.Float32Var(&s.QPS, "qps", s.QPS, "Queries per second used when communicating with the Kubernetes API, not including bursting") + + flags.kubernetesEnvSettings = s } func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) error { @@ -320,18 +365,25 @@ func addManagerExperienceFlags(cmd *cobra.Command, flags *InstallCmdFlags) error } func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { - if os.Getuid() != 0 { - return fmt.Errorf("install command must be run as root") + if !slices.Contains([]string{"linux", "kubernetes"}, flags.target) { + return fmt.Errorf(`invalid target (must be one of: "linux", "kubernetes")`) } - // set the umask to 022 so that we can create files/directories with 755 permissions - // this does not return an error - it returns the previous umask - _ = syscall.Umask(0o022) + if err := preRunInstallCommon(cmd, flags, rc); err != nil { + return err + } - if !slices.Contains([]string{"linux", "kubernetes"}, flags.target) { - return fmt.Errorf(`invalid target (must be one of: "linux", "kubernetes")`) + switch flags.target { + case "linux": + return preRunInstallLinux(cmd, flags, rc) + case "kubernetes": + return preRunInstallKubernetes(cmd, flags) } + return nil +} + +func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { // license file can be empty for restore if flags.licenseFile != "" { b, err := os.ReadFile(flags.licenseFile) @@ -361,6 +413,33 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. flags.isAirgap = flags.airgapBundle != "" + proxy, err := proxyConfigFromCmd(cmd, flags.assumeYes) + if err != nil { + return err + } + + // restore command doesn't have a password flag + if cmd.Flags().Lookup("admin-console-password") != nil { + if err := ensureAdminConsolePassword(flags); err != nil { + return err + } + } + + // TODO: runtimeconfig is only relevant for linux installs + rc.SetProxySpec(proxy) + + return nil +} + +func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { + if os.Getuid() != 0 { + return fmt.Errorf("install command must be run as root") + } + + // set the umask to 022 so that we can create files/directories with 755 permissions + // this does not return an error - it returns the previous umask + _ = syscall.Umask(0o022) + hostCABundlePath, err := findHostCABundle() if err != nil { return fmt.Errorf("unable to find host CA bundle: %w", err) @@ -386,11 +465,6 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. return fmt.Errorf("process overrides file: %w", err) } - proxy, err := proxyConfigFromCmd(cmd, flags.assumeYes) - if err != nil { - return err - } - cidrCfg, err := cidrConfigFromCmd(cmd) if err != nil { return err @@ -412,15 +486,33 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. rc.SetAdminConsolePort(flags.adminConsolePort) rc.SetHostCABundlePath(hostCABundlePath) rc.SetNetworkSpec(networkSpec) - rc.SetProxySpec(proxy) - // restore command doesn't have a password flag - if cmd.Flags().Lookup("admin-console-password") != nil { - if err := ensureAdminConsolePassword(flags); err != nil { - return err + return nil +} + +func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags) error { + // If set, validate that the kubeconfig file exists and can be read + if flags.kubernetesEnvSettings.KubeConfig != "" { + if _, err := os.Stat(flags.kubernetesEnvSettings.KubeConfig); os.IsNotExist(err) { + return fmt.Errorf("kubeconfig file does not exist: %s", flags.kubernetesEnvSettings.KubeConfig) + } else if err != nil { + return fmt.Errorf("unable to stat kubeconfig file: %w", err) } } + restConfig, err := flags.kubernetesEnvSettings.RESTClientGetter().ToRESTConfig() + if err != nil { + return fmt.Errorf("failed to discover kubeconfig: %w", err) + } + + // If this is the default host, there was probably no kubeconfig discovered. + // HACK: This is fragile but it is the best thing I could come up with + if flags.kubernetesEnvSettings.KubeConfig == "" && restConfig.Host == "http://localhost:8080" { + return fmt.Errorf("a kubeconfig is required when using kubernetes") + } + + flags.installConfig.kubernetesRestConfig = restConfig + return nil } diff --git a/cmd/installer/cli/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index fee57aa4b..8cf75d8f4 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -50,9 +50,8 @@ func InstallRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { }, } - if err := addInstallFlags(cmd, &flags); err != nil { - panic(err) - } + mustAddInstallFlags(cmd, &flags) + if err := addInstallAdminConsoleFlags(cmd, &flags); err != nil { panic(err) } diff --git a/cmd/installer/cli/proxy.go b/cmd/installer/cli/proxy.go index f069741d0..5573e9afc 100644 --- a/cmd/installer/cli/proxy.go +++ b/cmd/installer/cli/proxy.go @@ -8,6 +8,7 @@ import ( newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) // NetworkLookup defines the interface for network lookups @@ -23,12 +24,10 @@ func (d *defaultNetworkLookup) FirstValidIPNet(networkInterface string) (*net.IP var defaultNetworkLookupImpl NetworkLookup = &defaultNetworkLookup{} -func addProxyFlags(cmd *cobra.Command) error { - cmd.Flags().String("http-proxy", "", "HTTP proxy to use for the installation (overrides http_proxy/HTTP_PROXY environment variables)") - cmd.Flags().String("https-proxy", "", "HTTPS proxy to use for the installation (overrides https_proxy/HTTPS_PROXY environment variables)") - cmd.Flags().String("no-proxy", "", "Comma-separated list of hosts for which not to use a proxy (overrides no_proxy/NO_PROXY environment variables)") - - return nil +func mustAddProxyFlags(flagSet *pflag.FlagSet) { + flagSet.String("http-proxy", "", "HTTP proxy to use for the installation (overrides http_proxy/HTTP_PROXY environment variables)") + flagSet.String("https-proxy", "", "HTTPS proxy to use for the installation (overrides https_proxy/HTTPS_PROXY environment variables)") + 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) { diff --git a/cmd/installer/cli/proxy_test.go b/cmd/installer/cli/proxy_test.go index 23ac640f4..d6e320b88 100644 --- a/cmd/installer/cli/proxy_test.go +++ b/cmd/installer/cli/proxy_test.go @@ -151,8 +151,8 @@ func Test_getProxySpecFromFlags(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &cobra.Command{} - addCIDRFlags(cmd.Flags()) - addProxyFlags(cmd) + mustAddCIDRFlags(cmd.Flags()) + mustAddProxyFlags(cmd.Flags()) cmd.Flags().String("network-interface", "", "The network interface to use for the cluster") flagSet := cmd.Flags() diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index 5b08701af..37ffb8d53 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -120,9 +120,7 @@ func RestoreCmd(ctx context.Context, name string) *cobra.Command { addS3Flags(cmd, &s3Store) cmd.Flags().BoolVar(&skipStoreValidation, "skip-store-validation", false, "Skip validation of the backup storage location") - if err := addInstallFlags(cmd, &flags); err != nil { - panic(err) - } + mustAddInstallFlags(cmd, &flags) return cmd } From b824d7761401f6b8eba4b1c59713e024d286dca2 Mon Sep 17 00:00:00 2001 From: Steven Crespo <96719548+screspod@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:52:49 -0700 Subject: [PATCH 36/48] feat: Refactor addons package to decouple from runtimeconfig (#2372) * refactor addons package to decouple from runtimeconfig * f * f * f * f * f --- api/integration/install_test.go | 5 +- api/internal/managers/infra/install.go | 17 ++-- .../cli/adminconsole_resetpassword.go | 4 +- cmd/installer/cli/enable_ha.go | 24 ++++- cmd/installer/cli/install.go | 34 ++++--- cmd/installer/cli/join.go | 25 ++++- cmd/installer/cli/join_printcommand.go | 4 +- cmd/installer/cli/join_runpreflights.go | 3 +- cmd/installer/cli/materialize.go | 4 +- cmd/installer/cli/reset_firewalld.go | 4 +- cmd/installer/cli/restore.go | 74 ++++++++++----- cmd/installer/cli/shell.go | 4 +- cmd/installer/cli/update.go | 7 +- operator/pkg/upgrade/job.go | 11 ++- operator/pkg/upgrade/upgrade.go | 33 ++++++- pkg-new/constants/constants.go | 14 +++ pkg-new/k0s/k0s.go | 3 +- pkg-new/tlsutils/tls.go | 4 +- pkg/addons/adminconsole/adminconsole.go | 21 +++-- pkg/addons/adminconsole/install.go | 22 ++--- pkg/addons/adminconsole/install_test.go | 12 +-- .../integration/hostcabundle_test.go | 13 +-- pkg/addons/adminconsole/upgrade.go | 5 +- pkg/addons/adminconsole/values.go | 13 ++- pkg/addons/adminconsole/values_test.go | 21 ++--- .../embeddedclusteroperator.go | 5 +- pkg/addons/embeddedclusteroperator/install.go | 6 +- .../integration/hostcabundle_test.go | 7 +- pkg/addons/embeddedclusteroperator/upgrade.go | 7 +- pkg/addons/embeddedclusteroperator/values.go | 7 +- .../embeddedclusteroperator/values_test.go | 11 +-- pkg/addons/highavailability.go | 75 ++++++++------- pkg/addons/highavailability_test.go | 2 +- pkg/addons/install.go | 91 +++++++++++++----- pkg/addons/install_test.go | 35 ++----- pkg/addons/interface.go | 17 ++-- pkg/addons/mock.go | 12 +-- pkg/addons/openebs/install.go | 6 +- pkg/addons/openebs/openebs.go | 1 + pkg/addons/openebs/upgrade.go | 7 +- pkg/addons/openebs/values.go | 5 +- pkg/addons/registry/install.go | 5 +- pkg/addons/registry/migrate/pod.go | 24 ++--- pkg/addons/registry/registry.go | 4 +- pkg/addons/registry/upgrade.go | 7 +- pkg/addons/registry/values.go | 3 +- pkg/addons/seaweedfs/install.go | 6 +- pkg/addons/seaweedfs/seaweedfs.go | 7 +- pkg/addons/seaweedfs/upgrade.go | 7 +- pkg/addons/seaweedfs/values.go | 7 +- pkg/addons/types/types.go | 7 +- pkg/addons/upgrade.go | 75 ++++++++++----- pkg/addons/upgrade_test.go | 94 ++++++------------- pkg/addons/util.go | 8 +- pkg/addons/util_test.go | 20 +++- pkg/addons/velero/install.go | 6 +- .../velero/integration/hostcabundle_test.go | 9 +- .../velero/integration/k0ssubdir_test.go | 11 +-- pkg/addons/velero/upgrade.go | 7 +- pkg/addons/velero/values.go | 11 +-- pkg/addons/velero/values_test.go | 11 +-- pkg/addons/velero/velero.go | 9 +- pkg/constants/restore.go | 3 - pkg/disasterrecovery/backup.go | 4 +- pkg/kubeutils/installation.go | 3 +- pkg/runtimeconfig/defaults.go | 17 ---- pkg/runtimeconfig/interface.go | 2 +- pkg/runtimeconfig/mock.go | 4 +- pkg/runtimeconfig/runtimeconfig.go | 4 +- pkg/support/materialize.go | 3 +- .../kind/openebs/analytics_test.go | 5 +- .../kind/openebs/customdatadir_test.go | 12 +-- tests/integration/kind/registry/ha_test.go | 55 ++++++++--- tests/integration/kind/velero/ca_test.go | 11 +-- web/static.go | 1 + web/static_test.go | 21 +++++ 76 files changed, 660 insertions(+), 498 deletions(-) create mode 100644 pkg-new/constants/constants.go delete mode 100644 pkg/constants/restore.go diff --git a/api/integration/install_test.go b/api/integration/install_test.go index 645fb9e4e..797b273f7 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -28,6 +28,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" "github.com/replicatedhq/embedded-cluster/pkg/helm" @@ -1334,11 +1335,11 @@ func TestPostSetupInfra(t *testing.T) { // Verify kotsadm namespace and kotsadm-password secret were created var gotKotsadmNamespace corev1.Namespace - err = fakeKcli.Get(t.Context(), client.ObjectKey{Name: runtimeconfig.KotsadmNamespace}, &gotKotsadmNamespace) + err = fakeKcli.Get(t.Context(), client.ObjectKey{Name: constants.KotsadmNamespace}, &gotKotsadmNamespace) require.NoError(t, err) var gotKotsadmPasswordSecret corev1.Secret - err = fakeKcli.Get(t.Context(), client.ObjectKey{Namespace: runtimeconfig.KotsadmNamespace, Name: "kotsadm-password"}, &gotKotsadmPasswordSecret) + err = fakeKcli.Get(t.Context(), client.ObjectKey{Namespace: constants.KotsadmNamespace, Name: "kotsadm-password"}, &gotKotsadmPasswordSecret) require.NoError(t, err) assert.NotEmpty(t, gotKotsadmPasswordSecret.Data["passwordBcrypt"]) diff --git a/api/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index 9ac224aea..17aae15e6 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -10,6 +10,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" ecmetadata "github.com/replicatedhq/embedded-cluster/pkg-new/metadata" "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" @@ -74,10 +75,7 @@ func (m *infraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConf func (m *infraManager) initComponentsList(license *kotsv1beta1.License, rc runtimeconfig.RuntimeConfig) error { components := []types.InfraComponent{{Name: K0sComponentName}} - addOns := addons.GetAddOnsForInstall(rc, addons.InstallOptions{ - IsAirgap: m.airgapBundle != "", - DisasterRecoveryEnabled: license.Spec.IsDisasterRecoverySupported, - }) + addOns := addons.GetAddOnsForInstall(m.getAddonInstallOpts(license, rc)) for _, addOn := range addOns { components = append(components, types.InfraComponent{Name: addOn.Name()}) } @@ -274,7 +272,7 @@ func (m *infraManager) installAddOns(ctx context.Context, kcli client.Client, mc addons.WithKubernetesClient(kcli), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(utils.GetDomains(m.releaseData)), addons.WithProgressChannel(progressChan), ) @@ -293,6 +291,7 @@ func (m *infraManager) getAddonInstallOpts(license *kotsv1beta1.License, rc runt opts := addons.InstallOptions{ AdminConsolePwd: m.password, + AdminConsolePort: rc.AdminConsolePort(), License: license, IsAirgap: m.airgapBundle != "", TLSCertBytes: m.tlsConfig.CertBytes, @@ -302,6 +301,12 @@ func (m *infraManager) getAddonInstallOpts(license *kotsv1beta1.License, rc runt IsMultiNodeEnabled: license.Spec.IsEmbeddedClusterMultiNodeEnabled, EmbeddedConfigSpec: m.getECConfigSpec(), EndUserConfigSpec: m.getEndUserConfigSpec(), + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + ServiceCIDR: rc.ServiceCIDR(), } if m.kotsInstaller != nil { // used for testing @@ -312,7 +317,7 @@ func (m *infraManager) getAddonInstallOpts(license *kotsv1beta1.License, rc runt RuntimeConfig: rc, AppSlug: license.Spec.AppSlug, License: m.license, - Namespace: runtimeconfig.KotsadmNamespace, + Namespace: constants.KotsadmNamespace, AirgapBundle: m.airgapBundle, ConfigValuesFile: m.configValues, ReplicatedAppEndpoint: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), diff --git a/cmd/installer/cli/adminconsole_resetpassword.go b/cmd/installer/cli/adminconsole_resetpassword.go index 137131116..d8932d29d 100644 --- a/cmd/installer/cli/adminconsole_resetpassword.go +++ b/cmd/installer/cli/adminconsole_resetpassword.go @@ -7,6 +7,7 @@ import ( "os" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" @@ -22,7 +23,8 @@ func AdminConsoleResetPasswordCmd(ctx context.Context, name string) *cobra.Comma Args: cobra.MaximumNArgs(1), Short: fmt.Sprintf("Reset the %s Admin Console password. If no password is provided, you will be prompted to enter a new one.", name), PreRunE: func(cmd *cobra.Command, args []string) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("reset-password command must be run as root") } diff --git a/cmd/installer/cli/enable_ha.go b/cmd/installer/cli/enable_ha.go index 83e3b2a4d..cc94bfe3c 100644 --- a/cmd/installer/cli/enable_ha.go +++ b/cmd/installer/cli/enable_ha.go @@ -5,9 +5,12 @@ import ( "fmt" "os" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/addons" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" + "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/replicatedhq/embedded-cluster/pkg/spinner" @@ -24,7 +27,8 @@ func EnableHACmd(ctx context.Context, name string) *cobra.Command { Use: "enable-ha", Short: fmt.Sprintf("Enable high availability for the %s cluster", name), PreRunE: func(cmd *cobra.Command, args []string) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("enable-ha command must be run as root") } @@ -91,7 +95,7 @@ func runEnableHA(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { addons.WithKubernetesClientSet(kclient), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(domains.GetDomains(in.Spec.Config, release.GetChannelRelease())), ) canEnableHA, reason, err := addOns.CanEnableHA(ctx) @@ -106,5 +110,19 @@ func runEnableHA(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { loading := spinner.Start() defer loading.Close() - return addOns.EnableHA(ctx, in.Spec, loading) + opts := addons.EnableHAOptions{ + AdminConsolePort: rc.AdminConsolePort(), + IsAirgap: in.Spec.AirGap, + IsMultiNodeEnabled: in.Spec.LicenseInfo != nil && in.Spec.LicenseInfo.IsMultiNodeEnabled, + EmbeddedConfigSpec: in.Spec.Config, + EndUserConfigSpec: nil, // TODO: add support for end user config spec + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + SeaweedFSDataDir: rc.EmbeddedClusterSeaweedFSSubDir(), + ServiceCIDR: rc.ServiceCIDR(), + } + + return addOns.EnableHA(ctx, opts, loading) } diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 942b9be44..e884b9a6e 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -19,6 +19,8 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/cloudutils" newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" ecmetadata "github.com/replicatedhq/embedded-cluster/pkg-new/metadata" @@ -740,6 +742,7 @@ func getAddonInstallOpts(flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, opts := &addons.InstallOptions{ AdminConsolePwd: flags.adminConsolePassword, + AdminConsolePort: rc.AdminConsolePort(), License: flags.license, IsAirgap: flags.airgapBundle != "", TLSCertBytes: flags.tlsCertBytes, @@ -749,12 +752,18 @@ func getAddonInstallOpts(flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, IsMultiNodeEnabled: flags.license.Spec.IsEmbeddedClusterMultiNodeEnabled, EmbeddedConfigSpec: embCfgSpec, EndUserConfigSpec: euCfgSpec, + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + ServiceCIDR: rc.ServiceCIDR(), KotsInstaller: func() error { opts := kotscli.InstallOptions{ RuntimeConfig: rc, AppSlug: flags.license.Spec.AppSlug, License: flags.licenseBytes, - Namespace: runtimeconfig.KotsadmNamespace, + Namespace: constants.KotsadmNamespace, AirgapBundle: flags.airgapBundle, ConfigValuesFile: flags.configValues, ReplicatedAppEndpoint: replicatedAppURL(), @@ -1046,7 +1055,7 @@ func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interf addons.WithKubernetesClient(kcli), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(getDomains()), addons.WithProgressChannel(progressChan), ) @@ -1062,6 +1071,15 @@ func installAddons(ctx context.Context, kcli client.Client, mcli metadata.Interf return nil } +func getDomains() ecv1beta1.Domains { + var embCfgSpec *ecv1beta1.ConfigSpec + if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { + embCfgSpec = &embCfg.Spec + } + + return domains.GetDomains(embCfgSpec, release.GetChannelRelease()) +} + func installExtensions(ctx context.Context, hcli helm.Client) error { progressChan := make(chan extensions.ExtensionsProgress) defer close(progressChan) @@ -1213,20 +1231,12 @@ func validateAdminConsolePassword(password, passwordCheck string) bool { } func replicatedAppURL() string { - var embCfgSpec *ecv1beta1.ConfigSpec - if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { - embCfgSpec = &embCfg.Spec - } - domains := runtimeconfig.GetDomains(embCfgSpec) + domains := getDomains() return netutils.MaybeAddHTTPS(domains.ReplicatedAppDomain) } func proxyRegistryURL() string { - var embCfgSpec *ecv1beta1.ConfigSpec - if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { - embCfgSpec = &embCfg.Spec - } - domains := runtimeconfig.GetDomains(embCfgSpec) + domains := getDomains() return netutils.MaybeAddHTTPS(domains.ProxyRegistryDomain) } diff --git a/cmd/installer/cli/join.go b/cmd/installer/cli/join.go index c643e7b2f..e31d5e70c 100644 --- a/cmd/installer/cli/join.go +++ b/cmd/installer/cli/join.go @@ -14,12 +14,14 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/kinds/types/join" newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/airgap" "github.com/replicatedhq/embedded-cluster/pkg/config" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/kotsadm" @@ -111,7 +113,8 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { } func preRunJoin(flags *JoinCmdFlags) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("join command must be run as root") } @@ -451,7 +454,7 @@ func installK0sBinary(rc runtimeconfig.RuntimeConfig) error { } func applyNetworkConfiguration(rc runtimeconfig.RuntimeConfig, jcmd *join.JoinCommandResponse) error { - domains := runtimeconfig.GetDomains(jcmd.InstallationSpec.Config) + domains := domains.GetDomains(jcmd.InstallationSpec.Config, release.GetChannelRelease()) clusterSpec := config.RenderK0sConfig(domains.ProxyRegistryDomain) address, err := netutils.FirstValidAddress(rc.NetworkInterface()) @@ -617,7 +620,7 @@ func maybeEnableHA(ctx context.Context, kcli client.Client, mcli metadata.Interf addons.WithKubernetesClientSet(kclient), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(getDomains()), ) canEnableHA, _, err := addOns.CanEnableHA(ctx) @@ -653,5 +656,19 @@ func maybeEnableHA(ctx context.Context, kcli client.Client, mcli metadata.Interf loading := spinner.Start() defer loading.Close() - return addOns.EnableHA(ctx, jcmd.InstallationSpec, loading) + opts := addons.EnableHAOptions{ + AdminConsolePort: rc.AdminConsolePort(), + IsAirgap: jcmd.InstallationSpec.AirGap, + IsMultiNodeEnabled: jcmd.InstallationSpec.LicenseInfo != nil && jcmd.InstallationSpec.LicenseInfo.IsMultiNodeEnabled, + EmbeddedConfigSpec: jcmd.InstallationSpec.Config, + EndUserConfigSpec: nil, // TODO: add support for end user config spec + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + SeaweedFSDataDir: rc.EmbeddedClusterSeaweedFSSubDir(), + ServiceCIDR: rc.ServiceCIDR(), + } + + return addOns.EnableHA(ctx, opts, loading) } diff --git a/cmd/installer/cli/join_printcommand.go b/cmd/installer/cli/join_printcommand.go index 13534aa78..d980eb5d5 100644 --- a/cmd/installer/cli/join_printcommand.go +++ b/cmd/installer/cli/join_printcommand.go @@ -6,6 +6,7 @@ import ( "os" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/spf13/cobra" @@ -18,7 +19,8 @@ func JoinPrintCommandCmd(ctx context.Context, name string) *cobra.Command { Use: "print-command", Short: fmt.Sprintf("Print controller join command for %s", name), PreRunE: func(cmd *cobra.Command, args []string) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("print-command command must be run as root") } diff --git a/cmd/installer/cli/join_runpreflights.go b/cmd/installer/cli/join_runpreflights.go index 187078fe5..a8d774b45 100644 --- a/cmd/installer/cli/join_runpreflights.go +++ b/cmd/installer/cli/join_runpreflights.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/kinds/types/join" newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" "github.com/replicatedhq/embedded-cluster/pkg/kotsadm" @@ -101,7 +102,7 @@ func runJoinPreflights(ctx context.Context, jcmd *join.JoinCommandResponse, flag return fmt.Errorf("unable to find first valid address: %w", err) } - domains := runtimeconfig.GetDomains(jcmd.InstallationSpec.Config) + domains := domains.GetDomains(jcmd.InstallationSpec.Config, release.GetChannelRelease()) hpf, err := preflights.Prepare(ctx, preflights.PrepareOptions{ HostPreflightSpec: release.GetHostPreflights(), diff --git a/cmd/installer/cli/materialize.go b/cmd/installer/cli/materialize.go index 1a2495149..1d389aa42 100644 --- a/cmd/installer/cli/materialize.go +++ b/cmd/installer/cli/materialize.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/spf13/cobra" ) @@ -20,7 +21,8 @@ func MaterializeCmd(ctx context.Context, name string) *cobra.Command { Short: "Materialize embedded assets into the data directory", Hidden: true, PreRunE: func(cmd *cobra.Command, args []string) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("materialize command must be run as root") } diff --git a/cmd/installer/cli/reset_firewalld.go b/cmd/installer/cli/reset_firewalld.go index 1f8a0e4b8..4d4d4f42c 100644 --- a/cmd/installer/cli/reset_firewalld.go +++ b/cmd/installer/cli/reset_firewalld.go @@ -6,6 +6,7 @@ import ( "os" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/sirupsen/logrus" @@ -20,7 +21,8 @@ func ResetFirewalldCmd(ctx context.Context, name string) *cobra.Command { Short: "Remove %s firewalld configuration from the current node", Hidden: true, PreRunE: func(cmd *cobra.Command, args []string) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("reset firewalld command must be run as root") } diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index 37ffb8d53..590967335 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -21,11 +21,11 @@ import ( apitypes "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" "github.com/replicatedhq/embedded-cluster/pkg/addons" addontypes "github.com/replicatedhq/embedded-cluster/pkg/addons/types" - "github.com/replicatedhq/embedded-cluster/pkg/constants" "github.com/replicatedhq/embedded-cluster/pkg/disasterrecovery" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" @@ -434,7 +434,7 @@ func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, Path: s3Store.prefix, AccessKeyID: s3Store.accessKeyID, SecretAccessKey: s3Store.secretAccessKey, - Namespace: runtimeconfig.KotsadmNamespace, + Namespace: constants.KotsadmNamespace, }); err != nil { return err } @@ -472,15 +472,18 @@ func installAddonsForRestore(ctx context.Context, kcli client.Client, mcli metad addons.WithKubernetesClient(kcli), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(getDomains()), addons.WithProgressChannel(progressChan), ) - if err := addOns.Install(ctx, addons.InstallOptions{ - IsAirgap: flags.airgapBundle != "", - IsRestore: true, + if err := addOns.Restore(ctx, addons.RestoreOptions{ EmbeddedConfigSpec: embCfgSpec, EndUserConfigSpec: nil, // TODO: support for end user config overrides + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), }); err != nil { return fmt.Errorf("install addons: %w", err) } @@ -601,6 +604,15 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, 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 + } + hcli, err := helm.NewClient(helm.HelmOptions{ KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, @@ -616,10 +628,24 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, addons.WithKubernetesClient(kcli), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(getDomains()), ) - err = addOns.EnableAdminConsoleHA(ctx, flags.isAirgap, in.Spec.Config, in.Spec.LicenseInfo) + opts := addons.EnableHAOptions{ + AdminConsolePort: rc.AdminConsolePort(), + IsAirgap: in.Spec.AirGap, + IsMultiNodeEnabled: in.Spec.LicenseInfo != nil && in.Spec.LicenseInfo.IsMultiNodeEnabled, + EmbeddedConfigSpec: in.Spec.Config, + EndUserConfigSpec: euCfgSpec, + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + SeaweedFSDataDir: rc.EmbeddedClusterSeaweedFSSubDir(), + ServiceCIDR: rc.ServiceCIDR(), + } + + err = addOns.EnableAdminConsoleHA(ctx, opts) if err != nil { return err } @@ -770,7 +796,7 @@ func getECRestoreState(ctx context.Context) ecRestoreState { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.EmbeddedClusterNamespace, + Namespace: constants.EmbeddedClusterNamespace, Name: constants.EcRestoreStateCMName, }, } @@ -802,7 +828,7 @@ func setECRestoreState(ctx context.Context, state ecRestoreState, backupName str ns := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: runtimeconfig.EmbeddedClusterNamespace, + Name: constants.EmbeddedClusterNamespace, }, } @@ -812,7 +838,7 @@ func setECRestoreState(ctx context.Context, state ecRestoreState, backupName str cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.EmbeddedClusterNamespace, + Namespace: constants.EmbeddedClusterNamespace, Name: constants.EcRestoreStateCMName, }, Data: map[string]string{ @@ -845,7 +871,7 @@ func resetECRestoreState(ctx context.Context) error { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.EmbeddedClusterNamespace, + Namespace: constants.EmbeddedClusterNamespace, Name: constants.EcRestoreStateCMName, }, } @@ -870,7 +896,7 @@ func getBackupFromRestoreState(ctx context.Context, isAirgap bool, rc runtimecon cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.EmbeddedClusterNamespace, + Namespace: constants.EmbeddedClusterNamespace, Name: constants.EcRestoreStateCMName, }, } @@ -884,7 +910,7 @@ func getBackupFromRestoreState(ctx context.Context, isAirgap bool, rc runtimecon return nil, nil } - backup, err := disasterrecovery.GetReplicatedBackup(ctx, kcli, runtimeconfig.VeleroNamespace, backupName) + backup, err := disasterrecovery.GetReplicatedBackup(ctx, kcli, constants.VeleroNamespace, backupName) if err != nil { return nil, err } @@ -1223,7 +1249,7 @@ func waitForVeleroRestoreCompleted(ctx context.Context, restoreName string) (*ve for { restore := velerov1.Restore{} - err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: runtimeconfig.VeleroNamespace}, &restore) + err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: constants.VeleroNamespace}, &restore) if err != nil { return nil, fmt.Errorf("unable to get restore: %w", err) } @@ -1321,7 +1347,7 @@ func ensureRestoreResourceModifiers(ctx context.Context, backup *velerov1.Backup cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.VeleroNamespace, + Namespace: constants.VeleroNamespace, Name: resourceModifiersCMName, }, Data: map[string]string{ @@ -1377,7 +1403,7 @@ func waitForDRComponent(ctx context.Context, drComponent disasterRecoveryCompone return fmt.Errorf("unable to create kube client: %w", err) } - if err := restoreWaitForAdminConsoleReady(ctx, kcli, runtimeconfig.KotsadmNamespace, loading); err != nil { + if err := restoreWaitForAdminConsoleReady(ctx, kcli, constants.KotsadmNamespace, loading); err != nil { return fmt.Errorf("unable to wait for admin console: %w", err) } } else if drComponent == disasterRecoveryComponentSeaweedFS { @@ -1387,7 +1413,7 @@ func waitForDRComponent(ctx context.Context, drComponent disasterRecoveryCompone return fmt.Errorf("unable to create kube client: %w", err) } - if err := restoreWaitForSeaweedfsReady(ctx, kcli, runtimeconfig.SeaweedFSNamespace, nil); err != nil { + if err := restoreWaitForSeaweedfsReady(ctx, kcli, constants.SeaweedFSNamespace, nil); err != nil { return fmt.Errorf("unable to wait for seaweedfs to be ready: %w", err) } } else if drComponent == disasterRecoveryComponentRegistry { @@ -1397,7 +1423,7 @@ func waitForDRComponent(ctx context.Context, drComponent disasterRecoveryCompone return fmt.Errorf("unable to create kube client: %w", err) } - if err := kubeutils.WaitForDeployment(ctx, kcli, runtimeconfig.RegistryNamespace, "registry", nil); err != nil { + if err := kubeutils.WaitForDeployment(ctx, kcli, constants.RegistryNamespace, "registry", nil); err != nil { return fmt.Errorf("unable to wait for registry to be ready: %w", err) } } else if drComponent == disasterRecoveryComponentECO { @@ -1408,7 +1434,7 @@ func waitForDRComponent(ctx context.Context, drComponent disasterRecoveryCompone } if isV2 { - if err := kubeutils.WaitForDeployment(ctx, kcli, runtimeconfig.EmbeddedClusterNamespace, "embedded-cluster-operator", nil); err != nil { + if err := kubeutils.WaitForDeployment(ctx, kcli, constants.EmbeddedClusterNamespace, "embedded-cluster-operator", nil); err != nil { return fmt.Errorf("unable to wait for embedded cluster operator to be ready: %w", err) } } else { @@ -1488,14 +1514,14 @@ func restoreAppFromBackup(ctx context.Context, backup *velerov1.Backup, restore // check if a restore object already exists rest := velerov1.Restore{} - err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: runtimeconfig.VeleroNamespace}, &rest) + err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: constants.VeleroNamespace}, &rest) if err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("unable to get restore: %w", err) } // create a new restore object if it doesn't exist if k8serrors.IsNotFound(err) { - restore.Namespace = runtimeconfig.VeleroNamespace + restore.Namespace = constants.VeleroNamespace restore.Name = restoreName if restore.Annotations == nil { restore.Annotations = map[string]string{} @@ -1530,7 +1556,7 @@ func restoreFromBackup(ctx context.Context, backup *velerov1.Backup, drComponent // check if a restore object already exists rest := velerov1.Restore{} - err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: runtimeconfig.VeleroNamespace}, &rest) + err = kcli.Get(ctx, types.NamespacedName{Name: restoreName, Namespace: constants.VeleroNamespace}, &rest) if err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("unable to get restore: %w", err) } @@ -1553,7 +1579,7 @@ func restoreFromBackup(ctx context.Context, backup *velerov1.Backup, drComponent restore := &velerov1.Restore{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.VeleroNamespace, + Namespace: constants.VeleroNamespace, Name: restoreName, Annotations: map[string]string{ disasterrecovery.BackupIsECAnnotation: "true", diff --git a/cmd/installer/cli/shell.go b/cmd/installer/cli/shell.go index 25e2c23a2..c3b330ae7 100644 --- a/cmd/installer/cli/shell.go +++ b/cmd/installer/cli/shell.go @@ -11,6 +11,7 @@ import ( "syscall" "github.com/creack/pty" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/sirupsen/logrus" @@ -34,7 +35,8 @@ func ShellCmd(ctx context.Context, name string) *cobra.Command { Use: "shell", Short: fmt.Sprintf("Start a shell with access to the %s cluster", name), PreRunE: func(cmd *cobra.Command, args []string) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("shell command must be run as root") } diff --git a/cmd/installer/cli/update.go b/cmd/installer/cli/update.go index cfc80d919..ec16ef7ab 100644 --- a/cmd/installer/cli/update.go +++ b/cmd/installer/cli/update.go @@ -6,6 +6,8 @@ import ( "os" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" @@ -21,7 +23,8 @@ func UpdateCmd(ctx context.Context, name string) *cobra.Command { Use: "update", Short: fmt.Sprintf("Update %s with a new air gap bundle", name), PreRunE: func(cmd *cobra.Command, args []string) error { - if os.Getuid() != 0 { + // Skip root check if dryrun mode is enabled + if !dryrun.Enabled() && os.Getuid() != 0 { return fmt.Errorf("update command must be run as root") } @@ -55,7 +58,7 @@ func UpdateCmd(ctx context.Context, name string) *cobra.Command { if err := kotscli.AirgapUpdate(kotscli.AirgapUpdateOptions{ RuntimeConfig: rc, AppSlug: rel.AppSlug, - Namespace: runtimeconfig.KotsadmNamespace, + Namespace: constants.KotsadmNamespace, AirgapBundle: airgapBundle, }); err != nil { return err diff --git a/operator/pkg/upgrade/job.go b/operator/pkg/upgrade/job.go index faf269c61..c13fb5a5b 100644 --- a/operator/pkg/upgrade/job.go +++ b/operator/pkg/upgrade/job.go @@ -14,6 +14,8 @@ import ( "github.com/replicatedhq/embedded-cluster/operator/pkg/autopilot" "github.com/replicatedhq/embedded-cluster/operator/pkg/metadata" "github.com/replicatedhq/embedded-cluster/operator/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" batchv1 "k8s.io/api/batch/v1" @@ -30,7 +32,7 @@ import ( const ( upgradeJobName = "embedded-cluster-upgrade-%s" - upgradeJobNamespace = runtimeconfig.KotsadmNamespace + upgradeJobNamespace = constants.KotsadmNamespace upgradeJobConfigMap = "upgrade-job-configmap-%s" ) @@ -180,7 +182,7 @@ func CreateUpgradeJob( }, }, RestartPolicy: corev1.RestartPolicyNever, - ServiceAccountName: runtimeconfig.KotsadmServiceAccount, + ServiceAccountName: constants.KotsadmServiceAccount, Volumes: []corev1.Volume{ { Name: "config", @@ -292,7 +294,10 @@ func operatorImageName(ctx context.Context, cli client.Client, in *ecv1beta1.Ins } for _, image := range meta.Images { if strings.Contains(image, "embedded-cluster-operator-image") { - domains := runtimeconfig.GetDomains(in.Spec.Config) + // TODO: This will not work in a non-production environment. + // The domains in the release are used to supply alternative defaults for staging and the dev environment. + // The GetDomains function will always fall back to production defaults. + domains := domains.GetDomains(in.Spec.Config, nil) image = strings.Replace(image, "proxy.replicated.com", domains.ProxyRegistryDomain, 1) return image, nil } diff --git a/operator/pkg/upgrade/upgrade.go b/operator/pkg/upgrade/upgrade.go index 59a5baeda..45e1abc07 100644 --- a/operator/pkg/upgrade/upgrade.go +++ b/operator/pkg/upgrade/upgrade.go @@ -13,6 +13,7 @@ import ( ectypes "github.com/replicatedhq/embedded-cluster/kinds/types" "github.com/replicatedhq/embedded-cluster/operator/pkg/autopilot" "github.com/replicatedhq/embedded-cluster/operator/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/config" "github.com/replicatedhq/embedded-cluster/pkg/extensions" @@ -180,7 +181,10 @@ func updateClusterConfig(ctx context.Context, cli client.Client, in *ecv1beta1.I return fmt.Errorf("get cluster config: %w", err) } - domains := runtimeconfig.GetDomains(in.Spec.Config) + // TODO: This will not work in a non-production environment. + // The domains in the release are used to supply alternative defaults for staging and the dev environment. + // The GetDomains function will always fall back to production defaults. + domains := domains.GetDomains(in.Spec.Config, nil) didUpdate := false @@ -267,14 +271,37 @@ func upgradeAddons(ctx context.Context, cli client.Client, hcli helm.Client, rc return fmt.Errorf("create metadata client: %w", err) } + // TODO: This will not work in a non-production environment. + // The domains in the release are used to supply alternative defaults for staging and the dev environment. + // The GetDomains function will always fall back to production defaults. + domains := domains.GetDomains(in.Spec.Config, nil) + addOns := addons.New( addons.WithLogFunc(slog.Info), addons.WithKubernetesClient(cli), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(domains), ) - if err := addOns.Upgrade(ctx, in, meta); err != nil { + + opts := addons.UpgradeOptions{ + AdminConsolePort: rc.AdminConsolePort(), + IsAirgap: in.Spec.AirGap, + IsHA: in.Spec.HighAvailability, + DisasterRecoveryEnabled: in.Spec.LicenseInfo != nil && in.Spec.LicenseInfo.IsDisasterRecoverySupported, + IsMultiNodeEnabled: in.Spec.LicenseInfo != nil && in.Spec.LicenseInfo.IsMultiNodeEnabled, + EmbeddedConfigSpec: in.Spec.Config, + EndUserConfigSpec: nil, // TODO: add support for end user config spec + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + SeaweedFSDataDir: rc.EmbeddedClusterSeaweedFSSubDir(), + ServiceCIDR: rc.ServiceCIDR(), + } + + if err := addOns.Upgrade(ctx, in, meta, opts); err != nil { return fmt.Errorf("upgrade addons: %w", err) } diff --git a/pkg-new/constants/constants.go b/pkg-new/constants/constants.go new file mode 100644 index 000000000..3ee7a5017 --- /dev/null +++ b/pkg-new/constants/constants.go @@ -0,0 +1,14 @@ +package constants + +const ( + KotsadmNamespace = "kotsadm" + KotsadmServiceAccount = "kotsadm" + SeaweedFSNamespace = "seaweedfs" + RegistryNamespace = "registry" + VeleroNamespace = "velero" + EmbeddedClusterNamespace = "embedded-cluster" +) + +const ( + EcRestoreStateCMName = "embedded-cluster-restore-state" +) diff --git a/pkg-new/k0s/k0s.go b/pkg-new/k0s/k0s.go index 3edc1d65b..43a01e2db 100644 --- a/pkg-new/k0s/k0s.go +++ b/pkg-new/k0s/k0s.go @@ -11,6 +11,7 @@ import ( k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/airgap" "github.com/replicatedhq/embedded-cluster/pkg/config" "github.com/replicatedhq/embedded-cluster/pkg/helpers" @@ -100,7 +101,7 @@ func NewK0sConfig(networkInterface string, isAirgap bool, podCIDR string, servic embCfgSpec = &embCfg.Spec } - domains := runtimeconfig.GetDomains(embCfgSpec) + domains := domains.GetDomains(embCfgSpec, release.GetChannelRelease()) cfg := config.RenderK0sConfig(domains.ProxyRegistryDomain) address, err := netutils.FirstValidAddress(networkInterface) diff --git a/pkg-new/tlsutils/tls.go b/pkg-new/tlsutils/tls.go index 6a83bffbf..30daa2b37 100644 --- a/pkg-new/tlsutils/tls.go +++ b/pkg-new/tlsutils/tls.go @@ -5,7 +5,7 @@ import ( "fmt" "net" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" certutil "k8s.io/client-go/util/cert" ) @@ -57,7 +57,7 @@ func GetTLSConfig(cert tls.Certificate) *tls.Config { } func generateCertHostnames(hostname string) (string, []string) { - namespace := runtimeconfig.KotsadmNamespace + namespace := constants.KotsadmNamespace if hostname == "" { hostname = fmt.Sprintf("kotsadm.%s.svc.cluster.local", namespace) diff --git a/pkg/addons/adminconsole/adminconsole.go b/pkg/addons/adminconsole/adminconsole.go index 30a512ef2..6b03c89e7 100644 --- a/pkg/addons/adminconsole/adminconsole.go +++ b/pkg/addons/adminconsole/adminconsole.go @@ -5,13 +5,14 @@ import ( "strings" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) const ( _releaseName = "admin-console" - _namespace = runtimeconfig.KotsadmNamespace + + _namespace = constants.KotsadmNamespace ) var _ types.AddOn = (*AdminConsole)(nil) @@ -21,12 +22,18 @@ type AdminConsole struct { IsHA bool Proxy *ecv1beta1.ProxySpec ServiceCIDR string - Password string - TLSCertBytes []byte - TLSKeyBytes []byte - Hostname string - KotsInstaller KotsInstaller IsMultiNodeEnabled bool + HostCABundlePath string + DataDir string + K0sDataDir string + AdminConsolePort int + + // These options are only used during installation + Password string + TLSCertBytes []byte + TLSKeyBytes []byte + Hostname string + KotsInstaller KotsInstaller // DryRun is a flag to enable dry-run mode for Admin Console. // If true, Admin Console will only render the helm template and additional manifests, but not install diff --git a/pkg/addons/adminconsole/install.go b/pkg/addons/adminconsole/install.go index 236bb8975..7cffd2820 100644 --- a/pkg/addons/adminconsole/install.go +++ b/pkg/addons/adminconsole/install.go @@ -13,7 +13,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "golang.org/x/crypto/bcrypt" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -38,16 +37,15 @@ func init() { func (a *AdminConsole) Install( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, - overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { // some resources are not part of the helm chart and need to be created before the chart is installed // TODO: move this to the helm chart - if err := a.createPreRequisites(ctx, logf, kcli, mcli, rc); err != nil { + if err := a.createPreRequisites(ctx, logf, kcli, mcli); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := a.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := a.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } @@ -85,7 +83,7 @@ func (a *AdminConsole) Install( return nil } -func (a *AdminConsole) createPreRequisites(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, rc runtimeconfig.RuntimeConfig) error { +func (a *AdminConsole) createPreRequisites(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface) error { if err := a.createNamespace(ctx, kcli); err != nil { return errors.Wrap(err, "create namespace") } @@ -98,7 +96,7 @@ func (a *AdminConsole) createPreRequisites(ctx context.Context, logf types.LogFu return errors.Wrap(err, "create kots TLS secret") } - if err := a.ensureCAConfigmap(ctx, logf, kcli, mcli, rc); err != nil { + if err := a.ensureCAConfigmap(ctx, logf, kcli, mcli); err != nil { return errors.Wrap(err, "ensure CA configmap") } @@ -265,17 +263,17 @@ func (a *AdminConsole) createTLSSecret(ctx context.Context, kcli client.Client) return nil } -func (a *AdminConsole) ensureCAConfigmap(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, rc runtimeconfig.RuntimeConfig) error { - if rc.HostCABundlePath() == "" { +func (a *AdminConsole) ensureCAConfigmap(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface) error { + if a.HostCABundlePath == "" { return nil } if a.DryRun { - checksum, err := calculateFileChecksum(rc.HostCABundlePath()) + checksum, err := calculateFileChecksum(a.HostCABundlePath) if err != nil { return fmt.Errorf("calculate checksum: %w", err) } - new, err := newCAConfigMap(rc.HostCABundlePath(), checksum) + new, err := newCAConfigMap(a.HostCABundlePath, checksum) if err != nil { return fmt.Errorf("create map: %w", err) } @@ -287,7 +285,7 @@ func (a *AdminConsole) ensureCAConfigmap(ctx context.Context, logf types.LogFunc return nil } - err := EnsureCAConfigmap(ctx, logf, kcli, mcli, rc.HostCABundlePath()) + err := EnsureCAConfigmap(ctx, logf, kcli, mcli, a.HostCABundlePath) if k8serrors.IsRequestEntityTooLargeError(err) || errors.Is(err, fs.ErrNotExist) { // This can result in issues installing in environments with a MITM HTTP proxy. diff --git a/pkg/addons/adminconsole/install_test.go b/pkg/addons/adminconsole/install_test.go index 50fffaf2b..dcecc2d2e 100644 --- a/pkg/addons/adminconsole/install_test.go +++ b/pkg/addons/adminconsole/install_test.go @@ -9,7 +9,6 @@ import ( "path/filepath" "testing" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -174,13 +173,12 @@ func TestAdminConsole_ensureCAConfigmap(t *testing.T) { kcli, mcli := tt.initClients(t) - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - rc.SetHostCABundlePath(tt.caPath) - // Run test - addon := &AdminConsole{} - err = addon.ensureCAConfigmap(t.Context(), t.Logf, kcli, mcli, rc) + addon := &AdminConsole{ + DataDir: t.TempDir(), + HostCABundlePath: tt.caPath, + } + err = addon.ensureCAConfigmap(t.Context(), t.Logf, kcli, mcli) // Check results if tt.expectedErr { diff --git a/pkg/addons/adminconsole/integration/hostcabundle_test.go b/pkg/addons/adminconsole/integration/hostcabundle_test.go index 82097ce1b..44abaaa60 100644 --- a/pkg/addons/adminconsole/integration/hostcabundle_test.go +++ b/pkg/addons/adminconsole/integration/hostcabundle_test.go @@ -10,7 +10,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -20,20 +19,18 @@ import ( ) func TestHostCABundle(t *testing.T) { - rc := runtimeconfig.New(nil) - rc.SetHostCABundlePath(filepath.Join(t.TempDir(), "ca-certificates.crt")) - addon := &adminconsole.AdminConsole{ - DryRun: true, + DryRun: true, + HostCABundlePath: filepath.Join(t.TempDir(), "ca-certificates.crt"), } - err := os.WriteFile(rc.HostCABundlePath(), []byte("test"), 0644) + err := os.WriteFile(addon.HostCABundlePath, []byte("test"), 0644) require.NoError(t, err, "Failed to write CA bundle file") hcli, err := helm.NewClient(helm.HelmOptions{}) require.NoError(t, err, "NewClient should not return an error") - err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, ecv1beta1.Domains{}, nil) + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, ecv1beta1.Domains{}, nil) require.NoError(t, err, "adminconsole.Install should not return an error") manifests := addon.DryRunManifests() @@ -60,7 +57,7 @@ func TestHostCABundle(t *testing.T) { } } if assert.NotNil(t, volume, "Admin Console host-ca-bundle volume should not be nil") { - assert.Equal(t, rc.HostCABundlePath(), volume.VolumeSource.HostPath.Path) + assert.Equal(t, addon.HostCABundlePath, volume.VolumeSource.HostPath.Path) assert.Equal(t, ptr.To(corev1.HostPathFileOrCreate), volume.VolumeSource.HostPath.Type) } diff --git a/pkg/addons/adminconsole/upgrade.go b/pkg/addons/adminconsole/upgrade.go index a1cbd450f..ffd85b0a8 100644 --- a/pkg/addons/adminconsole/upgrade.go +++ b/pkg/addons/adminconsole/upgrade.go @@ -7,7 +7,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/metadata" @@ -17,7 +16,7 @@ import ( func (a *AdminConsole) Upgrade( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { exists, err := hcli.ReleaseExists(ctx, a.Namespace(), a.ReleaseName()) if err != nil { @@ -28,7 +27,7 @@ func (a *AdminConsole) Upgrade( return errors.New("admin console release not found") } - values, err := a.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := a.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/adminconsole/values.go b/pkg/addons/adminconsole/values.go index f5ae65b95..f3fed995e 100644 --- a/pkg/addons/adminconsole/values.go +++ b/pkg/addons/adminconsole/values.go @@ -11,7 +11,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/versions" "gopkg.in/yaml.v3" "sigs.k8s.io/controller-runtime/pkg/client" @@ -48,7 +47,7 @@ func init() { } } -func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { +func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { // create a copy of the helm values so we don't modify the original marshalled, err := helm.MarshalValues(helmValues) if err != nil { @@ -66,8 +65,8 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien } copiedValues["embeddedClusterID"] = metrics.ClusterID().String() - copiedValues["embeddedClusterDataDir"] = rc.EmbeddedClusterHomeDirectory() - copiedValues["embeddedClusterK0sDir"] = rc.EmbeddedClusterK0sSubDir() + copiedValues["embeddedClusterDataDir"] = a.DataDir + copiedValues["embeddedClusterK0sDir"] = a.K0sDataDir copiedValues["isHA"] = a.IsHA copiedValues["isMultiNodeEnabled"] = a.IsMultiNodeEnabled @@ -118,11 +117,11 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien extraVolumes := []map[string]interface{}{} extraVolumeMounts := []map[string]interface{}{} - if rc.HostCABundlePath() != "" { + if a.HostCABundlePath != "" { extraVolumes = append(extraVolumes, map[string]interface{}{ "name": "host-ca-bundle", "hostPath": map[string]interface{}{ - "path": rc.HostCABundlePath(), + "path": a.HostCABundlePath, "type": "FileOrCreate", }, }) @@ -142,7 +141,7 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien copiedValues["extraVolumes"] = extraVolumes copiedValues["extraVolumeMounts"] = extraVolumeMounts - err = helm.SetValue(copiedValues, "kurlProxy.nodePort", rc.AdminConsolePort()) + err = helm.SetValue(copiedValues, "kurlProxy.nodePort", a.AdminConsolePort) if err != nil { return nil, errors.Wrap(err, "set kurlProxy.nodePort") } diff --git a/pkg/addons/adminconsole/values_test.go b/pkg/addons/adminconsole/values_test.go index 6475310da..2106a6cf9 100644 --- a/pkg/addons/adminconsole/values_test.go +++ b/pkg/addons/adminconsole/values_test.go @@ -5,20 +5,18 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { t.Run("with host CA bundle path", func(t *testing.T) { - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - rc.SetHostCABundlePath("/etc/ssl/certs/ca-certificates.crt") - - adminConsole := &AdminConsole{} + adminConsole := &AdminConsole{ + DataDir: t.TempDir(), + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", + } - values, err := adminConsole.GenerateHelmValues(context.Background(), nil, rc, ecv1beta1.Domains{}, nil) + values, err := adminConsole.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") // Verify structure types @@ -61,13 +59,12 @@ func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { }) t.Run("without host CA bundle path", func(t *testing.T) { - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) // HostCABundlePath intentionally not set + adminConsole := &AdminConsole{ + DataDir: t.TempDir(), + } - adminConsole := &AdminConsole{} - - values, err := adminConsole.GenerateHelmValues(context.Background(), nil, rc, ecv1beta1.Domains{}, nil) + values, err := adminConsole.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") // Verify structure types diff --git a/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go b/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go index e58c42360..b94eceffa 100644 --- a/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go +++ b/pkg/addons/embeddedclusteroperator/embeddedclusteroperator.go @@ -29,8 +29,9 @@ func init() { var _ types.AddOn = (*EmbeddedClusterOperator)(nil) type EmbeddedClusterOperator struct { - IsAirgap bool - Proxy *ecv1beta1.ProxySpec + IsAirgap bool + Proxy *ecv1beta1.ProxySpec + HostCABundlePath string ChartLocationOverride string ChartVersionOverride string diff --git a/pkg/addons/embeddedclusteroperator/install.go b/pkg/addons/embeddedclusteroperator/install.go index b58db33b8..5e0e1f590 100644 --- a/pkg/addons/embeddedclusteroperator/install.go +++ b/pkg/addons/embeddedclusteroperator/install.go @@ -7,7 +7,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -15,10 +14,9 @@ import ( func (e *EmbeddedClusterOperator) Install( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, - overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { - values, err := e.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := e.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/embeddedclusteroperator/integration/hostcabundle_test.go b/pkg/addons/embeddedclusteroperator/integration/hostcabundle_test.go index a97037755..df0307c08 100644 --- a/pkg/addons/embeddedclusteroperator/integration/hostcabundle_test.go +++ b/pkg/addons/embeddedclusteroperator/integration/hostcabundle_test.go @@ -9,7 +9,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/embeddedclusteroperator" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -22,18 +21,16 @@ func TestHostCABundle(t *testing.T) { chartLocation, err := filepath.Abs("../../../../operator/charts/embedded-cluster-operator") require.NoError(t, err, "Failed to get chart location") - rc := runtimeconfig.New(nil) - rc.SetHostCABundlePath("/etc/ssl/certs/ca-certificates.crt") - addon := &embeddedclusteroperator.EmbeddedClusterOperator{ DryRun: true, ChartLocationOverride: chartLocation, + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", } hcli, err := helm.NewClient(helm.HelmOptions{}) require.NoError(t, err, "NewClient should not return an error") - err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, ecv1beta1.Domains{}, nil) + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, ecv1beta1.Domains{}, nil) require.NoError(t, err, "embeddedclusteroperator.Install should not return an error") manifests := addon.DryRunManifests() diff --git a/pkg/addons/embeddedclusteroperator/upgrade.go b/pkg/addons/embeddedclusteroperator/upgrade.go index d56fb455b..cbc266807 100644 --- a/pkg/addons/embeddedclusteroperator/upgrade.go +++ b/pkg/addons/embeddedclusteroperator/upgrade.go @@ -7,7 +7,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" @@ -16,7 +15,7 @@ import ( func (e *EmbeddedClusterOperator) Upgrade( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { exists, err := hcli.ReleaseExists(ctx, e.Namespace(), e.ReleaseName()) if err != nil { @@ -24,13 +23,13 @@ func (e *EmbeddedClusterOperator) Upgrade( } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", e.ReleaseName(), e.Namespace()) - if err := e.Install(ctx, logf, kcli, mcli, hcli, rc, domains, overrides); err != nil { + if err := e.Install(ctx, logf, kcli, mcli, hcli, domains, overrides); err != nil { return errors.Wrap(err, "install") } return nil } - values, err := e.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := e.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/embeddedclusteroperator/values.go b/pkg/addons/embeddedclusteroperator/values.go index ed10fee77..e433a28b7 100644 --- a/pkg/addons/embeddedclusteroperator/values.go +++ b/pkg/addons/embeddedclusteroperator/values.go @@ -10,7 +10,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/versions" "gopkg.in/yaml.v3" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,7 +37,7 @@ func init() { helmValues["embeddedClusterK0sVersion"] = versions.K0sVersion } -func (e *EmbeddedClusterOperator) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { +func (e *EmbeddedClusterOperator) GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { // create a copy of the helm values so we don't modify the original marshalled, err := helm.MarshalValues(helmValues) if err != nil { @@ -92,11 +91,11 @@ func (e *EmbeddedClusterOperator) GenerateHelmValues(ctx context.Context, kcli c }...) } - if rc.HostCABundlePath() != "" { + if e.HostCABundlePath != "" { extraVolumes = append(extraVolumes, map[string]any{ "name": "host-ca-bundle", "hostPath": map[string]any{ - "path": rc.HostCABundlePath(), + "path": e.HostCABundlePath, "type": "FileOrCreate", }, }) diff --git a/pkg/addons/embeddedclusteroperator/values_test.go b/pkg/addons/embeddedclusteroperator/values_test.go index 0b286f9af..1288368c4 100644 --- a/pkg/addons/embeddedclusteroperator/values_test.go +++ b/pkg/addons/embeddedclusteroperator/values_test.go @@ -5,19 +5,16 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - rc.SetHostCABundlePath("/etc/ssl/certs/ca-certificates.crt") - - e := &EmbeddedClusterOperator{} + e := &EmbeddedClusterOperator{ + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", + } - values, err := e.GenerateHelmValues(context.Background(), nil, rc, ecv1beta1.Domains{}, nil) + values, err := e.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") require.NotEmpty(t, values["extraVolumes"]) diff --git a/pkg/addons/highavailability.go b/pkg/addons/highavailability.go index 11e5cf770..fab4e3716 100644 --- a/pkg/addons/highavailability.go +++ b/pkg/addons/highavailability.go @@ -7,13 +7,12 @@ import ( "github.com/pkg/errors" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" registrymigrate "github.com/replicatedhq/embedded-cluster/pkg/addons/registry/migrate" "github.com/replicatedhq/embedded-cluster/pkg/addons/seaweedfs" - "github.com/replicatedhq/embedded-cluster/pkg/constants" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" @@ -22,6 +21,20 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +type EnableHAOptions struct { + AdminConsolePort int + IsAirgap bool + IsMultiNodeEnabled bool + EmbeddedConfigSpec *ecv1beta1.ConfigSpec + EndUserConfigSpec *ecv1beta1.ConfigSpec + ProxySpec *ecv1beta1.ProxySpec + HostCABundlePath string + DataDir string + K0sDataDir string + SeaweedFSDataDir string + ServiceCIDR string +} + // CanEnableHA checks if high availability can be enabled in the cluster. func (a *AddOns) CanEnableHA(ctx context.Context) (bool, string, error) { in, err := kubeutils.GetLatestInstallation(ctx, a.kcli) @@ -49,8 +62,8 @@ func (a *AddOns) CanEnableHA(ctx context.Context) (bool, string, error) { } // EnableHA enables high availability. -func (a *AddOns) EnableHA(ctx context.Context, inSpec ecv1beta1.InstallationSpec, spinner *spinner.MessageWriter) error { - if inSpec.AirGap { +func (a *AddOns) EnableHA(ctx context.Context, opts EnableHAOptions, spinner *spinner.MessageWriter) error { + if opts.IsAirgap { logrus.Debugf("Enabling high availability") spinner.Infof("Enabling high availability") @@ -59,7 +72,7 @@ func (a *AddOns) EnableHA(ctx context.Context, inSpec ecv1beta1.InstallationSpec return errors.Wrap(err, "check if registry data has been migrated") } else if !hasMigrated { logrus.Debugf("Installing seaweedfs") - err = a.ensureSeaweedfs(ctx, a.rc.ServiceCIDR(), inSpec.Config) + err = a.ensureSeaweedfs(ctx, opts) if err != nil { return errors.Wrap(err, "ensure seaweedfs") } @@ -75,7 +88,7 @@ func (a *AddOns) EnableHA(ctx context.Context, inSpec ecv1beta1.InstallationSpec logrus.Debugf("Migrating data for high availability") spinner.Infof("Migrating data for high availability") - err = a.migrateRegistryData(ctx, inSpec.Config, spinner) + err = a.migrateRegistryData(ctx, opts.EmbeddedConfigSpec, spinner) if err != nil { return errors.Wrap(err, "migrate registry data") } @@ -83,7 +96,7 @@ func (a *AddOns) EnableHA(ctx context.Context, inSpec ecv1beta1.InstallationSpec logrus.Debugf("Enabling high availability for the registry") spinner.Infof("Enabling high availability for the registry") - err = a.enableRegistryHA(ctx, a.rc.ServiceCIDR(), inSpec.Config) + err = a.enableRegistryHA(ctx, opts) if err != nil { return errors.Wrap(err, "enable registry high availability") } @@ -93,7 +106,7 @@ func (a *AddOns) EnableHA(ctx context.Context, inSpec ecv1beta1.InstallationSpec logrus.Debugf("Updating the Admin Console for high availability") spinner.Infof("Updating the Admin Console for high availability") - err := a.EnableAdminConsoleHA(ctx, inSpec.AirGap, inSpec.Config, inSpec.LicenseInfo) + err := a.EnableAdminConsoleHA(ctx, opts) if err != nil { return errors.Wrap(err, "enable admin console high availability") } @@ -122,7 +135,7 @@ func (a *AddOns) maybeScaleRegistryBackOnFailure() { deploy := &appsv1.Deployment{} // this should use the background context as we want it to run even if the context expired - err := a.kcli.Get(context.Background(), client.ObjectKey{Namespace: runtimeconfig.RegistryNamespace, Name: "registry"}, deploy) + err := a.kcli.Get(context.Background(), client.ObjectKey{Namespace: constants.RegistryNamespace, Name: "registry"}, deploy) if err != nil { logrus.Errorf("Failed to get registry deployment: %v", err) return @@ -150,7 +163,7 @@ func (a *AddOns) maybeScaleRegistryBackOnFailure() { // scaleRegistryDown scales the registry deployment to 0 replicas. func (a *AddOns) scaleRegistryDown(ctx context.Context) error { deploy := &appsv1.Deployment{} - err := a.kcli.Get(ctx, client.ObjectKey{Namespace: runtimeconfig.RegistryNamespace, Name: "registry"}, deploy) + err := a.kcli.Get(ctx, client.ObjectKey{Namespace: constants.RegistryNamespace, Name: "registry"}, deploy) if err != nil { return fmt.Errorf("get registry deployment: %w", err) } @@ -176,9 +189,8 @@ func (a *AddOns) migrateRegistryData(ctx context.Context, cfgspec *ecv1beta1.Con if err != nil { return errors.Wrap(err, "get operator image") } - domains := runtimeconfig.GetDomains(cfgspec) - if domains.ProxyRegistryDomain != "" { - operatorImage = strings.Replace(operatorImage, "proxy.replicated.com", domains.ProxyRegistryDomain, 1) + if a.domains.ProxyRegistryDomain != "" { + operatorImage = strings.Replace(operatorImage, "proxy.replicated.com", a.domains.ProxyRegistryDomain, 1) } // TODO: timeout @@ -195,15 +207,14 @@ func (a *AddOns) migrateRegistryData(ctx context.Context, cfgspec *ecv1beta1.Con } // ensureSeaweedfs ensures that seaweedfs is installed. -func (a *AddOns) ensureSeaweedfs(ctx context.Context, serviceCIDR string, cfgspec *ecv1beta1.ConfigSpec) error { - domains := runtimeconfig.GetDomains(cfgspec) - +func (a *AddOns) ensureSeaweedfs(ctx context.Context, opts EnableHAOptions) error { // TODO (@salah): add support for end user overrides sw := &seaweedfs.SeaweedFS{ - ServiceCIDR: serviceCIDR, + ServiceCIDR: opts.ServiceCIDR, + SeaweedFSDataDir: opts.SeaweedFSDataDir, } - if err := sw.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.rc, domains, a.addOnOverrides(sw, cfgspec, nil)); err != nil { + if err := sw.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.domains, a.addOnOverrides(sw, opts.EmbeddedConfigSpec, opts.EndUserConfigSpec)); err != nil { return errors.Wrap(err, "upgrade seaweedfs") } @@ -212,15 +223,13 @@ func (a *AddOns) ensureSeaweedfs(ctx context.Context, serviceCIDR string, cfgspe // enableRegistryHA enables high availability for the registry and scales the registry deployment // to the desired number of replicas. -func (a *AddOns) enableRegistryHA(ctx context.Context, serviceCIDR string, cfgspec *ecv1beta1.ConfigSpec) error { - domains := runtimeconfig.GetDomains(cfgspec) - +func (a *AddOns) enableRegistryHA(ctx context.Context, opts EnableHAOptions) error { // TODO (@salah): add support for end user overrides r := ®istry.Registry{ - ServiceCIDR: serviceCIDR, + ServiceCIDR: opts.ServiceCIDR, IsHA: true, } - if err := r.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.rc, domains, a.addOnOverrides(r, cfgspec, nil)); err != nil { + if err := r.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.domains, a.addOnOverrides(r, opts.EmbeddedConfigSpec, opts.EndUserConfigSpec)); err != nil { return errors.Wrap(err, "upgrade registry") } @@ -228,22 +237,24 @@ func (a *AddOns) enableRegistryHA(ctx context.Context, serviceCIDR string, cfgsp } // EnableAdminConsoleHA enables high availability for the admin console. -func (a *AddOns) EnableAdminConsoleHA(ctx context.Context, isAirgap bool, cfgspec *ecv1beta1.ConfigSpec, licenseInfo *ecv1beta1.LicenseInfo) error { - domains := runtimeconfig.GetDomains(cfgspec) - +func (a *AddOns) EnableAdminConsoleHA(ctx context.Context, opts EnableHAOptions) error { // TODO (@salah): add support for end user overrides ac := &adminconsole.AdminConsole{ - IsAirgap: isAirgap, + IsAirgap: opts.IsAirgap, IsHA: true, - Proxy: a.rc.ProxySpec(), - ServiceCIDR: a.rc.ServiceCIDR(), - IsMultiNodeEnabled: licenseInfo != nil && licenseInfo.IsMultiNodeEnabled, + Proxy: opts.ProxySpec, + ServiceCIDR: opts.ServiceCIDR, + IsMultiNodeEnabled: opts.IsMultiNodeEnabled, + HostCABundlePath: opts.HostCABundlePath, + DataDir: opts.DataDir, + K0sDataDir: opts.K0sDataDir, + AdminConsolePort: opts.AdminConsolePort, } - if err := ac.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.rc, domains, a.addOnOverrides(ac, cfgspec, nil)); err != nil { + if err := ac.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.domains, a.addOnOverrides(ac, opts.EmbeddedConfigSpec, opts.EndUserConfigSpec)); err != nil { return errors.Wrap(err, "upgrade admin console") } - if err := kubeutils.WaitForStatefulset(ctx, a.kcli, runtimeconfig.KotsadmNamespace, "kotsadm-rqlite", nil); err != nil { + if err := kubeutils.WaitForStatefulset(ctx, a.kcli, constants.KotsadmNamespace, "kotsadm-rqlite", nil); err != nil { return errors.Wrap(err, "wait for rqlite to be ready") } diff --git a/pkg/addons/highavailability_test.go b/pkg/addons/highavailability_test.go index f522c6b0d..4d05f9499 100644 --- a/pkg/addons/highavailability_test.go +++ b/pkg/addons/highavailability_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/constants" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v12 "k8s.io/api/core/v1" diff --git a/pkg/addons/install.go b/pkg/addons/install.go index dac066c11..4892f3a4a 100644 --- a/pkg/addons/install.go +++ b/pkg/addons/install.go @@ -12,12 +12,12 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" ) type InstallOptions struct { AdminConsolePwd string + AdminConsolePort int License *kotsv1beta1.License IsAirgap bool TLSCertBytes []byte @@ -28,23 +28,52 @@ type InstallOptions struct { EmbeddedConfigSpec *ecv1beta1.ConfigSpec EndUserConfigSpec *ecv1beta1.ConfigSpec KotsInstaller adminconsole.KotsInstaller - IsRestore bool + ProxySpec *ecv1beta1.ProxySpec + HostCABundlePath string + DataDir string + K0sDataDir string + OpenEBSDataDir string + ServiceCIDR string } func (a *AddOns) Install(ctx context.Context, opts InstallOptions) error { - addons := GetAddOnsForInstall(a.rc, opts) - if opts.IsRestore { - addons = GetAddOnsForRestore(a.rc, opts) + addons := GetAddOnsForInstall(opts) + + for _, addon := range addons { + a.sendProgress(addon.Name(), apitypes.StateRunning, "Installing") + + overrides := a.addOnOverrides(addon, opts.EmbeddedConfigSpec, opts.EndUserConfigSpec) + + if err := addon.Install(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.domains, overrides); err != nil { + a.sendProgress(addon.Name(), apitypes.StateFailed, err.Error()) + return errors.Wrapf(err, "install %s", addon.Name()) + } + + a.sendProgress(addon.Name(), apitypes.StateSucceeded, "Installed") } - domains := runtimeconfig.GetDomains(opts.EmbeddedConfigSpec) + return nil +} + +type RestoreOptions struct { + EmbeddedConfigSpec *ecv1beta1.ConfigSpec + EndUserConfigSpec *ecv1beta1.ConfigSpec + ProxySpec *ecv1beta1.ProxySpec + HostCABundlePath string + DataDir string + OpenEBSDataDir string + K0sDataDir string +} + +func (a *AddOns) Restore(ctx context.Context, opts RestoreOptions) error { + addons := GetAddOnsForRestore(opts) for _, addon := range addons { a.sendProgress(addon.Name(), apitypes.StateRunning, "Installing") overrides := a.addOnOverrides(addon, opts.EmbeddedConfigSpec, opts.EndUserConfigSpec) - if err := addon.Install(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.rc, domains, overrides); err != nil { + if err := addon.Install(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.domains, overrides); err != nil { a.sendProgress(addon.Name(), apitypes.StateFailed, err.Error()) return errors.Wrapf(err, "install %s", addon.Name()) } @@ -55,48 +84,64 @@ func (a *AddOns) Install(ctx context.Context, opts InstallOptions) error { return nil } -func GetAddOnsForInstall(rc runtimeconfig.RuntimeConfig, opts InstallOptions) []types.AddOn { +func GetAddOnsForInstall(opts InstallOptions) []types.AddOn { addOns := []types.AddOn{ - &openebs.OpenEBS{}, + &openebs.OpenEBS{ + OpenEBSDataDir: opts.OpenEBSDataDir, + }, &embeddedclusteroperator.EmbeddedClusterOperator{ - IsAirgap: opts.IsAirgap, - Proxy: rc.ProxySpec(), + IsAirgap: opts.IsAirgap, + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, }, } if opts.IsAirgap { addOns = append(addOns, ®istry.Registry{ - ServiceCIDR: rc.ServiceCIDR(), + ServiceCIDR: opts.ServiceCIDR, + IsHA: false, }) } if opts.DisasterRecoveryEnabled { addOns = append(addOns, &velero.Velero{ - Proxy: rc.ProxySpec(), + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, + K0sDataDir: opts.K0sDataDir, }) } adminConsoleAddOn := &adminconsole.AdminConsole{ IsAirgap: opts.IsAirgap, - Proxy: rc.ProxySpec(), - ServiceCIDR: rc.ServiceCIDR(), - Password: opts.AdminConsolePwd, - TLSCertBytes: opts.TLSCertBytes, - TLSKeyBytes: opts.TLSKeyBytes, - Hostname: opts.Hostname, - KotsInstaller: opts.KotsInstaller, + IsHA: false, + Proxy: opts.ProxySpec, + ServiceCIDR: opts.ServiceCIDR, IsMultiNodeEnabled: opts.IsMultiNodeEnabled, + HostCABundlePath: opts.HostCABundlePath, + DataDir: opts.DataDir, + K0sDataDir: opts.K0sDataDir, + AdminConsolePort: opts.AdminConsolePort, + + Password: opts.AdminConsolePwd, + TLSCertBytes: opts.TLSCertBytes, + TLSKeyBytes: opts.TLSKeyBytes, + Hostname: opts.Hostname, + KotsInstaller: opts.KotsInstaller, } addOns = append(addOns, adminConsoleAddOn) return addOns } -func GetAddOnsForRestore(rc runtimeconfig.RuntimeConfig, opts InstallOptions) []types.AddOn { +func GetAddOnsForRestore(opts RestoreOptions) []types.AddOn { addOns := []types.AddOn{ - &openebs.OpenEBS{}, + &openebs.OpenEBS{ + OpenEBSDataDir: opts.OpenEBSDataDir, + }, &velero.Velero{ - Proxy: rc.ProxySpec(), + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, + K0sDataDir: opts.K0sDataDir, }, } return addOns diff --git a/pkg/addons/install_test.go b/pkg/addons/install_test.go index f68e468df..d451c8617 100644 --- a/pkg/addons/install_test.go +++ b/pkg/addons/install_test.go @@ -10,7 +10,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,7 +17,6 @@ import ( func Test_getAddOnsForInstall(t *testing.T) { tests := []struct { name string - rc runtimeconfig.RuntimeConfig opts InstallOptions before func() verify func(t *testing.T, addons []types.AddOn) @@ -26,7 +24,6 @@ func Test_getAddOnsForInstall(t *testing.T) { }{ { name: "online installation", - rc: runtimeconfig.New(nil), opts: InstallOptions{ IsAirgap: false, DisasterRecoveryEnabled: false, @@ -59,15 +56,11 @@ func Test_getAddOnsForInstall(t *testing.T) { }, { name: "airgap installation", - rc: func() runtimeconfig.RuntimeConfig { - rc := runtimeconfig.New(nil) - rc.SetNetworkSpec(ecv1beta1.NetworkSpec{ServiceCIDR: "10.96.0.0/12"}) - return rc - }(), opts: InstallOptions{ IsAirgap: true, DisasterRecoveryEnabled: false, AdminConsolePwd: "password123", + ServiceCIDR: "10.96.0.0/12", }, verify: func(t *testing.T, addons []types.AddOn) { assert.Len(t, addons, 4) @@ -100,15 +93,11 @@ func Test_getAddOnsForInstall(t *testing.T) { }, { name: "disaster recovery enabled", - rc: func() runtimeconfig.RuntimeConfig { - rc := runtimeconfig.New(nil) - rc.SetNetworkSpec(ecv1beta1.NetworkSpec{ServiceCIDR: "10.96.0.0/12"}) - return rc - }(), opts: InstallOptions{ IsAirgap: false, DisasterRecoveryEnabled: true, AdminConsolePwd: "password123", + ServiceCIDR: "10.96.0.0/12", }, verify: func(t *testing.T, addons []types.AddOn) { assert.Len(t, addons, 4) @@ -141,20 +130,16 @@ func Test_getAddOnsForInstall(t *testing.T) { }, { name: "airgap with disaster recovery and proxy", - rc: func() runtimeconfig.RuntimeConfig { - rc := runtimeconfig.New(nil) - rc.SetNetworkSpec(ecv1beta1.NetworkSpec{ServiceCIDR: "10.96.0.0/12"}) - rc.SetProxySpec(&ecv1beta1.ProxySpec{ - HTTPProxy: "http://proxy.example.com", - HTTPSProxy: "https://proxy.example.com", - NoProxy: "localhost,127.0.0.1", - }) - return rc - }(), opts: InstallOptions{ IsAirgap: true, DisasterRecoveryEnabled: true, AdminConsolePwd: "password123", + ServiceCIDR: "10.96.0.0/12", + ProxySpec: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://proxy.example.com", + HTTPSProxy: "https://proxy.example.com", + NoProxy: "localhost,127.0.0.1", + }, }, verify: func(t *testing.T, addons []types.AddOn) { assert.Len(t, addons, 5) @@ -200,12 +185,10 @@ func Test_getAddOnsForInstall(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rc := tt.rc - rc.SetDataDir(t.TempDir()) if tt.before != nil { tt.before() } - tt.verify(t, GetAddOnsForInstall(rc, tt.opts)) + tt.verify(t, GetAddOnsForInstall(tt.opts)) if tt.after != nil { tt.after() } diff --git a/pkg/addons/interface.go b/pkg/addons/interface.go index 3b90763fe..170f5283d 100644 --- a/pkg/addons/interface.go +++ b/pkg/addons/interface.go @@ -7,7 +7,6 @@ import ( ectypes "github.com/replicatedhq/embedded-cluster/kinds/types" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "github.com/sirupsen/logrus" "k8s.io/client-go/kubernetes" @@ -19,13 +18,13 @@ type AddOnsInterface interface { // Install installs all addons Install(ctx context.Context, opts InstallOptions) error // Upgrade upgrades all addons - Upgrade(ctx context.Context, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) error + Upgrade(ctx context.Context, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata, opts UpgradeOptions) error // CanEnableHA checks if high availability can be enabled in the cluster CanEnableHA(context.Context) (bool, string, error) // EnableHA enables high availability for the cluster - EnableHA(ctx context.Context, inSpec ecv1beta1.InstallationSpec, spinner *spinner.MessageWriter) error + EnableHA(ctx context.Context, opts EnableHAOptions, spinner *spinner.MessageWriter) error // EnableAdminConsoleHA enables high availability for the admin console - EnableAdminConsoleHA(ctx context.Context, isAirgap bool, cfgspec *ecv1beta1.ConfigSpec, licenseInfo *ecv1beta1.LicenseInfo) error + EnableAdminConsoleHA(ctx context.Context, opts EnableHAOptions) error } var _ AddOnsInterface = (*AddOns)(nil) @@ -36,7 +35,7 @@ type AddOns struct { kcli client.Client mcli metadata.Interface kclient kubernetes.Interface - rc runtimeconfig.RuntimeConfig + domains ecv1beta1.Domains progress chan<- types.AddOnProgress } @@ -72,9 +71,9 @@ func WithKubernetesClientSet(kclient kubernetes.Interface) AddOnsOption { } } -func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) AddOnsOption { +func WithDomains(domains ecv1beta1.Domains) AddOnsOption { return func(a *AddOns) { - a.rc = rc + a.domains = domains } } @@ -94,9 +93,5 @@ func New(opts ...AddOnsOption) *AddOns { a.logf = logrus.Debugf } - if a.rc == nil { - a.rc = runtimeconfig.New(nil) - } - return a } diff --git a/pkg/addons/mock.go b/pkg/addons/mock.go index aea4ea546..4eae18d9e 100644 --- a/pkg/addons/mock.go +++ b/pkg/addons/mock.go @@ -23,8 +23,8 @@ func (m *MockAddOns) Install(ctx context.Context, opts InstallOptions) error { } // Upgrade mocks the Upgrade method -func (m *MockAddOns) Upgrade(ctx context.Context, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) error { - args := m.Called(ctx, in, meta) +func (m *MockAddOns) Upgrade(ctx context.Context, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata, opts UpgradeOptions) error { + args := m.Called(ctx, in, meta, opts) return args.Error(0) } @@ -35,13 +35,13 @@ func (m *MockAddOns) CanEnableHA(ctx context.Context) (bool, string, error) { } // EnableHA mocks the EnableHA method -func (m *MockAddOns) EnableHA(ctx context.Context, inSpec ecv1beta1.InstallationSpec, spinner *spinner.MessageWriter) error { - args := m.Called(ctx, inSpec, spinner) +func (m *MockAddOns) EnableHA(ctx context.Context, opts EnableHAOptions, spinner *spinner.MessageWriter) error { + args := m.Called(ctx, opts, spinner) return args.Error(0) } // EnableAdminConsoleHA mocks the EnableAdminConsoleHA method -func (m *MockAddOns) EnableAdminConsoleHA(ctx context.Context, isAirgap bool, cfgspec *ecv1beta1.ConfigSpec, licenseInfo *ecv1beta1.LicenseInfo) error { - args := m.Called(ctx, isAirgap, cfgspec, licenseInfo) +func (m *MockAddOns) EnableAdminConsoleHA(ctx context.Context, opts EnableHAOptions) error { + args := m.Called(ctx, opts) return args.Error(0) } diff --git a/pkg/addons/openebs/install.go b/pkg/addons/openebs/install.go index f4801b1e4..d5752ba94 100644 --- a/pkg/addons/openebs/install.go +++ b/pkg/addons/openebs/install.go @@ -7,7 +7,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -15,10 +14,9 @@ import ( func (o *OpenEBS) Install( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, - overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { - values, err := o.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := o.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/openebs/openebs.go b/pkg/addons/openebs/openebs.go index 6723caebc..3c0c353f6 100644 --- a/pkg/addons/openebs/openebs.go +++ b/pkg/addons/openebs/openebs.go @@ -15,6 +15,7 @@ const ( var _ types.AddOn = (*OpenEBS)(nil) type OpenEBS struct { + OpenEBSDataDir string } func (o *OpenEBS) Name() string { diff --git a/pkg/addons/openebs/upgrade.go b/pkg/addons/openebs/upgrade.go index 99a4cea34..d891b95da 100644 --- a/pkg/addons/openebs/upgrade.go +++ b/pkg/addons/openebs/upgrade.go @@ -7,7 +7,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" @@ -16,7 +15,7 @@ import ( func (o *OpenEBS) Upgrade( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { exists, err := hcli.ReleaseExists(ctx, o.Namespace(), o.ReleaseName()) if err != nil { @@ -24,13 +23,13 @@ func (o *OpenEBS) Upgrade( } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", o.ReleaseName(), o.Namespace()) - if err := o.Install(ctx, logf, kcli, mcli, hcli, rc, domains, overrides); err != nil { + if err := o.Install(ctx, logf, kcli, mcli, hcli, domains, overrides); err != nil { return errors.Wrap(err, "install") } return nil } - values, err := o.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := o.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/openebs/values.go b/pkg/addons/openebs/values.go index d3e4e0bf6..f359700ae 100644 --- a/pkg/addons/openebs/values.go +++ b/pkg/addons/openebs/values.go @@ -9,7 +9,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "gopkg.in/yaml.v3" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -33,7 +32,7 @@ func init() { helmValues = hv } -func (o *OpenEBS) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { +func (o *OpenEBS) GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { // create a copy of the helm values so we don't modify the original marshalled, err := helm.MarshalValues(helmValues) if err != nil { @@ -50,7 +49,7 @@ func (o *OpenEBS) GenerateHelmValues(ctx context.Context, kcli client.Client, rc return nil, errors.Wrap(err, "unmarshal helm values") } - err = helm.SetValue(copiedValues, "localpv-provisioner.localpv.basePath", rc.EmbeddedClusterOpenEBSLocalSubDir()) + err = helm.SetValue(copiedValues, "localpv-provisioner.localpv.basePath", o.OpenEBSDataDir) if err != nil { return nil, errors.Wrap(err, "set localpv-provisioner.localpv.basePath") } diff --git a/pkg/addons/registry/install.go b/pkg/addons/registry/install.go index cb587c994..d27030517 100644 --- a/pkg/addons/registry/install.go +++ b/pkg/addons/registry/install.go @@ -10,7 +10,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/certs" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "golang.org/x/crypto/bcrypt" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,7 +20,7 @@ import ( func (r *Registry) Install( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, + domains ecv1beta1.Domains, overrides []string, ) error { registryIP, err := GetRegistryClusterIP(r.ServiceCIDR) @@ -33,7 +32,7 @@ func (r *Registry) Install( return errors.Wrap(err, "create prerequisites") } - values, err := r.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := r.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/registry/migrate/pod.go b/pkg/addons/registry/migrate/pod.go index 37694f3da..b44dc243b 100644 --- a/pkg/addons/registry/migrate/pod.go +++ b/pkg/addons/registry/migrate/pod.go @@ -10,9 +10,9 @@ import ( "time" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/addons/seaweedfs" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -92,7 +92,7 @@ func ensureDataMigrationPod(ctx context.Context, cli client.Client, image string func maybeDeleteDataMigrationPod(ctx context.Context, cli client.Client) error { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, Name: dataMigrationPodName, }, } @@ -105,7 +105,7 @@ func maybeDeleteDataMigrationPod(ctx context.Context, cli client.Client) error { return nil } - err = kubeutils.WaitForPodDeleted(ctx, cli, runtimeconfig.RegistryNamespace, dataMigrationPodName, &kubeutils.WaitOptions{ + err = kubeutils.WaitForPodDeleted(ctx, cli, constants.RegistryNamespace, dataMigrationPodName, &kubeutils.WaitOptions{ Backoff: &wait.Backoff{ Steps: 30, Duration: 2 * time.Second, @@ -132,7 +132,7 @@ func monitorPodStatus(ctx context.Context, cli client.Client, errCh chan<- error Jitter: 0.1, }, } - pod, err := kubeutils.WaitForPodComplete(ctx, cli, runtimeconfig.RegistryNamespace, dataMigrationPodName, opts) + pod, err := kubeutils.WaitForPodComplete(ctx, cli, constants.RegistryNamespace, dataMigrationPodName, opts) if err != nil { errCh <- err } @@ -178,7 +178,7 @@ func streamPodLogs(ctx context.Context, kclient kubernetes.Interface) (io.ReadCl Follow: true, TailLines: ptr.To(int64(100)), } - podLogs, err := kclient.CoreV1().Pods(runtimeconfig.RegistryNamespace).GetLogs(dataMigrationPodName, &logOpts).Stream(ctx) + podLogs, err := kclient.CoreV1().Pods(constants.RegistryNamespace).GetLogs(dataMigrationPodName, &logOpts).Stream(ctx) if err != nil { return nil, fmt.Errorf("get logs: %w", err) } @@ -225,7 +225,7 @@ func newMigrationPod(image string) *corev1.Pod { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: dataMigrationPodName, - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", @@ -279,7 +279,7 @@ func ensureMigrationServiceAccount(ctx context.Context, cli client.Client) error newRole := rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ Name: "registry-data-migration-role", - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, }, TypeMeta: metav1.TypeMeta{ Kind: "Role", @@ -306,7 +306,7 @@ func ensureMigrationServiceAccount(ctx context.Context, cli client.Client) error newServiceAccount := corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: serviceAccountName, - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, }, TypeMeta: metav1.TypeMeta{ Kind: "ServiceAccount", @@ -321,7 +321,7 @@ func ensureMigrationServiceAccount(ctx context.Context, cli client.Client) error newRoleBinding := rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: "registry-data-migration-rolebinding", - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, }, TypeMeta: metav1.TypeMeta{ Kind: "RoleBinding", @@ -336,7 +336,7 @@ func ensureMigrationServiceAccount(ctx context.Context, cli client.Client) error { Kind: "ServiceAccount", Name: serviceAccountName, - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, }, }, } @@ -357,7 +357,7 @@ func ensureS3Secret(ctx context.Context, kcli client.Client) error { var secret corev1.Secret err = kcli.Get(ctx, client.ObjectKey{ - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, Name: seaweedfsS3SecretName, }, &secret) if client.IgnoreNotFound(err) != nil { @@ -377,7 +377,7 @@ func ensureS3Secret(ctx context.Context, kcli client.Client) error { secret = corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.RegistryNamespace, + Namespace: constants.RegistryNamespace, Name: seaweedfsS3SecretName, Labels: seaweedfs.ApplyLabels(secret.ObjectMeta.Labels, "s3"), }, diff --git a/pkg/addons/registry/registry.go b/pkg/addons/registry/registry.go index bce79d286..91b752e39 100644 --- a/pkg/addons/registry/registry.go +++ b/pkg/addons/registry/registry.go @@ -7,9 +7,9 @@ import ( "github.com/pkg/errors" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helpers" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" appsv1 "k8s.io/api/apps/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -23,7 +23,7 @@ type Registry struct { const ( _releaseName = "docker-registry" - _namespace = runtimeconfig.RegistryNamespace + _namespace = constants.RegistryNamespace _tlsSecretName = "registry-tls" _lowerBandIPIndex = 10 diff --git a/pkg/addons/registry/upgrade.go b/pkg/addons/registry/upgrade.go index 74db53e56..b8a6a6081 100644 --- a/pkg/addons/registry/upgrade.go +++ b/pkg/addons/registry/upgrade.go @@ -8,7 +8,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/seaweedfs" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -27,7 +26,7 @@ const ( func (r *Registry) Upgrade( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { exists, err := hcli.ReleaseExists(ctx, r.Namespace(), r.ReleaseName()) if err != nil { @@ -35,7 +34,7 @@ func (r *Registry) Upgrade( } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", r.ReleaseName(), r.Namespace()) - if err := r.Install(ctx, logf, kcli, mcli, hcli, rc, domains, overrides); err != nil { + if err := r.Install(ctx, logf, kcli, mcli, hcli, domains, overrides); err != nil { return errors.Wrap(err, "install") } return nil @@ -45,7 +44,7 @@ func (r *Registry) Upgrade( return errors.Wrap(err, "create prerequisites") } - values, err := r.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := r.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/registry/values.go b/pkg/addons/registry/values.go index 62b3f5a4a..b6edc85cc 100644 --- a/pkg/addons/registry/values.go +++ b/pkg/addons/registry/values.go @@ -10,7 +10,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/seaweedfs" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -46,7 +45,7 @@ func init() { helmValuesHA = hvHA } -func (r *Registry) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { +func (r *Registry) GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { var values map[string]interface{} if r.IsHA { values = helmValuesHA diff --git a/pkg/addons/seaweedfs/install.go b/pkg/addons/seaweedfs/install.go index 92e854d51..892fb68bf 100644 --- a/pkg/addons/seaweedfs/install.go +++ b/pkg/addons/seaweedfs/install.go @@ -10,7 +10,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,14 +21,13 @@ import ( func (s *SeaweedFS) Install( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, - overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { if err := s.ensurePreRequisites(ctx, kcli); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := s.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := s.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/seaweedfs/seaweedfs.go b/pkg/addons/seaweedfs/seaweedfs.go index 61da07006..f0a288cc1 100644 --- a/pkg/addons/seaweedfs/seaweedfs.go +++ b/pkg/addons/seaweedfs/seaweedfs.go @@ -4,13 +4,13 @@ import ( "strings" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) const ( _releaseName = "seaweedfs" - _namespace = runtimeconfig.SeaweedFSNamespace + _namespace = constants.SeaweedFSNamespace // _s3SVCName is the name of the Seaweedfs S3 service managed by the operator. // HACK: This service has a hardcoded service IP shared by the cli and operator as it is used @@ -28,7 +28,8 @@ const ( var _ types.AddOn = (*SeaweedFS)(nil) type SeaweedFS struct { - ServiceCIDR string + ServiceCIDR string + SeaweedFSDataDir string } func (s *SeaweedFS) Name() string { diff --git a/pkg/addons/seaweedfs/upgrade.go b/pkg/addons/seaweedfs/upgrade.go index ab4221a2f..52e4c9b68 100644 --- a/pkg/addons/seaweedfs/upgrade.go +++ b/pkg/addons/seaweedfs/upgrade.go @@ -7,7 +7,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" @@ -16,7 +15,7 @@ import ( func (s *SeaweedFS) Upgrade( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { exists, err := hcli.ReleaseExists(ctx, s.Namespace(), s.ReleaseName()) if err != nil { @@ -24,14 +23,14 @@ func (s *SeaweedFS) Upgrade( } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", s.ReleaseName(), s.Namespace()) - return s.Install(ctx, logf, kcli, mcli, hcli, rc, domains, overrides) + return s.Install(ctx, logf, kcli, mcli, hcli, domains, overrides) } if err := s.ensurePreRequisites(ctx, kcli); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := s.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := s.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/seaweedfs/values.go b/pkg/addons/seaweedfs/values.go index 21b7b6118..a687f1832 100644 --- a/pkg/addons/seaweedfs/values.go +++ b/pkg/addons/seaweedfs/values.go @@ -10,7 +10,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "gopkg.in/yaml.v3" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -34,7 +33,7 @@ func init() { helmValues = hv } -func (s *SeaweedFS) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { +func (s *SeaweedFS) GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { // create a copy of the helm values so we don't modify the original marshalled, err := helm.MarshalValues(helmValues) if err != nil { @@ -51,13 +50,13 @@ func (s *SeaweedFS) GenerateHelmValues(ctx context.Context, kcli client.Client, return nil, errors.Wrap(err, "unmarshal helm values") } - dataPath := filepath.Join(rc.EmbeddedClusterSeaweedfsSubDir(), "ssd") + dataPath := filepath.Join(s.SeaweedFSDataDir, "ssd") err = helm.SetValue(copiedValues, "master.data.hostPathPrefix", dataPath) if err != nil { return nil, errors.Wrap(err, "set helm values global.data.hostPathPrefix") } - logsPath := filepath.Join(rc.EmbeddedClusterSeaweedfsSubDir(), "storage") + logsPath := filepath.Join(s.SeaweedFSDataDir, "storage") err = helm.SetValue(copiedValues, "master.logs.hostPathPrefix", logsPath) if err != nil { return nil, errors.Wrap(err, "set helm values global.logs.hostPathPrefix") diff --git a/pkg/addons/types/types.go b/pkg/addons/types/types.go index a6990353e..baa5ee1f8 100644 --- a/pkg/addons/types/types.go +++ b/pkg/addons/types/types.go @@ -6,7 +6,6 @@ import ( apitypes "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -18,9 +17,9 @@ type AddOn interface { Version() string ReleaseName() string Namespace() string - GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) - Install(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) error - Upgrade(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) error + GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) + Install(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, domains ecv1beta1.Domains, overrides []string) error + Upgrade(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, domains ecv1beta1.Domains, overrides []string) error } type AddOnProgress struct { diff --git a/pkg/addons/upgrade.go b/pkg/addons/upgrade.go index a9f483030..9ffe74aa4 100644 --- a/pkg/addons/upgrade.go +++ b/pkg/addons/upgrade.go @@ -17,20 +17,34 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (a *AddOns) Upgrade(ctx context.Context, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) error { - domains := runtimeconfig.GetDomains(in.Spec.Config) +type UpgradeOptions struct { + AdminConsolePort int + IsAirgap bool + IsHA bool + DisasterRecoveryEnabled bool + IsMultiNodeEnabled bool + EmbeddedConfigSpec *ecv1beta1.ConfigSpec + EndUserConfigSpec *ecv1beta1.ConfigSpec + ProxySpec *ecv1beta1.ProxySpec + HostCABundlePath string + DataDir string + K0sDataDir string + OpenEBSDataDir string + SeaweedFSDataDir string + ServiceCIDR string +} - addons, err := a.getAddOnsForUpgrade(domains, in, meta) +func (a *AddOns) Upgrade(ctx context.Context, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata, opts UpgradeOptions) error { + addons, err := a.getAddOnsForUpgrade(meta, opts) if err != nil { return errors.Wrap(err, "get addons for upgrade") } for _, addon := range addons { - if err := a.upgradeAddOn(ctx, domains, in, addon); err != nil { + if err := a.upgradeAddOn(ctx, in, addon); err != nil { return errors.Wrapf(err, "addon %s", addon.Name()) } } @@ -38,13 +52,13 @@ func (a *AddOns) Upgrade(ctx context.Context, in *ecv1beta1.Installation, meta * return nil } -func (a *AddOns) getAddOnsForUpgrade(domains ecv1beta1.Domains, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) ([]types.AddOn, error) { +func (a *AddOns) getAddOnsForUpgrade(meta *ectypes.ReleaseMetadata, opts UpgradeOptions) ([]types.AddOn, error) { addOns := []types.AddOn{ - &openebs.OpenEBS{}, + &openebs.OpenEBS{ + OpenEBSDataDir: opts.OpenEBSDataDir, + }, } - serviceCIDR := a.rc.ServiceCIDR() - // ECO's embedded (wrong) metadata values do not match the published (correct) metadata values. // This is because we re-generate the metadata.yaml file _after_ building the ECO binary / image. // We do that because the SHA of the image needs to be included in the metadata.yaml file. @@ -53,13 +67,15 @@ func (a *AddOns) getAddOnsForUpgrade(domains ecv1beta1.Domains, in *ecv1beta1.In if err != nil { return nil, errors.Wrap(err, "get operator chart location") } - ecoImageRepo, ecoImageTag, ecoUtilsImage, err := a.operatorImages(meta.Images, domains.ProxyRegistryDomain) + ecoImageRepo, ecoImageTag, ecoUtilsImage, err := a.operatorImages(meta.Images) if err != nil { return nil, errors.Wrap(err, "get operator images") } addOns = append(addOns, &embeddedclusteroperator.EmbeddedClusterOperator{ - IsAirgap: in.Spec.AirGap, - Proxy: a.rc.ProxySpec(), + IsAirgap: opts.IsAirgap, + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, + ChartLocationOverride: ecoChartLocation, ChartVersionOverride: ecoChartVersion, ImageRepoOverride: ecoImageRepo, @@ -67,37 +83,44 @@ func (a *AddOns) getAddOnsForUpgrade(domains ecv1beta1.Domains, in *ecv1beta1.In UtilsImageOverride: ecoUtilsImage, }) - if in.Spec.AirGap { + if opts.IsAirgap { addOns = append(addOns, ®istry.Registry{ - ServiceCIDR: serviceCIDR, - IsHA: in.Spec.HighAvailability, + ServiceCIDR: opts.ServiceCIDR, + IsHA: opts.IsHA, }) - if in.Spec.HighAvailability { + if opts.IsHA { addOns = append(addOns, &seaweedfs.SeaweedFS{ - ServiceCIDR: serviceCIDR, + ServiceCIDR: opts.ServiceCIDR, + SeaweedFSDataDir: opts.SeaweedFSDataDir, }) } } - if in.Spec.LicenseInfo != nil && in.Spec.LicenseInfo.IsDisasterRecoverySupported { + if opts.DisasterRecoveryEnabled { addOns = append(addOns, &velero.Velero{ - Proxy: a.rc.ProxySpec(), + Proxy: opts.ProxySpec, + HostCABundlePath: opts.HostCABundlePath, + K0sDataDir: opts.K0sDataDir, }) } addOns = append(addOns, &adminconsole.AdminConsole{ - IsAirgap: in.Spec.AirGap, - IsHA: in.Spec.HighAvailability, - Proxy: a.rc.ProxySpec(), - ServiceCIDR: serviceCIDR, - IsMultiNodeEnabled: in.Spec.LicenseInfo != nil && in.Spec.LicenseInfo.IsMultiNodeEnabled, + IsAirgap: opts.IsAirgap, + IsHA: opts.IsHA, + Proxy: opts.ProxySpec, + ServiceCIDR: opts.ServiceCIDR, + IsMultiNodeEnabled: opts.IsMultiNodeEnabled, + HostCABundlePath: opts.HostCABundlePath, + DataDir: opts.DataDir, + K0sDataDir: opts.K0sDataDir, + AdminConsolePort: opts.AdminConsolePort, }) return addOns, nil } -func (a *AddOns) upgradeAddOn(ctx context.Context, domains ecv1beta1.Domains, in *ecv1beta1.Installation, addon types.AddOn) error { +func (a *AddOns) upgradeAddOn(ctx context.Context, in *ecv1beta1.Installation, addon types.AddOn) error { // check if we already processed this addon if kubeutils.CheckInstallationConditionStatus(in.Status, a.conditionName(addon)) == metav1.ConditionTrue { slog.Info(addon.Name() + " is ready") @@ -114,7 +137,7 @@ func (a *AddOns) upgradeAddOn(ctx context.Context, domains ecv1beta1.Domains, in // TODO (@salah): add support for end user overrides overrides := a.addOnOverrides(addon, in.Spec.Config, nil) - err := addon.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.rc, domains, overrides) + err := addon.Upgrade(ctx, a.logf, a.kcli, a.mcli, a.hcli, a.domains, overrides) if err != nil { message := helpers.CleanErrorMessage(err) if err := a.setCondition(ctx, in, a.conditionName(addon), metav1.ConditionFalse, "UpgradeFailed", message); err != nil { diff --git a/pkg/addons/upgrade_test.go b/pkg/addons/upgrade_test.go index 27d0a57bb..529666ec5 100644 --- a/pkg/addons/upgrade_test.go +++ b/pkg/addons/upgrade_test.go @@ -12,7 +12,6 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/seaweedfs" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,20 +36,17 @@ func Test_getAddOnsForUpgrade(t *testing.T) { tests := []struct { name string domains ecv1beta1.Domains - in *ecv1beta1.Installation meta *ectypes.ReleaseMetadata + opts UpgradeOptions verify func(t *testing.T, addons []types.AddOn, err error) }{ { name: "online installation", - in: &ecv1beta1.Installation{ - Spec: ecv1beta1.InstallationSpec{ - AirGap: false, - HighAvailability: false, - BinaryName: "test-binary-name", - }, - }, meta: meta, + opts: UpgradeOptions{ + IsAirgap: false, + IsHA: false, + }, verify: func(t *testing.T, addons []types.AddOn, err error) { assert.NoError(t, err) assert.Len(t, addons, 3) @@ -78,19 +74,12 @@ func Test_getAddOnsForUpgrade(t *testing.T) { }, { name: "airgap installation", - in: &ecv1beta1.Installation{ - Spec: ecv1beta1.InstallationSpec{ - AirGap: true, - HighAvailability: false, - BinaryName: "test-binary-name", - RuntimeConfig: &ecv1beta1.RuntimeConfigSpec{ - Network: ecv1beta1.NetworkSpec{ - ServiceCIDR: "10.96.0.0/12", - }, - }, - }, - }, meta: meta, + opts: UpgradeOptions{ + ServiceCIDR: "10.96.0.0/12", + IsAirgap: true, + IsHA: false, + }, verify: func(t *testing.T, addons []types.AddOn, err error) { assert.NoError(t, err) assert.Len(t, addons, 4) @@ -123,22 +112,13 @@ func Test_getAddOnsForUpgrade(t *testing.T) { }, { name: "with disaster recovery", - in: &ecv1beta1.Installation{ - Spec: ecv1beta1.InstallationSpec{ - AirGap: false, - HighAvailability: false, - LicenseInfo: &ecv1beta1.LicenseInfo{ - IsDisasterRecoverySupported: true, - }, - BinaryName: "test-binary-name", - RuntimeConfig: &ecv1beta1.RuntimeConfigSpec{ - Network: ecv1beta1.NetworkSpec{ - ServiceCIDR: "10.96.0.0/12", - }, - }, - }, - }, meta: meta, + opts: UpgradeOptions{ + ServiceCIDR: "10.96.0.0/12", + IsAirgap: false, + IsHA: false, + DisasterRecoveryEnabled: true, + }, verify: func(t *testing.T, addons []types.AddOn, err error) { assert.NoError(t, err) assert.Len(t, addons, 4) @@ -170,27 +150,18 @@ func Test_getAddOnsForUpgrade(t *testing.T) { }, { name: "airgap HA with proxy and disaster recovery", - in: &ecv1beta1.Installation{ - Spec: ecv1beta1.InstallationSpec{ - AirGap: true, - HighAvailability: true, - LicenseInfo: &ecv1beta1.LicenseInfo{ - IsDisasterRecoverySupported: true, - }, - BinaryName: "test-binary-name", - RuntimeConfig: &ecv1beta1.RuntimeConfigSpec{ - Network: ecv1beta1.NetworkSpec{ - ServiceCIDR: "10.96.0.0/12", - }, - Proxy: &ecv1beta1.ProxySpec{ - HTTPProxy: "http://proxy.example.com", - HTTPSProxy: "https://proxy.example.com", - NoProxy: "localhost,127.0.0.1", - }, - }, + meta: meta, + opts: UpgradeOptions{ + ServiceCIDR: "10.96.0.0/12", + ProxySpec: &ecv1beta1.ProxySpec{ + HTTPProxy: "http://proxy.example.com", + HTTPSProxy: "https://proxy.example.com", + NoProxy: "localhost,127.0.0.1", }, + IsAirgap: true, + IsHA: true, + DisasterRecoveryEnabled: true, }, - meta: meta, verify: func(t *testing.T, addons []types.AddOn, err error) { assert.NoError(t, err) assert.Len(t, addons, 6) @@ -237,15 +208,13 @@ func Test_getAddOnsForUpgrade(t *testing.T) { }, { name: "invalid metadata - missing chart", - in: &ecv1beta1.Installation{ - Spec: ecv1beta1.InstallationSpec{}, - }, meta: &ectypes.ReleaseMetadata{ Configs: ecv1beta1.Helm{ Charts: []ecv1beta1.Chart{}, }, Images: meta.Images, }, + opts: UpgradeOptions{}, verify: func(t *testing.T, addons []types.AddOn, err error) { assert.Error(t, err) assert.Contains(t, err.Error(), "no embedded-cluster-operator chart found") @@ -253,13 +222,11 @@ func Test_getAddOnsForUpgrade(t *testing.T) { }, { name: "invalid metadata - missing images", - in: &ecv1beta1.Installation{ - Spec: ecv1beta1.InstallationSpec{}, - }, meta: &ectypes.ReleaseMetadata{ Configs: meta.Configs, Images: []string{}, }, + opts: UpgradeOptions{}, verify: func(t *testing.T, addons []types.AddOn, err error) { assert.Error(t, err) assert.Contains(t, err.Error(), "no embedded-cluster-operator-image found") @@ -269,9 +236,8 @@ func Test_getAddOnsForUpgrade(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rc := runtimeconfig.New(tt.in.Spec.RuntimeConfig) - addOns := New(WithRuntimeConfig(rc)) - addons, err := addOns.getAddOnsForUpgrade(tt.domains, tt.in, tt.meta) + addOns := New() + addons, err := addOns.getAddOnsForUpgrade(tt.meta, tt.opts) tt.verify(t, addons, err) }) } diff --git a/pkg/addons/util.go b/pkg/addons/util.go index 04f3d4ac6..dadadd3ea 100644 --- a/pkg/addons/util.go +++ b/pkg/addons/util.go @@ -32,7 +32,7 @@ func (a *AddOns) operatorChart(meta *ectypes.ReleaseMetadata) (string, string, e return "", "", errors.New("no embedded-cluster-operator chart found in release metadata") } -func (a *AddOns) operatorImages(images []string, proxyRegistryDomain string) (string, string, string, error) { +func (a *AddOns) operatorImages(images []string) (string, string, string, error) { // determine the images to use for the operator chart ecOperatorImage := "" ecUtilsImage := "" @@ -54,9 +54,9 @@ func (a *AddOns) operatorImages(images []string, proxyRegistryDomain string) (st } // the override images for operator during upgrades also need to be updated to use a whitelabeled proxy registry - if proxyRegistryDomain != "" { - ecOperatorImage = strings.Replace(ecOperatorImage, "proxy.replicated.com", proxyRegistryDomain, 1) - ecUtilsImage = strings.Replace(ecUtilsImage, "proxy.replicated.com", proxyRegistryDomain, 1) + if a.domains.ProxyRegistryDomain != "" { + ecOperatorImage = strings.Replace(ecOperatorImage, "proxy.replicated.com", a.domains.ProxyRegistryDomain, 1) + ecUtilsImage = strings.Replace(ecUtilsImage, "proxy.replicated.com", a.domains.ProxyRegistryDomain, 1) } repo := strings.Split(ecOperatorImage, ":")[0] diff --git a/pkg/addons/util_test.go b/pkg/addons/util_test.go index 2983a2950..d6f55b453 100644 --- a/pkg/addons/util_test.go +++ b/pkg/addons/util_test.go @@ -3,6 +3,7 @@ package addons import ( "testing" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/stretchr/testify/require" ) @@ -12,13 +13,14 @@ func Test_operatorImages(t *testing.T) { images []string wantRepo string wantTag string - proxyRegistry string + domains ecv1beta1.Domains wantUtilsImage string wantErr string }{ { name: "no images", images: []string{}, + domains: ecv1beta1.Domains{}, wantErr: "no embedded-cluster-operator-image found in images", }, { @@ -26,6 +28,7 @@ func Test_operatorImages(t *testing.T) { images: []string{ "docker.io/replicated/another-image:latest-arm64@sha256:a9ab9db181f9898283a87be0f79d85cb8f3d22a790b71f52c8a9d339e225dedd", }, + domains: ecv1beta1.Domains{}, wantErr: "no embedded-cluster-operator-image found in images", }, { @@ -34,6 +37,7 @@ func Test_operatorImages(t *testing.T) { "docker.io/replicated/another-image:latest-arm64@sha256:a9ab9db181f9898283a87be0f79d85cb8f3d22a790b71f52c8a9d339e225dedd", "docker.io/replicated/embedded-cluster-operator-image:latest-amd64@sha256:eeed01216b5d2192afbd90e2e1f70419a8758551d8708f9d4b4f50f41d106ce8", }, + domains: ecv1beta1.Domains{}, wantErr: "no ec-utils found in images", }, { @@ -42,6 +46,7 @@ func Test_operatorImages(t *testing.T) { "docker.io/replicated/embedded-cluster-operator-image:latest-amd64", "docker.io/replicated/ec-utils:latest-amd64", }, + domains: ecv1beta1.Domains{}, wantRepo: "docker.io/replicated/embedded-cluster-operator-image", wantTag: "latest-amd64", wantUtilsImage: "docker.io/replicated/ec-utils:latest-amd64", @@ -72,6 +77,7 @@ func Test_operatorImages(t *testing.T) { "proxy.replicated.com/anonymous/replicated/embedded-cluster-local-artifact-mirror:v1.14.2-k8s-1.29@sha256:54463ce6b6fba13a25138890aa1ac28ae4f93f53cdb78a99d15abfdc1b5eddf5", "proxy.replicated.com/anonymous/replicated/embedded-cluster-operator-image:v1.14.2-k8s-1.29-amd64@sha256:45a45e2ec6b73d2db029354cccfe7eb150dd7ef9dffe806db36de9b9ba0a66c6", }, + domains: ecv1beta1.Domains{}, wantRepo: "proxy.replicated.com/anonymous/replicated/embedded-cluster-operator-image", wantTag: "v1.14.2-k8s-1.29-amd64@sha256:45a45e2ec6b73d2db029354cccfe7eb150dd7ef9dffe806db36de9b9ba0a66c6", wantUtilsImage: "proxy.replicated.com/anonymous/replicated/ec-utils:latest-amd64@sha256:2f3c5d81565eae3aea22f408af9a8ee91cd4ba010612c50c6be564869390639f", @@ -82,7 +88,9 @@ func Test_operatorImages(t *testing.T) { "proxy.replicated.com/replicated/embedded-cluster-operator-image:latest-amd64", "proxy.replicated.com/replicated/ec-utils:latest-amd64", }, - proxyRegistry: "myproxy.test", + domains: ecv1beta1.Domains{ + ProxyRegistryDomain: "myproxy.test", + }, wantRepo: "myproxy.test/replicated/embedded-cluster-operator-image", wantTag: "latest-amd64", wantUtilsImage: "myproxy.test/replicated/ec-utils:latest-amd64", @@ -113,7 +121,9 @@ func Test_operatorImages(t *testing.T) { "proxy.replicated.com/anonymous/replicated/embedded-cluster-local-artifact-mirror:v1.14.2-k8s-1.29@sha256:54463ce6b6fba13a25138890aa1ac28ae4f93f53cdb78a99d15abfdc1b5eddf5", "proxy.replicated.com/anonymous/replicated/embedded-cluster-operator-image:v1.14.2-k8s-1.29-amd64@sha256:45a45e2ec6b73d2db029354cccfe7eb150dd7ef9dffe806db36de9b9ba0a66c6", }, - proxyRegistry: "myproxy.test", + domains: ecv1beta1.Domains{ + ProxyRegistryDomain: "myproxy.test", + }, wantRepo: "myproxy.test/anonymous/replicated/embedded-cluster-operator-image", wantTag: "v1.14.2-k8s-1.29-amd64@sha256:45a45e2ec6b73d2db029354cccfe7eb150dd7ef9dffe806db36de9b9ba0a66c6", wantUtilsImage: "myproxy.test/anonymous/replicated/ec-utils:latest-amd64@sha256:2f3c5d81565eae3aea22f408af9a8ee91cd4ba010612c50c6be564869390639f", @@ -122,7 +132,9 @@ func Test_operatorImages(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := require.New(t) - gotRepo, gotTag, gotUtilsImage, err := New().operatorImages(tt.images, tt.proxyRegistry) + + addOns := New(WithDomains(tt.domains)) + gotRepo, gotTag, gotUtilsImage, err := addOns.operatorImages(tt.images) if tt.wantErr != "" { req.Error(err) req.EqualError(err, tt.wantErr) diff --git a/pkg/addons/velero/install.go b/pkg/addons/velero/install.go index 7298205e3..dce9b5f2e 100644 --- a/pkg/addons/velero/install.go +++ b/pkg/addons/velero/install.go @@ -8,7 +8,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,14 +18,13 @@ import ( func (v *Velero) Install( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, - overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { if err := v.createPreRequisites(ctx, kcli); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := v.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := v.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/velero/integration/hostcabundle_test.go b/pkg/addons/velero/integration/hostcabundle_test.go index 8fbe5452c..a51aa0e01 100644 --- a/pkg/addons/velero/integration/hostcabundle_test.go +++ b/pkg/addons/velero/integration/hostcabundle_test.go @@ -8,7 +8,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -18,17 +17,15 @@ import ( ) func TestHostCABundle(t *testing.T) { - rc := runtimeconfig.New(nil) - rc.SetHostCABundlePath("/etc/ssl/certs/ca-certificates.crt") - addon := &velero.Velero{ - DryRun: true, + DryRun: true, + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", } hcli, err := helm.NewClient(helm.HelmOptions{}) require.NoError(t, err, "NewClient should not return an error") - err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, ecv1beta1.Domains{}, nil) + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, ecv1beta1.Domains{}, nil) require.NoError(t, err, "velero.Install should not return an error") manifests := addon.DryRunManifests() diff --git a/pkg/addons/velero/integration/k0ssubdir_test.go b/pkg/addons/velero/integration/k0ssubdir_test.go index 562579d5c..41152e20d 100644 --- a/pkg/addons/velero/integration/k0ssubdir_test.go +++ b/pkg/addons/velero/integration/k0ssubdir_test.go @@ -9,7 +9,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -20,19 +19,15 @@ import ( func TestK0sDir(t *testing.T) { k0sDir := filepath.Join(t.TempDir(), "other-k0s") - rcSpec := ecv1beta1.GetDefaultRuntimeConfig() - rcSpec.K0sDataDirOverride = k0sDir - - rc := runtimeconfig.New(rcSpec) - addon := &velero.Velero{ - DryRun: true, + DryRun: true, + K0sDataDir: k0sDir, } hcli, err := helm.NewClient(helm.HelmOptions{}) require.NoError(t, err, "NewClient should not return an error") - err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, ecv1beta1.Domains{}, nil) + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, ecv1beta1.Domains{}, nil) require.NoError(t, err, "velero.Install should not return an error") manifests := addon.DryRunManifests() diff --git a/pkg/addons/velero/upgrade.go b/pkg/addons/velero/upgrade.go index b41bf119f..00560814c 100644 --- a/pkg/addons/velero/upgrade.go +++ b/pkg/addons/velero/upgrade.go @@ -7,7 +7,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/helm" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" @@ -16,7 +15,7 @@ import ( func (v *Velero) Upgrade( ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, - rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string, + domains ecv1beta1.Domains, overrides []string, ) error { exists, err := hcli.ReleaseExists(ctx, v.Namespace(), v.ReleaseName()) if err != nil { @@ -24,13 +23,13 @@ func (v *Velero) Upgrade( } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", v.ReleaseName(), v.Namespace()) - if err := v.Install(ctx, logf, kcli, mcli, hcli, rc, domains, overrides); err != nil { + if err := v.Install(ctx, logf, kcli, mcli, hcli, domains, overrides); err != nil { return errors.Wrap(err, "install") } return nil } - values, err := v.GenerateHelmValues(ctx, kcli, rc, domains, overrides) + values, err := v.GenerateHelmValues(ctx, kcli, domains, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/velero/values.go b/pkg/addons/velero/values.go index f2f1b123d..d69acde35 100644 --- a/pkg/addons/velero/values.go +++ b/pkg/addons/velero/values.go @@ -10,7 +10,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "gopkg.in/yaml.v3" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -34,7 +33,7 @@ func init() { helmValues = hv } -func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { +func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, domains ecv1beta1.Domains, overrides []string) (map[string]interface{}, error) { // create a copy of the helm values so we don't modify the original marshalled, err := helm.MarshalValues(helmValues) if err != nil { @@ -72,11 +71,11 @@ func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, rc }...) } - if rc.HostCABundlePath() != "" { + if v.HostCABundlePath != "" { extraVolumes = append(extraVolumes, map[string]any{ "name": "host-ca-bundle", "hostPath": map[string]any{ - "path": rc.HostCABundlePath(), + "path": v.HostCABundlePath, "type": "FileOrCreate", }, }) @@ -103,12 +102,12 @@ func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, rc "extraVolumeMounts": extraVolumeMounts, } - podVolumePath := filepath.Join(rc.EmbeddedClusterK0sSubDir(), "kubelet/pods") + podVolumePath := filepath.Join(v.K0sDataDir, "kubelet/pods") err = helm.SetValue(copiedValues, "nodeAgent.podVolumePath", podVolumePath) if err != nil { return nil, errors.Wrap(err, "set helm value nodeAgent.podVolumePath") } - pluginVolumePath := filepath.Join(rc.EmbeddedClusterK0sSubDir(), "kubelet/plugins") + pluginVolumePath := filepath.Join(v.K0sDataDir, "kubelet/plugins") err = helm.SetValue(copiedValues, "nodeAgent.pluginVolumePath", pluginVolumePath) if err != nil { return nil, errors.Wrap(err, "set helm value nodeAgent.pluginVolumePath") diff --git a/pkg/addons/velero/values_test.go b/pkg/addons/velero/values_test.go index fd3085921..9b1702c3d 100644 --- a/pkg/addons/velero/values_test.go +++ b/pkg/addons/velero/values_test.go @@ -5,19 +5,16 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { - rc := runtimeconfig.New(nil) - rc.SetDataDir(t.TempDir()) - rc.SetHostCABundlePath("/etc/ssl/certs/ca-certificates.crt") - - v := &Velero{} + v := &Velero{ + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", + } - values, err := v.GenerateHelmValues(context.Background(), nil, rc, ecv1beta1.Domains{}, nil) + values, err := v.GenerateHelmValues(context.Background(), nil, ecv1beta1.Domains{}, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") require.NotEmpty(t, values["extraVolumes"]) diff --git a/pkg/addons/velero/velero.go b/pkg/addons/velero/velero.go index 81c286f96..cf3f19b94 100644 --- a/pkg/addons/velero/velero.go +++ b/pkg/addons/velero/velero.go @@ -4,16 +4,17 @@ import ( "strings" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/addons/types" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "k8s.io/apimachinery/pkg/runtime" jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" ) const ( _releaseName = "velero" - _namespace = runtimeconfig.VeleroNamespace + + _namespace = constants.VeleroNamespace _credentialsSecretName = "cloud-credentials" ) @@ -32,7 +33,9 @@ func init() { var _ types.AddOn = (*Velero)(nil) type Velero struct { - Proxy *ecv1beta1.ProxySpec + Proxy *ecv1beta1.ProxySpec + HostCABundlePath string + K0sDataDir string // DryRun is a flag to enable dry-run mode for Velero. // If true, Velero will only render the helm template and additional manifests, but not install diff --git a/pkg/constants/restore.go b/pkg/constants/restore.go deleted file mode 100644 index 33954c2ce..000000000 --- a/pkg/constants/restore.go +++ /dev/null @@ -1,3 +0,0 @@ -package constants - -const EcRestoreStateCMName = "embedded-cluster-restore-state" diff --git a/pkg/disasterrecovery/backup.go b/pkg/disasterrecovery/backup.go index c95a2a394..8c71f94a4 100644 --- a/pkg/disasterrecovery/backup.go +++ b/pkg/disasterrecovery/backup.go @@ -8,8 +8,8 @@ import ( "strconv" "time" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -98,7 +98,7 @@ type ReplicatedBackup []velerov1.Backup // ListReplicatedBackups returns a sorted list of ReplicatedBackup backups by creation timestamp. func ListReplicatedBackups(ctx context.Context, cli client.Client) ([]ReplicatedBackup, error) { - backups, err := listBackups(ctx, cli, runtimeconfig.VeleroNamespace) + backups, err := listBackups(ctx, cli, constants.VeleroNamespace) if err != nil { return nil, err } diff --git a/pkg/kubeutils/installation.go b/pkg/kubeutils/installation.go index 98f2e7add..6e49a94a5 100644 --- a/pkg/kubeutils/installation.go +++ b/pkg/kubeutils/installation.go @@ -11,6 +11,7 @@ import ( "github.com/Masterminds/semver/v3" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/constants" "github.com/replicatedhq/embedded-cluster/pkg/crds" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -133,7 +134,7 @@ func RecordInstallation(ctx context.Context, kcli client.Client, opts RecordInst // ensure that the embedded-cluster namespace exists ns := corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: runtimeconfig.EmbeddedClusterNamespace, + Name: constants.EmbeddedClusterNamespace, }, } if err := kcli.Create(ctx, &ns); err != nil && !k8serrors.IsAlreadyExists(err) { diff --git a/pkg/runtimeconfig/defaults.go b/pkg/runtimeconfig/defaults.go index 87d724b82..754bfaf89 100644 --- a/pkg/runtimeconfig/defaults.go +++ b/pkg/runtimeconfig/defaults.go @@ -5,8 +5,6 @@ import ( "path/filepath" "github.com/gosimple/slug" - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/sirupsen/logrus" ) @@ -21,15 +19,6 @@ var DefaultNoProxy = []string{ "169.254.169.254", } -const ( - KotsadmNamespace = "kotsadm" - KotsadmServiceAccount = "kotsadm" - SeaweedFSNamespace = "seaweedfs" - RegistryNamespace = "registry" - VeleroNamespace = "velero" - EmbeddedClusterNamespace = "embedded-cluster" -) - const ( K0sBinaryPath = "/usr/local/bin/k0s" K0sStatusSocketPath = "/run/k0s/status.sock" @@ -71,9 +60,3 @@ func EmbeddedClusterLogsSubDir() string { func PathToLog(name string) string { return filepath.Join(EmbeddedClusterLogsSubDir(), name) } - -// GetDomains returns the domains for the embedded cluster. The first priority is the domains configured within the provided config spec. -// The second priority is the domains configured within the channel release. If neither is configured, the default domains are returned. -func GetDomains(cfgspec *ecv1beta1.ConfigSpec) ecv1beta1.Domains { - return domains.GetDomains(cfgspec, release.GetChannelRelease()) -} diff --git a/pkg/runtimeconfig/interface.go b/pkg/runtimeconfig/interface.go index 50e80f0f2..fd9853047 100644 --- a/pkg/runtimeconfig/interface.go +++ b/pkg/runtimeconfig/interface.go @@ -18,7 +18,7 @@ type RuntimeConfig interface { EmbeddedClusterChartsSubDirNoCreate() string EmbeddedClusterImagesSubDir() string EmbeddedClusterK0sSubDir() string - EmbeddedClusterSeaweedfsSubDir() string + EmbeddedClusterSeaweedFSSubDir() string EmbeddedClusterOpenEBSLocalSubDir() string PathToEmbeddedClusterBinary(name string) string PathToKubeConfig() string diff --git a/pkg/runtimeconfig/mock.go b/pkg/runtimeconfig/mock.go index 21ce1082b..36c3753d9 100644 --- a/pkg/runtimeconfig/mock.go +++ b/pkg/runtimeconfig/mock.go @@ -73,8 +73,8 @@ func (m *MockRuntimeConfig) EmbeddedClusterK0sSubDir() string { return args.String(0) } -// EmbeddedClusterSeaweedfsSubDir mocks the EmbeddedClusterSeaweedfsSubDir method -func (m *MockRuntimeConfig) EmbeddedClusterSeaweedfsSubDir() string { +// EmbeddedClusterSeaweedFSSubDir mocks the EmbeddedClusterSeaweedFSSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterSeaweedFSSubDir() string { args := m.Called() return args.String(0) } diff --git a/pkg/runtimeconfig/runtimeconfig.go b/pkg/runtimeconfig/runtimeconfig.go index 3d0dc70a4..9b67097af 100644 --- a/pkg/runtimeconfig/runtimeconfig.go +++ b/pkg/runtimeconfig/runtimeconfig.go @@ -149,8 +149,8 @@ func (rc *runtimeConfig) EmbeddedClusterK0sSubDir() string { return filepath.Join(rc.EmbeddedClusterHomeDirectory(), "k0s") } -// EmbeddedClusterSeaweedfsSubDir returns the path to the directory where seaweedfs data is stored. -func (rc *runtimeConfig) EmbeddedClusterSeaweedfsSubDir() string { +// EmbeddedClusterSeaweedFSSubDir returns the path to the directory where seaweedfs data is stored. +func (rc *runtimeConfig) EmbeddedClusterSeaweedFSSubDir() string { return filepath.Join(rc.EmbeddedClusterHomeDirectory(), "seaweedfs") } diff --git a/pkg/support/materialize.go b/pkg/support/materialize.go index 88e048a17..0b7421206 100644 --- a/pkg/support/materialize.go +++ b/pkg/support/materialize.go @@ -7,6 +7,7 @@ import ( "text/template" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -29,7 +30,7 @@ func MaterializeSupportBundleSpec(rc runtimeconfig.RuntimeConfig, isAirgap bool) if embCfg := release.GetEmbeddedClusterConfig(); embCfg != nil { embCfgSpec = &embCfg.Spec } - domains := runtimeconfig.GetDomains(embCfgSpec) + domains := domains.GetDomains(embCfgSpec, nil) data := TemplateData{ DataDir: rc.EmbeddedClusterHomeDirectory(), diff --git a/tests/integration/kind/openebs/analytics_test.go b/tests/integration/kind/openebs/analytics_test.go index 53cafcd22..6fd465bde 100644 --- a/tests/integration/kind/openebs/analytics_test.go +++ b/tests/integration/kind/openebs/analytics_test.go @@ -5,7 +5,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/openebs" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/tests/integration/util" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -21,14 +20,12 @@ func TestOpenEBS_AnalyticsDisabled(t *testing.T) { mcli := util.MetadataClient(t, kubeconfig) hcli := util.HelmClient(t, kubeconfig) - rc := runtimeconfig.New(nil) - domains := ecv1beta1.Domains{ ProxyRegistryDomain: "proxy.replicated.com", } addon := &openebs.OpenEBS{} - if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, rc, domains, nil); err != nil { + if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, domains, nil); err != nil { t.Fatalf("failed to install openebs: %v", err) } diff --git a/tests/integration/kind/openebs/customdatadir_test.go b/tests/integration/kind/openebs/customdatadir_test.go index 1d317e9de..8b4e6c4a5 100644 --- a/tests/integration/kind/openebs/customdatadir_test.go +++ b/tests/integration/kind/openebs/customdatadir_test.go @@ -8,7 +8,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/openebs" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/tests/integration/util" "github.com/replicatedhq/embedded-cluster/tests/integration/util/kind" "github.com/stretchr/testify/assert" @@ -28,9 +27,6 @@ func TestOpenEBS_CustomDataDir(t *testing.T) { }) kubeconfig := util.SetupKindClusterFromConfig(t, kindConfig) - rc := runtimeconfig.New(nil) - rc.SetDataDir("/custom") - kcli := util.CtrlClient(t, kubeconfig) mcli := util.MetadataClient(t, kubeconfig) hcli := util.HelmClient(t, kubeconfig) @@ -39,8 +35,10 @@ func TestOpenEBS_CustomDataDir(t *testing.T) { ProxyRegistryDomain: "proxy.replicated.com", } - addon := &openebs.OpenEBS{} - if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, rc, domains, nil); err != nil { + addon := &openebs.OpenEBS{ + OpenEBSDataDir: "/custom/openebs-local", + } + if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, domains, nil); err != nil { t.Fatalf("failed to install openebs: %v", err) } @@ -50,7 +48,7 @@ func TestOpenEBS_CustomDataDir(t *testing.T) { createPodAndPVC(t, kubeconfig) _, err := os.Stat(filepath.Join(dataDir, "openebs-local")) - require.NoError(t, err, "failed to find %s data dir") + require.NoError(t, err, "failed to find openebs data dir") entries, err := os.ReadDir(dataDir) require.NoError(t, err, "failed to read openebs data dir") assert.Len(t, entries, 1, "expected pvc dir file in openebs data dir") diff --git a/tests/integration/kind/registry/ha_test.go b/tests/integration/kind/registry/ha_test.go index 10f6fb17c..c2deae911 100644 --- a/tests/integration/kind/registry/ha_test.go +++ b/tests/integration/kind/registry/ha_test.go @@ -73,12 +73,16 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { }) domains := ecv1beta1.Domains{ - ProxyRegistryDomain: "proxy.replicated.com", + ReplicatedAppDomain: "replicated.app", + ProxyRegistryDomain: "proxy.replicated.com", + ReplicatedRegistryDomain: "registry.replicated.com", } t.Logf("%s installing openebs", formattedTime()) - addon := &openebs.OpenEBS{} - if err := addon.Install(ctx, t.Logf, kcli, mcli, hcli, rc, domains, nil); err != nil { + addon := &openebs.OpenEBS{ + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + } + if err := addon.Install(ctx, t.Logf, kcli, mcli, hcli, domains, nil); err != nil { t.Fatalf("failed to install openebs: %v", err) } @@ -90,18 +94,24 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { ServiceCIDR: "10.96.0.0/12", IsHA: false, } - require.NoError(t, registryAddon.Install(ctx, t.Logf, kcli, mcli, hcli, rc, domains, nil)) + require.NoError(t, registryAddon.Install(ctx, t.Logf, kcli, mcli, hcli, domains, nil)) t.Logf("%s creating hostport service", formattedTime()) registryAddr := createHostPortService(t, clusterName, kubeconfig) t.Logf("%s installing admin console", formattedTime()) adminConsoleAddon := &adminconsole.AdminConsole{ - IsAirgap: true, - IsHA: false, - ServiceCIDR: "10.96.0.0/12", + IsAirgap: true, + IsHA: false, + Proxy: rc.ProxySpec(), + ServiceCIDR: "10.96.0.0/12", + IsMultiNodeEnabled: false, + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + AdminConsolePort: rc.AdminConsolePort(), } - require.NoError(t, adminConsoleAddon.Install(ctx, t.Logf, kcli, mcli, hcli, rc, domains, nil)) + require.NoError(t, adminConsoleAddon.Install(ctx, t.Logf, kcli, mcli, hcli, domains, nil)) t.Logf("%s pushing image to registry", formattedTime()) copyImageToRegistry(t, registryAddr, "docker.io/library/busybox:1.36.1") @@ -117,9 +127,7 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { inSpec := ecv1beta1.InstallationSpec{ AirGap: true, Config: &ecv1beta1.ConfigSpec{ - Domains: ecv1beta1.Domains{ - ProxyRegistryDomain: "proxy.replicated.com", - }, + Domains: domains, }, RuntimeConfig: rc.Get(), } @@ -129,7 +137,7 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { addons.WithKubernetesClientSet(kclient), addons.WithMetadataClient(mcli), addons.WithHelmClient(hcli), - addons.WithRuntimeConfig(rc), + addons.WithDomains(domains), ) enableHAAndCancelContextOnMessage(t, addOns, inSpec, @@ -152,7 +160,11 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { loading := newTestingSpinner(t) func() { defer loading.Close() - err = addOns.EnableHA(t.Context(), inSpec, loading) + opts := addons.EnableHAOptions{ + ServiceCIDR: rc.ServiceCIDR(), + ProxySpec: rc.ProxySpec(), + } + err = addOns.EnableHA(t.Context(), opts, loading) require.NoError(t, err) }() @@ -209,8 +221,23 @@ func enableHAAndCancelContextOnMessage(t *testing.T, addOns *addons.AddOns, inSp loading := newTestingSpinner(t) defer loading.Close() + rc := runtimeconfig.New(inSpec.RuntimeConfig) + t.Logf("%s enabling HA and cancelling context on message", formattedTime()) - err = addOns.EnableHA(ctx, inSpec, loading) + opts := addons.EnableHAOptions{ + AdminConsolePort: rc.AdminConsolePort(), + IsAirgap: true, + IsMultiNodeEnabled: false, + EmbeddedConfigSpec: inSpec.Config, + EndUserConfigSpec: inSpec.Config, + ProxySpec: rc.ProxySpec(), + HostCABundlePath: rc.HostCABundlePath(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + SeaweedFSDataDir: rc.EmbeddedClusterSeaweedFSSubDir(), + ServiceCIDR: inSpec.RuntimeConfig.Network.ServiceCIDR, + } + err = addOns.EnableHA(ctx, opts, loading) require.ErrorIs(t, err, context.Canceled, "expected context to be cancelled") t.Logf("%s cancelled context and got error: %v", formattedTime(), err) } diff --git a/tests/integration/kind/velero/ca_test.go b/tests/integration/kind/velero/ca_test.go index d9ebed463..470594f69 100644 --- a/tests/integration/kind/velero/ca_test.go +++ b/tests/integration/kind/velero/ca_test.go @@ -5,7 +5,6 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/addons/velero" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/tests/integration/util" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -23,15 +22,15 @@ func TestVelero_HostCABundle(t *testing.T) { mcli := util.MetadataClient(t, kubeconfig) hcli := util.HelmClient(t, kubeconfig) - rc := runtimeconfig.New(nil) - rc.SetHostCABundlePath("/etc/ssl/certs/ca-certificates.crt") - domains := ecv1beta1.Domains{ ProxyRegistryDomain: "proxy.replicated.com", } - addon := &velero.Velero{} - if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, rc, domains, nil); err != nil { + addon := &velero.Velero{ + HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", + } + + if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, domains, nil); err != nil { t.Fatalf("failed to install velero: %v", err) } diff --git a/web/static.go b/web/static.go index e785308f5..58016a3f5 100644 --- a/web/static.go +++ b/web/static.go @@ -143,6 +143,7 @@ func (web *Web) rootHandler(w http.ResponseWriter, r *http.Request) { web.logger.WithError(err). Info("failed to execute HTML template") http.Error(w, "Template execution error", 500) + return } // Write the buffer contents to the response writer diff --git a/web/static_test.go b/web/static_test.go index abaf9a2bc..1131e7dc7 100644 --- a/web/static_test.go +++ b/web/static_test.go @@ -73,6 +73,9 @@ func TestNew(t *testing.T) { // Create a test logger logger, _ := logtest.NewNullLogger() + // Set the development environment variable to false + t.Setenv("EC_DEV_ENV", "false") + // Create a new Web instance web, err := New(initialState, WithLogger(logger), WithAssetsFS(createMockFS())) require.NoError(t, err, "Failed to create Web instance") @@ -98,6 +101,9 @@ func TestNewWithDefaultFS(t *testing.T) { // Create a test logger logger, _ := logtest.NewNullLogger() + // Set the development environment variable to false + t.Setenv("EC_DEV_ENV", "false") + // Create a new Web instance web, err := New(initialState, WithLogger(logger)) require.NoError(t, err, "Failed to create Web instance") @@ -139,6 +145,9 @@ func TestNewWithIndexHTML(t *testing.T) { // Create a test logger logger, _ := logtest.NewNullLogger() + // Set the development environment variable to false + t.Setenv("EC_DEV_ENV", "false") + // Create a new Web instance, using the actual index.html template web, err := New(initialState, WithLogger(logger), WithAssetsFS(mockFS)) require.NoError(t, err, "Failed to create Web instance") @@ -171,6 +180,9 @@ func TestNewWithNonExistentTemplate(t *testing.T) { // Create a test logger logger, _ := logtest.NewNullLogger() + // Set the development environment variable to false + t.Setenv("EC_DEV_ENV", "false") + // Try to create a new Web instance without providing an HTML template web, err := New(initialState, WithLogger(logger), WithAssetsFS(mockFS)) @@ -193,6 +205,9 @@ func TestRootHandler(t *testing.T) { // Create a test logger logger, _ := logtest.NewNullLogger() + // Set the development environment variable to false + t.Setenv("EC_DEV_ENV", "false") + // Create a new Web instance web, err := New(initialState, WithLogger(logger), WithAssetsFS(createMockFS())) require.NoError(t, err, "Failed to create Web instance") @@ -228,6 +243,9 @@ func TestRootHandlerTemplateError(t *testing.T) { // Create a test logger logger, _ := logtest.NewNullLogger() + // Set the development environment variable to false + t.Setenv("EC_DEV_ENV", "false") + // Create a new Web instance web, err := New(initialState, WithLogger(logger), WithAssetsFS(createMockFS())) require.NoError(t, err, "Failed to create Web instance") @@ -266,6 +284,9 @@ func TestRegisterRoutes(t *testing.T) { // Create a test logger logger, _ := logtest.NewNullLogger() + // Set the development environment variable to false + t.Setenv("EC_DEV_ENV", "false") + // Create a new Web instance web, err := New(initialState, WithLogger(logger), WithAssetsFS(createMockFS())) require.NoError(t, err, "Failed to create Web instance") From 58b56c3f5754b363449162eb94bd4b65da7ca615 Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:34:57 -0400 Subject: [PATCH 37/48] feat(ui): show modal when frontend loses connection to backend (#2370) --- web/src/App.tsx | 2 + .../components/common/ConnectionMonitor.tsx | 124 ++++++++++++++++++ .../common/tests/ConnectionMonitor.test.tsx | 104 +++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 web/src/components/common/ConnectionMonitor.tsx create mode 100644 web/src/components/common/tests/ConnectionMonitor.test.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 341ea5c7b..7ef175cb9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,6 +3,7 @@ import { ConfigProvider } from "./contexts/ConfigContext"; import { WizardModeProvider } from "./contexts/WizardModeContext"; import { BrandingProvider } from "./contexts/BrandingContext"; import { AuthProvider } from "./contexts/AuthContext"; +import ConnectionMonitor from "./components/common/ConnectionMonitor"; import InstallWizard from "./components/wizard/InstallWizard"; import { QueryClientProvider } from "@tanstack/react-query"; import { getQueryClient } from "./query-client"; @@ -33,6 +34,7 @@ function App() { + ); } diff --git a/web/src/components/common/ConnectionMonitor.tsx b/web/src/components/common/ConnectionMonitor.tsx new file mode 100644 index 000000000..6c0875ad7 --- /dev/null +++ b/web/src/components/common/ConnectionMonitor.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState, useCallback } from 'react'; + +// Connection modal component +const ConnectionModal: React.FC<{ onRetry: () => void; isRetrying: boolean }> = ({ onRetry, isRetrying }) => { + const [retryCount, setRetryCount] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setRetryCount(count => count + 1); + }, 1000); + return () => clearInterval(interval); + }, []); + + return ( +
+
+
+
+ + + +
+
+ +

+ Cannot connect +

+ +

+ We're unable to reach the server right now. Please check that the + installer is running and accessible. +

+ +
+
+
+ Trying again in {Math.max(1, 10 - (retryCount % 10))} second{Math.max(1, 10 - (retryCount % 10)) !== 1 ? 's' : ''} +
+ +
+
+
+ ); +}; + +const ConnectionMonitor: React.FC = () => { + const [isConnected, setIsConnected] = useState(true); + const [isChecking, setIsChecking] = useState(false); + + const checkConnection = useCallback(async () => { + setIsChecking(true); + + try { + // Try up to 3 times before marking as disconnected + let attempts = 0; + const maxAttempts = 3; + + while (attempts < maxAttempts) { + try { + // Create a timeout promise + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 5000) + ); + + const fetchPromise = fetch('/api/health', { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await Promise.race([fetchPromise, timeoutPromise]) as Response; + + if (response.ok) { + setIsConnected(true); + return; + } else { + throw new Error(`HTTP ${response.status}`); + } + } catch { + attempts++; + if (attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } + + // All attempts failed - show modal immediately + setIsConnected(false); + + } catch { + setIsConnected(false); + } finally { + setIsChecking(false); + } + }, []); + + useEffect(() => { + // Initial check + checkConnection(); + + // Set up periodic health checks every 5 seconds + const interval = setInterval(checkConnection, 5000); + + return () => clearInterval(interval); + }, [checkConnection]); + + return ( + <> + {!isConnected && ( + + )} + + ); +}; + +export default ConnectionMonitor; diff --git a/web/src/components/common/tests/ConnectionMonitor.test.tsx b/web/src/components/common/tests/ConnectionMonitor.test.tsx new file mode 100644 index 000000000..d3b8f3b01 --- /dev/null +++ b/web/src/components/common/tests/ConnectionMonitor.test.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import ConnectionMonitor from '../ConnectionMonitor'; + +const server = setupServer( + http.get('*/api/health', () => { + return new HttpResponse(JSON.stringify({ status: 'ok' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }) +); + +describe('ConnectionMonitor', () => { + beforeEach(() => { + server.listen({ onUnhandledRequest: 'warn' }); + }); + + afterEach(() => { + server.resetHandlers(); + vi.clearAllMocks(); + }); + + it('should not show modal when API is available', async () => { + render(); + + // Modal should not appear when connected + await new Promise(resolve => setTimeout(resolve, 100)); + expect(screen.queryByText('Cannot connect')).not.toBeInTheDocument(); + }); + + it('should show modal when health check fails', async () => { + server.use( + http.get('*/api/health', () => { + return HttpResponse.error(); + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByText('Cannot connect')).toBeInTheDocument(); + }, { timeout: 4000 }); + }, 6000); + + it('should handle manual retry', async () => { + let manualRetryClicked = false; + + server.use( + http.get('*/api/health', () => { + + // Keep failing until manual retry is clicked, then succeed + if (!manualRetryClicked) { + return HttpResponse.error(); + } + + return new HttpResponse(JSON.stringify({ status: 'ok' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }) + ); + + render(); + + // Wait for modal to appear after first health check fails + await waitFor(() => { + expect(screen.getByText('Cannot connect')).toBeInTheDocument(); + }, { timeout: 6000 }); + + // Wait for the retry button to be available + await waitFor(() => { + expect(screen.getByText('Try Now')).toBeInTheDocument(); + }, { timeout: 1000 }); + + // Mark that manual retry was clicked, then click it + manualRetryClicked = true; + fireEvent.click(screen.getByText('Try Now')); + + // Modal should disappear when connection is restored + await waitFor(() => { + expect(screen.queryByText('Cannot connect')).not.toBeInTheDocument(); + }, { timeout: 6000 }); + }, 12000); + + it('should show retry countdown timer', async () => { + server.use( + http.get('*/api/health', () => { + return HttpResponse.error(); + }) + ); + + render(); + + await waitFor(() => { + expect(screen.getByText('Cannot connect')).toBeInTheDocument(); + }, { timeout: 4000 }); + + expect(screen.getByText(/Trying again in \d+ second/)).toBeInTheDocument(); + }, 6000); +}); From 2dfd9274915e0843a2ea1c97ca1bcdf82838c310 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 26 Jun 2025 09:57:37 -0700 Subject: [PATCH 38/48] chore(api): export and document for more clear interface (#2375) * chore(api): export types for more clear interface * docs --- api/api.go | 37 ++++++++++++------- .../linux/install/hostpreflight.go | 6 +-- api/handlers.go | 2 +- api/routes.go | 10 +++-- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/api/api.go b/api/api.go index 36f564f17..414a3c003 100644 --- a/api/api.go +++ b/api/api.go @@ -13,6 +13,8 @@ import ( "github.com/sirupsen/logrus" ) +// API represents the main HTTP API server for the Embedded Cluster application. +// // @title Embedded Cluster API // @version 0.1 // @description This is the API for the Embedded Cluster project. @@ -31,7 +33,7 @@ import ( // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ -type api struct { +type API struct { cfg types.APIConfig logger logrus.FieldLogger @@ -44,40 +46,47 @@ type api struct { handlers handlers } -type apiOption func(*api) +// Option is a function that configures the API. +type Option func(*API) -func WithAuthController(authController auth.Controller) apiOption { - return func(a *api) { +// WithAuthController configures the auth controller for the API. +func WithAuthController(authController auth.Controller) Option { + return func(a *API) { a.authController = authController } } -func WithConsoleController(consoleController console.Controller) apiOption { - return func(a *api) { +// WithConsoleController configures the console controller for the API. +func WithConsoleController(consoleController console.Controller) Option { + return func(a *API) { a.consoleController = consoleController } } -func WithLinuxInstallController(linuxInstallController linuxinstall.Controller) apiOption { - return func(a *api) { +// WithLinuxInstallController configures the linux install controller for the API. +func WithLinuxInstallController(linuxInstallController linuxinstall.Controller) Option { + return func(a *API) { a.linuxInstallController = linuxInstallController } } -func WithLogger(logger logrus.FieldLogger) apiOption { - return func(a *api) { +// WithLogger configures the logger for the API. If not provided, a default logger will be created. +func WithLogger(logger logrus.FieldLogger) Option { + return func(a *API) { a.logger = logger } } -func WithMetricsReporter(metricsReporter metrics.ReporterInterface) apiOption { - return func(a *api) { +// WithMetricsReporter configures the metrics reporter for the API. +func WithMetricsReporter(metricsReporter metrics.ReporterInterface) Option { + return func(a *API) { a.metricsReporter = metricsReporter } } -func New(cfg types.APIConfig, opts ...apiOption) (*api, error) { - api := &api{ +// New creates a new API instance. +func New(cfg types.APIConfig, opts ...Option) (*API, error) { + api := &API{ cfg: cfg, } diff --git a/api/controllers/linux/install/hostpreflight.go b/api/controllers/linux/install/hostpreflight.go index cc4e1e174..63f5bc087 100644 --- a/api/controllers/linux/install/hostpreflight.go +++ b/api/controllers/linux/install/hostpreflight.go @@ -43,12 +43,12 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP IsUI: opts.IsUI, }) if err != nil { - return fmt.Errorf("failed to prepare host preflights: %w", err) + return fmt.Errorf("prepare host preflights: %w", err) } err = c.stateMachine.Transition(lock, StatePreflightsRunning) if err != nil { - return fmt.Errorf("failed to transition states: %w", err) + return fmt.Errorf("transition states: %w", err) } go func() (finalErr error) { @@ -78,7 +78,7 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP HostPreflightSpec: hpf, }) if err != nil { - return fmt.Errorf("failed to run host preflights: %w", err) + return fmt.Errorf("run host preflights: %w", err) } return nil diff --git a/api/handlers.go b/api/handlers.go index 3864eb4b0..b61ea5c78 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -16,7 +16,7 @@ type handlers struct { linux *linuxhandler.Handler } -func (a *api) initHandlers() error { +func (a *API) initHandlers() error { // Auth handler authHandler, err := authhandler.New( a.cfg.Password, diff --git a/api/routes.go b/api/routes.go index f036efd7a..0407bee6e 100644 --- a/api/routes.go +++ b/api/routes.go @@ -8,7 +8,9 @@ import ( httpSwagger "github.com/swaggo/http-swagger/v2" ) -func (a *api) RegisterRoutes(router *mux.Router) { +// RegisterRoutes registers the routes for the API. A router is passed in to allow for the routes +// to be registered on a subrouter. +func (a *API) RegisterRoutes(router *mux.Router) { router.HandleFunc("/health", a.handlers.health.GetHealth).Methods("GET") // Hack to fix issue @@ -30,7 +32,7 @@ func (a *api) RegisterRoutes(router *mux.Router) { a.registerConsoleRoutes(authenticatedRouter) } -func (a *api) registerLinuxRoutes(router *mux.Router) { +func (a *API) registerLinuxRoutes(router *mux.Router) { linuxRouter := router.PathPrefix("/linux").Subrouter() installRouter := linuxRouter.PathPrefix("/install").Subrouter() @@ -50,11 +52,11 @@ func (a *api) registerLinuxRoutes(router *mux.Router) { installRouter.HandleFunc("/status", a.handlers.linux.PostSetStatus).Methods("POST") } -func (a *api) registerKubernetesRoutes(router *mux.Router) { +func (a *API) registerKubernetesRoutes(router *mux.Router) { // kubernetesRouter := router.PathPrefix("/kubernetes").Subrouter() } -func (a *api) registerConsoleRoutes(router *mux.Router) { +func (a *API) registerConsoleRoutes(router *mux.Router) { consoleRouter := router.PathPrefix("/console").Subrouter() consoleRouter.HandleFunc("/available-network-interfaces", a.handlers.console.GetListAvailableNetworkInterfaces).Methods("GET") } From 82e4dd968ef89e28ca4bfd1eb3136e5b7f044bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Antunes?= Date: Thu, 26 Jun 2025 19:26:51 +0100 Subject: [PATCH 39/48] feat(reporting): add metrics reporting to the new manager experience (#2337) * chore: initialize the reporter * feat: use the new state machine event handlers * chore: rework the valid state transitions * chore: more tests * chore: tests for configuration storage failure * chore: add new states for the host preflights execution * fix: correctly handle state machine transtitions for host preflight execution * chore: tests for the multiple preflight execution transitions * fix: infra installation flow and more tests * chore: more tests for the infra reporting * chore: installation tests * chore: gofmt forgot to do its things * fix: don't assert on current state before the final move --- api/controllers/linux/install/controller.go | 20 +- .../linux/install/controller_test.go | 508 +++++++++++++++--- .../linux/install/hostpreflight.go | 30 +- api/controllers/linux/install/infra.go | 58 +- api/controllers/linux/install/installation.go | 25 +- .../linux/install/reporting_handlers.go | 69 +++ api/controllers/linux/install/statemachine.go | 53 +- .../linux/install/statemachine_test.go | 59 +- api/integration/install_test.go | 86 +-- .../managers/preflight/hostpreflight.go | 7 +- .../managers/preflight/hostpreflight_test.go | 33 +- api/internal/managers/preflight/manager.go | 8 - api/internal/store/store_mock.go | 31 ++ cmd/installer/cli/install.go | 23 +- pkg/metrics/reporter_mock.go | 20 +- 15 files changed, 777 insertions(+), 253 deletions(-) create mode 100644 api/controllers/linux/install/reporting_handlers.go create mode 100644 api/internal/store/store_mock.go diff --git a/api/controllers/linux/install/controller.go b/api/controllers/linux/install/controller.go index 08368bc6f..ce90a2e54 100644 --- a/api/controllers/linux/install/controller.go +++ b/api/controllers/linux/install/controller.go @@ -168,18 +168,27 @@ func WithStateMachine(stateMachine statemachine.Interface) InstallControllerOpti } } +func WithStore(store store.Store) InstallControllerOption { + return func(c *InstallController) { + c.store = store + } +} + func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { controller := &InstallController{ - store: store.NewMemoryStore(), - rc: runtimeconfig.New(nil), - logger: logger.NewDiscardLogger(), - stateMachine: NewStateMachine(), + store: store.NewMemoryStore(), + rc: runtimeconfig.New(nil), + logger: logger.NewDiscardLogger(), } for _, opt := range opts { opt(controller) } + if controller.stateMachine == nil { + controller.stateMachine = NewStateMachine(WithStateMachineLogger(controller.logger)) + } + if controller.hostUtils == nil { controller.hostUtils = hostutils.New( hostutils.WithLogger(controller.logger), @@ -204,7 +213,6 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, if controller.hostPreflightManager == nil { controller.hostPreflightManager = preflight.NewHostPreflightManager( preflight.WithLogger(controller.logger), - preflight.WithMetricsReporter(controller.metricsReporter), preflight.WithHostPreflightStore(controller.store.PreflightStore()), preflight.WithNetUtils(controller.netUtils), ) @@ -224,5 +232,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, ) } + controller.registerReportingHandlers() + return controller, nil } diff --git a/api/controllers/linux/install/controller_test.go b/api/controllers/linux/install/controller_test.go index fe9bf04f4..d147badff 100644 --- a/api/controllers/linux/install/controller_test.go +++ b/api/controllers/linux/install/controller_test.go @@ -13,6 +13,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" + "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/metrics" @@ -21,6 +22,33 @@ import ( troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" ) +var failedPreflightOutput = &types.HostPreflightsOutput{ + Fail: []types.HostPreflightsRecord{ + { + Title: "Test Check", + Message: "Test check failed", + }, + }, +} + +var successfulPreflightOutput = &types.HostPreflightsOutput{ + Pass: []types.HostPreflightsRecord{ + { + Title: "Test Check", + Message: "Test check passed", + }, + }, +} + +var warnPreflightOutput = &types.HostPreflightsOutput{ + Warn: []types.HostPreflightsRecord{ + { + Title: "Test Check", + Message: "Test check warning", + }, + }, +} + func TestGetInstallationConfig(t *testing.T) { tests := []struct { name string @@ -119,7 +147,7 @@ func TestConfigureInstallation(t *testing.T) { config types.InstallationConfig currentState statemachine.State expectedState statemachine.State - setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, types.InstallationConfig) + setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, types.InstallationConfig, *store.MockStore, *metrics.MockReporter) expectedErr bool }{ { @@ -130,7 +158,8 @@ func TestConfigureInstallation(t *testing.T) { }, currentState: StateNew, expectedState: StateHostConfigured, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { + + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", config).Return(nil), @@ -140,12 +169,56 @@ func TestConfigureInstallation(t *testing.T) { expectedErr: false, }, { - name: "validate error", + name: "validatation error", config: types.InstallationConfig{}, currentState: StateNew, - expectedState: StateNew, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { - m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")) + expectedState: StateInstallationConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")), + // Status is set in the store by the controller when configuring the installation + st.InstallationMockStore.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { + return status.State == types.StateFailed && status.Description == "validate: validation error" + })).Return(nil), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "validate: validation error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("validate: validation error")), + ) + }, + expectedErr: true, + }, + { + name: "validation error on retry from host already configured", + config: types.InstallationConfig{}, + currentState: StateHostConfigured, + expectedState: StateInstallationConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")), + // Status is set in the store by the controller when configuring the installation + st.InstallationMockStore.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { + return status.State == types.StateFailed && status.Description == "validate: validation error" + })).Return(nil), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "validate: validation error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("validate: validation error")), + ) + }, + expectedErr: true, + }, + { + name: "validation error on retry from host that failed to configure", + config: types.InstallationConfig{}, + currentState: StateHostConfigurationFailed, + expectedState: StateInstallationConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")), + // Status is set in the store by the controller when configuring the installation + st.InstallationMockStore.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { + return status.State == types.StateFailed && status.Description == "validate: validation error" + })).Return(nil), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "validate: validation error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("validate: validation error")), + ) }, expectedErr: true, }, @@ -153,11 +226,55 @@ func TestConfigureInstallation(t *testing.T) { name: "set config error", config: types.InstallationConfig{}, currentState: StateNew, - expectedState: StateNew, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { + expectedState: StateInstallationConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(nil), + m.On("SetConfig", config).Return(errors.New("set config error")), + // Status is set in the store by the controller when configuring the installation + st.InstallationMockStore.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { + return status.State == types.StateFailed && status.Description == "write: set config error" + })).Return(nil), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "validate: validation error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("validate: validation error")), + ) + }, + expectedErr: true, + }, + { + name: "set config error on retry from host already configured", + config: types.InstallationConfig{}, + currentState: StateHostConfigured, + expectedState: StateInstallationConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(nil), + m.On("SetConfig", config).Return(errors.New("set config error")), + // Status is set in the store by the controller when configuring the installation + st.InstallationMockStore.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { + return status.State == types.StateFailed && status.Description == "write: set config error" + })).Return(nil), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "write: set config error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("write: set config error")), + ) + }, + expectedErr: true, + }, + { + name: "set config error on retry from host that failed to configure", + config: types.InstallationConfig{}, + currentState: StateHostConfigurationFailed, + expectedState: StateInstallationConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", config).Return(errors.New("set config error")), + // Status is set in the store by the controller when configuring the installation + st.InstallationMockStore.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { + return status.State == types.StateFailed && status.Description == "write: set config error" + })).Return(nil), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "write: set config error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("write: set config error")), ) }, expectedErr: true, @@ -169,12 +286,52 @@ func TestConfigureInstallation(t *testing.T) { DataDirectory: t.TempDir(), }, currentState: StateNew, - expectedState: StateInstallationConfigured, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { + expectedState: StateHostConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { mock.InOrder( m.On("ValidateConfig", config, 9001).Return(nil), m.On("SetConfig", config).Return(nil), m.On("ConfigureHost", mock.Anything, rc).Return(errors.New("configure host error")), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "configure host error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("configure host error")), + ) + }, + expectedErr: false, + }, + { + name: "configure host error on retry from host already configured", + config: types.InstallationConfig{ + LocalArtifactMirrorPort: 9000, + DataDirectory: t.TempDir(), + }, + currentState: StateHostConfigured, + expectedState: StateHostConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(nil), + m.On("SetConfig", config).Return(nil), + m.On("ConfigureHost", mock.Anything, rc).Return(errors.New("configure host error")), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "configure host error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("configure host error")), + ) + }, + expectedErr: false, + }, + { + name: "configure host error on retry from host that failed to configure", + config: types.InstallationConfig{ + LocalArtifactMirrorPort: 9000, + DataDirectory: t.TempDir(), + }, + currentState: StateHostConfigurationFailed, + expectedState: StateHostConfigurationFailed, + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { + mock.InOrder( + m.On("ValidateConfig", config, 9001).Return(nil), + m.On("SetConfig", config).Return(nil), + m.On("ConfigureHost", mock.Anything, rc).Return(errors.New("configure host error")), + st.InstallationMockStore.On("GetStatus").Return(types.Status{Description: "configure host error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("configure host error")), ) }, expectedErr: false, @@ -187,7 +344,7 @@ func TestConfigureInstallation(t *testing.T) { }, currentState: StateNew, expectedState: StateHostConfigured, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { // Create a copy with expected CIDR values after computation configWithCIDRs := config configWithCIDRs.PodCIDR = "10.0.0.0/17" @@ -209,7 +366,7 @@ func TestConfigureInstallation(t *testing.T) { }, currentState: StateInfrastructureInstalling, expectedState: StateInfrastructureInstalling, - setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, rc runtimeconfig.RuntimeConfig, config types.InstallationConfig, st *store.MockStore, mr *metrics.MockReporter) { }, expectedErr: true, }, @@ -224,23 +381,23 @@ func TestConfigureInstallation(t *testing.T) { sm := NewStateMachine(WithCurrentState(tt.currentState)) mockManager := &installation.MockInstallationManager{} + metricsReporter := &metrics.MockReporter{} + mockStore := &store.MockStore{} - tt.setupMock(mockManager, rc, tt.config) + tt.setupMock(mockManager, rc, tt.config, mockStore, metricsReporter) controller, err := NewInstallController( WithRuntimeConfig(rc), WithStateMachine(sm), WithInstallationManager(mockManager), + WithStore(mockStore), + WithMetricsReporter(metricsReporter), ) require.NoError(t, err) err = controller.ConfigureInstallation(t.Context(), tt.config) if tt.expectedErr { assert.Error(t, err) - } else { - assert.NoError(t, err) - - assert.NotEqual(t, tt.currentState, sm.CurrentState(), "state should have changed and should not be %s", tt.currentState) } assert.Eventually(t, func() bool { @@ -249,6 +406,10 @@ func TestConfigureInstallation(t *testing.T) { assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after configuration") mockManager.AssertExpectations(t) + metricsReporter.AssertExpectations(t) + mockStore.InfraMockStore.AssertExpectations(t) + mockStore.InstallationMockStore.AssertExpectations(t) + mockStore.PreflightMockStore.AssertExpectations(t) }) } } @@ -325,19 +486,194 @@ func TestRunHostPreflights(t *testing.T) { name string currentState statemachine.State expectedState statemachine.State - setupMocks func(*preflight.MockHostPreflightManager, runtimeconfig.RuntimeConfig) + setupMocks func(*preflight.MockHostPreflightManager, runtimeconfig.RuntimeConfig, *metrics.MockReporter, *store.MockStore) expectedErr bool }{ { - name: "successful run preflights", + name: "successful run preflights without preflight errors", currentState: StateHostConfigured, expectedState: StatePreflightsSucceeded, - setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(successfulPreflightOutput, nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights from preflights execution failed state without preflight errors", + currentState: StatePreflightsExecutionFailed, + expectedState: StatePreflightsSucceeded, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(successfulPreflightOutput, nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights from preflights failed state without preflight errors", + currentState: StatePreflightsFailed, + expectedState: StatePreflightsSucceeded, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(successfulPreflightOutput, nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights from preflights failure bypassed state without preflight errors", + currentState: StatePreflightsFailedBypassed, + expectedState: StatePreflightsSucceeded, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { return expectedHPF == opts.HostPreflightSpec })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(successfulPreflightOutput, nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights with preflight errors", + currentState: StatePreflightsFailedBypassed, + expectedState: StatePreflightsFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(failedPreflightOutput, nil), + st.PreflightMockStore.On("GetOutput").Return(failedPreflightOutput, nil), + mr.On("ReportPreflightsFailed", mock.Anything, failedPreflightOutput).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights with preflight errors and failure to get output for reporting", + currentState: StatePreflightsFailedBypassed, + expectedState: StatePreflightsFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(failedPreflightOutput, nil), + st.PreflightMockStore.On("GetOutput").Return(nil, assert.AnError), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights from preflights execution failed state with preflight errors", + currentState: StatePreflightsExecutionFailed, + expectedState: StatePreflightsFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(failedPreflightOutput, nil), + st.PreflightMockStore.On("GetOutput").Return(failedPreflightOutput, nil), + mr.On("ReportPreflightsFailed", mock.Anything, failedPreflightOutput).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights from preflights failed state with preflight errors", + currentState: StatePreflightsFailed, + expectedState: StatePreflightsFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(failedPreflightOutput, nil), + st.PreflightMockStore.On("GetOutput").Return(failedPreflightOutput, nil), + mr.On("ReportPreflightsFailed", mock.Anything, failedPreflightOutput).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights from preflights failure bypassed state with preflight errors", + currentState: StatePreflightsFailedBypassed, + expectedState: StatePreflightsFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(failedPreflightOutput, nil), + st.PreflightMockStore.On("GetOutput").Return(failedPreflightOutput, nil), + mr.On("ReportPreflightsFailed", mock.Anything, failedPreflightOutput).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights with get preflight output error", + currentState: StateHostConfigured, + expectedState: StatePreflightsExecutionFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(nil, assert.AnError), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights with nil preflight output", + currentState: StateHostConfigured, + expectedState: StatePreflightsSucceeded, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(nil, nil), + ) + }, + expectedErr: false, + }, + { + name: "successful run preflights with preflight warnings", + currentState: StateHostConfigured, + expectedState: StatePreflightsSucceeded, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Return(nil), + pm.On("GetHostPreflightOutput", mock.Anything).Return(warnPreflightOutput, nil), ) }, expectedErr: false, @@ -346,7 +682,7 @@ func TestRunHostPreflights(t *testing.T) { name: "prepare preflights error", currentState: StateHostConfigured, expectedState: StateHostConfigured, - setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(nil, errors.New("prepare error")), ) @@ -356,8 +692,8 @@ func TestRunHostPreflights(t *testing.T) { { name: "run preflights error", currentState: StateHostConfigured, - expectedState: StatePreflightsFailed, - setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { + expectedState: StatePreflightsExecutionFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { @@ -367,11 +703,25 @@ func TestRunHostPreflights(t *testing.T) { }, expectedErr: false, }, + { + name: "run preflights panic", + currentState: StateHostConfigured, + expectedState: StatePreflightsExecutionFailed, + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + pm.On("PrepareHostPreflights", t.Context(), rc, mock.Anything).Return(expectedHPF, nil), + pm.On("RunHostPreflights", mock.Anything, rc, mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec + })).Panic("this is a panic"), + ) + }, + expectedErr: false, + }, { name: "invalid state transition", currentState: StateInfrastructureInstalling, expectedState: StateInfrastructureInstalling, - setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(pm *preflight.MockHostPreflightManager, rc runtimeconfig.RuntimeConfig, mr *metrics.MockReporter, st *store.MockStore) { }, expectedErr: true, }, @@ -391,13 +741,17 @@ func TestRunHostPreflights(t *testing.T) { sm := NewStateMachine(WithCurrentState(tt.currentState)) mockPreflightManager := &preflight.MockHostPreflightManager{} - tt.setupMocks(mockPreflightManager, rc) + mockReporter := &metrics.MockReporter{} + mockStore := &store.MockStore{} + tt.setupMocks(mockPreflightManager, rc, mockReporter, mockStore) controller, err := NewInstallController( WithRuntimeConfig(rc), WithStateMachine(sm), WithHostPreflightManager(mockPreflightManager), WithReleaseData(getTestReleaseData()), + WithMetricsReporter(mockReporter), + WithStore(mockStore), ) require.NoError(t, err) @@ -417,6 +771,10 @@ func TestRunHostPreflights(t *testing.T) { assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after running preflights") mockPreflightManager.AssertExpectations(t) + mockReporter.AssertExpectations(t) + mockStore.InfraMockStore.AssertExpectations(t) + mockStore.InstallationMockStore.AssertExpectations(t) + mockStore.PreflightMockStore.AssertExpectations(t) }) } } @@ -484,25 +842,11 @@ func TestGetHostPreflightOutput(t *testing.T) { { name: "successful get output", setupMock: func(m *preflight.MockHostPreflightManager) { - output := &types.HostPreflightsOutput{ - Pass: []types.HostPreflightsRecord{ - { - Title: "Test Check", - Message: "Test check passed", - }, - }, - } + output := successfulPreflightOutput m.On("GetHostPreflightOutput", t.Context()).Return(output, nil) }, - expectedErr: false, - expectedValue: &types.HostPreflightsOutput{ - Pass: []types.HostPreflightsRecord{ - { - Title: "Test Check", - Message: "Test check passed", - }, - }, - }, + expectedErr: false, + expectedValue: successfulPreflightOutput, }, { name: "get output error", @@ -646,7 +990,7 @@ func TestSetupInfra(t *testing.T) { serverAllowIgnoreHostPreflights bool // From CLI flag currentState statemachine.State expectedState statemachine.State - setupMocks func(runtimeconfig.RuntimeConfig, *preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter) + setupMocks func(runtimeconfig.RuntimeConfig, *preflight.MockHostPreflightManager, *installation.MockInstallationManager, *infra.MockInfraManager, *metrics.MockReporter, *store.MockStore) expectedErr error }{ { @@ -655,9 +999,10 @@ func TestSetupInfra(t *testing.T) { serverAllowIgnoreHostPreflights: true, currentState: StatePreflightsSucceeded, expectedState: StateSucceeded, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( fm.On("Install", mock.Anything, rc).Return(nil), + mr.On("ReportInstallationSucceeded", mock.Anything), ) }, expectedErr: nil, @@ -668,19 +1013,12 @@ func TestSetupInfra(t *testing.T) { serverAllowIgnoreHostPreflights: true, currentState: StatePreflightsFailed, expectedState: StateSucceeded, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightOutput := &types.HostPreflightsOutput{ - Fail: []types.HostPreflightsRecord{ - { - Title: "Test Check", - Message: "Test check failed", - }, - }, - } + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( - pm.On("GetHostPreflightOutput", t.Context()).Return(preflightOutput, nil), - r.On("ReportPreflightsBypassed", t.Context(), preflightOutput).Return(nil), + st.PreflightMockStore.On("GetOutput").Return(failedPreflightOutput, nil), + mr.On("ReportPreflightsBypassed", mock.Anything, failedPreflightOutput), fm.On("Install", mock.Anything, rc).Return(nil), + mr.On("ReportInstallationSucceeded", mock.Anything), ) }, expectedErr: nil, @@ -691,32 +1029,50 @@ func TestSetupInfra(t *testing.T) { serverAllowIgnoreHostPreflights: true, currentState: StatePreflightsFailed, expectedState: StatePreflightsFailed, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { }, expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), }, { - name: "preflight output error", - clientIgnoreHostPreflights: true, + name: "install infra error", + clientIgnoreHostPreflights: false, serverAllowIgnoreHostPreflights: true, - currentState: StatePreflightsFailed, - expectedState: StatePreflightsFailed, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + currentState: StatePreflightsSucceeded, + expectedState: StateInfrastructureInstallFailed, + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( - pm.On("GetHostPreflightOutput", t.Context()).Return(nil, errors.New("get output error")), + fm.On("Install", mock.Anything, rc).Return(errors.New("install error")), + st.InfraMockStore.On("GetStatus").Return(types.Status{Description: "install error"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("install error")), ) }, - expectedErr: errors.New("any error"), // Just check that an error occurs, don't care about exact message + expectedErr: nil, }, { - name: "install infra error", + name: "install infra error without report if infra store fails", clientIgnoreHostPreflights: false, serverAllowIgnoreHostPreflights: true, currentState: StatePreflightsSucceeded, - expectedState: StateFailed, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + expectedState: StateInfrastructureInstallFailed, + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { mock.InOrder( fm.On("Install", mock.Anything, rc).Return(errors.New("install error")), + st.InfraMockStore.On("GetStatus").Return(nil, assert.AnError), + ) + }, + expectedErr: nil, + }, + { + name: "install infra panic", + clientIgnoreHostPreflights: false, + serverAllowIgnoreHostPreflights: true, + currentState: StatePreflightsSucceeded, + expectedState: StateInfrastructureInstallFailed, + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + fm.On("Install", mock.Anything, rc).Panic("this is a panic"), + st.InfraMockStore.On("GetStatus").Return(types.Status{Description: "this is a panic"}, nil), + mr.On("ReportInstallationFailed", mock.Anything, errors.New("this is a panic")), ) }, expectedErr: nil, @@ -727,9 +1083,9 @@ func TestSetupInfra(t *testing.T) { serverAllowIgnoreHostPreflights: true, currentState: StateInstallationConfigured, expectedState: StateInstallationConfigured, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { }, - expectedErr: errors.New("invalid transition"), // Just check that an error occurs, don't care about exact message + expectedErr: assert.AnError, // Just check that an error occurs, don't care about exact message }, { name: "failed preflights with ignore flag but CLI flag disabled", @@ -737,7 +1093,7 @@ func TestSetupInfra(t *testing.T) { serverAllowIgnoreHostPreflights: false, currentState: StatePreflightsFailed, expectedState: StatePreflightsFailed, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { }, expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), }, @@ -747,7 +1103,7 @@ func TestSetupInfra(t *testing.T) { serverAllowIgnoreHostPreflights: false, currentState: StatePreflightsFailed, expectedState: StatePreflightsFailed, - setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { + setupMocks: func(rc runtimeconfig.RuntimeConfig, pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, mr *metrics.MockReporter, st *store.MockStore) { }, expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), }, @@ -765,7 +1121,8 @@ func TestSetupInfra(t *testing.T) { mockInstallationManager := &installation.MockInstallationManager{} mockInfraManager := &infra.MockInfraManager{} mockMetricsReporter := &metrics.MockReporter{} - tt.setupMocks(rc, mockPreflightManager, mockInstallationManager, mockInfraManager, mockMetricsReporter) + mockStore := &store.MockStore{} + tt.setupMocks(rc, mockPreflightManager, mockInstallationManager, mockInfraManager, mockMetricsReporter, mockStore) controller, err := NewInstallController( WithRuntimeConfig(rc), @@ -773,8 +1130,9 @@ func TestSetupInfra(t *testing.T) { WithHostPreflightManager(mockPreflightManager), WithInstallationManager(mockInstallationManager), WithInfraManager(mockInfraManager), - WithMetricsReporter(mockMetricsReporter), WithAllowIgnoreHostPreflights(tt.serverAllowIgnoreHostPreflights), + WithMetricsReporter(mockMetricsReporter), + WithStore(mockStore), ) require.NoError(t, err) @@ -799,14 +1157,18 @@ func TestSetupInfra(t *testing.T) { } assert.Eventually(t, func() bool { + t.Logf("Current state: %s, Expected state: %s", sm.CurrentState(), tt.expectedState) return sm.CurrentState() == tt.expectedState - }, time.Second, 100*time.Millisecond, "state should be %s but is %s", tt.expectedState, sm.CurrentState()) + }, time.Second, 100*time.Millisecond, "state should be %s", tt.expectedState) assert.False(t, sm.IsLockAcquired(), "state machine should not be locked after running infra setup") mockPreflightManager.AssertExpectations(t) mockInstallationManager.AssertExpectations(t) mockInfraManager.AssertExpectations(t) mockMetricsReporter.AssertExpectations(t) + mockStore.InfraMockStore.AssertExpectations(t) + mockStore.InstallationMockStore.AssertExpectations(t) + mockStore.PreflightMockStore.AssertExpectations(t) }) } } diff --git a/api/controllers/linux/install/hostpreflight.go b/api/controllers/linux/install/hostpreflight.go index 63f5bc087..33f7eb741 100644 --- a/api/controllers/linux/install/hostpreflight.go +++ b/api/controllers/linux/install/hostpreflight.go @@ -6,6 +6,7 @@ import ( "runtime/debug" "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg/netutils" @@ -61,16 +62,21 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP if r := recover(); r != nil { finalErr = fmt.Errorf("panic running host preflights: %v: %s", r, string(debug.Stack())) } + // Handle errors from preflight execution if finalErr != nil { c.logger.Error(finalErr) - if err := c.stateMachine.Transition(lock, StatePreflightsFailed); err != nil { - c.logger.Errorf("failed to transition states: %w", err) - } - } else { - if err := c.stateMachine.Transition(lock, StatePreflightsSucceeded); err != nil { + if err := c.stateMachine.Transition(lock, StatePreflightsExecutionFailed); err != nil { c.logger.Errorf("failed to transition states: %w", err) } + return + } + + // Get the state from the preflights output + state := c.getStateFromPreflightsOutput(ctx) + // Transition to the appropriate state based on preflight results + if err := c.stateMachine.Transition(lock, state); err != nil { + c.logger.Errorf("failed to transition states: %w", err) } }() @@ -87,6 +93,20 @@ func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostP return nil } +func (c *InstallController) getStateFromPreflightsOutput(ctx context.Context) statemachine.State { + output, err := c.GetHostPreflightOutput(ctx) + // If there was an error getting the state we assume preflight execution failed + if err != nil { + c.logger.WithError(err).Error("error getting preflight output") + return StatePreflightsExecutionFailed + } + // If there is no output, we assume preflights succeeded + if output == nil || !output.HasFail() { + return StatePreflightsSucceeded + } + return StatePreflightsFailed +} + func (c *InstallController) GetHostPreflightStatus(ctx context.Context) (types.Status, error) { return c.hostPreflightManager.GetHostPreflightStatus(ctx) } diff --git a/api/controllers/linux/install/infra.go b/api/controllers/linux/install/infra.go index 7d2f47f80..cf7adeb2f 100644 --- a/api/controllers/linux/install/infra.go +++ b/api/controllers/linux/install/infra.go @@ -10,18 +10,10 @@ import ( ) var ( - ErrPreflightChecksFailed = errors.New("preflight checks failed") - ErrPreflightChecksNotComplete = errors.New("preflight checks not complete") + ErrPreflightChecksFailed = errors.New("preflight checks failed") ) func (c *InstallController) SetupInfra(ctx context.Context, ignoreHostPreflights bool) (finalErr error) { - if c.stateMachine.CurrentState() == StatePreflightsFailed { - err := c.bypassPreflights(ctx, ignoreHostPreflights) - if err != nil { - return fmt.Errorf("bypass preflights: %w", err) - } - } - lock, err := c.stateMachine.AcquireLock() if err != nil { return types.NewConflictError(err) @@ -36,6 +28,17 @@ func (c *InstallController) SetupInfra(ctx context.Context, ignoreHostPreflights } }() + // Check if preflights have failed and if we should ignore them + if c.stateMachine.CurrentState() == StatePreflightsFailed { + if !ignoreHostPreflights || !c.allowIgnoreHostPreflights { + return types.NewBadRequestError(ErrPreflightChecksFailed) + } + err = c.stateMachine.Transition(lock, StatePreflightsFailedBypassed) + if err != nil { + return fmt.Errorf("failed to transition states: %w", err) + } + } + err = c.stateMachine.Transition(lock, StateInfrastructureInstalling) if err != nil { return types.NewConflictError(err) @@ -54,7 +57,7 @@ func (c *InstallController) SetupInfra(ctx context.Context, ignoreHostPreflights if finalErr != nil { c.logger.Error(finalErr) - if err := c.stateMachine.Transition(lock, StateFailed); err != nil { + if err := c.stateMachine.Transition(lock, StateInfrastructureInstallFailed); err != nil { c.logger.Errorf("failed to transition states: %w", err) } } else { @@ -74,41 +77,6 @@ func (c *InstallController) SetupInfra(ctx context.Context, ignoreHostPreflights return nil } -func (c *InstallController) bypassPreflights(ctx context.Context, ignoreHostPreflights bool) error { - if !ignoreHostPreflights || !c.allowIgnoreHostPreflights { - return types.NewBadRequestError(ErrPreflightChecksFailed) - } - - lock, err := c.stateMachine.AcquireLock() - if err != nil { - return types.NewConflictError(err) - } - defer lock.Release() - - if err := c.stateMachine.ValidateTransition(lock, StatePreflightsFailedBypassed); err != nil { - return types.NewConflictError(err) - } - - // TODO (@ethan): we have already sent the preflight output when we sent the failed event. - // We should evaluate if we should send it again. - preflightOutput, err := c.GetHostPreflightOutput(ctx) - if err != nil { - return fmt.Errorf("get install host preflight output: %w", err) - } - - // Report that preflights were bypassed - if preflightOutput != nil { - c.metricsReporter.ReportPreflightsBypassed(ctx, preflightOutput) - } - - err = c.stateMachine.Transition(lock, StatePreflightsFailedBypassed) - if err != nil { - return types.NewConflictError(err) - } - - return nil -} - func (c *InstallController) GetInfra(ctx context.Context) (types.Infra, error) { return c.infraManager.Get() } diff --git a/api/controllers/linux/install/installation.go b/api/controllers/linux/install/installation.go index 71b2e80c7..037e1665d 100644 --- a/api/controllers/linux/install/installation.go +++ b/api/controllers/linux/install/installation.go @@ -3,6 +3,7 @@ package install import ( "context" "fmt" + "time" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" @@ -48,6 +49,10 @@ func (c *InstallController) ConfigureInstallation(ctx context.Context, config ty if err != nil { c.logger.Error("failed to configure host", "error", err) + err = c.stateMachine.Transition(lock, StateHostConfigurationFailed) + if err != nil { + c.logger.Error("failed to transition states", "error", err) + } } else { err = c.stateMachine.Transition(lock, StateHostConfigured) if err != nil { @@ -59,7 +64,7 @@ func (c *InstallController) ConfigureInstallation(ctx context.Context, config ty return nil } -func (c *InstallController) configureInstallation(ctx context.Context, config types.InstallationConfig) error { +func (c *InstallController) configureInstallation(ctx context.Context, config types.InstallationConfig) (finalErr error) { lock, err := c.stateMachine.AcquireLock() if err != nil { return types.NewConflictError(err) @@ -70,6 +75,24 @@ func (c *InstallController) configureInstallation(ctx context.Context, config ty return types.NewConflictError(err) } + defer func() { + if finalErr != nil { + failureStatus := types.Status{ + State: types.StateFailed, + Description: finalErr.Error(), + LastUpdated: time.Now(), + } + + if err = c.store.InstallationStore().SetStatus(failureStatus); err != nil { + c.logger.Errorf("failed to update status: %w", err) + } + + if err := c.stateMachine.Transition(lock, StateInstallationConfigurationFailed); err != nil { + c.logger.Errorf("failed to transition states: %w", err) + } + } + }() + if err := c.installationManager.ValidateConfig(config, c.rc.ManagerPort()); err != nil { return fmt.Errorf("validate: %w", err) } diff --git a/api/controllers/linux/install/reporting_handlers.go b/api/controllers/linux/install/reporting_handlers.go new file mode 100644 index 000000000..7c8f49787 --- /dev/null +++ b/api/controllers/linux/install/reporting_handlers.go @@ -0,0 +1,69 @@ +package install + +import ( + "context" + "errors" + "fmt" + + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" + "github.com/replicatedhq/embedded-cluster/api/types" +) + +func (c *InstallController) registerReportingHandlers() { + c.stateMachine.RegisterEventHandler(StateSucceeded, c.reportInstallSucceeded) + c.stateMachine.RegisterEventHandler(StateInfrastructureInstallFailed, c.reportInstallFailed) + c.stateMachine.RegisterEventHandler(StateHostConfigurationFailed, c.reportInstallFailed) + c.stateMachine.RegisterEventHandler(StateInstallationConfigurationFailed, c.reportInstallFailed) + c.stateMachine.RegisterEventHandler(StatePreflightsFailed, c.reportPreflightsFailed) + c.stateMachine.RegisterEventHandler(StatePreflightsFailedBypassed, c.reportPreflightsBypassed) +} + +func (c *InstallController) reportInstallSucceeded(ctx context.Context, _, _ statemachine.State) { + c.metricsReporter.ReportInstallationSucceeded(ctx) +} + +func (c *InstallController) reportInstallFailed(ctx context.Context, _, toState statemachine.State) { + var status types.Status + var err error + + switch toState { + case StateInstallationConfigurationFailed: + status, err = c.store.InstallationStore().GetStatus() + if err != nil { + err = fmt.Errorf("failed to get status from installation store: %w", err) + } + case StateHostConfigurationFailed: + status, err = c.store.InstallationStore().GetStatus() + if err != nil { + err = fmt.Errorf("failed to get status from installation store: %w", err) + } + case StateInfrastructureInstallFailed: + status, err = c.store.InfraStore().GetStatus() + if err != nil { + err = fmt.Errorf("failed to get status from infra store: %w", err) + } + } + if err != nil { + c.logger.WithError(err).Error("failed to report failled install") + return + } + c.metricsReporter.ReportInstallationFailed(ctx, errors.New(status.Description)) +} + +func (c *InstallController) reportPreflightsFailed(ctx context.Context, _, _ statemachine.State) { + output, err := c.store.PreflightStore().GetOutput() + if err != nil { + c.logger.WithError(fmt.Errorf("failed to get output from preflight store: %w", err)).Error("failed to report preflights failed") + return + } + c.metricsReporter.ReportPreflightsFailed(ctx, output) +} + +func (c *InstallController) reportPreflightsBypassed(ctx context.Context, _, _ statemachine.State) { + output, err := c.store.PreflightStore().GetOutput() + if err != nil { + c.logger.WithError(fmt.Errorf("failed to get output from preflight store: %w", err)).Error("failed to report preflights bypassed") + return + } + c.metricsReporter.ReportPreflightsBypassed(ctx, output) +} diff --git a/api/controllers/linux/install/statemachine.go b/api/controllers/linux/install/statemachine.go index 10b3928c2..ece8fbb6d 100644 --- a/api/controllers/linux/install/statemachine.go +++ b/api/controllers/linux/install/statemachine.go @@ -1,45 +1,59 @@ package install -import "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" +import ( + "github.com/replicatedhq/embedded-cluster/api/internal/statemachine" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/sirupsen/logrus" +) const ( // StateNew is the initial state of the install process StateNew statemachine.State = "New" + // StateInstallationConfigurationFailed is the state of the install process when the installation failed to be configured + StateInstallationConfigurationFailed statemachine.State = "InstallationConfigurationFailed" // StateInstallationConfigured is the state of the install process when the installation is configured StateInstallationConfigured statemachine.State = "InstallationConfigured" + // StateHostConfigurationFailed is the state of the install process when the installation failed to be configured + StateHostConfigurationFailed statemachine.State = "HostConfigurationFailed" // StateHostConfigured is the state of the install process when the host is configured StateHostConfigured statemachine.State = "HostConfigured" // StatePreflightsRunning is the state of the install process when the preflights are running StatePreflightsRunning statemachine.State = "PreflightsRunning" + // StatePreflightsExecutionFailed is the state of the install process when the preflights failed to execute due to an underlying system error + StatePreflightsExecutionFailed statemachine.State = "PreflightsExecutionFailed" // StatePreflightsSucceeded is the state of the install process when the preflights have succeeded StatePreflightsSucceeded statemachine.State = "PreflightsSucceeded" - // StatePreflightsFailed is the state of the install process when the preflights have failed + // StatePreflightsFailed is the state of the install process when the preflights execution succeeded but the preflights detected issues on the host StatePreflightsFailed statemachine.State = "PreflightsFailed" - // StatePreflightsFailedBypassed is the state of the install process when the preflights have failed bypassed + // StatePreflightsFailedBypassed is the state of the install process when, despite preflights failing, the user has chosen to bypass the preflights and continue with the installation StatePreflightsFailedBypassed statemachine.State = "PreflightsFailedBypassed" // StateInfrastructureInstalling is the state of the install process when the infrastructure is being installed StateInfrastructureInstalling statemachine.State = "InfrastructureInstalling" + // StateInfrastructureInstallFailed is a final state of the install process when the infrastructure failed to isntall + StateInfrastructureInstallFailed statemachine.State = "InfrastructureInstallFailed" // StateSucceeded is the final state of the install process when the install has succeeded StateSucceeded statemachine.State = "Succeeded" - // StateFailed is the final state of the install process when the install has failed - StateFailed statemachine.State = "Failed" ) var validStateTransitions = map[statemachine.State][]statemachine.State{ - StateNew: {StateInstallationConfigured}, - StateInstallationConfigured: {StateHostConfigured, StateInstallationConfigured}, - StateHostConfigured: {StatePreflightsRunning, StateInstallationConfigured}, - StatePreflightsRunning: {StatePreflightsSucceeded, StatePreflightsFailed}, - StatePreflightsSucceeded: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured}, - StatePreflightsFailed: {StatePreflightsFailedBypassed, StatePreflightsRunning, StateInstallationConfigured}, - StatePreflightsFailedBypassed: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured}, - StateInfrastructureInstalling: {StateSucceeded, StateFailed}, - StateSucceeded: {}, - StateFailed: {}, + StateNew: {StateInstallationConfigured, StateInstallationConfigurationFailed}, + StateInstallationConfigurationFailed: {StateInstallationConfigured, StateInstallationConfigurationFailed}, + StateInstallationConfigured: {StateHostConfigured, StateHostConfigurationFailed}, + StateHostConfigurationFailed: {StateInstallationConfigured, StateInstallationConfigurationFailed}, + StateHostConfigured: {StatePreflightsRunning, StateInstallationConfigured, StateInstallationConfigurationFailed}, + StatePreflightsRunning: {StatePreflightsSucceeded, StatePreflightsFailed, StatePreflightsExecutionFailed}, + StatePreflightsExecutionFailed: {StatePreflightsRunning, StateInstallationConfigured, StateInstallationConfigurationFailed}, + StatePreflightsSucceeded: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured, StateInstallationConfigurationFailed}, + StatePreflightsFailed: {StatePreflightsFailedBypassed, StatePreflightsRunning, StateInstallationConfigured, StateInstallationConfigurationFailed}, + StatePreflightsFailedBypassed: {StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured, StateInstallationConfigurationFailed}, + StateInfrastructureInstalling: {StateSucceeded, StateInfrastructureInstallFailed}, + StateInfrastructureInstallFailed: {}, + StateSucceeded: {}, } type StateMachineOptions struct { CurrentState statemachine.State + Logger logrus.FieldLogger } type StateMachineOption func(*StateMachineOptions) @@ -50,13 +64,20 @@ func WithCurrentState(currentState statemachine.State) StateMachineOption { } } +func WithStateMachineLogger(logger logrus.FieldLogger) StateMachineOption { + return func(o *StateMachineOptions) { + o.Logger = logger + } +} + // NewStateMachine creates a new state machine starting in the New state func NewStateMachine(opts ...StateMachineOption) statemachine.Interface { options := &StateMachineOptions{ CurrentState: StateNew, + Logger: logger.NewDiscardLogger(), } for _, opt := range opts { opt(options) } - return statemachine.New(options.CurrentState, validStateTransitions) + return statemachine.New(options.CurrentState, validStateTransitions, statemachine.WithLogger(options.Logger)) } diff --git a/api/controllers/linux/install/statemachine_test.go b/api/controllers/linux/install/statemachine_test.go index 48ab5c882..508b6a521 100644 --- a/api/controllers/linux/install/statemachine_test.go +++ b/api/controllers/linux/install/statemachine_test.go @@ -15,79 +15,110 @@ func TestStateMachineTransitions(t *testing.T) { validTransitions []statemachine.State }{ { - name: `State "New" can transition to "InstallationConfigured"`, + name: `State "New" can transition to "InstallationConfigured" or "InstallationConfigurationFailed"`, startState: StateNew, validTransitions: []statemachine.State{ StateInstallationConfigured, + StateInstallationConfigurationFailed, }, }, { - name: `State "InstallationConfigured" can transition to "HostConfigured" or "InstallationConfigured"`, + name: `State "InstallationConfigurationFailed" can transition to "InstallationConfigured" or "InstallationConfigurationFailed"`, + startState: StateInstallationConfigurationFailed, + validTransitions: []statemachine.State{ + StateInstallationConfigured, + StateInstallationConfigurationFailed, + }, + }, + { + name: `State "InstallationConfigured" can transition to "HostConfigured" or "HostConfigurationFailed"`, startState: StateInstallationConfigured, validTransitions: []statemachine.State{ StateHostConfigured, + StateHostConfigurationFailed, + }, + }, + { + name: `State "HostConfigurationFailed" can transition to "InstallationConfigured" or "InstallationConfigurationFailed"`, + startState: StateHostConfigurationFailed, + validTransitions: []statemachine.State{ StateInstallationConfigured, + StateInstallationConfigurationFailed, }, }, { - name: `State "HostConfigured" can transition to "PreflightsRunning" or "InstallationConfigured"`, + name: `State "HostConfigured" can transition to "PreflightsRunning" or "InstallationConfigured" or "InstallationConfigurationFailed"`, startState: StateHostConfigured, validTransitions: []statemachine.State{ StatePreflightsRunning, StateInstallationConfigured, + StateInstallationConfigurationFailed, }, }, { - name: `State "PreflightsRunning" can transition to "PreflightsSucceeded" or "PreflightsFailed"`, + name: `State "PreflightsRunning" can transition to "PreflightsSucceeded", "PreflightsFailed", or "PreflightsExecutionFailed"`, startState: StatePreflightsRunning, validTransitions: []statemachine.State{ StatePreflightsSucceeded, StatePreflightsFailed, + StatePreflightsExecutionFailed, }, }, { - name: `State "PreflightsSucceeded" can transition to "InfrastructureInstalling", "PreflightsRunning" or "InstallationConfigured"`, + name: `State "PreflightsExecutionFailed" can transition to "PreflightsRunning", "InstallationConfigured", or "InstallationConfigurationFailed"`, + startState: StatePreflightsExecutionFailed, + validTransitions: []statemachine.State{ + StatePreflightsRunning, + StateInstallationConfigured, + StateInstallationConfigurationFailed, + }, + }, + { + name: `State "PreflightsSucceeded" can transition to "InfrastructureInstalling", "PreflightsRunning", "InstallationConfigured", or "InstallationConfigurationFailed"`, startState: StatePreflightsSucceeded, validTransitions: []statemachine.State{ StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured, + StateInstallationConfigurationFailed, }, }, { - name: `State "PreflightsFailed" can transition to "PreflightsFailedBypassed" , "PreflightsRunning" or "InstallationConfigured"`, + name: `State "PreflightsFailed" can transition to "PreflightsFailedBypassed", "PreflightsRunning", "InstallationConfigured", or "InstallationConfigurationFailed"`, startState: StatePreflightsFailed, validTransitions: []statemachine.State{ StatePreflightsFailedBypassed, StatePreflightsRunning, StateInstallationConfigured, + StateInstallationConfigurationFailed, }, }, { - name: `State "PreflightsFailedBypassed" can transition to "InfrastructureInstalling", "PreflightsRunning" or "InstallationConfigured"`, + name: `State "PreflightsFailedBypassed" can transition to "InfrastructureInstalling", "PreflightsRunning", "InstallationConfigured", or "InstallationConfigurationFailed"`, startState: StatePreflightsFailedBypassed, validTransitions: []statemachine.State{ StateInfrastructureInstalling, StatePreflightsRunning, StateInstallationConfigured, + StateInstallationConfigurationFailed, }, }, { - name: `State "InfrastructureInstalling" can transition to "Succeeded" or "Failed"`, + name: `State "InfrastructureInstalling" can transition to "Succeeded" or "InfrastructureInstallFailed"`, startState: StateInfrastructureInstalling, validTransitions: []statemachine.State{ StateSucceeded, - StateFailed, + StateInfrastructureInstallFailed, }, }, { - name: `State "Succeeded" can not transition to any other state`, - startState: StateSucceeded, + name: `State "InfrastructureInstallFailed" can not transition to any other state`, + startState: StateInfrastructureInstallFailed, validTransitions: []statemachine.State{}, }, { - name: `State "Failed" can not transition to any other state`, - startState: StateFailed, + name: `State "Succeeded" can not transition to any other state`, + startState: StateSucceeded, validTransitions: []statemachine.State{}, }, } @@ -120,7 +151,7 @@ func TestStateMachineTransitions(t *testing.T) { func TestIsFinalState(t *testing.T) { finalStates := []statemachine.State{ StateSucceeded, - StateFailed, + StateInfrastructureInstallFailed, } for state := range validStateTransitions { diff --git a/api/integration/install_test.go b/api/integration/install_test.go index 797b273f7..f71655e95 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -6,6 +6,7 @@ import ( _ "embed" "encoding/json" "errors" + "fmt" "net" "net/http" "net/http/httptest" @@ -18,7 +19,6 @@ import ( k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "github.com/replicatedhq/embedded-cluster/api" apiclient "github.com/replicatedhq/embedded-cluster/api/client" - "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" linuxinstall "github.com/replicatedhq/embedded-cluster/api/controllers/linux/install" "github.com/replicatedhq/embedded-cluster/api/internal/managers/infra" "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" @@ -137,7 +137,8 @@ func TestConfigureInstallation(t *testing.T) { mockNetUtils *utils.MockNetUtils token string config types.InstallationConfig - expectedStatus int + expectedStatus *types.Status + expectedStatusCode int expectedError bool validateRuntimeConfig func(t *testing.T, rc runtimeconfig.RuntimeConfig) }{ @@ -169,8 +170,12 @@ func TestConfigureInstallation(t *testing.T) { GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", }, - expectedStatus: http.StatusOK, - expectedError: false, + expectedStatus: &types.Status{ + State: types.StateSucceeded, + Description: "Installation configured", + }, + expectedStatusCode: http.StatusOK, + expectedError: false, validateRuntimeConfig: func(t *testing.T, rc runtimeconfig.RuntimeConfig) { assert.Equal(t, "/tmp/data", rc.EmbeddedClusterHomeDirectory()) assert.Equal(t, 8000, rc.AdminConsolePort()) @@ -223,8 +228,12 @@ func TestConfigureInstallation(t *testing.T) { HTTPSProxy: "https://proxy.example.com", NoProxy: "somecompany.internal,192.168.17.0/24", }, - expectedStatus: http.StatusOK, - expectedError: false, + expectedStatus: &types.Status{ + State: types.StateSucceeded, + Description: "Installation configured", + }, + expectedStatusCode: http.StatusOK, + expectedError: false, validateRuntimeConfig: func(t *testing.T, rc runtimeconfig.RuntimeConfig) { assert.Equal(t, "/tmp/data", rc.EmbeddedClusterHomeDirectory()) assert.Equal(t, 8000, rc.AdminConsolePort()) @@ -256,17 +265,21 @@ func TestConfigureInstallation(t *testing.T) { GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", }, - expectedStatus: http.StatusBadRequest, - expectedError: true, + expectedStatus: &types.Status{ + State: types.StateFailed, + Description: "validate: field errors: adminConsolePort and localArtifactMirrorPort cannot be equal", + }, + expectedStatusCode: http.StatusBadRequest, + expectedError: true, }, { - name: "Unauthorized", - mockHostUtils: &hostutils.MockHostUtils{}, - mockNetUtils: &utils.MockNetUtils{}, - token: "NOT_A_TOKEN", - config: types.InstallationConfig{}, - expectedStatus: http.StatusUnauthorized, - expectedError: true, + name: "Unauthorized", + mockHostUtils: &hostutils.MockHostUtils{}, + mockNetUtils: &utils.MockNetUtils{}, + token: "NOT_A_TOKEN", + config: types.InstallationConfig{}, + expectedStatusCode: http.StatusUnauthorized, + expectedError: true, }, } @@ -278,7 +291,7 @@ func TestConfigureInstallation(t *testing.T) { // Create an install controller with the config manager installController, err := linuxinstall.NewInstallController( linuxinstall.WithRuntimeConfig(rc), - linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured))), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateNew))), linuxinstall.WithHostUtils(tc.mockHostUtils), linuxinstall.WithNetUtils(tc.mockNetUtils), ) @@ -313,7 +326,7 @@ func TestConfigureInstallation(t *testing.T) { router.ServeHTTP(rec, req) // Check the response - assert.Equal(t, tc.expectedStatus, rec.Code) + assert.Equal(t, tc.expectedStatusCode, rec.Code) t.Logf("Response body: %s", rec.Body.String()) @@ -322,7 +335,7 @@ func TestConfigureInstallation(t *testing.T) { var apiError types.APIError err = json.NewDecoder(rec.Body).Decode(&apiError) require.NoError(t, err) - assert.Equal(t, tc.expectedStatus, apiError.StatusCode) + assert.Equal(t, tc.expectedStatusCode, apiError.StatusCode) assert.NotEmpty(t, apiError.Message) } else { var status types.Status @@ -334,13 +347,16 @@ func TestConfigureInstallation(t *testing.T) { assert.NotEqual(t, types.StatePending, status.State) } - if !tc.expectedError { - // The status is set to succeeded in a goroutine, so we need to wait for it + // We might not have an expected status if the test is expected to fail before running the controller logic + if tc.expectedStatus != nil { + // The status is set in a goroutine, so we need to wait for it + var status types.Status assert.Eventually(t, func() bool { - status, err := installController.GetInstallationStatus(t.Context()) + status, err = installController.GetInstallationStatus(t.Context()) require.NoError(t, err) - return status.State == types.StateSucceeded && status.Description == "Installation configured" - }, 1*time.Second, 100*time.Millisecond, "status should eventually be succeeded") + return status.State == tc.expectedStatus.State + }, 1*time.Second, 100*time.Millisecond, fmt.Sprintf("Expected status to be %s", tc.expectedStatus.State)) + assert.Contains(t, status.Description, tc.expectedStatus.Description) } if !tc.expectedError { @@ -1409,10 +1425,10 @@ func TestPostSetupInfra(t *testing.T) { ) // Create an install controller with CLI flag allowing bypass - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsFailed))), - install.WithHostPreflightManager(pfManager), - install.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsFailed))), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass ) require.NoError(t, err) @@ -1466,10 +1482,10 @@ func TestPostSetupInfra(t *testing.T) { ) // Create an install controller with CLI flag NOT allowing bypass - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsFailed))), - install.WithHostPreflightManager(pfManager), - install.WithAllowIgnoreHostPreflights(false), // CLI flag does NOT allow bypass + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsFailed))), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithAllowIgnoreHostPreflights(false), // CLI flag does NOT allow bypass ) require.NoError(t, err) @@ -1530,10 +1546,10 @@ func TestPostSetupInfra(t *testing.T) { ) // Create an install controller with CLI flag allowing bypass - installController, err := install.NewInstallController( - install.WithStateMachine(install.NewStateMachine(install.WithCurrentState(install.StatePreflightsFailed))), - install.WithHostPreflightManager(pfManager), - install.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsFailed))), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass ) require.NoError(t, err) diff --git a/api/internal/managers/preflight/hostpreflight.go b/api/internal/managers/preflight/hostpreflight.go index e0496fccd..001ee3322 100644 --- a/api/internal/managers/preflight/hostpreflight.go +++ b/api/internal/managers/preflight/hostpreflight.go @@ -105,13 +105,8 @@ func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, rc runtime m.logger.WithField("error", err).Warn("copy preflight bundle to embedded-cluster support dir") } - if output.HasFail() || output.HasWarn() { - if m.metricsReporter != nil { - m.metricsReporter.ReportPreflightsFailed(ctx, output) - } - } - // Set final status based on results + // TODO @jgantunes: we're currently not handling warnings in the output. if output.HasFail() { if err := m.setCompletedStatus(types.StateFailed, "Host preflights failed", output); err != nil { return fmt.Errorf("set failed status: %w", err) diff --git a/api/internal/managers/preflight/hostpreflight_test.go b/api/internal/managers/preflight/hostpreflight_test.go index 158dc15d0..d11dd453e 100644 --- a/api/internal/managers/preflight/hostpreflight_test.go +++ b/api/internal/managers/preflight/hostpreflight_test.go @@ -16,7 +16,6 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" - "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" ) @@ -204,7 +203,6 @@ func TestHostPreflightManager_PrepareHostPreflights(t *testing.T) { // Setup mocks mockRunner := &preflights.MockPreflightRunner{} mockStore := &preflightstore.MockStore{} - mockMetrics := &metrics.MockReporter{} mockNetUtils := &utils.MockNetUtils{} // Create real runtime config @@ -217,7 +215,6 @@ func TestHostPreflightManager_PrepareHostPreflights(t *testing.T) { WithPreflightRunner(mockRunner), WithHostPreflightStore(mockStore), WithLogger(logger.NewDiscardLogger()), - WithMetricsReporter(mockMetrics), WithNetUtils(mockNetUtils), ) @@ -249,7 +246,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { name string opts RunHostPreflightOptions initialState types.HostPreflights - setupMocks func(*preflights.MockPreflightRunner, *metrics.MockReporter, runtimeconfig.RuntimeConfig) + setupMocks func(*preflights.MockPreflightRunner, runtimeconfig.RuntimeConfig) expectedFinalState types.State // This is the expected error message returned by the RunHostPreflights method, synchronously expectedError string @@ -264,7 +261,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { State: types.StatePending, }, }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(runner *preflights.MockPreflightRunner, rc runtimeconfig.RuntimeConfig) { // Mock successful preflight execution output := &types.HostPreflightsOutput{} runner.On("Run", mock.Anything, mock.Anything, rc).Return(output, "", nil) @@ -285,7 +282,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { State: types.StatePending, }, }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(runner *preflights.MockPreflightRunner, rc runtimeconfig.RuntimeConfig) { // Mock failed preflight execution output := &types.HostPreflightsOutput{ Fail: []types.HostPreflightsRecord{{ @@ -299,9 +296,6 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { // Mock save operations runner.On("SaveToDisk", output, rc.PathToEmbeddedClusterSupportFile("host-preflight-results.json")).Return(nil) runner.On("CopyBundleTo", rc.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz")).Return(nil) - - // Mock metrics reporting - metricsReporter.On("ReportPreflightsFailed", mock.Anything, output).Return() }, expectedFinalState: types.StateFailed, }, @@ -315,7 +309,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { opts: RunHostPreflightOptions{ HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(runner *preflights.MockPreflightRunner, rc runtimeconfig.RuntimeConfig) { // Mock preflight execution with warnings output := &types.HostPreflightsOutput{ Warn: []types.HostPreflightsRecord{{ @@ -328,9 +322,6 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { // Mock save operations runner.On("SaveToDisk", output, rc.PathToEmbeddedClusterSupportFile("host-preflight-results.json")).Return(nil) runner.On("CopyBundleTo", rc.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz")).Return(nil) - - // Mock metrics reporting - metricsReporter.On("ReportPreflightsFailed", mock.Anything, output).Return() }, expectedFinalState: types.StateSucceeded, }, @@ -344,7 +335,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { opts: RunHostPreflightOptions{ HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(runner *preflights.MockPreflightRunner, rc runtimeconfig.RuntimeConfig) { // Mock preflight execution with both failures and warnings output := &types.HostPreflightsOutput{ Fail: []types.HostPreflightsRecord{{ @@ -361,9 +352,6 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { // Mock save operations runner.On("SaveToDisk", output, rc.PathToEmbeddedClusterSupportFile("host-preflight-results.json")).Return(nil) runner.On("CopyBundleTo", rc.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz")).Return(nil) - - // Mock metrics reporting - metricsReporter.On("ReportPreflightsFailed", mock.Anything, output).Return() }, expectedFinalState: types.StateFailed, }, @@ -377,7 +365,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { opts: RunHostPreflightOptions{ HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(runner *preflights.MockPreflightRunner, rc runtimeconfig.RuntimeConfig) { // Mock runner failure runner.On("Run", mock.Anything, mock.Anything, rc).Return(nil, "stderr output", assert.AnError) }, @@ -393,7 +381,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { opts: RunHostPreflightOptions{ HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(runner *preflights.MockPreflightRunner, rc runtimeconfig.RuntimeConfig) { // Mock successful preflight execution output := &types.HostPreflightsOutput{} runner.On("Run", mock.Anything, mock.Anything, rc).Return(output, "", nil) @@ -414,7 +402,7 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { opts: RunHostPreflightOptions{ HostPreflightSpec: &troubleshootv1beta2.HostPreflightSpec{}, }, - setupMocks: func(runner *preflights.MockPreflightRunner, metricsReporter *metrics.MockReporter, rc runtimeconfig.RuntimeConfig) { + setupMocks: func(runner *preflights.MockPreflightRunner, rc runtimeconfig.RuntimeConfig) { // Mock successful preflight execution output := &types.HostPreflightsOutput{} runner.On("Run", mock.Anything, mock.Anything, rc).Return(output, "", nil) @@ -431,20 +419,18 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Setup mocks mockRunner := &preflights.MockPreflightRunner{} - mockMetrics := &metrics.MockReporter{} // Create runtime config rc := runtimeconfig.New(nil) rc.SetDataDir(t.TempDir()) - tt.setupMocks(mockRunner, mockMetrics, rc) + tt.setupMocks(mockRunner, rc) // Create manager using builder pattern manager := NewHostPreflightManager( WithPreflightRunner(mockRunner), WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(tt.initialState))), WithLogger(logger.NewDiscardLogger()), - WithMetricsReporter(mockMetrics), ) // Execute @@ -463,7 +449,6 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { // Additional verification that calls were made in the correct order mockRunner.AssertExpectations(t) - mockMetrics.AssertExpectations(t) }) } } diff --git a/api/internal/managers/preflight/manager.go b/api/internal/managers/preflight/manager.go index 82f9e7e75..ff4bfefdf 100644 --- a/api/internal/managers/preflight/manager.go +++ b/api/internal/managers/preflight/manager.go @@ -8,7 +8,6 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" - "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/sirupsen/logrus" @@ -28,7 +27,6 @@ type hostPreflightManager struct { runner preflights.PreflightsRunnerInterface netUtils utils.NetUtils logger logrus.FieldLogger - metricsReporter metrics.ReporterInterface } type HostPreflightManagerOption func(*hostPreflightManager) @@ -39,12 +37,6 @@ func WithLogger(logger logrus.FieldLogger) HostPreflightManagerOption { } } -func WithMetricsReporter(metricsReporter metrics.ReporterInterface) HostPreflightManagerOption { - return func(m *hostPreflightManager) { - m.metricsReporter = metricsReporter - } -} - func WithHostPreflightStore(hostPreflightStore preflight.Store) HostPreflightManagerOption { return func(m *hostPreflightManager) { m.hostPreflightStore = hostPreflightStore diff --git a/api/internal/store/store_mock.go b/api/internal/store/store_mock.go new file mode 100644 index 000000000..8c56a560b --- /dev/null +++ b/api/internal/store/store_mock.go @@ -0,0 +1,31 @@ +package store + +import ( + "github.com/replicatedhq/embedded-cluster/api/internal/store/infra" + "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/store/preflight" +) + +var _ Store = (*MockStore)(nil) + +// MockStore is a mock implementation of the Store interface +type MockStore struct { + PreflightMockStore preflight.MockStore + InfraMockStore infra.MockStore + InstallationMockStore installation.MockStore +} + +// PreflightStore returns the mock preflight store +func (m *MockStore) PreflightStore() preflight.Store { + return &m.PreflightMockStore +} + +// InstallationStore returns the mock installation store +func (m *MockStore) InstallationStore() installation.Store { + return &m.InstallationMockStore +} + +// InfraStore returns the mock infra store +func (m *MockStore) InfraStore() infra.Store { + return &m.InfraMockStore +} diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index e884b9a6e..4ed8e46db 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -125,12 +125,6 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { return err } - if flags.enableManagerExperience { - return runManagerExperienceInstall(ctx, flags, rc) - } - - _ = rc.SetEnv() - clusterID := metrics.ClusterID() installReporter := newInstallReporter( replicatedAppURL(), clusterID, cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), @@ -138,6 +132,12 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { ) installReporter.ReportInstallationStarted(ctx) + if flags.enableManagerExperience { + return runManagerExperienceInstall(ctx, flags, rc, installReporter) + } + + _ = rc.SetEnv() + // Setup signal handler with the metrics reporter cleanup function signalHandler(ctx, cancel, func(ctx context.Context, sig os.Signal) { installReporter.ReportSignalAborted(ctx, sig) @@ -545,7 +545,7 @@ func cidrConfigFromCmd(cmd *cobra.Command) (*newconfig.CIDRConfig, error) { return cidrCfg, nil } -func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) (finalErr error) { +func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, installReporter *InstallReporter) (finalErr error) { // 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() @@ -602,11 +602,10 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc } apiConfig := apiOptions{ - InstallTarget: flags.target, - RuntimeConfig: rc, - // TODO (@salah): implement reporting in api - // MetricsReporter: installReporter, - Password: flags.adminConsolePassword, + InstallTarget: flags.target, + RuntimeConfig: rc, + MetricsReporter: installReporter.reporter, + Password: flags.adminConsolePassword, TLSConfig: apitypes.TLSConfig{ CertBytes: flags.tlsCertBytes, KeyBytes: flags.tlsKeyBytes, diff --git a/pkg/metrics/reporter_mock.go b/pkg/metrics/reporter_mock.go index e6c4368b4..9fdff200a 100644 --- a/pkg/metrics/reporter_mock.go +++ b/pkg/metrics/reporter_mock.go @@ -15,47 +15,49 @@ type MockReporter struct { mock.Mock } +// TODO: all the methods in this file aren't passing over the context.Context to avoid potential data races when using this struct in state machine event handler tests. See: https://github.com/stretchr/testify/issues/1597 + // ReportInstallationStarted mocks the ReportInstallationStarted method func (m *MockReporter) ReportInstallationStarted(ctx context.Context, licenseID string, appSlug string) { - m.Called(ctx, licenseID, appSlug) + m.Called(mock.Anything, licenseID, appSlug) } // ReportInstallationSucceeded mocks the ReportInstallationSucceeded method func (m *MockReporter) ReportInstallationSucceeded(ctx context.Context) { - m.Called(ctx) + m.Called(mock.Anything) } // ReportInstallationFailed mocks the ReportInstallationFailed method func (m *MockReporter) ReportInstallationFailed(ctx context.Context, err error) { - m.Called(ctx, err) + m.Called(mock.Anything, err) } // ReportJoinStarted mocks the ReportJoinStarted method func (m *MockReporter) ReportJoinStarted(ctx context.Context) { - m.Called(ctx) + m.Called(mock.Anything) } // ReportJoinSucceeded mocks the ReportJoinSucceeded method func (m *MockReporter) ReportJoinSucceeded(ctx context.Context) { - m.Called(ctx) + m.Called(mock.Anything) } // ReportJoinFailed mocks the ReportJoinFailed method func (m *MockReporter) ReportJoinFailed(ctx context.Context, err error) { - m.Called(ctx, err) + m.Called(mock.Anything, err) } // ReportPreflightsFailed mocks the ReportPreflightsFailed method func (m *MockReporter) ReportPreflightsFailed(ctx context.Context, output *apitypes.HostPreflightsOutput) { - m.Called(ctx, output) + m.Called(mock.Anything, output) } // ReportPreflightsBypassed mocks the ReportPreflightsBypassed method func (m *MockReporter) ReportPreflightsBypassed(ctx context.Context, output *apitypes.HostPreflightsOutput) { - m.Called(ctx, output) + m.Called(mock.Anything, output) } // ReportSignalAborted mocks the ReportSignalAborted method func (m *MockReporter) ReportSignalAborted(ctx context.Context, signal os.Signal) { - m.Called(ctx, signal) + m.Called(mock.Anything, signal) } From 87275e41eaf7a6978f2d77af4949ae763fe838b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Antunes?= Date: Thu, 26 Jun 2025 22:29:34 +0100 Subject: [PATCH 40/48] chore(test): fix web unit test flake (#2377) --- .../wizard/tests/ValidationStep.test.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web/src/components/wizard/tests/ValidationStep.test.tsx b/web/src/components/wizard/tests/ValidationStep.test.tsx index 404396716..998420e9b 100644 --- a/web/src/components/wizard/tests/ValidationStep.test.tsx +++ b/web/src/components/wizard/tests/ValidationStep.test.tsx @@ -70,7 +70,7 @@ describe('ValidationStep', () => { // Wait for preflights to complete and show failures await waitFor(() => { expect(screen.getByText('Host Requirements Not Met')).toBeInTheDocument(); - }); + }, { timeout: 2000 }); // Button should be enabled when CLI flag allows ignoring failures const nextButton = screen.getByText('Next: Start Installation'); @@ -294,10 +294,10 @@ describe('ValidationStep', () => { // Mock infra setup endpoint to capture request body http.post('*/api/linux/install/infra/setup', async ({ request }) => { const body = await request.json(); - + // Verify the request includes ignoreHostPreflights parameter expect(body).toHaveProperty('ignoreHostPreflights', true); - + return HttpResponse.json({ success: true }); }) ); @@ -348,10 +348,10 @@ describe('ValidationStep', () => { // Mock infra setup endpoint to capture request body http.post('*/api/linux/install/infra/setup', async ({ request }) => { const body = await request.json(); - + // Verify the request includes ignoreHostPreflights parameter as false expect(body).toHaveProperty('ignoreHostPreflights', false); - + return HttpResponse.json({ success: true }); }) ); @@ -404,9 +404,9 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { // Mock infra setup endpoint to return API error http.post('*/api/linux/install/infra/setup', () => { return HttpResponse.json( - { + { statusCode: 400, - message: 'Preflight checks failed. Cannot proceed with installation.' + message: 'Preflight checks failed. Cannot proceed with installation.' }, { status: 400 } ); @@ -441,7 +441,7 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { server.use( http.get('*/api/linux/install/host-preflights/status', () => { return HttpResponse.json({ - titles: ['Host Check'], + titles: ['Host Check'], status: { state: 'Succeeded' }, output: { fail: [], warn: [], pass: [{ title: 'Test', message: 'Pass' }] }, allowIgnoreHostPreflights: false @@ -534,9 +534,9 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { // Mock infra setup endpoint to return CLI flag error http.post('*/api/linux/install/infra/setup', () => { return HttpResponse.json( - { + { statusCode: 400, - message: 'preflight checks failed' + message: 'preflight checks failed' }, { status: 400 } ); @@ -680,4 +680,4 @@ describe('ValidationStep - Error Handling & Edge Cases', () => { // Button should still be available for another attempt expect(screen.getByText('Next: Start Installation')).toBeInTheDocument(); }); -}); \ No newline at end of file +}); From 21f4438d3e37ab1c0fcce93ae70c730a4e1aec81 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 26 Jun 2025 18:34:49 -0700 Subject: [PATCH 41/48] chore(api): pass kubernetes configuration (#2376) --- api/integration/hostpreflights_test.go | 6 +- api/types/api.go | 27 +- cmd/installer/cli/api.go | 45 +-- cmd/installer/cli/api_test.go | 17 +- cmd/installer/cli/install.go | 82 ++-- cmd/installer/cli/install_runpreflights.go | 5 +- cmd/installer/cli/restore.go | 4 +- .../embedded-cluster-operator/values.yaml | 3 + ...eplicated.com_kubernetesinstallations.yaml | 323 ++++++++++++++++ ...sinstallation-embeddedcluster-v1beta1.json | 364 ++++++++++++++++++ 10 files changed, 799 insertions(+), 77 deletions(-) create mode 100644 operator/config/crd/bases/embeddedcluster.replicated.com_kubernetesinstallations.yaml create mode 100644 operator/schemas/kubernetesinstallation-embeddedcluster-v1beta1.json diff --git a/api/integration/hostpreflights_test.go b/api/integration/hostpreflights_test.go index 938bbbc15..587e4199a 100644 --- a/api/integration/hostpreflights_test.go +++ b/api/integration/hostpreflights_test.go @@ -220,8 +220,10 @@ func TestGetHostPreflightsStatusWithIgnoreFlag(t *testing.T) { // Create the API with allow ignore host preflights flag apiInstance, err := api.New( types.APIConfig{ - Password: "password", - AllowIgnoreHostPreflights: tt.allowIgnoreHostPreflights, + Password: "password", + LinuxConfig: types.LinuxConfig{ + AllowIgnoreHostPreflights: tt.allowIgnoreHostPreflights, + }, }, api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), diff --git a/api/types/api.go b/api/types/api.go index 93b10c617..77ca1ed25 100644 --- a/api/types/api.go +++ b/api/types/api.go @@ -2,19 +2,32 @@ package types import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "k8s.io/client-go/rest" ) // APIConfig holds the configuration for the API server type APIConfig struct { + Password string + TLSConfig TLSConfig + License []byte + AirgapBundle string + ConfigValues string + ReleaseData *release.ReleaseData + EndUserConfig *ecv1beta1.Config + + LinuxConfig + KubernetesConfig +} + +type LinuxConfig struct { RuntimeConfig runtimeconfig.RuntimeConfig - Password string - TLSConfig TLSConfig - License []byte - AirgapBundle string - ConfigValues string - ReleaseData *release.ReleaseData - EndUserConfig *ecv1beta1.Config AllowIgnoreHostPreflights bool } + +type KubernetesConfig struct { + RESTConfig *rest.Config + Installation kubernetesinstallation.Installation +} diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go index 5640fa5d0..a1eaf0c9d 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -16,44 +16,37 @@ import ( "github.com/replicatedhq/embedded-cluster/api" apilogger "github.com/replicatedhq/embedded-cluster/api/pkg/logger" apitypes "github.com/replicatedhq/embedded-cluster/api/types" - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/cloudutils" "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" - "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/web" "github.com/sirupsen/logrus" ) // apiOptions holds the configuration options for the API server type apiOptions struct { - InstallTarget string - RuntimeConfig runtimeconfig.RuntimeConfig - Logger logrus.FieldLogger - MetricsReporter metrics.ReporterInterface - Password string - TLSConfig apitypes.TLSConfig - ManagerPort int - License []byte - AirgapBundle string - ConfigValues string - ReleaseData *release.ReleaseData - EndUserConfig *ecv1beta1.Config - AllowIgnoreHostPreflights bool - WebAssetsFS fs.FS + apitypes.APIConfig + + ManagerPort int + InstallTarget string + + Logger logrus.FieldLogger + MetricsReporter metrics.ReporterInterface + WebAssetsFS fs.FS } -func startAPI(ctx context.Context, cert tls.Certificate, opts apiOptions) error { +func startAPI(ctx context.Context, cert tls.Certificate, opts apiOptions, cancel context.CancelFunc) error { listener, err := net.Listen("tcp", fmt.Sprintf(":%d", opts.ManagerPort)) if err != nil { return fmt.Errorf("unable to create listener: %w", err) } go func() { + // If the api exits, we want to exit the entire process + defer cancel() if err := serveAPI(ctx, listener, cert, opts); err != nil { if !errors.Is(err, http.ErrServerClosed) { - logrus.Errorf("api error: %v", err) + logrus.Errorf("API server exited with error: %v", err) } } }() @@ -81,20 +74,8 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, return fmt.Errorf("new api logger: %w", err) } - cfg := apitypes.APIConfig{ - RuntimeConfig: opts.RuntimeConfig, - Password: opts.Password, - TLSConfig: opts.TLSConfig, - License: opts.License, - AirgapBundle: opts.AirgapBundle, - ConfigValues: opts.ConfigValues, - ReleaseData: opts.ReleaseData, - EndUserConfig: opts.EndUserConfig, - AllowIgnoreHostPreflights: opts.AllowIgnoreHostPreflights, - } - api, err := api.New( - cfg, + opts.APIConfig, api.WithLogger(logger), api.WithMetricsReporter(opts.MetricsReporter), ) diff --git a/cmd/installer/cli/api_test.go b/cmd/installer/cli/api_test.go index 3f0510410..3f2cca699 100644 --- a/cmd/installer/cli/api_test.go +++ b/cmd/installer/cli/api_test.go @@ -12,6 +12,7 @@ import ( "time" apilogger "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + apitypes "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -55,15 +56,17 @@ func Test_serveAPI(t *testing.T) { require.NoError(t, err) config := apiOptions{ - Logger: apilogger.NewDiscardLogger(), - Password: "password", - ManagerPort: portInt, - WebAssetsFS: webAssetsFS, - ReleaseData: &release.ReleaseData{ - Application: &kotsv1beta1.Application{ - Spec: kotsv1beta1.ApplicationSpec{}, + APIConfig: apitypes.APIConfig{ + Password: "password", + ReleaseData: &release.ReleaseData{ + Application: &kotsv1beta1.Application{ + Spec: kotsv1beta1.ApplicationSpec{}, + }, }, }, + ManagerPort: portInt, + Logger: apilogger.NewDiscardLogger(), + WebAssetsFS: webAssetsFS, } go func() { diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 4ed8e46db..e0dbb18df 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -34,6 +34,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/extensions" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" + "github.com/replicatedhq/embedded-cluster/pkg/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" @@ -102,7 +103,9 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { var flags InstallCmdFlags ctx, cancel := context.WithCancel(ctx) + rc := runtimeconfig.New(nil) + ki := kubernetesinstallation.New(nil) short := fmt.Sprintf("Install %s", name) if os.Getenv("ENABLE_V3") == "1" { @@ -121,7 +124,7 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { if err := verifyAndPrompt(ctx, name, flags, prompts.New()); err != nil { return err } - if err := preRunInstall(cmd, &flags, rc); err != nil { + if err := preRunInstall(cmd, &flags, rc, ki); err != nil { return err } @@ -133,7 +136,7 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { installReporter.ReportInstallationStarted(ctx) if flags.enableManagerExperience { - return runManagerExperienceInstall(ctx, flags, rc, installReporter) + return runManagerExperienceInstall(ctx, flags, rc, ki, installReporter) } _ = rc.SetEnv() @@ -366,12 +369,12 @@ func addManagerExperienceFlags(cmd *cobra.Command, flags *InstallCmdFlags) error return nil } -func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) error { if !slices.Contains([]string{"linux", "kubernetes"}, flags.target) { return fmt.Errorf(`invalid target (must be one of: "linux", "kubernetes")`) } - if err := preRunInstallCommon(cmd, flags, rc); err != nil { + if err := preRunInstallCommon(cmd, flags, rc, ki); err != nil { return err } @@ -379,13 +382,13 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. case "linux": return preRunInstallLinux(cmd, flags, rc) case "kubernetes": - return preRunInstallKubernetes(cmd, flags) + return preRunInstallKubernetes(cmd, flags, ki) } return nil } -func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { +func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation) error { // license file can be empty for restore if flags.licenseFile != "" { b, err := os.ReadFile(flags.licenseFile) @@ -415,6 +418,12 @@ func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimec flags.isAirgap = flags.airgapBundle != "" + 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 @@ -427,8 +436,14 @@ func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimec } } - // TODO: runtimeconfig is only relevant for linux installs + rc.SetAdminConsolePort(flags.adminConsolePort) + ki.SetAdminConsolePort(flags.adminConsolePort) + + rc.SetManagerPort(flags.managerPort) + ki.SetManagerPort(flags.managerPort) + rc.SetProxySpec(proxy) + ki.SetProxySpec(proxy) return nil } @@ -485,14 +500,13 @@ func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeco // TODO: validate that a single port isn't used for multiple services rc.SetDataDir(flags.dataDir) rc.SetLocalArtifactMirrorPort(flags.localArtifactMirrorPort) - rc.SetAdminConsolePort(flags.adminConsolePort) rc.SetHostCABundlePath(hostCABundlePath) rc.SetNetworkSpec(networkSpec) return nil } -func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags) error { +func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, _ kubernetesinstallation.Installation) error { // If set, validate that the kubeconfig file exists and can be read if flags.kubernetesEnvSettings.KubeConfig != "" { if _, err := os.Stat(flags.kubernetesEnvSettings.KubeConfig); os.IsNotExist(err) { @@ -545,7 +559,7 @@ func cidrConfigFromCmd(cmd *cobra.Command) (*newconfig.CIDRConfig, error) { return cidrCfg, nil } -func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, installReporter *InstallReporter) (finalErr error) { +func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, ki kubernetesinstallation.Installation, installReporter *InstallReporter) (finalErr error) { // 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() @@ -602,30 +616,44 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc } apiConfig := apiOptions{ + APIConfig: apitypes.APIConfig{ + Password: flags.adminConsolePassword, + TLSConfig: apitypes.TLSConfig{ + CertBytes: flags.tlsCertBytes, + KeyBytes: flags.tlsKeyBytes, + Hostname: flags.hostname, + }, + License: flags.licenseBytes, + AirgapBundle: flags.airgapBundle, + ConfigValues: flags.configValues, + ReleaseData: release.GetReleaseData(), + EndUserConfig: eucfg, + + LinuxConfig: apitypes.LinuxConfig{ + RuntimeConfig: rc, + AllowIgnoreHostPreflights: flags.ignoreHostPreflights, + }, + KubernetesConfig: apitypes.KubernetesConfig{ + RESTConfig: flags.installConfig.kubernetesRestConfig, + Installation: ki, + }, + }, + + ManagerPort: flags.managerPort, InstallTarget: flags.target, - RuntimeConfig: rc, MetricsReporter: installReporter.reporter, - Password: flags.adminConsolePassword, - TLSConfig: apitypes.TLSConfig{ - CertBytes: flags.tlsCertBytes, - KeyBytes: flags.tlsKeyBytes, - Hostname: flags.hostname, - }, - ManagerPort: flags.managerPort, - License: flags.licenseBytes, - AirgapBundle: flags.airgapBundle, - ConfigValues: flags.configValues, - ReleaseData: release.GetReleaseData(), - EndUserConfig: eucfg, - AllowIgnoreHostPreflights: flags.ignoreHostPreflights, } - if err := startAPI(ctx, flags.tlsCert, apiConfig); err != nil { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if err := startAPI(ctx, flags.tlsCert, apiConfig, cancel); err != nil { return fmt.Errorf("unable to start api: %w", err) } - // TODO: add app name to this message (e.g., App Name manager) - logrus.Infof("\nVisit the manager to continue: %s\n", getManagerURL(flags.hostname, flags.managerPort)) + logrus.Infof("\nVisit the %s manager to continue: %s\n", + runtimeconfig.BinaryName(), + getManagerURL(flags.hostname, flags.managerPort)) <-ctx.Done() return nil diff --git a/cmd/installer/cli/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index 8cf75d8f4..b8f70cd49 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -8,6 +8,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/prompts" @@ -23,14 +24,16 @@ var ErrPreflightsHaveFail = metrics.NewErrorNoFail(fmt.Errorf("host preflight fa func InstallRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { var flags InstallCmdFlags + rc := runtimeconfig.New(nil) + ki := kubernetesinstallation.New(nil) cmd := &cobra.Command{ Use: "run-preflights", Short: "Run install host preflights", Hidden: true, PreRunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc); err != nil { + if err := preRunInstall(cmd, &flags, rc, ki); err != nil { return err } diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index 590967335..0192386a5 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -29,6 +29,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/disasterrecovery" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" + "github.com/replicatedhq/embedded-cluster/pkg/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/prompts" @@ -92,12 +93,13 @@ func RestoreCmd(ctx context.Context, name string) *cobra.Command { var skipStoreValidation bool rc := runtimeconfig.New(nil) + ki := kubernetesinstallation.New(nil) cmd := &cobra.Command{ Use: "restore", Short: fmt.Sprintf("Restore %s from a backup", name), PreRunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags, rc); err != nil { + if err := preRunInstall(cmd, &flags, rc, ki); err != nil { return err } diff --git a/operator/charts/embedded-cluster-operator/values.yaml b/operator/charts/embedded-cluster-operator/values.yaml index aceb2ec4f..4e066a319 100644 --- a/operator/charts/embedded-cluster-operator/values.yaml +++ b/operator/charts/embedded-cluster-operator/values.yaml @@ -13,6 +13,7 @@ image: pullPolicy: IfNotPresent utilsImage: busybox:latest +goldpingerImage: bloomberg/goldpinger:latest extraEnv: [] # - name: HTTP_PROXY @@ -56,6 +57,8 @@ affinity: operator: In values: - linux + - key: node-role.kubernetes.io/control-plane + operator: Exists metrics: enabled: false diff --git a/operator/config/crd/bases/embeddedcluster.replicated.com_kubernetesinstallations.yaml b/operator/config/crd/bases/embeddedcluster.replicated.com_kubernetesinstallations.yaml new file mode 100644 index 000000000..35a9a7db3 --- /dev/null +++ b/operator/config/crd/bases/embeddedcluster.replicated.com_kubernetesinstallations.yaml @@ -0,0 +1,323 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: kubernetesinstallations.embeddedcluster.replicated.com +spec: + group: embeddedcluster.replicated.com + names: + kind: KubernetesInstallation + listKind: KubernetesInstallationList + plural: kubernetesinstallations + singular: kubernetesinstallation + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: KubernetesInstallation is the Schema for the kubernetes installations + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: KubernetesInstallationSpec defines the desired state of KubernetesInstallation. + properties: + adminConsole: + description: AdminConsole holds the Admin Console configuration. + properties: + port: + description: Port holds the port on which the admin console will + be served. + type: integer + type: object + airGap: + description: AirGap indicates if the installation is airgapped. + type: boolean + binaryName: + description: |- + BinaryName holds the name of the binary used to install the cluster. + this will follow the pattern 'appslug-channelslug' + type: string + clusterID: + description: ClusterID holds the cluster id, generated during the + installation. + type: string + config: + description: Config holds the configuration used at installation time. + properties: + binaryOverrideUrl: + type: string + domains: + properties: + proxyRegistryDomain: + type: string + replicatedAppDomain: + type: string + replicatedRegistryDomain: + type: string + type: object + extensions: + properties: + helm: + description: Helm contains helm extension settings + properties: + charts: + items: + description: Chart single helm addon + properties: + chartname: + type: string + forceUpgrade: + description: 'ForceUpgrade when set to false, disables + the use of the "--force" flag when upgrading the + the chart (default: true).' + type: boolean + name: + type: string + namespace: + type: string + order: + type: integer + timeout: + description: |- + Timeout specifies the timeout for how long to wait for the chart installation to finish. + A duration string is a sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + type: string + x-kubernetes-int-or-string: true + values: + type: string + version: + type: string + type: object + type: array + concurrencyLevel: + type: integer + repositories: + items: + description: Repository describes single repository + entry. Fields map to the CLI flags for the "helm add" + command + properties: + caFile: + description: CA bundle file to use when verifying + HTTPS-enabled servers. + type: string + certFile: + description: The TLS certificate file to use for + HTTPS client authentication. + type: string + insecure: + description: Whether to skip TLS certificate checks + when connecting to the repository. + type: boolean + keyfile: + description: The TLS key file to use for HTTPS client + authentication. + type: string + name: + description: The repository name. + minLength: 1 + type: string + password: + description: Password for Basic HTTP authentication. + type: string + url: + description: The repository URL. + minLength: 1 + type: string + username: + description: Username for Basic HTTP authentication. + type: string + required: + - name + - url + type: object + type: array + type: object + type: object + metadataOverrideUrl: + type: string + roles: + description: Roles is the various roles in the cluster. + properties: + controller: + description: NodeRole is the role of a node in the cluster. + properties: + description: + type: string + labels: + additionalProperties: + type: string + type: object + name: + type: string + nodeCount: + description: NodeCount holds a series of rules for a given + node role. + properties: + range: + description: |- + NodeRange contains a min and max or only one of them (conflicts + with Values). + properties: + max: + description: Max is the maximum number of nodes. + type: integer + min: + description: Min is the minimum number of nodes. + type: integer + type: object + values: + description: Values holds a list of allowed node counts. + items: + type: integer + type: array + type: object + type: object + custom: + items: + description: NodeRole is the role of a node in the cluster. + properties: + description: + type: string + labels: + additionalProperties: + type: string + type: object + name: + type: string + nodeCount: + description: NodeCount holds a series of rules for a + given node role. + properties: + range: + description: |- + NodeRange contains a min and max or only one of them (conflicts + with Values). + properties: + max: + description: Max is the maximum number of nodes. + type: integer + min: + description: Min is the minimum number of nodes. + type: integer + type: object + values: + description: Values holds a list of allowed node + counts. + items: + type: integer + type: array + type: object + type: object + type: array + type: object + unsupportedOverrides: + description: |- + UnsupportedOverrides holds the config overrides used to configure + the cluster. + properties: + builtInExtensions: + description: |- + BuiltInExtensions holds overrides for the default add-ons we ship + with Embedded Cluster. + items: + description: BuiltInExtension holds the override for a built-in + extension (add-on). + properties: + name: + description: The name of the helm chart to override + values of, for instance `openebs`. + type: string + values: + description: |- + YAML-formatted helm values that will override those provided to the + chart by Embedded Cluster. Properties are overridden individually - + setting a new value for `images.tag` here will not prevent Embedded + Cluster from setting `images.pullPolicy = IfNotPresent`, for example. + type: string + required: + - name + - values + type: object + type: array + k0s: + description: |- + K0s holds the overrides used to configure k0s. These overrides + are merged on top of the default k0s configuration. As the data + layout inside this configuration is very dynamic we have chosen + to use a string here. + type: string + type: object + version: + type: string + type: object + highAvailability: + description: HighAvailability indicates if the installation is high + availability. + type: boolean + licenseInfo: + description: LicenseInfo holds information about the license used + to install the cluster. + properties: + isDisasterRecoverySupported: + type: boolean + isMultiNodeEnabled: + type: boolean + type: object + manager: + description: Manager holds the Manager configuration. + properties: + port: + description: Port holds the port on which the manager will be + served. + type: integer + type: object + metricsBaseURL: + description: MetricsBaseURL holds the base URL for the metrics server. + type: string + proxy: + description: Proxy holds the proxy configuration. + properties: + httpProxy: + type: string + httpsProxy: + type: string + noProxy: + type: string + providedNoProxy: + type: string + type: object + type: object + status: + description: KubernetesInstallationStatus defines the observed state of + KubernetesInstallation + properties: + reason: + description: Reason holds the reason for the current state. + type: string + state: + description: State holds the current state of the installation. + type: string + type: object + type: object + served: true + storage: true diff --git a/operator/schemas/kubernetesinstallation-embeddedcluster-v1beta1.json b/operator/schemas/kubernetesinstallation-embeddedcluster-v1beta1.json new file mode 100644 index 000000000..aa105b43c --- /dev/null +++ b/operator/schemas/kubernetesinstallation-embeddedcluster-v1beta1.json @@ -0,0 +1,364 @@ +{ + "description": "KubernetesInstallation is the Schema for the kubernetes installations API", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "type": "object" + }, + "spec": { + "description": "KubernetesInstallationSpec defines the desired state of KubernetesInstallation.", + "type": "object", + "properties": { + "adminConsole": { + "description": "AdminConsole holds the Admin Console configuration.", + "type": "object", + "properties": { + "port": { + "description": "Port holds the port on which the admin console will be served.", + "type": "integer" + } + } + }, + "airGap": { + "description": "AirGap indicates if the installation is airgapped.", + "type": "boolean" + }, + "binaryName": { + "description": "BinaryName holds the name of the binary used to install the cluster.\nthis will follow the pattern 'appslug-channelslug'", + "type": "string" + }, + "clusterID": { + "description": "ClusterID holds the cluster id, generated during the installation.", + "type": "string" + }, + "config": { + "description": "Config holds the configuration used at installation time.", + "type": "object", + "properties": { + "binaryOverrideUrl": { + "type": "string" + }, + "domains": { + "type": "object", + "properties": { + "proxyRegistryDomain": { + "type": "string" + }, + "replicatedAppDomain": { + "type": "string" + }, + "replicatedRegistryDomain": { + "type": "string" + } + } + }, + "extensions": { + "type": "object", + "properties": { + "helm": { + "description": "Helm contains helm extension settings", + "type": "object", + "properties": { + "charts": { + "type": "array", + "items": { + "description": "Chart single helm addon", + "type": "object", + "properties": { + "chartname": { + "type": "string" + }, + "forceUpgrade": { + "description": "ForceUpgrade when set to false, disables the use of the \"--force\" flag when upgrading the the chart (default: true).", + "type": "boolean" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "order": { + "type": "integer" + }, + "timeout": { + "description": "Timeout specifies the timeout for how long to wait for the chart installation to finish.\nA duration string is a sequence of decimal numbers, each with optional fraction and a unit suffix, such as \"300ms\" or \"2h45m\". Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".", + "type": "string", + "x-kubernetes-int-or-string": true + }, + "values": { + "type": "string" + }, + "version": { + "type": "string" + } + } + } + }, + "concurrencyLevel": { + "type": "integer" + }, + "repositories": { + "type": "array", + "items": { + "description": "Repository describes single repository entry. Fields map to the CLI flags for the \"helm add\" command", + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "caFile": { + "description": "CA bundle file to use when verifying HTTPS-enabled servers.", + "type": "string" + }, + "certFile": { + "description": "The TLS certificate file to use for HTTPS client authentication.", + "type": "string" + }, + "insecure": { + "description": "Whether to skip TLS certificate checks when connecting to the repository.", + "type": "boolean" + }, + "keyfile": { + "description": "The TLS key file to use for HTTPS client authentication.", + "type": "string" + }, + "name": { + "description": "The repository name.", + "type": "string", + "minLength": 1 + }, + "password": { + "description": "Password for Basic HTTP authentication.", + "type": "string" + }, + "url": { + "description": "The repository URL.", + "type": "string", + "minLength": 1 + }, + "username": { + "description": "Username for Basic HTTP authentication.", + "type": "string" + } + } + } + } + } + } + } + }, + "metadataOverrideUrl": { + "type": "string" + }, + "roles": { + "description": "Roles is the various roles in the cluster.", + "type": "object", + "properties": { + "controller": { + "description": "NodeRole is the role of a node in the cluster.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "nodeCount": { + "description": "NodeCount holds a series of rules for a given node role.", + "type": "object", + "properties": { + "range": { + "description": "NodeRange contains a min and max or only one of them (conflicts\nwith Values).", + "type": "object", + "properties": { + "max": { + "description": "Max is the maximum number of nodes.", + "type": "integer" + }, + "min": { + "description": "Min is the minimum number of nodes.", + "type": "integer" + } + } + }, + "values": { + "description": "Values holds a list of allowed node counts.", + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + }, + "custom": { + "type": "array", + "items": { + "description": "NodeRole is the role of a node in the cluster.", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "nodeCount": { + "description": "NodeCount holds a series of rules for a given node role.", + "type": "object", + "properties": { + "range": { + "description": "NodeRange contains a min and max or only one of them (conflicts\nwith Values).", + "type": "object", + "properties": { + "max": { + "description": "Max is the maximum number of nodes.", + "type": "integer" + }, + "min": { + "description": "Min is the minimum number of nodes.", + "type": "integer" + } + } + }, + "values": { + "description": "Values holds a list of allowed node counts.", + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "unsupportedOverrides": { + "description": "UnsupportedOverrides holds the config overrides used to configure\nthe cluster.", + "type": "object", + "properties": { + "builtInExtensions": { + "description": "BuiltInExtensions holds overrides for the default add-ons we ship\nwith Embedded Cluster.", + "type": "array", + "items": { + "description": "BuiltInExtension holds the override for a built-in extension (add-on).", + "type": "object", + "required": [ + "name", + "values" + ], + "properties": { + "name": { + "description": "The name of the helm chart to override values of, for instance `openebs`.", + "type": "string" + }, + "values": { + "description": "YAML-formatted helm values that will override those provided to the\nchart by Embedded Cluster. Properties are overridden individually -\nsetting a new value for `images.tag` here will not prevent Embedded\nCluster from setting `images.pullPolicy = IfNotPresent`, for example.", + "type": "string" + } + } + } + }, + "k0s": { + "description": "K0s holds the overrides used to configure k0s. These overrides\nare merged on top of the default k0s configuration. As the data\nlayout inside this configuration is very dynamic we have chosen\nto use a string here.", + "type": "string" + } + } + }, + "version": { + "type": "string" + } + } + }, + "highAvailability": { + "description": "HighAvailability indicates if the installation is high availability.", + "type": "boolean" + }, + "licenseInfo": { + "description": "LicenseInfo holds information about the license used to install the cluster.", + "type": "object", + "properties": { + "isDisasterRecoverySupported": { + "type": "boolean" + }, + "isMultiNodeEnabled": { + "type": "boolean" + } + } + }, + "manager": { + "description": "Manager holds the Manager configuration.", + "type": "object", + "properties": { + "port": { + "description": "Port holds the port on which the manager will be served.", + "type": "integer" + } + } + }, + "metricsBaseURL": { + "description": "MetricsBaseURL holds the base URL for the metrics server.", + "type": "string" + }, + "proxy": { + "description": "Proxy holds the proxy configuration.", + "type": "object", + "properties": { + "httpProxy": { + "type": "string" + }, + "httpsProxy": { + "type": "string" + }, + "noProxy": { + "type": "string" + }, + "providedNoProxy": { + "type": "string" + } + } + } + } + }, + "status": { + "description": "KubernetesInstallationStatus defines the observed state of KubernetesInstallation", + "type": "object", + "properties": { + "reason": { + "description": "Reason holds the reason for the current state.", + "type": "string" + }, + "state": { + "description": "State holds the current state of the installation.", + "type": "string" + } + } + } + } +} \ No newline at end of file From 84c3e1156f1800a040f386c90e24229efa805916 Mon Sep 17 00:00:00 2001 From: Steven Crespo <96719548+screspod@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:21:13 -0700 Subject: [PATCH 42/48] bug: Use app slug in cli help menu (#2379) Use app slug in cli help menu --- cmd/installer/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/installer/main.go b/cmd/installer/main.go index 375d71b45..b23b175c4 100644 --- a/cmd/installer/main.go +++ b/cmd/installer/main.go @@ -3,12 +3,12 @@ package main import ( "context" "os" - "path" "syscall" "github.com/mattn/go-isatty" "github.com/replicatedhq/embedded-cluster/cmd/installer/cli" "github.com/replicatedhq/embedded-cluster/pkg/prompts" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" ) func main() { @@ -18,7 +18,7 @@ func main() { prompts.SetTerminal(isatty.IsTerminal(os.Stdout.Fd())) - name := path.Base(os.Args[0]) + name := runtimeconfig.BinaryName() // set the umask to 022 so that we can create files/directories with 755 permissions // this does not return an error - it returns the previous umask From f3a25db9d6f0b67c1a5a793881b287f07fe45dc0 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Fri, 27 Jun 2025 07:13:11 -0700 Subject: [PATCH 43/48] chore(ci): fix unit test race condition (#2381) --- api/controllers/linux/install/controller_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/api/controllers/linux/install/controller_test.go b/api/controllers/linux/install/controller_test.go index d147badff..4471bd445 100644 --- a/api/controllers/linux/install/controller_test.go +++ b/api/controllers/linux/install/controller_test.go @@ -398,6 +398,8 @@ func TestConfigureInstallation(t *testing.T) { err = controller.ConfigureInstallation(t.Context(), tt.config) if tt.expectedErr { assert.Error(t, err) + } else { + assert.NoError(t, err) } assert.Eventually(t, func() bool { @@ -761,8 +763,6 @@ func TestRunHostPreflights(t *testing.T) { require.Error(t, err) } else { require.NoError(t, err) - - assert.NotEqual(t, sm.CurrentState(), tt.currentState, "state should have changed and should not be %s", tt.currentState) } assert.Eventually(t, func() bool { @@ -1152,8 +1152,6 @@ func TestSetupInfra(t *testing.T) { } } else { require.NoError(t, err) - - assert.NotEqual(t, sm.CurrentState(), tt.currentState, "state should have changed and should not be %s", tt.currentState) } assert.Eventually(t, func() bool { From 005e5766ddb3b38648cce6ff865bb4182c35fd52 Mon Sep 17 00:00:00 2001 From: Chuck D'Antonio Date: Fri, 27 Jun 2025 14:33:51 -0400 Subject: [PATCH 44/48] Adds a secret skip preflight environment variable (#2364) * Adds a secret skip preflight environment variable TL;DR ----- Supports the `SKIP_HOST_PREFLIGHTS` environment variable to skip the preflight checks. Thjis is not documented and not for general use. Details ------- Offers a "hidden" way to skip preflight checks by setting the environment variable `SKIP_HOST_PREFLIGHTS`. I'm adding the flag to support our Instruqt labs, since the Instruqt environment require a wildcard DNS entry that causes the host preflights to fail. I decided on this approach to avoid teaching any bad habits by telling the user to use `--ignore-host-preflights` when teaching them about the Embedded Cluster. I wanted instead to avoid the checks on their behalf without them realized it. This variable is not deliberately not documented to avoid similar encouragement of bad habits. * Reformats code * Restores missing brace * Moves to the Linux pre install per @sgalsaleh review * Checks specific values per @sgalsaleh review * Fixes failing test --- cmd/installer/cli/install.go | 4 ++ cmd/installer/cli/install_test.go | 96 +++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index e0dbb18df..e72824532 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -449,6 +449,10 @@ func preRunInstallCommon(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimec } 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 + } + if os.Getuid() != 0 { return fmt.Errorf("install command must be run as root") } diff --git a/cmd/installer/cli/install_test.go b/cmd/installer/cli/install_test.go index b63d79813..3c4ac1571 100644 --- a/cmd/installer/cli/install_test.go +++ b/cmd/installer/cli/install_test.go @@ -14,7 +14,9 @@ import ( "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" "github.com/stretchr/testify/require" ) @@ -607,3 +609,97 @@ 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 +} From 744bb512020dfa897047698edb0baf50d041bd5a Mon Sep 17 00:00:00 2001 From: Diamon Wiggins <38189728+diamonwiggins@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:35:53 -0400 Subject: [PATCH 45/48] fix(ui): improve ux of backend disconnect modal (#2378) * handle backend disconnect * add new lines * fix lint * use a simple component instead of react context * new lines * make health check timeout less aggressive * remove consecutiveattempts logic for simplicity * clean up * synchornize timer with retry button * remove merge markers * fix lint * remove retry button --- .../components/common/ConnectionMonitor.tsx | 148 +++++++++++------- .../common/tests/ConnectionMonitor.test.tsx | 27 ++-- 2 files changed, 100 insertions(+), 75 deletions(-) diff --git a/web/src/components/common/ConnectionMonitor.tsx b/web/src/components/common/ConnectionMonitor.tsx index 6c0875ad7..c5f45dc33 100644 --- a/web/src/components/common/ConnectionMonitor.tsx +++ b/web/src/components/common/ConnectionMonitor.tsx @@ -1,15 +1,34 @@ import React, { useEffect, useState, useCallback } from 'react'; +const RETRY_INTERVAL = 10000; // 10 seconds + +// Reusable spinner component +const Spinner: React.FC = () => ( +
+); + // Connection modal component -const ConnectionModal: React.FC<{ onRetry: () => void; isRetrying: boolean }> = ({ onRetry, isRetrying }) => { - const [retryCount, setRetryCount] = useState(0); +const ConnectionModal: React.FC<{ + nextRetryTime?: number; +}> = ({ nextRetryTime }) => { + const [secondsUntilRetry, setSecondsUntilRetry] = useState(0); useEffect(() => { - const interval = setInterval(() => { - setRetryCount(count => count + 1); - }, 1000); + if (!nextRetryTime) return; + + const updateCountdown = () => { + const now = Date.now(); + const remaining = Math.max(0, Math.floor((nextRetryTime - now) / 1000)); + setSecondsUntilRetry(remaining); + }; + + // Update immediately + updateCountdown(); + + // Update every second + const interval = setInterval(updateCountdown, 1000); return () => clearInterval(interval); - }, []); + }, [nextRetryTime]); return (
@@ -31,71 +50,56 @@ const ConnectionModal: React.FC<{ onRetry: () => void; isRetrying: boolean }> = installer is running and accessible.

-
+
-
- Trying again in {Math.max(1, 10 - (retryCount % 10))} second{Math.max(1, 10 - (retryCount % 10)) !== 1 ? 's' : ''} + {secondsUntilRetry > 0 ? ( + <> + + Retrying in {secondsUntilRetry} second{secondsUntilRetry !== 1 ? 's' : ''} + + ) : ( + <> + + Retrying now... + + )}
-
); }; -const ConnectionMonitor: React.FC = () => { +// Custom hook for connection monitoring logic +const useConnectionMonitor = () => { const [isConnected, setIsConnected] = useState(true); - const [isChecking, setIsChecking] = useState(false); + const [nextRetryTime, setNextRetryTime] = useState(); + const [checkInterval, setCheckInterval] = useState(null); const checkConnection = useCallback(async () => { - setIsChecking(true); - try { - // Try up to 3 times before marking as disconnected - let attempts = 0; - const maxAttempts = 3; + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 5000) + ); - while (attempts < maxAttempts) { - try { - // Create a timeout promise - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), 5000) - ); - - const fetchPromise = fetch('/api/health', { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }); - - const response = await Promise.race([fetchPromise, timeoutPromise]) as Response; - - if (response.ok) { - setIsConnected(true); - return; - } else { - throw new Error(`HTTP ${response.status}`); - } - } catch { - attempts++; - if (attempts < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, 1000)); - } - } - } + const fetchPromise = fetch('/api/health', { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); - // All attempts failed - show modal immediately - setIsConnected(false); + const response = await Promise.race([fetchPromise, timeoutPromise]) as Response; + if (response.ok) { + setIsConnected(true); + setNextRetryTime(undefined); + } else { + throw new Error(`HTTP ${response.status}`); + } } catch { + // Connection failed - set up countdown for next retry setIsConnected(false); - } finally { - setIsChecking(false); + const retryTime = Date.now() + RETRY_INTERVAL; + setNextRetryTime(retryTime); } }, []); @@ -103,18 +107,42 @@ const ConnectionMonitor: React.FC = () => { // Initial check checkConnection(); - // Set up periodic health checks every 5 seconds - const interval = setInterval(checkConnection, 5000); + // Set up regular interval checks + const interval = setInterval(checkConnection, RETRY_INTERVAL); + setCheckInterval(interval); - return () => clearInterval(interval); - }, [checkConnection]); + // Cleanup on unmount + return () => { + if (interval) { + clearInterval(interval); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Empty dependency array to prevent infinite loops + + // Cleanup interval when it changes + useEffect(() => { + return () => { + if (checkInterval) { + clearInterval(checkInterval); + } + }; + }, [checkInterval]); + + return { + isConnected, + nextRetryTime, + }; +}; + +const ConnectionMonitor: React.FC = () => { + const { isConnected, nextRetryTime } = useConnectionMonitor(); return ( <> {!isConnected && ( )} diff --git a/web/src/components/common/tests/ConnectionMonitor.test.tsx b/web/src/components/common/tests/ConnectionMonitor.test.tsx index d3b8f3b01..ddab8da9a 100644 --- a/web/src/components/common/tests/ConnectionMonitor.test.tsx +++ b/web/src/components/common/tests/ConnectionMonitor.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, waitFor, fireEvent } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; @@ -46,14 +46,15 @@ describe('ConnectionMonitor', () => { }, { timeout: 4000 }); }, 6000); - it('should handle manual retry', async () => { - let manualRetryClicked = false; + it('should handle automatic retry', async () => { + let retryCount = 0; server.use( http.get('*/api/health', () => { + retryCount++; - // Keep failing until manual retry is clicked, then succeed - if (!manualRetryClicked) { + // Fail first time, succeed on second automatic retry + if (retryCount === 1) { return HttpResponse.error(); } @@ -71,20 +72,16 @@ describe('ConnectionMonitor', () => { expect(screen.getByText('Cannot connect')).toBeInTheDocument(); }, { timeout: 6000 }); - // Wait for the retry button to be available + // Should show countdown await waitFor(() => { - expect(screen.getByText('Try Now')).toBeInTheDocument(); + expect(screen.getByText(/Retrying in \d+ second/)).toBeInTheDocument(); }, { timeout: 1000 }); - // Mark that manual retry was clicked, then click it - manualRetryClicked = true; - fireEvent.click(screen.getByText('Try Now')); - - // Modal should disappear when connection is restored + // Modal should disappear when automatic retry succeeds await waitFor(() => { expect(screen.queryByText('Cannot connect')).not.toBeInTheDocument(); - }, { timeout: 6000 }); - }, 12000); + }, { timeout: 12000 }); + }, 15000); it('should show retry countdown timer', async () => { server.use( @@ -99,6 +96,6 @@ describe('ConnectionMonitor', () => { expect(screen.getByText('Cannot connect')).toBeInTheDocument(); }, { timeout: 4000 }); - expect(screen.getByText(/Trying again in \d+ second/)).toBeInTheDocument(); + expect(screen.getByText(/Retrying in \d+ second/)).toBeInTheDocument(); }, 6000); }); From 681cec69851feae7efe63fc2f43b0a0a6d20a29e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 12:06:00 +0000 Subject: [PATCH 46/48] build(deps): bump react-router-dom from 6.30.0 to 7.6.3 in /web (#2385) Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.30.0 to 7.6.3. - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.6.3/packages/react-router-dom) --- updated-dependencies: - dependency-name: react-router-dom dependency-version: 7.6.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 60 ++++++++++++++++++++++++------------------- web/package.json | 2 +- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index a21d794a1..abed41ef9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,7 +13,7 @@ "lucide-react": "^0.519.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.22.3" + "react-router-dom": "^7.6.3" }, "devDependencies": { "@eslint/js": "^9.29.0", @@ -1629,15 +1629,6 @@ "node": ">=14" } }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.11", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", @@ -5844,35 +5835,47 @@ } }, "node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", - "license": "MIT", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz", + "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", "dependencies": { - "@remix-run/router": "1.23.0" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-router-dom": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", - "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", - "license": "MIT", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz", + "integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.0" + "react-router": "7.6.3" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" } }, "node_modules/read-cache": { @@ -6067,6 +6070,11 @@ "semver": "bin/semver.js" } }, + "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==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/web/package.json b/web/package.json index 46706ecb4..a7d96aada 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,7 @@ "lucide-react": "^0.519.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.22.3" + "react-router-dom": "^7.6.3" }, "devDependencies": { "@eslint/js": "^9.29.0", From 4602e6319007cb9b005d60ab82bdaa8f43957267 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 00:17:44 +0000 Subject: [PATCH 47/48] build(deps): bump react, react-dom and @types/react in /web (#2387) Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react), [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together. Updates `react` from 18.3.1 to 19.1.0 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react) Updates `react-dom` from 18.3.1 to 19.1.0 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.1.0/packages/react-dom) Updates `@types/react` from 18.3.11 to 19.1.8 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) --- updated-dependencies: - dependency-name: react dependency-version: 19.1.0 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: react-dom dependency-version: 19.1.0 dependency-type: direct:production update-type: version-update:semver-major - dependency-name: "@types/react" dependency-version: 19.1.8 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package-lock.json | 62 +++++++++++++------------------------------ web/package.json | 6 ++--- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index abed41ef9..50b78ff8e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,8 +11,8 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.80.10", "lucide-react": "^0.519.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^7.6.3" }, "devDependencies": { @@ -22,7 +22,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.4.3", "@types/node": "^24.0.3", - "@types/react": "^18.3.5", + "@types/react": "^19.1.8", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.5.2", "autoprefixer": "^10.4.21", @@ -2371,19 +2371,12 @@ "undici-types": "~7.8.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true - }, "node_modules/@types/react": { - "version": "18.3.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", - "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "dev": true, "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, @@ -4901,7 +4894,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -5090,17 +5084,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", @@ -5794,26 +5777,22 @@ ] }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.0" } }, "node_modules/react-is": { @@ -6053,12 +6032,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, "node_modules/semver": { "version": "6.3.1", diff --git a/web/package.json b/web/package.json index a7d96aada..1c2448186 100644 --- a/web/package.json +++ b/web/package.json @@ -15,8 +15,8 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.80.10", "lucide-react": "^0.519.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^7.6.3" }, "devDependencies": { @@ -26,7 +26,7 @@ "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.4.3", "@types/node": "^24.0.3", - "@types/react": "^18.3.5", + "@types/react": "^19.1.8", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.5.2", "autoprefixer": "^10.4.21", From ba766aeaf16bba2905ce576d11cccc9f316bd122 Mon Sep 17 00:00:00 2001 From: emosbaugh <371319+emosbaugh@users.noreply.github.com> Date: Sun, 29 Jun 2025 01:37:24 +0000 Subject: [PATCH 48/48] updated seaweedfs version --- pkg/addons/seaweedfs/static/metadata.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/addons/seaweedfs/static/metadata.yaml b/pkg/addons/seaweedfs/static/metadata.yaml index d0d4b0b47..b200cc902 100644 --- a/pkg/addons/seaweedfs/static/metadata.yaml +++ b/pkg/addons/seaweedfs/static/metadata.yaml @@ -5,11 +5,11 @@ # $ make buildtools # $ output/bin/buildtools update addon # -version: 4.0.390 +version: 4.0.392 location: oci://proxy.replicated.com/anonymous/registry.replicated.com/ec-charts/seaweedfs images: seaweedfs: repo: proxy.replicated.com/anonymous/replicated/ec-seaweedfs tag: - amd64: 3.90-r0-amd64@sha256:d9f7a010cf9da7a374422a7b55a2830c768c78359679d0475441550a2b21cab4 - arm64: 3.90-r0-arm64@sha256:c0e9f06f01179f16d6198ea5240842da0cac11ea7fa941aa515276f7314e7f61 + amd64: 3.92-r1-amd64@sha256:24a86ee500925c088373835359b37c990a81ddb0ae885320df478850431c77a9 + arm64: 3.92-r1-arm64@sha256:dfa78daea9996fbb1613aa0c13ebf29641abf1a9b80bc1df61019733fad728c9