diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6c446a6ce7..c7f07db3fd 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 7467452537..7e57e01188 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) ./... @@ -339,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$* \ @@ -352,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/api/Makefile b/api/Makefile index 6b0bdc0476..ce38fe7f42 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/README.md b/api/README.md index edc2325a97..83dcc6223a 100644 --- a/api/README.md +++ b/api/README.md @@ -12,8 +12,14 @@ 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. +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. #### `/types` Defines the core data structures and types used throughout the API. This includes: @@ -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 2414062464..414a3c003c 100644 --- a/api/api.go +++ b/api/api.go @@ -1,28 +1,20 @@ 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" ) +// 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. @@ -42,110 +34,68 @@ 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 - licenseFile string - airgapBundle string - configValues string - endUserConfig *ecv1beta1.Config - 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) +// Option is a function that configures the API. +type Option func(*API) -func WithAuthController(authController auth.Controller) APIOption { +// 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 { +// WithConsoleController configures the console controller for the API. +func WithConsoleController(consoleController console.Controller) Option { return func(a *API) { a.consoleController = consoleController } } -func WithInstallController(installController install.Controller) APIOption { - return func(a *API) { - a.installController = installController - } -} - -func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) APIOption { +// WithLinuxInstallController configures the linux install controller for the API. +func WithLinuxInstallController(linuxInstallController linuxinstall.Controller) Option { return func(a *API) { - a.rc = rc + a.linuxInstallController = linuxInstallController } } -func WithLogger(logger logrus.FieldLogger) APIOption { +// 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 WithHostUtils(hostUtils hostutils.HostUtilsInterface) APIOption { - return func(a *API) { - a.hostUtils = hostUtils - } -} - -func WithMetricsReporter(metricsReporter metrics.ReporterInterface) APIOption { +// WithMetricsReporter configures the metrics reporter for the API. +func WithMetricsReporter(metricsReporter metrics.ReporterInterface) Option { 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 WithLicenseFile(licenseFile string) APIOption { - return func(a *API) { - a.licenseFile = licenseFile - } -} - -func WithAirgapBundle(airgapBundle string) APIOption { - return func(a *API) { - a.airgapBundle = airgapBundle +// New creates a new API instance. +func New(cfg types.APIConfig, opts ...Option) (*API, error) { + api := &API{ + cfg: cfg, } -} - -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 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 { @@ -156,137 +106,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.WithLicenseFile(api.licenseFile), - install.WithAirgapBundle(api.airgapBundle), - install.WithConfigValues(api.configValues), - install.WithEndUserConfig(api.endUserConfig), - ) - if err != nil { - return nil, fmt.Errorf("new install controller: %w", err) - } - api.installController = installController + if err := api.initHandlers(); 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 dec938a320..0000000000 --- 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.go b/api/client/client.go index badf282275..3eb7e0537c 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 6f40fa5138..84f8df824d 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")) @@ -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) @@ -138,7 +137,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") @@ -151,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")) @@ -177,9 +176,8 @@ 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) assert.Equal(t, "Configuring installation", status.Description) @@ -194,9 +192,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") @@ -208,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")) @@ -216,7 +214,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", }, @@ -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) @@ -246,7 +242,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") @@ -258,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")) @@ -266,7 +262,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", }, @@ -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) @@ -295,7 +290,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") @@ -307,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")) @@ -325,13 +320,12 @@ 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", } newStatus, err := c.SetInstallStatus(status) assert.NoError(t, err) - assert.NotNil(t, newStatus) assert.Equal(t, status, newStatus) // Test error response @@ -347,7 +341,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 1c368d555e..30f1b7b9da 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) { - req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/config", nil) +func (c *client) GetInstallationConfig() (types.InstallationConfig, error) { + req, err := http.NewRequest("GET", c.apiURL+"/api/linux/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(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 + 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 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) { - req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/status", nil) +func (c *client) GetInstallationStatus() (types.Status, error) { + req, err := http.NewRequest("GET", c.apiURL+"/api/linux/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) { - req, err := http.NewRequest("POST", c.apiURL+"/api/install/infra/setup", nil) +func (c *client) SetupInfra() (types.Infra, error) { + req, err := http.NewRequest("POST", c.apiURL+"/api/linux/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) { - req, err := http.NewRequest("GET", c.apiURL+"/api/install/infra/status", nil) +func (c *client) GetInfraStatus() (types.Infra, error) { + req, err := http.NewRequest("GET", c.apiURL+"/api/linux/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)) + req, err := http.NewRequest("POST", c.apiURL+"/api/linux/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/console.go b/api/console.go deleted file mode 100644 index 3e7c3a2bc4..0000000000 --- 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 66ede9f4e4..c2bea48e2e 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 { @@ -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_test.go b/api/controllers/install/controller_test.go deleted file mode 100644 index b997e424fa..0000000000 --- a/api/controllers/install/controller_test.go +++ /dev/null @@ -1,881 +0,0 @@ -package install - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "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/types" - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/metrics" - "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" - troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" -) - -func TestGetInstallationConfig(t *testing.T) { - tests := []struct { - name string - setupMock func(*installation.MockInstallationManager) - expectedErr bool - expectedValue *types.InstallationConfig - }{ - { - name: "successful get", - setupMock: func(m *installation.MockInstallationManager) { - config := &types.InstallationConfig{ - AdminConsolePort: 9000, - GlobalCIDR: "10.0.0.1/16", - } - - mock.InOrder( - m.On("GetConfig").Return(config, nil), - m.On("SetConfigDefaults", config).Return(nil), - m.On("ValidateConfig", config).Return(nil), - ) - }, - expectedErr: false, - expectedValue: &types.InstallationConfig{ - AdminConsolePort: 9000, - GlobalCIDR: "10.0.0.1/16", - }, - }, - { - name: "read config error", - setupMock: func(m *installation.MockInstallationManager) { - m.On("GetConfig").Return(nil, errors.New("read error")) - }, - expectedErr: true, - expectedValue: nil, - }, - { - name: "set defaults error", - setupMock: func(m *installation.MockInstallationManager) { - config := &types.InstallationConfig{} - mock.InOrder( - m.On("GetConfig").Return(config, nil), - m.On("SetConfigDefaults", config).Return(errors.New("defaults error")), - ) - }, - expectedErr: true, - expectedValue: nil, - }, - { - name: "validate error", - setupMock: func(m *installation.MockInstallationManager) { - config := &types.InstallationConfig{} - mock.InOrder( - m.On("GetConfig").Return(config, nil), - m.On("SetConfigDefaults", config).Return(nil), - m.On("ValidateConfig", config).Return(errors.New("validation error")), - ) - }, - expectedErr: true, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{})) - rc.SetDataDir(t.TempDir()) - - mockManager := &installation.MockInstallationManager{} - tt.setupMock(mockManager) - - controller, err := NewInstallController( - WithRuntimeConfig(rc), - WithInstallationManager(mockManager), - ) - require.NoError(t, err) - - result, err := controller.GetInstallationConfig(t.Context()) - - if tt.expectedErr { - assert.Error(t, err) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedValue, result) - } - - mockManager.AssertExpectations(t) - }) - } -} - -func TestConfigureInstallation(t *testing.T) { - tests := []struct { - name string - config *types.InstallationConfig - setupMock func(*installation.MockInstallationManager, *types.InstallationConfig) - expectedErr bool - }{ - { - name: "successful configure installation", - config: &types.InstallationConfig{ - LocalArtifactMirrorPort: 9000, - DataDirectory: t.TempDir(), - }, - setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { - mock.InOrder( - m.On("ValidateConfig", config).Return(nil), - m.On("SetConfig", *config).Return(nil), - m.On("ConfigureHost", t.Context()).Return(nil), - ) - }, - expectedErr: false, - }, - { - name: "validate error", - config: &types.InstallationConfig{}, - setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { - m.On("ValidateConfig", config).Return(errors.New("validation error")) - }, - expectedErr: true, - }, - { - name: "set config error", - config: &types.InstallationConfig{}, - setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { - mock.InOrder( - m.On("ValidateConfig", config).Return(nil), - m.On("SetConfig", *config).Return(errors.New("set config error")), - ) - }, - expectedErr: true, - }, - { - name: "with global CIDR", - config: &types.InstallationConfig{ - GlobalCIDR: "10.0.0.0/16", - DataDirectory: t.TempDir(), - }, - setupMock: func(m *installation.MockInstallationManager, 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("SetConfig", configWithCIDRs).Return(nil), - m.On("ConfigureHost", t.Context()).Return(nil), - ) - }, - expectedErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{})) - rc.SetDataDir(t.TempDir()) - - mockManager := &installation.MockInstallationManager{} - - // Create a copy of the config to avoid modifying the original - configCopy := *tt.config - - tt.setupMock(mockManager, &configCopy) - - controller, err := NewInstallController( - WithRuntimeConfig(rc), - WithInstallationManager(mockManager), - ) - require.NoError(t, err) - - err = controller.ConfigureInstallation(t.Context(), tt.config) - if tt.expectedErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - - mockManager.AssertExpectations(t) - }) - } -} - -// TestIntegrationComputeCIDRs tests the CIDR computation with real networking utility -func TestIntegrationComputeCIDRs(t *testing.T) { - tests := []struct { - name string - globalCIDR string - expectedPod string - expectedSvc string - expectedErr bool - }{ - { - name: "valid cidr 10.0.0.0/16", - globalCIDR: "10.0.0.0/16", - expectedPod: "10.0.0.0/17", - expectedSvc: "10.0.128.0/17", - expectedErr: false, - }, - { - name: "valid cidr 192.168.0.0/16", - globalCIDR: "192.168.0.0/16", - expectedPod: "192.168.0.0/17", - expectedSvc: "192.168.128.0/17", - expectedErr: false, - }, - { - name: "no global cidr", - globalCIDR: "", - expectedPod: "", // Should remain unchanged - expectedSvc: "", // Should remain unchanged - expectedErr: false, - }, - { - name: "invalid cidr", - globalCIDR: "not-a-cidr", - expectedErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - controller, err := NewInstallController() - require.NoError(t, err) - - config := &types.InstallationConfig{ - GlobalCIDR: tt.globalCIDR, - } - - err = controller.computeCIDRs(config) - - if tt.expectedErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedPod, config.PodCIDR) - assert.Equal(t, tt.expectedSvc, config.ServiceCIDR) - } - }) - } -} - -func TestRunHostPreflights(t *testing.T) { - expectedHPF := &troubleshootv1beta2.HostPreflightSpec{ - Collectors: []*troubleshootv1beta2.HostCollect{ - { - Time: &troubleshootv1beta2.HostTime{}, - }, - }, - } - - tests := []struct { - name string - setupMocks func(*preflight.MockHostPreflightManager) - expectedErr bool - }{ - { - name: "successful run preflights", - setupMocks: func(pm *preflight.MockHostPreflightManager) { - mock.InOrder( - pm.On("PrepareHostPreflights", t.Context(), mock.Anything).Return(expectedHPF, nil), - pm.On("RunHostPreflights", t.Context(), mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { - return expectedHPF == opts.HostPreflightSpec - })).Return(nil), - ) - }, - expectedErr: false, - }, - { - name: "prepare preflights error", - setupMocks: func(pm *preflight.MockHostPreflightManager) { - mock.InOrder( - pm.On("PrepareHostPreflights", t.Context(), mock.Anything).Return(nil, errors.New("prepare error")), - ) - }, - expectedErr: true, - }, - { - name: "run preflights error", - setupMocks: func(pm *preflight.MockHostPreflightManager) { - mock.InOrder( - pm.On("PrepareHostPreflights", t.Context(), mock.Anything).Return(expectedHPF, nil), - pm.On("RunHostPreflights", t.Context(), mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { - return expectedHPF == opts.HostPreflightSpec - })).Return(errors.New("run preflights error")), - ) - }, - expectedErr: true, - }, - } - - 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{ - HTTPProxy: "http://proxy.example.com", - HTTPSProxy: "https://proxy.example.com", - ProvidedNoProxy: "provided-proxy.com", - NoProxy: "no-proxy.com", - }) - - controller, err := NewInstallController( - WithRuntimeConfig(rc), - WithHostPreflightManager(mockPreflightManager), - WithReleaseData(getTestReleaseData()), - ) - require.NoError(t, err) - - err = controller.RunHostPreflights(t.Context(), RunHostPreflightsOptions{}) - - if tt.expectedErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - - mockPreflightManager.AssertExpectations(t) - }) - } -} - -func TestGetHostPreflightStatus(t *testing.T) { - tests := []struct { - name string - setupMock func(*preflight.MockHostPreflightManager) - expectedErr bool - expectedValue *types.Status - }{ - { - name: "successful get status", - setupMock: func(m *preflight.MockHostPreflightManager) { - status := &types.Status{ - State: types.StateFailed, - } - m.On("GetHostPreflightStatus", t.Context()).Return(status, nil) - }, - expectedErr: false, - expectedValue: &types.Status{ - State: types.StateFailed, - }, - }, - { - name: "get status error", - setupMock: func(m *preflight.MockHostPreflightManager) { - m.On("GetHostPreflightStatus", t.Context()).Return(nil, errors.New("get status error")) - }, - expectedErr: true, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockManager := &preflight.MockHostPreflightManager{} - tt.setupMock(mockManager) - - controller, err := NewInstallController(WithHostPreflightManager(mockManager)) - require.NoError(t, err) - - result, err := controller.GetHostPreflightStatus(t.Context()) - - if tt.expectedErr { - assert.Error(t, err) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedValue, result) - } - - mockManager.AssertExpectations(t) - }) - } -} - -func TestGetHostPreflightOutput(t *testing.T) { - tests := []struct { - name string - setupMock func(*preflight.MockHostPreflightManager) - expectedErr bool - expectedValue *types.HostPreflightsOutput - }{ - { - name: "successful get output", - setupMock: func(m *preflight.MockHostPreflightManager) { - output := &types.HostPreflightsOutput{ - Pass: []types.HostPreflightsRecord{ - { - Title: "Test Check", - Message: "Test check passed", - }, - }, - } - m.On("GetHostPreflightOutput", t.Context()).Return(output, nil) - }, - expectedErr: false, - expectedValue: &types.HostPreflightsOutput{ - Pass: []types.HostPreflightsRecord{ - { - Title: "Test Check", - Message: "Test check passed", - }, - }, - }, - }, - { - name: "get output error", - setupMock: func(m *preflight.MockHostPreflightManager) { - m.On("GetHostPreflightOutput", t.Context()).Return(nil, errors.New("get output error")) - }, - expectedErr: true, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockManager := &preflight.MockHostPreflightManager{} - tt.setupMock(mockManager) - - controller, err := NewInstallController(WithHostPreflightManager(mockManager)) - require.NoError(t, err) - - result, err := controller.GetHostPreflightOutput(t.Context()) - - if tt.expectedErr { - assert.Error(t, err) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedValue, result) - } - - mockManager.AssertExpectations(t) - }) - } -} - -func TestGetHostPreflightTitles(t *testing.T) { - tests := []struct { - name string - setupMock func(*preflight.MockHostPreflightManager) - expectedErr bool - expectedValue []string - }{ - { - name: "successful get titles", - setupMock: func(m *preflight.MockHostPreflightManager) { - titles := []string{"Check 1", "Check 2"} - m.On("GetHostPreflightTitles", t.Context()).Return(titles, nil) - }, - expectedErr: false, - expectedValue: []string{"Check 1", "Check 2"}, - }, - { - name: "get titles error", - setupMock: func(m *preflight.MockHostPreflightManager) { - m.On("GetHostPreflightTitles", t.Context()).Return(nil, errors.New("get titles error")) - }, - expectedErr: true, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockManager := &preflight.MockHostPreflightManager{} - tt.setupMock(mockManager) - - controller, err := NewInstallController(WithHostPreflightManager(mockManager)) - require.NoError(t, err) - - result, err := controller.GetHostPreflightTitles(t.Context()) - - if tt.expectedErr { - assert.Error(t, err) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedValue, result) - } - - mockManager.AssertExpectations(t) - }) - } -} - -func TestGetInstallationStatus(t *testing.T) { - tests := []struct { - name string - setupMock func(*installation.MockInstallationManager) - expectedErr bool - expectedValue *types.Status - }{ - { - name: "successful get status", - setupMock: func(m *installation.MockInstallationManager) { - status := &types.Status{ - State: types.StateRunning, - } - m.On("GetStatus").Return(status, nil) - }, - expectedErr: false, - expectedValue: &types.Status{ - State: types.StateRunning, - }, - }, - { - name: "get status error", - setupMock: func(m *installation.MockInstallationManager) { - m.On("GetStatus").Return(nil, errors.New("get status error")) - }, - expectedErr: true, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockManager := &installation.MockInstallationManager{} - tt.setupMock(mockManager) - - controller, err := NewInstallController(WithInstallationManager(mockManager)) - require.NoError(t, err) - - result, err := controller.GetInstallationStatus(t.Context()) - - if tt.expectedErr { - assert.Error(t, err) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedValue, result) - } - - mockManager.AssertExpectations(t) - }) - } -} - -func TestSetupInfra(t *testing.T) { - tests := []struct { - name string - setupMocks func(*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) { - 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), - ) - }, - expectedErr: false, - }, - { - name: "successful setup with failed preflights", - setupMocks: func(pm *preflight.MockHostPreflightManager, im *installation.MockInstallationManager, fm *infra.MockInfraManager, r *metrics.MockReporter) { - preflightStatus := &types.Status{ - State: types.StateFailed, - } - preflightOutput := &types.HostPreflightsOutput{ - Fail: []types.HostPreflightsRecord{ - { - Title: "Test Check", - Message: "Test check failed", - }, - }, - } - 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), - ) - }, - expectedErr: false, - }, - { - name: "preflight status error", - setupMocks: func(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) { - preflightStatus := &types.Status{ - State: types.StateRunning, - } - pm.On("GetHostPreflightStatus", t.Context()).Return(preflightStatus, nil) - }, - expectedErr: true, - }, - { - name: "preflight output error", - setupMocks: func(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")), - ) - }, - 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) { - 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")), - ) - }, - expectedErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockPreflightManager := &preflight.MockHostPreflightManager{} - mockInstallationManager := &installation.MockInstallationManager{} - mockInfraManager := &infra.MockInfraManager{} - mockMetricsReporter := &metrics.MockReporter{} - tt.setupMocks(mockPreflightManager, mockInstallationManager, mockInfraManager, mockMetricsReporter) - - controller, err := NewInstallController( - WithHostPreflightManager(mockPreflightManager), - WithInstallationManager(mockInstallationManager), - WithInfraManager(mockInfraManager), - WithMetricsReporter(mockMetricsReporter), - ) - require.NoError(t, err) - - err = controller.SetupInfra(t.Context()) - - if tt.expectedErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - - mockPreflightManager.AssertExpectations(t) - mockInstallationManager.AssertExpectations(t) - mockInfraManager.AssertExpectations(t) - mockMetricsReporter.AssertExpectations(t) - }) - } -} - -func TestGetInfra(t *testing.T) { - tests := []struct { - name string - setupMock func(*infra.MockInfraManager) - expectedErr bool - expectedValue *types.Infra - }{ - { - name: "successful get infra", - setupMock: func(m *infra.MockInfraManager) { - infra := &types.Infra{ - Components: []types.InfraComponent{ - { - Name: infra.K0sComponentName, - Status: &types.Status{ - State: types.StateRunning, - }, - }, - }, - Status: &types.Status{ - State: types.StateRunning, - }, - } - m.On("Get").Return(infra, nil) - }, - expectedErr: false, - expectedValue: &types.Infra{ - Components: []types.InfraComponent{ - { - Name: infra.K0sComponentName, - Status: &types.Status{ - State: types.StateRunning, - }, - }, - }, - Status: &types.Status{ - State: types.StateRunning, - }, - }, - }, - { - name: "get infra error", - setupMock: func(m *infra.MockInfraManager) { - m.On("Get").Return(nil, errors.New("get infra error")) - }, - expectedErr: true, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockManager := &infra.MockInfraManager{} - tt.setupMock(mockManager) - - controller, err := NewInstallController(WithInfraManager(mockManager)) - require.NoError(t, err) - - result, err := controller.GetInfra(t.Context()) - - if tt.expectedErr { - assert.Error(t, err) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expectedValue, result) - } - - mockManager.AssertExpectations(t) - }) - } -} - -func TestGetStatus(t *testing.T) { - tests := []struct { - name string - install *types.Install - expectedValue *types.Status - }{ - { - name: "successful get status", - install: &types.Install{ - Status: &types.Status{ - State: types.StateFailed, - }, - }, - expectedValue: &types.Status{ - State: types.StateFailed, - }, - }, - { - name: "nil status", - install: &types.Install{}, - expectedValue: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - controller := &InstallController{ - install: tt.install, - } - - result, err := controller.GetStatus(t.Context()) - - assert.NoError(t, err) - assert.Equal(t, tt.expectedValue, result) - }) - } -} - -func TestSetStatus(t *testing.T) { - tests := []struct { - name string - status *types.Status - expectedErr bool - }{ - { - name: "successful set status", - status: &types.Status{ - State: types.StateFailed, - }, - expectedErr: false, - }, - { - name: "nil status", - status: nil, - expectedErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - controller, err := NewInstallController() - require.NoError(t, err) - - err = controller.SetStatus(t.Context(), tt.status) - - if tt.expectedErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.status, controller.install.Status) - } - }) - } -} - -func getTestReleaseData() *release.ReleaseData { - return &release.ReleaseData{ - EmbeddedClusterConfig: &ecv1beta1.Config{}, - ChannelRelease: &release.ChannelRelease{}, - } -} - -func WithInfraManager(infraManager infra.InfraManager) InstallControllerOption { - return func(c *InstallController) { - c.infraManager = infraManager - } -} - -type testEnvSetter struct { - env map[string]string -} - -func (e *testEnvSetter) Setenv(key string, val string) error { - if e.env == nil { - e.env = make(map[string]string) - } - e.env[key] = val - return nil -} diff --git a/api/controllers/install/hostpreflight.go b/api/controllers/install/hostpreflight.go deleted file mode 100644 index 997b75cf33..0000000000 --- a/api/controllers/install/hostpreflight.go +++ /dev/null @@ -1,46 +0,0 @@ -package install - -import ( - "context" - "fmt" - - "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" - "github.com/replicatedhq/embedded-cluster/api/types" - "github.com/replicatedhq/embedded-cluster/pkg/netutils" -) - -func (c *InstallController) RunHostPreflights(ctx context.Context, opts RunHostPreflightsOptions) error { - // Get the configured custom domains - ecDomains := utils.GetDomains(c.releaseData) - - // Prepare host preflights - hpf, err := c.hostPreflightManager.PrepareHostPreflights(ctx, preflight.PrepareHostPreflightOptions{ - ReplicatedAppURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), - ProxyRegistryURL: netutils.MaybeAddHTTPS(ecDomains.ProxyRegistryDomain), - HostPreflightSpec: c.releaseData.HostPreflights, - EmbeddedClusterConfig: c.releaseData.EmbeddedClusterConfig, - IsAirgap: c.airgapBundle != "", - IsUI: opts.IsUI, - }) - if err != nil { - return fmt.Errorf("failed to prepare host preflights: %w", err) - } - - // Run host preflights - return c.hostPreflightManager.RunHostPreflights(ctx, preflight.RunHostPreflightOptions{ - HostPreflightSpec: hpf, - }) -} - -func (c *InstallController) GetHostPreflightStatus(ctx context.Context) (*types.Status, error) { - return c.hostPreflightManager.GetHostPreflightStatus(ctx) -} - -func (c *InstallController) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) { - return c.hostPreflightManager.GetHostPreflightOutput(ctx) -} - -func (c *InstallController) GetHostPreflightTitles(ctx context.Context) ([]string, error) { - return c.hostPreflightManager.GetHostPreflightTitles(ctx) -} diff --git a/api/controllers/install/infra.go b/api/controllers/install/infra.go deleted file mode 100644 index f0c4e2e594..0000000000 --- a/api/controllers/install/infra.go +++ /dev/null @@ -1,45 +0,0 @@ -package install - -import ( - "context" - "fmt" - - "github.com/replicatedhq/embedded-cluster/api/types" -) - -func (c *InstallController) SetupInfra(ctx context.Context) error { - preflightStatus, err := c.GetHostPreflightStatus(ctx) - if err != nil { - return fmt.Errorf("get install host preflight status: %w", err) - } - - if preflightStatus.State != types.StateFailed && preflightStatus.State != types.StateSucceeded { - return fmt.Errorf("host preflight checks did not complete") - } - - if preflightStatus.State == types.StateFailed && c.metricsReporter != nil { - preflightOutput, err := c.GetHostPreflightOutput(ctx) - if err != nil { - return fmt.Errorf("get install host preflight output: %w", err) - } - if preflightOutput != nil { - c.metricsReporter.ReportPreflightsFailed(ctx, preflightOutput) - } - } - - // 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 { - return fmt.Errorf("install infra: %w", err) - } - - return nil -} - -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 deleted file mode 100644 index b1a122a704..0000000000 --- a/api/controllers/install/installation.go +++ /dev/null @@ -1,95 +0,0 @@ -package install - -import ( - "context" - "fmt" - - "github.com/replicatedhq/embedded-cluster/api/types" - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" - "github.com/replicatedhq/embedded-cluster/pkg/netutils" -) - -func (c *InstallController) GetInstallationConfig(ctx context.Context) (*types.InstallationConfig, error) { - config, err := c.installationManager.GetConfig() - if err != nil { - return nil, 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.ValidateConfig(config); err != nil { - return nil, fmt.Errorf("validate: %w", err) - } - - return config, nil -} - -func (c *InstallController) ConfigureInstallation(ctx context.Context, config *types.InstallationConfig) error { - if err := c.installationManager.ValidateConfig(config); err != nil { - return fmt.Errorf("validate: %w", err) - } - - if err := c.computeCIDRs(config); err != nil { - return fmt.Errorf("compute cidrs: %w", err) - } - - if err := c.installationManager.SetConfig(*config); err != nil { - return fmt.Errorf("write: %w", err) - } - - proxy, err := newconfig.GetProxySpec(config.HTTPProxy, config.HTTPSProxy, config.NoProxy, config.PodCIDR, config.ServiceCIDR, config.NetworkInterface, c.netUtils) - if err != nil { - return fmt.Errorf("get proxy spec: %w", err) - } - - networkSpec := ecv1beta1.NetworkSpec{ - NetworkInterface: config.NetworkInterface, - GlobalCIDR: config.GlobalCIDR, - PodCIDR: config.PodCIDR, - ServiceCIDR: config.ServiceCIDR, - NodePortRange: c.rc.NodePortRange(), - } - - // TODO (@team): discuss the distinction between the runtime config and the installation config - // update the runtime config - c.rc.SetDataDir(config.DataDirectory) - c.rc.SetLocalArtifactMirrorPort(config.LocalArtifactMirrorPort) - c.rc.SetAdminConsolePort(config.AdminConsolePort) - c.rc.SetProxySpec(proxy) - c.rc.SetNetworkSpec(networkSpec) - - // update process env vars from the runtime config - if err := c.rc.SetEnv(); err != nil { - return fmt.Errorf("set env vars: %w", err) - } - - if err := c.installationManager.ConfigureHost(ctx); err != nil { - return fmt.Errorf("configure: %w", err) - } - - return nil -} - -func (c *InstallController) computeCIDRs(config *types.InstallationConfig) error { - if config.GlobalCIDR != "" { - podCIDR, serviceCIDR, err := netutils.SplitNetworkCIDR(config.GlobalCIDR) - if err != nil { - return fmt.Errorf("split network cidr: %w", err) - } - config.PodCIDR = podCIDR - config.ServiceCIDR = serviceCIDR - } - - return nil -} - -func (c *InstallController) GetInstallationStatus(ctx context.Context) (*types.Status, error) { - return c.installationManager.GetStatus() -} diff --git a/api/controllers/install/controller.go b/api/controllers/linux/install/controller.go similarity index 68% rename from api/controllers/install/controller.go rename to api/controllers/linux/install/controller.go index 7e99a87f56..ce90a2e54e 100644 --- a/api/controllers/install/controller.go +++ b/api/controllers/linux/install/controller.go @@ -7,8 +7,10 @@ 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/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" @@ -19,17 +21,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) + 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) } type RunHostPreflightsOptions struct { @@ -39,23 +41,27 @@ type RunHostPreflightsOptions struct { var _ Controller = (*InstallController)(nil) type InstallController struct { - install *types.Install 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 + allowIgnoreHostPreflights bool } type InstallControllerOption func(*InstallController) @@ -108,9 +114,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 } } @@ -132,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 @@ -144,21 +156,37 @@ func WithHostPreflightManager(hostPreflightManager preflight.HostPreflightManage } } +func WithInfraManager(infraManager infra.InfraManager) InstallControllerOption { + return func(c *InstallController) { + c.infraManager = infraManager + } +} + +func WithStateMachine(stateMachine statemachine.Interface) InstallControllerOption { + return func(c *InstallController) { + c.stateMachine = stateMachine + } +} + +func WithStore(store store.Store) InstallControllerOption { + return func(c *InstallController) { + c.store = store + } +} + func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { controller := &InstallController{ - install: types.NewInstall(), + store: store.NewMemoryStore(), + rc: runtimeconfig.New(nil), + logger: logger.NewDiscardLogger(), } for _, opt := range opts { opt(controller) } - if controller.rc == nil { - controller.rc = runtimeconfig.New(nil) - } - - if controller.logger == nil { - controller.logger = logger.NewDiscardLogger() + if controller.stateMachine == nil { + controller.stateMachine = NewStateMachine(WithStateMachineLogger(controller.logger)) } if controller.hostUtils == nil { @@ -173,10 +201,9 @@ 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), + installation.WithInstallationStore(controller.store.InstallationStore()), + installation.WithLicense(controller.license), installation.WithAirgapBundle(controller.airgapBundle), installation.WithHostUtils(controller.hostUtils), installation.WithNetUtils(controller.netUtils), @@ -185,22 +212,19 @@ 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)), + preflight.WithHostPreflightStore(controller.store.PreflightStore()), preflight.WithNetUtils(controller.netUtils), ) } if controller.infraManager == nil { controller.infraManager = infra.NewInfraManager( - infra.WithRuntimeConfig(controller.rc), 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), + infra.WithLicense(controller.license), infra.WithAirgapBundle(controller.airgapBundle), infra.WithConfigValues(controller.configValues), infra.WithReleaseData(controller.releaseData), @@ -208,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 new file mode 100644 index 0000000000..4471bd445c --- /dev/null +++ b/api/controllers/linux/install/controller_test.go @@ -0,0 +1,1339 @@ +package install + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "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/types" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" + "github.com/replicatedhq/embedded-cluster/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + 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 + setupMock func(*installation.MockInstallationManager) + expectedErr bool + expectedValue types.InstallationConfig + }{ + { + name: "successful get", + setupMock: func(m *installation.MockInstallationManager) { + config := types.InstallationConfig{ + AdminConsolePort: 9000, + GlobalCIDR: "10.0.0.1/16", + } + + mock.InOrder( + m.On("GetConfig").Return(config, nil), + m.On("SetConfigDefaults", &config).Return(nil), + m.On("ValidateConfig", config, 9001).Return(nil), + ) + }, + expectedErr: false, + expectedValue: types.InstallationConfig{ + AdminConsolePort: 9000, + GlobalCIDR: "10.0.0.1/16", + }, + }, + { + name: "read config error", + setupMock: func(m *installation.MockInstallationManager) { + m.On("GetConfig").Return(nil, errors.New("read error")) + }, + expectedErr: true, + expectedValue: types.InstallationConfig{}, + }, + { + name: "set defaults error", + setupMock: func(m *installation.MockInstallationManager) { + config := types.InstallationConfig{} + mock.InOrder( + m.On("GetConfig").Return(config, nil), + m.On("SetConfigDefaults", &config).Return(errors.New("defaults error")), + ) + }, + expectedErr: true, + expectedValue: types.InstallationConfig{}, + }, + { + name: "validate error", + setupMock: func(m *installation.MockInstallationManager) { + config := types.InstallationConfig{} + mock.InOrder( + m.On("GetConfig").Return(config, nil), + m.On("SetConfigDefaults", &config).Return(nil), + m.On("ValidateConfig", config, 9001).Return(errors.New("validation error")), + ) + }, + expectedErr: true, + expectedValue: types.InstallationConfig{}, + }, + } + + for _, tt := range tests { + 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) + + controller, err := NewInstallController( + WithRuntimeConfig(rc), + WithInstallationManager(mockManager), + ) + require.NoError(t, err) + + result, err := controller.GetInstallationConfig(t.Context()) + + if tt.expectedErr { + assert.Error(t, err) + assert.Equal(t, types.InstallationConfig{}, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } + + mockManager.AssertExpectations(t) + }) + } +} + +func TestConfigureInstallation(t *testing.T) { + tests := []struct { + name string + config types.InstallationConfig + currentState statemachine.State + expectedState statemachine.State + setupMock func(*installation.MockInstallationManager, runtimeconfig.RuntimeConfig, types.InstallationConfig, *store.MockStore, *metrics.MockReporter) + expectedErr bool + }{ + { + name: "successful configure installation", + config: types.InstallationConfig{ + LocalArtifactMirrorPort: 9000, + DataDirectory: t.TempDir(), + }, + currentState: StateNew, + expectedState: StateHostConfigured, + + 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(nil), + ) + }, + expectedErr: false, + }, + { + name: "validatation error", + config: types.InstallationConfig{}, + currentState: StateNew, + 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, + }, + { + name: "set config error", + config: types.InstallationConfig{}, + currentState: StateNew, + 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, + }, + { + name: "configure host error", + config: types.InstallationConfig{ + LocalArtifactMirrorPort: 9000, + DataDirectory: t.TempDir(), + }, + currentState: StateNew, + 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, + }, + { + 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, st *store.MockStore, mr *metrics.MockReporter) { + // 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, 9001).Return(nil), + m.On("SetConfig", configWithCIDRs).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, st *store.MockStore, mr *metrics.MockReporter) { + }, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := runtimeconfig.New(nil, runtimeconfig.WithEnvSetter(&testEnvSetter{})) + rc.SetDataDir(t.TempDir()) + rc.SetManagerPort(9001) + + sm := NewStateMachine(WithCurrentState(tt.currentState)) + + mockManager := &installation.MockInstallationManager{} + metricsReporter := &metrics.MockReporter{} + mockStore := &store.MockStore{} + + 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.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) + metricsReporter.AssertExpectations(t) + mockStore.InfraMockStore.AssertExpectations(t) + mockStore.InstallationMockStore.AssertExpectations(t) + mockStore.PreflightMockStore.AssertExpectations(t) + }) + } +} + +// TestIntegrationComputeCIDRs tests the CIDR computation with real networking utility +func TestIntegrationComputeCIDRs(t *testing.T) { + tests := []struct { + name string + globalCIDR string + expectedPod string + expectedSvc string + expectedErr bool + }{ + { + name: "valid cidr 10.0.0.0/16", + globalCIDR: "10.0.0.0/16", + expectedPod: "10.0.0.0/17", + expectedSvc: "10.0.128.0/17", + expectedErr: false, + }, + { + name: "valid cidr 192.168.0.0/16", + globalCIDR: "192.168.0.0/16", + expectedPod: "192.168.0.0/17", + expectedSvc: "192.168.128.0/17", + expectedErr: false, + }, + { + name: "no global cidr", + globalCIDR: "", + expectedPod: "", // Should remain unchanged + expectedSvc: "", // Should remain unchanged + expectedErr: false, + }, + { + name: "invalid cidr", + globalCIDR: "not-a-cidr", + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller, err := NewInstallController() + require.NoError(t, err) + + config := types.InstallationConfig{ + GlobalCIDR: tt.globalCIDR, + } + + err = controller.computeCIDRs(&config) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedPod, config.PodCIDR) + assert.Equal(t, tt.expectedSvc, config.ServiceCIDR) + } + }) + } +} + +func TestRunHostPreflights(t *testing.T) { + expectedHPF := &troubleshootv1beta2.HostPreflightSpec{ + Collectors: []*troubleshootv1beta2.HostCollect{ + { + Time: &troubleshootv1beta2.HostTime{}, + }, + }, + } + + tests := []struct { + name string + currentState statemachine.State + expectedState statemachine.State + setupMocks func(*preflight.MockHostPreflightManager, runtimeconfig.RuntimeConfig, *metrics.MockReporter, *store.MockStore) + expectedErr bool + }{ + { + name: "successful run preflights without preflight errors", + 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(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, + }, + { + name: "prepare preflights error", + currentState: StateHostConfigured, + expectedState: StateHostConfigured, + 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")), + ) + }, + expectedErr: true, + }, + { + name: "run preflights 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(errors.New("run preflights error")), + ) + }, + 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, mr *metrics.MockReporter, st *store.MockStore) { + }, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rc := runtimeconfig.New(nil) + rc.SetDataDir(t.TempDir()) + rc.SetProxySpec(&ecv1beta1.ProxySpec{ + HTTPProxy: "http://proxy.example.com", + HTTPSProxy: "https://proxy.example.com", + ProvidedNoProxy: "provided-proxy.com", + NoProxy: "no-proxy.com", + }) + + sm := NewStateMachine(WithCurrentState(tt.currentState)) + + mockPreflightManager := &preflight.MockHostPreflightManager{} + 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) + + err = controller.RunHostPreflights(t.Context(), RunHostPreflightsOptions{}) + + if tt.expectedErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + 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) + mockReporter.AssertExpectations(t) + mockStore.InfraMockStore.AssertExpectations(t) + mockStore.InstallationMockStore.AssertExpectations(t) + mockStore.PreflightMockStore.AssertExpectations(t) + }) + } +} + +func TestGetHostPreflightStatus(t *testing.T) { + tests := []struct { + name string + setupMock func(*preflight.MockHostPreflightManager) + expectedErr bool + expectedValue types.Status + }{ + { + name: "successful get status", + setupMock: func(m *preflight.MockHostPreflightManager) { + status := types.Status{ + State: types.StateFailed, + } + m.On("GetHostPreflightStatus", t.Context()).Return(status, nil) + }, + expectedErr: false, + expectedValue: types.Status{ + State: types.StateFailed, + }, + }, + { + name: "get status error", + setupMock: func(m *preflight.MockHostPreflightManager) { + m.On("GetHostPreflightStatus", t.Context()).Return(nil, errors.New("get status error")) + }, + expectedErr: true, + expectedValue: types.Status{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockManager := &preflight.MockHostPreflightManager{} + tt.setupMock(mockManager) + + controller, err := NewInstallController(WithHostPreflightManager(mockManager)) + require.NoError(t, err) + + result, err := controller.GetHostPreflightStatus(t.Context()) + + if tt.expectedErr { + assert.Error(t, err) + assert.Equal(t, types.Status{}, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } + + mockManager.AssertExpectations(t) + }) + } +} + +func TestGetHostPreflightOutput(t *testing.T) { + tests := []struct { + name string + setupMock func(*preflight.MockHostPreflightManager) + expectedErr bool + expectedValue *types.HostPreflightsOutput + }{ + { + name: "successful get output", + setupMock: func(m *preflight.MockHostPreflightManager) { + output := successfulPreflightOutput + m.On("GetHostPreflightOutput", t.Context()).Return(output, nil) + }, + expectedErr: false, + expectedValue: successfulPreflightOutput, + }, + { + name: "get output error", + setupMock: func(m *preflight.MockHostPreflightManager) { + m.On("GetHostPreflightOutput", t.Context()).Return(nil, errors.New("get output error")) + }, + expectedErr: true, + expectedValue: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockManager := &preflight.MockHostPreflightManager{} + tt.setupMock(mockManager) + + controller, err := NewInstallController(WithHostPreflightManager(mockManager)) + require.NoError(t, err) + + result, err := controller.GetHostPreflightOutput(t.Context()) + + if tt.expectedErr { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } + + mockManager.AssertExpectations(t) + }) + } +} + +func TestGetHostPreflightTitles(t *testing.T) { + tests := []struct { + name string + setupMock func(*preflight.MockHostPreflightManager) + expectedErr bool + expectedValue []string + }{ + { + name: "successful get titles", + setupMock: func(m *preflight.MockHostPreflightManager) { + titles := []string{"Check 1", "Check 2"} + m.On("GetHostPreflightTitles", t.Context()).Return(titles, nil) + }, + expectedErr: false, + expectedValue: []string{"Check 1", "Check 2"}, + }, + { + name: "get titles error", + setupMock: func(m *preflight.MockHostPreflightManager) { + m.On("GetHostPreflightTitles", t.Context()).Return(nil, errors.New("get titles error")) + }, + expectedErr: true, + expectedValue: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockManager := &preflight.MockHostPreflightManager{} + tt.setupMock(mockManager) + + controller, err := NewInstallController(WithHostPreflightManager(mockManager)) + require.NoError(t, err) + + result, err := controller.GetHostPreflightTitles(t.Context()) + + if tt.expectedErr { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } + + mockManager.AssertExpectations(t) + }) + } +} + +func TestGetInstallationStatus(t *testing.T) { + tests := []struct { + name string + setupMock func(*installation.MockInstallationManager) + expectedErr bool + expectedValue types.Status + }{ + { + name: "successful get status", + setupMock: func(m *installation.MockInstallationManager) { + status := types.Status{ + State: types.StateRunning, + } + m.On("GetStatus").Return(status, nil) + }, + expectedErr: false, + expectedValue: types.Status{ + State: types.StateRunning, + }, + }, + { + name: "get status error", + setupMock: func(m *installation.MockInstallationManager) { + m.On("GetStatus").Return(nil, errors.New("get status error")) + }, + expectedErr: true, + expectedValue: types.Status{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockManager := &installation.MockInstallationManager{} + tt.setupMock(mockManager) + + controller, err := NewInstallController(WithInstallationManager(mockManager)) + require.NoError(t, err) + + result, err := controller.GetInstallationStatus(t.Context()) + + if tt.expectedErr { + assert.Error(t, err) + assert.Equal(t, types.Status{}, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } + + mockManager.AssertExpectations(t) + }) + } +} + +func TestSetupInfra(t *testing.T) { + tests := []struct { + 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, *store.MockStore) + expectedErr error + }{ + { + 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, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + fm.On("Install", mock.Anything, rc).Return(nil), + mr.On("ReportInstallationSucceeded", mock.Anything), + ) + }, + expectedErr: nil, + }, + { + 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, mr *metrics.MockReporter, st *store.MockStore) { + mock.InOrder( + 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, + }, + { + 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, mr *metrics.MockReporter, st *store.MockStore) { + }, + expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), + }, + { + name: "install infra error", + 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).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: nil, + }, + { + name: "install infra error without report if infra store fails", + 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).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, + }, + { + 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, mr *metrics.MockReporter, st *store.MockStore) { + }, + 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", + clientIgnoreHostPreflights: true, + serverAllowIgnoreHostPreflights: false, + currentState: StatePreflightsFailed, + expectedState: StatePreflightsFailed, + 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: "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, mr *metrics.MockReporter, st *store.MockStore) { + }, + expectedErr: types.NewBadRequestError(ErrPreflightChecksFailed), + }, + } + + 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) + + mockPreflightManager := &preflight.MockHostPreflightManager{} + mockInstallationManager := &installation.MockInstallationManager{} + mockInfraManager := &infra.MockInfraManager{} + mockMetricsReporter := &metrics.MockReporter{} + mockStore := &store.MockStore{} + tt.setupMocks(rc, mockPreflightManager, mockInstallationManager, mockInfraManager, mockMetricsReporter, mockStore) + + controller, err := NewInstallController( + WithRuntimeConfig(rc), + WithStateMachine(sm), + WithHostPreflightManager(mockPreflightManager), + WithInstallationManager(mockInstallationManager), + WithInfraManager(mockInfraManager), + WithAllowIgnoreHostPreflights(tt.serverAllowIgnoreHostPreflights), + WithMetricsReporter(mockMetricsReporter), + WithStore(mockStore), + ) + require.NoError(t, err) + + err = controller.SetupInfra(t.Context(), tt.clientIgnoreHostPreflights) + + 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) + } + + 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", 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) + }) + } +} + +func TestGetInfra(t *testing.T) { + tests := []struct { + name string + setupMock func(*infra.MockInfraManager) + expectedErr bool + expectedValue types.Infra + }{ + { + name: "successful get infra", + setupMock: func(m *infra.MockInfraManager) { + infra := types.Infra{ + Components: []types.InfraComponent{ + { + Name: infra.K0sComponentName, + Status: types.Status{ + State: types.StateRunning, + }, + }, + }, + Status: types.Status{ + State: types.StateRunning, + }, + } + m.On("Get").Return(infra, nil) + }, + expectedErr: false, + expectedValue: types.Infra{ + Components: []types.InfraComponent{ + { + Name: infra.K0sComponentName, + Status: types.Status{ + State: types.StateRunning, + }, + }, + }, + Status: types.Status{ + State: types.StateRunning, + }, + }, + }, + { + name: "get infra error", + setupMock: func(m *infra.MockInfraManager) { + m.On("Get").Return(nil, errors.New("get infra error")) + }, + expectedErr: true, + expectedValue: types.Infra{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockManager := &infra.MockInfraManager{} + tt.setupMock(mockManager) + + controller, err := NewInstallController(WithInfraManager(mockManager)) + require.NoError(t, err) + + result, err := controller.GetInfra(t.Context()) + + if tt.expectedErr { + assert.Error(t, err) + assert.Equal(t, types.Infra{}, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } + + mockManager.AssertExpectations(t) + }) + } +} + +func TestGetStatus(t *testing.T) { + tests := []struct { + name string + install types.Install + expectedValue types.Status + }{ + { + name: "successful get status", + install: types.Install{ + Status: types.Status{ + State: types.StateFailed, + }, + }, + expectedValue: types.Status{ + State: types.StateFailed, + }, + }, + { + name: "empty status", + install: types.Install{}, + expectedValue: types.Status{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller := &InstallController{ + install: tt.install, + } + + result, err := controller.GetStatus(t.Context()) + + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + }) + } +} + +func TestSetStatus(t *testing.T) { + tests := []struct { + name string + status types.Status + expectedErr bool + }{ + { + name: "successful set status", + status: types.Status{ + State: types.StateFailed, + }, + expectedErr: false, + }, + { + name: "nil status", + status: types.Status{}, + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + controller, err := NewInstallController() + require.NoError(t, err) + + err = controller.SetStatus(t.Context(), tt.status) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.status, controller.install.Status) + } + }) + } +} + +func getTestReleaseData() *release.ReleaseData { + return &release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{}, + } +} + +type testEnvSetter struct { + env map[string]string +} + +func (e *testEnvSetter) Setenv(key string, val string) error { + if e.env == nil { + e.env = make(map[string]string) + } + e.env[key] = val + return nil +} diff --git a/api/controllers/linux/install/hostpreflight.go b/api/controllers/linux/install/hostpreflight.go new file mode 100644 index 0000000000..33f7eb741e --- /dev/null +++ b/api/controllers/linux/install/hostpreflight.go @@ -0,0 +1,120 @@ +package install + +import ( + "context" + "fmt" + "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" +) + +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) + } + + // Get the configured custom domains + ecDomains := utils.GetDomains(c.releaseData) + + // Prepare host preflights + hpf, err := c.hostPreflightManager.PrepareHostPreflights(ctx, c.rc, preflight.PrepareHostPreflightOptions{ + ReplicatedAppURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), + ProxyRegistryURL: netutils.MaybeAddHTTPS(ecDomains.ProxyRegistryDomain), + HostPreflightSpec: c.releaseData.HostPreflights, + EmbeddedClusterConfig: c.releaseData.EmbeddedClusterConfig, + IsAirgap: c.airgapBundle != "", + IsUI: opts.IsUI, + }) + if err != nil { + return fmt.Errorf("prepare host preflights: %w", err) + } + + err = c.stateMachine.Transition(lock, StatePreflightsRunning) + if err != nil { + return fmt.Errorf("transition states: %w", err) + } + + go func() (finalErr error) { + // 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 { + 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, 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) + } + }() + + err := c.hostPreflightManager.RunHostPreflights(ctx, c.rc, preflight.RunHostPreflightOptions{ + HostPreflightSpec: hpf, + }) + if err != nil { + return fmt.Errorf("run host preflights: %w", err) + } + + return nil + }() + + 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) +} + +func (c *InstallController) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightsOutput, error) { + return c.hostPreflightManager.GetHostPreflightOutput(ctx) +} + +func (c *InstallController) GetHostPreflightTitles(ctx context.Context) ([]string, error) { + return c.hostPreflightManager.GetHostPreflightTitles(ctx) +} diff --git a/api/controllers/linux/install/infra.go b/api/controllers/linux/install/infra.go new file mode 100644 index 0000000000..cf7adeb2f8 --- /dev/null +++ b/api/controllers/linux/install/infra.go @@ -0,0 +1,82 @@ +package install + +import ( + "context" + "errors" + "fmt" + "runtime/debug" + + "github.com/replicatedhq/embedded-cluster/api/types" +) + +var ( + ErrPreflightChecksFailed = errors.New("preflight checks failed") +) + +func (c *InstallController) SetupInfra(ctx context.Context, ignoreHostPreflights bool) (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() + } + }() + + // 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) + } + + go func() (finalErr error) { + // 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 { + finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) + } + if finalErr != nil { + c.logger.Error(finalErr) + + if err := c.stateMachine.Transition(lock, StateInfrastructureInstallFailed); 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) + } + } + }() + + if err := c.infraManager.Install(ctx, c.rc); err != nil { + return fmt.Errorf("failed to install infrastructure: %w", err) + } + + return nil + }() + + 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 new file mode 100644 index 0000000000..037e1665d7 --- /dev/null +++ b/api/controllers/linux/install/installation.go @@ -0,0 +1,157 @@ +package install + +import ( + "context" + "fmt" + "time" + + "github.com/replicatedhq/embedded-cluster/api/types" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" +) + +func (c *InstallController) GetInstallationConfig(ctx context.Context) (types.InstallationConfig, error) { + config, err := c.installationManager.GetConfig() + if err != nil { + return types.InstallationConfig{}, 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 types.InstallationConfig{}, fmt.Errorf("validate: %w", err) + } + + return config, nil +} + +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) + 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 { + c.logger.Error("failed to transition states", "error", err) + } + } + }() + + return nil +} + +func (c *InstallController) configureInstallation(ctx context.Context, config types.InstallationConfig) (finalErr 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) + } + + 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) + } + + if err := c.computeCIDRs(&config); err != nil { + return fmt.Errorf("compute cidrs: %w", err) + } + + if err := c.installationManager.SetConfig(config); err != nil { + return fmt.Errorf("write: %w", err) + } + + proxy, err := newconfig.GetProxySpec(config.HTTPProxy, config.HTTPSProxy, config.NoProxy, config.PodCIDR, config.ServiceCIDR, config.NetworkInterface, c.netUtils) + if err != nil { + return fmt.Errorf("get proxy spec: %w", err) + } + + networkSpec := ecv1beta1.NetworkSpec{ + NetworkInterface: config.NetworkInterface, + GlobalCIDR: config.GlobalCIDR, + PodCIDR: config.PodCIDR, + ServiceCIDR: config.ServiceCIDR, + NodePortRange: c.rc.NodePortRange(), + } + + // TODO (@team): discuss the distinction between the runtime config and the installation config + // update the runtime config + c.rc.SetDataDir(config.DataDirectory) + c.rc.SetLocalArtifactMirrorPort(config.LocalArtifactMirrorPort) + c.rc.SetAdminConsolePort(config.AdminConsolePort) + c.rc.SetProxySpec(proxy) + c.rc.SetNetworkSpec(networkSpec) + + // update process env vars from the runtime config + if err := c.rc.SetEnv(); err != nil { + return fmt.Errorf("set env vars: %w", err) + } + + err = c.stateMachine.Transition(lock, StateInstallationConfigured) + if err != nil { + return fmt.Errorf("failed to transition states: %w", err) + } + + return nil +} + +func (c *InstallController) computeCIDRs(config *types.InstallationConfig) error { + if config.GlobalCIDR != "" { + podCIDR, serviceCIDR, err := netutils.SplitNetworkCIDR(config.GlobalCIDR) + if err != nil { + return fmt.Errorf("split network cidr: %w", err) + } + config.PodCIDR = podCIDR + config.ServiceCIDR = serviceCIDR + } + + return nil +} + +func (c *InstallController) GetInstallationStatus(ctx context.Context) (types.Status, error) { + return c.installationManager.GetStatus() +} diff --git a/api/controllers/linux/install/reporting_handlers.go b/api/controllers/linux/install/reporting_handlers.go new file mode 100644 index 0000000000..7c8f497878 --- /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 new file mode 100644 index 0000000000..ece8fbb6d3 --- /dev/null +++ b/api/controllers/linux/install/statemachine.go @@ -0,0 +1,83 @@ +package install + +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 execution succeeded but the preflights detected issues on the host + StatePreflightsFailed statemachine.State = "PreflightsFailed" + // 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" +) + +var validStateTransitions = map[statemachine.State][]statemachine.State{ + 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) + +func WithCurrentState(currentState statemachine.State) StateMachineOption { + return func(o *StateMachineOptions) { + o.CurrentState = currentState + } +} + +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, statemachine.WithLogger(options.Logger)) +} diff --git a/api/controllers/linux/install/statemachine_test.go b/api/controllers/linux/install/statemachine_test.go new file mode 100644 index 0000000000..508b6a5216 --- /dev/null +++ b/api/controllers/linux/install/statemachine_test.go @@ -0,0 +1,171 @@ +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" or "InstallationConfigurationFailed"`, + startState: StateNew, + validTransitions: []statemachine.State{ + StateInstallationConfigured, + StateInstallationConfigurationFailed, + }, + }, + { + 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" or "InstallationConfigurationFailed"`, + startState: StateHostConfigured, + validTransitions: []statemachine.State{ + StatePreflightsRunning, + StateInstallationConfigured, + StateInstallationConfigurationFailed, + }, + }, + { + name: `State "PreflightsRunning" can transition to "PreflightsSucceeded", "PreflightsFailed", or "PreflightsExecutionFailed"`, + startState: StatePreflightsRunning, + validTransitions: []statemachine.State{ + StatePreflightsSucceeded, + StatePreflightsFailed, + StatePreflightsExecutionFailed, + }, + }, + { + 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", "InstallationConfigured", or "InstallationConfigurationFailed"`, + startState: StatePreflightsFailed, + validTransitions: []statemachine.State{ + StatePreflightsFailedBypassed, + StatePreflightsRunning, + StateInstallationConfigured, + StateInstallationConfigurationFailed, + }, + }, + { + 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 "InfrastructureInstallFailed"`, + startState: StateInfrastructureInstalling, + validTransitions: []statemachine.State{ + StateSucceeded, + StateInfrastructureInstallFailed, + }, + }, + { + name: `State "InfrastructureInstallFailed" can not transition to any other state`, + startState: StateInfrastructureInstallFailed, + validTransitions: []statemachine.State{}, + }, + { + name: `State "Succeeded" can not transition to any other state`, + startState: StateSucceeded, + 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, + StateInfrastructureInstallFailed, + } + + 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/controllers/install/status.go b/api/controllers/linux/install/status.go similarity index 71% rename from api/controllers/install/status.go rename to api/controllers/linux/install/status.go index f8359e3ae2..442ee8aeab 100644 --- a/api/controllers/install/status.go +++ b/api/controllers/linux/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/docs/docs.go b/api/docs/docs.go index bd222dede3..efc372c7dd 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},"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.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","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 68ba5f3bfd..ff60253c16 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},"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.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","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 d3cdcdb6db..f2b37d253f 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: @@ -61,6 +69,8 @@ components: $ref: '#/components/schemas/types.InfraComponent' type: array uniqueItems: false + logs: + type: string status: $ref: '#/components/schemas/types.Status' type: object @@ -71,8 +81,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: @@ -152,6 +169,7 @@ paths: /auth/login: post: description: Authenticate a user + operationId: postAuthLogin requestBody: content: application/json: @@ -175,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: @@ -188,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: @@ -210,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: @@ -226,10 +261,18 @@ 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: + schema: + $ref: '#/components/schemas/types.InfraSetupRequest' + description: Infra Setup Request + required: true responses: "200": content: @@ -241,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: @@ -256,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: @@ -271,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: @@ -293,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: @@ -308,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: @@ -323,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: @@ -344,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 0000000000..b61ea5c787 --- /dev/null +++ b/api/handlers.go @@ -0,0 +1,63 @@ +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" +) + +type handlers struct { + auth *authhandler.Handler + console *consolehandler.Handler + health *healthhandler.Handler + linux *linuxhandler.Handler +} + +func (a *API) initHandlers() error { + // Auth handler + authHandler, err := authhandler.New( + a.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( + a.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 e38709b18e..0000000000 --- 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 f9dedc01c6..0000000000 --- a/api/install.go +++ /dev/null @@ -1,235 +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, - } - - 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 -// @Produce json -// @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()) - 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/assets/license.yaml b/api/integration/assets/license.yaml new file mode 100644 index 0000000000..ec35c3b0d8 --- /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/auth_controller_test.go b/api/integration/auth_controller_test.go index a625ba3112..02aef1ffd3 100644 --- a/api/integration/auth_controller_test.go +++ b/api/integration/auth_controller_test.go @@ -11,25 +11,27 @@ 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/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" ) 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,13 +162,12 @@ 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 - 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/console_test.go b/api/integration/console_test.go index 169e789110..f16b6b5e09 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" @@ -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 00e0d7c573..587e4199a3 100644 --- a/api/integration/hostpreflights_test.go +++ b/api/integration/hostpreflights_test.go @@ -10,9 +10,11 @@ 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" + 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,17 +58,23 @@ 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 - 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()), ) @@ -79,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() @@ -104,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() @@ -131,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()), ) @@ -142,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() @@ -161,6 +171,93 @@ 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 := linuxinstall.NewInstallController(linuxinstall.WithHostPreflightManager(manager)) + require.NoError(t, err) + + // Create the API with allow ignore host preflights flag + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + LinuxConfig: types.LinuxConfig{ + AllowIgnoreHostPreflights: tt.allowIgnoreHostPreflights, + }, + }, + api.WithLinuxInstallController(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.MethodGet, "/linux/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 @@ -172,26 +269,27 @@ 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( - preflight.WithRuntimeConfig(rc), preflight.WithPreflightRunner(runner), ) // Create an installation manager iManager := installation.NewInstallationManager( - installation.WithRuntimeConfig(rc), - installation.WithInstallationStore(installation.NewMemoryStore(inst)), + installation.WithInstallationStore(installationstore.NewMemoryStore(installationstore.WithInstallation(inst))), ) // Create an install controller with the mocked manager - installController, err := install.NewInstallController( - install.WithHostPreflightManager(pfManager), - install.WithInstallationManager(iManager), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), + )), + 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{ @@ -200,7 +298,7 @@ func TestPostRunHostPreflights(t *testing.T) { }, }, }), - install.WithRuntimeConfig(rc), + linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) @@ -232,8 +330,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()), ) @@ -244,7 +344,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() @@ -263,7 +363,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") @@ -288,20 +388,25 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller - installController, err := install.NewInstallController( - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), + )), + 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()), ) @@ -312,7 +417,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() @@ -343,20 +448,25 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller with the failing manager - installController, err := install.NewInstallController( - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), + )), + 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()), ) @@ -366,7 +476,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() @@ -399,20 +509,25 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create an install controller with the failing manager - installController, err := install.NewInstallController( - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StateHostConfigured), + )), + 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()), ) @@ -422,7 +537,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() @@ -441,7 +556,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") @@ -458,30 +573,35 @@ 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 - installController, err := install.NewInstallController( - install.WithHostPreflightManager(manager), - install.WithReleaseData(&release.ReleaseData{ + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine( + linuxinstall.WithCurrentState(linuxinstall.StatePreflightsRunning), + )), + 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()), ) @@ -491,7 +611,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 3adc99c21d..f71655e95f 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -3,31 +3,57 @@ package integration import ( "bytes" "context" + _ "embed" "encoding/json" + "errors" + "fmt" "net" "net/http" "net/http/httptest" + "os" "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" - "github.com/replicatedhq/embedded-cluster/api/controllers/install" + apiclient "github.com/replicatedhq/embedded-cluster/api/client" + 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" + 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/constants" "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" ) -// Mock implementation of the install.Controller interface +var ( + //go:embed assets/license.yaml + licenseData []byte +) + +// Mock implementation of the linuxinstall.Controller interface type mockInstallController struct { configureInstallationError error getInstallationConfigError error @@ -41,33 +67,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 { +func (m *mockInstallController) RunHostPreflights(ctx context.Context, opts linuxinstall.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) { @@ -84,23 +110,23 @@ 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 } -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) { @@ -111,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) }{ @@ -143,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()) @@ -197,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()) @@ -230,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, }, } @@ -250,17 +289,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.WithHostUtils(tc.mockHostUtils), - install.WithNetUtils(tc.mockNetUtils), + installController, err := linuxinstall.NewInstallController( + linuxinstall.WithRuntimeConfig(rc), + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StateNew))), + 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()), ) @@ -275,7 +317,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() @@ -284,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()) @@ -293,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 @@ -305,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 { @@ -344,15 +389,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), + 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()), ) @@ -376,7 +424,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() @@ -393,7 +441,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) } @@ -404,14 +452,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), + 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()), ) @@ -421,7 +472,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") @@ -445,8 +496,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()), ) @@ -464,7 +517,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() @@ -487,9 +540,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) @@ -506,8 +559,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()), ) @@ -520,7 +575,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() @@ -559,16 +614,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()), ) @@ -579,7 +636,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() @@ -606,7 +663,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() @@ -632,8 +689,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()), ) @@ -643,7 +702,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() @@ -662,10 +721,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 @@ -673,13 +732,15 @@ 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 apiInstance, err := api.New( - "password", - api.WithInstallController(installController), + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithLogger(logger.NewDiscardLogger()), ) @@ -692,7 +753,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() @@ -716,7 +777,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() @@ -742,8 +803,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()), ) @@ -753,7 +816,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() @@ -772,16 +835,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()), ) @@ -805,7 +870,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() @@ -839,7 +904,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") @@ -857,7 +922,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") @@ -885,8 +950,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()), ) @@ -904,7 +971,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() @@ -933,14 +1000,13 @@ func TestInstallWithAPIClient(t *testing.T) { // Create a config manager installationManager := installation.NewInstallationManager( - installation.WithRuntimeConfig(rc), installation.WithHostUtils(mockHostUtils), ) // 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) @@ -965,9 +1031,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) @@ -981,14 +1049,13 @@ 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 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) @@ -1002,7 +1069,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) }) @@ -1019,12 +1085,11 @@ 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 + var installStatus types.Status if !assert.Eventually(t, func() bool { installStatus, err = c.GetInstallationStatus() require.NoError(t, err, "GetInstallationStatus should succeed") @@ -1048,7 +1113,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 @@ -1064,18 +1129,14 @@ 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 t.Run("SetInstallStatus", func(t *testing.T) { // Create a status - status := &types.Status{ + status := types.Status{ State: types.StateFailed, Description: "Installation failed", } @@ -1083,11 +1144,734 @@ 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") }) } +// 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 infra manager with mocks + infraManager := infra.NewInfraManager( + infra.WithK0s(k0sMock), + infra.WithKubeClient(fakeKcli), + infra.WithMetadataClient(fakeMcli), + infra.WithHelmClient(helmMock), + infra.WithLicense(licenseData), + 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 := 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{ + ReplicatedAppDomain: "replicated.example.com", + ProxyRegistryDomain: "some-proxy.example.com", + }, + }, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(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 with proper JSON body + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + 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() + + // 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, "/linux/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.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: constants.KotsadmNamespace}, &gotKotsadmNamespace) + require.NoError(t, err) + + var gotKotsadmPasswordSecret corev1.Secret + err = fakeKcli.Get(t.Context(), client.ObjectKey{Namespace: constants.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( + types.APIConfig{ + Password: "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 with proper JSON body + requestBody := types.InfraSetupRequest{ + IgnoreHostPreflights: false, + } + reqBodyBytes, err := json.Marshal(requestBody) + require.NoError(t, err) + + 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() + + // 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 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 := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsFailed))), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(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, "/linux/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 := 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) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(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, "/linux/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 := linuxinstall.NewInstallController( + linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(linuxinstall.StatePreflightsFailed))), + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithAllowIgnoreHostPreflights(true), // CLI flag allows bypass + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(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, "/linux/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) + 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 := 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( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // 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, "/linux/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 + assert.Equal(t, http.StatusConflict, rec.Code) + assert.Contains(t, rec.Body.String(), "invalid transition") + }) + + // Test k0s already installed error + t.Run("K0s already installed", func(t *testing.T) { + // 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))), + ) + + // Create an install controller + 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{}, + }), + ) + require.NoError(t, err) + + // Create the API with the install controller + apiInstance, err := api.New( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // 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, "/linux/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 + assert.Equal(t, http.StatusConflict, rec.Code) + assert.Contains(t, rec.Body.String(), "invalid transition") + }) + + // 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 managers + pfManager := preflight.NewHostPreflightManager( + preflight.WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(hpf))), + ) + infraManager := infra.NewInfraManager( + infra.WithK0s(k0sMock), + infra.WithHostUtils(hostutilsMock), + infra.WithLicense(licenseData), + ) + + // 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 := linuxinstall.NewInstallController( + linuxinstall.WithHostPreflightManager(pfManager), + linuxinstall.WithInfraManager(infraManager), + linuxinstall.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{}, + }), + 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( + types.APIConfig{ + Password: "password", + }, + api.WithLinuxInstallController(installController), + api.WithAuthController(&staticAuthController{"TOKEN"}), + api.WithLogger(logger.NewDiscardLogger()), + ) + require.NoError(t, err) + + router := mux.NewRouter() + apiInstance.RegisterRoutes(router) + + // 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, "/linux/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 + 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, "/linux/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/handlers/auth/auth.go b/api/internal/handlers/auth/auth.go new file mode 100644 index 0000000000..5f1cc8568a --- /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 0000000000..34e5e3f17c --- /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 0000000000..c9035a25c7 --- /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 0000000000..7f033c7672 --- /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 0000000000..efcb3687b3 --- /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 0000000000..976c5f6169 --- /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 0000000000..57fc071a55 --- /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 8a35376cc5..3d6e26ca2c 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/internal/managers/infra/install.go b/api/internal/managers/infra/install.go index fba39c5f53..17aae15e67 100644 --- a/api/internal/managers/infra/install.go +++ b/api/internal/managers/infra/install.go @@ -6,27 +6,26 @@ 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" - "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" - "github.com/replicatedhq/embedded-cluster/pkg-new/k0s" + "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" 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" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "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" + kyaml "sigs.k8s.io/yaml" ) const K0sComponentName = "Runtime" @@ -38,56 +37,45 @@ func AlreadyInstalledError() error { ) } -func (m *infraManager) Install(ctx context.Context, config *types.InstallationConfig) (finalErr error) { - m.mu.Lock() - defer m.mu.Unlock() - - installed, err := k0s.IsInstalled() +func (m *infraManager) Install(ctx context.Context, rc runtimeconfig.RuntimeConfig) (finalErr error) { + 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 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 { - return fmt.Errorf("init components: %w", err) - } - if err := m.setStatus(types.StateRunning, ""); err != nil { return fmt.Errorf("set status: %w", err) } - // Run install in background - go m.install(context.Background(), config, license) + 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") + } + } + }() + + if err := m.install(ctx, rc); err != nil { + return err + } 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{ - 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()}) } @@ -102,49 +90,43 @@ 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) { - 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, config) + _, err := m.installK0s(ctx, rc) if err != nil { 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() + hcli, err := m.helmClient(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, config, license, kcli, mcli, hcli); err != nil { + if err := m.installAddOns(ctx, kcli, mcli, hcli, license, rc); err != nil { return fmt.Errorf("install addons: %w", err) } @@ -156,14 +138,14 @@ func (m *infraManager) install(ctx context.Context, config *types.InstallationCo 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) } 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 { @@ -185,67 +167,76 @@ func (m *infraManager) installK0s(ctx context.Context, config *types.Installatio } }() - m.logger.Debug("creating k0s configuration file") - k0sCfg, err := k0s.WriteK0sConfig(ctx, config.NetworkInterface, m.airgapBundle, config.PodCIDR, config.ServiceCIDR, m.endUserConfig, nil) + m.setStatusDesc(fmt.Sprintf("Installing %s", componentName)) + + logFn := m.logFn("k0s") + + logFn("creating k0s configuration file") + k0sCfg, err := m.k0scli.WriteK0sConfig(ctx, rc.NetworkInterface(), m.airgapBundle, rc.PodCIDR(), rc.ServiceCIDR(), m.endUserConfig, nil) if err != nil { return nil, fmt.Errorf("create config file: %w", err) } - m.logger.Debug("creating systemd unit files") - if err := hostutils.CreateSystemdUnitFiles(ctx, m.logger, m.rc, false); err != nil { + logFn("creating systemd unit files") + if err := m.hostUtils.CreateSystemdUnitFiles(ctx, m.logger, rc, false); err != nil { return nil, fmt.Errorf("create systemd unit files: %w", err) } - m.logger.Debug("installing k0s") - if err := k0s.Install(m.rc, config.NetworkInterface); err != nil { + logFn("installing k0s") + if err := m.k0scli.Install(rc); err != nil { return nil, fmt.Errorf("install cluster: %w", err) } - m.logger.Debug("waiting for k0s to be ready") - if err := k0s.WaitForK0s(); err != nil { + logFn("waiting for k0s to be ready") + 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) } - 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") - registryIP, err := registry.GetRegistryClusterIP(config.ServiceCIDR) + logFn("adding registry to containerd") + registryIP, err := registry.GetRegistryClusterIP(rc.ServiceCIDR()) 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) } 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 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, ConfigSpec: m.getECConfigSpec(), MetricsBaseURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), - RuntimeConfig: m.rc.Get(), + RuntimeConfig: rc.Get(), EndUserConfig: m.endUserConfig, }) if err != nil { 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,40 +244,54 @@ 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 { - // get the configured custom domains - ecDomains := utils.GetDomains(m.releaseData) - +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) 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), - addons.WithRuntimeConfig(m.rc), + addons.WithDomains(utils.GetDomains(m.releaseData)), addons.WithProgressChannel(progressChan), ) - m.logger.Debugf("installing addons") - if err := addOns.Install(ctx, addons.InstallOptions{ + opts := m.getAddonInstallOpts(license, rc) + + logFn("installing addons") + 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, + AdminConsolePort: rc.AdminConsolePort(), License: license, IsAirgap: m.airgapBundle != "", TLSCertBytes: m.tlsConfig.CertBytes, @@ -296,12 +301,23 @@ func (m *infraManager) installAddOns( IsMultiNodeEnabled: license.Spec.IsEmbeddedClusterMultiNodeEnabled, EmbeddedConfigSpec: m.getECConfigSpec(), EndUserConfigSpec: m.getEndUserConfigSpec(), - KotsInstaller: func() error { + 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 + opts.KotsInstaller = m.kotsInstaller + } else { + opts.KotsInstaller = func() error { opts := kotscli.InstallOptions{ - RuntimeConfig: m.rc, + RuntimeConfig: rc, AppSlug: license.Spec.AppSlug, - LicenseFile: m.licenseFile, - Namespace: runtimeconfig.KotsadmNamespace, + License: m.license, + Namespace: constants.KotsadmNamespace, AirgapBundle: m.airgapBundle, ConfigValuesFile: m.configValues, ReplicatedAppEndpoint: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), @@ -309,12 +325,10 @@ func (m *infraManager) installAddOns( // 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) { @@ -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/manager.go b/api/internal/managers/infra/manager.go index 7d673ff6a5..4d8103a33a 100644 --- a/api/internal/managers/infra/manager.go +++ b/api/internal/managers/infra/manager.go @@ -4,59 +4,57 @@ 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) - Install(ctx context.Context, config *types.InstallationConfig) 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 - rc runtimeconfig.RuntimeConfig + infraStore infra.Store password string tlsConfig types.TLSConfig - licenseFile string + license []byte airgapBundle string configValues string 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 } 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 } } -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 } @@ -74,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 } } @@ -104,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{} @@ -112,25 +146,25 @@ func NewInfraManager(opts ...InfraManagerOption) *infraManager { opt(manager) } - if manager.rc == nil { - manager.rc = runtimeconfig.New(nil) - } - if manager.logger == nil { 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 426a49500f..12345c5db0 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,15 +15,15 @@ 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) } -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 05db00980f..a7b303b1fc 100644 --- a/api/internal/managers/infra/status.go +++ b/api/internal/managers/infra/status.go @@ -1,13 +1,12 @@ package infra import ( - "fmt" "time" "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() } @@ -15,23 +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 == nil { - return false, nil - } - 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, @@ -40,14 +22,12 @@ 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{ + 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 a6a4b72916..c003433a82 100644 --- a/api/internal/managers/infra/status_test.go +++ b/api/internal/managers/infra/status_test.go @@ -2,110 +2,21 @@ 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) -} - -func TestInstallDidRun(t *testing.T) { - tests := []struct { - name string - currentStatus *types.Status - expectedResult bool - expectedErr bool - }{ - { - name: "nil status", - currentStatus: nil, - 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() - if tt.currentStatus != nil { - 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) - } - }) - } + assert.Contains(t, infra.Logs, "[test] Test log message") + assert.Contains(t, infra.Logs, "[test] Another log message with arg: value") } diff --git a/api/internal/managers/infra/store.go b/api/internal/managers/infra/store.go deleted file mode 100644 index fb4de7af9e..0000000000 --- a/api/internal/managers/infra/store.go +++ /dev/null @@ -1,80 +0,0 @@ -package infra - -import ( - "fmt" - "sync" - "time" - - "github.com/replicatedhq/embedded-cluster/api/types" -) - -// 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 - RegisterComponent(name string) error - SetComponentStatus(name string, status *types.Status) error -} - -// memoryStore is an in-memory implementation of Store -type memoryStore struct { - infra *types.Infra - mu sync.RWMutex -} - -// NewMemoryStore creates a new memory store -func NewMemoryStore(infra *types.Infra) Store { - return &memoryStore{ - infra: infra, - } -} - -func (s *memoryStore) Get() (*types.Infra, error) { - s.mu.RLock() - defer s.mu.RUnlock() - return s.infra, nil -} - -func (s *memoryStore) GetStatus() (*types.Status, error) { - s.mu.RLock() - defer s.mu.RUnlock() - return s.infra.Status, nil -} - -func (s *memoryStore) SetStatus(status types.Status) error { - s.mu.Lock() - defer s.mu.Unlock() - s.infra.Status = &status - return nil -} - -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{ - State: types.StatePending, - Description: "", - LastUpdated: time.Now(), - }, - }) - - return nil -} - -func (s *memoryStore) SetComponentStatus(name string, status *types.Status) error { - s.mu.Lock() - defer s.mu.Unlock() - - for i, component := range s.infra.Components { - if component.Name == name { - s.infra.Components[i].Status = status - return nil - } - } - - return fmt.Errorf("component %s not found", name) -} diff --git a/api/internal/managers/infra/store_mock.go b/api/internal/managers/infra/store_mock.go deleted file mode 100644 index 4c03580bef..0000000000 --- a/api/internal/managers/infra/store_mock.go +++ /dev/null @@ -1,44 +0,0 @@ -package infra - -import ( - "github.com/replicatedhq/embedded-cluster/api/types" - "github.com/stretchr/testify/mock" -) - -var _ Store = (*MockStore)(nil) - -// MockStore is a mock implementation of Store -type MockStore struct { - mock.Mock -} - -func (m *MockStore) Get() (*types.Infra, error) { - args := m.Called() - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*types.Infra), args.Error(1) -} - -func (m *MockStore) 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) -} - -func (m *MockStore) SetStatus(status types.Status) error { - args := m.Called(status) - return args.Error(0) -} - -func (m *MockStore) RegisterComponent(name string) error { - args := m.Called(name) - return args.Error(0) -} - -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/util.go b/api/internal/managers/infra/util.go index 5e8405c5d6..46e2b8ca56 100644 --- a/api/internal/managers/infra/util.go +++ b/api/internal/managers/infra/util.go @@ -9,7 +9,9 @@ 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" + "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -25,15 +27,42 @@ func (m *infraManager) waitForNode(ctx context.Context, kcli client.Client) erro return nil } -func (m *infraManager) getHelmClient() (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 = 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"), }) if err != nil { return nil, fmt.Errorf("create helm client: %w", err) @@ -54,3 +83,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 0000000000..b7f3d396df --- /dev/null +++ b/api/internal/managers/infra/util_test.go @@ -0,0 +1,85 @@ +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" +) + +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 := &infrastore.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 := &infrastore.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/internal/managers/installation/config.go b/api/internal/managers/installation/config.go index 25f7a0bcfc..ece4ec1cf3 100644 --- a/api/internal/managers/installation/config.go +++ b/api/internal/managers/installation/config.go @@ -11,9 +11,10 @@ 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) { +func (m *installationManager) GetConfig() (types.InstallationConfig, error) { return m.installationStore.GetConfig() } @@ -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) } @@ -55,7 +56,7 @@ func (m *installationManager) ValidateConfig(config *types.InstallationConfig) e 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 @@ -68,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 } @@ -78,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 } @@ -88,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") } @@ -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,14 +133,14 @@ 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") } 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") } @@ -202,28 +203,11 @@ func (m *installationManager) setCIDRDefaults(config *types.InstallationConfig) return nil } -func (m *installationManager) ConfigureHost(ctx context.Context) 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) } - go m.configureHost(context.Background()) - - return nil -} - -func (m *installationManager) configureHost(ctx context.Context) (finalErr error) { defer func() { if r := recover(); r != nil { finalErr = fmt.Errorf("panic: %v: %s", r, string(debug.Stack())) @@ -240,11 +224,11 @@ func (m *installationManager) configureHost(ctx context.Context) (finalErr error }() opts := hostutils.InitForInstallOptions{ - LicenseFile: m.licenseFile, + License: m.license, AirgapBundle: m.airgapBundle, } - if err := m.hostUtils.ConfigureHost(ctx, m.rc, opts); err != nil { - return fmt.Errorf("configure installation: %w", err) + if err := m.hostUtils.ConfigureHost(ctx, rc, opts); err != nil { + 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 dc561acbc4..ddf0f23b55 100644 --- a/api/internal/managers/installation/config_test.go +++ b/api/internal/managers/installation/config_test.go @@ -4,12 +4,12 @@ import ( "context" "errors" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" + "github.com/replicatedhq/embedded-cluster/api/internal/store/installation" + "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" @@ -21,12 +21,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 +37,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 +49,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 +59,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 +69,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 +80,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 +91,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 +102,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 +112,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 +122,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 +132,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 +148,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 +164,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, @@ -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) @@ -205,13 +205,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 +221,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 +235,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 +250,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 +263,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,10 +283,8 @@ 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) assert.Equal(t, tt.expectedConfig, tt.inputConfig) }) } @@ -298,8 +296,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 @@ -325,7 +323,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) @@ -339,7 +336,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 +351,8 @@ 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("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 { @@ -365,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), @@ -373,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 *MockInstallationStore) { - im.On("GetStatus").Return(&types.Status{State: types.StateRunning}, nil) - }, - expectedErr: true, - }, { name: "configure installation fails", rc: func() runtimeconfig.RuntimeConfig { @@ -394,23 +377,22 @@ 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("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,9 +402,8 @@ 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("SetStatus", mock.Anything).Return(errors.New("failed to set status")), ) }, @@ -436,31 +417,27 @@ func TestConfigureHost(t *testing.T) { // Create mocks mockHostUtils := &hostutils.MockHostUtils{} - mockStore := &MockInstallationStore{} + mockStore := &installation.MockStore{} // Setup mocks tt.setupMocks(mockHostUtils, mockStore) // Create manager with mocks manager := NewInstallationManager( - WithRuntimeConfig(rc), WithHostUtils(mockHostUtils), WithInstallationStore(mockStore), - WithLicenseFile("license.yaml"), + WithLicense([]byte("metadata:\n name: test-license")), WithAirgapBundle("bundle.tar"), ) // Run the test - err := manager.ConfigureHost(context.Background()) + err := manager.ConfigureHost(context.Background(), rc) // Assertions if tt.expectedErr { 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 16b281c2f7..1410e8a050 100644 --- a/api/internal/managers/installation/manager.go +++ b/api/internal/managers/installation/manager.go @@ -2,10 +2,10 @@ package installation import ( "context" - "sync" + "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" @@ -16,57 +16,42 @@ 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) 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 + installationStore installation.Store + license []byte airgapBundle string netUtils utils.NetUtils hostUtils hostutils.HostUtilsInterface logger logrus.FieldLogger - mu sync.RWMutex } 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 } } -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 } } -func WithLicenseFile(licenseFile string) InstallationManagerOption { +func WithLicense(license []byte) InstallationManagerOption { return func(c *installationManager) { - c.licenseFile = licenseFile + c.license = license } } @@ -96,20 +81,12 @@ func NewInstallationManager(opts ...InstallationManagerOption) *installationMana opt(manager) } - if manager.rc == nil { - manager.rc = runtimeconfig.New(nil) - } - if manager.logger == nil { 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 68754cf36e..eb38178498 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" ) @@ -15,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 @@ -30,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 @@ -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/installation/status.go b/api/internal/managers/installation/status.go index 9557a29a30..684b8aeaed 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() } @@ -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/installation/status_test.go b/api/internal/managers/installation/status_test.go index aee4f8a74d..d799de72d2 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/managers/installation/store.go b/api/internal/managers/installation/store.go deleted file mode 100644 index 7159ed6e69..0000000000 --- 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 871e151928..0000000000 --- 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 b22b20bba3..001ee33221 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,47 +29,9 @@ type RunHostPreflightOptions struct { HostPreflightSpec *troubleshootv1beta2.HostPreflightSpec } -func (m *hostPreflightManager) PrepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, error) { - hpf, err := m.prepareHostPreflights(ctx, opts) - if err != nil { - return nil, err - } - return hpf, nil -} - -func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, 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) - } - - // Run preflights in background - go m.runHostPreflights(context.Background(), 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, 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 +41,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,52 +68,68 @@ 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) (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, 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 != "" { 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 } - 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") } - 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 { - 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 { @@ -167,7 +146,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(), @@ -179,9 +158,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(), @@ -193,7 +170,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 f7223c645e..d11dd453e6 100644 --- a/api/internal/managers/preflight/hostpreflight_test.go +++ b/api/internal/managers/preflight/hostpreflight_test.go @@ -10,12 +10,12 @@ 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/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" - "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" ) @@ -202,8 +202,7 @@ func TestHostPreflightManager_PrepareHostPreflights(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Setup mocks mockRunner := &preflights.MockPreflightRunner{} - mockStore := &MockHostPreflightStore{} - mockMetrics := &metrics.MockReporter{} + mockStore := &preflightstore.MockStore{} mockNetUtils := &utils.MockNetUtils{} // Create real runtime config @@ -215,14 +214,12 @@ 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 != "" { @@ -248,8 +245,8 @@ func TestHostPreflightManager_RunHostPreflights(t *testing.T) { tests := []struct { name string opts RunHostPreflightOptions - initialState *types.HostPreflights - setupMocks func(*preflights.MockPreflightRunner, *metrics.MockReporter, runtimeconfig.RuntimeConfig) + initialState types.HostPreflights + setupMocks func(*preflights.MockPreflightRunner, runtimeconfig.RuntimeConfig) expectedFinalState types.State // This is the expected error message returned by the RunHostPreflights method, synchronously expectedError string @@ -259,12 +256,12 @@ 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, }, }, - 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) @@ -280,12 +277,12 @@ 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, }, }, - 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,23 +296,20 @@ 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, }, { name: "execution with preflight warnings", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, 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,23 +322,20 @@ 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, }, { name: "execution with both failures and warnings", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, 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,23 +352,20 @@ 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, }, { name: "runner execution fails", - initialState: &types.HostPreflights{ - Status: &types.Status{ + initialState: types.HostPreflights{ + Status: types.Status{ State: types.StatePending, }, }, 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) }, @@ -385,15 +373,15 @@ 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, }, }, 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) @@ -406,15 +394,15 @@ 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, }, }, 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) @@ -425,66 +413,42 @@ 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 { 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(NewMemoryStore(tt.initialState)), - WithRuntimeConfig(rc), + WithHostPreflightStore(preflightstore.NewMemoryStore(preflightstore.WithHostPreflight(tt.initialState))), 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) 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) - mockMetrics.AssertExpectations(t) }) } } @@ -492,27 +456,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", @@ -522,7 +486,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 @@ -537,7 +501,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) @@ -553,13 +517,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) }, @@ -567,7 +531,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", @@ -577,7 +541,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 @@ -607,13 +571,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) }, @@ -621,14 +585,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", @@ -638,7 +602,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 fa9b51a224..ff4bfefdf0 100644 --- a/api/internal/managers/preflight/manager.go +++ b/api/internal/managers/preflight/manager.go @@ -2,13 +2,12 @@ package preflight import ( "context" - "sync" + "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" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/sirupsen/logrus" @@ -16,44 +15,29 @@ 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 - GetHostPreflightStatus(ctx context.Context) (*types.Status, 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) } type hostPreflightManager struct { - hostPreflightStore HostPreflightStore + hostPreflightStore preflight.Store runner preflights.PreflightsRunnerInterface netUtils utils.NetUtils - rc runtimeconfig.RuntimeConfig logger logrus.FieldLogger - metricsReporter metrics.ReporterInterface - mu sync.RWMutex } 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 } } -func WithMetricsReporter(metricsReporter metrics.ReporterInterface) HostPreflightManagerOption { - return func(m *hostPreflightManager) { - m.metricsReporter = metricsReporter - } -} - -func WithHostPreflightStore(hostPreflightStore HostPreflightStore) HostPreflightManagerOption { +func WithHostPreflightStore(hostPreflightStore preflight.Store) HostPreflightManagerOption { return func(m *hostPreflightManager) { m.hostPreflightStore = hostPreflightStore } @@ -79,16 +63,12 @@ func NewHostPreflightManager(opts ...HostPreflightManagerOption) HostPreflightMa opt(manager) } - if manager.rc == nil { - manager.rc = runtimeconfig.New(nil) - } - if manager.logger == nil { manager.logger = logger.NewDiscardLogger() } 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 4f087af8e6..6d659ccfa3 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,18 +26,18 @@ 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) } // 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 7f10bd5e78..0000000000 --- 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/statemachine/event_handler.go b/api/internal/statemachine/event_handler.go new file mode 100644 index 0000000000..d616bfbea1 --- /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 0000000000..93e749483f --- /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 new file mode 100644 index 0000000000..312393975d --- /dev/null +++ b/api/internal/statemachine/statemachine.go @@ -0,0 +1,182 @@ +package statemachine + +import ( + "context" + "fmt" + "slices" + "sync" + + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/sirupsen/logrus" +) + +// 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 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 + } +} + +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) 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() + + 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) (finalError error) { + sm.mu.Lock() + defer func() { + if finalError != nil { + 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) + } + + 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 { + 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 0000000000..4365cafab5 --- /dev/null +++ b/api/internal/statemachine/statemachine_test.go @@ -0,0 +1,824 @@ +package statemachine + +import ( + "context" + "slices" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +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) + 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) + assert.NoError(t, err) + assert.Equal(t, StateInstallationConfigured, sm.CurrentState()) + + // 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) + + // 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()) +} + +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() + 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 + 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()) + // 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 + + // 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") + // There should be no lock acquired at the end + assert.False(t, sm.IsLockAcquired()) +} + +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) + 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) + 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()) + + 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") +} + +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) + 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()) + + 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") +} + +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") +} + +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() +} + +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) +} diff --git a/api/internal/store/infra/store.go b/api/internal/store/infra/store.go new file mode 100644 index 0000000000..4f70322d3f --- /dev/null +++ b/api/internal/store/infra/store.go @@ -0,0 +1,142 @@ +package infra + +import ( + "fmt" + "sync" + "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) + 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 +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(opts ...StoreOption) Store { + s := &memoryStore{} + + for _, opt := range opts { + opt(s) + } + + return s +} + +func (s *memoryStore) Get() (types.Infra, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + 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) { + s.mu.RLock() + defer s.mu.RUnlock() + + 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 { + s.mu.Lock() + defer s.mu.Unlock() + s.infra.Status = status + return nil +} + +func (s *memoryStore) SetStatusDesc(desc string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.infra.Status.State == "" { + return fmt.Errorf("state not set") + } + + s.infra.Status.Description = desc + return nil +} + +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{ + State: types.StatePending, + Description: "", + LastUpdated: time.Now(), + }, + }) + + return nil +} + +func (s *memoryStore) SetComponentStatus(name string, status types.Status) error { + s.mu.Lock() + defer s.mu.Unlock() + + for i, component := range s.infra.Components { + if component.Name == name { + s.infra.Components[i].Status = status + return nil + } + } + + 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/store/infra/store_mock.go b/api/internal/store/infra/store_mock.go new file mode 100644 index 0000000000..ac0717f5fd --- /dev/null +++ b/api/internal/store/infra/store_mock.go @@ -0,0 +1,59 @@ +package infra + +import ( + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/stretchr/testify/mock" +) + +var _ Store = (*MockStore)(nil) + +// MockStore is a mock implementation of Store +type MockStore struct { + mock.Mock +} + +func (m *MockStore) Get() (types.Infra, error) { + args := m.Called() + if args.Get(0) == nil { + return types.Infra{}, args.Error(1) + } + return args.Get(0).(types.Infra), args.Error(1) +} + +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) +} + +func (m *MockStore) SetStatus(status types.Status) error { + args := m.Called(status) + 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) +} + +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/store/infra/store_test.go b/api/internal/store/infra/store_test.go new file mode 100644 index 0000000000..d4591da81c --- /dev/null +++ b/api/internal/store/infra/store_test.go @@ -0,0 +1,300 @@ +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(WithInfra(infra)) +} + +func TestNewMemoryStore(t *testing.T) { + store := newMemoryStore() + + assert.NotNil(t, store) + infra, err := store.Get() + require.NoError(t, err) + 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.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{}, + } + + // Test setting status description when status is nil + err := store.SetStatusDesc("Should fail") + assert.Error(t, err) + 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 0000000000..d1d18b177d --- /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 0000000000..8339f85017 --- /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 3eca603d04..643304813d 100644 --- a/api/internal/managers/installation/store_test.go +++ b/api/internal/store/installation/store_test.go @@ -10,41 +10,39 @@ 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) assert.Equal(t, inst, store.installation) } 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 +55,34 @@ 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 +95,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 0000000000..16b246773e --- /dev/null +++ b/api/internal/store/preflight/store.go @@ -0,0 +1,106 @@ +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 +} + +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 +} diff --git a/api/internal/managers/preflight/store_mock.go b/api/internal/store/preflight/store_mock.go similarity index 50% rename from api/internal/managers/preflight/store_mock.go rename to api/internal/store/preflight/store_mock.go index fc4ec22ac2..2790e89007 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,22 @@ 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 { - 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 65% rename from api/internal/managers/preflight/store_test.go rename to api/internal/store/preflight/store_test.go index f3f37e5f99..d8d6f4c38c 100644 --- a/api/internal/managers/preflight/store_test.go +++ b/api/internal/store/preflight/store_test.go @@ -11,19 +11,18 @@ 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) assert.Equal(t, hostPreflight, store.hostPreflight) } 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 +32,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 +45,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 +63,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 +75,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 +87,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 +104,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,32 +120,31 @@ 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() require.NoError(t, err) - assert.NotNil(t, result) assert.Equal(t, status, result) } 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(), @@ -162,64 +160,10 @@ 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(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.NewHostPreflights() - store := NewMemoryStore(hostPreflight) + hostPreflight := types.HostPreflights{} + store := NewMemoryStore(WithHostPreflight(hostPreflight)) var wg sync.WaitGroup // Test concurrent reads and writes @@ -273,13 +217,13 @@ 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) { defer wg.Done() for j := 0; j < numOperations; j++ { - status := &types.Status{ + status := types.Status{ State: types.StateRunning, Description: "Running", LastUpdated: time.Now(), @@ -297,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/api/internal/store/store.go b/api/internal/store/store.go new file mode 100644 index 0000000000..3f04f135d7 --- /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/internal/store/store_mock.go b/api/internal/store/store_mock.go new file mode 100644 index 0000000000..8c56a560b0 --- /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/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 diff --git a/api/routes.go b/api/routes.go new file mode 100644 index 0000000000..0407bee6e0 --- /dev/null +++ b/api/routes.go @@ -0,0 +1,62 @@ +package api + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/replicatedhq/embedded-cluster/api/docs" + httpSwagger "github.com/swaggo/http-swagger/v2" +) + +// 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 + // 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 0000000000..77ca1ed255 --- /dev/null +++ b/api/types/api.go @@ -0,0 +1,33 @@ +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 + AllowIgnoreHostPreflights bool +} + +type KubernetesConfig struct { + RESTConfig *rest.Config + Installation kubernetesinstallation.Installation +} diff --git a/api/types/errors.go b/api/types/errors.go index fef75b9739..031cd8f214 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 { @@ -68,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, @@ -105,64 +108,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/api/types/infra.go b/api/types/infra.go index 2e68d3d998..ef251c552f 100644 --- a/api/types/infra.go +++ b/api/types/infra.go @@ -1,17 +1,17 @@ package types +// InfraSetupRequest represents a request to set up infrastructure +type InfraSetupRequest struct { + IgnoreHostPreflights bool `json:"ignoreHostPreflights"` +} + type Infra struct { Components []InfraComponent `json:"components"` - Status *Status `json:"status"` -} -type InfraComponent struct { - Name string `json:"name"` - Status *Status `json:"status"` + Logs string `json:"logs"` + Status Status `json:"status"` } -func NewInfra() *Infra { - return &Infra{ - Components: []InfraComponent{}, - Status: NewStatus(), - } +type InfraComponent struct { + Name string `json:"name"` + Status Status `json:"status"` } diff --git a/api/types/install.go b/api/types/install.go index 5cb37d9e0c..573c67336b 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 3b2dca16f7..19d7ef5cd4 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 1948d2ced9..158c7fa86d 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 { @@ -23,12 +24,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 44db4cdf17..1bbc55b429 100644 --- a/api/types/responses.go +++ b/api/types/responses.go @@ -2,7 +2,13 @@ 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"` +} + +// GetListAvailableNetworkInterfacesResponse represents the response when listing available network interfaces +type GetListAvailableNetworkInterfacesResponse struct { + NetworkInterfaces []string `json:"networkInterfaces"` } diff --git a/api/types/status.go b/api/types/status.go index 19a769e424..bf9d8af5b3 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 8032d18de1..b6f3476a0a 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/adminconsole_resetpassword.go b/cmd/installer/cli/adminconsole_resetpassword.go index 137131116c..d8932d29da 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/api.go b/cmd/installer/cli/api.go index 5401dac0cb..a1eaf0c9de 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -14,50 +14,44 @@ 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" "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" ) -// apiConfig holds the configuration for the API server -type apiConfig struct { - RuntimeConfig runtimeconfig.RuntimeConfig +// apiOptions holds the configuration options for the API server +type apiOptions struct { + apitypes.APIConfig + + ManagerPort int + InstallTarget string + Logger logrus.FieldLogger MetricsReporter metrics.ReporterInterface - Password string - TLSConfig apitypes.TLSConfig - ManagerPort int - LicenseFile string - AirgapBundle string - ConfigValues string - ReleaseData *release.ReleaseData - EndUserConfig *ecv1beta1.Config 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, 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 err := serveAPI(ctx, listener, cert, config); err != nil { + // 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) } } }() - 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) } @@ -65,41 +59,35 @@ 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) } api, err := api.New( - config.Password, + opts.APIConfig, api.WithLogger(logger), - api.WithRuntimeConfig(config.RuntimeConfig), - api.WithMetricsReporter(config.MetricsReporter), - api.WithReleaseData(config.ReleaseData), - api.WithTLSConfig(config.TLSConfig), - api.WithLicenseFile(config.LicenseFile), - api.WithAirgapBundle(config.AirgapBundle), - api.WithConfigValues(config.ConfigValues), - api.WithEndUserConfig(config.EndUserConfig), + 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, + InstallTarget: opts.InstallTarget, + }, web.WithLogger(logger), web.WithAssetsFS(opts.WebAssetsFS)) if err != nil { return fmt.Errorf("new web server: %w", err) } @@ -117,15 +105,15 @@ 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, "", "") } -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 { @@ -171,45 +159,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/api_test.go b/cmd/installer/cli/api_test.go index 4d17bf90ba..3f2cca6995 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" @@ -54,16 +55,18 @@ func Test_serveAPI(t *testing.T) { portInt, err := strconv.Atoi(port) require.NoError(t, err) - config := apiConfig{ - Logger: apilogger.NewDiscardLogger(), - Password: "password", - ManagerPort: portInt, - WebAssetsFS: webAssetsFS, - ReleaseData: &release.ReleaseData{ - Application: &kotsv1beta1.Application{ - Spec: kotsv1beta1.ApplicationSpec{}, + config := apiOptions{ + 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/cidr.go b/cmd/installer/cli/cidr.go index c477b9f883..95b4d2f4d0 100644 --- a/cmd/installer/cli/cidr.go +++ b/cmd/installer/cli/cidr.go @@ -7,20 +7,17 @@ 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 { - return err - } - cmd.Flags().String("service-cidr", k0sv1beta1.DefaultNetwork().ServiceCIDR, "IP address range for Services") - if err := cmd.Flags().MarkHidden("service-cidr"); err != nil { - return err - } - cmd.Flags().String("cidr", ecv1beta1.DefaultNetworkCIDR, "CIDR block of available private IP addresses (/16 or larger)") +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 aba77aa76c..2cc36e67fa 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) + mustAddCIDRFlags(cmd.Flags()) test.setFlags(cmd.Flags()) diff --git a/cmd/installer/cli/enable_ha.go b/cmd/installer/cli/enable_ha.go index 83e3b2a4d8..cc94bfe3c1 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/flags.go b/cmd/installer/cli/flags.go new file mode 100644 index 0000000000..627bdc85f0 --- /dev/null +++ b/cmd/installer/cli/flags.go @@ -0,0 +1,144 @@ +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 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 + } + 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 ebb4500fd1..e728245324 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" @@ -18,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" @@ -31,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" @@ -44,37 +48,51 @@ 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 managerPort int tlsCertFile string 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. @@ -85,11 +103,19 @@ 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" { + 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 @@ -98,16 +124,10 @@ 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 } - if flags.enableManagerExperience { - return runManagerExperienceInstall(ctx, flags, rc) - } - - _ = rc.SetEnv() - clusterID := metrics.ClusterID() installReporter := newInstallReporter( replicatedAppURL(), clusterID, cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), @@ -115,6 +135,12 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { ) installReporter.ReportInstallationStarted(ctx) + if flags.enableManagerExperience { + return runManagerExperienceInstall(ctx, flags, rc, ki, 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) @@ -135,9 +161,10 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { }, } - if err := addInstallFlags(cmd, &flags); err != nil { - panic(err) - } + cmd.SetUsageTemplate(defaultUsageTemplateV3) + + mustAddInstallFlags(cmd, &flags) + if err := addInstallAdminConsoleFlags(cmd, &flags); err != nil { panic(err) } @@ -150,44 +177,151 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { return cmd } -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) +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 +` +) - 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 +func installCmdExample(name string) string { + if os.Getenv("ENABLE_V3") != "1" { + return "" } - 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 - } + return fmt.Sprintf(installCmdExampleText, name, name) +} - if err := addProxyFlags(cmd); err != nil { - return err +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) } - if err := addCIDRFlags(cmd); err != nil { - return err + + linuxFlagSet := newLinuxInstallFlags(flags) + cmd.Flags().AddFlagSet(linuxFlagSet) + if fn := linuxFlagSet.GetNormalizeFunc(); fn != nil { + normalizeFuncs = append(normalizeFuncs, fn) } - 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 + kubernetesFlagSet := newKubernetesInstallFlags(flags, enableV3) + cmd.Flags().AddFlagSet(kubernetesFlagSet) + if fn := kubernetesFlagSet.GetNormalizeFunc(); fn != nil { + normalizeFuncs = append(normalizeFuncs, fn) } - 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().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") } - cmd.Flags().BoolVar(&flags.ignoreHostPreflights, "ignore-host-preflights", false, "Allow bypassing host preflight failures") - return nil + 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.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") + 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.") + 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") + + mustAddCIDRFlags(flagSet) + + flagSet.VisitAll(func(flag *pflag.Flag) { + mustSetFlagTargetLinux(flagSet, flag.Name) + }) + + return flagSet +} + +func newKubernetesInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.FlagSet { + flagSet := pflag.NewFlagSet("kubernetes", pflag.ContinueOnError) + + addKubernetesCLIFlags(flagSet, flags) + + flagSet.VisitAll(func(flag *pflag.Flag) { + if !enableV3 { + mustMarkFlagHidden(flagSet, flag.Name) + } + mustSetFlagTargetKubernetes(flagSet, flag.Name) + }) + + return flagSet +} + +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 { @@ -203,42 +337,66 @@ 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 !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 + } } - if err := cmd.Flags().MarkHidden("tls-cert"); err != nil { - return err + + return nil +} + +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 := cmd.Flags().MarkHidden("tls-key"); err != nil { + + if err := preRunInstallCommon(cmd, flags, rc, ki); err != nil { return err } - if err := cmd.Flags().MarkHidden("hostname"); err != nil { - return err + + switch flags.target { + case "linux": + return preRunInstallLinux(cmd, flags, rc) + case "kubernetes": + return preRunInstallKubernetes(cmd, flags, ki) } return nil } -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") - } - - // 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) - +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) + 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 { @@ -260,6 +418,49 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. 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 + } + + // restore command doesn't have a password flag + if cmd.Flags().Lookup("admin-console-password") != nil { + if err := ensureAdminConsolePassword(flags); err != nil { + return err + } + } + + rc.SetAdminConsolePort(flags.adminConsolePort) + ki.SetAdminConsolePort(flags.adminConsolePort) + + rc.SetManagerPort(flags.managerPort) + ki.SetManagerPort(flags.managerPort) + + rc.SetProxySpec(proxy) + ki.SetProxySpec(proxy) + + return nil +} + +func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { + if !cmd.Flags().Changed("skip-host-preflights") && (os.Getenv("SKIP_HOST_PREFLIGHTS") == "1" || os.Getenv("SKIP_HOST_PREFLIGHTS") == "true") { + flags.skipHostPreflights = true + } + + 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) @@ -285,11 +486,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 @@ -308,18 +504,35 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig. // 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) - 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, _ 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) { + 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 } @@ -350,7 +563,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, 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() @@ -406,30 +619,45 @@ func runManagerExperienceInstall(ctx context.Context, flags InstallCmdFlags, rc return fmt.Errorf("process overrides file: %w", err) } - apiConfig := apiConfig{ - // TODO (@salah): implement reporting in api - // MetricsReporter: installReporter, - RuntimeConfig: rc, - Password: flags.adminConsolePassword, - TLSConfig: apitypes.TLSConfig{ - CertBytes: flags.tlsCertBytes, - KeyBytes: flags.tlsKeyBytes, - Hostname: flags.hostname, + 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, - LicenseFile: flags.licenseFile, - AirgapBundle: flags.airgapBundle, - ConfigValues: flags.configValues, - ReleaseData: release.GetReleaseData(), - EndUserConfig: eucfg, + + ManagerPort: flags.managerPort, + InstallTarget: flags.target, + MetricsReporter: installReporter.reporter, } - 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 @@ -486,7 +714,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 +747,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) } @@ -545,6 +773,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, @@ -554,12 +783,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, - LicenseFile: flags.licenseFile, - Namespace: runtimeconfig.KotsadmNamespace, + License: flags.licenseBytes, + Namespace: constants.KotsadmNamespace, AirgapBundle: flags.airgapBundle, ConfigValuesFile: flags.configValues, ReplicatedAppEndpoint: replicatedAppURL(), @@ -765,8 +1000,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") @@ -800,7 +1040,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) } @@ -846,7 +1086,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), ) @@ -862,6 +1102,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) @@ -1013,20 +1262,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/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index 8409c1eb99..b8f70cd495 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -4,9 +4,11 @@ import ( "context" "errors" "fmt" + "os" "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" @@ -22,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 } @@ -49,9 +53,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) } @@ -64,9 +67,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/cli/install_test.go b/cmd/installer/cli/install_test.go index b63d79813f..3c4ac15710 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 +} diff --git a/cmd/installer/cli/join.go b/cmd/installer/cli/join.go index 2b25a5e51a..e31d5e70c8 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") } @@ -341,7 +344,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) } @@ -390,7 +394,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) } } @@ -450,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()) @@ -616,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) @@ -652,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 13534aa780..d980eb5d51 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 187078fe5b..a8d774b45b 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 1a2495149d..1d389aa42a 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/proxy.go b/cmd/installer/cli/proxy.go index f069741d07..5573e9afce 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 2ff8701418..d6e320b88a 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) - 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/reset_firewalld.go b/cmd/installer/cli/reset_firewalld.go index 1f8a0e4b86..4d4d4f42cf 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 8b0a40acb8..0192386a5b 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -21,15 +21,15 @@ 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/airgap" - "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" + "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" @@ -93,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 } @@ -121,9 +122,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 } @@ -437,7 +436,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 } @@ -475,15 +474,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) } @@ -604,6 +606,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, @@ -619,10 +630,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 } @@ -665,7 +690,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) } @@ -773,7 +798,7 @@ func getECRestoreState(ctx context.Context) ecRestoreState { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.EmbeddedClusterNamespace, + Namespace: constants.EmbeddedClusterNamespace, Name: constants.EcRestoreStateCMName, }, } @@ -805,7 +830,7 @@ func setECRestoreState(ctx context.Context, state ecRestoreState, backupName str ns := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: runtimeconfig.EmbeddedClusterNamespace, + Name: constants.EmbeddedClusterNamespace, }, } @@ -815,7 +840,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{ @@ -848,7 +873,7 @@ func resetECRestoreState(ctx context.Context) error { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: runtimeconfig.EmbeddedClusterNamespace, + Namespace: constants.EmbeddedClusterNamespace, Name: constants.EcRestoreStateCMName, }, } @@ -873,7 +898,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, }, } @@ -887,7 +912,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 } @@ -1226,7 +1251,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) } @@ -1324,7 +1349,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{ @@ -1380,7 +1405,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 { @@ -1390,7 +1415,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 { @@ -1400,7 +1425,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 { @@ -1411,7 +1436,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 { @@ -1491,14 +1516,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{} @@ -1533,7 +1558,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) } @@ -1556,7 +1581,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 25e2c23a26..c3b330ae7c 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 cfc80d9196..ec16ef7abf 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/cmd/installer/goods/support/host-support-bundle.tmpl.yaml b/cmd/installer/goods/support/host-support-bundle.tmpl.yaml index fb93075f7f..6b8a750cc9 100644 --- a/cmd/installer/goods/support/host-support-bundle.tmpl.yaml +++ b/cmd/installer/goods/support/host-support-bundle.tmpl.yaml @@ -173,6 +173,97 @@ 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" + 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" diff --git a/cmd/installer/kotscli/kotscli.go b/cmd/installer/kotscli/kotscli.go index 5d4dc62068..a77557c5c4 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/cmd/installer/main.go b/cmd/installer/main.go index 375d71b453..b23b175c48 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 diff --git a/e2e/cluster/cmx/cluster.go b/e2e/cluster/cmx/cluster.go index edc2bf1836..cfd25b3409 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", } } diff --git a/e2e/playwright/package-lock.json b/e2e/playwright/package-lock.json index 97121082bf..1965279ff3 100644 --- a/e2e/playwright/package-lock.json +++ b/e2e/playwright/package-lock.json @@ -11,17 +11,17 @@ "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": { - "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,11 +31,10 @@ } }, "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.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.7.tgz", + "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~7.8.0" } @@ -56,13 +55,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 +74,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": { @@ -88,10 +87,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 9cd1dd4794..9475e6363c 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" } } diff --git a/go.mod b/go.mod index 41030bb9db..1f392109bd 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.82 + github.com/aws/aws-sdk-go-v2/service/s3 v1.82.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 @@ -27,7 +27,7 @@ require ( github.com/gosimple/slug v1.15.0 github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/k0sproject/k0s v1.31.9-0.20250428141639-26a9908cf691 - github.com/ohler55/ojg v1.26.6 + github.com/ohler55/ojg v1.26.7 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.37.0 github.com/replicatedhq/embedded-cluster/kinds v0.0.0 @@ -40,24 +40,25 @@ 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 - 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 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.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.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 + sigs.k8s.io/yaml v1.5.0 ) replace ( @@ -66,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 @@ -77,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 @@ -92,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 @@ -131,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 @@ -196,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 @@ -220,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 @@ -235,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 @@ -279,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 @@ -294,9 +295,11 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.3 // 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 @@ -305,18 +308,19 @@ 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.2 // indirect + k8s.io/component-base v0.33.2 // indirect k8s.io/kubelet v0.32.3 // indirect - k8s.io/metrics v0.32.3 // 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.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 ( - github.com/Masterminds/semver/v3 v3.3.1 + github.com/Masterminds/semver/v3 v3.4.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -329,10 +333,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 @@ -363,13 +367,13 @@ 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 - k8s.io/apiextensions-apiserver v0.32.3 + k8s.io/apiextensions-apiserver v0.33.2 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 5031b3bb90..729eab3cb2 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= @@ -662,8 +662,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= @@ -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.82 h1:EO13QJTCD1Ig2IrQnoHTRrn981H9mB7afXsZ89WptI4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82/go.mod h1:AGh1NCg0SH+uyJamiJA5tTQcql4MMRDXGRdMmCxCXzY= +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.82.0 h1:JubM8CGDDFaAOmBrd8CRYNr49ZNgEAiLwGwgNMdS0nw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.82.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= @@ -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= @@ -1372,8 +1372,8 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= -github.com/ohler55/ojg v1.26.6 h1:0cOJJcTUOfx4HpqYE2/rNn0IOJShTjH/gY8T+EKv/OQ= -github.com/ohler55/ojg v1.26.6/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= +github.com/ohler55/ojg v1.26.7 h1:yZLS2xlZF/qk5LHM4LFhxxTDyMgZl+46Z6p7wQm8KAU= +github.com/ohler55/ojg v1.26.7/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -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= @@ -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= @@ -1615,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= @@ -1640,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= @@ -1654,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= @@ -1683,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= @@ -1693,6 +1695,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1709,8 +1715,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= @@ -1773,8 +1779,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= @@ -1895,8 +1901,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= @@ -2041,8 +2047,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= @@ -2437,8 +2443,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= @@ -2447,30 +2453,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.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-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.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.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4= -k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU= +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= @@ -2509,8 +2515,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= @@ -2521,11 +2527,15 @@ 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/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +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/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/kinds/apis/v1beta1/kubernetes_installation_types.go b/kinds/apis/v1beta1/kubernetes_installation_types.go new file mode 100644 index 0000000000..5e4e3da0c6 --- /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 39ca1dca30..881191eefb 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/kinds/go.mod b/kinds/go.mod index f00d89650f..414d0992a5 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 ) @@ -23,10 +23,8 @@ 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 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 6b1b56bd99..fbbd81abaf 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= @@ -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= diff --git a/operator/charts/embedded-cluster-operator/values.yaml b/operator/charts/embedded-cluster-operator/values.yaml index aceb2ec4fe..4e066a3194 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 0000000000..35a9a7db3c --- /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/pkg/upgrade/job.go b/operator/pkg/upgrade/job.go index faf269c615..c13fb5a5b2 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 c431a82422..45e1abc077 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" @@ -67,7 +68,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) } @@ -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/operator/schemas/kubernetesinstallation-embeddedcluster-v1beta1.json b/operator/schemas/kubernetesinstallation-embeddedcluster-v1beta1.json new file mode 100644 index 0000000000..aa105b43ca --- /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 diff --git a/pkg-new/constants/constants.go b/pkg-new/constants/constants.go new file mode 100644 index 0000000000..3ee7a50170 --- /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/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 a730a100f6..42b522405f 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/files.go b/pkg-new/hostutils/files.go index 3aaa4a8a9e..32378eb3b1 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-new/hostutils/initialize.go b/pkg-new/hostutils/initialize.go index 27ba75281d..024735b08a 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) } } diff --git a/pkg-new/hostutils/interface.go b/pkg-new/hostutils/interface.go index 4ded1db181..9feb63a9fc 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 a2b6d7660d..9154441ece 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 19e65f888d..021647817d 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 d87c3fcd43..43a01e2db8 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" @@ -30,6 +31,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,14 +57,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 (k *K0s) 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) } @@ -96,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) @@ -131,7 +136,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 +195,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 210971aea3..9f6627ecb5 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-new/preflights/host-preflight.yaml b/pkg-new/preflights/host-preflight.yaml index 8e279747d9..a3750c70b4 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." diff --git a/pkg-new/tlsutils/tls.go b/pkg-new/tlsutils/tls.go index 6a83bffbfc..30daa2b377 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 30a512ef24..6b03c89e7a 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 236bb89751..7cffd28200 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 50fffaf2b0..dcecc2d2e8 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 82097ce1be..44abaaa609 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 a1cbd450f5..ffd85b0a86 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 f5ae65b958..f3fed995e3 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 6475310dab..2106a6cf9a 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 e58c423607..b94eceffa1 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 b58db33b88..5e0e1f5909 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 a97037755f..df0307c081 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 d56fb455b2..cbc2668076 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 ed10fee779..e433a28b7f 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 0b286f9afc..1288368c46 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 11e5cf7701..fab4e37162 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 f522c6b0df..4d05f94995 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 dac066c11f..4892f3a4a0 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 f68e468df9..d451c86174 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 3b90763fec..170f5283d4 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 aea4ea5469..4eae18d9ed 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 f4801b1e42..d5752ba942 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 6723caebcd..3c0c353f6e 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 99a4cea348..d891b95da5 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 d3e4e0bf60..f359700ae4 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 cb587c994d..d27030517f 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 37694f3dac..b44dc243bc 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 bce79d2864..91b752e398 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 74db53e56a..b8a6a6081e 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 62b3f5a4a5..b6edc85cc9 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 92e854d51a..892fb68bfd 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 61da070068..f0a288cc14 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 ab4221a2f2..52e4c9b685 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 21b7b61181..a687f18320 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 a6990353e3..baa5ee1f82 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 a9f4830300..9ffe74aa4c 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 27d0a57bb4..529666ec55 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 04f3d4ac64..dadadd3eab 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 2983a29507..d6f55b4534 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 7298205e30..dce9b5f2ea 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 8fbe5452ce..a51aa0e014 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 562579d5c6..41152e20d7 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/static/metadata.yaml b/pkg/addons/velero/static/metadata.yaml index 3180396097..fa8a99efb9 100644 --- a/pkg/addons/velero/static/metadata.yaml +++ b/pkg/addons/velero/static/metadata.yaml @@ -5,26 +5,26 @@ # $ make buildtools # $ output/bin/buildtools update addon # -version: 10.0.1 +version: 10.0.8 location: oci://proxy.replicated.com/anonymous/registry.replicated.com/ec-charts/velero images: kubectl: repo: proxy.replicated.com/anonymous/replicated/ec-kubectl tag: - amd64: 1.33.1-r2-amd64@sha256:d12713fa87e9bd0f8c276e909ec86cc385c45a41449fc4df288bfe53ee1404ce - arm64: 1.33.1-r2-arm64@sha256:829211426dd63d8bd54f6e93edfa360088bebc0dfe44bcdf4fc93d2a5736247a + amd64: 1.33.2-r0-amd64@sha256:ce016fb68ac997043a389453dd371308d76201fb2094baef79e200fe659e964e + arm64: 1.33.2-r0-arm64@sha256:b5d72679c5a7024a9b0161df3498620845196f9bb8af62ac59bb10adf7fdba35 velero: repo: proxy.replicated.com/anonymous/replicated/ec-velero tag: - amd64: 1.16.1-r0-amd64@sha256:a4dc13ebad06ed8a4e04eaea7f7aaf46028c8c868bcf095b220392d85a467d75 - arm64: 1.16.1-r0-arm64@sha256:3f1752a3c6c8e97e1073aa093111e253c4987434f99fcf721ab92ef053536e16 + amd64: 1.16.1-r1-amd64@sha256:ea4921cb20436d41de55324b5fed43867b96f0acd2a8d93abe766aec8ca7cce3 + arm64: 1.16.1-r1-arm64@sha256:e8e643b6ce229475ff1a72a3cc022d54925075f132deb2bed28b7c525ffc1f17 velero-plugin-for-aws: repo: proxy.replicated.com/anonymous/replicated/ec-velero-plugin-for-aws tag: - amd64: 1.12.1-r0-amd64@sha256:d37d0abacfafe9c1b4c6c8ae6540549897e00c99cca9d5bcdb21057eef7ce635 - arm64: 1.12.1-r0-arm64@sha256:ffa59576bb9eb876dc54eb7b7f0056018b9c96e691aacc443d3b15a0c5d9fa80 + amd64: 1.12.1-r1-amd64@sha256:dec822ff1db3a4adf6f7258229857efcd759c18acdd2f65668ccdd6ff6ea48fa + arm64: 1.12.1-r1-arm64@sha256:11a4f4b587a6c430b3a227da7debc67e13aa10fed3538d395e862ea34bcc7d17 velero-restore-helper: repo: proxy.replicated.com/anonymous/replicated/ec-velero-restore-helper tag: - amd64: 1.16.1-r0-amd64@sha256:0f910e5162c9502e068735d38fa4b594fec9af598c2afc83bdec279a4438f7b9 - arm64: 1.16.1-r0-arm64@sha256:7bdcdcc4a627cf44877fef4e9515fdcb02d9d6ec3855d1d136cec27a2bf4d08e + amd64: 1.16.1-r1-amd64@sha256:277379ba064c379ea05e0e60a5bfc3acf759ac1c96eab7a8c52c9e12df708479 + arm64: 1.16.1-r1-arm64@sha256:6de35ebe1fc7d4bafefb2bdcd0cd6f82dbdf470adcdaded8dc80e999be58ff68 diff --git a/pkg/addons/velero/upgrade.go b/pkg/addons/velero/upgrade.go index b41bf119f2..00560814ca 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 f2f1b123db..d69acde358 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 fd3085921b..9b1702c3da 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 81c286f966..cf3f19b944 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 33954c2cee..0000000000 --- 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 c95a2a3946..8c71f94a43 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/dryrun/k0s.go b/pkg/dryrun/k0s.go index e729ac791e..997e391a05 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" ) @@ -17,14 +19,22 @@ func (c *K0s) GetStatus(ctx context.Context) (*k0s.K0sStatus, error) { return c.Status, nil } -func (c *K0s) Install(rc runtimeconfig.RuntimeConfig, networkInterface string) error { - return nil // TODO: implement +func (c *K0s) Install(rc runtimeconfig.RuntimeConfig) error { + 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/helm/client.go b/pkg/helm/client.go index 05c08f19d2..f743f2a8d2 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) } diff --git a/pkg/kubernetesinstallation/installation.go b/pkg/kubernetesinstallation/installation.go new file mode 100644 index 0000000000..89e9921616 --- /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 0000000000..2539c0d0ef --- /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 0000000000..31cdf3e9ff --- /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) +} diff --git a/pkg/kubeutils/installation.go b/pkg/kubeutils/installation.go index 98f2e7add5..6e49a94a53 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/metrics/reporter_mock.go b/pkg/metrics/reporter_mock.go index e6c4368b4e..9fdff200aa 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) } diff --git a/pkg/runtimeconfig/defaults.go b/pkg/runtimeconfig/defaults.go index 87d724b825..754bfaf89b 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 50e80f0f28..fd98530474 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 21ce1082b8..36c3753d9a 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 3d0dc70a45..9b67097af8 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/hostbundle.go b/pkg/support/hostbundle.go index eeef211424..b66740dcfa 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) diff --git a/pkg/support/materialize.go b/pkg/support/materialize.go index 98722ffbb9..0b74212068 100644 --- a/pkg/support/materialize.go +++ b/pkg/support/materialize.go @@ -6,21 +6,48 @@ import ( "os" "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" ) 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 := domains.GetDomains(embCfgSpec, nil) + 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 0000000000..d20f95363c --- /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) + }) + } +} diff --git a/tests/integration/kind/openebs/analytics_test.go b/tests/integration/kind/openebs/analytics_test.go index 53cafcd223..6fd465bde6 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 1d317e9de6..8b4e6c4a50 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 10f6fb17cd..c2deae9118 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 d9ebed4635..470594f694 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/package-lock.json b/web/package-lock.json index 56799522bb..463cef5e10 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,36 +9,36 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.80.7", - "lucide-react": "^0.515.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.22.3" + "@tanstack/react-query": "^5.81.5", + "lucide-react": "^0.525.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.3" }, "devDependencies": { - "@eslint/js": "^9.29.0", - "@faker-js/faker": "^8.0.2", + "@eslint/js": "^9.30.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.1", - "@types/react": "^18.3.5", + "@types/node": "^24.0.7", + "@types/react": "^19.1.8", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^4.6.0", "autoprefixer": "^10.4.21", - "eslint": "^9.29.0", + "eslint": "^9.30.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", + "postcss": "^8.5.6", "tailwindcss": "^3.4.1", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0", - "vite": "^5.4.2", - "vite-plugin-static-copy": "^3.0.0", - "vitest": "^0.32.2" + "typescript-eslint": "^8.35.0", + "vite": "^6.3.5", + "vite-plugin-static-copy": "^3.1.0", + "vitest": "^3.2.4" } }, "node_modules/@adobe/css-tools": { @@ -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": { @@ -942,11 +999,10 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", - "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -957,11 +1013,10 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1017,11 +1072,10 @@ } }, "node_modules/@eslint/js": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", - "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", + "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1034,7 +1088,6 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1054,9 +1107,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 +1119,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": { @@ -1572,225 +1625,287 @@ "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", - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", - "dev": true, - "license": "MIT" + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "dev": true }, "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" @@ -1816,22 +1931,20 @@ } }, "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==", - "license": "MIT", + "version": "5.81.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz", + "integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "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==", - "license": "MIT", + "version": "5.81.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz", + "integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==", "dependencies": { - "@tanstack/query-core": "5.80.7" + "@tanstack/query-core": "5.81.5" }, "funding": { "type": "github", @@ -2041,181 +2154,85 @@ } }, "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" + "@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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "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==", + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "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": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, - "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==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "peer": true }, - "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==", + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "deep-equal": "^2.0.5" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "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==", + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "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" + "@babel/types": "^7.0.0" } }, - "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==", + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/user-event": { - "version": "14.6.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", - "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { @@ -2228,20 +2245,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,11 +2261,19 @@ "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", - "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", @@ -2337,28 +2355,20 @@ "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.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.7.tgz", + "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==", "dev": true, - "license": "MIT", "dependencies": { "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" } }, @@ -2420,17 +2430,16 @@ "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", "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.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2444,7 +2453,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.34.0", + "@typescript-eslint/parser": "^8.35.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2454,22 +2463,20 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "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.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4" }, "engines": { @@ -2485,14 +2492,13 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.34.0", - "@typescript-eslint/types": "^8.34.0", + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", "debug": "^4.3.4" }, "engines": { @@ -2507,14 +2513,13 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "@typescript-eslint/visitor-keys": "8.34.0" + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2525,11 +2530,10 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2542,14 +2546,13 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.34.0", - "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2566,11 +2569,10 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2580,16 +2582,15 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", "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.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2613,7 +2614,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2623,7 +2623,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2639,7 +2638,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2648,16 +2646,15 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", "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.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2672,14 +2669,13 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.34.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.35.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2690,16 +2686,15 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz", - "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.11", + "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -2711,177 +2706,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.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "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.4", + "@vitest/utils": "3.2.4", + "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.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/utils": "0.32.4", - "p-limit": "^4.0.0", - "pathe": "^1.1.1" + "@vitest/spy": "3.2.4", + "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.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "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.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@vitest/utils": "3.2.4", + "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.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "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.4", + "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.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", "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.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", "dev": true, "license": "MIT", "dependencies": { - "diff-sequences": "^29.4.3", - "loupe": "^2.3.6", - "pretty-format": "^29.5.0" + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "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 +2843,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", @@ -3024,31 +2949,14 @@ "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": "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": { @@ -3089,22 +2997,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", @@ -3186,70 +3078,20 @@ "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==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "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": ">=6" } }, - "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", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "engines": { "node": ">= 6" } @@ -3276,35 +3118,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 +3333,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 +3417,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,93 +3442,21 @@ "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" } }, - "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", @@ -3735,21 +3494,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", @@ -3780,96 +3524,52 @@ "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-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==", + "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", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, "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": { @@ -3882,19 +3582,18 @@ } }, "node_modules/eslint": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", - "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", + "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.1", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.29.0", + "@eslint/js": "9.30.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -4129,6 +3828,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 +3864,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", @@ -4267,22 +3986,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", @@ -4347,16 +4050,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", @@ -4377,55 +4070,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", - "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", @@ -4479,10 +4123,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" }, @@ -4490,19 +4135,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", @@ -4514,8 +4146,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/graphql": { "version": "16.11.0", @@ -4527,61 +4158,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", @@ -4699,136 +4275,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", + "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": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "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", + "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-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" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4864,19 +4327,6 @@ "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", @@ -4892,23 +4342,6 @@ "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", @@ -4916,126 +4349,6 @@ "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", - "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-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", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "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", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5556,7 +4869,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", @@ -5717,19 +5031,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", @@ -5758,26 +5059,12 @@ "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": "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", @@ -5790,10 +5077,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==", - "license": "ISC", + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", + "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -5804,6 +5090,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -5818,16 +5105,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", @@ -5888,26 +5165,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", @@ -6050,98 +5307,37 @@ "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==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.8.0" } }, - "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==", + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "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", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", - "dev": true, - "license": "MIT" - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" @@ -6259,20 +5455,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": { @@ -6308,39 +5504,10 @@ "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", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "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", @@ -6500,6 +5667,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6515,6 +5683,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -6525,6 +5694,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -6581,26 +5751,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": { @@ -6608,7 +5774,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", @@ -6621,35 +5788,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": { @@ -6685,27 +5864,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", @@ -6759,12 +5917,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" @@ -6774,22 +5933,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" } }, @@ -6822,24 +5985,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", @@ -6861,12 +6006,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", @@ -6878,39 +6020,10 @@ "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/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", @@ -6931,82 +6044,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", @@ -7090,20 +6127,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", @@ -7226,18 +6249,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", @@ -7339,6 +6369,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", @@ -7385,9 +6422,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": { @@ -7395,9 +6442,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": { @@ -7479,7 +6526,6 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.12" }, @@ -7504,16 +6550,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", @@ -7542,15 +6578,14 @@ } }, "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.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", "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.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7564,13 +6599,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", @@ -7646,20 +6674,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" @@ -7668,19 +6700,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 }, @@ -7701,1101 +6739,171 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "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.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": { "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.1.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.0.tgz", + "integrity": "sha512-ONFBaYoN1qIiCxMCfeHI96lqLza7ujx/QClIXp4kEULUbyH2qLgYoaL8JHhk3FWjSB4TpzoaN3iMCyCFldyXzw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "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" - ], - "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" - ], + "dependencies": { + "chokidar": "^3.5.3", + "fs-extra": "^11.3.0", + "p-map": "^7.0.3", + "picocolors": "^1.1.1", + "tinyglobby": "^0.2.14" + }, "engines": { - "node": ">=12" + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "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" - ], + "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", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "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, - "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" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "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" + "node_modules/vitest": { + "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.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", + "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.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" }, "bin": { - "vite": "bin/vite.js" + "vitest": "vitest.mjs" }, "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.4", + "@vitest/ui": "3.2.4", + "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", @@ -8870,67 +6978,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 4ef07cfa0d..b66d64f8bb 100644 --- a/web/package.json +++ b/web/package.json @@ -13,35 +13,35 @@ }, "dependencies": { "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.80.7", - "lucide-react": "^0.515.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.22.3" + "@tanstack/react-query": "^5.81.5", + "lucide-react": "^0.525.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.6.3" }, "devDependencies": { - "@eslint/js": "^9.29.0", - "@faker-js/faker": "^8.0.2", + "@eslint/js": "^9.30.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.1", - "@types/react": "^18.3.5", + "@types/node": "^24.0.7", + "@types/react": "^19.1.8", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.5.2", + "@vitejs/plugin-react": "^4.6.0", "autoprefixer": "^10.4.21", - "eslint": "^9.29.0", + "eslint": "^9.30.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", + "postcss": "^8.5.6", "tailwindcss": "^3.4.1", "typescript": "^5.8.3", - "typescript-eslint": "^8.34.0", - "vite": "^5.4.2", - "vite-plugin-static-copy": "^3.0.0", - "vitest": "^0.32.2" + "typescript-eslint": "^8.35.0", + "vite": "^6.3.5", + "vite-plugin-static-copy": "^3.1.0", + "vitest": "^3.2.4" } } diff --git a/web/src/App.tsx b/web/src/App.tsx index 341ea5c7bb..7ef175cb9c 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 0000000000..c5f45dc33a --- /dev/null +++ b/web/src/components/common/ConnectionMonitor.tsx @@ -0,0 +1,152 @@ +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<{ + nextRetryTime?: number; +}> = ({ nextRetryTime }) => { + const [secondsUntilRetry, setSecondsUntilRetry] = useState(0); + + useEffect(() => { + 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 ( +
+
+
+
+ + + +
+
+ +

+ Cannot connect +

+ +

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

+ +
+
+ {secondsUntilRetry > 0 ? ( + <> + + Retrying in {secondsUntilRetry} second{secondsUntilRetry !== 1 ? 's' : ''} + + ) : ( + <> + + Retrying now... + + )} +
+
+
+
+ ); +}; + +// Custom hook for connection monitoring logic +const useConnectionMonitor = () => { + const [isConnected, setIsConnected] = useState(true); + const [nextRetryTime, setNextRetryTime] = useState(); + const [checkInterval, setCheckInterval] = useState(null); + + const checkConnection = useCallback(async () => { + try { + 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); + setNextRetryTime(undefined); + } else { + throw new Error(`HTTP ${response.status}`); + } + } catch { + // Connection failed - set up countdown for next retry + setIsConnected(false); + const retryTime = Date.now() + RETRY_INTERVAL; + setNextRetryTime(retryTime); + } + }, []); + + useEffect(() => { + // Initial check + checkConnection(); + + // Set up regular interval checks + const interval = setInterval(checkConnection, RETRY_INTERVAL); + setCheckInterval(interval); + + // 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 && ( + + )} + + ); +}; + +export default ConnectionMonitor; diff --git a/web/src/components/common/Modal.tsx b/web/src/components/common/Modal.tsx new file mode 100644 index 0000000000..bff81af566 --- /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/common/tests/ConnectionMonitor.test.tsx b/web/src/components/common/tests/ConnectionMonitor.test.tsx new file mode 100644 index 0000000000..ddab8da9ae --- /dev/null +++ b/web/src/components/common/tests/ConnectionMonitor.test.tsx @@ -0,0 +1,101 @@ +import React from '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'; +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 automatic retry', async () => { + let retryCount = 0; + + server.use( + http.get('*/api/health', () => { + retryCount++; + + // Fail first time, succeed on second automatic retry + if (retryCount === 1) { + 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 }); + + // Should show countdown + await waitFor(() => { + expect(screen.getByText(/Retrying in \d+ second/)).toBeInTheDocument(); + }, { timeout: 1000 }); + + // Modal should disappear when automatic retry succeeds + await waitFor(() => { + expect(screen.queryByText('Cannot connect')).not.toBeInTheDocument(); + }, { timeout: 12000 }); + }, 15000); + + 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(/Retrying in \d+ second/)).toBeInTheDocument(); + }, 6000); +}); diff --git a/web/src/components/wizard/InstallationStep.tsx b/web/src/components/wizard/InstallationStep.tsx index 7bdee546e0..9fdadadde3 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,14 +20,14 @@ 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 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}`, @@ -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/SetupStep.tsx b/web/src/components/wizard/SetupStep.tsx index b1908898e6..e7df99ef79 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/StepNavigation.tsx b/web/src/components/wizard/StepNavigation.tsx index 635a7b7aab..7fd8235f13 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/ValidationStep.tsx b/web/src/components/wizard/ValidationStep.tsx index 317d53877c..37a86e8fc4 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 () => { - const response = await fetch("/api/install/infra/setup", { + mutationFn: async ({ ignoreHostPreflights }: { ignoreHostPreflights: boolean }) => { + const response = await fetch("/api/linux/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/installation/LogViewer.tsx b/web/src/components/wizard/installation/LogViewer.tsx index 80186fcea5..1c81ca8b78 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/preflight/LinuxPreflightCheck.tsx b/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx index 1a445a882e..004531334e 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 { @@ -65,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}`, @@ -90,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")}`, @@ -111,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")}`, @@ -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/setup/LinuxSetup.tsx b/web/src/components/wizard/setup/LinuxSetup.tsx index 3bfa4db123..198a288edc 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; @@ -16,7 +37,7 @@ interface LinuxSetupProps { globalCidr?: string; }; prototypeSettings: { - clusterMode: string; + installTarget: string; availableNetworkInterfaces?: Array<{ name: 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/InstallationStep.test.tsx b/web/src/components/wizard/tests/InstallationStep.test.tsx index 0589e7668f..c086647a79 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"; @@ -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: { @@ -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/linux/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/components/wizard/tests/LinuxPreflightCheck.test.tsx b/web/src/components/wizard/tests/LinuxPreflightCheck.test.tsx index cc72c21569..6dad078dd8 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"; @@ -11,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 }); @@ -20,23 +19,25 @@ 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 }); } 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, }); }), // 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 }); @@ -58,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 }); @@ -82,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 }); @@ -126,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 }); @@ -152,12 +153,12 @@ 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 () => { 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 }); @@ -167,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 }); @@ -196,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 }); @@ -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/linux/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/linux/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/LinuxSetup.test.tsx b/web/src/components/wizard/tests/LinuxSetup.test.tsx new file mode 100644 index 0000000000..cfb7d3386f --- /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)"); + }); +}); diff --git a/web/src/components/wizard/tests/SetupStep.test.tsx b/web/src/components/wizard/tests/SetupStep.test.tsx index 2ced28379e..0b31971afa 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,14 +19,13 @@ 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 }); }) ); 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: { @@ -122,12 +121,12 @@ 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 }); }) ); - renderWithProviders(, { + renderWithProviders(, { wrapperProps: { authenticated: true, preloadedState: { @@ -188,7 +187,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 +211,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(); @@ -230,7 +229,7 @@ describe("SetupStep", () => { }) ); - renderWithProviders(, { + renderWithProviders(, { wrapperProps: { authenticated: true, preloadedState: { 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 0000000000..1a55837a5d --- /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 + }); + }); +}); 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 0000000000..998420e9b9 --- /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/linux/install/installation/status', () => { + return HttpResponse.json({ + state: 'Succeeded', + description: 'Installation initialized', + lastUpdated: '2024-01-01T00:00:00Z', + }); + }), + + // Mock start installation endpoint + http.post('*/api/linux/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/linux/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(); + }, { timeout: 2000 }); + + // 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/linux/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/linux/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/linux/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/linux/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/linux/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/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 }); + }) + ); + + 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/linux/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/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 }); + }) + ); + + 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/linux/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/linux/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/linux/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/linux/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/linux/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/linux/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/linux/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/linux/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/linux/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/linux/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/linux/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(); + }); +}); diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index 49c54e0528..9c93dabee4 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}`, }, diff --git a/web/src/contexts/ConfigContext.tsx b/web/src/contexts/ConfigContext.tsx index eb4eaf76b1..f206068538 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 0acc1d33e8..d4a4d79454 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 ab361cdbe4..e67927c531 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 9e2adb2a07..cb6f4a59a8 100644 --- a/web/src/test/setup.tsx +++ b/web/src/test/setup.tsx @@ -19,12 +19,17 @@ 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; failInstallation: boolean; failHostPreflights: boolean; - clusterMode: "existing" | "embedded"; + installTarget: "linux" | "kubernetes"; themeColor: string; skipNodeValidation: boolean; useSelfSignedCert: boolean; @@ -86,7 +91,7 @@ const MockProvider = ({ children, queryClient, contexts }: MockProviderProps) => return ( - + {children} @@ -124,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 328d35e23a..f71fb01537 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/src/types/index.ts b/web/src/types/index.ts index 36eb9c4972..ea610af863 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 { diff --git a/web/static.go b/web/static.go index 987f7363dc..58016a3f50 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 { @@ -142,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 abaf9a2bc3..1131e7dc75 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")