diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 62893489d..91074204b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,7 @@ name: CI on: pull_request: {} + push: branches: - main diff --git a/Makefile b/Makefile index c886cf8d5..4f5858b72 100644 --- a/Makefile +++ b/Makefile @@ -274,7 +274,8 @@ envtest: .PHONY: unit-tests unit-tests: envtest KUBEBUILDER_ASSETS="$(shell ./operator/bin/setup-envtest use $(ENVTEST_K8S_VERSION) --bin-dir $(shell pwd)/operator/bin -p path)" \ - go test -tags $(GO_BUILD_TAGS) -v ./pkg/... ./cmd/... ./api/... ./web/... ./pkg-new/... + go test -tags $(GO_BUILD_TAGS) -v ./pkg/... ./cmd/... ./web/... ./pkg-new/... + $(MAKE) -C api unit-tests $(MAKE) -C operator test $(MAKE) -C utils unit-tests diff --git a/api/Makefile b/api/Makefile index ac4fcaecf..dda8819ad 100644 --- a/api/Makefile +++ b/api/Makefile @@ -1,5 +1,7 @@ SHELL := /bin/bash +include ../common.mk + .PHONY: swagger swagger: swag swag fmt -g api.go @@ -8,3 +10,7 @@ swagger: swag .PHONY: swag swag: which swag || (go install github.com/swaggo/swag/v2/cmd/swag@latest) + +.PHONY: unit-tests +unit-tests: + go test -tags $(GO_BUILD_TAGS) -v ./... diff --git a/api/README.md b/api/README.md index 979b65a43..edc2325a9 100644 --- a/api/README.md +++ b/api/README.md @@ -10,7 +10,10 @@ The root directory contains the main API setup files and request handlers. ### Subpackages #### `/controllers` -Contains the business logic for different API endpoints. Each controller package focuses on a specific domain of functionality (e.g., authentication, console, installation) and implements the core business logic for that domain. +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/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. #### `/types` Defines the core data structures and types used throughout the API. This includes: @@ -36,12 +39,12 @@ Provides a client library for interacting with the API. The client package imple 1. **New API Endpoints**: - Add route definitions in the root API setup - - Create corresponding controller in `/controllers` + - Create or update corresponding controller in `/controllers` - Define request/response types in `/types` 2. **New Business Logic**: - - Place in appropriate controller under `/controllers` - - Share common logic in `/pkg` if used across multiple controllers + - Place in appropriate controller under `/controllers` if the logic represents a distinct domain or workflow + - Place in appropriate manager under `/internal/managers` if the logic represents a distinct subdomain 3. **New Types/Models**: - Add to `/types` directory diff --git a/api/api.go b/api/api.go index 484061b3f..715f7a80a 100644 --- a/api/api.go +++ b/api/api.go @@ -11,7 +11,12 @@ import ( "github.com/replicatedhq/embedded-cluster/api/controllers/console" "github.com/replicatedhq/embedded-cluster/api/controllers/install" "github.com/replicatedhq/embedded-cluster/api/docs" + "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/replicatedhq/embedded-cluster/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" httpSwagger "github.com/swaggo/http-swagger/v2" ) @@ -38,8 +43,14 @@ type API struct { authController auth.Controller consoleController console.Controller installController install.Controller + rc runtimeconfig.RuntimeConfig + releaseData *release.ReleaseData + licenseFile string + airgapBundle string configChan chan<- *types.InstallationConfig logger logrus.FieldLogger + hostUtils hostutils.HostUtilsInterface + metricsReporter metrics.ReporterInterface } type APIOption func(*API) @@ -62,24 +73,79 @@ func WithInstallController(installController install.Controller) APIOption { } } +func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) APIOption { + return func(a *API) { + a.rc = rc + } +} + func WithLogger(logger logrus.FieldLogger) APIOption { 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 { + return func(a *API) { + a.metricsReporter = metricsReporter + } +} + +func WithReleaseData(releaseData *release.ReleaseData) APIOption { + return func(a *API) { + a.releaseData = releaseData + } +} + func WithConfigChan(configChan chan<- *types.InstallationConfig) APIOption { return func(a *API) { a.configChan = configChan } } +func WithLicenseFile(licenseFile string) APIOption { + return func(a *API) { + a.licenseFile = licenseFile + } +} + +func WithAirgapBundle(airgapBundle string) APIOption { + return func(a *API) { + a.airgapBundle = airgapBundle + } +} + 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.logger == nil { + l, err := logger.NewLogger() + if err != nil { + return nil, fmt.Errorf("create logger: %w", err) + } + 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 { @@ -97,17 +163,21 @@ func New(password string, opts ...APIOption) (*API, error) { } if api.installController == nil { - installController, err := install.NewInstallController() + 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.WithLicenseFile(api.licenseFile), + install.WithAirgapBundle(api.airgapBundle), + ) if err != nil { return nil, fmt.Errorf("new install controller: %w", err) } api.installController = installController } - if api.logger == nil { - api.logger = NewDiscardLogger() - } - return api, nil } @@ -129,10 +199,19 @@ func (a *API) RegisterRoutes(router *mux.Router) { authenticatedRouter.Use(a.authMiddleware) installRouter := authenticatedRouter.PathPrefix("/install").Subrouter() - installRouter.HandleFunc("", a.getInstall).Methods("GET") - installRouter.HandleFunc("/config", a.setInstallConfig).Methods("POST") - installRouter.HandleFunc("/status", a.setInstallStatus).Methods("POST") + installRouter.HandleFunc("/installation/config", a.getInstallInstallationConfig).Methods("GET") + installRouter.HandleFunc("/installation/status", a.getInstallInstallationStatus).Methods("GET") + installRouter.HandleFunc("/installation/configure", a.postInstallConfigureInstallation).Methods("POST") + + installRouter.HandleFunc("/host-preflights/status", a.getInstallHostPreflightsStatus).Methods("GET") + installRouter.HandleFunc("/host-preflights/run", a.postInstallRunHostPreflights).Methods("POST") + + installRouter.HandleFunc("/node/setup", a.postInstallSetupNode).Methods("POST") + + // 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") diff --git a/api/api_test.go b/api/api_test.go index 10056fbb8..8a35376cc 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "testing" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/stretchr/testify/assert" ) @@ -84,7 +85,7 @@ func TestAPI_jsonError(t *testing.T) { // Call the JSON method api := &API{ - logger: NewDiscardLogger(), + logger: logger.NewDiscardLogger(), } api.jsonError(rec, httptest.NewRequest("GET", "/api/test", nil), tt.apiErr) diff --git a/api/client/client.go b/api/client/client.go index eb0fc5703..42b3056bd 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -11,9 +11,10 @@ import ( type Client interface { Authenticate(password string) error - GetInstall() (*types.Install, error) - SetInstallConfig(config types.InstallationConfig) (*types.Install, error) - SetInstallStatus(status types.InstallationStatus) (*types.Install, error) + GetInstallationConfig() (*types.InstallationConfig, error) + GetInstallationStatus() (*types.Status, error) + ConfigureInstallation(config *types.InstallationConfig) (*types.Status, 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 1bea517b9..761128023 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -99,33 +99,31 @@ func TestLogin(t *testing.T) { assert.Equal(t, "Invalid password", apiErr.Message) } -func TestGetInstall(t *testing.T) { +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", r.URL.Path) + assert.Equal(t, "/api/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")) // Return successful response w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.Install{ - Config: types.InstallationConfig{ - GlobalCIDR: "10.0.0.0/24", - AdminConsolePort: 8080, - }, + json.NewEncoder(w).Encode(types.InstallationConfig{ + GlobalCIDR: "10.0.0.0/24", + AdminConsolePort: 8080, }) })) defer server.Close() // Test successful get c := New(server.URL, WithToken("test-token")) - install, err := c.GetInstall() + config, err := c.GetInstallationConfig() assert.NoError(t, err) - assert.NotNil(t, install) - assert.Equal(t, "10.0.0.0/24", install.Config.GlobalCIDR) - assert.Equal(t, 8080, install.Config.AdminConsolePort) + assert.NotNil(t, config) + assert.Equal(t, "10.0.0.0/24", config.GlobalCIDR) + assert.Equal(t, 8080, config.AdminConsolePort) // Test error response errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -138,9 +136,9 @@ func TestGetInstall(t *testing.T) { defer errorServer.Close() c = New(errorServer.URL, WithToken("test-token")) - install, err = c.GetInstall() + config, err = c.GetInstallationConfig() assert.Error(t, err) - assert.Nil(t, install) + assert.Nil(t, config) apiErr, ok := err.(*types.APIError) require.True(t, ok, "Expected err to be of type *types.APIError") @@ -148,12 +146,12 @@ func TestGetInstall(t *testing.T) { assert.Equal(t, "Internal Server Error", apiErr.Message) } -func TestSetInstallConfig(t *testing.T) { +func TestConfigureInstallation(t *testing.T) { // Create a test server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check request method and path - assert.Equal(t, "POST", r.Method) // Corrected from PUT to POST based on implementation - assert.Equal(t, "/api/install/config", r.URL.Path) + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/api/install/installation/configure", r.URL.Path) // Check headers assert.Equal(t, "application/json", r.Header.Get("Content-Type")) @@ -166,22 +164,24 @@ func TestSetInstallConfig(t *testing.T) { // Return successful response w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.Install{ - Config: config, + json.NewEncoder(w).Encode(types.Status{ + State: types.StateRunning, + Description: "Configuring installation", }) })) defer server.Close() - // Test successful set + // Test successful configure c := New(server.URL, WithToken("test-token")) config := types.InstallationConfig{ GlobalCIDR: "20.0.0.0/24", LocalArtifactMirrorPort: 9081, } - install, err := c.SetInstallConfig(config) + status, err := c.ConfigureInstallation(&config) assert.NoError(t, err) - assert.NotNil(t, install) - assert.Equal(t, config, install.Config) + assert.NotNil(t, status) + assert.Equal(t, types.StateRunning, status.State) + assert.Equal(t, "Configuring installation", status.Description) // Test error response errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -194,9 +194,9 @@ func TestSetInstallConfig(t *testing.T) { defer errorServer.Close() c = New(errorServer.URL, WithToken("test-token")) - install, err = c.SetInstallConfig(config) + status, err = c.ConfigureInstallation(&config) assert.Error(t, err) - assert.Nil(t, install) + assert.Nil(t, status) apiErr, ok := err.(*types.APIError) require.True(t, ok, "Expected err to be of type *types.APIError") @@ -214,28 +214,26 @@ func TestSetInstallStatus(t *testing.T) { assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) // Decode request body - var status types.InstallationStatus + var status types.Status err := json.NewDecoder(r.Body).Decode(&status) require.NoError(t, err, "Failed to decode request body") // Return successful response w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(types.Install{ - Status: status, - }) + json.NewEncoder(w).Encode(status) })) defer server.Close() // Test successful set c := New(server.URL, WithToken("test-token")) - status := types.InstallationStatus{ - State: types.InstallationStateSucceeded, + status := &types.Status{ + State: types.StateSucceeded, Description: "Installation successful", } - install, err := c.SetInstallStatus(status) + newStatus, err := c.SetInstallStatus(status) assert.NoError(t, err) - assert.NotNil(t, install) - assert.Equal(t, status, install.Status) + assert.NotNil(t, newStatus) + assert.Equal(t, status, newStatus) // Test error response errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -248,9 +246,9 @@ func TestSetInstallStatus(t *testing.T) { defer errorServer.Close() c = New(errorServer.URL, WithToken("test-token")) - install, err = c.SetInstallStatus(status) + newStatus, err = c.SetInstallStatus(status) assert.Error(t, err) - assert.Nil(t, install) + assert.Nil(t, 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 902f47e08..7436c1fef 100644 --- a/api/client/install.go +++ b/api/client/install.go @@ -8,8 +8,8 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -func (c *client) GetInstall() (*types.Install, error) { - req, err := http.NewRequest("GET", c.apiURL+"/api/install", nil) +func (c *client) GetInstallationConfig() (*types.InstallationConfig, error) { + req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/config", nil) if err != nil { return nil, err } @@ -26,22 +26,22 @@ func (c *client) GetInstall() (*types.Install, error) { return nil, errorFromResponse(resp) } - var install types.Install - err = json.NewDecoder(resp.Body).Decode(&install) + var config types.InstallationConfig + err = json.NewDecoder(resp.Body).Decode(&config) if err != nil { return nil, err } - return &install, nil + return &config, nil } -func (c *client) SetInstallConfig(config types.InstallationConfig) (*types.Install, error) { - b, err := json.Marshal(config) +func (c *client) ConfigureInstallation(cfg *types.InstallationConfig) (*types.Status, error) { + b, err := json.Marshal(cfg) if err != nil { return nil, err } - req, err := http.NewRequest("POST", c.apiURL+"/api/install/config", bytes.NewBuffer(b)) + req, err := http.NewRequest("POST", c.apiURL+"/api/install/installation/configure", bytes.NewBuffer(b)) if err != nil { return nil, err } @@ -58,17 +58,44 @@ func (c *client) SetInstallConfig(config types.InstallationConfig) (*types.Insta return nil, errorFromResponse(resp) } - var install types.Install - err = json.NewDecoder(resp.Body).Decode(&install) + var status types.Status + err = json.NewDecoder(resp.Body).Decode(&status) if err != nil { return nil, err } - return &install, nil + return &status, nil } -func (c *client) SetInstallStatus(status types.InstallationStatus) (*types.Install, error) { - b, err := json.Marshal(status) +func (c *client) GetInstallationStatus() (*types.Status, error) { + req, err := http.NewRequest("GET", c.apiURL+"/api/install/installation/status", nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + setAuthorizationHeader(req, c.token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errorFromResponse(resp) + } + + var status types.Status + err = json.NewDecoder(resp.Body).Decode(&status) + if err != nil { + return nil, err + } + + return &status, nil +} + +func (c *client) SetInstallStatus(s *types.Status) (*types.Status, error) { + b, err := json.Marshal(s) if err != nil { return nil, err } @@ -90,11 +117,11 @@ func (c *client) SetInstallStatus(status types.InstallationStatus) (*types.Insta return nil, errorFromResponse(resp) } - var install types.Install - err = json.NewDecoder(resp.Body).Decode(&install) + var status types.Status + err = json.NewDecoder(resp.Body).Decode(&status) if err != nil { return nil, err } - return &install, nil + return &status, nil } diff --git a/api/controllers/install/controller.go b/api/controllers/install/controller.go index c0a6cae41..95a234b13 100644 --- a/api/controllers/install/controller.go +++ b/api/controllers/install/controller.go @@ -2,116 +2,146 @@ package install import ( "context" - "fmt" + "sync" - "github.com/replicatedhq/embedded-cluster/api/pkg/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/managers/preflight" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" - "github.com/replicatedhq/embedded-cluster/pkg/netutils" + "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" ) type Controller interface { - Get(ctx context.Context) (*types.Install, error) - SetConfig(ctx context.Context, config *types.InstallationConfig) error - SetStatus(ctx context.Context, status *types.InstallationStatus) error - ReadStatus(ctx context.Context) (*types.InstallationStatus, 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) error + GetHostPreflightStatus(ctx context.Context) (*types.Status, error) + GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightOutput, error) + GetHostPreflightTitles(ctx context.Context) ([]string, error) + SetupNode(ctx context.Context) error + SetStatus(ctx context.Context, status *types.Status) error + GetStatus(ctx context.Context) (*types.Status, error) } var _ Controller = (*InstallController)(nil) type InstallController struct { - installationManager installation.InstallationManager + install *types.Install + installationManager installation.InstallationManager + hostPreflightManager preflight.HostPreflightManager + rc runtimeconfig.RuntimeConfig + logger logrus.FieldLogger + hostUtils hostutils.HostUtilsInterface + metricsReporter metrics.ReporterInterface + releaseData *release.ReleaseData + licenseFile string + airgapBundle string + mu sync.RWMutex } type InstallControllerOption func(*InstallController) -func WithInstallationManager(installationManager installation.InstallationManager) InstallControllerOption { +func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) InstallControllerOption { return func(c *InstallController) { - c.installationManager = installationManager + c.rc = rc } } -func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { - controller := &InstallController{} - - for _, opt := range opts { - opt(controller) +func WithLogger(logger logrus.FieldLogger) InstallControllerOption { + return func(c *InstallController) { + c.logger = logger } +} - if controller.installationManager == nil { - controller.installationManager = installation.NewInstallationManager() +func WithHostUtils(hostUtils hostutils.HostUtilsInterface) InstallControllerOption { + return func(c *InstallController) { + c.hostUtils = hostUtils } - - return controller, nil } -func (c *InstallController) Get(ctx context.Context) (*types.Install, error) { - config, err := c.installationManager.ReadConfig() - if err != nil { - return nil, err +func WithMetricsReporter(metricsReporter metrics.ReporterInterface) InstallControllerOption { + return func(c *InstallController) { + c.metricsReporter = metricsReporter } +} - if err := c.installationManager.SetDefaults(config); err != nil { - return nil, fmt.Errorf("set defaults: %w", err) +func WithReleaseData(releaseData *release.ReleaseData) InstallControllerOption { + return func(c *InstallController) { + c.releaseData = releaseData } +} - if err := c.installationManager.ValidateConfig(config); err != nil { - return nil, fmt.Errorf("validate: %w", err) +func WithLicenseFile(licenseFile string) InstallControllerOption { + return func(c *InstallController) { + c.licenseFile = licenseFile } +} - status, err := c.installationManager.ReadStatus() - if err != nil { - return nil, fmt.Errorf("read status: %w", err) +func WithAirgapBundle(airgapBundle string) InstallControllerOption { + return func(c *InstallController) { + c.airgapBundle = airgapBundle } +} - install := &types.Install{ - Config: *config, - Status: *status, +func WithInstallationManager(installationManager installation.InstallationManager) InstallControllerOption { + return func(c *InstallController) { + c.installationManager = installationManager } - - return install, nil } -func (c *InstallController) SetConfig(ctx context.Context, config *types.InstallationConfig) error { - if err := c.installationManager.ValidateConfig(config); err != nil { - return fmt.Errorf("validate: %w", err) +func WithHostPreflightManager(hostPreflightManager preflight.HostPreflightManager) InstallControllerOption { + return func(c *InstallController) { + c.hostPreflightManager = hostPreflightManager } +} - if err := c.computeCIDRs(config); err != nil { - return fmt.Errorf("compute cidrs: %w", err) +func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { + controller := &InstallController{ + install: types.NewInstall(), } - if err := c.installationManager.WriteConfig(*config); err != nil { - return fmt.Errorf("write: %w", err) + for _, opt := range opts { + opt(controller) } - return nil -} - -func (c *InstallController) SetStatus(ctx context.Context, status *types.InstallationStatus) error { - if err := c.installationManager.ValidateStatus(status); err != nil { - return fmt.Errorf("validate: %w", err) + if controller.rc == nil { + controller.rc = runtimeconfig.New(nil) } - if err := c.installationManager.WriteStatus(*status); err != nil { - return fmt.Errorf("write: %w", err) + if controller.logger == nil { + controller.logger = logger.NewDiscardLogger() } - return nil -} + if controller.hostUtils == nil { + controller.hostUtils = hostutils.New( + hostutils.WithLogger(controller.logger), + ) + } -func (c *InstallController) ReadStatus(ctx context.Context) (*types.InstallationStatus, error) { - return c.installationManager.ReadStatus() -} + 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.WithAirgapBundle(controller.airgapBundle), + installation.WithHostUtils(controller.hostUtils), + ) + } -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 + if controller.hostPreflightManager == nil { + controller.hostPreflightManager = preflight.NewHostPreflightManager( + preflight.WithRuntimeConfig(controller.rc), + preflight.WithLogger(controller.logger), + preflight.WithMetricsReporter(controller.metricsReporter), + preflight.WithHostPreflight(controller.install.Steps.HostPreflight), + ) } - return nil + return controller, nil } diff --git a/api/controllers/install/controller_test.go b/api/controllers/install/controller_test.go index 91719b1a6..32b88dd5c 100644 --- a/api/controllers/install/controller_test.go +++ b/api/controllers/install/controller_test.go @@ -4,131 +4,76 @@ import ( "context" "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/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" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" ) -// MockInstallationManager is a mock implementation of installation.InstallationManager -type MockInstallationManager struct { - mock.Mock -} - -func (m *MockInstallationManager) ReadConfig() (*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) -} - -func (m *MockInstallationManager) WriteConfig(config types.InstallationConfig) error { - args := m.Called(config) - return args.Error(0) -} - -func (m *MockInstallationManager) ReadStatus() (*types.InstallationStatus, error) { - args := m.Called() - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*types.InstallationStatus), args.Error(1) -} - -func (m *MockInstallationManager) WriteStatus(status types.InstallationStatus) error { - args := m.Called(status) - return args.Error(0) -} - -func (m *MockInstallationManager) ValidateConfig(config *types.InstallationConfig) error { - args := m.Called(config) - return args.Error(0) -} - -func (m *MockInstallationManager) ValidateStatus(status *types.InstallationStatus) error { - args := m.Called(status) - return args.Error(0) -} - -func (m *MockInstallationManager) SetDefaults(config *types.InstallationConfig) error { - args := m.Called(config) - return args.Error(0) -} - -func TestGet(t *testing.T) { +func TestGetInstallationConfig(t *testing.T) { tests := []struct { name string - setupMock func(*MockInstallationManager) + setupMock func(*installation.MockInstallationManager) expectedErr bool - expectedValue *types.Install + expectedValue *types.InstallationConfig }{ { name: "successful get", - setupMock: func(m *MockInstallationManager) { + setupMock: func(m *installation.MockInstallationManager) { config := &types.InstallationConfig{ AdminConsolePort: 9000, GlobalCIDR: "10.0.0.1/16", } - status := &types.InstallationStatus{ - State: "Running", - } - m.On("ReadConfig").Return(config, nil) - m.On("SetDefaults", config).Return(nil) - m.On("ValidateConfig", config).Return(nil) - m.On("ReadStatus").Return(status, nil) + mock.InOrder( + m.On("GetConfig").Return(config, nil), + m.On("SetConfigDefaults", config).Return(nil), + m.On("ValidateConfig", config).Return(nil), + ) }, expectedErr: false, - expectedValue: &types.Install{ - Config: types.InstallationConfig{ - AdminConsolePort: 9000, - GlobalCIDR: "10.0.0.1/16", - }, - Status: types.InstallationStatus{ - State: "Running", - }, + expectedValue: &types.InstallationConfig{ + AdminConsolePort: 9000, + GlobalCIDR: "10.0.0.1/16", }, }, { name: "read config error", - setupMock: func(m *MockInstallationManager) { - m.On("ReadConfig").Return(nil, errors.New("read 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 *MockInstallationManager) { + setupMock: func(m *installation.MockInstallationManager) { config := &types.InstallationConfig{} - m.On("ReadConfig").Return(config, nil) - m.On("SetDefaults", config).Return(errors.New("defaults error")) + 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 *MockInstallationManager) { - config := &types.InstallationConfig{} - m.On("ReadConfig").Return(config, nil) - m.On("SetDefaults", config).Return(nil) - m.On("ValidateConfig", config).Return(errors.New("validation error")) - }, - expectedErr: true, - expectedValue: nil, - }, - { - name: "read status error", - setupMock: func(m *MockInstallationManager) { + setupMock: func(m *installation.MockInstallationManager) { config := &types.InstallationConfig{} - m.On("ReadConfig").Return(config, nil) - m.On("SetDefaults", config).Return(nil) - m.On("ValidateConfig", config).Return(nil) - m.On("ReadStatus").Return(nil, errors.New("status error")) + 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, @@ -137,14 +82,13 @@ func TestGet(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockManager := &MockInstallationManager{} + mockManager := &installation.MockInstallationManager{} tt.setupMock(mockManager) - controller := &InstallController{ - installationManager: mockManager, - } + controller, err := NewInstallController(WithInstallationManager(mockManager)) + require.NoError(t, err) - result, err := controller.Get(context.Background()) + result, err := controller.GetInstallationConfig(context.Background()) if tt.expectedErr { assert.Error(t, err) @@ -159,39 +103,44 @@ func TestGet(t *testing.T) { } } -func TestSetConfig(t *testing.T) { +func TestConfigureInstallation(t *testing.T) { tests := []struct { name string config *types.InstallationConfig - setupMock func(*MockInstallationManager, *types.InstallationConfig) + setupMock func(*installation.MockInstallationManager, *types.InstallationConfig) expectedErr bool }{ { - name: "successful set config", + name: "successful configure installation", config: &types.InstallationConfig{ LocalArtifactMirrorPort: 9000, DataDirectory: "/data/dir", }, - setupMock: func(m *MockInstallationManager, config *types.InstallationConfig) { - m.On("ValidateConfig", config).Return(nil) - m.On("WriteConfig", *config).Return(nil) + setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { + mock.InOrder( + m.On("ValidateConfig", config).Return(nil), + m.On("SetConfig", *config).Return(nil), + m.On("ConfigureForInstall", context.Background(), config).Return(nil), + ) }, expectedErr: false, }, { name: "validate error", config: &types.InstallationConfig{}, - setupMock: func(m *MockInstallationManager, config *types.InstallationConfig) { + setupMock: func(m *installation.MockInstallationManager, config *types.InstallationConfig) { m.On("ValidateConfig", config).Return(errors.New("validation error")) }, expectedErr: true, }, { - name: "write config error", + name: "set config error", config: &types.InstallationConfig{}, - setupMock: func(m *MockInstallationManager, config *types.InstallationConfig) { - m.On("ValidateConfig", config).Return(nil) - m.On("WriteConfig", *config).Return(errors.New("write error")) + 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, }, @@ -200,14 +149,17 @@ func TestSetConfig(t *testing.T) { config: &types.InstallationConfig{ GlobalCIDR: "10.0.0.0/16", }, - setupMock: func(m *MockInstallationManager, config *types.InstallationConfig) { + 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" - m.On("ValidateConfig", config).Return(nil) - m.On("WriteConfig", configWithCIDRs).Return(nil) + mock.InOrder( + m.On("ValidateConfig", config).Return(nil), + m.On("SetConfig", configWithCIDRs).Return(nil), + m.On("ConfigureForInstall", context.Background(), &configWithCIDRs).Return(nil), + ) }, expectedErr: false, }, @@ -215,18 +167,17 @@ func TestSetConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockManager := &MockInstallationManager{} + mockManager := &installation.MockInstallationManager{} // Create a copy of the config to avoid modifying the original configCopy := *tt.config tt.setupMock(mockManager, &configCopy) - controller := &InstallController{ - installationManager: mockManager, - } + controller, err := NewInstallController(WithInstallationManager(mockManager)) + require.NoError(t, err) - err := controller.SetConfig(context.Background(), tt.config) + err = controller.ConfigureInstallation(context.Background(), tt.config) if tt.expectedErr { assert.Error(t, err) @@ -239,38 +190,119 @@ func TestSetConfig(t *testing.T) { } } -func TestSetStatus(t *testing.T) { +// TestIntegrationComputeCIDRs tests the CIDR computation with real networking utility +func TestIntegrationComputeCIDRs(t *testing.T) { tests := []struct { name string - status *types.InstallationStatus - setupMock func(*MockInstallationManager, *types.InstallationStatus) + globalCIDR string + expectedPod string + expectedSvc string expectedErr bool }{ { - name: "successful set status", - status: &types.InstallationStatus{ - State: types.InstallationStateFailed, + 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{}, }, - setupMock: func(m *MockInstallationManager, status *types.InstallationStatus) { - m.On("ValidateStatus", status).Return(nil) - m.On("WriteStatus", *status).Return(nil) + }, + } + + expectedProxy := &ecv1beta1.ProxySpec{ + HTTPProxy: "http://proxy.example.com", + HTTPSProxy: "https://proxy.example.com", + ProvidedNoProxy: "provided-proxy.com", + NoProxy: "no-proxy.com", + } + + tests := []struct { + name string + setupMocks func(*installation.MockInstallationManager, *preflight.MockHostPreflightManager) + expectedErr bool + }{ + { + name: "successful run preflights", + setupMocks: func(im *installation.MockInstallationManager, pm *preflight.MockHostPreflightManager) { + mock.InOrder( + im.On("GetConfig").Return(&types.InstallationConfig{DataDirectory: "/data/dir"}, nil), + pm.On("PrepareHostPreflights", context.Background(), mock.Anything).Return(expectedHPF, expectedProxy, nil), + pm.On("RunHostPreflights", context.Background(), mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec && expectedProxy == opts.Proxy + })).Return(nil), + ) }, expectedErr: false, }, { - name: "validate error", - status: &types.InstallationStatus{}, - setupMock: func(m *MockInstallationManager, status *types.InstallationStatus) { - m.On("ValidateStatus", status).Return(errors.New("validation error")) + name: "prepare preflights error", + setupMocks: func(im *installation.MockInstallationManager, pm *preflight.MockHostPreflightManager) { + mock.InOrder( + im.On("GetConfig").Return(&types.InstallationConfig{DataDirectory: "/data/dir"}, nil), + pm.On("PrepareHostPreflights", context.Background(), mock.Anything).Return(nil, nil, errors.New("prepare error")), + ) }, expectedErr: true, }, { - name: "write status error", - status: &types.InstallationStatus{}, - setupMock: func(m *MockInstallationManager, status *types.InstallationStatus) { - m.On("ValidateStatus", status).Return(nil) - m.On("WriteStatus", *status).Return(errors.New("write error")) + name: "run preflights error", + setupMocks: func(im *installation.MockInstallationManager, pm *preflight.MockHostPreflightManager) { + mock.InOrder( + im.On("GetConfig").Return(&types.InstallationConfig{DataDirectory: "/data/dir"}, nil), + pm.On("PrepareHostPreflights", context.Background(), mock.Anything).Return(expectedHPF, expectedProxy, nil), + pm.On("RunHostPreflights", context.Background(), mock.MatchedBy(func(opts preflight.RunHostPreflightOptions) bool { + return expectedHPF == opts.HostPreflightSpec && expectedProxy == opts.Proxy + })).Return(errors.New("run preflights error")), + ) }, expectedErr: true, }, @@ -278,19 +310,77 @@ func TestSetStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockManager := &MockInstallationManager{} - tt.setupMock(mockManager, tt.status) + mockInstallationManager := &installation.MockInstallationManager{} + mockPreflightManager := &preflight.MockHostPreflightManager{} + tt.setupMocks(mockInstallationManager, mockPreflightManager) - controller := &InstallController{ - installationManager: mockManager, + controller, err := NewInstallController( + WithInstallationManager(mockInstallationManager), + WithHostPreflightManager(mockPreflightManager), + WithReleaseData(getTestReleaseData()), + ) + require.NoError(t, err) + + err = controller.RunHostPreflights(context.Background()) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) } - err := controller.SetStatus(context.Background(), tt.status) + mockInstallationManager.AssertExpectations(t) + 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", context.Background()).Return(status, nil) + }, + expectedErr: false, + expectedValue: &types.Status{ + State: types.StateFailed, + }, + }, + { + name: "get status error", + setupMock: func(m *preflight.MockHostPreflightManager) { + m.On("GetHostPreflightStatus", context.Background()).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(context.Background()) 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) @@ -298,30 +388,40 @@ func TestSetStatus(t *testing.T) { } } -func TestReadStatus(t *testing.T) { +func TestGetHostPreflightOutput(t *testing.T) { tests := []struct { name string - setupMock func(*MockInstallationManager) + setupMock func(*preflight.MockHostPreflightManager) expectedErr bool - expectedValue *types.InstallationStatus + expectedValue *types.HostPreflightOutput }{ { - name: "successful read status", - setupMock: func(m *MockInstallationManager) { - status := &types.InstallationStatus{ - State: types.InstallationStateFailed, + name: "successful get output", + setupMock: func(m *preflight.MockHostPreflightManager) { + output := &types.HostPreflightOutput{ + Pass: []types.HostPreflightRecord{ + { + Title: "Test Check", + Message: "Test check passed", + }, + }, } - m.On("ReadStatus").Return(status, nil) + m.On("GetHostPreflightOutput", context.Background()).Return(output, nil) }, expectedErr: false, - expectedValue: &types.InstallationStatus{ - State: types.InstallationStateFailed, + expectedValue: &types.HostPreflightOutput{ + Pass: []types.HostPreflightRecord{ + { + Title: "Test Check", + Message: "Test check passed", + }, + }, }, }, { - name: "read error", - setupMock: func(m *MockInstallationManager) { - m.On("ReadStatus").Return(nil, errors.New("read error")) + name: "get output error", + setupMock: func(m *preflight.MockHostPreflightManager) { + m.On("GetHostPreflightOutput", context.Background()).Return(nil, errors.New("get output error")) }, expectedErr: true, expectedValue: nil, @@ -330,14 +430,13 @@ func TestReadStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockManager := &MockInstallationManager{} + mockManager := &preflight.MockHostPreflightManager{} tt.setupMock(mockManager) - controller := &InstallController{ - installationManager: mockManager, - } + controller, err := NewInstallController(WithHostPreflightManager(mockManager)) + require.NoError(t, err) - result, err := controller.ReadStatus(context.Background()) + result, err := controller.GetHostPreflightOutput(context.Background()) if tt.expectedErr { assert.Error(t, err) @@ -352,131 +451,282 @@ func TestReadStatus(t *testing.T) { } } -// TestControllerWithRealManager tests the controller with the real installation manager -func TestControllerWithRealManager(t *testing.T) { - // Create controller with real manager - controller, err := NewInstallController() - assert.NoError(t, err) - assert.NotNil(t, controller) +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", context.Background()).Return(titles, nil) + }, + expectedErr: false, + expectedValue: []string{"Check 1", "Check 2"}, + }, + { + name: "get titles error", + setupMock: func(m *preflight.MockHostPreflightManager) { + m.On("GetHostPreflightTitles", context.Background()).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) - // Test the full cycle of operations + controller, err := NewInstallController(WithHostPreflightManager(mockManager)) + require.NoError(t, err) + + result, err := controller.GetHostPreflightTitles(context.Background()) + + if tt.expectedErr { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } - // 1. Set an invalid config - testConfig := &types.InstallationConfig{ - AdminConsolePort: 8800, - GlobalCIDR: "10.0.0.0/24", - DataDirectory: "/data/dir", + mockManager.AssertExpectations(t) + }) } +} - err = controller.SetConfig(context.Background(), testConfig) - assert.Error(t, err) - assert.Contains(t, err.Error(), "validate") - - // 2. Verify we can read the config with defaults - install, err := controller.Get(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, install) - assert.Equal(t, 50000, install.Config.LocalArtifactMirrorPort, "Default LocalArtifactMirrorPort should be set") - assert.Equal(t, "/var/lib/embedded-cluster", install.Config.DataDirectory, "Default DataDirectory should be set") - - // 3. Set a valid config - install.Config.LocalArtifactMirrorPort = 9000 - install.Config.DataDirectory = "/data/dir" - err = controller.SetConfig(context.Background(), &install.Config) - assert.NoError(t, err) - - // 4. Verify we can read the config again and it has the new values - install, err = controller.Get(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, install) - assert.Equal(t, 9000, install.Config.LocalArtifactMirrorPort, "LocalArtifactMirrorPort should be set to 9000") - assert.Equal(t, "/data/dir", install.Config.DataDirectory, "DataDirectory should be set to /data/dir") - - // 5. Set an invalid status - testStatus := &types.InstallationStatus{ - State: "Not a real state", - Description: "Installation in progress", - LastUpdated: time.Now(), +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, + }, } - err = controller.SetStatus(context.Background(), testStatus) - assert.Error(t, err) - assert.Contains(t, err.Error(), "validate") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockManager := &installation.MockInstallationManager{} + tt.setupMock(mockManager) - // 6. Set a valid status - testStatus = &types.InstallationStatus{ - State: types.InstallationStateRunning, - Description: "Installation in progress", - LastUpdated: time.Now(), - } + controller, err := NewInstallController(WithInstallationManager(mockManager)) + require.NoError(t, err) + + result, err := controller.GetInstallationStatus(context.Background()) - err = controller.SetStatus(context.Background(), testStatus) - assert.NoError(t, err) + if tt.expectedErr { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, result) + } - // 7. Verify we can read status directly - status, err := controller.ReadStatus(context.Background()) - assert.NoError(t, err) - assert.NotNil(t, status) - assert.Equal(t, types.InstallationStateRunning, status.State) - assert.Equal(t, "Installation in progress", status.Description) + mockManager.AssertExpectations(t) + }) + } } -// TestIntegrationComputeCIDRs tests the CIDR computation with real networking utility -func TestIntegrationComputeCIDRs(t *testing.T) { +func TestSetupNode(t *testing.T) { tests := []struct { name string - globalCIDR string - expectedPod string - expectedSvc string + setupMocks func(*preflight.MockHostPreflightManager, *metrics.MockReporter) 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", + name: "successful setup with passed preflights", + setupMocks: func(m *preflight.MockHostPreflightManager, r *metrics.MockReporter) { + preflightStatus := &types.Status{ + State: types.StateSucceeded, + } + m.On("GetHostPreflightStatus", context.Background()).Return(preflightStatus, nil) + }, 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", + name: "successful setup with failed preflights", + setupMocks: func(m *preflight.MockHostPreflightManager, r *metrics.MockReporter) { + preflightStatus := &types.Status{ + State: types.StateFailed, + } + preflightOutput := &types.HostPreflightOutput{ + Fail: []types.HostPreflightRecord{ + { + Title: "Test Check", + Message: "Test check failed", + }, + }, + } + mock.InOrder( + m.On("GetHostPreflightStatus", context.Background()).Return(preflightStatus, nil), + m.On("GetHostPreflightOutput", context.Background()).Return(preflightOutput, nil), + r.On("ReportPreflightsFailed", context.Background(), preflightOutput).Return(nil), + ) + }, expectedErr: false, }, { - name: "no global cidr", - globalCIDR: "", - expectedPod: "", // Should remain unchanged - expectedSvc: "", // Should remain unchanged - expectedErr: false, + name: "preflight status error", + setupMocks: func(m *preflight.MockHostPreflightManager, r *metrics.MockReporter) { + m.On("GetHostPreflightStatus", context.Background()).Return(nil, errors.New("get status error")) + }, + expectedErr: true, }, { - name: "invalid cidr", - globalCIDR: "not-a-cidr", + name: "preflight not completed", + setupMocks: func(m *preflight.MockHostPreflightManager, r *metrics.MockReporter) { + preflightStatus := &types.Status{ + State: types.StateRunning, + } + m.On("GetHostPreflightStatus", context.Background()).Return(preflightStatus, nil) + }, + expectedErr: true, + }, + { + name: "preflight output error", + setupMocks: func(m *preflight.MockHostPreflightManager, r *metrics.MockReporter) { + preflightStatus := &types.Status{ + State: types.StateFailed, + } + mock.InOrder( + m.On("GetHostPreflightStatus", context.Background()).Return(preflightStatus, nil), + m.On("GetHostPreflightOutput", context.Background()).Return(nil, errors.New("get output error")), + ) + }, expectedErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - controller := &InstallController{ - installationManager: &MockInstallationManager{}, + mockPreflightManager := &preflight.MockHostPreflightManager{} + mockMetricsReporter := &metrics.MockReporter{} + tt.setupMocks(mockPreflightManager, mockMetricsReporter) + + controller, err := NewInstallController( + WithHostPreflightManager(mockPreflightManager), + WithMetricsReporter(mockMetricsReporter), + ) + require.NoError(t, err) + + err = controller.SetupNode(context.Background()) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) } - config := &types.InstallationConfig{ - GlobalCIDR: tt.globalCIDR, + mockPreflightManager.AssertExpectations(t) + mockMetricsReporter.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, } - err := controller.computeCIDRs(config) + result, err := controller.GetStatus(context.Background()) + + 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(context.Background(), tt.status) 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) + assert.Equal(t, tt.status, controller.install.Status) } }) } } + +func getTestReleaseData() *release.ReleaseData { + return &release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{}, + } +} diff --git a/api/controllers/install/hostpreflight.go b/api/controllers/install/hostpreflight.go new file mode 100644 index 000000000..e587e1f02 --- /dev/null +++ b/api/controllers/install/hostpreflight.go @@ -0,0 +1,53 @@ +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) error { + // Get current installation config and add it to options + config, err := c.installationManager.GetConfig() + if err != nil { + return fmt.Errorf("failed to read installation config: %w", err) + } + + // Get the configured custom domains + ecDomains := utils.GetDomains(c.releaseData) + + // Prepare host preflights + hpf, proxy, err := c.hostPreflightManager.PrepareHostPreflights(ctx, preflight.PrepareHostPreflightOptions{ + InstallationConfig: config, + ReplicatedAppURL: netutils.MaybeAddHTTPS(ecDomains.ReplicatedAppDomain), + ProxyRegistryURL: netutils.MaybeAddHTTPS(ecDomains.ProxyRegistryDomain), + HostPreflightSpec: c.releaseData.HostPreflights, + EmbeddedClusterConfig: c.releaseData.EmbeddedClusterConfig, + IsAirgap: c.airgapBundle != "", + }) + 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, + Proxy: proxy, + }) +} + +func (c *InstallController) GetHostPreflightStatus(ctx context.Context) (*types.Status, error) { + return c.hostPreflightManager.GetHostPreflightStatus(ctx) +} + +func (c *InstallController) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightOutput, 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/installation.go b/api/controllers/install/installation.go new file mode 100644 index 000000000..a865700be --- /dev/null +++ b/api/controllers/install/installation.go @@ -0,0 +1,72 @@ +package install + +import ( + "context" + "fmt" + + "github.com/replicatedhq/embedded-cluster/api/types" + "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) + } + + // update the runtime config + c.rc.SetDataDir(config.DataDirectory) + c.rc.SetLocalArtifactMirrorPort(config.LocalArtifactMirrorPort) + c.rc.SetAdminConsolePort(config.AdminConsolePort) + + if err := c.installationManager.ConfigureForInstall(ctx, config); 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/node.go b/api/controllers/install/node.go new file mode 100644 index 000000000..8d44ed404 --- /dev/null +++ b/api/controllers/install/node.go @@ -0,0 +1,33 @@ +package install + +import ( + "context" + "fmt" + + "github.com/replicatedhq/embedded-cluster/api/types" +) + +func (c *InstallController) SetupNode(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) + } + } + + // TODO: implement node setup + + return nil +} diff --git a/api/controllers/install/status.go b/api/controllers/install/status.go new file mode 100644 index 000000000..f8359e3ae --- /dev/null +++ b/api/controllers/install/status.go @@ -0,0 +1,18 @@ +package install + +import ( + "context" + + "github.com/replicatedhq/embedded-cluster/api/types" +) + +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) { + return c.install.Status, nil +} diff --git a/api/docs/docs.go b/api/docs/docs.go index 556d49a6b..100e9cfca 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.HostPreflightOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.HostPreflightState":{"type":"string","x-enum-varnames":["HostPreflightStatePending","HostPreflightStateRunning","HostPreflightStateSucceeded","HostPreflightStateFailed"]},"types.HostPreflightStatus":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.HostPreflightState"}},"type":"object"},"types.HostPreflightStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightOutput"},"status":{"$ref":"#/components/schemas/types.HostPreflightStatus"}},"type":"object"},"types.Install":{"properties":{"config":{"$ref":"#/components/schemas/types.InstallationConfig"},"status":{"$ref":"#/components/schemas/types.InstallationStatus"}},"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.InstallationState":{"type":"string","x-enum-varnames":["InstallationStatePending","InstallationStateRunning","InstallationStateSucceeded","InstallationStateFailed"]},"types.InstallationStatus":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.InstallationState"}},"type":"object"},"types.RunHostPreflightResponse":{"properties":{"status":{"$ref":"#/components/schemas/types.HostPreflightStatus"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightOutput"},"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.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":{"get":{"description":"get the install object","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Install"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the install object","tags":["install"]}},"/install/config":{"post":{"description":"set the installation config","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.Install"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the installation config","tags":["install"]}},"/install/host-preflights":{"get":{"description":"Get the current status and results of host preflight checks","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.HostPreflightStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status","tags":["install"]},"post":{"description":"Run install host preflight checks using installation config and client-provided data","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.RunHostPreflightResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/status":{"get":{"description":"get the installation status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationStatus"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation status","tags":["install"]},"post":{"description":"set the installation status","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationStatus"}}},"description":"Installation status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Install"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the installation status","tags":["install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","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/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/node/setup":{"post":{"description":"Setup a node","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup a node","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 239a16c1c..1eb7d1d7f 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.HostPreflightOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.HostPreflightState":{"type":"string","x-enum-varnames":["HostPreflightStatePending","HostPreflightStateRunning","HostPreflightStateSucceeded","HostPreflightStateFailed"]},"types.HostPreflightStatus":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.HostPreflightState"}},"type":"object"},"types.HostPreflightStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightOutput"},"status":{"$ref":"#/components/schemas/types.HostPreflightStatus"}},"type":"object"},"types.Install":{"properties":{"config":{"$ref":"#/components/schemas/types.InstallationConfig"},"status":{"$ref":"#/components/schemas/types.InstallationStatus"}},"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.InstallationState":{"type":"string","x-enum-varnames":["InstallationStatePending","InstallationStateRunning","InstallationStateSucceeded","InstallationStateFailed"]},"types.InstallationStatus":{"properties":{"description":{"type":"string"},"lastUpdated":{"type":"string"},"state":{"$ref":"#/components/schemas/types.InstallationState"}},"type":"object"},"types.RunHostPreflightResponse":{"properties":{"status":{"$ref":"#/components/schemas/types.HostPreflightStatus"}},"type":"object"}},"securitySchemes":{"bearerauth":{"bearerFormat":"JWT","scheme":"bearer","type":"http"}}}, + "components": {"schemas":{"types.APIError":{"properties":{"errors":{"items":{"$ref":"#/components/schemas/types.APIError"},"type":"array","uniqueItems":false},"field":{"type":"string"},"message":{"type":"string"},"status_code":{"type":"integer"}},"type":"object"},"types.AuthRequest":{"properties":{"password":{"type":"string"}},"type":"object"},"types.AuthResponse":{"properties":{"token":{"type":"string"}},"type":"object"},"types.Health":{"properties":{"status":{"type":"string"}},"type":"object"},"types.HostPreflightOutput":{"properties":{"fail":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"pass":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false},"warn":{"items":{"$ref":"#/components/schemas/types.HostPreflightRecord"},"type":"array","uniqueItems":false}},"type":"object"},"types.HostPreflightRecord":{"properties":{"message":{"type":"string"},"title":{"type":"string"}},"type":"object"},"types.InstallHostPreflightsStatusResponse":{"properties":{"output":{"$ref":"#/components/schemas/types.HostPreflightOutput"},"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.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":{"get":{"description":"get the install object","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Install"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the install object","tags":["install"]}},"/install/config":{"post":{"description":"set the installation config","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.Install"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the installation config","tags":["install"]}},"/install/host-preflights":{"get":{"description":"Get the current status and results of host preflight checks","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.HostPreflightStatusResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get host preflight status","tags":["install"]},"post":{"description":"Run install host preflight checks using installation config and client-provided data","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.RunHostPreflightResponse"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Run install host preflight checks","tags":["install"]}},"/install/status":{"get":{"description":"get the installation status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationStatus"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the installation status","tags":["install"]},"post":{"description":"set the installation status","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.InstallationStatus"}}},"description":"Installation status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Install"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the installation status","tags":["install"]}}}, + "paths": {"/auth/login":{"post":{"description":"Authenticate a user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthRequest"}}},"description":"Auth Request","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.AuthResponse"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.APIError"}}},"description":"Unauthorized"}},"summary":"Authenticate a user","tags":["auth"]}},"/health":{"get":{"description":"get the health of the API","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Health"}}},"description":"OK"}},"summary":"Get the health of the API","tags":["health"]}},"/install/host-preflights/run":{"post":{"description":"Run install host preflight checks using installation config and client-provided data","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/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/node/setup":{"post":{"description":"Setup a node","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Setup a node","tags":["install"]}},"/install/status":{"get":{"description":"Get the current status of the install workflow","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Get the status of the install workflow","tags":["install"]},"post":{"description":"Set the status of the install workflow","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"Status","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/types.Status"}}},"description":"OK"}},"security":[{"bearerauth":[]}],"summary":"Set the status of the install workflow","tags":["install"]}}}, "openapi": "3.1.0", "servers": [ {"url":"/api"} diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index ffe17f7ff..924123d25 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -54,35 +54,17 @@ components: title: type: string type: object - types.HostPreflightState: - type: string - x-enum-varnames: - - HostPreflightStatePending - - HostPreflightStateRunning - - HostPreflightStateSucceeded - - HostPreflightStateFailed - types.HostPreflightStatus: - properties: - description: - type: string - lastUpdated: - type: string - state: - $ref: '#/components/schemas/types.HostPreflightState' - type: object - types.HostPreflightStatusResponse: + types.InstallHostPreflightsStatusResponse: properties: output: $ref: '#/components/schemas/types.HostPreflightOutput' status: - $ref: '#/components/schemas/types.HostPreflightStatus' - type: object - types.Install: - properties: - config: - $ref: '#/components/schemas/types.InstallationConfig' - status: - $ref: '#/components/schemas/types.InstallationStatus' + $ref: '#/components/schemas/types.Status' + titles: + items: + type: string + type: array + uniqueItems: false type: object types.InstallationConfig: properties: @@ -107,26 +89,21 @@ components: serviceCidr: type: string type: object - types.InstallationState: + types.State: type: string x-enum-varnames: - - InstallationStatePending - - InstallationStateRunning - - InstallationStateSucceeded - - InstallationStateFailed - types.InstallationStatus: + - StatePending + - StateRunning + - StateSucceeded + - StateFailed + types.Status: properties: description: type: string lastUpdated: type: string state: - $ref: '#/components/schemas/types.InstallationState' - type: object - types.RunHostPreflightResponse: - properties: - status: - $ref: '#/components/schemas/types.HostPreflightStatus' + $ref: '#/components/schemas/types.State' type: object securitySchemes: bearerauth: @@ -189,24 +166,56 @@ paths: summary: Get the health of the API tags: - health - /install: + /install/host-preflights/run: + post: + description: Run install host preflight checks using installation config and + client-provided data + 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 install object + description: Get the current status and results of host preflight checks for + install responses: "200": content: application/json: schema: - $ref: '#/components/schemas/types.Install' + $ref: '#/components/schemas/types.InstallHostPreflightsStatusResponse' description: OK security: - bearerauth: [] - summary: Get the install object + summary: Get host preflight status for install tags: - install - /install/config: + /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: set the installation config + description: configure the installation for install requestBody: content: application/json: @@ -219,77 +228,77 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/types.Install' + $ref: '#/components/schemas/types.Status' description: OK security: - bearerauth: [] - summary: Set the installation config + summary: Configure the installation for install tags: - install - /install/host-preflights: + /install/installation/status: get: - description: Get the current status and results of host preflight checks + description: Get the current status of the installation configuration for install responses: "200": content: application/json: schema: - $ref: '#/components/schemas/types.HostPreflightStatusResponse' + $ref: '#/components/schemas/types.Status' description: OK security: - bearerauth: [] - summary: Get host preflight status + summary: Get installation configuration status for install tags: - install + /install/node/setup: post: - description: Run install host preflight checks using installation config and - client-provided data + description: Setup a node responses: "200": content: application/json: schema: - $ref: '#/components/schemas/types.RunHostPreflightResponse' + $ref: '#/components/schemas/types.Status' description: OK security: - bearerauth: [] - summary: Run install host preflight checks + summary: Setup a node tags: - install /install/status: get: - description: get the installation status + description: Get the current status of the install workflow responses: "200": content: application/json: schema: - $ref: '#/components/schemas/types.InstallationStatus' + $ref: '#/components/schemas/types.Status' description: OK security: - bearerauth: [] - summary: Get the installation status + summary: Get the status of the install workflow tags: - install post: - description: set the installation status + description: Set the status of the install workflow requestBody: content: application/json: schema: - $ref: '#/components/schemas/types.InstallationStatus' - description: Installation status + $ref: '#/components/schemas/types.Status' + description: Status required: true responses: "200": content: application/json: schema: - $ref: '#/components/schemas/types.Install' + $ref: '#/components/schemas/types.Status' description: OK security: - bearerauth: [] - summary: Set the installation status + summary: Set the status of the install workflow tags: - install servers: diff --git a/api/install.go b/api/install.go index c5d8e8a17..e098886e7 100644 --- a/api/install.go +++ b/api/install.go @@ -7,38 +7,38 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ) -// getInstall handler to get the install object +// getInstallInstallationConfig handler to get the installation config // -// @Summary Get the install object -// @Description get the install object +// @Summary Get the installation config +// @Description get the installation config // @Tags install // @Security bearerauth // @Produce json -// @Success 200 {object} types.Install -// @Router /install [get] -func (a *API) getInstall(w http.ResponseWriter, r *http.Request) { - install, err := a.installController.Get(r.Context()) +// @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") + a.logError(r, err, "failed to get installation config") a.jsonError(w, r, err) return } - a.json(w, r, http.StatusOK, install) + a.json(w, r, http.StatusOK, config) } -// setInstallConfig handler to set the installation config +// postInstallConfigureInstallation handler to configure the installation for install // -// @Summary Set the installation config -// @Description set the installation config +// @Summary Configure the installation for install +// @Description configure the installation for install // @Tags install // @Security bearerauth // @Accept json // @Produce json -// @Param request body types.InstallationConfig true "Installation config" -// @Success 200 {object} types.Install -// @Router /install/config [post] -func (a *API) setInstallConfig(w http.ResponseWriter, r *http.Request) { +// @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 := json.NewDecoder(r.Body).Decode(&config); err != nil { a.logError(r, err, "failed to decode installation config") @@ -46,90 +46,177 @@ func (a *API) setInstallConfig(w http.ResponseWriter, r *http.Request) { return } - if err := a.installController.SetConfig(r.Context(), &config); err != nil { + if err := a.installController.ConfigureInstallation(r.Context(), &config); err != nil { a.logError(r, err, "failed to set installation config") a.jsonError(w, r, err) return } - a.getInstall(w, r) + a.getInstallInstallationStatus(w, r) +} - // TODO: this is a hack to get the config to the CLI - if a.configChan != nil { - a.configChan <- &config +// 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) } -// setInstallStatus handler to set the installation status +// postInstallRunHostPreflights handler to run install host preflight checks // -// @Summary Set the installation status -// @Description set the installation status +// @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.InstallationStatus true "Installation status" -// @Success 200 {object} types.Install -// @Router /install/status [post] -func (a *API) setInstallStatus(w http.ResponseWriter, r *http.Request) { - var status types.InstallationStatus - if err := json.NewDecoder(r.Body).Decode(&status); err != nil { - a.logError(r, err, "failed to decode installation status") - a.jsonError(w, r, types.NewBadRequestError(err)) +// @Success 200 {object} types.InstallHostPreflightsStatusResponse +// @Router /install/host-preflights/run [post] +func (a *API) postInstallRunHostPreflights(w http.ResponseWriter, r *http.Request) { + err := a.installController.RunHostPreflights(r.Context()) + if err != nil { + a.logError(r, err, "failed to run install host preflights") + a.jsonError(w, r, err) return } - if err := a.installController.SetStatus(r.Context(), &status); err != nil { - a.logError(r, err, "failed to set installation status") + 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 } - a.getInstall(w, r) + response := types.InstallHostPreflightsStatusResponse{ + Titles: titles, + Output: output, + Status: status, + } + + a.json(w, r, http.StatusOK, response) } -// getInstallStatus handler to get the installation status +// postInstallSetupNode handler to setup a node // -// @Summary Get the installation status -// @Description get the installation status +// @Summary Setup a node +// @Description Setup a node // @Tags install // @Security bearerauth // @Produce json -// @Success 200 {object} types.InstallationStatus -// @Router /install/status [get] -func (a *API) getInstallStatus(w http.ResponseWriter, r *http.Request) { - status, err := a.installController.ReadStatus(r.Context()) +// @Success 200 {object} types.Status +// @Router /install/node/setup [post] +func (a *API) postInstallSetupNode(w http.ResponseWriter, r *http.Request) { + err := a.installController.SetupNode(r.Context()) if err != nil { - a.logError(r, err, "failed to get installation status") + a.logError(r, err, "failed to setup node") a.jsonError(w, r, err) return } - a.json(w, r, http.StatusOK, status) + 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 + } + + // TODO: this is a hack to get the config to the CLI + if a.configChan != nil { + a.configChan <- config + } + + a.getInstallStatus(w, r) } -// runInstallHostPreflights handler to run install host preflight checks +// postInstallSetInstallStatus handler to set the status of the install workflow // -// @Summary Run install host preflight checks -// @Description Run install host preflight checks using installation config and client-provided data +// @Summary Set the status of the install workflow +// @Description Set the status of the install workflow // @Tags install // @Security bearerauth +// @Accept json // @Produce json -// @Success 200 {object} types.RunHostPreflightResponse -// @Router /install/host-preflights [post] -func (a *API) runInstallHostPreflights(w http.ResponseWriter, r *http.Request) { - // TODO: implement +// @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 := json.NewDecoder(r.Body).Decode(&status); err != nil { + a.logError(r, err, "failed to decode install status") + a.jsonError(w, r, types.NewBadRequestError(err)) + 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) } -// getInstallHostPreflightStatus handler to get host preflight status +// getInstallStatus handler to get the status of the install workflow // -// @Summary Get host preflight status -// @Description Get the current status and results of host preflight checks +// @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.HostPreflightStatusResponse -// @Router /install/host-preflights [get] -func (a *API) getInstallHostPreflightStatus(w http.ResponseWriter, r *http.Request) { - // TODO: implement +// @Success 200 {object} types.Status +// @Router /install/status [get] +func (a *API) getInstallStatus(w http.ResponseWriter, r *http.Request) { + status, err := a.installController.GetStatus(r.Context()) + if err != nil { + a.logError(r, err, "failed to get install status") + a.jsonError(w, r, err) + return + } + + a.json(w, r, http.StatusOK, status) } diff --git a/api/integration/auth_controller_test.go b/api/integration/auth_controller_test.go index 2223b0270..322c33810 100644 --- a/api/integration/auth_controller_test.go +++ b/api/integration/auth_controller_test.go @@ -12,7 +12,8 @@ import ( "github.com/replicatedhq/embedded-cluster/api/client" "github.com/replicatedhq/embedded-cluster/api/controllers/auth" "github.com/replicatedhq/embedded-cluster/api/controllers/install" - "github.com/replicatedhq/embedded-cluster/api/pkg/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" + "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,7 +39,7 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { password, api.WithAuthController(authController), api.WithInstallController(installController), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -108,7 +109,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", nil) + req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) rec := httptest.NewRecorder() // Serve the request @@ -120,7 +121,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", nil) + req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"invalid-token") rec := httptest.NewRecorder() @@ -135,22 +136,10 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { func TestAPIClientLogin(t *testing.T) { password := "test-password" - // Create an auth controller - authController, err := auth.NewAuthController(password) - require.NoError(t, err) - - // Create an install controller - installController, err := install.NewInstallController( - install.WithInstallationManager(installation.NewInstallationManager()), - ) - require.NoError(t, err) - // Create the API with the auth controller apiInstance, err := api.New( password, - api.WithAuthController(authController), - api.WithInstallController(installController), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -172,9 +161,9 @@ func TestAPIClientLogin(t *testing.T) { require.NoError(t, err, "API client login should succeed with correct password") // Verify we can make authenticated requests after login - install, err := c.GetInstall() - require.NoError(t, err, "API client should be able to get install after successful login") - assert.NotNil(t, install, "Install should not be nil") + status, 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 @@ -192,7 +181,7 @@ func TestAPIClientLogin(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, apiErr.StatusCode, "Error should have Unauthorized status code") // Verify we can't make authenticated requests - _, err = c.GetInstall() - require.Error(t, err, "API client should not be able to get install after failed login") + _, err = c.GetInstallationStatus() + require.Error(t, err, "API client should not be able to get installation status after failed login") }) } diff --git a/api/integration/console_test.go b/api/integration/console_test.go index ada08b522..d5afd10c9 100644 --- a/api/integration/console_test.go +++ b/api/integration/console_test.go @@ -10,6 +10,7 @@ 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/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,7 +30,7 @@ func TestConsoleListAvailableNetworkInterfaces(t *testing.T) { "password", api.WithConsoleController(consoleController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -74,7 +75,7 @@ func TestConsoleListAvailableNetworkInterfacesUnauthorized(t *testing.T) { "password", api.WithConsoleController(consoleController), api.WithAuthController(&staticAuthController{"VALID_TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -115,7 +116,7 @@ func TestConsoleListAvailableNetworkInterfacesError(t *testing.T) { "password", api.WithConsoleController(consoleController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) diff --git a/api/integration/install_test.go b/api/integration/install_test.go index 578ecf259..88de539ff 100644 --- a/api/integration/install_test.go +++ b/api/integration/install_test.go @@ -14,49 +14,96 @@ import ( "github.com/replicatedhq/embedded-cluster/api" "github.com/replicatedhq/embedded-cluster/api/client" "github.com/replicatedhq/embedded-cluster/api/controllers/install" - "github.com/replicatedhq/embedded-cluster/api/pkg/installation" + "github.com/replicatedhq/embedded-cluster/api/internal/managers/installation" + "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/runtimeconfig" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) -var _ install.Controller = &mockInstallController{} - // Mock implementation of the install.Controller interface type mockInstallController struct { - setConfigError error - getError error - setStatusError error - readStatusError error + configureInstallationError error + getInstallationConfigError error + runHostPreflightsError error + getHostPreflightStatusError error + getHostPreflightOutputError error + getHostPreflightTitlesError error + setupNodeError error + setStatusError error + readStatusError error +} + +func (m *mockInstallController) GetInstallationConfig(ctx context.Context) (*types.InstallationConfig, error) { + if m.getInstallationConfigError != nil { + return nil, m.getInstallationConfigError + } + return &types.InstallationConfig{}, nil +} + +func (m *mockInstallController) ConfigureInstallation(ctx context.Context, config *types.InstallationConfig) error { + return m.configureInstallationError } -func (m *mockInstallController) Get(ctx context.Context) (*types.Install, error) { - if m.getError != nil { - return nil, m.getError +func (m *mockInstallController) GetInstallationStatus(ctx context.Context) (*types.Status, error) { + if m.readStatusError != nil { + return nil, m.readStatusError } - return &types.Install{ - Config: types.InstallationConfig{}, - }, nil + return &types.Status{}, nil } -func (m *mockInstallController) SetConfig(ctx context.Context, config *types.InstallationConfig) error { - return m.setConfigError +func (m *mockInstallController) RunHostPreflights(ctx context.Context) error { + return m.runHostPreflightsError +} + +func (m *mockInstallController) GetHostPreflightStatus(ctx context.Context) (*types.Status, error) { + if m.getHostPreflightStatusError != nil { + return nil, m.getHostPreflightStatusError + } + return &types.Status{}, nil +} + +func (m *mockInstallController) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightOutput, error) { + if m.getHostPreflightOutputError != nil { + return nil, m.getHostPreflightOutputError + } + return &types.HostPreflightOutput{}, nil } -func (m *mockInstallController) SetStatus(ctx context.Context, status *types.InstallationStatus) error { +func (m *mockInstallController) GetHostPreflightTitles(ctx context.Context) ([]string, error) { + if m.getHostPreflightTitlesError != nil { + return nil, m.getHostPreflightTitlesError + } + return []string{}, nil +} + +func (m *mockInstallController) SetupNode(ctx context.Context) error { + return m.setupNodeError +} + +func (m *mockInstallController) SetStatus(ctx context.Context, status *types.Status) error { return m.setStatusError } -func (m *mockInstallController) ReadStatus(ctx context.Context) (*types.InstallationStatus, error) { +func (m *mockInstallController) GetStatus(ctx context.Context) (*types.Status, error) { return nil, m.readStatusError } -func TestSetInstallConfig(t *testing.T) { - manager := installation.NewInstallationManager() +func TestConfigureInstallation(t *testing.T) { + // Create a mock host utils + mockHostUtils := &hostutils.MockHostUtils{} + mockHostUtils.On("ConfigureForInstall", mock.Anything, mock.Anything).Return(nil).Once() // for the successful test + + // Create a runtime config + rc := runtimeconfig.New(nil) // Create an install controller with the config manager installController, err := install.NewInstallController( - install.WithInstallationManager(manager), + install.WithHostUtils(mockHostUtils), + install.WithRuntimeConfig(rc), ) require.NoError(t, err) @@ -65,7 +112,7 @@ func TestSetInstallConfig(t *testing.T) { "password", api.WithInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -123,7 +170,7 @@ func TestSetInstallConfig(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/config", bytes.NewReader(configJSON)) + req := httptest.NewRequest(http.MethodPost, "/install/installation/configure", bytes.NewReader(configJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+tc.token) rec := httptest.NewRecorder() @@ -144,35 +191,38 @@ func TestSetInstallConfig(t *testing.T) { assert.Equal(t, tc.expectedStatus, apiError.StatusCode) assert.NotEmpty(t, apiError.Message) } else { - var install types.Install - err = json.NewDecoder(rec.Body).Decode(&install) + var status types.Status + err = json.NewDecoder(rec.Body).Decode(&status) require.NoError(t, err) - // Verify that the config was properly set - assert.Equal(t, tc.config.DataDirectory, install.Config.DataDirectory) - assert.Equal(t, tc.config.AdminConsolePort, install.Config.AdminConsolePort) + // Verify that the status was properly set + assert.Equal(t, types.StateRunning, status.State) + assert.Equal(t, "Configuring installation", status.Description) } - // Also verify that the config is in the store if !tc.expectedError { - storedConfig, err := manager.ReadConfig() + // Verify that the config is in the store + storedConfig, err := installController.GetInstallationConfig(t.Context()) require.NoError(t, err) assert.Equal(t, tc.config.DataDirectory, storedConfig.DataDirectory) assert.Equal(t, tc.config.AdminConsolePort, storedConfig.AdminConsolePort) + + // Verify that the runtime config is updated + assert.Equal(t, tc.config.DataDirectory, rc.EmbeddedClusterHomeDirectory()) + assert.Equal(t, tc.config.AdminConsolePort, rc.AdminConsolePort()) + assert.Equal(t, tc.config.LocalArtifactMirrorPort, rc.LocalArtifactMirrorPort()) } }) } + + // Verify host confiuration was performed for successful tests + mockHostUtils.AssertExpectations(t) } // Test that config validation errors are properly returned -func TestSetInstallConfigValidation(t *testing.T) { - // Create a memory store - manager := installation.NewInstallationManager() - +func TestConfigureInstallationValidation(t *testing.T) { // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithInstallationManager(manager), - ) + installController, err := install.NewInstallController() require.NoError(t, err) // Create the API with the install controller @@ -180,7 +230,7 @@ func TestSetInstallConfigValidation(t *testing.T) { "password", api.WithInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -202,7 +252,7 @@ func TestSetInstallConfigValidation(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/config", bytes.NewReader(configJSON)) + req := httptest.NewRequest(http.MethodPost, "/install/installation/configure", bytes.NewReader(configJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -225,21 +275,16 @@ func TestSetInstallConfigValidation(t *testing.T) { } // Test that the endpoint properly handles malformed JSON -func TestSetInstallConfigBadRequest(t *testing.T) { - // Create a memory store and API - manager := installation.NewInstallationManager() - +func TestConfigureInstallationBadRequest(t *testing.T) { // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithInstallationManager(manager), - ) + installController, err := install.NewInstallController() require.NoError(t, err) apiInstance, err := api.New( "password", api.WithInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -247,7 +292,7 @@ func TestSetInstallConfigBadRequest(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request with invalid JSON - req := httptest.NewRequest(http.MethodPost, "/install/config", + req := httptest.NewRequest(http.MethodPost, "/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") @@ -263,10 +308,10 @@ func TestSetInstallConfigBadRequest(t *testing.T) { } // Test that the server returns proper errors when the API controller fails -func TestSetInstallConfigControllerError(t *testing.T) { +func TestConfigureInstallationControllerError(t *testing.T) { // Create a mock controller that returns an error mockController := &mockInstallController{ - setConfigError: assert.AnError, + configureInstallationError: assert.AnError, } // Create the API with the mock controller @@ -274,7 +319,7 @@ func TestSetInstallConfigControllerError(t *testing.T) { "password", api.WithInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -290,7 +335,7 @@ func TestSetInstallConfigControllerError(t *testing.T) { require.NoError(t, err) // Create a request - req := httptest.NewRequest(http.MethodPost, "/install/config", bytes.NewReader(configJSON)) + req := httptest.NewRequest(http.MethodPost, "/install/installation/configure", bytes.NewReader(configJSON)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -304,7 +349,8 @@ func TestSetInstallConfigControllerError(t *testing.T) { t.Logf("Response body: %s", rec.Body.String()) } -func TestGetInstall(t *testing.T) { +// Test the getInstall endpoint returns installation data correctly +func TestGetInstallationConfig(t *testing.T) { // Create a config manager installationManager := installation.NewInstallationManager() @@ -322,7 +368,7 @@ func TestGetInstall(t *testing.T) { GlobalCIDR: "10.0.0.0/16", NetworkInterface: "eth0", } - err = installationManager.WriteConfig(initialConfig) + err = installationManager.SetConfig(initialConfig) require.NoError(t, err) // Create the API with the install controller @@ -330,7 +376,7 @@ func TestGetInstall(t *testing.T) { "password", api.WithInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -341,7 +387,7 @@ func TestGetInstall(t *testing.T) { // Test successful get t.Run("Success", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install", nil) + req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -353,16 +399,16 @@ func TestGetInstall(t *testing.T) { assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) // Parse the response body - var install types.Install - err = json.NewDecoder(rec.Body).Decode(&install) + var config types.InstallationConfig + err = json.NewDecoder(rec.Body).Decode(&config) require.NoError(t, err) // Verify the installation data matches what we expect - assert.Equal(t, initialConfig.DataDirectory, install.Config.DataDirectory) - assert.Equal(t, initialConfig.AdminConsolePort, install.Config.AdminConsolePort) - assert.Equal(t, initialConfig.LocalArtifactMirrorPort, install.Config.LocalArtifactMirrorPort) - assert.Equal(t, initialConfig.GlobalCIDR, install.Config.GlobalCIDR) - assert.Equal(t, initialConfig.NetworkInterface, install.Config.NetworkInterface) + assert.Equal(t, initialConfig.DataDirectory, config.DataDirectory) + assert.Equal(t, initialConfig.AdminConsolePort, config.AdminConsolePort) + assert.Equal(t, initialConfig.LocalArtifactMirrorPort, config.LocalArtifactMirrorPort) + assert.Equal(t, initialConfig.GlobalCIDR, config.GlobalCIDR) + assert.Equal(t, initialConfig.NetworkInterface, config.NetworkInterface) }) // Test get with default/empty configuration @@ -383,7 +429,7 @@ func TestGetInstall(t *testing.T) { "password", api.WithInstallController(emptyInstallController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -392,7 +438,7 @@ func TestGetInstall(t *testing.T) { emptyAPI.RegisterRoutes(emptyRouter) // Create a request - req := httptest.NewRequest(http.MethodGet, "/install", nil) + req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -404,22 +450,22 @@ func TestGetInstall(t *testing.T) { assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) // Parse the response body - var install types.Install - err = json.NewDecoder(rec.Body).Decode(&install) + var config types.InstallationConfig + err = json.NewDecoder(rec.Body).Decode(&config) require.NoError(t, err) // Verify the installation data contains defaults or empty values - assert.Equal(t, "/var/lib/embedded-cluster", install.Config.DataDirectory) - assert.Equal(t, 30000, install.Config.AdminConsolePort) - assert.Equal(t, 50000, install.Config.LocalArtifactMirrorPort) - assert.Equal(t, "10.244.0.0/16", install.Config.GlobalCIDR) - assert.Equal(t, "eth0", install.Config.NetworkInterface) + assert.Equal(t, "/var/lib/embedded-cluster", config.DataDirectory) + assert.Equal(t, 30000, config.AdminConsolePort) + assert.Equal(t, 50000, config.LocalArtifactMirrorPort) + assert.Equal(t, "10.244.0.0/16", config.GlobalCIDR) + assert.Equal(t, "eth0", config.NetworkInterface) }) // Test authorization t.Run("Authorization error", func(t *testing.T) { // Create a request - req := httptest.NewRequest(http.MethodGet, "/install", nil) + req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"NOT_A_TOKEN") rec := httptest.NewRecorder() @@ -440,7 +486,7 @@ func TestGetInstall(t *testing.T) { t.Run("Controller error", func(t *testing.T) { // Create a mock controller that returns an error mockController := &mockInstallController{ - getError: assert.AnError, + getInstallationConfigError: assert.AnError, } // Create the API with the mock controller @@ -448,7 +494,7 @@ func TestGetInstall(t *testing.T) { "password", api.WithInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -456,7 +502,7 @@ func TestGetInstall(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a request - req := httptest.NewRequest(http.MethodGet, "/install", nil) + req := httptest.NewRequest(http.MethodGet, "/install/installation/config", nil) req.Header.Set("Authorization", "Bearer "+"TOKEN") rec := httptest.NewRecorder() @@ -475,23 +521,18 @@ func TestGetInstall(t *testing.T) { }) } -// Test the getInstallStatus endpoint returns installation status correctly +// Test the getInstallStatus endpoint returns install status correctly func TestGetInstallStatus(t *testing.T) { - // Create a config manager - installationManager := installation.NewInstallationManager() - // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithInstallationManager(installationManager), - ) + installController, err := install.NewInstallController() require.NoError(t, err) // Set some initial status - initialStatus := types.InstallationStatus{ - State: types.InstallationStatePending, + initialStatus := types.Status{ + State: types.StatePending, Description: "Installation in progress", } - err = installationManager.WriteStatus(initialStatus) + err = installController.SetStatus(t.Context(), &initialStatus) require.NoError(t, err) // Create the API with the install controller @@ -499,7 +540,7 @@ func TestGetInstallStatus(t *testing.T) { "password", api.WithInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -522,7 +563,7 @@ func TestGetInstallStatus(t *testing.T) { assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) // Parse the response body - var status types.InstallationStatus + var status types.Status err = json.NewDecoder(rec.Body).Decode(&status) require.NoError(t, err) @@ -563,7 +604,7 @@ func TestGetInstallStatus(t *testing.T) { "password", api.WithInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -590,15 +631,10 @@ func TestGetInstallStatus(t *testing.T) { }) } -// Test the setInstallStatus endpoint sets installation status correctly +// Test the setInstallStatus endpoint sets install status correctly func TestSetInstallStatus(t *testing.T) { - // Create a config manager - installationManager := installation.NewInstallationManager() - // Create an install controller with the config manager - installController, err := install.NewInstallController( - install.WithInstallationManager(installationManager), - ) + installController, err := install.NewInstallController() require.NoError(t, err) // Create the API with the install controller @@ -606,7 +642,7 @@ func TestSetInstallStatus(t *testing.T) { "password", api.WithInstallController(installController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -617,9 +653,9 @@ func TestSetInstallStatus(t *testing.T) { t.Run("Valid status is passed", func(t *testing.T) { now := time.Now() - status := types.InstallationStatus{ - State: types.InstallationStatePending, - Description: "Installation in progress", + status := types.Status{ + State: types.StatePending, + Description: "Install is pending", LastUpdated: now, } @@ -642,17 +678,17 @@ func TestSetInstallStatus(t *testing.T) { t.Logf("Response body: %s", rec.Body.String()) // Parse the response body - var install types.Install - err = json.NewDecoder(rec.Body).Decode(&install) + var respStatus types.Status + err = json.NewDecoder(rec.Body).Decode(&respStatus) require.NoError(t, err) // Verify that the status was properly set - assert.Equal(t, status.State, install.Status.State) - assert.Equal(t, status.Description, install.Status.Description) - assert.Equal(t, now.Format(time.RFC3339), install.Status.LastUpdated.Format(time.RFC3339)) + assert.Equal(t, status.State, respStatus.State) + assert.Equal(t, status.Description, respStatus.Description) + assert.Equal(t, now.Format(time.RFC3339), respStatus.LastUpdated.Format(time.RFC3339)) // Also verify that the status is in the store - storedStatus, err := installationManager.ReadStatus() + storedStatus, err := installController.GetStatus(t.Context()) require.NoError(t, err) assert.Equal(t, status.State, storedStatus.State) assert.Equal(t, status.Description, storedStatus.Description) @@ -711,7 +747,7 @@ func TestSetInstallStatus(t *testing.T) { "password", api.WithInstallController(mockController), api.WithAuthController(&staticAuthController{"TOKEN"}), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -719,8 +755,8 @@ func TestSetInstallStatus(t *testing.T) { apiInstance.RegisterRoutes(router) // Create a valid status - status := types.InstallationStatus{ - State: types.InstallationStatePending, + status := types.Status{ + State: types.StatePending, Description: "Installation in progress", } statusJSON, err := json.Marshal(status) @@ -763,7 +799,15 @@ func TestInstallWithAPIClient(t *testing.T) { GlobalCIDR: "192.168.0.0/16", NetworkInterface: "eth1", } - err = installationManager.WriteConfig(initialConfig) + err = installationManager.SetConfig(initialConfig) + require.NoError(t, err) + + // Set some initial status + initialStatus := types.Status{ + State: types.StatePending, + Description: "Installation pending", + } + err = installationManager.SetStatus(initialStatus) require.NoError(t, err) // Create the API with controllers @@ -771,7 +815,7 @@ func TestInstallWithAPIClient(t *testing.T) { password, api.WithAuthController(&staticAuthController{"TOKEN"}), api.WithInstallController(installController), - api.WithLogger(api.NewDiscardLogger()), + api.WithLogger(logger.NewDiscardLogger()), ) require.NoError(t, err) @@ -787,22 +831,31 @@ func TestInstallWithAPIClient(t *testing.T) { c := client.New(server.URL, client.WithToken("TOKEN")) require.NoError(t, err, "API client login should succeed") - // Test GetInstall - t.Run("GetInstall", func(t *testing.T) { - install, err := c.GetInstall() - require.NoError(t, err, "GetInstall should succeed") - assert.NotNil(t, install, "Install should not be nil") + // 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", install.Config.DataDirectory) - assert.Equal(t, 9080, install.Config.AdminConsolePort) - assert.Equal(t, 9081, install.Config.LocalArtifactMirrorPort) - assert.Equal(t, "192.168.0.0/16", install.Config.GlobalCIDR) - assert.Equal(t, "eth1", install.Config.NetworkInterface) + assert.Equal(t, "/tmp/test-data-for-client", config.DataDirectory) + assert.Equal(t, 9080, config.AdminConsolePort) + assert.Equal(t, 9081, config.LocalArtifactMirrorPort) + assert.Equal(t, "192.168.0.0/16", config.GlobalCIDR) + assert.Equal(t, "eth1", config.NetworkInterface) }) - // Test SetInstallConfig - t.Run("SetInstallConfig", func(t *testing.T) { + // Test GetInstallationStatus + 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) + }) + + // Test ConfigureInstallation + t.Run("ConfigureInstallation", func(t *testing.T) { // Create a valid config config := types.InstallationConfig{ DataDirectory: "/tmp/new-dir", @@ -812,28 +865,27 @@ func TestInstallWithAPIClient(t *testing.T) { NetworkInterface: "eth0", } - // Set the config using the client - install, err := c.SetInstallConfig(config) - require.NoError(t, err, "SetInstallConfig should succeed with valid config") - assert.NotNil(t, install, "Install should not be nil") + // Configure the installation using the client + status, err := c.ConfigureInstallation(&config) + require.NoError(t, err, "ConfigureInstallation should succeed with valid config") + assert.NotNil(t, status, "Status should not be nil") - // Verify the config was set correctly - assert.Equal(t, config.DataDirectory, install.Config.DataDirectory) - assert.Equal(t, config.AdminConsolePort, install.Config.AdminConsolePort) - assert.Equal(t, config.NetworkInterface, install.Config.NetworkInterface) + // Verify the status was set correctly + assert.Equal(t, types.StateRunning, status.State) + assert.Equal(t, "Configuring installation", status.Description) // Get the config to verify it persisted - install, err = c.GetInstall() - require.NoError(t, err, "GetInstall should succeed after setting config") - assert.Equal(t, config.DataDirectory, install.Config.DataDirectory) - assert.Equal(t, config.AdminConsolePort, install.Config.AdminConsolePort) - assert.Equal(t, config.NetworkInterface, install.Config.NetworkInterface) + newConfig, err := c.GetInstallationConfig() + require.NoError(t, err, "GetInstallationConfig should succeed after setting config") + assert.Equal(t, config.DataDirectory, newConfig.DataDirectory) + assert.Equal(t, config.AdminConsolePort, newConfig.AdminConsolePort) + assert.Equal(t, config.NetworkInterface, newConfig.NetworkInterface) }) - // Test SetInstallConfig validation error - t.Run("SetInstallConfig validation error", func(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 @@ -841,9 +893,9 @@ func TestInstallWithAPIClient(t *testing.T) { NetworkInterface: "eth0", } - // Set the config using the client - _, err := c.SetInstallConfig(config) - require.Error(t, err, "SetInstallConfig should fail with invalid config") + // Configure the installation using the client + _, err := c.ConfigureInstallation(config) + require.Error(t, err, "ConfigureInstallation should fail with invalid config") // Verify the error is of type APIError apiErr, ok := err.(*types.APIError) @@ -860,15 +912,15 @@ func TestInstallWithAPIClient(t *testing.T) { // Test SetInstallStatus t.Run("SetInstallStatus", func(t *testing.T) { // Create a status - status := types.InstallationStatus{ - State: types.InstallationStateFailed, + status := &types.Status{ + State: types.StateFailed, Description: "Installation failed", } // Set the status using the client - install, err := c.SetInstallStatus(status) + newStatus, err := c.SetInstallStatus(status) require.NoError(t, err, "SetInstallStatus should succeed") - assert.NotNil(t, install, "Install should not be nil") - assert.NotNil(t, install.Status, status, "Install status should match the one set") + assert.NotNil(t, newStatus, "Install should not be nil") + assert.Equal(t, status, newStatus, "Install status should match the one set") }) } diff --git a/api/pkg/installation/config.go b/api/internal/managers/installation/config.go similarity index 64% rename from api/pkg/installation/config.go rename to api/internal/managers/installation/config.go index 240788e89..0d08dec2c 100644 --- a/api/pkg/installation/config.go +++ b/api/internal/managers/installation/config.go @@ -1,82 +1,23 @@ package installation import ( + "context" "errors" "fmt" - "github.com/replicatedhq/embedded-cluster/api/pkg/utils" "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-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/netutils" ) -var _ InstallationManager = (*installationManager)(nil) - -// InstallationManager provides methods for validating and setting defaults for installation configuration -type InstallationManager interface { - ReadConfig() (*types.InstallationConfig, error) - WriteConfig(config types.InstallationConfig) error - ReadStatus() (*types.InstallationStatus, error) - WriteStatus(status types.InstallationStatus) error - ValidateConfig(config *types.InstallationConfig) error - ValidateStatus(status *types.InstallationStatus) error - SetDefaults(config *types.InstallationConfig) error +func (m *installationManager) GetConfig() (*types.InstallationConfig, error) { + return m.installationStore.GetConfig() } -// installationManager is an implementation of the InstallationManager interface -type installationManager struct { - installationStore InstallationStore - netUtils utils.NetUtils -} - -type InstallationManagerOption func(*installationManager) - -func WithInstallationStore(installationStore InstallationStore) InstallationManagerOption { - return func(c *installationManager) { - c.installationStore = installationStore - } -} - -func WithNetUtils(netUtils utils.NetUtils) InstallationManagerOption { - return func(c *installationManager) { - c.netUtils = netUtils - } -} - -// NewInstallationManager creates a new InstallationManager with the provided network utilities -func NewInstallationManager(opts ...InstallationManagerOption) *installationManager { - manager := &installationManager{} - - for _, opt := range opts { - opt(manager) - } - - if manager.installationStore == nil { - manager.installationStore = NewMemoryStore() - } - - if manager.netUtils == nil { - manager.netUtils = utils.NewNetUtils() - } - - return manager -} - -func (m *installationManager) ReadConfig() (*types.InstallationConfig, error) { - return m.installationStore.ReadConfig() -} - -func (m *installationManager) WriteConfig(config types.InstallationConfig) error { - return m.installationStore.WriteConfig(config) -} - -func (m *installationManager) ReadStatus() (*types.InstallationStatus, error) { - return m.installationStore.ReadStatus() -} - -func (m *installationManager) WriteStatus(status types.InstallationStatus) error { - return m.installationStore.WriteStatus(status) +func (m *installationManager) SetConfig(config types.InstallationConfig) error { + return m.installationStore.SetConfig(config) } func (m *installationManager) ValidateConfig(config *types.InstallationConfig) error { @@ -113,27 +54,6 @@ func (m *installationManager) ValidateConfig(config *types.InstallationConfig) e return ve.ErrorOrNil() } -func (m *installationManager) ValidateStatus(status *types.InstallationStatus) error { - var ve *types.APIError - - if status == nil { - return types.NewBadRequestError(errors.New("a status is required")) - } - - switch status.State { - case types.InstallationStatePending, types.InstallationStateRunning, types.InstallationStateSucceeded, types.InstallationStateFailed: - // valid states - default: - ve = types.AppendFieldError(ve, "state", fmt.Errorf("invalid state: %s", status.State)) - } - - if status.Description == "" { - ve = types.AppendFieldError(ve, "description", errors.New("description is required")) - } - - return ve.ErrorOrNil() -} - func (m *installationManager) validateGlobalCIDR(config *types.InstallationConfig) error { if config.GlobalCIDR != "" { if err := netutils.ValidateCIDR(config.GlobalCIDR, 16, true); err != nil { @@ -218,8 +138,8 @@ func (m *installationManager) validateDataDirectory(config *types.InstallationCo return nil } -// SetDefaults sets default values for the installation configuration -func (m *installationManager) SetDefaults(config *types.InstallationConfig) error { +// SetConfigDefaults sets default values for the installation configuration +func (m *installationManager) SetConfigDefaults(config *types.InstallationConfig) error { if config.AdminConsolePort == 0 { config.AdminConsolePort = ecv1beta1.DefaultAdminConsolePort } @@ -272,3 +192,51 @@ func (m *installationManager) setCIDRDefaults(config *types.InstallationConfig) } return nil } + +func (m *installationManager) ConfigureForInstall(ctx context.Context, config *types.InstallationConfig) 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") + } + + if err := m.setRunningStatus("Configuring installation"); err != nil { + return fmt.Errorf("set running status: %w", err) + } + + go m.configureForInstall(context.Background(), config) + + return nil +} + +func (m *installationManager) configureForInstall(ctx context.Context, config *types.InstallationConfig) { + defer func() { + if r := recover(); r != nil { + if err := m.setFailedStatus(fmt.Sprintf("panic: %v", r)); err != nil { + m.logger.WithField("error", err).Error("set failed status") + } + } + }() + + opts := hostutils.InitForInstallOptions{ + LicenseFile: m.licenseFile, + AirgapBundle: m.airgapBundle, + PodCIDR: config.PodCIDR, + ServiceCIDR: config.ServiceCIDR, + } + if err := m.hostUtils.ConfigureForInstall(ctx, m.rc, opts); err != nil { + if err := m.setFailedStatus(fmt.Sprintf("configure installation: %v", err)); err != nil { + m.logger.WithField("error", err).Error("set failed status") + } + return + } + + if err := m.setCompletedStatus(types.StateSucceeded, "Installation configured"); err != nil { + m.logger.WithField("error", err).Error("set succeeded status") + } +} diff --git a/api/pkg/installation/config_test.go b/api/internal/managers/installation/config_test.go similarity index 64% rename from api/pkg/installation/config_test.go rename to api/internal/managers/installation/config_test.go index 691818a73..43649fe71 100644 --- a/api/pkg/installation/config_test.go +++ b/api/internal/managers/installation/config_test.go @@ -1,6 +1,7 @@ package installation import ( + "context" "errors" "testing" "time" @@ -8,25 +9,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "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" ) -// MockNetUtils is a mock implementation of utils.NetUtils -type MockNetUtils struct { - mock.Mock -} - -func (m *MockNetUtils) DetermineBestNetworkInterface() (string, error) { - args := m.Called() - return args.String(0), args.Error(1) -} - -func (m *MockNetUtils) ListValidNetworkInterfaces() ([]string, error) { - args := m.Called() - return []string{args.String(0)}, args.Error(1) -} - func TestValidateConfig(t *testing.T) { // Create test cases for validation tests := []struct { @@ -167,90 +155,9 @@ func TestValidateConfig(t *testing.T) { } } -func TestValidateStatus(t *testing.T) { - tests := []struct { - name string - status *types.InstallationStatus - expectedErr bool - }{ - { - name: "valid status - pending", - status: &types.InstallationStatus{ - State: types.InstallationStatePending, - Description: "Installation pending", - LastUpdated: time.Now(), - }, - expectedErr: false, - }, - { - name: "valid status - running", - status: &types.InstallationStatus{ - State: types.InstallationStateRunning, - Description: "Installation in progress", - LastUpdated: time.Now(), - }, - expectedErr: false, - }, - { - name: "valid status - succeeded", - status: &types.InstallationStatus{ - State: types.InstallationStateSucceeded, - Description: "Installation completed successfully", - LastUpdated: time.Now(), - }, - expectedErr: false, - }, - { - name: "valid status - failed", - status: &types.InstallationStatus{ - State: types.InstallationStateFailed, - Description: "Installation failed", - LastUpdated: time.Now(), - }, - expectedErr: false, - }, - { - name: "nil status", - status: nil, - expectedErr: true, - }, - { - name: "invalid state", - status: &types.InstallationStatus{ - State: "Invalid", - Description: "Invalid state", - LastUpdated: time.Now(), - }, - expectedErr: true, - }, - { - name: "missing description", - status: &types.InstallationStatus{ - State: types.InstallationStateRunning, - Description: "", - LastUpdated: time.Now(), - }, - expectedErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - manager := NewInstallationManager() - err := manager.ValidateStatus(tt.status) - - if tt.expectedErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestSetDefaults(t *testing.T) { +func TestSetConfigDefaults(t *testing.T) { // Create a mock for network utilities - mockNetUtils := &MockNetUtils{} + mockNetUtils := &utils.MockNetUtils{} mockNetUtils.On("DetermineBestNetworkInterface").Return("eth0", nil) tests := []struct { @@ -333,7 +240,7 @@ func TestSetDefaults(t *testing.T) { t.Run(tt.name, func(t *testing.T) { manager := NewInstallationManager(WithNetUtils(mockNetUtils)) - err := manager.SetDefaults(tt.inputConfig) + err := manager.SetConfigDefaults(tt.inputConfig) assert.NoError(t, err) assert.NotNil(t, tt.inputConfig) @@ -343,13 +250,13 @@ func TestSetDefaults(t *testing.T) { // Test when network interface detection fails t.Run("network interface detection fails", func(t *testing.T) { - failingMockNetUtils := &MockNetUtils{} + failingMockNetUtils := &utils.MockNetUtils{} failingMockNetUtils.On("DetermineBestNetworkInterface").Return("", errors.New("failed to detect network interface")) manager := NewInstallationManager(WithNetUtils(failingMockNetUtils)) config := &types.InstallationConfig{} - err := manager.SetDefaults(config) + err := manager.SetConfigDefaults(config) assert.NoError(t, err) // Network interface should remain empty when detection fails @@ -357,60 +264,139 @@ func TestSetDefaults(t *testing.T) { }) } -func TestReadWriteOperations(t *testing.T) { +func TestConfigSetAndGet(t *testing.T) { manager := NewInstallationManager() - // Test Config operations - t.Run("config operations", func(t *testing.T) { - // Test writing a config - configToWrite := types.InstallationConfig{ - AdminConsolePort: 8800, - DataDirectory: "/var/lib/embedded-cluster", - LocalArtifactMirrorPort: 8888, - NetworkInterface: "eth0", - GlobalCIDR: "10.0.0.0/16", - } + // Test writing a config + configToWrite := types.InstallationConfig{ + AdminConsolePort: 8800, + DataDirectory: "/var/lib/embedded-cluster", + LocalArtifactMirrorPort: 8888, + NetworkInterface: "eth0", + GlobalCIDR: "10.0.0.0/16", + } - err := manager.WriteConfig(configToWrite) - assert.NoError(t, err) + err := manager.SetConfig(configToWrite) + assert.NoError(t, err) - // Test reading it back - readConfig, err := manager.ReadConfig() - assert.NoError(t, err) - assert.NotNil(t, readConfig) + // 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) - assert.Equal(t, configToWrite.DataDirectory, readConfig.DataDirectory) - assert.Equal(t, configToWrite.LocalArtifactMirrorPort, readConfig.LocalArtifactMirrorPort) - assert.Equal(t, configToWrite.NetworkInterface, readConfig.NetworkInterface) - assert.Equal(t, configToWrite.GlobalCIDR, readConfig.GlobalCIDR) - }) + // Verify the values match + assert.Equal(t, configToWrite.AdminConsolePort, readConfig.AdminConsolePort) + assert.Equal(t, configToWrite.DataDirectory, readConfig.DataDirectory) + assert.Equal(t, configToWrite.LocalArtifactMirrorPort, readConfig.LocalArtifactMirrorPort) + assert.Equal(t, configToWrite.NetworkInterface, readConfig.NetworkInterface) + assert.Equal(t, configToWrite.GlobalCIDR, readConfig.GlobalCIDR) +} - // Test Status operations - t.Run("status operations", func(t *testing.T) { - // Test writing a status - statusToWrite := types.InstallationStatus{ - State: types.InstallationStateRunning, - Description: "Installation in progress", - LastUpdated: time.Now().UTC().Truncate(time.Second), // Truncate to avoid time precision issues - } +func TestConfigureForInstall(t *testing.T) { + tests := []struct { + name string + config *types.InstallationConfig + setupMocks func(*hostutils.MockHostUtils, *MockInstallationStore) + expectedErr bool + }{ + { + name: "successful configuration", + config: &types.InstallationConfig{ + DataDirectory: "/var/lib/embedded-cluster", + PodCIDR: "10.0.0.0/16", + ServiceCIDR: "10.1.0.0/16", + }, + setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { + 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("ConfigureForInstall", mock.Anything, hostutils.InitForInstallOptions{ + LicenseFile: "license.yaml", + AirgapBundle: "bundle.tar", + PodCIDR: "10.0.0.0/16", + ServiceCIDR: "10.1.0.0/16", + }).Return(nil), + im.On("SetStatus", mock.MatchedBy(func(status types.Status) bool { return status.State == types.StateSucceeded })).Return(nil), + ) + }, + expectedErr: false, + }, + { + name: "already running", + config: &types.InstallationConfig{ + DataDirectory: "/var/lib/embedded-cluster", + }, + setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { + im.On("GetStatus").Return(&types.Status{State: types.StateRunning}, nil) + }, + expectedErr: true, + }, + { + name: "configure installation fails", + config: &types.InstallationConfig{ + DataDirectory: "/var/lib/embedded-cluster", + }, + setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { + 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("ConfigureForInstall", mock.Anything, hostutils.InitForInstallOptions{ + LicenseFile: "license.yaml", + 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, + }, + { + name: "set running status fails", + config: &types.InstallationConfig{ + DataDirectory: "/var/lib/embedded-cluster", + }, + setupMocks: func(hum *hostutils.MockHostUtils, im *MockInstallationStore) { + mock.InOrder( + im.On("GetStatus").Return(&types.Status{State: types.StatePending}, nil), + im.On("SetStatus", mock.Anything).Return(errors.New("failed to set status")), + ) + }, + expectedErr: true, + }, + } - err := manager.WriteStatus(statusToWrite) - assert.NoError(t, err) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mocks + mockHostUtils := &hostutils.MockHostUtils{} + mockStore := &MockInstallationStore{} - // Test reading it back - readStatus, err := manager.ReadStatus() - assert.NoError(t, err) - assert.NotNil(t, readStatus) + // Setup mocks + tt.setupMocks(mockHostUtils, mockStore) - // Verify the values match - assert.Equal(t, statusToWrite.State, readStatus.State) - assert.Equal(t, statusToWrite.Description, readStatus.Description) + // Create manager with mocks + manager := NewInstallationManager( + WithHostUtils(mockHostUtils), + WithInstallationStore(mockStore), + WithLicenseFile("license.yaml"), + WithAirgapBundle("bundle.tar"), + ) - // 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) - }) + // Run the test + err := manager.ConfigureForInstall(context.Background(), tt.config) + + // 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 + mockStore.AssertExpectations(t) + mockHostUtils.AssertExpectations(t) + }) + } } diff --git a/api/internal/managers/installation/manager.go b/api/internal/managers/installation/manager.go new file mode 100644 index 000000000..303eace3b --- /dev/null +++ b/api/internal/managers/installation/manager.go @@ -0,0 +1,124 @@ +package installation + +import ( + "context" + "sync" + + "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" + "github.com/sirupsen/logrus" +) + +var _ InstallationManager = &installationManager{} + +// InstallationManager provides methods for validating and setting defaults for installation configuration +type InstallationManager interface { + GetConfig() (*types.InstallationConfig, error) + SetConfig(config types.InstallationConfig) error + GetStatus() (*types.Status, error) + SetStatus(status types.Status) error + ValidateConfig(config *types.InstallationConfig) error + SetConfigDefaults(config *types.InstallationConfig) error + ConfigureForInstall(ctx context.Context, config *types.InstallationConfig) error +} + +// installationManager is an implementation of the InstallationManager interface +type installationManager struct { + installation *types.Installation + installationStore InstallationStore + rc runtimeconfig.RuntimeConfig + licenseFile string + airgapBundle string + netUtils utils.NetUtils + 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 { + return func(c *installationManager) { + c.installationStore = installationStore + } +} + +func WithLicenseFile(licenseFile string) InstallationManagerOption { + return func(c *installationManager) { + c.licenseFile = licenseFile + } +} + +func WithAirgapBundle(airgapBundle string) InstallationManagerOption { + return func(c *installationManager) { + c.airgapBundle = airgapBundle + } +} + +func WithNetUtils(netUtils utils.NetUtils) InstallationManagerOption { + return func(c *installationManager) { + c.netUtils = netUtils + } +} + +func WithHostUtils(hostUtils hostutils.HostUtilsInterface) InstallationManagerOption { + return func(c *installationManager) { + c.hostUtils = hostUtils + } +} + +// NewInstallationManager creates a new InstallationManager with the provided options +func NewInstallationManager(opts ...InstallationManagerOption) *installationManager { + manager := &installationManager{} + + for _, opt := range opts { + 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) + } + + if manager.netUtils == nil { + manager.netUtils = utils.NewNetUtils() + } + + if manager.hostUtils == nil { + manager.hostUtils = hostutils.New() + } + + return manager +} diff --git a/api/internal/managers/installation/manager_mock.go b/api/internal/managers/installation/manager_mock.go new file mode 100644 index 000000000..2cd0a5a3d --- /dev/null +++ b/api/internal/managers/installation/manager_mock.go @@ -0,0 +1,63 @@ +package installation + +import ( + "context" + + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/stretchr/testify/mock" +) + +var _ InstallationManager = (*MockInstallationManager)(nil) + +// MockInstallationManager is a mock implementation of the InstallationManager interface +type MockInstallationManager struct { + mock.Mock +} + +// GetConfig mocks the GetConfig method +func (m *MockInstallationManager) 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 *MockInstallationManager) SetConfig(config types.InstallationConfig) error { + args := m.Called(config) + return args.Error(0) +} + +// GetStatus mocks the GetStatus method +func (m *MockInstallationManager) 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 *MockInstallationManager) SetStatus(status types.Status) error { + args := m.Called(status) + return args.Error(0) +} + +// ValidateConfig mocks the ValidateConfig method +func (m *MockInstallationManager) ValidateConfig(config *types.InstallationConfig) error { + args := m.Called(config) + return args.Error(0) +} + +// SetConfigDefaults mocks the SetConfigDefaults method +func (m *MockInstallationManager) SetConfigDefaults(config *types.InstallationConfig) error { + args := m.Called(config) + return args.Error(0) +} + +// ConfigureForInstall mocks the ConfigureForInstall method +func (m *MockInstallationManager) ConfigureForInstall(ctx context.Context, config *types.InstallationConfig) error { + args := m.Called(ctx, config) + return args.Error(0) +} diff --git a/api/internal/managers/installation/status.go b/api/internal/managers/installation/status.go new file mode 100644 index 000000000..9557a29a3 --- /dev/null +++ b/api/internal/managers/installation/status.go @@ -0,0 +1,49 @@ +package installation + +import ( + "time" + + "github.com/replicatedhq/embedded-cluster/api/types" +) + +func (m *installationManager) GetStatus() (*types.Status, error) { + return m.installationStore.GetStatus() +} + +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, + Description: description, + LastUpdated: time.Now(), + }) +} + +func (m *installationManager) setFailedStatus(description string) error { + m.logger.Error(description) + + return m.SetStatus(types.Status{ + State: types.StateFailed, + Description: description, + LastUpdated: time.Now(), + }) +} + +func (m *installationManager) setCompletedStatus(state types.State, description string) error { + return m.SetStatus(types.Status{ + State: state, + Description: description, + LastUpdated: time.Now(), + }) +} diff --git a/api/internal/managers/installation/status_test.go b/api/internal/managers/installation/status_test.go new file mode 100644 index 000000000..aee4f8a74 --- /dev/null +++ b/api/internal/managers/installation/status_test.go @@ -0,0 +1,106 @@ +package installation + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/replicatedhq/embedded-cluster/api/types" +) + +func TestStatusSetAndGet(t *testing.T) { + manager := NewInstallationManager() + + // 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) + + // 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) + 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 TestSetRunningStatus(t *testing.T) { + manager := NewInstallationManager() + description := "Installation is running" + + err := manager.setRunningStatus(description) + assert.NoError(t, err) + + 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) + assert.NotZero(t, status.LastUpdated) +} + +func TestSetFailedStatus(t *testing.T) { + manager := NewInstallationManager() + description := "Installation failed" + + err := manager.setFailedStatus(description) + assert.NoError(t, err) + + 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) + assert.NotZero(t, status.LastUpdated) +} + +func TestSetCompletedStatus(t *testing.T) { + tests := []struct { + name string + state types.State + description string + }{ + { + name: "completed with success state", + state: types.StateSucceeded, + description: "completed successfully", + }, + { + name: "completed with failed state", + state: types.StateFailed, + description: "completed with errors", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := NewInstallationManager() + + err := manager.setCompletedStatus(tt.state, tt.description) + assert.NoError(t, err) + + 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) + assert.NotZero(t, status.LastUpdated) + }) + } +} diff --git a/api/internal/managers/installation/store.go b/api/internal/managers/installation/store.go new file mode 100644 index 000000000..d100ca02f --- /dev/null +++ b/api/internal/managers/installation/store.go @@ -0,0 +1,57 @@ +package installation + +import ( + "sync" + + "github.com/replicatedhq/embedded-cluster/api/types" +) + +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 new file mode 100644 index 000000000..871e15192 --- /dev/null +++ b/api/internal/managers/installation/store_mock.go @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000..39b3f70d8 --- /dev/null +++ b/api/internal/managers/preflight/hostpreflight.go @@ -0,0 +1,241 @@ +package preflight + +import ( + "context" + "fmt" + "time" + + "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/netutils" + troubleshootanalyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) + +type PrepareHostPreflightOptions struct { + InstallationConfig *types.InstallationConfig + ReplicatedAppURL string + ProxyRegistryURL string + HostPreflightSpec *troubleshootv1beta2.HostPreflightSpec + EmbeddedClusterConfig *ecv1beta1.Config + TCPConnectionsRequired []string + IsAirgap bool + IsJoin bool +} + +type RunHostPreflightOptions struct { + HostPreflightSpec *troubleshootv1beta2.HostPreflightSpec + Proxy *ecv1beta1.ProxySpec +} + +func (m *hostPreflightManager) PrepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, *ecv1beta1.ProxySpec, error) { + hpf, proxy, err := m.prepareHostPreflights(ctx, opts) + if err != nil { + return nil, nil, err + } + return hpf, proxy, nil +} + +func (m *hostPreflightManager) RunHostPreflights(ctx context.Context, opts RunHostPreflightOptions) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.hostPreflightStore.IsRunning() { + return 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.HostPreflightOutput, 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, *ecv1beta1.ProxySpec, error) { + // Use provided installation config + config := opts.InstallationConfig + if config == nil { + return nil, nil, fmt.Errorf("installation config is required") + } + + // Get node IP + nodeIP, err := netutils.FirstValidAddress(config.NetworkInterface) + if err != nil { + return nil, nil, fmt.Errorf("determine node ip: %w", err) + } + + // Build proxy spec + var proxy *ecv1beta1.ProxySpec + if config.HTTPProxy != "" || config.HTTPSProxy != "" || config.NoProxy != "" { + proxy = &ecv1beta1.ProxySpec{ + HTTPProxy: config.HTTPProxy, + HTTPSProxy: config.HTTPSProxy, + NoProxy: config.NoProxy, + } + } + + var globalCIDR *string + if config.GlobalCIDR != "" { + globalCIDR = &config.GlobalCIDR + } + + // Use the shared Prepare function to prepare host preflights + hpf, err := preflights.Prepare(ctx, preflights.PrepareOptions{ + HostPreflightSpec: opts.HostPreflightSpec, + ReplicatedAppURL: opts.ReplicatedAppURL, + ProxyRegistryURL: opts.ProxyRegistryURL, + AdminConsolePort: opts.InstallationConfig.AdminConsolePort, + LocalArtifactMirrorPort: opts.InstallationConfig.LocalArtifactMirrorPort, + DataDir: opts.InstallationConfig.DataDirectory, + K0sDataDir: m.rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: m.rc.EmbeddedClusterOpenEBSLocalSubDir(), + Proxy: proxy, + PodCIDR: config.PodCIDR, + ServiceCIDR: config.ServiceCIDR, + GlobalCIDR: globalCIDR, + NodeIP: nodeIP, + IsAirgap: opts.IsAirgap, + TCPConnectionsRequired: opts.TCPConnectionsRequired, + IsJoin: opts.IsJoin, + }) + if err != nil { + return nil, nil, fmt.Errorf("prepare host preflights: %w", err) + } + + return hpf, proxy, nil +} + +func (m *hostPreflightManager) runHostPreflights(ctx context.Context, opts RunHostPreflightOptions) { + defer func() { + if r := recover(); r != nil { + if err := m.setFailedStatus(fmt.Sprintf("panic: %v", r)); err != nil { + m.logger.WithField("error", err).Error("set failed status") + } + } + }() + + // Run the preflights using the shared core function + output, stderr, err := preflights.Run(ctx, opts.HostPreflightSpec, opts.Proxy, m.rc) + if err != nil { + errMsg := fmt.Sprintf("Host preflights failed to run: %v", err) + if stderr != "" { + errMsg += fmt.Sprintf(" (stderr: %s)", stderr) + } + if err := m.setFailedStatus(errMsg); err != nil { + m.logger.WithField("error", err).Error("set failed status") + } + return + } + + if err := preflights.SaveToDisk(output, m.rc.PathToEmbeddedClusterSupportFile("host-preflight-results.json")); err != nil { + m.logger.WithField("error", err).Warn("save preflights output") + } + + if err := preflights.CopyBundleTo(m.rc.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz")); err != nil { + m.logger.WithField("error", err).Warn("copy preflight bundle to embedded-cluster support dir") + } + + // TODO (@salah): report bypassing preflights on a separate api endpoint if the user chooses to bypass and continue + if output.HasFail() || output.HasWarn() { + if m.metricsReporter != nil { + m.metricsReporter.ReportPreflightsFailed(ctx, output) + } + } + + // Set final status based on results + if output.HasFail() { + if err := m.setCompletedStatus(types.StateFailed, "Host preflights failed", output); err != nil { + m.logger.WithField("error", err).Error("set failed status") + } + } else { + if err := m.setCompletedStatus(types.StateSucceeded, "Host preflights passed", output); err != nil { + m.logger.WithField("error", err).Error("set succeeded status") + } + } +} + +func (m *hostPreflightManager) setRunningStatus(hpf *troubleshootv1beta2.HostPreflightSpec) error { + titles, err := m.getTitles(hpf) + if err != nil { + return fmt.Errorf("get titles: %w", err) + } + + if err := m.hostPreflightStore.SetTitles(titles); err != nil { + return fmt.Errorf("set titles: %w", err) + } + + if err := m.hostPreflightStore.SetOutput(nil); err != nil { + return fmt.Errorf("reset output: %w", err) + } + + if err := m.hostPreflightStore.SetStatus(&types.Status{ + State: types.StateRunning, + Description: "Running host preflights", + LastUpdated: time.Now(), + }); err != nil { + return fmt.Errorf("set status: %w", err) + } + + return nil +} + +func (m *hostPreflightManager) setFailedStatus(description string) error { + m.logger.Error(description) + + return m.hostPreflightStore.SetStatus(&types.Status{ + State: types.StateFailed, + Description: description, + LastUpdated: time.Now(), + }) +} + +func (m *hostPreflightManager) setCompletedStatus(state types.State, description string, output *types.HostPreflightOutput) error { + if err := m.hostPreflightStore.SetOutput(output); err != nil { + return fmt.Errorf("set output: %w", err) + } + + return m.hostPreflightStore.SetStatus(&types.Status{ + State: state, + Description: description, + LastUpdated: time.Now(), + }) +} + +func (m *hostPreflightManager) getTitles(hpf *troubleshootv1beta2.HostPreflightSpec) ([]string, error) { + if hpf == nil || hpf.Analyzers == nil { + return nil, nil + } + + titles := []string{} + for _, a := range hpf.Analyzers { + analyzer, ok := troubleshootanalyze.GetHostAnalyzer(a) + if !ok { + continue + } + excluded, err := analyzer.IsExcluded() + if err != nil { + return nil, fmt.Errorf("check if analyzer is excluded: %w", err) + } + if !excluded { + titles = append(titles, analyzer.Title()) + } + } + + return titles, nil +} diff --git a/api/internal/managers/preflight/manager.go b/api/internal/managers/preflight/manager.go new file mode 100644 index 000000000..45f941c73 --- /dev/null +++ b/api/internal/managers/preflight/manager.go @@ -0,0 +1,91 @@ +package preflight + +import ( + "context" + "sync" + + "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/metrics" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/sirupsen/logrus" +) + +// HostPreflightManager provides methods for running host preflights +type HostPreflightManager interface { + PrepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, *ecv1beta1.ProxySpec, error) + RunHostPreflights(ctx context.Context, opts RunHostPreflightOptions) error + GetHostPreflightStatus(ctx context.Context) (*types.Status, error) + GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightOutput, error) + GetHostPreflightTitles(ctx context.Context) ([]string, error) +} + +type hostPreflightManager struct { + hostPreflight *types.HostPreflight + hostPreflightStore HostPreflightStore + 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 WithHostPreflight(hostPreflight *types.HostPreflight) HostPreflightManagerOption { + return func(m *hostPreflightManager) { + m.hostPreflight = hostPreflight + } +} + +func WithHostPreflightStore(hostPreflightStore HostPreflightStore) HostPreflightManagerOption { + return func(m *hostPreflightManager) { + m.hostPreflightStore = hostPreflightStore + } +} + +// NewHostPreflightManager creates a new HostPreflightManager +func NewHostPreflightManager(opts ...HostPreflightManagerOption) HostPreflightManager { + manager := &hostPreflightManager{} + + for _, opt := range opts { + opt(manager) + } + + if manager.rc == nil { + manager.rc = runtimeconfig.New(nil) + } + + if manager.logger == nil { + manager.logger = logger.NewDiscardLogger() + } + + if manager.hostPreflight == nil { + manager.hostPreflight = types.NewHostPreflight() + } + + if manager.hostPreflightStore == nil { + manager.hostPreflightStore = NewMemoryStore(manager.hostPreflight) + } + + return manager +} diff --git a/api/internal/managers/preflight/manager_mock.go b/api/internal/managers/preflight/manager_mock.go new file mode 100644 index 000000000..893c1f835 --- /dev/null +++ b/api/internal/managers/preflight/manager_mock.go @@ -0,0 +1,62 @@ +package preflight + +import ( + "context" + + "github.com/replicatedhq/embedded-cluster/api/types" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/stretchr/testify/mock" +) + +var _ HostPreflightManager = (*MockHostPreflightManager)(nil) + +// MockHostPreflightManager is a mock implementation of the HostPreflightManager interface +type MockHostPreflightManager struct { + mock.Mock +} + +// PrepareHostPreflights mocks the PrepareHostPreflights method +func (m *MockHostPreflightManager) PrepareHostPreflights(ctx context.Context, opts PrepareHostPreflightOptions) (*troubleshootv1beta2.HostPreflightSpec, *ecv1beta1.ProxySpec, error) { + args := m.Called(ctx, opts) + if args.Get(0) == nil { + return nil, nil, args.Error(2) + } + if args.Get(1) == nil { + return args.Get(0).(*troubleshootv1beta2.HostPreflightSpec), nil, args.Error(2) + } + return args.Get(0).(*troubleshootv1beta2.HostPreflightSpec), args.Get(1).(*ecv1beta1.ProxySpec), args.Error(2) +} + +// RunHostPreflights mocks the RunHostPreflights method +func (m *MockHostPreflightManager) RunHostPreflights(ctx context.Context, opts RunHostPreflightOptions) error { + args := m.Called(ctx, opts) + return args.Error(0) +} + +// GetHostPreflightStatus mocks the GetHostPreflightStatus method +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 args.Get(0).(*types.Status), args.Error(1) +} + +// GetHostPreflightOutput mocks the GetHostPreflightOutput method +func (m *MockHostPreflightManager) GetHostPreflightOutput(ctx context.Context) (*types.HostPreflightOutput, error) { + args := m.Called(ctx) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*types.HostPreflightOutput), args.Error(1) +} + +// GetHostPreflightTitles mocks the GetHostPreflightTitles method +func (m *MockHostPreflightManager) GetHostPreflightTitles(ctx context.Context) ([]string, error) { + args := m.Called(ctx) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]string), args.Error(1) +} diff --git a/api/internal/managers/preflight/store.go b/api/internal/managers/preflight/store.go new file mode 100644 index 000000000..26f889c93 --- /dev/null +++ b/api/internal/managers/preflight/store.go @@ -0,0 +1,82 @@ +package preflight + +import ( + "sync" + + "github.com/replicatedhq/embedded-cluster/api/types" +) + +type HostPreflightStore interface { + GetTitles() ([]string, error) + SetTitles(titles []string) error + GetOutput() (*types.HostPreflightOutput, error) + SetOutput(output *types.HostPreflightOutput) error + GetStatus() (*types.Status, error) + SetStatus(status *types.Status) error + IsRunning() bool +} + +var _ HostPreflightStore = &MemoryStore{} + +type MemoryStore struct { + mu sync.RWMutex + hostPreflight *types.HostPreflight +} + +func NewMemoryStore(hostPreflight *types.HostPreflight) *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.HostPreflightOutput, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.hostPreflight.Output, nil +} + +func (s *MemoryStore) SetOutput(output *types.HostPreflightOutput) error { + s.mu.Lock() + defer s.mu.Unlock() + s.hostPreflight.Output = output + + return nil +} + +func (s *MemoryStore) GetStatus() (*types.Status, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.hostPreflight.Status, nil +} + +func (s *MemoryStore) SetStatus(status *types.Status) error { + s.mu.Lock() + defer s.mu.Unlock() + s.hostPreflight.Status = status + + return nil +} + +func (s *MemoryStore) IsRunning() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.hostPreflight.Status.State == types.StateRunning +} diff --git a/api/internal/managers/preflight/store_mock.go b/api/internal/managers/preflight/store_mock.go new file mode 100644 index 000000000..2b310b485 --- /dev/null +++ b/api/internal/managers/preflight/store_mock.go @@ -0,0 +1,64 @@ +package preflight + +import ( + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/stretchr/testify/mock" +) + +var _ HostPreflightStore = (*MockHostPreflightStore)(nil) + +// MockHostPreflightStore is a mock implementation of the HostPreflightStore interface +type MockHostPreflightStore struct { + mock.Mock +} + +// GetTitles mocks the GetTitles method +func (m *MockHostPreflightStore) GetTitles() ([]string, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]string), args.Error(1) +} + +// SetTitles mocks the SetTitles method +func (m *MockHostPreflightStore) SetTitles(titles []string) error { + args := m.Called(titles) + return args.Error(0) +} + +// GetOutput mocks the GetOutput method +func (m *MockHostPreflightStore) GetOutput() (*types.HostPreflightOutput, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*types.HostPreflightOutput), args.Error(1) +} + +// SetOutput mocks the SetOutput method +func (m *MockHostPreflightStore) SetOutput(output *types.HostPreflightOutput) error { + args := m.Called(output) + return args.Error(0) +} + +// GetStatus mocks the GetStatus method +func (m *MockHostPreflightStore) 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 *MockHostPreflightStore) 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/pkg/installation/store.go b/api/pkg/installation/store.go deleted file mode 100644 index dea3bae23..000000000 --- a/api/pkg/installation/store.go +++ /dev/null @@ -1,59 +0,0 @@ -package installation - -import ( - "sync" - - "github.com/replicatedhq/embedded-cluster/api/types" -) - -type InstallationStore interface { - ReadConfig() (*types.InstallationConfig, error) - WriteConfig(cfg types.InstallationConfig) error - ReadStatus() (*types.InstallationStatus, error) - WriteStatus(status types.InstallationStatus) error -} - -var _ InstallationStore = &MemoryStore{} - -type MemoryStore struct { - mu sync.RWMutex - config *types.InstallationConfig - status *types.InstallationStatus -} - -func NewMemoryStore() *MemoryStore { - return &MemoryStore{ - config: &types.InstallationConfig{}, - status: &types.InstallationStatus{}, - } -} - -func (s *MemoryStore) ReadConfig() (*types.InstallationConfig, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.config, nil -} - -func (s *MemoryStore) WriteConfig(cfg types.InstallationConfig) error { - s.mu.Lock() - defer s.mu.Unlock() - s.config = &cfg - - return nil -} - -func (s *MemoryStore) ReadStatus() (*types.InstallationStatus, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.status, nil -} - -func (s *MemoryStore) WriteStatus(status types.InstallationStatus) error { - s.mu.Lock() - defer s.mu.Unlock() - s.status = &status - - return nil -} diff --git a/api/logging.go b/api/pkg/logger/logger.go similarity index 98% rename from api/logging.go rename to api/pkg/logger/logger.go index 645f5df5d..c108fb9c8 100644 --- a/api/logging.go +++ b/api/pkg/logger/logger.go @@ -1,4 +1,4 @@ -package api +package logger import ( "fmt" diff --git a/api/pkg/utils/domains.go b/api/pkg/utils/domains.go new file mode 100644 index 000000000..e594a23bd --- /dev/null +++ b/api/pkg/utils/domains.go @@ -0,0 +1,20 @@ +package utils + +import ( + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" + "github.com/replicatedhq/embedded-cluster/pkg/release" +) + +// GetDomains returns the configured custom domains for the release. +func GetDomains(releaseData *release.ReleaseData) ecv1beta1.Domains { + var cfgspec *ecv1beta1.ConfigSpec + if releaseData != nil && releaseData.EmbeddedClusterConfig != nil { + cfgspec = &releaseData.EmbeddedClusterConfig.Spec + } + var rel *release.ChannelRelease + if releaseData != nil && releaseData.ChannelRelease != nil { + rel = releaseData.ChannelRelease + } + return domains.GetDomains(cfgspec, rel) +} diff --git a/api/pkg/utils/netutils_mock.go b/api/pkg/utils/netutils_mock.go new file mode 100644 index 000000000..57962f55d --- /dev/null +++ b/api/pkg/utils/netutils_mock.go @@ -0,0 +1,27 @@ +package utils + +import ( + "github.com/stretchr/testify/mock" +) + +var _ NetUtils = (*MockNetUtils)(nil) + +// MockNetUtils is a mock implementation of the NetUtils interface +type MockNetUtils struct { + mock.Mock +} + +// ListValidNetworkInterfaces mocks the ListValidNetworkInterfaces method +func (m *MockNetUtils) ListValidNetworkInterfaces() ([]string, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]string), args.Error(1) +} + +// DetermineBestNetworkInterface mocks the DetermineBestNetworkInterface method +func (m *MockNetUtils) DetermineBestNetworkInterface() (string, error) { + args := m.Called() + return args.String(0), args.Error(1) +} diff --git a/api/types/install.go b/api/types/install.go index 10837b4d6..66e875b52 100644 --- a/api/types/install.go +++ b/api/types/install.go @@ -1,6 +1,24 @@ package types +// Install represents the install workflow state type Install struct { - Config InstallationConfig `json:"config"` - Status InstallationStatus `json:"status"` + Steps InstallSteps `json:"steps"` + Status *Status `json:"status"` +} + +// InstallSteps represents the steps of the install workflow +type InstallSteps struct { + Installation *Installation `json:"installation"` + HostPreflight *HostPreflight `json:"hostPreflight"` +} + +// NewInstall initializes a new install workflow state +func NewInstall() *Install { + return &Install{ + Steps: InstallSteps{ + Installation: NewInstallation(), + HostPreflight: NewHostPreflight(), + }, + Status: NewStatus(), + } } diff --git a/api/types/installation.go b/api/types/installation.go index ac8db9d5f..3b2dca16f 100644 --- a/api/types/installation.go +++ b/api/types/installation.go @@ -1,7 +1,11 @@ package types -import "time" +type Installation struct { + Config *InstallationConfig `json:"config"` + Status *Status `json:"status"` +} +// InstallationConfig represents the configuration for an installation type InstallationConfig struct { AdminConsolePort int `json:"adminConsolePort"` DataDirectory string `json:"dataDirectory"` @@ -15,17 +19,10 @@ type InstallationConfig struct { GlobalCIDR string `json:"globalCidr"` } -type InstallationStatus struct { - State InstallationState `json:"state"` - Description string `json:"description"` - LastUpdated time.Time `json:"lastUpdated"` +// NewInstallation initializes a new installation state +func NewInstallation() *Installation { + return &Installation{ + Config: &InstallationConfig{}, + Status: NewStatus(), + } } - -type InstallationState string - -const ( - InstallationStatePending InstallationState = "Pending" - InstallationStateRunning InstallationState = "Running" - InstallationStateSucceeded InstallationState = "Succeeded" - InstallationStateFailed InstallationState = "Failed" -) diff --git a/api/types/preflight.go b/api/types/preflight.go index bdbf284d8..ff349fa74 100644 --- a/api/types/preflight.go +++ b/api/types/preflight.go @@ -1,36 +1,12 @@ package types -import "time" - -// RunHostPreflightResponse represents the response from starting host preflight checks -type RunHostPreflightResponse struct { - Status HostPreflightStatus `json:"status"` -} - -// HostPreflightStatusResponse represents the response when polling host preflight status -type HostPreflightStatusResponse struct { - Status HostPreflightStatus `json:"status"` - Output *HostPreflightOutput `json:"output,omitempty"` +// HostPreflight represents the host preflight checks state +type HostPreflight struct { + Titles []string `json:"titles"` + Output *HostPreflightOutput `json:"output"` + Status *Status `json:"status"` } -// HostPreflightStatus represents the current status of host preflight checks -type HostPreflightStatus struct { - State HostPreflightState `json:"state"` - Description string `json:"description"` - LastUpdated time.Time `json:"lastUpdated"` -} - -// HostPreflightState represents the possible states of host preflight execution -type HostPreflightState string - -const ( - HostPreflightStatePending HostPreflightState = "Pending" - HostPreflightStateRunning HostPreflightState = "Running" - HostPreflightStateSucceeded HostPreflightState = "Succeeded" - HostPreflightStateFailed HostPreflightState = "Failed" -) - -// HostPreflightOutput represents the output of host preflight checks type HostPreflightOutput struct { Pass []HostPreflightRecord `json:"pass"` Warn []HostPreflightRecord `json:"warn"` @@ -42,3 +18,19 @@ type HostPreflightRecord struct { Title string `json:"title"` Message string `json:"message"` } + +func NewHostPreflight() *HostPreflight { + return &HostPreflight{ + Status: NewStatus(), + } +} + +// HasFail returns true if any of the preflight checks failed. +func (o HostPreflightOutput) HasFail() bool { + return len(o.Fail) > 0 +} + +// HasWarn returns true if any of the preflight checks returned a warning. +func (o HostPreflightOutput) HasWarn() bool { + return len(o.Warn) > 0 +} diff --git a/api/types/responses.go b/api/types/responses.go new file mode 100644 index 000000000..a93f1754a --- /dev/null +++ b/api/types/responses.go @@ -0,0 +1,8 @@ +package types + +// InstallHostPreflightsStatusResponse represents the response when polling install host preflights status +type InstallHostPreflightsStatusResponse struct { + Titles []string `json:"titles"` + Output *HostPreflightOutput `json:"output,omitempty"` + Status *Status `json:"status,omitempty"` +} diff --git a/api/types/status.go b/api/types/status.go new file mode 100644 index 000000000..19a769e42 --- /dev/null +++ b/api/types/status.go @@ -0,0 +1,49 @@ +package types + +import ( + "errors" + "fmt" + "time" +) + +type Status struct { + State State `json:"state"` + Description string `json:"description"` + LastUpdated time.Time `json:"lastUpdated"` +} + +type State string + +const ( + StatePending State = "Pending" + StateRunning State = "Running" + StateSucceeded State = "Succeeded" + StateFailed State = "Failed" +) + +func NewStatus() *Status { + return &Status{ + State: StatePending, + } +} + +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 + default: + ve = AppendFieldError(ve, "state", fmt.Errorf("invalid state: %s", status.State)) + } + + if status.Description == "" { + ve = AppendFieldError(ve, "description", errors.New("description is required")) + } + + return ve.ErrorOrNil() +} diff --git a/api/types/status_test.go b/api/types/status_test.go new file mode 100644 index 000000000..8032d18de --- /dev/null +++ b/api/types/status_test.go @@ -0,0 +1,88 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestValidateStatus(t *testing.T) { + tests := []struct { + name string + status *Status + expectedErr bool + }{ + { + name: "valid status - pending", + status: &Status{ + State: StatePending, + Description: "Installation pending", + LastUpdated: time.Now(), + }, + expectedErr: false, + }, + { + name: "valid status - running", + status: &Status{ + State: StateRunning, + Description: "Installation in progress", + LastUpdated: time.Now(), + }, + expectedErr: false, + }, + { + name: "valid status - succeeded", + status: &Status{ + State: StateSucceeded, + Description: "Installation completed successfully", + LastUpdated: time.Now(), + }, + expectedErr: false, + }, + { + name: "valid status - failed", + status: &Status{ + State: StateFailed, + Description: "Installation failed", + LastUpdated: time.Now(), + }, + expectedErr: false, + }, + { + name: "nil status", + status: nil, + expectedErr: true, + }, + { + name: "invalid state", + status: &Status{ + State: "Invalid", + Description: "Invalid state", + LastUpdated: time.Now(), + }, + expectedErr: true, + }, + { + name: "missing description", + status: &Status{ + State: StateRunning, + Description: "", + LastUpdated: time.Now(), + }, + expectedErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateStatus(tt.status) + + if tt.expectedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/installer/cli/adminconsole_resetpassword.go b/cmd/installer/cli/adminconsole_resetpassword.go index 89dc53051..137131116 100644 --- a/cmd/installer/cli/adminconsole_resetpassword.go +++ b/cmd/installer/cli/adminconsole_resetpassword.go @@ -8,12 +8,15 @@ import ( "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" "github.com/replicatedhq/embedded-cluster/pkg/prompts" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) func AdminConsoleResetPasswordCmd(ctx context.Context, name string) *cobra.Command { + var rc runtimeconfig.RuntimeConfig + cmd := &cobra.Command{ Use: "reset-password [password]", Args: cobra.MaximumNArgs(1), @@ -23,8 +26,10 @@ func AdminConsoleResetPasswordCmd(ctx context.Context, name string) *cobra.Comma return fmt.Errorf("reset-password command must be run as root") } - if err := rcutil.InitRuntimeConfigFromCluster(cmd.Context()); err != nil { - return fmt.Errorf("failed to init runtime config from cluster: %w", err) + var err error + rc, err = rcutil.GetRuntimeConfigFromCluster(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to get runtime config from cluster: %w", err) } return nil @@ -55,7 +60,7 @@ func AdminConsoleResetPasswordCmd(ctx context.Context, name string) *cobra.Comma } } - if err := kotscli.ResetPassword(password); err != nil { + if err := kotscli.ResetPassword(rc, password); err != nil { return err } diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go new file mode 100644 index 000000000..507948134 --- /dev/null +++ b/cmd/installer/cli/api.go @@ -0,0 +1,210 @@ +package cli + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "time" + + "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" + "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 + Logger logrus.FieldLogger + MetricsReporter metrics.ReporterInterface + Password string + ManagerPort int + LicenseFile string + AirgapBundle string + ConfigChan chan<- *apitypes.InstallationConfig + ReleaseData *release.ReleaseData + WebAssetsFS fs.FS +} + +func startAPI(ctx context.Context, cert tls.Certificate, config APIConfig) error { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.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 !errors.Is(err, http.ErrServerClosed) { + logrus.Errorf("api error: %v", err) + } + } + }() + + if err := waitForAPI(ctx, listener.Addr().String()); err != nil { + return fmt.Errorf("unable to wait for api: %w", err) + } + + return nil +} + +func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, config APIConfig) error { + router := mux.NewRouter() + + if config.ReleaseData == nil { + return fmt.Errorf("release not found") + } + if config.ReleaseData.Application == nil { + return fmt.Errorf("application not found") + } + + logger, err := loggerFromConfig(config) + if err != nil { + return fmt.Errorf("new api logger: %w", err) + } + + api, err := api.New( + config.Password, + api.WithLogger(logger), + api.WithRuntimeConfig(config.RuntimeConfig), + api.WithMetricsReporter(config.MetricsReporter), + api.WithReleaseData(config.ReleaseData), + api.WithLicenseFile(config.LicenseFile), + api.WithAirgapBundle(config.AirgapBundle), + api.WithConfigChan(config.ConfigChan), + ) + 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)) + if err != nil { + return fmt.Errorf("new web server: %w", err) + } + + api.RegisterRoutes(router.PathPrefix("/api").Subrouter()) + webServer.RegisterRoutes(router.PathPrefix("/").Subrouter()) + + server := &http.Server{ + // ErrorLog outputs TLS errors and warnings to the console, we want to make sure we use the same logrus logger for them + ErrorLog: log.New(logger.WithField("http-server", "std-log").Writer(), "", 0), + Handler: router, + TLSConfig: tlsutils.GetTLSConfig(cert), + } + + go func() { + <-ctx.Done() + logrus.Debugf("Shutting down API") + server.Shutdown(context.Background()) + }() + + return server.ServeTLS(listener, "", "") +} + +func loggerFromConfig(config APIConfig) (logrus.FieldLogger, error) { + if config.Logger != nil { + return config.Logger, nil + } + logger, err := apilogger.NewLogger() + if err != nil { + return nil, fmt.Errorf("new api logger: %w", err) + } + return logger, nil +} + +func waitForAPI(ctx context.Context, addr string) error { + httpClient := http.Client{ + Timeout: 2 * time.Second, + Transport: &http.Transport{ + Proxy: nil, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + timeout := time.After(10 * time.Second) + var lastErr error + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-timeout: + if lastErr != nil { + return fmt.Errorf("api did not start in time: %w", lastErr) + } + return fmt.Errorf("api did not start in time") + case <-time.Tick(1 * time.Second): + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/health", addr), nil) + if err != nil { + lastErr = fmt.Errorf("unable to create request: %w", err) + continue + } + resp, err := httpClient.Do(req) + if err != nil { + lastErr = fmt.Errorf("unable to connect to api: %w", err) + } else if resp.StatusCode == http.StatusOK { + return nil + } + } + } +} + +func markUIInstallComplete(password string, managerPort int) 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) + } + + _, err := apiClient.SetInstallStatus(&apitypes.Status{ + State: apitypes.StateSucceeded, + Description: "Install Complete", + 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) + } + ipaddr := runtimeconfig.TryDiscoverPublicIP() + if ipaddr == "" { + if addr := os.Getenv("EC_PUBLIC_ADDRESS"); addr != "" { + ipaddr = addr + } else { + logrus.Errorf("Unable to determine node IP address") + ipaddr = "NODE-IP-ADDRESS" + } + } + return fmt.Sprintf("https://%s:%v", ipaddr, port) +} diff --git a/cmd/installer/cli/api_test.go b/cmd/installer/cli/api_test.go new file mode 100644 index 000000000..3466d50c6 --- /dev/null +++ b/cmd/installer/cli/api_test.go @@ -0,0 +1,96 @@ +package cli + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + "net/http" + "strconv" + "testing" + "testing/fstest" + "time" + + apilogger "github.com/replicatedhq/embedded-cluster/api/pkg/logger" + "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" + "github.com/replicatedhq/embedded-cluster/pkg/release" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_serveAPI(t *testing.T) { + logrus.SetLevel(logrus.DebugLevel) + + listener, err := net.Listen("tcp", ":0") + require.NoError(t, err) + t.Cleanup(func() { + _ = listener.Close() + }) + + ctx, cancel := context.WithCancel(t.Context()) + t.Cleanup(cancel) + + errCh := make(chan error) + + _, port, err := net.SplitHostPort(listener.Addr().String()) + require.NoError(t, err) + + cert, _, _, err := tlsutils.GenerateCertificate("localhost", nil) + require.NoError(t, err) + + certPool := x509.NewCertPool() + certPool.AddCert(cert.Leaf) + + // Mock the web assets filesystem so that we don't need to embed the web assets. + webAssetsFS = fstest.MapFS{ + "index.html": &fstest.MapFile{ + Data: []byte(""), + Mode: 0644, + }, + } + + 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{}, + }, + }, + } + + go func() { + err := serveAPI(ctx, listener, cert, config) + t.Logf("Install API exited with error: %v", err) + errCh <- err + }() + + url := "https://" + net.JoinHostPort("localhost", port) + "/api/health" + t.Logf("Making request to %s", url) + httpClient := http.Client{ + Timeout: 2 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, + }, + } + resp, err := httpClient.Get(url) + require.NoError(t, err) + if resp != nil { + defer resp.Body.Close() + } + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + cancel() + assert.ErrorIs(t, <-errCh, http.ErrServerClosed) + t.Logf("Install API exited") +} diff --git a/cmd/installer/cli/enable_ha.go b/cmd/installer/cli/enable_ha.go index e65f50aa9..35aee230b 100644 --- a/cmd/installer/cli/enable_ha.go +++ b/cmd/installer/cli/enable_ha.go @@ -18,6 +18,8 @@ import ( // EnableHACmd is the command for enabling HA mode. func EnableHACmd(ctx context.Context, name string) *cobra.Command { + var rc runtimeconfig.RuntimeConfig + cmd := &cobra.Command{ Use: "enable-ha", Short: fmt.Sprintf("Enable high availability for the %s cluster", name), @@ -26,18 +28,18 @@ func EnableHACmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("enable-ha command must be run as root") } - rcutil.InitBestRuntimeConfig(cmd.Context()) + rc = rcutil.InitBestRuntimeConfig(cmd.Context()) - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeConfig()) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("KUBECONFIG", rc.PathToKubeConfig()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - if err := runEnableHA(cmd.Context()); err != nil { + if err := runEnableHA(cmd.Context(), rc); err != nil { return err } @@ -48,7 +50,7 @@ func EnableHACmd(ctx context.Context, name string) *cobra.Command { return cmd } -func runEnableHA(ctx context.Context) error { +func runEnableHA(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { kcli, err := kubeutils.KubeClient() if err != nil { return fmt.Errorf("unable to get kube client: %w", err) @@ -80,11 +82,11 @@ func runEnableHA(ctx context.Context) error { airgapChartsPath := "" if in.Spec.AirGap { - airgapChartsPath = runtimeconfig.EmbeddedClusterChartsSubDir() + airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: runtimeconfig.PathToKubeConfig(), + KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, }) @@ -96,5 +98,5 @@ func runEnableHA(ctx context.Context) error { loading := spinner.Start() defer loading.Close() - return addons.EnableHA(ctx, logrus.Debugf, kcli, mcli, kclient, hcli, in.Spec.Network.ServiceCIDR, in.Spec, loading) + return addons.EnableHA(ctx, logrus.Debugf, kcli, mcli, kclient, hcli, rc, in.Spec.Network.ServiceCIDR, in.Spec, loading) } diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index bb8c139a0..44456d7ce 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -7,9 +7,6 @@ import ( "errors" "fmt" "io/fs" - "log" - "net" - "net/http" "os" "path/filepath" "runtime" @@ -19,17 +16,17 @@ import ( "time" "github.com/AlecAivazis/survey/v2/terminal" - "github.com/gorilla/mux" "github.com/gosimple/slug" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" - "github.com/replicatedhq/embedded-cluster/api" - apiclient "github.com/replicatedhq/embedded-cluster/api/client" apitypes "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/kinds/types" 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-new/tlsutils" "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" @@ -46,16 +43,13 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/k0s" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/metrics" - "github.com/replicatedhq/embedded-cluster/pkg/netutil" "github.com/replicatedhq/embedded-cluster/pkg/netutils" - "github.com/replicatedhq/embedded-cluster/pkg/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "github.com/replicatedhq/embedded-cluster/pkg/support" "github.com/replicatedhq/embedded-cluster/pkg/versions" - "github.com/replicatedhq/embedded-cluster/web" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -105,40 +99,42 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command { var flags InstallCmdFlags ctx, cancel := context.WithCancel(ctx) + rc := runtimeconfig.New(nil) cmd := &cobra.Command{ Use: "install", Short: fmt.Sprintf("Install %s", name), PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() cancel() // Cancel context when command completes }, RunE: func(cmd *cobra.Command, args []string) error { - if err := preRunInstall(cmd, &flags); err != nil { + if err := preRunInstall(cmd, &flags, rc); err != nil { return err } + clusterID := metrics.ClusterID() - metricsReporter := NewInstallReporter( + installReporter := newInstallReporter( replicatedAppURL(), clusterID, cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), flags.license.Spec.LicenseID, flags.license.Spec.AppSlug, ) - metricsReporter.ReportInstallationStarted(ctx) + installReporter.ReportInstallationStarted(ctx) // Setup signal handler with the metrics reporter cleanup function signalHandler(ctx, cancel, func(ctx context.Context, sig os.Signal) { - metricsReporter.ReportSignalAborted(ctx, sig) + installReporter.ReportSignalAborted(ctx, sig) }) - if err := runInstall(cmd.Context(), name, flags, metricsReporter); err != nil { + if err := runInstall(cmd.Context(), name, flags, rc, installReporter); err != nil { // Check if this is an interrupt error from the terminal if errors.Is(err, terminal.InterruptErr) { - metricsReporter.ReportSignalAborted(ctx, syscall.SIGINT) + installReporter.ReportSignalAborted(ctx, syscall.SIGINT) } else { - metricsReporter.ReportInstallationFailed(ctx, err) + installReporter.ReportInstallationFailed(ctx, err) } return err } - metricsReporter.ReportInstallationSucceeded(ctx) + installReporter.ReportInstallationSucceeded(ctx) // If in guided UI mode, keep the process running until interrupted if flags.enableManagerExperience { @@ -245,7 +241,7 @@ func addManagerExperienceFlags(cmd *cobra.Command, flags *InstallCmdFlags) error return nil } -func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error { +func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { if os.Getuid() != 0 { return fmt.Errorf("install command must be run as root") } @@ -321,17 +317,30 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error { flags.tlsKeyBytes = keyData } - if err := preRunInstallAPI(cmd.Context(), flags.tlsCert, flags.adminConsolePassword, flags.managerPort, configChan); err != nil { - return fmt.Errorf("unable to start install API: %w", err) + apiConfig := APIConfig{ + // TODO (@salah): implement reporting in api + // MetricsReporter: reporter, + RuntimeConfig: rc, + Password: flags.adminConsolePassword, + ManagerPort: flags.managerPort, + LicenseFile: flags.licenseFile, + AirgapBundle: flags.airgapBundle, + ConfigChan: configChan, + ReleaseData: release.GetReleaseData(), + } + + if err := startAPI(cmd.Context(), flags.tlsCert, apiConfig); err != nil { + return fmt.Errorf("unable to start api: %w", err) } // TODO: fix this message logrus.Info("") logrus.Infof("Visit %s to configure your cluster", getManagerURL(flags.hostname, flags.managerPort)) + logrus.Info("") installConfig, ok := <-configChan if !ok { - return fmt.Errorf("install API closed channel") + return fmt.Errorf("api closed channel") } proxy, err := newconfig.GetProxySpec( @@ -395,26 +404,26 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error { } // TODO: validate that a single port isn't used for multiple services - runtimeconfig.SetDataDir(flags.dataDir) - runtimeconfig.SetManagerPort(flags.managerPort) - runtimeconfig.SetLocalArtifactMirrorPort(flags.localArtifactMirrorPort) - runtimeconfig.SetAdminConsolePort(flags.adminConsolePort) + rc.SetDataDir(flags.dataDir) + rc.SetManagerPort(flags.managerPort) + rc.SetLocalArtifactMirrorPort(flags.localArtifactMirrorPort) + rc.SetAdminConsolePort(flags.adminConsolePort) - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeConfig()) // this is needed for restore as well since it shares this function - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("KUBECONFIG", rc.PathToKubeConfig()) // this is needed for restore as well since it shares this function + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) hostCABundlePath, err := findHostCABundle() if err != nil { return fmt.Errorf("unable to find host CA bundle: %w", err) } - runtimeconfig.SetHostCABundlePath(hostCABundlePath) + rc.SetHostCABundlePath(hostCABundlePath) logrus.Debugf("using host CA bundle: %s", hostCABundlePath) - if err := runtimeconfig.WriteToDisk(); err != nil { + if err := rc.WriteToDisk(); err != nil { return fmt.Errorf("unable to write runtime config to disk: %w", err) } - if err := os.Chmod(runtimeconfig.EmbeddedClusterHomeDirectory(), 0755); err != nil { + if err := os.Chmod(rc.EmbeddedClusterHomeDirectory(), 0755); err != nil { // don't fail as there are cases where we can't change the permissions (bind mounts, selinux, etc...), // and we handle and surface those errors to the user later (host preflights, checking exec errors, etc...) logrus.Debugf("unable to chmod embedded-cluster home dir: %s", err) @@ -423,124 +432,27 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error { return nil } -func preRunInstallAPI(ctx context.Context, cert tls.Certificate, password string, managerPort int, configChan chan<- *apitypes.InstallationConfig) error { - logger, err := api.NewLogger() - if err != nil { - logrus.Warnf("Unable to setup API logging: %v", err) - } - - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", managerPort)) - if err != nil { - return fmt.Errorf("unable to create listener: %w", err) +func runInstall(ctx context.Context, name string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, installReporter *InstallReporter) error { + if err := runInstallVerifyAndPrompt(ctx, name, flags, prompts.New()); err != nil { + return err } - go func() { - if err := runInstallAPI(ctx, listener, cert, logger, password, configChan); err != nil { - if !errors.Is(err, http.ErrServerClosed) { - logrus.Errorf("install API error: %v", err) - } + if !flags.enableManagerExperience { + logrus.Debug("initializing install") + if err := initializeInstall(ctx, flags, rc); err != nil { + return fmt.Errorf("unable to initialize install: %w", err) } - }() - - if err := waitForInstallAPI(ctx, listener.Addr().String()); err != nil { - return fmt.Errorf("unable to wait for install API: %w", err) - } - - return nil -} - -func runInstallAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, logger logrus.FieldLogger, password string, configChan chan<- *apitypes.InstallationConfig) error { - router := mux.NewRouter() - - api, err := api.New( - password, - api.WithLogger(logger), - api.WithConfigChan(configChan), - ) - if err != nil { - return fmt.Errorf("new api: %w", err) - } - app := release.GetApplication() - if app == nil { - return fmt.Errorf("application not found") - } - - webServer, err := web.New(web.InitialState{ - Title: app.Spec.Title, - Icon: app.Spec.Icon, - }, web.WithLogger(logger), web.WithAssetsFS(webAssetsFS)) - if err != nil { - return fmt.Errorf("new web server: %w", err) - } - - api.RegisterRoutes(router.PathPrefix("/api").Subrouter()) - webServer.RegisterRoutes(router.PathPrefix("/").Subrouter()) - - server := &http.Server{ - // ErrorLog outputs TLS errors and warnings to the console, we want to make sure we use the same logrus logger for them - ErrorLog: log.New(logger.WithField("http-server", "std-log").Writer(), "", 0), - Handler: router, - TLSConfig: tlsutils.GetTLSConfig(cert), - } - - go func() { - <-ctx.Done() - logrus.Debugf("Shutting down install API") - server.Shutdown(context.Background()) - }() - - return server.ServeTLS(listener, "", "") -} -func waitForInstallAPI(ctx context.Context, addr string) error { - httpClient := http.Client{ - Timeout: 2 * time.Second, - Transport: &http.Transport{ - Proxy: nil, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - }, - } - timeout := time.After(10 * time.Second) - var lastErr error - for { - select { - case <-timeout: - if lastErr != nil { - return fmt.Errorf("install API did not start in time: %w", lastErr) - } - return fmt.Errorf("install API did not start in time") - case <-time.Tick(1 * time.Second): - resp, err := httpClient.Get(fmt.Sprintf("https://%s/api/health", addr)) - if err != nil { - lastErr = fmt.Errorf("unable to connect to install API: %w", err) - } else if resp.StatusCode == http.StatusOK { - return nil + logrus.Debugf("running install preflights") + if err := runInstallPreflights(ctx, flags, rc, installReporter.reporter); err != nil { + if errors.Is(err, preflights.ErrPreflightsHaveFail) { + return NewErrorNothingElseToAdd(err) } + return fmt.Errorf("unable to run install preflights: %w", err) } } -} -func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metricsReporter preflights.MetricsReporter) error { - if err := runInstallVerifyAndPrompt(ctx, name, &flags, prompts.New()); err != nil { - return err - } - - logrus.Debug("initializing install") - if err := initializeInstall(ctx, flags); err != nil { - return fmt.Errorf("unable to initialize install: %w", err) - } - - logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, metricsReporter); err != nil { - if errors.Is(err, preflights.ErrPreflightsHaveFail) { - return NewErrorNothingElseToAdd(err) - } - return fmt.Errorf("unable to run install preflights: %w", err) - } - - k0sCfg, err := installAndStartCluster(ctx, flags.networkInterface, flags.airgapBundle, flags.proxy, flags.cidrCfg, flags.overrides, nil) + k0sCfg, err := installAndStartCluster(ctx, flags, rc, nil) if err != nil { return fmt.Errorf("unable to install cluster: %w", err) } @@ -553,7 +465,7 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics errCh := kubeutils.WaitForKubernetes(ctx, kcli) defer logKubernetesErrors(errCh) - in, err := recordInstallation(ctx, kcli, flags, k0sCfg, flags.license) + in, err := recordInstallation(ctx, kcli, flags, rc, k0sCfg, flags.license) if err != nil { return fmt.Errorf("unable to record installation: %w", err) } @@ -589,11 +501,11 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics airgapChartsPath := "" if flags.isAirgap { - airgapChartsPath = runtimeconfig.EmbeddedClusterChartsSubDir() + airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: runtimeconfig.PathToKubeConfig(), + KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, }) @@ -603,12 +515,12 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics defer hcli.Close() logrus.Debugf("installing addons") - if err := addons.Install(ctx, logrus.Debugf, hcli, addons.InstallOptions{ + if err := addons.Install(ctx, logrus.Debugf, hcli, rc, addons.InstallOptions{ AdminConsolePwd: flags.adminConsolePassword, License: flags.license, IsAirgap: flags.airgapBundle != "", Proxy: flags.proxy, - HostCABundlePath: runtimeconfig.HostCABundlePath(), + HostCABundlePath: rc.HostCABundlePath(), TLSCertBytes: flags.tlsCertBytes, TLSKeyBytes: flags.tlsKeyBytes, Hostname: flags.hostname, @@ -619,6 +531,7 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics EndUserConfigSpec: euCfgSpec, KotsInstaller: func(msg *spinner.MessageWriter) error { opts := kotscli.InstallOptions{ + RuntimeConfig: rc, AppSlug: flags.license.Spec.AppSlug, LicenseFile: flags.licenseFile, Namespace: runtimeconfig.KotsadmNamespace, @@ -650,7 +563,7 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics return fmt.Errorf("unable to mark ui install complete: %w", err) } } else { - if err := printSuccessMessage(flags.license, flags.hostname, flags.networkInterface); err != nil { + if err := printSuccessMessage(flags.license, flags.hostname, flags.networkInterface, rc); err != nil { return err } } @@ -658,36 +571,7 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics return nil } -func markUIInstallComplete(password string, managerPort int) 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) - } - - _, err := apiClient.SetInstallStatus(apitypes.InstallationStatus{ - State: apitypes.InstallationStateSucceeded, - Description: "Install Complete", - LastUpdated: time.Now(), - }) - if err != nil { - return fmt.Errorf("unable to set install status: %w", err) - } - - return nil -} - -func runInstallVerifyAndPrompt(ctx context.Context, name string, flags *InstallCmdFlags, prompt prompts.Prompt) error { +func runInstallVerifyAndPrompt(ctx context.Context, name string, flags InstallCmdFlags, prompt prompts.Prompt) error { logrus.Debugf("checking if k0s is already installed") err := verifyNoInstallation(name, "reinstall") if err != nil { @@ -727,7 +611,7 @@ func runInstallVerifyAndPrompt(ctx context.Context, name string, flags *InstallC } logrus.Debug("User confirmed prompt to proceed installing with `http_proxy` set and `https_proxy` unset") - if err := preflights.ValidateApp(); err != nil { + if err := release.ValidateECConfig(); err != nil { return err } @@ -881,91 +765,43 @@ func verifyNoInstallation(name string, cmdName string) error { return nil } -func initializeInstall(ctx context.Context, flags InstallCmdFlags) error { +func initializeInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { logrus.Info("") spinner := spinner.Start() spinner.Infof("Initializing") - if err := materializeFiles(flags.airgapBundle); err != nil { - spinner.ErrorClosef("Initialization failed") - return fmt.Errorf("unable to materialize files: %w", err) - } - - logrus.Debugf("copy license file to %s", flags.dataDir) - if err := copyLicenseFileToDataDir(flags.licenseFile, flags.dataDir); err != nil { - // We have decided not to report this error - logrus.Warnf("Unable to copy license file to %s: %v", flags.dataDir, err) - } - - logrus.Debugf("configuring sysctl") - if err := configutils.ConfigureSysctl(); err != nil { - logrus.Debugf("unable to configure sysctl: %v", err) - } - - logrus.Debugf("configuring kernel modules") - if err := configutils.ConfigureKernelModules(); err != nil { - logrus.Debugf("unable to configure kernel modules: %v", err) - } - - logrus.Debugf("configuring network manager") - if err := configureNetworkManager(ctx); err != nil { + if err := hostutils.ConfigureForInstall(ctx, rc, hostutils.InitForInstallOptions{ + LicenseFile: flags.licenseFile, + AirgapBundle: flags.airgapBundle, + PodCIDR: flags.cidrCfg.PodCIDR, + ServiceCIDR: flags.cidrCfg.ServiceCIDR, + }); err != nil { spinner.ErrorClosef("Initialization failed") - return fmt.Errorf("unable to configure network manager: %w", err) - } - - logrus.Debugf("configuring firewalld") - if err := configureFirewalld(ctx, flags.cidrCfg.PodCIDR, flags.cidrCfg.ServiceCIDR); err != nil { - logrus.Debugf("unable to configure firewalld: %v", err) + return fmt.Errorf("configure host for install: %w", err) } spinner.Closef("Initialization complete") return nil } -func materializeFiles(airgapBundle string) error { - materializer := goods.NewMaterializer() - if err := materializer.Materialize(); err != nil { - return fmt.Errorf("materialize binaries: %w", err) - } - if err := support.MaterializeSupportBundleSpec(); err != nil { - return fmt.Errorf("materialize support bundle spec: %w", err) - } - - if airgapBundle != "" { - // read file from path - rawfile, err := os.Open(airgapBundle) - if err != nil { - return fmt.Errorf("failed to open airgap file: %w", err) - } - defer rawfile.Close() - - if err := airgap.MaterializeAirgap(rawfile); err != nil { - err = fmt.Errorf("materialize airgap files: %w", err) - return err - } - } - - return nil -} - -func installAndStartCluster(ctx context.Context, networkInterface string, airgapBundle string, proxy *ecv1beta1.ProxySpec, cidrCfg *newconfig.CIDRConfig, overrides string, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { +func installAndStartCluster(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { loading := spinner.Start() loading.Infof("Installing node") logrus.Debugf("creating k0s configuration file") - cfg, err := k0s.WriteK0sConfig(ctx, networkInterface, airgapBundle, cidrCfg.PodCIDR, cidrCfg.ServiceCIDR, overrides, mutate) + cfg, err := k0s.WriteK0sConfig(ctx, flags.networkInterface, flags.airgapBundle, flags.cidrCfg.PodCIDR, flags.cidrCfg.ServiceCIDR, flags.overrides, mutate) if err != nil { loading.ErrorClosef("Failed to install node") return nil, fmt.Errorf("create config file: %w", err) } logrus.Debugf("creating systemd unit files") - if err := createSystemdUnitFiles(ctx, false, proxy); err != nil { + if err := createSystemdUnitFiles(ctx, rc, false, flags.proxy); err != nil { loading.ErrorClosef("Failed to install node") return nil, fmt.Errorf("create systemd unit files: %w", err) } logrus.Debugf("installing k0s") - if err := k0s.Install(networkInterface); err != nil { + if err := k0s.Install(rc, flags.networkInterface); err != nil { loading.ErrorClosef("Failed to install node") return nil, fmt.Errorf("install cluster: %w", err) } @@ -987,36 +823,6 @@ func installAndStartCluster(ctx context.Context, networkInterface string, airgap return cfg, nil } -// configureNetworkManager configures the network manager (if the host is using it) to ignore -// the calico interfaces. This function restarts the NetworkManager service if the configuration -// was changed. -func configureNetworkManager(ctx context.Context) error { - if active, err := helpers.IsSystemdServiceActive(ctx, "NetworkManager"); err != nil { - return fmt.Errorf("unable to check if NetworkManager is active: %w", err) - } else if !active { - logrus.Debugf("NetworkManager is not active, skipping configuration") - return nil - } - - dir := "/etc/NetworkManager/conf.d" - if _, err := os.Stat(dir); err != nil { - logrus.Debugf("skiping NetworkManager config (%s): %v", dir, err) - return nil - } - - logrus.Debugf("creating NetworkManager config file") - materializer := goods.NewMaterializer() - if err := materializer.CalicoNetworkManagerConfig(); err != nil { - return fmt.Errorf("unable to materialize configuration: %w", err) - } - - logrus.Debugf("network manager config created, restarting the service") - if _, err := helpers.RunCommand("systemctl", "restart", "NetworkManager"); err != nil { - return fmt.Errorf("unable to restart network manager: %w", err) - } - return nil -} - func checkAirgapMatches(airgapBundle string) error { rel := release.GetChannelRelease() if rel == nil { @@ -1149,7 +955,7 @@ func replicatedAppURL() string { embCfgSpec = &embCfg.Spec } domains := runtimeconfig.GetDomains(embCfgSpec) - return netutil.MaybeAddHTTPS(domains.ReplicatedAppDomain) + return netutils.MaybeAddHTTPS(domains.ReplicatedAppDomain) } func proxyRegistryURL() string { @@ -1158,12 +964,12 @@ func proxyRegistryURL() string { embCfgSpec = &embCfg.Spec } domains := runtimeconfig.GetDomains(embCfgSpec) - return netutil.MaybeAddHTTPS(domains.ProxyRegistryDomain) + return netutils.MaybeAddHTTPS(domains.ProxyRegistryDomain) } // createSystemdUnitFiles links the k0s systemd unit file. this also creates a new // systemd unit file for the local artifact mirror service. -func createSystemdUnitFiles(ctx context.Context, isWorker bool, proxy *ecv1beta1.ProxySpec) error { +func createSystemdUnitFiles(ctx context.Context, rc runtimeconfig.RuntimeConfig, isWorker bool, proxy *ecv1beta1.ProxySpec) error { dst := systemdUnitFileName() if _, err := os.Lstat(dst); err == nil { if err := os.Remove(dst); err != nil { @@ -1187,7 +993,7 @@ func createSystemdUnitFiles(ctx context.Context, isWorker bool, proxy *ecv1beta1 if _, err := helpers.RunCommand("systemctl", "daemon-reload"); err != nil { return fmt.Errorf("unable to get reload systemctl daemon: %w", err) } - if err := installAndEnableLocalArtifactMirror(ctx); err != nil { + if err := installAndEnableLocalArtifactMirror(ctx, rc); err != nil { return fmt.Errorf("unable to install and enable local artifact mirror: %w", err) } return nil @@ -1222,12 +1028,12 @@ Environment="NO_PROXY=%s"`, httpProxy, httpsProxy, noProxy) // installAndEnableLocalArtifactMirror installs and enables the local artifact mirror. This // service is responsible for serving on localhost, through http, all files that are used // during a cluster upgrade. -func installAndEnableLocalArtifactMirror(ctx context.Context) error { - materializer := goods.NewMaterializer() +func installAndEnableLocalArtifactMirror(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + materializer := goods.NewMaterializer(rc) if err := materializer.LocalArtifactMirrorUnitFile(); err != nil { return fmt.Errorf("failed to materialize artifact mirror unit: %w", err) } - if err := writeLocalArtifactMirrorDropInFile(); err != nil { + if err := writeLocalArtifactMirrorDropInFile(rc); err != nil { return fmt.Errorf("failed to write local artifact mirror environment file: %w", err) } if _, err := helpers.RunCommand("systemctl", "daemon-reload"); err != nil { @@ -1287,12 +1093,12 @@ ExecStart=%s serve ` ) -func writeLocalArtifactMirrorDropInFile() error { +func writeLocalArtifactMirrorDropInFile(rc runtimeconfig.RuntimeConfig) error { contents := fmt.Sprintf( localArtifactMirrorDropInFileContents, - runtimeconfig.LocalArtifactMirrorPort(), - runtimeconfig.EmbeddedClusterHomeDirectory(), - runtimeconfig.PathToEmbeddedClusterBinary("local-artifact-mirror"), + rc.LocalArtifactMirrorPort(), + rc.EmbeddedClusterHomeDirectory(), + rc.PathToEmbeddedClusterBinary("local-artifact-mirror"), ) err := systemd.WriteDropInFile("local-artifact-mirror.service", "embedded-cluster.conf", []byte(contents)) if err != nil { @@ -1308,7 +1114,7 @@ func waitForK0s() error { var success bool for i := 0; i < 30; i++ { time.Sleep(2 * time.Second) - spath := runtimeconfig.PathToK0sStatusSocket() + spath := runtimeconfig.K0sStatusSocketPath if _, err := os.Stat(spath); err != nil { continue } @@ -1321,7 +1127,7 @@ func waitForK0s() error { } for i := 1; ; i++ { - _, err := helpers.RunCommand(runtimeconfig.K0sBinaryPath(), "status") + _, err := helpers.RunCommand(runtimeconfig.K0sBinaryPath, "status") if err == nil { return nil } else if i == 30 { @@ -1348,7 +1154,7 @@ func waitForNode(ctx context.Context) error { } func recordInstallation( - ctx context.Context, kcli client.Client, flags InstallCmdFlags, + ctx context.Context, kcli client.Client, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, k0sCfg *k0sv1beta1.ClusterConfig, license *kotsv1beta1.License, ) (*ecv1beta1.Installation, error) { // ensure that the embedded-cluster namespace exists @@ -1393,7 +1199,7 @@ func recordInstallation( Proxy: flags.proxy, Network: networkSpecFromK0sConfig(k0sCfg), Config: cfgspec, - RuntimeConfig: runtimeconfig.Get(), + RuntimeConfig: rc.Get(), EndUserK0sConfigOverrides: euOverrides, BinaryName: runtimeconfig.BinaryName(), LicenseInfo: &ecv1beta1.LicenseInfo{ @@ -1536,7 +1342,7 @@ func gatherVersionMetadata(withChannelRelease bool) (*types.ReleaseMetadata, err Repositories: append(repconfig, additionalRepos...), } - k0sCfg := config.RenderK0sConfig(runtimeconfig.DefaultProxyRegistryDomain) + k0sCfg := config.RenderK0sConfig(domains.DefaultProxyRegistryDomain) meta.K0sImages = config.ListK0sImages(k0sCfg) meta.K0sImages = append(meta.K0sImages, addons.GetAdditionalImages()...) meta.K0sImages = helpers.UniqueStringSlice(meta.K0sImages) @@ -1576,22 +1382,8 @@ func normalizeNoPromptToYes(f *pflag.FlagSet, name string) pflag.NormalizedName return pflag.NormalizedName(name) } -func copyLicenseFileToDataDir(licenseFile, dataDir string) error { - if licenseFile == "" { - return nil - } - licenseData, err := os.ReadFile(licenseFile) - if err != nil { - return fmt.Errorf("unable to read license file: %w", err) - } - if err := os.WriteFile(filepath.Join(dataDir, "license.yaml"), licenseData, 0400); err != nil { - return fmt.Errorf("unable to write license file: %w", err) - } - return nil -} - -func printSuccessMessage(license *kotsv1beta1.License, hostname string, networkInterface string) error { - adminConsoleURL := getAdminConsoleURL(hostname, networkInterface, runtimeconfig.AdminConsolePort()) +func printSuccessMessage(license *kotsv1beta1.License, hostname string, networkInterface string, rc runtimeconfig.RuntimeConfig) error { + adminConsoleURL := getAdminConsoleURL(hostname, networkInterface, rc.AdminConsolePort()) // Create the message content message := fmt.Sprintf("Visit the Admin Console to configure and install %s:", license.Spec.AppSlug) @@ -1621,22 +1413,6 @@ func printSuccessMessage(license *kotsv1beta1.License, hostname string, networkI return nil } -func getManagerURL(hostname string, port int) string { - if hostname != "" { - return fmt.Sprintf("https://%s:%v", hostname, port) - } - ipaddr := runtimeconfig.TryDiscoverPublicIP() - if ipaddr == "" { - if addr := os.Getenv("EC_PUBLIC_ADDRESS"); addr != "" { - ipaddr = addr - } else { - logrus.Errorf("Unable to determine node IP address") - ipaddr = "NODE-IP-ADDRESS" - } - } - return fmt.Sprintf("https://%s:%v", ipaddr, port) -} - func getAdminConsoleURL(hostname string, networkInterface string, port int) string { if hostname != "" { return fmt.Sprintf("http://%s:%v", hostname, port) diff --git a/cmd/installer/cli/install_runpreflights.go b/cmd/installer/cli/install_runpreflights.go index dcd991f15..2fe79f40b 100644 --- a/cmd/installer/cli/install_runpreflights.go +++ b/cmd/installer/cli/install_runpreflights.go @@ -5,34 +5,41 @@ import ( "errors" "fmt" - "github.com/replicatedhq/embedded-cluster/pkg/configutils" + "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" + "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" - "github.com/replicatedhq/embedded-cluster/pkg/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" + "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +// ErrPreflightsHaveFail is an error returned when we managed to execute the host preflights but +// they contain failures. We use this to differentiate the way we provide user feedback. +var ErrPreflightsHaveFail = metrics.NewErrorNoFail(fmt.Errorf("host preflight failures detected")) + func InstallRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { var flags InstallCmdFlags + rc := runtimeconfig.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); err != nil { + if err := preRunInstall(cmd, &flags, rc); err != nil { return err } return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - if err := runInstallRunPreflights(cmd.Context(), name, flags); err != nil { + if err := runInstallRunPreflights(cmd.Context(), name, flags, rc); err != nil { return err } @@ -50,28 +57,28 @@ func InstallRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { return cmd } -func runInstallRunPreflights(ctx context.Context, name string, flags InstallCmdFlags) error { - if err := runInstallVerifyAndPrompt(ctx, name, &flags, prompts.New()); err != nil { +func runInstallRunPreflights(ctx context.Context, name string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { + if err := runInstallVerifyAndPrompt(ctx, name, flags, prompts.New()); err != nil { return err } logrus.Debugf("materializing binaries") - if err := materializeFiles(flags.airgapBundle); err != nil { + if err := hostutils.MaterializeFiles(rc, flags.airgapBundle); err != nil { return fmt.Errorf("unable to materialize files: %w", err) } logrus.Debugf("configuring sysctl") - if err := configutils.ConfigureSysctl(); err != nil { + if err := hostutils.ConfigureSysctl(); err != nil { logrus.Debugf("unable to configure sysctl: %v", err) } logrus.Debugf("configuring kernel modules") - if err := configutils.ConfigureKernelModules(); err != nil { + if err := hostutils.ConfigureKernelModules(); err != nil { logrus.Debugf("unable to configure kernel modules: %v", err) } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, nil); err != nil { + if err := runInstallPreflights(ctx, flags, rc, nil); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } @@ -83,7 +90,7 @@ func runInstallRunPreflights(ctx context.Context, name string, flags InstallCmdF return nil } -func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, metricsReported preflights.MetricsReporter) error { +func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, metricsReporter metrics.ReporterInterface) error { replicatedAppURL := replicatedAppURL() proxyRegistryURL := proxyRegistryURL() @@ -92,20 +99,27 @@ func runInstallPreflights(ctx context.Context, flags InstallCmdFlags, metricsRep return fmt.Errorf("unable to find first valid address: %w", err) } - if err := preflights.PrepareAndRun(ctx, preflights.PrepareAndRunOptions{ - ReplicatedAppURL: replicatedAppURL, - ProxyRegistryURL: proxyRegistryURL, - Proxy: flags.proxy, - PodCIDR: flags.cidrCfg.PodCIDR, - ServiceCIDR: flags.cidrCfg.ServiceCIDR, - GlobalCIDR: flags.cidrCfg.GlobalCIDR, - NodeIP: nodeIP, - IsAirgap: flags.isAirgap, - SkipHostPreflights: flags.skipHostPreflights, - IgnoreHostPreflights: flags.ignoreHostPreflights, - AssumeYes: flags.assumeYes, - MetricsReporter: metricsReported, - }); err != nil { + hpf, err := preflights.Prepare(ctx, preflights.PrepareOptions{ + HostPreflightSpec: release.GetHostPreflights(), + ReplicatedAppURL: replicatedAppURL, + ProxyRegistryURL: proxyRegistryURL, + AdminConsolePort: rc.AdminConsolePort(), + LocalArtifactMirrorPort: rc.LocalArtifactMirrorPort(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + Proxy: flags.proxy, + PodCIDR: flags.cidrCfg.PodCIDR, + ServiceCIDR: flags.cidrCfg.ServiceCIDR, + GlobalCIDR: flags.cidrCfg.GlobalCIDR, + NodeIP: nodeIP, + IsAirgap: flags.isAirgap, + }) + if err != nil { + return err + } + + if err := runHostPreflights(ctx, hpf, flags.proxy, rc, flags.skipHostPreflights, flags.ignoreHostPreflights, flags.assumeYes, metricsReporter); err != nil { return err } diff --git a/cmd/installer/cli/install_test.go b/cmd/installer/cli/install_test.go index cd1560f03..b63d79813 100644 --- a/cmd/installer/cli/install_test.go +++ b/cmd/installer/cli/install_test.go @@ -3,26 +3,18 @@ package cli import ( "bytes" "context" - "crypto/tls" - "crypto/x509" "fmt" - "net" "net/http" "net/http/httptest" "os" "path/filepath" "testing" - "testing/fstest" - "time" - "github.com/replicatedhq/embedded-cluster/api" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/prompts/plain" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -537,87 +529,6 @@ versionLabel: testversion } } -func Test_runInstallAPI(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) - - listener, err := net.Listen("tcp", ":0") - require.NoError(t, err) - t.Cleanup(func() { - _ = listener.Close() - }) - - ctx, cancel := context.WithCancel(t.Context()) - t.Cleanup(cancel) - - errCh := make(chan error) - - logger := api.NewDiscardLogger() - - _, port, err := net.SplitHostPort(listener.Addr().String()) - require.NoError(t, err) - - cert, _, _, err := tlsutils.GenerateCertificate("localhost", nil) - require.NoError(t, err) - - certPool := x509.NewCertPool() - certPool.AddCert(cert.Leaf) - - // We need a release object to pass over to the Web component. - dataMap := map[string][]byte{ - "kots-app.yaml": []byte(` -apiVersion: kots.io/v1beta1 -kind: Application -`), - } - err = release.SetReleaseDataForTests(dataMap) - require.NoError(t, err) - - t.Cleanup(func() { - release.SetReleaseDataForTests(nil) - }) - - // Mock the web assets filesystem so that we don't need to embed the web assets. - webAssetsFS = fstest.MapFS{ - "index.html": &fstest.MapFile{ - Data: []byte(""), - Mode: 0644, - }, - } - defer func() { webAssetsFS = nil }() - - go func() { - err := runInstallAPI(ctx, listener, cert, logger, "password", nil) - t.Logf("Install API exited with error: %v", err) - errCh <- err - }() - - t.Logf("Waiting for install API to start on %s", listener.Addr().String()) - err = waitForInstallAPI(ctx, net.JoinHostPort("localhost", port)) - assert.NoError(t, err) - - url := "https://" + net.JoinHostPort("localhost", port) + "/api/health" - t.Logf("Making request to %s", url) - httpClient := http.Client{ - Timeout: 2 * time.Second, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: certPool, - }, - }, - } - resp, err := httpClient.Get(url) - require.NoError(t, err) - if resp != nil { - defer resp.Body.Close() - } - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - cancel() - assert.ErrorIs(t, <-errCh, http.ErrServerClosed) - t.Logf("Install API exited") -} - func Test_verifyProxyConfig(t *testing.T) { tests := []struct { name string diff --git a/cmd/installer/cli/join.go b/cmd/installer/cli/join.go index 79f7c9710..4c39ffe8f 100644 --- a/cmd/installer/cli/join.go +++ b/cmd/installer/cli/join.go @@ -14,17 +14,17 @@ 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/hostutils" + "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/configutils" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/k0s" "github.com/replicatedhq/embedded-cluster/pkg/kotsadm" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" "github.com/replicatedhq/embedded-cluster/pkg/netutils" - "github.com/replicatedhq/embedded-cluster/pkg/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -52,6 +52,7 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { var flags JoinCmdFlags ctx, cancel := context.WithCancel(ctx) + rc := runtimeconfig.New(nil) cmd := &cobra.Command{ Use: "join ", @@ -65,7 +66,7 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() cancel() // Cancel context when command completes }, RunE: func(cmd *cobra.Command, args []string) error { @@ -74,27 +75,27 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command { if err != nil { return fmt.Errorf("unable to get join token: %w", err) } - metricsReporter := NewJoinReporter( + joinReporter := newJoinReporter( jcmd.InstallationSpec.MetricsBaseURL, jcmd.ClusterID, cmd.CalledAs(), flagsToStringSlice(cmd.Flags()), ) - metricsReporter.ReportJoinStarted(ctx) + joinReporter.ReportJoinStarted(ctx) // Setup signal handler with the metrics reporter cleanup function signalHandler(ctx, cancel, func(ctx context.Context, sig os.Signal) { - metricsReporter.ReportSignalAborted(ctx, sig) + joinReporter.ReportSignalAborted(ctx, sig) }) - if err := runJoin(cmd.Context(), name, flags, jcmd, args[0], metricsReporter); err != nil { + if err := runJoin(cmd.Context(), name, flags, rc, jcmd, args[0], joinReporter); err != nil { // Check if this is an interrupt error from the terminal if errors.Is(err, terminal.InterruptErr) { - metricsReporter.ReportSignalAborted(ctx, syscall.SIGINT) + joinReporter.ReportSignalAborted(ctx, syscall.SIGINT) } else { - metricsReporter.ReportJoinFailed(ctx, err) + joinReporter.ReportJoinFailed(ctx, err) } return err } - metricsReporter.ReportJoinSucceeded(ctx) + joinReporter.ReportJoinSucceeded(ctx) return nil }, } @@ -149,24 +150,24 @@ func addJoinFlags(cmd *cobra.Command, flags *JoinCmdFlags) error { return nil } -func runJoin(ctx context.Context, name string, flags JoinCmdFlags, jcmd *join.JoinCommandResponse, kotsAPIAddress string, metricsReporter preflights.MetricsReporter) error { +func runJoin(ctx context.Context, name string, flags JoinCmdFlags, rc runtimeconfig.RuntimeConfig, jcmd *join.JoinCommandResponse, kotsAPIAddress string, joinReporter *JoinReporter) error { // both controller and worker nodes will have 'worker' in the join command isWorker := !strings.Contains(jcmd.K0sJoinCommand, "controller") if !isWorker { logrus.Warn("\nDo not join another node until this node has joined successfully.") } - if err := runJoinVerifyAndPrompt(name, flags, jcmd); err != nil { + if err := runJoinVerifyAndPrompt(name, flags, rc, jcmd); err != nil { return err } - cidrCfg, err := initializeJoin(ctx, name, jcmd, kotsAPIAddress) + cidrCfg, err := initializeJoin(ctx, name, rc, jcmd, kotsAPIAddress) if err != nil { return fmt.Errorf("unable to initialize join: %w", err) } logrus.Debugf("running join preflights") - if err := runJoinPreflights(ctx, jcmd, flags, cidrCfg, metricsReporter); err != nil { + if err := runJoinPreflights(ctx, jcmd, flags, rc, cidrCfg, joinReporter.reporter); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } @@ -176,7 +177,7 @@ func runJoin(ctx context.Context, name string, flags JoinCmdFlags, jcmd *join.Jo logrus.Debugf("installing and joining cluster") loading := spinner.Start() loading.Infof("Installing node") - if err := installAndJoinCluster(ctx, jcmd, name, flags, isWorker); err != nil { + if err := installAndJoinCluster(ctx, rc, jcmd, name, flags, isWorker); err != nil { loading.ErrorClosef("Failed to install node") return err } @@ -214,7 +215,7 @@ func runJoin(ctx context.Context, name string, flags JoinCmdFlags, jcmd *join.Jo return nil } - if err := maybeEnableHA(ctx, kcli, mcli, flags, cidrCfg.ServiceCIDR, jcmd); err != nil { + if err := maybeEnableHA(ctx, kcli, mcli, flags, rc, cidrCfg.ServiceCIDR, jcmd); err != nil { return fmt.Errorf("unable to enable high availability: %w", err) } @@ -222,27 +223,27 @@ func runJoin(ctx context.Context, name string, flags JoinCmdFlags, jcmd *join.Jo return nil } -func runJoinVerifyAndPrompt(name string, flags JoinCmdFlags, jcmd *join.JoinCommandResponse) error { +func runJoinVerifyAndPrompt(name string, flags JoinCmdFlags, rc runtimeconfig.RuntimeConfig, jcmd *join.JoinCommandResponse) error { logrus.Debugf("checking if k0s is already installed") err := verifyNoInstallation(name, "join a node") if err != nil { return err } - runtimeconfig.Set(jcmd.InstallationSpec.RuntimeConfig) + rc.Set(jcmd.InstallationSpec.RuntimeConfig) isWorker := !strings.Contains(jcmd.K0sJoinCommand, "controller") if isWorker { - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeletConfig()) + os.Setenv("KUBECONFIG", rc.PathToKubeletConfig()) } else { - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeConfig()) + os.Setenv("KUBECONFIG", rc.PathToKubeConfig()) } - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) - if err := runtimeconfig.WriteToDisk(); err != nil { + if err := rc.WriteToDisk(); err != nil { return fmt.Errorf("unable to write runtime config: %w", err) } - if err := os.Chmod(runtimeconfig.EmbeddedClusterHomeDirectory(), 0755); err != nil { + if err := os.Chmod(rc.EmbeddedClusterHomeDirectory(), 0755); err != nil { // don't fail as there are cases where we can't change the permissions (bind mounts, selinux, etc...), // and we handle and surface those errors to the user later (host preflights, checking exec errors, etc...) logrus.Debugf("unable to chmod embedded-cluster home dir: %s", err) @@ -278,7 +279,7 @@ func runJoinVerifyAndPrompt(name string, flags JoinCmdFlags, jcmd *join.JoinComm return nil } -func initializeJoin(ctx context.Context, name string, jcmd *join.JoinCommandResponse, kotsAPIAddress string) (cidrCfg *newconfig.CIDRConfig, err error) { +func initializeJoin(ctx context.Context, name string, rc runtimeconfig.RuntimeConfig, jcmd *join.JoinCommandResponse, kotsAPIAddress string) (cidrCfg *newconfig.CIDRConfig, err error) { logrus.Info("") spinner := spinner.Start() spinner.Infof("Initializing") @@ -294,29 +295,29 @@ func initializeJoin(ctx context.Context, name string, jcmd *join.JoinCommandResp // this does not return an error - it returns the previous umask _ = syscall.Umask(0o022) - if err := os.Chmod(runtimeconfig.EmbeddedClusterHomeDirectory(), 0755); err != nil { + if err := os.Chmod(rc.EmbeddedClusterHomeDirectory(), 0755); err != nil { // don't fail as there are cases where we can't change the permissions (bind mounts, selinux, etc...), // and we handle and surface those errors to the user later (host preflights, checking exec errors, etc...) logrus.Debugf("unable to chmod embedded-cluster home dir: %s", err) } logrus.Debugf("materializing %s binaries", name) - if err := materializeFilesForJoin(ctx, jcmd, kotsAPIAddress); err != nil { + if err := materializeFilesForJoin(ctx, rc, jcmd, kotsAPIAddress); err != nil { return nil, fmt.Errorf("failed to materialize files: %w", err) } logrus.Debugf("configuring sysctl") - if err := configutils.ConfigureSysctl(); err != nil { + if err := hostutils.ConfigureSysctl(); err != nil { logrus.Debugf("unable to configure sysctl: %v", err) } logrus.Debugf("configuring kernel modules") - if err := configutils.ConfigureKernelModules(); err != nil { + if err := hostutils.ConfigureKernelModules(); err != nil { logrus.Debugf("unable to configure kernel modules: %v", err) } logrus.Debugf("configuring network manager") - if err := configureNetworkManager(ctx); err != nil { + if err := hostutils.ConfigureNetworkManager(ctx, rc); err != nil { return nil, fmt.Errorf("unable to configure network manager: %w", err) } @@ -326,24 +327,24 @@ func initializeJoin(ctx context.Context, name string, jcmd *join.JoinCommandResp } logrus.Debugf("configuring firewalld") - if err := configureFirewalld(ctx, cidrCfg.PodCIDR, cidrCfg.ServiceCIDR); err != nil { + if err := hostutils.ConfigureFirewalld(ctx, cidrCfg.PodCIDR, cidrCfg.ServiceCIDR); err != nil { logrus.Debugf("unable to configure firewalld: %v", err) } return cidrCfg, nil } -func materializeFilesForJoin(ctx context.Context, jcmd *join.JoinCommandResponse, kotsAPIAddress string) error { - materializer := goods.NewMaterializer() +func materializeFilesForJoin(ctx context.Context, rc runtimeconfig.RuntimeConfig, jcmd *join.JoinCommandResponse, kotsAPIAddress string) error { + materializer := goods.NewMaterializer(rc) if err := materializer.Materialize(); err != nil { return fmt.Errorf("materialize binaries: %w", err) } - if err := support.MaterializeSupportBundleSpec(); err != nil { + if err := support.MaterializeSupportBundleSpec(rc); err != nil { return fmt.Errorf("materialize support bundle spec: %w", err) } if jcmd.InstallationSpec.AirGap { - if err := airgap.FetchAndWriteArtifacts(ctx, kotsAPIAddress); err != nil { + if err := airgap.FetchAndWriteArtifacts(ctx, kotsAPIAddress, rc); err != nil { return fmt.Errorf("failed to fetch artifacts: %w", err) } } @@ -372,14 +373,14 @@ func getJoinCIDRConfig(jcmd *join.JoinCommandResponse) (*newconfig.CIDRConfig, e }, nil } -func installAndJoinCluster(ctx context.Context, jcmd *join.JoinCommandResponse, name string, flags JoinCmdFlags, isWorker bool) error { +func installAndJoinCluster(ctx context.Context, rc runtimeconfig.RuntimeConfig, jcmd *join.JoinCommandResponse, name string, flags JoinCmdFlags, isWorker bool) error { logrus.Debugf("saving token to disk") if err := saveTokenToDisk(jcmd.K0sToken); err != nil { return fmt.Errorf("unable to save token to disk: %w", err) } logrus.Debugf("installing %s binaries", name) - if err := installK0sBinary(); err != nil { + if err := installK0sBinary(rc); err != nil { return fmt.Errorf("unable to install k0s binary: %w", err) } @@ -390,7 +391,7 @@ func installAndJoinCluster(ctx context.Context, jcmd *join.JoinCommandResponse, } logrus.Debugf("creating systemd unit files") - if err := createSystemdUnitFiles(ctx, isWorker, jcmd.InstallationSpec.Proxy); err != nil { + if err := createSystemdUnitFiles(ctx, rc, isWorker, jcmd.InstallationSpec.Proxy); err != nil { return fmt.Errorf("unable to create systemd unit files: %w", err) } @@ -410,7 +411,7 @@ func installAndJoinCluster(ctx context.Context, jcmd *join.JoinCommandResponse, } logrus.Debugf("joining node to cluster") - if err := runK0sInstallCommand(flags.networkInterface, jcmd.K0sJoinCommand, profile); err != nil { + if err := runK0sInstallCommand(rc, flags.networkInterface, jcmd.K0sJoinCommand, profile); err != nil { return fmt.Errorf("unable to join node to cluster: %w", err) } @@ -434,9 +435,9 @@ func saveTokenToDisk(token string) error { } // installK0sBinary moves the embedded k0s binary to its destination. -func installK0sBinary() error { - ourbin := runtimeconfig.PathToEmbeddedClusterBinary("k0s") - hstbin := runtimeconfig.K0sBinaryPath() +func installK0sBinary(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) } @@ -469,7 +470,7 @@ func applyNetworkConfiguration(networkInterface string, jcmd *join.JoinCommandRe if err != nil { return fmt.Errorf("unable to marshal cluster spec: %w", err) } - err = os.WriteFile(runtimeconfig.PathToK0sConfig(), clusterSpecYaml, 0644) + err = os.WriteFile(runtimeconfig.K0sConfigPath, clusterSpecYaml, 0644) if err != nil { return fmt.Errorf("unable to write cluster spec to /etc/k0s/k0s.yaml: %w", err) } @@ -480,7 +481,7 @@ func applyNetworkConfiguration(networkInterface string, jcmd *join.JoinCommandRe // startAndWaitForK0s starts the k0s service and waits for the node to be ready. func startAndWaitForK0s(name string) error { logrus.Debugf("starting %s service", name) - if _, err := helpers.RunCommand(runtimeconfig.K0sBinaryPath(), "start"); err != nil { + if _, err := helpers.RunCommand(runtimeconfig.K0sBinaryPath, "start"); err != nil { return fmt.Errorf("unable to start service: %w", err) } @@ -503,7 +504,7 @@ func applyJoinConfigurationOverrides(jcmd *join.JoinCommandResponse) error { if data, err := yaml.Marshal(patch); err != nil { return fmt.Errorf("unable to marshal embedded overrides: %w", err) } else if err := k0s.PatchK0sConfig( - runtimeconfig.PathToK0sConfig(), string(data), + runtimeconfig.K0sConfigPath, string(data), ); err != nil { return fmt.Errorf("unable to patch config with embedded data: %w", err) } @@ -516,7 +517,7 @@ func applyJoinConfigurationOverrides(jcmd *join.JoinCommandResponse) error { if data, err := yaml.Marshal(patch); err != nil { return fmt.Errorf("unable to marshal embedded overrides: %w", err) } else if err := k0s.PatchK0sConfig( - runtimeconfig.PathToK0sConfig(), string(data), + runtimeconfig.K0sConfigPath, string(data), ); err != nil { return fmt.Errorf("unable to patch config with embedded data: %w", err) } @@ -524,7 +525,7 @@ func applyJoinConfigurationOverrides(jcmd *join.JoinCommandResponse) error { } func getFirstDefinedProfile() (string, error) { - k0scfg, err := os.Open(runtimeconfig.PathToK0sConfig()) + k0scfg, err := os.Open(runtimeconfig.K0sConfigPath) if err != nil { return "", fmt.Errorf("unable to open k0s config: %w", err) } @@ -540,7 +541,7 @@ func getFirstDefinedProfile() (string, error) { } // runK0sInstallCommand runs the k0s install command as provided by the kots -func runK0sInstallCommand(networkInterface string, fullcmd string, profile string) error { +func runK0sInstallCommand(rc runtimeconfig.RuntimeConfig, networkInterface string, fullcmd string, profile string) error { args := strings.Split(fullcmd, " ") args = append(args, "--token-file", "/etc/k0s/join-token") @@ -553,7 +554,7 @@ func runK0sInstallCommand(networkInterface string, fullcmd string, profile strin args = append(args, "--profile", profile) } - args = append(args, config.AdditionalInstallFlags(nodeIP)...) + args = append(args, config.AdditionalInstallFlags(rc, nodeIP)...) if strings.Contains(fullcmd, "controller") { args = append(args, config.AdditionalInstallFlagsController()...) @@ -572,7 +573,7 @@ func waitForNodeToJoin(ctx context.Context, kcli client.Client, hostname string, return nil } -func maybeEnableHA(ctx context.Context, kcli client.Client, mcli metadata.Interface, flags JoinCmdFlags, serviceCIDR string, jcmd *join.JoinCommandResponse) error { +func maybeEnableHA(ctx context.Context, kcli client.Client, mcli metadata.Interface, flags JoinCmdFlags, rc runtimeconfig.RuntimeConfig, serviceCIDR string, jcmd *join.JoinCommandResponse) error { if flags.noHA { logrus.Debug("--no-ha flag provided, skipping high availability") return nil @@ -615,10 +616,10 @@ func maybeEnableHA(ctx context.Context, kcli client.Client, mcli metadata.Interf airgapChartsPath := "" if jcmd.InstallationSpec.AirGap { - airgapChartsPath = runtimeconfig.EmbeddedClusterChartsSubDir() + airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: runtimeconfig.PathToKubeConfig(), + KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, }) @@ -637,6 +638,7 @@ func maybeEnableHA(ctx context.Context, kcli client.Client, mcli metadata.Interf mcli, kclient, hcli, + rc, serviceCIDR, jcmd.InstallationSpec, loading, diff --git a/cmd/installer/cli/join_printcommand.go b/cmd/installer/cli/join_printcommand.go index de1a6c7eb..4ca997209 100644 --- a/cmd/installer/cli/join_printcommand.go +++ b/cmd/installer/cli/join_printcommand.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/spf13/cobra" ) @@ -13,7 +14,8 @@ func JoinPrintCommandCmd(ctx context.Context, name string) *cobra.Command { Use: "print-command", Short: fmt.Sprintf("Print controller join command for %s", name), RunE: func(cmd *cobra.Command, args []string) error { - jcmd, err := kotscli.GetJoinCommand(cmd.Context()) + rc := runtimeconfig.New(nil) + jcmd, err := kotscli.GetJoinCommand(cmd.Context(), rc) if err != nil { return fmt.Errorf("unable to get join command: %w", err) } diff --git a/cmd/installer/cli/join_runpreflights.go b/cmd/installer/cli/join_runpreflights.go index 0cbe1bbf4..170b74b9e 100644 --- a/cmd/installer/cli/join_runpreflights.go +++ b/cmd/installer/cli/join_runpreflights.go @@ -7,11 +7,12 @@ import ( "github.com/replicatedhq/embedded-cluster/kinds/types/join" newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config" - "github.com/replicatedhq/embedded-cluster/pkg/configutils" + "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" + "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" "github.com/replicatedhq/embedded-cluster/pkg/kotsadm" - "github.com/replicatedhq/embedded-cluster/pkg/netutil" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/netutils" - "github.com/replicatedhq/embedded-cluster/pkg/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -19,6 +20,7 @@ import ( func JoinRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { var flags JoinCmdFlags + rc := runtimeconfig.New(nil) cmd := &cobra.Command{ Use: "run-preflights", @@ -32,7 +34,7 @@ func JoinRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { logrus.Debugf("fetching join token remotely") @@ -40,7 +42,7 @@ func JoinRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { if err != nil { return fmt.Errorf("unable to get join token: %w", err) } - if err := runJoinRunPreflights(cmd.Context(), name, flags, jcmd, args[0]); err != nil { + if err := runJoinRunPreflights(cmd.Context(), name, flags, rc, jcmd, args[0]); err != nil { return err } @@ -55,23 +57,23 @@ func JoinRunPreflightsCmd(ctx context.Context, name string) *cobra.Command { return cmd } -func runJoinRunPreflights(ctx context.Context, name string, flags JoinCmdFlags, jcmd *join.JoinCommandResponse, kotsAPIAddress string) error { - if err := runJoinVerifyAndPrompt(name, flags, jcmd); err != nil { +func runJoinRunPreflights(ctx context.Context, name string, flags JoinCmdFlags, rc runtimeconfig.RuntimeConfig, jcmd *join.JoinCommandResponse, kotsAPIAddress string) error { + if err := runJoinVerifyAndPrompt(name, flags, rc, jcmd); err != nil { return err } logrus.Debugf("materializing %s binaries", name) - if err := materializeFilesForJoin(ctx, jcmd, kotsAPIAddress); err != nil { + if err := materializeFilesForJoin(ctx, rc, jcmd, kotsAPIAddress); err != nil { return fmt.Errorf("failed to materialize files: %w", err) } logrus.Debugf("configuring sysctl") - if err := configutils.ConfigureSysctl(); err != nil { + if err := hostutils.ConfigureSysctl(); err != nil { logrus.Debugf("unable to configure sysctl: %v", err) } logrus.Debugf("configuring kernel modules") - if err := configutils.ConfigureKernelModules(); err != nil { + if err := hostutils.ConfigureKernelModules(); err != nil { logrus.Debugf("unable to configure kernel modules: %v", err) } @@ -81,7 +83,7 @@ func runJoinRunPreflights(ctx context.Context, name string, flags JoinCmdFlags, } logrus.Debugf("running join preflights") - if err := runJoinPreflights(ctx, jcmd, flags, cidrCfg, nil); err != nil { + if err := runJoinPreflights(ctx, jcmd, flags, rc, cidrCfg, nil); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } @@ -93,7 +95,7 @@ func runJoinRunPreflights(ctx context.Context, name string, flags JoinCmdFlags, return nil } -func runJoinPreflights(ctx context.Context, jcmd *join.JoinCommandResponse, flags JoinCmdFlags, cidrCfg *newconfig.CIDRConfig, metricsReported preflights.MetricsReporter) error { +func runJoinPreflights(ctx context.Context, jcmd *join.JoinCommandResponse, flags JoinCmdFlags, rc runtimeconfig.RuntimeConfig, cidrCfg *newconfig.CIDRConfig, metricsReporter metrics.ReporterInterface) error { nodeIP, err := netutils.FirstValidAddress(flags.networkInterface) if err != nil { return fmt.Errorf("unable to find first valid address: %w", err) @@ -101,20 +103,28 @@ func runJoinPreflights(ctx context.Context, jcmd *join.JoinCommandResponse, flag domains := runtimeconfig.GetDomains(jcmd.InstallationSpec.Config) - if err := preflights.PrepareAndRun(ctx, preflights.PrepareAndRunOptions{ - ReplicatedAppURL: netutil.MaybeAddHTTPS(domains.ReplicatedAppDomain), - ProxyRegistryURL: netutil.MaybeAddHTTPS(domains.ProxyRegistryDomain), - Proxy: jcmd.InstallationSpec.Proxy, - PodCIDR: cidrCfg.PodCIDR, - ServiceCIDR: cidrCfg.ServiceCIDR, - NodeIP: nodeIP, - IsAirgap: jcmd.InstallationSpec.AirGap, - SkipHostPreflights: flags.skipHostPreflights, - IgnoreHostPreflights: flags.ignoreHostPreflights, - AssumeYes: flags.assumeYes, - TCPConnectionsRequired: jcmd.TCPConnectionsRequired, - IsJoin: true, - }); err != nil { + hpf, err := preflights.Prepare(ctx, preflights.PrepareOptions{ + HostPreflightSpec: release.GetHostPreflights(), + ReplicatedAppURL: netutils.MaybeAddHTTPS(domains.ReplicatedAppDomain), + ProxyRegistryURL: netutils.MaybeAddHTTPS(domains.ProxyRegistryDomain), + AdminConsolePort: rc.AdminConsolePort(), + LocalArtifactMirrorPort: rc.LocalArtifactMirrorPort(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), + Proxy: jcmd.InstallationSpec.Proxy, + PodCIDR: cidrCfg.PodCIDR, + ServiceCIDR: cidrCfg.ServiceCIDR, + NodeIP: nodeIP, + IsAirgap: jcmd.InstallationSpec.AirGap, + TCPConnectionsRequired: jcmd.TCPConnectionsRequired, + IsJoin: true, + }) + if err != nil { + return err + } + + if err := runHostPreflights(ctx, hpf, jcmd.InstallationSpec.Proxy, rc, flags.skipHostPreflights, flags.ignoreHostPreflights, flags.assumeYes, metricsReporter); err != nil { return err } diff --git a/cmd/installer/cli/materialize.go b/cmd/installer/cli/materialize.go index fb4796eff..1a2495149 100644 --- a/cmd/installer/cli/materialize.go +++ b/cmd/installer/cli/materialize.go @@ -12,9 +12,8 @@ import ( ) func MaterializeCmd(ctx context.Context, name string) *cobra.Command { - var ( - dataDir string - ) + var dataDir string + rc := runtimeconfig.New(nil) cmd := &cobra.Command{ Use: "materialize", @@ -25,16 +24,16 @@ func MaterializeCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("materialize command must be run as root") } - runtimeconfig.SetDataDir(dataDir) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + rc.SetDataDir(dataDir) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - materializer := goods.NewMaterializer() + materializer := goods.NewMaterializer(rc) if err := materializer.Materialize(); err != nil { return fmt.Errorf("unable to materialize: %v", err) } diff --git a/cmd/installer/cli/metrics.go b/cmd/installer/cli/metrics.go index 861735fb8..e40aaeec7 100644 --- a/cmd/installer/cli/metrics.go +++ b/cmd/installer/cli/metrics.go @@ -5,18 +5,18 @@ import ( "os" "github.com/google/uuid" + apitypes "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg/metrics" - preflightstypes "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" "github.com/spf13/pflag" ) type InstallReporter struct { - reporter *metrics.Reporter + reporter metrics.ReporterInterface licenseID string appSlug string } -func NewInstallReporter(baseURL string, clusterID uuid.UUID, cmd string, args []string, licenseID string, appSlug string) *InstallReporter { +func newInstallReporter(baseURL string, clusterID uuid.UUID, cmd string, args []string, licenseID string, appSlug string) *InstallReporter { executionID := uuid.New().String() reporter := metrics.NewReporter(executionID, baseURL, clusterID, cmd, args) return &InstallReporter{ @@ -38,11 +38,11 @@ func (r *InstallReporter) ReportInstallationFailed(ctx context.Context, err erro r.reporter.ReportInstallationFailed(ctx, err) } -func (r *InstallReporter) ReportPreflightsFailed(ctx context.Context, output preflightstypes.Output) { +func (r *InstallReporter) ReportPreflightsFailed(ctx context.Context, output *apitypes.HostPreflightOutput) { r.reporter.ReportPreflightsFailed(ctx, output) } -func (r *InstallReporter) ReportPreflightsBypassed(ctx context.Context, output preflightstypes.Output) { +func (r *InstallReporter) ReportPreflightsBypassed(ctx context.Context, output *apitypes.HostPreflightOutput) { r.reporter.ReportPreflightsBypassed(ctx, output) } @@ -51,10 +51,10 @@ func (r *InstallReporter) ReportSignalAborted(ctx context.Context, sig os.Signal } type JoinReporter struct { - reporter *metrics.Reporter + reporter metrics.ReporterInterface } -func NewJoinReporter(baseURL string, clusterID uuid.UUID, cmd string, flags []string) *JoinReporter { +func newJoinReporter(baseURL string, clusterID uuid.UUID, cmd string, flags []string) *JoinReporter { executionID := uuid.New().String() reporter := metrics.NewReporter(executionID, baseURL, clusterID, cmd, flags) return &JoinReporter{ @@ -74,11 +74,11 @@ func (r *JoinReporter) ReportJoinFailed(ctx context.Context, err error) { r.reporter.ReportJoinFailed(ctx, err) } -func (r *JoinReporter) ReportPreflightsFailed(ctx context.Context, output preflightstypes.Output) { +func (r *JoinReporter) ReportPreflightsFailed(ctx context.Context, output *apitypes.HostPreflightOutput) { r.reporter.ReportPreflightsFailed(ctx, output) } -func (r *JoinReporter) ReportPreflightsBypassed(ctx context.Context, output preflightstypes.Output) { +func (r *JoinReporter) ReportPreflightsBypassed(ctx context.Context, output *apitypes.HostPreflightOutput) { r.reporter.ReportPreflightsBypassed(ctx, output) } diff --git a/cmd/installer/cli/preflights.go b/cmd/installer/cli/preflights.go new file mode 100644 index 000000000..a7fa69f24 --- /dev/null +++ b/cmd/installer/cli/preflights.go @@ -0,0 +1,154 @@ +package cli + +import ( + "context" + "fmt" + + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/dryrun" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" + "github.com/replicatedhq/embedded-cluster/pkg/prompts" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/replicatedhq/embedded-cluster/pkg/spinner" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/sirupsen/logrus" +) + +func runHostPreflights( + ctx context.Context, + hpf *troubleshootv1beta2.HostPreflightSpec, + proxy *ecv1beta1.ProxySpec, + rc runtimeconfig.RuntimeConfig, + skipHostPreflights bool, + ignoreHostPreflights bool, + assumeYes bool, + metricsReporter metrics.ReporterInterface, +) error { + if dryrun.Enabled() { + dryrun.RecordHostPreflightSpec(hpf) + return nil + } + + if len(hpf.Collectors) == 0 && len(hpf.Analyzers) == 0 { + return nil + } + + spinner := spinner.Start() + + if skipHostPreflights { + spinner.Closef("Host preflights skipped") + return nil + } + + spinner.Infof("Running host preflights") + + output, stderr, err := preflights.Run(ctx, hpf, proxy, rc) + if err != nil { + spinner.ErrorClosef("Failed to run host preflights") + return fmt.Errorf("host preflights failed to run: %w", err) + } + if stderr != "" { + logrus.Debugf("preflight stderr: %s", stderr) + } + + err = preflights.SaveToDisk(output, rc.PathToEmbeddedClusterSupportFile("host-preflight-results.json")) + if err != nil { + logrus.Warnf("save preflights output: %v", err) + } + + err = preflights.CopyBundleTo(rc.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz")) + if err != nil { + logrus.Warnf("copy preflight bundle to embedded-cluster support dir: %v", err) + } + + // Failures found + if output.HasFail() { + s := "preflights" + if len(output.Fail) == 1 { + s = "preflight" + } + + if output.HasWarn() { + spinner.ErrorClosef("%d host %s failed and %d warned", len(output.Fail), s, len(output.Warn)) + } else { + spinner.ErrorClosef("%d host %s failed", len(output.Fail), s) + } + + preflights.PrintTableWithoutInfo(output) + + if ignoreHostPreflights { + if assumeYes { + if metricsReporter != nil { + metricsReporter.ReportPreflightsBypassed(ctx, output) + } + return nil + } + confirmed, err := prompts.New().Confirm("Are you sure you want to ignore these failures and continue installing?", false) + if err != nil { + return fmt.Errorf("failed to get confirmation: %w", err) + } + if confirmed { + if metricsReporter != nil { + metricsReporter.ReportPreflightsBypassed(ctx, output) + } + return nil // user continued after host preflights failed + } + } + + if len(output.Fail)+len(output.Warn) > 1 { + logrus.Info("\n\033[1mPlease address these issues and try again.\033[0m\n") + } else { + logrus.Info("\n\033[1mPlease address this issue and try again.\033[0m\n") + } + + if metricsReporter != nil { + metricsReporter.ReportPreflightsFailed(ctx, output) + } + return ErrPreflightsHaveFail + } + + // Warnings found + if output.HasWarn() { + s := "preflights" + if len(output.Warn) == 1 { + s = "preflight" + } + + spinner.Warnf("%d host %s warned", len(output.Warn), s) + spinner.Close() + if assumeYes { + // We have warnings but we are not in interactive mode + // so we just print the warnings and continue + preflights.PrintTableWithoutInfo(output) + if metricsReporter != nil { + metricsReporter.ReportPreflightsBypassed(ctx, output) + } + return nil + } + + preflights.PrintTableWithoutInfo(output) + + confirmed, err := prompts.New().Confirm("Do you want to continue?", false) + if err != nil { + return fmt.Errorf("failed to get confirmation: %w", err) + } + if !confirmed { + if metricsReporter != nil { + metricsReporter.ReportPreflightsFailed(ctx, output) + } + return ErrPreflightsHaveFail + } + + if metricsReporter != nil { + metricsReporter.ReportPreflightsBypassed(ctx, output) + } + return nil + } + + // No failures or warnings + spinner.Infof("Host preflights passed") + spinner.Close() + + return nil +} diff --git a/cmd/installer/cli/reset.go b/cmd/installer/cli/reset.go index bb5e919b0..9f101f135 100644 --- a/cmd/installer/cli/reset.go +++ b/cmd/installer/cli/reset.go @@ -12,6 +12,7 @@ import ( autopilot "github.com/k0sproject/k0s/pkg/apis/autopilot/v1beta2" "github.com/k0sproject/k0s/pkg/etcd" + "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/config" "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/k0s" @@ -48,6 +49,8 @@ func ResetCmd(ctx context.Context, name string) *cobra.Command { assumeYes bool ) + var rc runtimeconfig.RuntimeConfig + cmd := &cobra.Command{ Use: "reset", Short: fmt.Sprintf("Remove %s from the current node", name), @@ -56,17 +59,17 @@ func ResetCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("reset command must be run as root") } - rcutil.InitBestRuntimeConfig(cmd.Context()) + rc = rcutil.InitBestRuntimeConfig(cmd.Context()) - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeConfig()) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("KUBECONFIG", rc.PathToKubeConfig()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) return nil }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - if err := maybePrintHAWarning(ctx); err != nil && !force { + if err := maybePrintHAWarning(ctx, rc); err != nil && !force { return err } @@ -136,18 +139,18 @@ func ResetCmd(ctx context.Context, name string) *cobra.Command { // reset logrus.Infof("Resetting node...") - err = stopAndResetK0s(runtimeconfig.EmbeddedClusterK0sSubDir()) + err = stopAndResetK0s(rc.EmbeddedClusterK0sSubDir()) if err != nil { logrus.Warnf("Failed to stop and reset k0s (continuing with reset anyway): %v", err) } logrus.Debugf("Resetting firewalld...") - err = resetFirewalld(ctx) + err = hostutils.ResetFirewalld(ctx) if !checkErrPrompt(assumeYes, force, err) { return fmt.Errorf("failed to reset firewalld: %w", err) } - if err := helpers.RemoveAll(runtimeconfig.PathToK0sConfig()); err != nil { + if err := helpers.RemoveAll(runtimeconfig.K0sConfigPath); err != nil { return fmt.Errorf("failed to remove k0s config: %w", err) } @@ -179,7 +182,7 @@ func ResetCmd(ctx context.Context, name string) *cobra.Command { // Now that k0s is nested under the data directory, we see the following error in the // dev environment because k0s is mounted in the docker container: // "failed to remove embedded cluster directory: remove k0s: unlinkat /var/lib/embedded-cluster/k0s: device or resource busy" - if err := helpers.RemoveAll(runtimeconfig.EmbeddedClusterHomeDirectory()); err != nil { + if err := helpers.RemoveAll(rc.EmbeddedClusterHomeDirectory()); err != nil { logrus.Debugf("Failed to remove embedded cluster directory: %v", err) } @@ -187,7 +190,7 @@ func ResetCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("failed to remove logs directory: %w", err) } - if err := helpers.RemoveAll(runtimeconfig.EmbeddedClusterOpenEBSLocalSubDir()); err != nil { + if err := helpers.RemoveAll(rc.EmbeddedClusterOpenEBSLocalSubDir()); err != nil { return fmt.Errorf("failed to remove openebs storage: %w", err) } @@ -199,7 +202,7 @@ func ResetCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("failed to remove k0s binary: %w", err) } - if err := helpers.RemoveAll(runtimeconfig.PathToECConfig()); err != nil { + if err := helpers.RemoveAll(runtimeconfig.ECConfigPath); err != nil { return fmt.Errorf("failed to remove embedded cluster data config: %w", err) } @@ -250,8 +253,8 @@ func checkErrPrompt(noPrompt bool, force bool, err error) bool { // maybePrintHAWarning prints a warning message when the user is running a reset a node // in a high availability cluster and there are only 3 control nodes. -func maybePrintHAWarning(ctx context.Context) error { - kubeconfig := runtimeconfig.PathToKubeConfig() +func maybePrintHAWarning(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + kubeconfig := rc.PathToKubeConfig() if _, err := os.Stat(kubeconfig); err != nil { return nil } diff --git a/cmd/installer/cli/reset_firewalld.go b/cmd/installer/cli/reset_firewalld.go index ace24375a..e7e5babc3 100644 --- a/cmd/installer/cli/reset_firewalld.go +++ b/cmd/installer/cli/reset_firewalld.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util" "github.com/sirupsen/logrus" @@ -12,6 +13,8 @@ import ( ) func ResetFirewalldCmd(ctx context.Context, name string) *cobra.Command { + var rc runtimeconfig.RuntimeConfig + cmd := &cobra.Command{ Use: "firewalld", Short: "Remove %s firewalld configuration from the current node", @@ -21,15 +24,15 @@ func ResetFirewalldCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("reset firewalld command must be run as root") } - rcutil.InitBestRuntimeConfig(cmd.Context()) + rc = rcutil.InitBestRuntimeConfig(cmd.Context()) - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeConfig()) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("KUBECONFIG", rc.PathToKubeConfig()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) return nil }, RunE: func(cmd *cobra.Command, args []string) error { - err := resetFirewalld(cmd.Context()) + err := hostutils.ResetFirewalld(cmd.Context()) if err != nil { return fmt.Errorf("failed to reset firewalld: %w", err) } diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index 7e59f2d5c..ec8ac5396 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -20,9 +20,10 @@ import ( k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "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/preflights" "github.com/replicatedhq/embedded-cluster/pkg/addons" "github.com/replicatedhq/embedded-cluster/pkg/airgap" - "github.com/replicatedhq/embedded-cluster/pkg/configutils" "github.com/replicatedhq/embedded-cluster/pkg/constants" "github.com/replicatedhq/embedded-cluster/pkg/disasterrecovery" "github.com/replicatedhq/embedded-cluster/pkg/extensions" @@ -30,7 +31,6 @@ import ( "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/preflights" "github.com/replicatedhq/embedded-cluster/pkg/prompts" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -90,21 +90,23 @@ func RestoreCmd(ctx context.Context, name string) *cobra.Command { var s3Store s3BackupStore var skipStoreValidation bool + rc := runtimeconfig.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); err != nil { + if err := preRunInstall(cmd, &flags, rc); err != nil { return err } return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - if err := runRestore(cmd.Context(), name, flags, s3Store, skipStoreValidation); err != nil { + if err := runRestore(cmd.Context(), name, flags, rc, s3Store, skipStoreValidation); err != nil { return err } @@ -122,7 +124,7 @@ func RestoreCmd(ctx context.Context, name string) *cobra.Command { return cmd } -func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store s3BackupStore, skipStoreValidation bool) error { +func runRestore(ctx context.Context, name string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, s3Store s3BackupStore, skipStoreValidation bool) error { err := verifyChannelRelease("restore", flags.isAirgap, flags.assumeYes) if err != nil { return err @@ -155,7 +157,7 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store if state != ecRestoreStateNew { logrus.Debugf("getting backup from restore state") var err error - backupToRestore, err = getBackupFromRestoreState(ctx, flags.isAirgap) + backupToRestore, err = getBackupFromRestoreState(ctx, flags.isAirgap, rc) if err != nil { return fmt.Errorf("unable to resume: %w", err) } @@ -163,29 +165,29 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store completionTimestamp := backupToRestore.GetCompletionTimestamp().Format("2006-01-02 15:04:05 UTC") logrus.Infof("Resuming restore from backup %q (%s)\n", backupToRestore.GetName(), completionTimestamp) - if err := overrideRuntimeConfigFromBackup(flags.localArtifactMirrorPort, *backupToRestore); err != nil { + if err := overrideRuntimeConfigFromBackup(flags.localArtifactMirrorPort, *backupToRestore, rc); err != nil { return fmt.Errorf("unable to override runtime config from backup: %w", err) } } } // If the installation is available, we can further augment the runtime config from the installation. - rc, err := getRuntimeConfigFromInstallation(ctx) + rcSpec, err := getRuntimeConfigFromInstallation(ctx) if err != nil { logrus.Debugf( "Unable to get runtime config from installation, this is expected if the installation is not yet available (restore state=%s): %v", state, err, ) } else { - runtimeconfig.Set(rc) + rc.Set(rcSpec) } - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeConfig()) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("KUBECONFIG", rc.PathToKubeConfig()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) switch state { case ecRestoreStateNew: - err = runRestoreStepNew(ctx, name, flags, &s3Store, skipStoreValidation) + err = runRestoreStepNew(ctx, name, flags, rc, &s3Store, skipStoreValidation) if err != nil { return err } @@ -199,7 +201,7 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store return fmt.Errorf("unable to set restore state: %w", err) } - backup, ok, err := runRestoreStepConfirmBackup(ctx, flags) + backup, ok, err := runRestoreStepConfirmBackup(ctx, flags, rc) if err != nil { return err } else if !ok { @@ -216,7 +218,7 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreECInstall(ctx, backupToRestore) + err = runRestoreECInstall(ctx, rc, backupToRestore) if err != nil { return err } @@ -244,7 +246,7 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreWaitForNodes(ctx, flags, backupToRestore) + err = runRestoreWaitForNodes(ctx, flags, rc, backupToRestore) if err != nil { return err } @@ -286,7 +288,7 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreEnableAdminConsoleHA(ctx, flags, backupToRestore) + err = runRestoreEnableAdminConsoleHA(ctx, flags, rc, backupToRestore) if err != nil { return err } @@ -314,7 +316,7 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store return fmt.Errorf("unable to set restore state: %w", err) } - err = runRestoreExtensions(ctx, flags) + err = runRestoreExtensions(ctx, flags, rc) if err != nil { return err } @@ -340,7 +342,7 @@ func runRestore(ctx context.Context, name string, flags InstallCmdFlags, s3Store return nil } -func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, s3Store *s3BackupStore, skipStoreValidation bool) error { +func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, s3Store *s3BackupStore, skipStoreValidation bool) error { logrus.Debugf("checking if k0s is already installed") err := verifyNoInstallation(name, "restore") if err != nil { @@ -365,39 +367,39 @@ func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, } logrus.Debugf("configuring sysctl") - if err := configutils.ConfigureSysctl(); err != nil { + if err := hostutils.ConfigureSysctl(); err != nil { logrus.Debugf("unable to configure sysctl: %v", err) } logrus.Debugf("configuring kernel modules") - if err := configutils.ConfigureKernelModules(); err != nil { + if err := hostutils.ConfigureKernelModules(); err != nil { logrus.Debugf("unable to configure kernel modules: %v", err) } logrus.Debugf("configuring network manager") - if err := configureNetworkManager(ctx); err != nil { + if err := hostutils.ConfigureNetworkManager(ctx, rc); err != nil { return fmt.Errorf("unable to configure network manager: %w", err) } logrus.Debugf("configuring firewalld") - if err := configureFirewalld(ctx, flags.cidrCfg.PodCIDR, flags.cidrCfg.ServiceCIDR); err != nil { + if err := hostutils.ConfigureFirewalld(ctx, flags.cidrCfg.PodCIDR, flags.cidrCfg.ServiceCIDR); err != nil { logrus.Debugf("unable to configure firewalld: %v", err) } logrus.Debugf("materializing binaries") - if err := materializeFiles(flags.airgapBundle); err != nil { + if err := hostutils.MaterializeFiles(rc, flags.airgapBundle); err != nil { return fmt.Errorf("unable to materialize binaries: %w", err) } logrus.Debugf("running install preflights") - if err := runInstallPreflights(ctx, flags, nil); err != nil { + if err := runInstallPreflights(ctx, flags, rc, nil); err != nil { if errors.Is(err, preflights.ErrPreflightsHaveFail) { return NewErrorNothingElseToAdd(err) } return fmt.Errorf("unable to run install preflights: %w", err) } - _, err = installAndStartCluster(ctx, flags.networkInterface, flags.airgapBundle, flags.proxy, flags.cidrCfg, flags.overrides, nil) + _, err = installAndStartCluster(ctx, flags, rc, nil) if err != nil { return err } @@ -415,11 +417,11 @@ func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, airgapChartsPath := "" if flags.isAirgap { - airgapChartsPath = runtimeconfig.EmbeddedClusterChartsSubDir() + airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: runtimeconfig.PathToKubeConfig(), + KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, }) @@ -434,10 +436,10 @@ func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, // TODO (@salah): update installation status to reflect what's happening logrus.Debugf("installing addons") - if err := addons.Install(ctx, logrus.Debugf, hcli, addons.InstallOptions{ + if err := addons.Install(ctx, logrus.Debugf, hcli, rc, addons.InstallOptions{ IsAirgap: flags.airgapBundle != "", Proxy: flags.proxy, - HostCABundlePath: runtimeconfig.HostCABundlePath(), + HostCABundlePath: rc.HostCABundlePath(), ServiceCIDR: flags.cidrCfg.ServiceCIDR, IsRestore: true, EmbeddedConfigSpec: embCfgSpec, @@ -447,6 +449,7 @@ func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, logrus.Debugf("configuring velero backup storage location") if err := kotscli.VeleroConfigureOtherS3(kotscli.VeleroConfigureOtherS3Options{ + RuntimeConfig: rc, Endpoint: s3Store.endpoint, Region: s3Store.region, Bucket: s3Store.bucket, @@ -461,7 +464,7 @@ func runRestoreStepNew(ctx context.Context, name string, flags InstallCmdFlags, return nil } -func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags) (*disasterrecovery.ReplicatedBackup, bool, error) { +func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) (*disasterrecovery.ReplicatedBackup, bool, error) { kcli, err := kubeutils.KubeClient() if err != nil { return nil, false, fmt.Errorf("unable to create kube client: %w", err) @@ -473,7 +476,7 @@ func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags) (*d } logrus.Debugf("waiting for backups to become available") - backups, err := waitForBackups(ctx, os.Stdout, kcli, k0sCfg, flags.isAirgap) + backups, err := waitForBackups(ctx, os.Stdout, kcli, k0sCfg, rc, flags.isAirgap) if err != nil { return nil, false, err } @@ -497,19 +500,19 @@ func runRestoreStepConfirmBackup(ctx context.Context, flags InstallCmdFlags) (*d return backupToRestore, true, nil } -func runRestoreECInstall(ctx context.Context, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreECInstall(ctx context.Context, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { logrus.Debugf("restoring embedded cluster installation from backup %q", backupToRestore.GetName()) if err := restoreFromReplicatedBackup(ctx, *backupToRestore, disasterRecoveryComponentECInstall, true); err != nil { return fmt.Errorf("unable to restore from backup: %w", err) } logrus.Debugf("updating installation from backup %q", backupToRestore.GetName()) - if err := restoreReconcileInstallationFromRuntimeConfig(ctx); err != nil { + if err := restoreReconcileInstallationFromRuntimeConfig(ctx, rc); err != nil { return fmt.Errorf("unable to update installation from backup: %w", err) } logrus.Debugf("updating local artifact mirror service from backup %q", backupToRestore.GetName()) - if err := updateLocalArtifactMirrorService(); err != nil { + if err := updateLocalArtifactMirrorService(rc); err != nil { return fmt.Errorf("unable to update local artifact mirror service from backup: %w", err) } @@ -525,7 +528,7 @@ func runRestoreAdminConsole(ctx context.Context, backupToRestore *disasterrecove return nil } -func runRestoreWaitForNodes(ctx context.Context, flags InstallCmdFlags, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreWaitForNodes(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { logrus.Debugf("checking if backup is high availability") highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { @@ -534,14 +537,14 @@ func runRestoreWaitForNodes(ctx context.Context, flags InstallCmdFlags, backupTo logrus.Debugf("waiting for additional nodes to be added") - if err := waitForAdditionalNodes(ctx, highAvailability, flags.networkInterface); err != nil { + if err := waitForAdditionalNodes(ctx, highAvailability, flags.networkInterface, rc); err != nil { return err } return nil } -func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, backupToRestore *disasterrecovery.ReplicatedBackup) error { +func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig, backupToRestore *disasterrecovery.ReplicatedBackup) error { highAvailability, err := isHighAvailabilityReplicatedBackup(*backupToRestore) if err != nil { return err @@ -571,11 +574,11 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, airgapChartsPath := "" if flags.isAirgap { - airgapChartsPath = runtimeconfig.EmbeddedClusterChartsSubDir() + airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: runtimeconfig.PathToKubeConfig(), + KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, }) @@ -584,7 +587,7 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, } defer hcli.Close() - err = addons.EnableAdminConsoleHA(ctx, logrus.Debugf, kcli, mcli, hcli, flags.isAirgap, flags.cidrCfg.ServiceCIDR, flags.proxy, in.Spec.Config, in.Spec.LicenseInfo) + err = addons.EnableAdminConsoleHA(ctx, logrus.Debugf, kcli, mcli, hcli, rc, flags.isAirgap, flags.cidrCfg.ServiceCIDR, flags.proxy, in.Spec.Config, in.Spec.LicenseInfo) if err != nil { return err } @@ -643,14 +646,14 @@ func runRestoreECO(ctx context.Context, backupToRestore *disasterrecovery.Replic return nil } -func runRestoreExtensions(ctx context.Context, flags InstallCmdFlags) error { +func runRestoreExtensions(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.RuntimeConfig) error { airgapChartsPath := "" if flags.isAirgap { - airgapChartsPath = runtimeconfig.EmbeddedClusterChartsSubDir() + airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: runtimeconfig.PathToKubeConfig(), + KubeConfig: rc.PathToKubeConfig(), K0sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, }) @@ -827,7 +830,7 @@ func resetECRestoreState(ctx context.Context) error { // It returns an error if a backup is defined in the restore state but: // - is not found by Velero anymore. // - is not restorable by the current binary. -func getBackupFromRestoreState(ctx context.Context, isAirgap bool) (*disasterrecovery.ReplicatedBackup, error) { +func getBackupFromRestoreState(ctx context.Context, isAirgap bool, rc runtimeconfig.RuntimeConfig) (*disasterrecovery.ReplicatedBackup, error) { kcli, err := kubeutils.KubeClient() if err != nil { return nil, fmt.Errorf("unable to create kube client: %w", err) @@ -865,7 +868,7 @@ func getBackupFromRestoreState(ctx context.Context, isAirgap bool) (*disasterrec return nil, fmt.Errorf("unable to get k0s config from disk: %w", err) } - if restorable, reason := isReplicatedBackupRestorable(backup, rel, isAirgap, k0sCfg); !restorable { + if restorable, reason := isReplicatedBackupRestorable(backup, rel, isAirgap, k0sCfg, rc); !restorable { return nil, fmt.Errorf("backup %q %s", backup.GetName(), reason) } @@ -963,7 +966,7 @@ func validateS3BackupStore(s *s3BackupStore) error { return nil } -func isReplicatedBackupRestorable(backup disasterrecovery.ReplicatedBackup, rel *release.ChannelRelease, isAirgap bool, k0sCfg *k0sv1beta1.ClusterConfig) (bool, string) { +func isReplicatedBackupRestorable(backup disasterrecovery.ReplicatedBackup, rel *release.ChannelRelease, isAirgap bool, k0sCfg *k0sv1beta1.ClusterConfig, rc runtimeconfig.RuntimeConfig) (bool, string) { if backup.GetExpectedBackupCount() != len(backup) { return false, fmt.Sprintf("has a different number of backups (%d) than the expected number (%d)", len(backup), backup.GetExpectedBackupCount()) } @@ -981,7 +984,7 @@ func isReplicatedBackupRestorable(backup disasterrecovery.ReplicatedBackup, rel } for _, b := range backup { - restorable, reason := isBackupRestorable(&b, rel, isAirgap, k0sCfg) + restorable, reason := isBackupRestorable(&b, rel, isAirgap, k0sCfg, rc) if !restorable { return false, reason } @@ -989,7 +992,7 @@ func isReplicatedBackupRestorable(backup disasterrecovery.ReplicatedBackup, rel return true, "" } -func isBackupRestorable(backup *velerov1.Backup, rel *release.ChannelRelease, isAirgap bool, k0sCfg *k0sv1beta1.ClusterConfig) (bool, string) { +func isBackupRestorable(backup *velerov1.Backup, rel *release.ChannelRelease, isAirgap bool, k0sCfg *k0sv1beta1.ClusterConfig, rc runtimeconfig.RuntimeConfig) (bool, string) { if backup.Annotations[disasterrecovery.BackupIsECAnnotation] != "true" { return false, "is not an embedded cluster backup" } @@ -1059,7 +1062,7 @@ func isBackupRestorable(backup *velerov1.Backup, rel *release.ChannelRelease, is } } - if v := backup.Annotations["kots.io/embedded-cluster-data-dir"]; v != "" && v != runtimeconfig.EmbeddedClusterHomeDirectory() { + if v := backup.Annotations["kots.io/embedded-cluster-data-dir"]; v != "" && v != rc.EmbeddedClusterHomeDirectory() { return false, fmt.Sprintf("has a different data directory than the current cluster. Please rerun with '--data-dir %s'.", v) } @@ -1077,7 +1080,7 @@ func isHighAvailabilityReplicatedBackup(backup disasterrecovery.ReplicatedBackup // waitForBackups waits for backups to become available. // It returns a list of restorable backups, or an error if none are found. -func waitForBackups(ctx context.Context, out io.Writer, kcli client.Client, k0sCfg *k0sv1beta1.ClusterConfig, isAirgap bool) ([]disasterrecovery.ReplicatedBackup, error) { +func waitForBackups(ctx context.Context, out io.Writer, kcli client.Client, k0sCfg *k0sv1beta1.ClusterConfig, rc runtimeconfig.RuntimeConfig, isAirgap bool) ([]disasterrecovery.ReplicatedBackup, error) { loading := spinner.Start(spinner.WithWriter(func(format string, a ...any) (int, error) { return fmt.Fprintf(out, format, a...) })) @@ -1101,7 +1104,7 @@ func waitForBackups(ctx context.Context, out io.Writer, kcli client.Client, k0sC invalidReasons := []string{} for _, backup := range replicatedBackups { - restorable, reason := isReplicatedBackupRestorable(backup, rel, isAirgap, k0sCfg) + restorable, reason := isReplicatedBackupRestorable(backup, rel, isAirgap, k0sCfg, rc) if restorable { validBackups = append(validBackups, backup) } else { @@ -1166,7 +1169,7 @@ func pickBackupToRestore(backups []disasterrecovery.ReplicatedBackup) *disasterr // getK0sConfigFromDisk reads and returns the k0s config from disk. func getK0sConfigFromDisk() (*k0sv1beta1.ClusterConfig, error) { - cfgBytes, err := os.ReadFile(runtimeconfig.PathToK0sConfig()) + cfgBytes, err := os.ReadFile(runtimeconfig.K0sConfigPath) if err != nil { return nil, fmt.Errorf("unable to read k0s config file: %w", err) } @@ -1577,13 +1580,13 @@ func ensureImprovedDrMetadata(restore *velerov1.Restore, backup *velerov1.Backup } // waitForAdditionalNodes waits for for user to add additional nodes to the cluster. -func waitForAdditionalNodes(ctx context.Context, highAvailability bool, networkInterface string) error { +func waitForAdditionalNodes(ctx context.Context, highAvailability bool, networkInterface string, rc runtimeconfig.RuntimeConfig) error { kcli, err := kubeutils.KubeClient() if err != nil { return fmt.Errorf("unable to create kube client: %w", err) } - adminConsoleURL := getAdminConsoleURL("", networkInterface, runtimeconfig.AdminConsolePort()) + adminConsoleURL := getAdminConsoleURL("", networkInterface, rc.AdminConsolePort()) successColor := "\033[32m" colorReset := "\033[0m" @@ -1629,7 +1632,7 @@ func waitForAdditionalNodes(ctx context.Context, highAvailability bool, networkI // restoreReconcileInstallationFromRuntimeConfig will update the installation to match the runtime // config from the original installation. -func restoreReconcileInstallationFromRuntimeConfig(ctx context.Context) error { +func restoreReconcileInstallationFromRuntimeConfig(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { kcli, err := kubeutils.KubeClient() if err != nil { return fmt.Errorf("create kube client: %w", err) @@ -1645,7 +1648,7 @@ func restoreReconcileInstallationFromRuntimeConfig(ctx context.Context) error { } err = kubeutils.UpdateInstallation(ctx, kcli, in, func(in *ecv1beta1.Installation) { - in.Spec.RuntimeConfig.LocalArtifactMirror.Port = runtimeconfig.LocalArtifactMirrorPort() + in.Spec.RuntimeConfig.LocalArtifactMirror.Port = rc.LocalArtifactMirrorPort() }) if err != nil { return fmt.Errorf("update installation: %w", err) @@ -1662,7 +1665,7 @@ func restoreReconcileInstallationFromRuntimeConfig(ctx context.Context) error { // overrideRuntimeConfigFromBackup will update the runtime config from the backup. These values may // be used during the install and set in the Installation object via the // restoreReconcileInstallationFromRuntimeConfig function. -func overrideRuntimeConfigFromBackup(localArtifactMirrorPort int, backup disasterrecovery.ReplicatedBackup) error { +func overrideRuntimeConfigFromBackup(localArtifactMirrorPort int, backup disasterrecovery.ReplicatedBackup, rc runtimeconfig.RuntimeConfig) error { if localArtifactMirrorPort != 0 { if val, _ := backup.GetAnnotation("kots.io/embedded-cluster-local-artifact-mirror-port"); val != "" { port, err := k8snet.ParsePort(val, false) @@ -1670,7 +1673,7 @@ func overrideRuntimeConfigFromBackup(localArtifactMirrorPort int, backup disaste return fmt.Errorf("parse local artifact mirror port: %w", err) } logrus.Debugf("updating local artifact mirror port to %d from backup %q", port, backup.GetName()) - runtimeconfig.SetLocalArtifactMirrorPort(port) + rc.SetLocalArtifactMirrorPort(port) } } @@ -1734,8 +1737,8 @@ func (e *invalidBackupsError) Error() string { } // updateLocalArtifactMirrorService updates the port on which the local artifact mirror is served. -func updateLocalArtifactMirrorService() error { - if err := writeLocalArtifactMirrorDropInFile(); err != nil { +func updateLocalArtifactMirrorService(rc runtimeconfig.RuntimeConfig) error { + if err := writeLocalArtifactMirrorDropInFile(rc); err != nil { return fmt.Errorf("failed to write local artifact mirror environment file: %w", err) } diff --git a/cmd/installer/cli/restore_test.go b/cmd/installer/cli/restore_test.go index 930ddf3f5..5e52da075 100644 --- a/cmd/installer/cli/restore_test.go +++ b/cmd/installer/cli/restore_test.go @@ -13,6 +13,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/disasterrecovery" "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/require" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -184,8 +185,9 @@ func Test_isReplicatedBackupRestorable(t *testing.T) { t.Run(tt.name, func(t *testing.T) { files := embedFSToMap(t, tt.releaseFS) release.SetReleaseDataForTests(files) + rc := runtimeconfig.New(nil) - got, got1 := isReplicatedBackupRestorable(tt.args.backup, tt.args.rel, tt.args.isAirgap, tt.args.k0sCfg) + got, got1 := isReplicatedBackupRestorable(tt.args.backup, tt.args.rel, tt.args.isAirgap, tt.args.k0sCfg, rc) assert.Equal(t, tt.want, got) assert.Equal(t, tt.want1, got1) }) @@ -324,8 +326,9 @@ func Test_waitForBackups(t *testing.T) { t.Run(tt.name, func(t *testing.T) { files := embedFSToMap(t, tt.releaseFS) release.SetReleaseDataForTests(files) + rc := runtimeconfig.New(nil) - got, err := waitForBackups(context.Background(), io.Discard, tt.args.kcli, tt.args.k0sCfg, tt.args.isAirgap) + got, err := waitForBackups(context.Background(), io.Discard, tt.args.kcli, tt.args.k0sCfg, rc, tt.args.isAirgap) if tt.wantErr { require.Error(t, err) } else { diff --git a/cmd/installer/cli/shell.go b/cmd/installer/cli/shell.go index 759e31c0a..25e2c23a2 100644 --- a/cmd/installer/cli/shell.go +++ b/cmd/installer/cli/shell.go @@ -28,6 +28,8 @@ const welcome = ` ` func ShellCmd(ctx context.Context, name string) *cobra.Command { + var rc runtimeconfig.RuntimeConfig + cmd := &cobra.Command{ Use: "shell", Short: fmt.Sprintf("Start a shell with access to the %s cluster", name), @@ -36,11 +38,11 @@ func ShellCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("shell command must be run as root") } - rcutil.InitBestRuntimeConfig(cmd.Context()) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + rc = rcutil.InitBestRuntimeConfig(cmd.Context()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) - if _, err := os.Stat(runtimeconfig.PathToKubeConfig()); err != nil { - return fmt.Errorf("kubeconfig not found at %s", runtimeconfig.PathToKubeConfig()) + if _, err := os.Stat(rc.PathToKubeConfig()); err != nil { + return fmt.Errorf("kubeconfig not found at %s", rc.PathToKubeConfig()) } return nil @@ -83,12 +85,12 @@ func ShellCmd(ctx context.Context, name string) *cobra.Command { _ = term.Restore(fd, state) }() - kcpath := runtimeconfig.PathToKubeConfig() + kcpath := rc.PathToKubeConfig() config := fmt.Sprintf("export KUBECONFIG=%q\n", kcpath) _, _ = shellpty.WriteString(config) _, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1)) - bindir := runtimeconfig.EmbeddedClusterBinsSubDir() + bindir := rc.EmbeddedClusterBinsSubDir() config = fmt.Sprintf("export PATH=\"$PATH:%s\"\n", bindir) _, _ = shellpty.WriteString(config) _, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1)) @@ -99,7 +101,7 @@ func ShellCmd(ctx context.Context, name string) *cobra.Command { _, _ = shellpty.WriteString(config) _, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1)) - comppath := runtimeconfig.PathToEmbeddedClusterBinary("kubectl_completion_bash.sh") + comppath := rc.PathToEmbeddedClusterBinary("kubectl_completion_bash.sh") config = fmt.Sprintf("source <(cat %s)\n", comppath) _, _ = shellpty.WriteString(config) _, _ = io.CopyN(io.Discard, shellpty, int64(len(config)+1)) diff --git a/cmd/installer/cli/supportbundle.go b/cmd/installer/cli/supportbundle.go index 1c7daf956..7d0adff5b 100644 --- a/cmd/installer/cli/supportbundle.go +++ b/cmd/installer/cli/supportbundle.go @@ -18,6 +18,8 @@ import ( ) func SupportBundleCmd(ctx context.Context, name string) *cobra.Command { + var rc runtimeconfig.RuntimeConfig + cmd := &cobra.Command{ Use: "support-bundle", Short: "Generate a support bundle", @@ -26,21 +28,21 @@ func SupportBundleCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("support-bundle command must be run as root") } - rcutil.InitBestRuntimeConfig(cmd.Context()) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + rc = rcutil.InitBestRuntimeConfig(cmd.Context()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { - supportBundle := runtimeconfig.PathToEmbeddedClusterBinary("kubectl-support_bundle") + supportBundle := rc.PathToEmbeddedClusterBinary("kubectl-support_bundle") if _, err := os.Stat(supportBundle); err != nil { return errors.New("support-bundle command can only be run after an install attempt") } - hostSupportBundle := runtimeconfig.PathToEmbeddedClusterSupportFile("host-support-bundle.yaml") + hostSupportBundle := rc.PathToEmbeddedClusterSupportFile("host-support-bundle.yaml") if _, err := os.Stat(hostSupportBundle); err != nil { return fmt.Errorf("unable to find host support bundle: %w", err) } @@ -53,7 +55,7 @@ func SupportBundleCmd(ctx context.Context, name string) *cobra.Command { fname := fmt.Sprintf("support-bundle-%s.tar.gz", now) destination := filepath.Join(pwd, fname) - kubeConfig := runtimeconfig.PathToKubeConfig() + kubeConfig := rc.PathToKubeConfig() arguments := []string{} if _, err := os.Stat(kubeConfig); err == nil { arguments = append(arguments, fmt.Sprintf("--kubeconfig=%s", kubeConfig)) diff --git a/cmd/installer/cli/update.go b/cmd/installer/cli/update.go index 54a22b311..cfc80d919 100644 --- a/cmd/installer/cli/update.go +++ b/cmd/installer/cli/update.go @@ -14,9 +14,8 @@ import ( ) func UpdateCmd(ctx context.Context, name string) *cobra.Command { - var ( - airgapBundle string - ) + var airgapBundle string + var rc runtimeconfig.RuntimeConfig cmd := &cobra.Command{ Use: "update", @@ -26,17 +25,19 @@ func UpdateCmd(ctx context.Context, name string) *cobra.Command { return fmt.Errorf("update command must be run as root") } - if err := rcutil.InitRuntimeConfigFromCluster(ctx); err != nil { + var err error + rc, err = rcutil.GetRuntimeConfigFromCluster(ctx) + if err != nil { return fmt.Errorf("failed to init runtime config from cluster: %w", err) } - os.Setenv("KUBECONFIG", runtimeconfig.PathToKubeConfig()) - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("KUBECONFIG", rc.PathToKubeConfig()) + os.Setenv("TMPDIR", rc.EmbeddedClusterTmpSubDir()) return nil }, PostRun: func(cmd *cobra.Command, args []string) { - runtimeconfig.Cleanup() + rc.Cleanup() }, RunE: func(cmd *cobra.Command, args []string) error { if airgapBundle != "" { @@ -52,9 +53,10 @@ func UpdateCmd(ctx context.Context, name string) *cobra.Command { } if err := kotscli.AirgapUpdate(kotscli.AirgapUpdateOptions{ - AppSlug: rel.AppSlug, - Namespace: runtimeconfig.KotsadmNamespace, - AirgapBundle: airgapBundle, + RuntimeConfig: rc, + AppSlug: rel.AppSlug, + Namespace: runtimeconfig.KotsadmNamespace, + AirgapBundle: airgapBundle, }); err != nil { return err } diff --git a/cmd/installer/goods/materializer.go b/cmd/installer/goods/materializer.go index ed499e000..d860ecfaf 100644 --- a/cmd/installer/goods/materializer.go +++ b/cmd/installer/goods/materializer.go @@ -15,12 +15,16 @@ import ( const PlaceHolder = ".placeholder" // Materializer is an entity capable of materialize (write to disk) embedded assets. -type Materializer struct{} +type Materializer struct { + rc runtimeconfig.RuntimeConfig +} // NewMaterializer returns a new entity capable of materialize (write to disk) embedded // assets. Other operations on embedded assets are also available. -func NewMaterializer() *Materializer { - return &Materializer{} +func NewMaterializer(rc runtimeconfig.RuntimeConfig) *Materializer { + return &Materializer{ + rc: rc, + } } // InternalBinary materializes an internal binary from inside internal/bins directory @@ -101,14 +105,14 @@ func (m *Materializer) SupportFiles() error { if err != nil { return fmt.Errorf("unable to read asset: %w", err) } - dstpath := runtimeconfig.PathToEmbeddedClusterSupportFile(entry.Name()) + dstpath := m.rc.PathToEmbeddedClusterSupportFile(entry.Name()) if err := os.WriteFile(dstpath, srcfile, 0644); err != nil { return fmt.Errorf("unable to write file %s: %w", dstpath, err) } } name := "host-support-bundle-remote.yaml" - dstpath := runtimeconfig.PathToEmbeddedClusterSupportFile(name) + dstpath := m.rc.PathToEmbeddedClusterSupportFile(name) if err := os.WriteFile(dstpath, support.GetRemoteHostSupportBundleSpec(), 0644); err != nil { return fmt.Errorf("unable to write file %s: %w", dstpath, err) } @@ -148,7 +152,7 @@ func (m *Materializer) Binaries() error { return fmt.Errorf("unable to read asset: %w", err) } - dstpath := runtimeconfig.PathToEmbeddedClusterBinary(entry.Name()) + dstpath := m.rc.PathToEmbeddedClusterBinary(entry.Name()) if _, err := os.Stat(dstpath); err == nil { tmp := fmt.Sprintf("%s.bkp", dstpath) if err := os.Rename(dstpath, tmp); err != nil { @@ -173,15 +177,15 @@ func (m *Materializer) Kubectl() error { // k0s supports symlinking kubectl, which would be ideal, but it cannot be a symlink because // local-artifact-mirror needs to serve the kubectl binary. // https://github.com/k0sproject/k0s/blob/5d48d20767851fe8e299aacd3d5aae6fcfbeab37/main.go#L40 - dstpath := runtimeconfig.PathToEmbeddedClusterBinary("kubectl") + dstpath := m.rc.PathToEmbeddedClusterBinary("kubectl") _ = os.RemoveAll(dstpath) - k0spath := runtimeconfig.K0sBinaryPath() + k0spath := runtimeconfig.K0sBinaryPath content := fmt.Sprintf(kubectlScript, k0spath) if err := os.WriteFile(dstpath, []byte(content), 0755); err != nil { return fmt.Errorf("write kubectl completion: %w", err) } - dstpath = runtimeconfig.PathToEmbeddedClusterBinary("kubectl_completion_bash.sh") + dstpath = m.rc.PathToEmbeddedClusterBinary("kubectl_completion_bash.sh") _ = os.RemoveAll(dstpath) contentBytes, err := completionAliasBash("kubectl", "k0s kubectl") if err != nil { @@ -203,7 +207,7 @@ func (m *Materializer) Ourselves() error { return fmt.Errorf("unable to get our own executable path: %w", err) } - dstpath := runtimeconfig.PathToEmbeddedClusterBinary(runtimeconfig.BinaryName()) + dstpath := m.rc.PathToEmbeddedClusterBinary(runtimeconfig.BinaryName()) if srcpath == dstpath { return nil } diff --git a/cmd/installer/kotscli/kotscli.go b/cmd/installer/kotscli/kotscli.go index 8ab444174..8358c1b47 100644 --- a/cmd/installer/kotscli/kotscli.go +++ b/cmd/installer/kotscli/kotscli.go @@ -22,6 +22,7 @@ var ( ) type InstallOptions struct { + RuntimeConfig runtimeconfig.RuntimeConfig AppSlug string LicenseFile string Namespace string @@ -31,7 +32,7 @@ type InstallOptions struct { } func Install(opts InstallOptions, msg *spinner.MessageWriter) error { - materializer := goods.NewMaterializer() + materializer := goods.NewMaterializer(opts.RuntimeConfig) kotsBinPath, err := materializer.InternalBinary("kubectl-kots") if err != nil { return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err) @@ -91,8 +92,8 @@ func Install(opts InstallOptions, msg *spinner.MessageWriter) error { return nil } -func ResetPassword(password string) error { - materializer := goods.NewMaterializer() +func ResetPassword(rc runtimeconfig.RuntimeConfig, password string) error { + materializer := goods.NewMaterializer(rc) kotsBinPath, err := materializer.InternalBinary("kubectl-kots") if err != nil { return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err) @@ -100,7 +101,7 @@ func ResetPassword(password string) error { defer os.Remove(kotsBinPath) runCommandOptions := helpers.RunCommandOptions{ - Env: map[string]string{"KUBECONFIG": runtimeconfig.PathToKubeConfig()}, + Env: map[string]string{"KUBECONFIG": rc.PathToKubeConfig()}, Stdin: strings.NewReader(fmt.Sprintf("%s\n", password)), } @@ -113,13 +114,14 @@ func ResetPassword(password string) error { } type AirgapUpdateOptions struct { - AppSlug string - Namespace string - AirgapBundle string + RuntimeConfig runtimeconfig.RuntimeConfig + AppSlug string + Namespace string + AirgapBundle string } func AirgapUpdate(opts AirgapUpdateOptions) error { - materializer := goods.NewMaterializer() + materializer := goods.NewMaterializer(opts.RuntimeConfig) kotsBinPath, err := materializer.InternalBinary("kubectl-kots") if err != nil { return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err) @@ -161,6 +163,7 @@ func AirgapUpdate(opts AirgapUpdateOptions) error { } type VeleroConfigureOtherS3Options struct { + RuntimeConfig runtimeconfig.RuntimeConfig Endpoint string Region string Bucket string @@ -171,7 +174,7 @@ type VeleroConfigureOtherS3Options struct { } func VeleroConfigureOtherS3(opts VeleroConfigureOtherS3Options) error { - materializer := goods.NewMaterializer() + materializer := goods.NewMaterializer(opts.RuntimeConfig) kotsBinPath, err := materializer.InternalBinary("kubectl-kots") if err != nil { return fmt.Errorf("unable to materialize kubectl-kots binary: %w", err) @@ -243,8 +246,8 @@ func MaskKotsOutputForAirgap() spinner.MaskFn { } } -func GetJoinCommand(ctx context.Context) (string, error) { - materializer := goods.NewMaterializer() +func GetJoinCommand(ctx context.Context, rc runtimeconfig.RuntimeConfig) (string, error) { + materializer := goods.NewMaterializer(rc) kotsBinPath, err := materializer.InternalBinary("kubectl-kots") if err != nil { return "", fmt.Errorf("unable to materialize kubectl-kots binary: %w", err) @@ -254,7 +257,7 @@ func GetJoinCommand(ctx context.Context) (string, error) { outBuffer := bytes.NewBuffer(nil) runCommandOptions := helpers.RunCommandOptions{ Context: ctx, - Env: map[string]string{"KUBECONFIG": runtimeconfig.PathToKubeConfig()}, + Env: map[string]string{"KUBECONFIG": rc.PathToKubeConfig()}, Stdin: strings.NewReader(""), Stdout: outBuffer, } diff --git a/cmd/local-artifact-mirror/cli.go b/cmd/local-artifact-mirror/cli.go index 45f6ebf94..e1510f299 100644 --- a/cmd/local-artifact-mirror/cli.go +++ b/cmd/local-artifact-mirror/cli.go @@ -12,6 +12,7 @@ import ( ) type CLI struct { + RC runtimeconfig.RuntimeConfig Name string V *viper.Viper KCLIGetter func() (client.Client, error) @@ -21,6 +22,7 @@ type CLI struct { func NewCLI(name string) *CLI { cli := &CLI{ + RC: runtimeconfig.New(nil), Name: name, V: viper.New(), KCLIGetter: kubeutils.KubeClient, @@ -45,8 +47,8 @@ func (cli *CLI) bindFlags(flags *pflag.FlagSet) { func (cli *CLI) setupDataDir() { dataDir := cli.V.GetString("data-dir") if dataDir != "" { - runtimeconfig.SetDataDir(dataDir) + cli.RC.SetDataDir(dataDir) } - os.Setenv("TMPDIR", runtimeconfig.EmbeddedClusterTmpSubDir()) + os.Setenv("TMPDIR", cli.RC.EmbeddedClusterTmpSubDir()) } diff --git a/cmd/local-artifact-mirror/pull_binaries.go b/cmd/local-artifact-mirror/pull_binaries.go index bafbcc0f7..0ccc1c79f 100644 --- a/cmd/local-artifact-mirror/pull_binaries.go +++ b/cmd/local-artifact-mirror/pull_binaries.go @@ -10,7 +10,6 @@ import ( "os/exec" "path/filepath" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -107,7 +106,7 @@ func PullBinariesCmd(cli *CLI) *cobra.Command { return fmt.Errorf("unable to change permissions on %s: %w", bin, err) } - materializeCmdArgs := []string{"materialize", "--data-dir", runtimeconfig.EmbeddedClusterHomeDirectory()} + materializeCmdArgs := []string{"materialize", "--data-dir", cli.RC.EmbeddedClusterHomeDirectory()} materializeCmd := exec.Command(namedBin, materializeCmdArgs...) logrus.Infof("running command: %s with args: %v", namedBin, materializeCmdArgs) diff --git a/cmd/local-artifact-mirror/pull_binaries_test.go b/cmd/local-artifact-mirror/pull_binaries_test.go index cc0ff1f2d..0028e8c37 100644 --- a/cmd/local-artifact-mirror/pull_binaries_test.go +++ b/cmd/local-artifact-mirror/pull_binaries_test.go @@ -11,6 +11,7 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -43,6 +44,9 @@ func TestPullBinariesCmd_Online(t *testing.T) { dataDir := t.TempDir() t.Setenv("TMPDIR", dataDir) // hack as the cli sets TMPDIR, this will reset it after the test + rc := runtimeconfig.New(nil) + rc.SetDataDir(dataDir) + // Create a test server that serves the test release archive server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check basic auth @@ -149,6 +153,7 @@ func TestPullBinariesCmd_Online(t *testing.T) { // Create the command cli := &CLI{ + RC: rc, Name: "local-artifact-mirror", V: viper.New(), KCLIGetter: func() (client.Client, error) { diff --git a/cmd/local-artifact-mirror/pull_helmcharts.go b/cmd/local-artifact-mirror/pull_helmcharts.go index e8e4443f9..f8ca3271a 100644 --- a/cmd/local-artifact-mirror/pull_helmcharts.go +++ b/cmd/local-artifact-mirror/pull_helmcharts.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/tgzutils" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -48,7 +47,7 @@ func PullHelmChartsCmd(cli *CLI) *cobra.Command { os.RemoveAll(location) }() - dst := runtimeconfig.EmbeddedClusterChartsSubDir() + dst := cli.RC.EmbeddedClusterChartsSubDir() src := filepath.Join(location, HelmChartsArtifactName) logrus.Infof("uncompressing %s", src) if err := tgzutils.Decompress(src, dst); err != nil { diff --git a/cmd/local-artifact-mirror/pull_images.go b/cmd/local-artifact-mirror/pull_images.go index d43113cb7..321081f62 100644 --- a/cmd/local-artifact-mirror/pull_images.go +++ b/cmd/local-artifact-mirror/pull_images.go @@ -6,7 +6,6 @@ import ( "path/filepath" "github.com/replicatedhq/embedded-cluster/pkg/helpers" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -48,7 +47,7 @@ func PullImagesCmd(cli *CLI) *cobra.Command { os.RemoveAll(location) }() - dst := filepath.Join(runtimeconfig.EmbeddedClusterImagesSubDir(), ImagesDstArtifactName) + dst := filepath.Join(cli.RC.EmbeddedClusterImagesSubDir(), ImagesDstArtifactName) src := filepath.Join(location, ImagesSrcArtifactName) logrus.Infof("%s > %s", src, dst) if err := helpers.MoveFile(src, dst); err != nil { diff --git a/cmd/local-artifact-mirror/pull_images_test.go b/cmd/local-artifact-mirror/pull_images_test.go index 806c2eeef..f8883c640 100644 --- a/cmd/local-artifact-mirror/pull_images_test.go +++ b/cmd/local-artifact-mirror/pull_images_test.go @@ -9,6 +9,7 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -24,6 +25,9 @@ func TestPullImagesCmd(t *testing.T) { dataDir := t.TempDir() t.Setenv("TMPDIR", dataDir) // hack as the cli sets TMPDIR, this will reset it after the test + rc := runtimeconfig.New(nil) + rc.SetDataDir(dataDir) + // Create a fake client with test Installation scheme := runtime.NewScheme() err := ecv1beta1.AddToScheme(scheme) @@ -164,6 +168,7 @@ func TestPullImagesCmd(t *testing.T) { // Create the command cli := &CLI{ + RC: rc, Name: "local-artifact-mirror", V: viper.New(), KCLIGetter: func() (client.Client, error) { diff --git a/cmd/local-artifact-mirror/serve.go b/cmd/local-artifact-mirror/serve.go index 8011c2e3a..2ac60ce52 100644 --- a/cmd/local-artifact-mirror/serve.go +++ b/cmd/local-artifact-mirror/serve.go @@ -45,14 +45,14 @@ func ServeCmd(cli *CLI) *cobra.Command { handler := http.NewServeMux() - fileServer := http.FileServer(http.Dir(runtimeconfig.EmbeddedClusterHomeDirectory())) + fileServer := http.FileServer(http.Dir(cli.RC.EmbeddedClusterHomeDirectory())) loggedFileServer := logAndFilterRequest(fileServer) handler.Handle("/", loggedFileServer) ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) defer cancel() - if err := startBinaryWatcher(cancel); err != nil { + if err := startBinaryWatcher(cancel, cli.RC); err != nil { panic(err) } @@ -89,8 +89,8 @@ func ServeCmd(cli *CLI) *cobra.Command { // startBinaryWatcher starts a loop that observes the binary until its modification // time changes. When the modification time changes a SIGTERM is send in the provided // channel. -func startBinaryWatcher(cancel context.CancelFunc) error { - fpath := runtimeconfig.PathToEmbeddedClusterBinary("local-artifact-mirror") +func startBinaryWatcher(cancel context.CancelFunc, rc runtimeconfig.RuntimeConfig) error { + fpath := rc.PathToEmbeddedClusterBinary("local-artifact-mirror") stat, err := os.Stat(fpath) if err != nil { return fmt.Errorf("unable to stat %s: %s", fpath, err) diff --git a/cmd/local-artifact-mirror/serve_test.go b/cmd/local-artifact-mirror/serve_test.go index fe1196da2..ecf0fa32d 100644 --- a/cmd/local-artifact-mirror/serve_test.go +++ b/cmd/local-artifact-mirror/serve_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,6 +22,9 @@ func TestServeCmd(t *testing.T) { dataDir := t.TempDir() t.Setenv("TMPDIR", dataDir) // hack as the cli sets TMPDIR, this will reset it after the test + rc := runtimeconfig.New(nil) + rc.SetDataDir(dataDir) + // Detect a free port listener, err := net.Listen("tcp", ":0") require.NoError(t, err) @@ -102,6 +106,7 @@ func TestServeCmd(t *testing.T) { // Setup the commands cli := &CLI{ + RC: rc, Name: "local-artifact-mirror", V: viper.New(), } diff --git a/dagger/main.go b/dagger/main.go index e61780861..88fb40ea7 100644 --- a/dagger/main.go +++ b/dagger/main.go @@ -41,6 +41,8 @@ func directoryWithCommonGoFiles(dir *dagger.Directory, src *dagger.Directory) *d WithFile("go.mod", src.File("go.mod")). WithFile("go.sum", src.File("go.sum")). WithDirectory("pkg", src.Directory("pkg")). + WithDirectory("pkg-new", src.Directory("pkg-new")). + WithDirectory("api", src.Directory("api")). WithDirectory("kinds", src.Directory("kinds")). WithDirectory("utils", src.Directory("utils")) } diff --git a/dev/dockerfiles/local-artifact-mirror/Dockerfile.ttlsh b/dev/dockerfiles/local-artifact-mirror/Dockerfile.ttlsh index 7837e5b98..b09da7d76 100644 --- a/dev/dockerfiles/local-artifact-mirror/Dockerfile.ttlsh +++ b/dev/dockerfiles/local-artifact-mirror/Dockerfile.ttlsh @@ -10,7 +10,9 @@ RUN --mount=type=cache,target="/go/pkg/mod" go mod download COPY common.mk common.mk COPY local-artifact-mirror/ local-artifact-mirror/ COPY pkg/ pkg/ +COPY pkg-new/ pkg-new/ COPY cmd/ cmd/ +COPY api/ api/ COPY kinds/ kinds/ COPY utils/ utils/ diff --git a/dev/dockerfiles/operator/Dockerfile.ttlsh b/dev/dockerfiles/operator/Dockerfile.ttlsh index df54a54a3..2262a61dc 100644 --- a/dev/dockerfiles/operator/Dockerfile.ttlsh +++ b/dev/dockerfiles/operator/Dockerfile.ttlsh @@ -10,6 +10,8 @@ RUN --mount=type=cache,target="/go/pkg/mod" go mod download COPY common.mk common.mk COPY operator/ operator/ COPY pkg/ pkg/ +COPY pkg-new/ pkg-new/ +COPY api/ api/ COPY kinds/ kinds/ COPY utils/ utils/ diff --git a/e2e/helm-charts/nginx-app/Chart.lock b/e2e/helm-charts/nginx-app/Chart.lock index a843ab3c1..32c38a544 100644 --- a/e2e/helm-charts/nginx-app/Chart.lock +++ b/e2e/helm-charts/nginx-app/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: replicated repository: oci://registry.replicated.com/library - version: 1.5.1 -digest: sha256:34d7438335de667fde932992a7ac61c3237be12ac0794b4c64a0efc9420dcab2 -generated: "2025-04-17T06:54:24.758248-07:00" + version: 1.5.3 +digest: sha256:e8679070b0d50709f5641020ec59ee5bf44e64dc26c5e650e2feee3d9c8b8194 +generated: "2025-06-03T15:31:32.283203-07:00" diff --git a/e2e/preflights_test.go b/e2e/preflights_test.go index c7f7676ef..da16794c8 100644 --- a/e2e/preflights_test.go +++ b/e2e/preflights_test.go @@ -4,8 +4,9 @@ import ( "strings" "testing" + apitypes "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/e2e/cluster/docker" - "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" + "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" ) func TestPreflights(t *testing.T) { @@ -64,18 +65,18 @@ func TestPreflights(t *testing.T) { t.Fatalf("failed to list preflight bundle: err=%v, stderr=%s", err, stderr) } - results, err := types.OutputFromReader(strings.NewReader(stdout)) + results, err := preflights.OutputFromReader(strings.NewReader(stdout)) if err != nil { t.Fatalf("failed to parse preflight results: %v", err) } tests := []struct { name string - assert func(t *testing.T, results *types.Output) + assert func(t *testing.T, results *apitypes.HostPreflightOutput) }{ { name: "Should contain fio results", - assert: func(t *testing.T, results *types.Output) { + assert: func(t *testing.T, results *apitypes.HostPreflightOutput) { for _, res := range results.Pass { if res.Title == "Filesystem Write Latency" { t.Logf("fio test passed: %s", res.Message) @@ -95,7 +96,7 @@ func TestPreflights(t *testing.T) { }, { name: "Should not contain unexpected failures", - assert: func(t *testing.T, results *types.Output) { + assert: func(t *testing.T, results *apitypes.HostPreflightOutput) { expected := map[string]bool{ // TODO: work to remove these "System Clock": true, @@ -120,7 +121,7 @@ func TestPreflights(t *testing.T) { }, { name: "Should not contain unexpected warnings", - assert: func(t *testing.T, results *types.Output) { + assert: func(t *testing.T, results *apitypes.HostPreflightOutput) { expected := map[string]bool{ "Default Route": true, } @@ -135,7 +136,7 @@ func TestPreflights(t *testing.T) { }, { name: "Should contain port failures", - assert: func(t *testing.T, results *types.Output) { + assert: func(t *testing.T, results *apitypes.HostPreflightOutput) { expected := map[string]bool{ "Kubelet Port Availability": false, "Calico Communication Port Availability": false, @@ -155,7 +156,7 @@ func TestPreflights(t *testing.T) { }, { name: "Should contain data directory permissions failures", - assert: func(t *testing.T, results *types.Output) { + assert: func(t *testing.T, results *apitypes.HostPreflightOutput) { for _, res := range results.Fail { if res.Title == "Data Directory Permissions" { // should not contain data dir as we automatically fix it diff --git a/operator/charts/embedded-cluster-operator/Chart.yaml b/operator/charts/embedded-cluster-operator/Chart.yaml index 836c1bb4e..884c6a365 100644 --- a/operator/charts/embedded-cluster-operator/Chart.yaml +++ b/operator/charts/embedded-cluster-operator/Chart.yaml @@ -1,4 +1,3 @@ -# DEVELOPMENT USE ONLY! THIE FILE IS NOT USED FOR DEPLOYS! apiVersion: v2 name: embedded-cluster-operator description: Embedded Cluster Operator diff --git a/operator/controllers/installation_controller.go b/operator/controllers/installation_controller.go index 82309345f..0a4054586 100644 --- a/operator/controllers/installation_controller.go +++ b/operator/controllers/installation_controller.go @@ -133,6 +133,7 @@ type InstallationReconciler struct { Discovery discovery.DiscoveryInterface Scheme *runtime.Scheme Recorder record.EventRecorder + RuntimeConfig runtimeconfig.RuntimeConfig } // NodeHasChanged returns true if the node configuration has changed when compared to @@ -308,7 +309,7 @@ func (r *InstallationReconciler) CopyHostPreflightResultsFromNodes(ctx context.C for _, event := range events.NodesAdded { log.Info("Creating job to copy host preflight results from node", "node", event.NodeName, "installation", in.Name) - job := constructHostPreflightResultsJob(in, event.NodeName) + job := constructHostPreflightResultsJob(r.RuntimeConfig, in, event.NodeName) // overrides the job image if the environment says so. if img := os.Getenv("EMBEDDEDCLUSTER_UTILS_IMAGE"); img != "" { @@ -327,7 +328,7 @@ func (r *InstallationReconciler) CopyHostPreflightResultsFromNodes(ctx context.C return nil } -func constructHostPreflightResultsJob(in *v1beta1.Installation, nodeName string) *batchv1.Job { +func constructHostPreflightResultsJob(rc runtimeconfig.RuntimeConfig, in *v1beta1.Installation, nodeName string) *batchv1.Job { labels := map[string]string{ "embedded-cluster/node-name": nodeName, "embedded-cluster/installation": in.Name, @@ -338,7 +339,7 @@ func constructHostPreflightResultsJob(in *v1beta1.Installation, nodeName string) job.Spec.Template.Labels, job.Labels = labels, labels job.Spec.Template.Spec.NodeName = nodeName - job.Spec.Template.Spec.Volumes[0].VolumeSource.HostPath.Path = runtimeconfig.EmbeddedClusterHomeDirectory() + job.Spec.Template.Spec.Volumes[0].VolumeSource.HostPath.Path = rc.EmbeddedClusterHomeDirectory() job.Spec.Template.Spec.Containers[0].Env = append( job.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: "EC_NODE_NAME", Value: nodeName}, @@ -384,7 +385,7 @@ func (r *InstallationReconciler) Reconcile(ctx context.Context, req ctrl.Request in := r.CoalesceInstallations(ctx, items) // set the runtime config from the installation spec - runtimeconfig.Set(in.Spec.RuntimeConfig) + r.RuntimeConfig.Set(in.Spec.RuntimeConfig) // if this cluster has no id we bail out immediately. if in.Spec.ClusterID == "" { diff --git a/operator/controllers/installation_controller_test.go b/operator/controllers/installation_controller_test.go index 68efaa893..466dee523 100644 --- a/operator/controllers/installation_controller_test.go +++ b/operator/controllers/installation_controller_test.go @@ -12,6 +12,7 @@ import ( "github.com/go-logr/logr" "github.com/go-logr/logr/testr" "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" corev1 "k8s.io/api/core/v1" @@ -38,7 +39,8 @@ func TestInstallationReconciler_constructCreateCMCommand(t *testing.T) { }, }, } - job := constructHostPreflightResultsJob(in, "my-node") + rc := runtimeconfig.New(nil) + job := constructHostPreflightResultsJob(rc, in, "my-node") require.Len(t, job.Spec.Template.Spec.Volumes, 1) require.Equal(t, "/var/lib/embedded-cluster", job.Spec.Template.Spec.Volumes[0].VolumeSource.HostPath.Path) require.Len(t, job.Spec.Template.Spec.Containers, 1) @@ -224,6 +226,7 @@ func TestInstallationReconciler_reconcileHostCABundle(t *testing.T) { reconciler := &InstallationReconciler{ Client: kcli, MetadataClient: mcli, + RuntimeConfig: runtimeconfig.New(nil), } // Create a mock logger diff --git a/operator/pkg/artifacts/upgrade.go b/operator/pkg/artifacts/upgrade.go index b01c7659c..f94eabdeb 100644 --- a/operator/pkg/artifacts/upgrade.go +++ b/operator/pkg/artifacts/upgrade.go @@ -108,7 +108,7 @@ var copyArtifactsJobCommandAirgap = []string{ // This is done by creating a job for each node in the cluster, which will pull the // artifacts from the internal registry. func EnsureArtifactsJobForNodes( - ctx context.Context, cli client.Client, + ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *clusterv1beta1.Installation, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion string, ) error { @@ -134,7 +134,7 @@ func EnsureArtifactsJobForNodes( for _, node := range nodes.Items { _, err := ensureArtifactsJobForNode( - ctx, cli, in, node, localArtifactMirrorImage, appSlug, channelID, appVersion, cfghash, + ctx, cli, rc, in, node, localArtifactMirrorImage, appSlug, channelID, appVersion, cfghash, ) if err != nil { return fmt.Errorf("ensure artifacts job for node: %w", err) @@ -226,12 +226,12 @@ func hashForAirgapConfig(in *clusterv1beta1.Installation) (string, error) { } func ensureArtifactsJobForNode( - ctx context.Context, cli client.Client, in *clusterv1beta1.Installation, + ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *clusterv1beta1.Installation, node corev1.Node, localArtifactMirrorImage, appSlug, channelID, appVersion string, cfghash string, ) (*batchv1.Job, error) { - job, err := getArtifactJobForNode(ctx, cli, in, node, localArtifactMirrorImage, appSlug, channelID, appVersion) + job, err := getArtifactJobForNode(ctx, cli, rc, in, node, localArtifactMirrorImage, appSlug, channelID, appVersion) if err != nil { return nil, fmt.Errorf("get job for node: %w", err) } @@ -256,7 +256,7 @@ func ensureArtifactsJobForNode( } func getArtifactJobForNode( - ctx context.Context, cli client.Client, in *clusterv1beta1.Installation, + ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *clusterv1beta1.Installation, node corev1.Node, localArtifactMirrorImage, appSlug, channelID, appVersion string, ) (*batchv1.Job, error) { @@ -276,7 +276,7 @@ func getArtifactJobForNode( job.ObjectMeta.Labels = applyECOperatorLabels(job.ObjectMeta.Labels, "upgrader") job.ObjectMeta.Annotations = applyArtifactsJobAnnotations(job.GetAnnotations(), in, hash) job.Spec.Template.Spec.NodeName = node.Name - job.Spec.Template.Spec.Volumes[0].VolumeSource.HostPath.Path = runtimeconfig.EmbeddedClusterHomeDirectory() + job.Spec.Template.Spec.Volumes[0].VolumeSource.HostPath.Path = rc.EmbeddedClusterHomeDirectory() if in.Spec.AirGap { job.Spec.Template.Spec.Containers[0].Command = copyArtifactsJobCommandAirgap } else { @@ -381,7 +381,7 @@ func deleteArtifactsJobForNode(ctx context.Context, cli client.Client, node core // CreateAutopilotAirgapPlanCommand creates the plan to execute an aigrap upgrade in all nodes. The // return of this function is meant to be used as part of an autopilot plan. -func CreateAutopilotAirgapPlanCommand(ctx context.Context, cli client.Client, in *clusterv1beta1.Installation) (*autopilotv1beta2.PlanCommand, error) { +func CreateAutopilotAirgapPlanCommand(ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *clusterv1beta1.Installation) (*autopilotv1beta2.PlanCommand, error) { meta, err := release.MetadataFor(ctx, in, cli) if err != nil { return nil, fmt.Errorf("failed to get release metadata: %w", err) @@ -399,7 +399,7 @@ func CreateAutopilotAirgapPlanCommand(ctx context.Context, cli client.Client, in imageURL := fmt.Sprintf( "http://127.0.0.1:%d/images/ec-images-amd64.tar", - runtimeconfig.LocalArtifactMirrorPort(), + rc.LocalArtifactMirrorPort(), ) return &autopilotv1beta2.PlanCommand{ diff --git a/operator/pkg/artifacts/upgrade_test.go b/operator/pkg/artifacts/upgrade_test.go index bbbc6eddc..77e2fbc0a 100644 --- a/operator/pkg/artifacts/upgrade_test.go +++ b/operator/pkg/artifacts/upgrade_test.go @@ -11,6 +11,7 @@ import ( "github.com/go-logr/logr/testr" clusterv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" @@ -267,8 +268,10 @@ func TestEnsureArtifactsJobForNodes(t *testing.T) { }() } + rc := runtimeconfig.New(nil) + if err := EnsureArtifactsJobForNodes( - ctx, cli, tt.args.in, + ctx, cli, rc, tt.args.in, tt.args.localArtifactMirrorImage, tt.args.licenseID, tt.args.appSlug, @@ -497,9 +500,11 @@ func TestGetArtifactJobForNode_HostCABundle(t *testing.T) { }, } + rc := runtimeconfig.New(nil) + // Call the function under test job, err := getArtifactJobForNode( - ctx, cli, installation, node, + ctx, cli, rc, installation, node, "local-artifact-mirror:latest", "app-slug", "channel-id", @@ -590,9 +595,11 @@ func TestGetArtifactJobForNode_HostCABundle(t *testing.T) { }, } + rc := runtimeconfig.New(nil) + // Call the function under test job, err := getArtifactJobForNode( - ctx, cli, installation, node, + ctx, cli, rc, installation, node, "local-artifact-mirror:latest", "app-slug", "channel-id", diff --git a/operator/pkg/cli/migrate_v2.go b/operator/pkg/cli/migrate_v2.go index e85c88f4d..8d956ce0e 100644 --- a/operator/pkg/cli/migrate_v2.go +++ b/operator/pkg/cli/migrate_v2.go @@ -15,6 +15,7 @@ func MigrateV2Cmd() *cobra.Command { var installationFile string var installation *ecv1beta1.Installation + rc := runtimeconfig.New(nil) cmd := &cobra.Command{ Use: "migrate-v2", @@ -29,7 +30,7 @@ func MigrateV2Cmd() *cobra.Command { // set the runtime config from the installation spec // NOTE: this is run in a pod so the data dir is not available - runtimeconfig.Set(installation.Spec.RuntimeConfig) + rc.Set(installation.Spec.RuntimeConfig) return nil }, diff --git a/operator/pkg/cli/root.go b/operator/pkg/cli/root.go index fd87e8606..38ced92d9 100644 --- a/operator/pkg/cli/root.go +++ b/operator/pkg/cli/root.go @@ -6,6 +6,7 @@ import ( "github.com/replicatedhq/embedded-cluster/operator/controllers" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/versions" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -72,6 +73,7 @@ func RootCmd() *cobra.Command { Scheme: mgr.GetScheme(), Discovery: discovery.NewDiscoveryClientForConfigOrDie(ctrl.GetConfigOrDie()), Recorder: mgr.GetEventRecorderFor("installation-controller"), + RuntimeConfig: runtimeconfig.New(nil), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Installation") os.Exit(1) diff --git a/operator/pkg/cli/upgrade.go b/operator/pkg/cli/upgrade.go index 2ea99e303..7071b37b8 100644 --- a/operator/pkg/cli/upgrade.go +++ b/operator/pkg/cli/upgrade.go @@ -19,9 +19,10 @@ import ( // It is called by KOTS admin console and will preposition images before creating a job to truly upgrade the cluster. func UpgradeCmd() *cobra.Command { var installationFile, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion string - var installation *ecv1beta1.Installation + rc := runtimeconfig.New(nil) + cmd := &cobra.Command{ Use: "upgrade", Short: "create a job to upgrade the embedded cluster operator", @@ -34,7 +35,7 @@ func UpgradeCmd() *cobra.Command { } // set the runtime config from the installation spec - runtimeconfig.Set(installation.Spec.RuntimeConfig) + rc.Set(installation.Spec.RuntimeConfig) return nil }, @@ -59,7 +60,7 @@ func UpgradeCmd() *cobra.Command { } err = upgrade.CreateUpgradeJob( - cmd.Context(), cli, installation, + cmd.Context(), cli, rc, installation, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion, previousInstallation.Spec.Config.Version, ) diff --git a/operator/pkg/cli/upgrade_job.go b/operator/pkg/cli/upgrade_job.go index 32a0ca3d7..656c5d712 100644 --- a/operator/pkg/cli/upgrade_job.go +++ b/operator/pkg/cli/upgrade_job.go @@ -29,6 +29,8 @@ func UpgradeJobCmd() *cobra.Command { var inFile, previousInVersion string var in *ecv1beta1.Installation + rc := runtimeconfig.New(nil) + cmd := &cobra.Command{ Use: "upgrade-job", Short: "Upgrade k0s and then all addons from within a job that may be restarted", @@ -41,7 +43,7 @@ func UpgradeJobCmd() *cobra.Command { } // set the runtime config from the installation spec - runtimeconfig.Set(in.Spec.RuntimeConfig) + rc.Set(in.Spec.RuntimeConfig) // initialize the cluster ID clusterUUID, err := uuid.Parse(in.Spec.ClusterID) @@ -63,7 +65,7 @@ func UpgradeJobCmd() *cobra.Command { airgapChartsPath := "" if in.Spec.AirGap { - airgapChartsPath = runtimeconfig.EmbeddedClusterChartsSubDirNoCreate() + airgapChartsPath = rc.EmbeddedClusterChartsSubDirNoCreate() } hcli, err := helm.NewClient(helm.HelmOptions{ @@ -78,7 +80,7 @@ func UpgradeJobCmd() *cobra.Command { } defer hcli.Close() - if upgradeErr := performUpgrade(cmd.Context(), kcli, hcli, in); upgradeErr != nil { + if upgradeErr := performUpgrade(cmd.Context(), kcli, hcli, rc, in); upgradeErr != nil { // if this is the last attempt, mark the installation as failed if err := maybeMarkAsFailed(cmd.Context(), kcli, in, upgradeErr); err != nil { slog.Error("Failed to mark installation as failed", "error", err) @@ -106,7 +108,7 @@ func UpgradeJobCmd() *cobra.Command { return cmd } -func performUpgrade(ctx context.Context, kcli client.Client, hcli helm.Client, in *ecv1beta1.Installation) (finalErr error) { +func performUpgrade(ctx context.Context, kcli client.Client, hcli helm.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation) (finalErr error) { defer func() { if r := recover(); r != nil { finalErr = fmt.Errorf("upgrade recovered from panic: %v: %s", r, string(debug.Stack())) @@ -117,7 +119,7 @@ func performUpgrade(ctx context.Context, kcli client.Client, hcli helm.Client, i return fmt.Errorf("failed to run v2 migration: %w", err) } - if err := upgrade.Upgrade(ctx, kcli, hcli, in); err != nil { + if err := upgrade.Upgrade(ctx, kcli, hcli, rc, in); err != nil { return err } return nil diff --git a/operator/pkg/upgrade/autopilot.go b/operator/pkg/upgrade/autopilot.go index 4a7fb5d27..4d46238c5 100644 --- a/operator/pkg/upgrade/autopilot.go +++ b/operator/pkg/upgrade/autopilot.go @@ -47,7 +47,7 @@ func determineUpgradeTargets(ctx context.Context, cli client.Client) (apv1b2.Pla } // startAutopilotUpgrade creates an autopilot plan to upgrade to version specified in spec.config.version. -func startAutopilotUpgrade(ctx context.Context, cli client.Client, in *v1beta1.Installation, meta *ectypes.ReleaseMetadata) error { +func startAutopilotUpgrade(ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *v1beta1.Installation, meta *ectypes.ReleaseMetadata) error { targets, err := determineUpgradeTargets(ctx, cli) if err != nil { return fmt.Errorf("failed to determine upgrade targets: %w", err) @@ -58,7 +58,7 @@ func startAutopilotUpgrade(ctx context.Context, cli client.Client, in *v1beta1.I // if we are running in an airgap environment all assets are already present in the // node and are served by the local-artifact-mirror binary listening on localhost // port 50000. we just need to get autopilot to fetch the k0s binary from there. - k0surl = fmt.Sprintf("http://127.0.0.1:%d/bin/k0s-upgrade", runtimeconfig.LocalArtifactMirrorPort()) + k0surl = fmt.Sprintf("http://127.0.0.1:%d/bin/k0s-upgrade", rc.LocalArtifactMirrorPort()) } else { artifact := meta.Artifacts["k0s"] if strings.HasPrefix(artifact, "https://") || strings.HasPrefix(artifact, "http://") { diff --git a/operator/pkg/upgrade/job.go b/operator/pkg/upgrade/job.go index 18f605ea3..df054e368 100644 --- a/operator/pkg/upgrade/job.go +++ b/operator/pkg/upgrade/job.go @@ -39,7 +39,7 @@ const ( // created to copy the images to the cluster. A configmap is then created containing the target installation // spec and the upgrade job is created. The upgrade job will update the cluster version, and then update the operator chart. func CreateUpgradeJob( - ctx context.Context, cli client.Client, in *ecv1beta1.Installation, + ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion string, previousInstallVersion string, ) error { @@ -60,7 +60,7 @@ func CreateUpgradeJob( return fmt.Errorf("copy version metadata to cluster: %w", err) } - err = distributeArtifacts(ctx, cli, in, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion) + err = distributeArtifacts(ctx, cli, rc, in, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion) if err != nil { return fmt.Errorf("distribute artifacts: %w", err) } @@ -198,7 +198,7 @@ func CreateUpgradeJob( HostPath: &corev1.HostPathVolumeSource{ // the job gets created by a process inside the kotsadm pod during an upgrade, // and kots doesn't (and shouldn't) have permissions to create this directory - Path: runtimeconfig.EmbeddedClusterChartsSubDirNoCreate(), + Path: rc.EmbeddedClusterChartsSubDirNoCreate(), }, }, }, @@ -224,7 +224,7 @@ func CreateUpgradeJob( }, { Name: "ec-charts-dir", - MountPath: runtimeconfig.EmbeddedClusterChartsSubDirNoCreate(), + MountPath: rc.EmbeddedClusterChartsSubDirNoCreate(), ReadOnly: true, }, }, @@ -301,12 +301,12 @@ func operatorImageName(ctx context.Context, cli client.Client, in *ecv1beta1.Ins } func distributeArtifacts( - ctx context.Context, cli client.Client, in *ecv1beta1.Installation, + ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion string, ) error { // let's make sure all assets have been copied to nodes. // this may take some time so we only move forward when 'ready'. - err := ensureArtifactsOnNodes(ctx, cli, in, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion) + err := ensureArtifactsOnNodes(ctx, cli, rc, in, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion) if err != nil { return fmt.Errorf("ensure artifacts: %w", err) } @@ -314,7 +314,7 @@ func distributeArtifacts( if in.Spec.AirGap { // once all assets are in place we can create the autopilot plan to push the images to // containerd. - err := ensureAirgapArtifactsInCluster(ctx, cli, in) + err := ensureAirgapArtifactsInCluster(ctx, cli, rc, in) if err != nil { return fmt.Errorf("autopilot copy airgap artifacts: %w", err) } @@ -324,7 +324,7 @@ func distributeArtifacts( } func ensureArtifactsOnNodes( - ctx context.Context, cli client.Client, in *ecv1beta1.Installation, + ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion string, ) error { log := controllerruntime.LoggerFrom(ctx) @@ -340,7 +340,7 @@ func ensureArtifactsOnNodes( } } - err := artifacts.EnsureArtifactsJobForNodes(ctx, cli, in, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion) + err := artifacts.EnsureArtifactsJobForNodes(ctx, cli, rc, in, localArtifactMirrorImage, licenseID, appSlug, channelID, appVersion) if err != nil { return fmt.Errorf("ensure artifacts job for nodes: %w", err) } @@ -391,12 +391,12 @@ func ensureArtifactsOnNodes( return nil } -func ensureAirgapArtifactsInCluster(ctx context.Context, cli client.Client, in *ecv1beta1.Installation) error { +func ensureAirgapArtifactsInCluster(ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation) error { log := controllerruntime.LoggerFrom(ctx) log.Info("Uploading container images...") - err := autopilotEnsureAirgapArtifactsPlan(ctx, cli, in) + err := autopilotEnsureAirgapArtifactsPlan(ctx, cli, rc, in) if err != nil { return fmt.Errorf("ensure autopilot plan: %w", err) } @@ -433,8 +433,8 @@ func ensureAirgapArtifactsInCluster(ctx context.Context, cli client.Client, in * return nil } -func autopilotEnsureAirgapArtifactsPlan(ctx context.Context, cli client.Client, in *ecv1beta1.Installation) error { - plan, err := getAutopilotAirgapArtifactsPlan(ctx, cli, in) +func autopilotEnsureAirgapArtifactsPlan(ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation) error { + plan, err := getAutopilotAirgapArtifactsPlan(ctx, cli, rc, in) if err != nil { return fmt.Errorf("get autopilot airgap artifacts plan: %w", err) } @@ -451,13 +451,13 @@ func autopilotEnsureAirgapArtifactsPlan(ctx context.Context, cli client.Client, return nil } -func getAutopilotAirgapArtifactsPlan(ctx context.Context, cli client.Client, in *ecv1beta1.Installation) (*v1beta2.Plan, error) { +func getAutopilotAirgapArtifactsPlan(ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation) (*v1beta2.Plan, error) { var commands []v1beta2.PlanCommand // if we are running in an airgap environment all assets are already present in the // node and are served by the local-artifact-mirror binary listening on localhost // port 50000. we just need to get autopilot to fetch the k0s binary from there. - command, err := artifacts.CreateAutopilotAirgapPlanCommand(ctx, cli, in) + command, err := artifacts.CreateAutopilotAirgapPlanCommand(ctx, cli, rc, in) if err != nil { return nil, fmt.Errorf("create autopilot airgap plan command: %w", err) } diff --git a/operator/pkg/upgrade/job_test.go b/operator/pkg/upgrade/job_test.go index 090833135..d74666b4c 100644 --- a/operator/pkg/upgrade/job_test.go +++ b/operator/pkg/upgrade/job_test.go @@ -7,6 +7,7 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/kinds/types" "github.com/replicatedhq/embedded-cluster/operator/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" batchv1 "k8s.io/api/batch/v1" @@ -56,9 +57,11 @@ func TestCreateUpgradeJob_NodeAffinity(t *testing.T) { WithObjects(installation). Build() + rc := runtimeconfig.New(nil) + // Call the function under test err := CreateUpgradeJob( - context.Background(), cli, installation, + context.Background(), cli, rc, installation, "registry.example.com/local-artifact-mirror:1.2.3", "license-id", "app-slug", "channel-id", testVersion, "1.2.2", @@ -134,9 +137,11 @@ func TestCreateUpgradeJob_HostCABundle(t *testing.T) { WithObjects(installation). Build() + rc := runtimeconfig.New(nil) + // Call the function under test err := CreateUpgradeJob( - context.Background(), cli, installation, + context.Background(), cli, rc, installation, "registry.example.com/local-artifact-mirror:1.2.3", "license-id", "app-slug", "channel-id", testVersion, "1.2.2", @@ -241,9 +246,11 @@ func TestCreateUpgradeJob_HostCABundle(t *testing.T) { WithObjects(installation). Build() + rc := runtimeconfig.New(nil) + // Call the function under test err := CreateUpgradeJob( - context.Background(), cli, installation, + context.Background(), cli, rc, installation, "registry.example.com/local-artifact-mirror:1.2.3", "license-id", "app-slug", "channel-id", testVersion, "1.2.2", diff --git a/operator/pkg/upgrade/upgrade.go b/operator/pkg/upgrade/upgrade.go index 067482fe5..0e1efbcd8 100644 --- a/operator/pkg/upgrade/upgrade.go +++ b/operator/pkg/upgrade/upgrade.go @@ -27,7 +27,7 @@ import ( // Upgrade upgrades the embedded cluster to the version specified in the installation. // First the k0s cluster is upgraded, then addon charts are upgraded, and finally the installation is unlocked. -func Upgrade(ctx context.Context, cli client.Client, hcli helm.Client, in *ecv1beta1.Installation) error { +func Upgrade(ctx context.Context, cli client.Client, hcli helm.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation) error { slog.Info("Upgrading Embedded Cluster", "version", in.Spec.Config.Version) // Augment the installation with data dirs that may not be present in the previous version. @@ -40,9 +40,9 @@ func Upgrade(ctx context.Context, cli client.Client, hcli helm.Client, in *ecv1b // In case the previous version was < 1.15, update the runtime config after we override the // installation data dirs from the previous installation. - runtimeconfig.Set(in.Spec.RuntimeConfig) + rc.Set(in.Spec.RuntimeConfig) - err = upgradeK0s(ctx, cli, in) + err = upgradeK0s(ctx, cli, rc, in) if err != nil { return fmt.Errorf("k0s upgrade: %w", err) } @@ -56,7 +56,7 @@ func Upgrade(ctx context.Context, cli client.Client, hcli helm.Client, in *ecv1b } slog.Info("Upgrading addons") - err = upgradeAddons(ctx, cli, hcli, in) + err = upgradeAddons(ctx, cli, hcli, rc, in) if err != nil { return fmt.Errorf("upgrade addons: %w", err) } @@ -92,7 +92,7 @@ func maybeOverrideInstallationDataDirs(ctx context.Context, cli client.Client, i return &next, nil } -func upgradeK0s(ctx context.Context, cli client.Client, in *ecv1beta1.Installation) error { +func upgradeK0s(ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation) error { meta, err := release.MetadataFor(ctx, in, cli) if err != nil { return fmt.Errorf("get release metadata: %w", err) @@ -117,7 +117,7 @@ func upgradeK0s(ctx context.Context, cli client.Client, in *ecv1beta1.Installati } // create an autopilot upgrade plan if one does not yet exist - if err := createAutopilotPlan(ctx, cli, desiredVersion, in, meta); err != nil { + if err := createAutopilotPlan(ctx, cli, rc, desiredVersion, in, meta); err != nil { return fmt.Errorf("create autpilot upgrade plan: %w", err) } @@ -147,7 +147,7 @@ func upgradeK0s(ctx context.Context, cli client.Client, in *ecv1beta1.Installati if err != nil { return fmt.Errorf("delete autopilot plan: %w", err) } - return upgradeK0s(ctx, cli, in) + return upgradeK0s(ctx, cli, rc, in) } match, err = clusterNodesMatchVersion(ctx, cli, desiredVersion) @@ -248,7 +248,7 @@ func updateClusterConfig(ctx context.Context, cli client.Client, in *ecv1beta1.I return nil } -func upgradeAddons(ctx context.Context, cli client.Client, hcli helm.Client, in *ecv1beta1.Installation) error { +func upgradeAddons(ctx context.Context, cli client.Client, hcli helm.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation) error { err := kubeutils.SetInstallationState(ctx, cli, in, ecv1beta1.InstallationStateAddonsInstalling, "Upgrading addons") if err != nil { return fmt.Errorf("set installation state: %w", err) @@ -262,7 +262,7 @@ func upgradeAddons(ctx context.Context, cli client.Client, hcli helm.Client, in return fmt.Errorf("no images available") } - if err := addons.Upgrade(ctx, slog.Info, hcli, in, meta); err != nil { + if err := addons.Upgrade(ctx, slog.Info, hcli, rc, in, meta); err != nil { return fmt.Errorf("upgrade addons: %w", err) } @@ -297,7 +297,7 @@ func upgradeExtensions(ctx context.Context, cli client.Client, hcli helm.Client, return nil } -func createAutopilotPlan(ctx context.Context, cli client.Client, desiredVersion string, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) error { +func createAutopilotPlan(ctx context.Context, cli client.Client, rc runtimeconfig.RuntimeConfig, desiredVersion string, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) error { var plan apv1b2.Plan okey := client.ObjectKey{Name: "autopilot"} if err := cli.Get(ctx, okey, &plan); err != nil && !errors.IsNotFound(err) { @@ -309,7 +309,7 @@ func createAutopilotPlan(ctx context.Context, cli client.Client, desiredVersion // there is no autopilot plan in the cluster so we are free to // start our own plan. here we link the plan to the installation // by its name. - if err := startAutopilotUpgrade(ctx, cli, in, meta); err != nil { + if err := startAutopilotUpgrade(ctx, cli, rc, in, meta); err != nil { return fmt.Errorf("start upgrade: %w", err) } } diff --git a/pkg-new/domains/domains.go b/pkg-new/domains/domains.go new file mode 100644 index 000000000..8be66088c --- /dev/null +++ b/pkg-new/domains/domains.go @@ -0,0 +1,50 @@ +package domains + +import ( + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/release" +) + +const DefaultReplicatedAppDomain = "replicated.app" +const DefaultProxyRegistryDomain = "proxy.replicated.com" +const DefaultReplicatedRegistryDomain = "registry.replicated.com" + +// 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, rel *release.ChannelRelease) ecv1beta1.Domains { + replicatedAppDomain := DefaultReplicatedAppDomain + proxyRegistryDomain := DefaultProxyRegistryDomain + replicatedRegistryDomain := DefaultReplicatedRegistryDomain + + // get defaults from channel release if available + if rel != nil { + if rel.DefaultDomains.ReplicatedAppDomain != "" { + replicatedAppDomain = rel.DefaultDomains.ReplicatedAppDomain + } + if rel.DefaultDomains.ProxyRegistryDomain != "" { + proxyRegistryDomain = rel.DefaultDomains.ProxyRegistryDomain + } + if rel.DefaultDomains.ReplicatedRegistryDomain != "" { + replicatedRegistryDomain = rel.DefaultDomains.ReplicatedRegistryDomain + } + } + + // get overrides from config spec if available + if cfgspec != nil { + if cfgspec.Domains.ReplicatedAppDomain != "" { + replicatedAppDomain = cfgspec.Domains.ReplicatedAppDomain + } + if cfgspec.Domains.ProxyRegistryDomain != "" { + proxyRegistryDomain = cfgspec.Domains.ProxyRegistryDomain + } + if cfgspec.Domains.ReplicatedRegistryDomain != "" { + replicatedRegistryDomain = cfgspec.Domains.ReplicatedRegistryDomain + } + } + + return ecv1beta1.Domains{ + ReplicatedAppDomain: replicatedAppDomain, + ProxyRegistryDomain: proxyRegistryDomain, + ReplicatedRegistryDomain: replicatedRegistryDomain, + } +} diff --git a/pkg-new/hostutils/files.go b/pkg-new/hostutils/files.go new file mode 100644 index 000000000..3aaa4a8a9 --- /dev/null +++ b/pkg-new/hostutils/files.go @@ -0,0 +1,36 @@ +package hostutils + +import ( + "fmt" + "os" + + "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" + "github.com/replicatedhq/embedded-cluster/pkg/airgap" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/replicatedhq/embedded-cluster/pkg/support" +) + +func (h *HostUtils) MaterializeFiles(rc runtimeconfig.RuntimeConfig, airgapBundle string) error { + materializer := goods.NewMaterializer(rc) + if err := materializer.Materialize(); err != nil { + return fmt.Errorf("materialize binaries: %w", err) + } + if err := support.MaterializeSupportBundleSpec(rc); err != nil { + return fmt.Errorf("materialize support bundle spec: %w", err) + } + + if airgapBundle != "" { + // read file from path + rawfile, err := os.Open(airgapBundle) + if err != nil { + return fmt.Errorf("failed to open airgap file: %w", err) + } + defer rawfile.Close() + + if err := airgap.MaterializeAirgap(rc, rawfile); err != nil { + return fmt.Errorf("materialize airgap files: %w", err) + } + } + + return nil +} diff --git a/pkg-new/hostutils/host.go b/pkg-new/hostutils/host.go new file mode 100644 index 000000000..5f10cf96c --- /dev/null +++ b/pkg-new/hostutils/host.go @@ -0,0 +1,31 @@ +package hostutils + +import ( + "github.com/sirupsen/logrus" +) + +var _ HostUtilsInterface = (*HostUtils)(nil) + +type HostUtils struct { + logger logrus.FieldLogger +} + +type HostUtilsOption func(*HostUtils) + +func WithLogger(logger logrus.FieldLogger) HostUtilsOption { + return func(h *HostUtils) { + h.logger = logger + } +} + +func New(opts ...HostUtilsOption) *HostUtils { + h := &HostUtils{ + logger: logrus.StandardLogger(), + } + + for _, opt := range opts { + opt(h) + } + + return h +} diff --git a/pkg-new/hostutils/initialize.go b/pkg-new/hostutils/initialize.go new file mode 100644 index 000000000..8968d4d38 --- /dev/null +++ b/pkg-new/hostutils/initialize.go @@ -0,0 +1,52 @@ +package hostutils + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/replicatedhq/embedded-cluster/pkg/helpers" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" +) + +type InitForInstallOptions struct { + LicenseFile string + AirgapBundle string + PodCIDR string + ServiceCIDR string +} + +func (h *HostUtils) ConfigureForInstall(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts InitForInstallOptions) error { + h.logger.Debugf("materializing files") + if err := h.MaterializeFiles(rc, opts.AirgapBundle); err != nil { + return fmt.Errorf("materialize files: %w", err) + } + + 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("copy license file to %s: %v", rc.EmbeddedClusterHomeDirectory(), err) + } + + h.logger.Debugf("configuring sysctl") + if err := h.ConfigureSysctl(); err != nil { + h.logger.Debugf("configure sysctl: %v", err) + } + + h.logger.Debugf("configuring kernel modules") + if err := h.ConfigureKernelModules(); err != nil { + h.logger.Debugf("configure kernel modules: %v", err) + } + + h.logger.Debugf("configuring network manager") + if err := h.ConfigureNetworkManager(ctx, rc); err != nil { + return fmt.Errorf("configure network manager: %w", err) + } + + h.logger.Debugf("configuring firewalld") + if err := h.ConfigureFirewalld(ctx, opts.PodCIDR, opts.ServiceCIDR); err != nil { + h.logger.Debugf("configure firewalld: %v", err) + } + + return nil +} diff --git a/pkg-new/hostutils/interface.go b/pkg-new/hostutils/interface.go new file mode 100644 index 000000000..abfb63051 --- /dev/null +++ b/pkg-new/hostutils/interface.go @@ -0,0 +1,58 @@ +package hostutils + +import ( + "context" + + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" +) + +var h HostUtilsInterface + +func init() { + Set(New()) +} + +func Set(_h HostUtilsInterface) { + h = _h +} + +type HostUtilsInterface interface { + ConfigureForInstall(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts InitForInstallOptions) error + ConfigureSysctl() error + ConfigureKernelModules() error + ConfigureNetworkManager(ctx context.Context, rc runtimeconfig.RuntimeConfig) error + ConfigureFirewalld(ctx context.Context, podNetwork, serviceNetwork string) error + ResetFirewalld(ctx context.Context) error + MaterializeFiles(rc runtimeconfig.RuntimeConfig, airgapBundle string) error +} + +// Convenience functions +// TODO (@salah): can be removed once CLI uses API for host operations) + +func ConfigureForInstall(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts InitForInstallOptions) error { + return h.ConfigureForInstall(ctx, rc, opts) +} + +func ConfigureSysctl() error { + return h.ConfigureSysctl() +} + +func ConfigureKernelModules() error { + return h.ConfigureKernelModules() +} + +func ConfigureNetworkManager(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + return h.ConfigureNetworkManager(ctx, rc) +} + +func ConfigureFirewalld(ctx context.Context, podNetwork, serviceNetwork string) error { + return h.ConfigureFirewalld(ctx, podNetwork, serviceNetwork) +} + +func ResetFirewalld(ctx context.Context) error { + return h.ResetFirewalld(ctx) +} + +func MaterializeFiles(rc runtimeconfig.RuntimeConfig, airgapBundle string) error { + return h.MaterializeFiles(rc, airgapBundle) +} diff --git a/pkg-new/hostutils/mock.go b/pkg-new/hostutils/mock.go new file mode 100644 index 000000000..d4a775b5c --- /dev/null +++ b/pkg-new/hostutils/mock.go @@ -0,0 +1,57 @@ +package hostutils + +import ( + "context" + + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/stretchr/testify/mock" +) + +var _ HostUtilsInterface = (*MockHostUtils)(nil) + +// MockHostUtils is a mock implementation of the HostUtilsInterface +type MockHostUtils struct { + mock.Mock +} + +// ConfigureForInstall mocks the ConfigureForInstall method +func (m *MockHostUtils) ConfigureForInstall(ctx context.Context, rc runtimeconfig.RuntimeConfig, opts InitForInstallOptions) error { + args := m.Called(ctx, opts) + return args.Error(0) +} + +// ConfigureSysctl mocks the ConfigureSysctl method +func (m *MockHostUtils) ConfigureSysctl() error { + args := m.Called() + return args.Error(0) +} + +// ConfigureKernelModules mocks the ConfigureKernelModules method +func (m *MockHostUtils) ConfigureKernelModules() error { + args := m.Called() + return args.Error(0) +} + +// ConfigureNetworkManager mocks the ConfigureNetworkManager method +func (m *MockHostUtils) ConfigureNetworkManager(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + args := m.Called(ctx, rc) + return args.Error(0) +} + +// ConfigureFirewalld mocks the ConfigureFirewalld method +func (m *MockHostUtils) ConfigureFirewalld(ctx context.Context, podNetwork, serviceNetwork string) error { + args := m.Called(ctx, podNetwork, serviceNetwork) + return args.Error(0) +} + +// ResetFirewalld mocks the ResetFirewalld method +func (m *MockHostUtils) ResetFirewalld(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +// MaterializeFiles mocks the MaterializeFiles method +func (m *MockHostUtils) MaterializeFiles(rc runtimeconfig.RuntimeConfig, airgapBundle string) error { + args := m.Called(rc, airgapBundle) + return args.Error(0) +} diff --git a/cmd/installer/cli/firewalld.go b/pkg-new/hostutils/network.go similarity index 72% rename from cmd/installer/cli/firewalld.go rename to pkg-new/hostutils/network.go index e37638b8b..47beb4a89 100644 --- a/cmd/installer/cli/firewalld.go +++ b/pkg-new/hostutils/network.go @@ -1,18 +1,52 @@ -package cli +package hostutils import ( "context" "fmt" + "os" + "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" + "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/helpers/firewalld" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" "go.uber.org/multierr" ) -// configureFirewalld configures firewalld for the cluster. It adds the ec-net zone for pod and +// ConfigureNetworkManager configures the network manager (if the host is using it) to ignore +// the calico interfaces. This function restarts the NetworkManager service if the configuration +// was changed. +func (h *HostUtils) ConfigureNetworkManager(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { + if active, err := helpers.IsSystemdServiceActive(ctx, "NetworkManager"); err != nil { + return fmt.Errorf("check if NetworkManager is active: %w", err) + } else if !active { + logrus.Debugf("NetworkManager is not active, skipping configuration") + return nil + } + + dir := "/etc/NetworkManager/conf.d" + if _, err := os.Stat(dir); err != nil { + logrus.Debugf("skiping NetworkManager config (%s): %v", dir, err) + return nil + } + + logrus.Debugf("creating NetworkManager config file") + materializer := goods.NewMaterializer(rc) + if err := materializer.CalicoNetworkManagerConfig(); err != nil { + return fmt.Errorf("materialize configuration: %w", err) + } + + logrus.Debugf("network manager config created, restarting the service") + if _, err := helpers.RunCommand("systemctl", "restart", "NetworkManager"); err != nil { + return fmt.Errorf("restart network manager: %w", err) + } + return nil +} + +// ConfigureFirewalld configures firewalld for the cluster. It adds the ec-net zone for pod and // service communication with default target ACCEPT, and opens the necessary ports in the default // zone for k0s and k8s components on the host network. -func configureFirewalld(ctx context.Context, podNetwork, serviceNetwork string) error { +func (h *HostUtils) ConfigureFirewalld(ctx context.Context, podNetwork, serviceNetwork string) error { isActive, err := firewalld.IsFirewalldActive(ctx) if err != nil { return fmt.Errorf("check if firewalld is active: %w", err) @@ -51,8 +85,8 @@ func configureFirewalld(ctx context.Context, podNetwork, serviceNetwork string) return nil } -// resetFirewalld removes all firewalld configuration added by the installer. -func resetFirewalld(ctx context.Context) (finalErr error) { +// ResetFirewalld removes all firewalld configuration added by the installer. +func (h *HostUtils) ResetFirewalld(ctx context.Context) (finalErr error) { cmdExists, err := firewalld.FirewallCmdExists(ctx) if err != nil { return fmt.Errorf("check if firewall-cmd exists: %w", err) diff --git a/pkg/configutils/static/modules-load.d/99-embedded-cluster.conf b/pkg-new/hostutils/static/modules-load.d/99-embedded-cluster.conf similarity index 100% rename from pkg/configutils/static/modules-load.d/99-embedded-cluster.conf rename to pkg-new/hostutils/static/modules-load.d/99-embedded-cluster.conf diff --git a/pkg/configutils/static/sysctl.d/99-embedded-cluster.conf b/pkg-new/hostutils/static/sysctl.d/99-embedded-cluster.conf similarity index 100% rename from pkg/configutils/static/sysctl.d/99-embedded-cluster.conf rename to pkg-new/hostutils/static/sysctl.d/99-embedded-cluster.conf diff --git a/pkg/configutils/runtime.go b/pkg-new/hostutils/system.go similarity index 98% rename from pkg/configutils/runtime.go rename to pkg-new/hostutils/system.go index a40bd1ca4..969502b8a 100644 --- a/pkg/configutils/runtime.go +++ b/pkg-new/hostutils/system.go @@ -1,4 +1,4 @@ -package configutils +package hostutils import ( "bufio" @@ -61,7 +61,7 @@ type sysctlValueGetter func(key string) (int64, error) // ConfigureSysctl writes the sysctl config files for the embedded cluster and reloads the sysctl configuration. // NOTE: do not run this after the cluster has already been installed as it may revert sysctl // settings set by k0s and its extensions. -func ConfigureSysctl() error { +func (h *HostUtils) ConfigureSysctl() error { if _, err := exec.LookPath("sysctl"); err != nil { return fmt.Errorf("find sysctl binary: %w", err) } @@ -150,7 +150,7 @@ func getCurrentSysctlValue(key string) (int64, error) { // ConfigureKernelModules writes the kernel modules config file and ensures the kernel modules are // loaded that are listed in the file. -func ConfigureKernelModules() error { +func (h *HostUtils) ConfigureKernelModules() error { if _, err := exec.LookPath("modprobe"); err != nil { return fmt.Errorf("find modprobe binary: %w", err) } diff --git a/pkg/configutils/runtime_test.go b/pkg-new/hostutils/system_test.go similarity index 98% rename from pkg/configutils/runtime_test.go rename to pkg-new/hostutils/system_test.go index 7b205b343..0d327d12a 100644 --- a/pkg/configutils/runtime_test.go +++ b/pkg-new/hostutils/system_test.go @@ -1,4 +1,4 @@ -package configutils +package hostutils import ( _ "embed" @@ -22,7 +22,8 @@ func TestSysctlConfig(t *testing.T) { sysctlConfigPath = orig }() - runtimeconfig.SetDataDir(basedir) + rc := runtimeconfig.New(nil) + rc.SetDataDir(basedir) // happy path. dstdir, err := os.MkdirTemp("", "embedded-cluster-test") diff --git a/pkg/preflights/host-preflight.yaml b/pkg-new/preflights/host-preflight.yaml similarity index 100% rename from pkg/preflights/host-preflight.yaml rename to pkg-new/preflights/host-preflight.yaml diff --git a/pkg/preflights/types/output.go b/pkg-new/preflights/output.go similarity index 63% rename from pkg/preflights/types/output.go rename to pkg-new/preflights/output.go index 584f0cedb..2c4749aa5 100644 --- a/pkg/preflights/types/output.go +++ b/pkg-new/preflights/output.go @@ -1,4 +1,4 @@ -package types +package preflights import ( "encoding/json" @@ -8,46 +8,28 @@ import ( "strings" "github.com/jedib0t/go-pretty/v6/table" + apitypes "github.com/replicatedhq/embedded-cluster/api/types" "github.com/sirupsen/logrus" "golang.org/x/term" ) -// Output is the output of a troubleshoot preflight check as returned by -// `preflight --format=json`. It just wraps a list of results, aka records, -// classified by status. -type Output struct { - Warn []Record `json:"warn"` - Pass []Record `json:"pass"` - Fail []Record `json:"fail"` -} - -// HasFail returns true if any of the preflight checks failed. -func (o Output) HasFail() bool { - return len(o.Fail) > 0 -} - -// HasWarn returns true if any of the preflight checks returned a warning. -func (o Output) HasWarn() bool { - return len(o.Warn) > 0 -} - // PrintTable prints the preflight output in a table format. -func (o Output) PrintTable() { - o.printTable() +func PrintTable(o *apitypes.HostPreflightOutput) { + printTable(o) } // PrintTableWithoutInfo prints the preflight output in a table format without info results. -func (o Output) PrintTableWithoutInfo() { - withoutInfo := Output{ +func PrintTableWithoutInfo(o *apitypes.HostPreflightOutput) { + withoutInfo := apitypes.HostPreflightOutput{ Warn: o.Warn, Fail: o.Fail, } - withoutInfo.printTable() + printTable(&withoutInfo) } // wrapText wraps the text and adds a line break after width characters. -func (o Output) wrapText(text string, width int) string { +func wrapText(text string, width int) string { if len(text) <= width { return text } @@ -72,7 +54,7 @@ func (o Output) wrapText(text string, width int) string { // maxWidth determines the maximum width of the terminal, if larger than 150 // characters then it returns 150. -func (o Output) maxWidth() int { +func maxWidth() int { width, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil { return 150 @@ -82,7 +64,7 @@ func (o Output) maxWidth() int { return width } -func (o Output) printTable() { +func printTable(o *apitypes.HostPreflightOutput) { tb := table.NewWriter() tb.SetStyle( table.Style{ @@ -91,15 +73,15 @@ func (o Output) printTable() { }, ) - maxwidth := o.maxWidth() + maxwidth := maxWidth() tb.SetAllowedRowLength(maxwidth) for _, rec := range append(o.Fail, o.Warn...) { - tb.AppendRow(table.Row{"•", o.wrapText(rec.Message, maxwidth-5)}) + tb.AppendRow(table.Row{"•", wrapText(rec.Message, maxwidth-5)}) } logrus.Infof("\n%s\n", tb.Render()) } -func (o Output) SaveToDisk(path string) error { +func SaveToDisk(o *apitypes.HostPreflightOutput, path string) error { // Store results on disk of the host that ran the preflights data, err := json.MarshalIndent(o, "", " ") if err != nil { @@ -117,16 +99,10 @@ func (o Output) SaveToDisk(path string) error { // OutputFromReader reads the provided reader and returns a Output // object. Expects the reader to contain a valid JSON object. -func OutputFromReader(from io.Reader) (*Output, error) { - result := &Output{} +func OutputFromReader(from io.Reader) (*apitypes.HostPreflightOutput, error) { + result := &apitypes.HostPreflightOutput{} if err := json.NewDecoder(from).Decode(result); err != nil { return result, fmt.Errorf("unable to decode preflight output: %w", err) } return result, nil } - -// Record is a single record of a troubleshoot preflight check. -type Record struct { - Title string `json:"title"` - Message string `json:"message"` -} diff --git a/pkg/preflights/preflights.go b/pkg-new/preflights/preflights.go similarity index 83% rename from pkg/preflights/preflights.go rename to pkg-new/preflights/preflights.go index 628dfc8a5..54cf7ed92 100644 --- a/pkg/preflights/preflights.go +++ b/pkg-new/preflights/preflights.go @@ -12,17 +12,17 @@ import ( "path/filepath" "strings" + apitypes "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helpers" - "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "sigs.k8s.io/yaml" ) -// SerializeSpec serialize the provided spec inside a HostPreflight object and +// serializeSpec serialize the provided spec inside a HostPreflight object and // returns the byte slice. -func SerializeSpec(spec *troubleshootv1beta2.HostPreflightSpec) ([]byte, error) { +func serializeSpec(spec *troubleshootv1beta2.HostPreflightSpec) ([]byte, error) { hpf := map[string]interface{}{ "apiVersion": "troubleshoot.sh/v1beta2", "kind": "HostPreflight", @@ -34,7 +34,7 @@ func SerializeSpec(spec *troubleshootv1beta2.HostPreflightSpec) ([]byte, error) // Run runs the provided host preflight spec locally. This function is meant to be // used when upgrading a local node. -func Run(ctx context.Context, spec *troubleshootv1beta2.HostPreflightSpec, proxy *ecv1beta1.ProxySpec) (*types.Output, string, error) { +func Run(ctx context.Context, spec *troubleshootv1beta2.HostPreflightSpec, proxy *ecv1beta1.ProxySpec, rc runtimeconfig.RuntimeConfig) (*apitypes.HostPreflightOutput, string, error) { // Deduplicate collectors and analyzers before running preflights spec.Collectors = dedup(spec.Collectors) spec.Analyzers = dedup(spec.Analyzers) @@ -45,12 +45,12 @@ func Run(ctx context.Context, spec *troubleshootv1beta2.HostPreflightSpec, proxy } defer os.Remove(fpath) - binpath := runtimeconfig.PathToEmbeddedClusterBinary("kubectl-preflight") + binpath := rc.PathToEmbeddedClusterBinary("kubectl-preflight") cmd := exec.Command(binpath, "--interactive=false", "--format=json", fpath) cmdEnv := cmd.Environ() cmdEnv = proxyEnv(cmdEnv, proxy) - cmdEnv = pathEnv(cmdEnv) + cmdEnv = pathEnv(cmdEnv, rc) cmd.Env = cmdEnv stdout := bytes.NewBuffer(nil) @@ -59,7 +59,7 @@ func Run(ctx context.Context, spec *troubleshootv1beta2.HostPreflightSpec, proxy err = cmd.Run() if err == nil { - out, err := types.OutputFromReader(stdout) + out, err := OutputFromReader(stdout) return out, stderr.String(), err } @@ -68,11 +68,11 @@ func Run(ctx context.Context, spec *troubleshootv1beta2.HostPreflightSpec, proxy return nil, stderr.String(), fmt.Errorf("error running host preflight: %w, stderr=%q", err, stderr.String()) } - out, err := types.OutputFromReader(stdout) + out, err := OutputFromReader(stdout) return out, stderr.String(), err } -func CopyBundleToECSupportDir() error { +func CopyBundleTo(dst string) error { matches, err := filepath.Glob("preflightbundle-*.tar.gz") if err != nil { return fmt.Errorf("find preflight bundle: %w", err) @@ -87,7 +87,6 @@ func CopyBundleToECSupportDir() error { src = match } } - dst := runtimeconfig.PathToEmbeddedClusterSupportFile("preflight-bundle.tar.gz") if err := helpers.MoveFile(src, dst); err != nil { return fmt.Errorf("move preflight bundle to %s: %w", dst, err) } @@ -103,7 +102,7 @@ func saveHostPreflightFile(spec *troubleshootv1beta2.HostPreflightSpec) (string, return "", fmt.Errorf("unable to create temporary file: %w", err) } defer tmpfile.Close() - if data, err := SerializeSpec(spec); err != nil { + if data, err := serializeSpec(spec); err != nil { return "", fmt.Errorf("unable to serialize host preflight spec: %w", err) } else if _, err := tmpfile.Write(data); err != nil { return "", fmt.Errorf("unable to write host preflight spec: %w", err) @@ -152,7 +151,7 @@ func proxyEnv(env []string, proxy *ecv1beta1.ProxySpec) []string { return next } -func pathEnv(env []string) []string { +func pathEnv(env []string, rc runtimeconfig.RuntimeConfig) []string { path := "" next := []string{} for _, e := range env { @@ -165,9 +164,9 @@ func pathEnv(env []string) []string { } } if path != "" { - next = append(next, fmt.Sprintf("PATH=%s:%s", path, runtimeconfig.EmbeddedClusterBinsSubDir())) + next = append(next, fmt.Sprintf("PATH=%s:%s", path, rc.EmbeddedClusterBinsSubDir())) } else { - next = append(next, fmt.Sprintf("PATH=%s", runtimeconfig.EmbeddedClusterBinsSubDir())) + next = append(next, fmt.Sprintf("PATH=%s", rc.EmbeddedClusterBinsSubDir())) } return next } diff --git a/pkg/preflights/preflights_test.go b/pkg-new/preflights/preflights_test.go similarity index 96% rename from pkg/preflights/preflights_test.go rename to pkg-new/preflights/preflights_test.go index 351e215ac..6fbd7f42f 100644 --- a/pkg/preflights/preflights_test.go +++ b/pkg-new/preflights/preflights_test.go @@ -127,8 +127,9 @@ func Test_proxyEnv(t *testing.T) { } func Test_pathEnv(t *testing.T) { - runtimeconfig.SetDataDir(t.TempDir()) - binDir := runtimeconfig.EmbeddedClusterBinsSubDir() + rc := runtimeconfig.New(nil) + rc.SetDataDir(t.TempDir()) + binDir := rc.EmbeddedClusterBinsSubDir() type args struct { env []string @@ -166,7 +167,7 @@ func Test_pathEnv(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := pathEnv(tt.args.env) + got := pathEnv(tt.args.env, rc) gotMap := make(map[string]string) for _, e := range got { parts := strings.SplitN(e, "=", 2) diff --git a/pkg-new/preflights/prepare.go b/pkg-new/preflights/prepare.go new file mode 100644 index 000000000..27012cd16 --- /dev/null +++ b/pkg-new/preflights/prepare.go @@ -0,0 +1,84 @@ +package preflights + +import ( + "context" + "fmt" + "runtime" + + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/preflights/types" + "github.com/replicatedhq/embedded-cluster/pkg/metrics" + "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) + +// ErrPreflightsHaveFail is an error returned when we managed to execute the host preflights but +// they contain failures. We use this to differentiate the way we provide user feedback. +var ErrPreflightsHaveFail = metrics.NewErrorNoFail(fmt.Errorf("host preflight failures detected")) + +// PrepareOptions contains options for preparing preflights (shared across CLI and API) +type PrepareOptions struct { + HostPreflightSpec *v1beta2.HostPreflightSpec + ReplicatedAppURL string + ProxyRegistryURL string + AdminConsolePort int + LocalArtifactMirrorPort int + DataDir string + K0sDataDir string + OpenEBSDataDir string + Proxy *ecv1beta1.ProxySpec + PodCIDR string + ServiceCIDR string + GlobalCIDR *string + NodeIP string + IsAirgap bool + TCPConnectionsRequired []string + IsJoin bool +} + +// Prepare prepares the host preflights spec by merging provided spec with cluster preflights +func Prepare(ctx context.Context, opts PrepareOptions) (*v1beta2.HostPreflightSpec, error) { + hpf := opts.HostPreflightSpec + if hpf == nil { + hpf = &v1beta2.HostPreflightSpec{} + } + + data, err := types.TemplateData{ + ReplicatedAppURL: opts.ReplicatedAppURL, + ProxyRegistryURL: opts.ProxyRegistryURL, + IsAirgap: opts.IsAirgap, + AdminConsolePort: opts.AdminConsolePort, + LocalArtifactMirrorPort: opts.LocalArtifactMirrorPort, + DataDir: opts.DataDir, + K0sDataDir: opts.K0sDataDir, + OpenEBSDataDir: opts.OpenEBSDataDir, + SystemArchitecture: runtime.GOARCH, + FromCIDR: opts.PodCIDR, + ToCIDR: opts.ServiceCIDR, + TCPConnectionsRequired: opts.TCPConnectionsRequired, + NodeIP: opts.NodeIP, + IsJoin: opts.IsJoin, + }.WithCIDRData(opts.PodCIDR, opts.ServiceCIDR, opts.GlobalCIDR) + + if err != nil { + return nil, fmt.Errorf("get host preflights data: %w", err) + } + + if opts.Proxy != nil { + data.HTTPProxy = opts.Proxy.HTTPProxy + data.HTTPSProxy = opts.Proxy.HTTPSProxy + data.ProvidedNoProxy = opts.Proxy.ProvidedNoProxy + data.NoProxy = opts.Proxy.NoProxy + } + + chpfs, err := GetClusterHostPreflights(ctx, data) + if err != nil { + return nil, fmt.Errorf("get cluster host preflights: %w", err) + } + + for _, h := range chpfs { + hpf.Collectors = append(hpf.Collectors, h.Spec.Collectors...) + hpf.Analyzers = append(hpf.Analyzers, h.Spec.Analyzers...) + } + + return hpf, nil +} diff --git a/pkg/preflights/template.go b/pkg-new/preflights/template.go similarity index 94% rename from pkg/preflights/template.go rename to pkg-new/preflights/template.go index 3f884d88c..6b5589994 100644 --- a/pkg/preflights/template.go +++ b/pkg-new/preflights/template.go @@ -7,7 +7,7 @@ import ( "fmt" "text/template" - "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" + "github.com/replicatedhq/embedded-cluster/pkg-new/preflights/types" "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/loader" ) diff --git a/pkg/preflights/template_test.go b/pkg-new/preflights/template_test.go similarity index 99% rename from pkg/preflights/template_test.go rename to pkg-new/preflights/template_test.go index cdcd50ea5..ecfda3783 100644 --- a/pkg/preflights/template_test.go +++ b/pkg-new/preflights/template_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" + "github.com/replicatedhq/embedded-cluster/pkg-new/preflights/types" "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/multitype" "github.com/stretchr/testify/require" diff --git a/pkg/preflights/types/template.go b/pkg-new/preflights/types/template.go similarity index 100% rename from pkg/preflights/types/template.go rename to pkg-new/preflights/types/template.go diff --git a/pkg/addons/adminconsole/install.go b/pkg/addons/adminconsole/install.go index 75005f6d1..e938ecc5f 100644 --- a/pkg/addons/adminconsole/install.go +++ b/pkg/addons/adminconsole/install.go @@ -12,6 +12,7 @@ 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" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "golang.org/x/crypto/bcrypt" corev1 "k8s.io/api/core/v1" @@ -34,14 +35,14 @@ func init() { }) } -func (a *AdminConsole) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error { +func (a *AdminConsole) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string, writer *spinner.MessageWriter) 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); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := a.GenerateHelmValues(ctx, kcli, overrides) + values, err := a.GenerateHelmValues(ctx, kcli, rc, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/adminconsole/integration/hostcabundle_test.go b/pkg/addons/adminconsole/integration/hostcabundle_test.go index 37604b777..d54b0d662 100644 --- a/pkg/addons/adminconsole/integration/hostcabundle_test.go +++ b/pkg/addons/adminconsole/integration/hostcabundle_test.go @@ -9,6 +9,7 @@ import ( "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" @@ -23,13 +24,15 @@ func TestHostCABundle(t *testing.T) { HostCABundlePath: filepath.Join(t.TempDir(), "ca-certificates.crt"), } + rc := runtimeconfig.New(nil) + 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, nil, nil) + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, nil, nil) require.NoError(t, err, "adminconsole.Install should not return an error") manifests := addon.DryRunManifests() diff --git a/pkg/addons/adminconsole/upgrade.go b/pkg/addons/adminconsole/upgrade.go index 498423d67..73f86de71 100644 --- a/pkg/addons/adminconsole/upgrade.go +++ b/pkg/addons/adminconsole/upgrade.go @@ -6,13 +6,14 @@ import ( "github.com/pkg/errors" "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" "sigs.k8s.io/controller-runtime/pkg/client" ) -func (a *AdminConsole) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string) error { +func (a *AdminConsole) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string) error { exists, err := hcli.ReleaseExists(ctx, namespace, releaseName) if err != nil { return errors.Wrap(err, "check if release exists") @@ -22,7 +23,7 @@ func (a *AdminConsole) Upgrade(ctx context.Context, logf types.LogFunc, kcli cli return errors.New("admin console release not found") } - values, err := a.GenerateHelmValues(ctx, kcli, overrides) + values, err := a.GenerateHelmValues(ctx, kcli, rc, 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 9b74c2a6b..95ddc80b7 100644 --- a/pkg/addons/adminconsole/values.go +++ b/pkg/addons/adminconsole/values.go @@ -8,12 +8,12 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" - "github.com/replicatedhq/embedded-cluster/pkg/netutil" + "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "sigs.k8s.io/controller-runtime/pkg/client" ) -func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Client, overrides []string) (map[string]interface{}, error) { +func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, 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 { @@ -31,8 +31,8 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien } copiedValues["embeddedClusterID"] = metrics.ClusterID().String() - copiedValues["embeddedClusterDataDir"] = runtimeconfig.EmbeddedClusterHomeDirectory() - copiedValues["embeddedClusterK0sDir"] = runtimeconfig.EmbeddedClusterK0sSubDir() + copiedValues["embeddedClusterDataDir"] = rc.EmbeddedClusterHomeDirectory() + copiedValues["embeddedClusterK0sDir"] = rc.EmbeddedClusterK0sSubDir() copiedValues["isHA"] = a.IsHA copiedValues["isMultiNodeEnabled"] = a.IsMultiNodeEnabled @@ -43,7 +43,7 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien } if a.ReplicatedAppDomain != "" { - copiedValues["replicatedAppEndpoint"] = netutil.MaybeAddHTTPS(a.ReplicatedAppDomain) + copiedValues["replicatedAppEndpoint"] = netutils.MaybeAddHTTPS(a.ReplicatedAppDomain) } if a.ReplicatedRegistryDomain != "" { copiedValues["replicatedRegistryDomain"] = a.ReplicatedRegistryDomain @@ -107,7 +107,7 @@ func (a *AdminConsole) GenerateHelmValues(ctx context.Context, kcli client.Clien copiedValues["extraVolumes"] = extraVolumes copiedValues["extraVolumeMounts"] = extraVolumeMounts - err = helm.SetValue(copiedValues, "kurlProxy.nodePort", runtimeconfig.AdminConsolePort()) + err = helm.SetValue(copiedValues, "kurlProxy.nodePort", rc.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 99a8ed921..2059fd3ee 100644 --- a/pkg/addons/adminconsole/values_test.go +++ b/pkg/addons/adminconsole/values_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,7 +15,9 @@ func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", } - values, err := adminConsole.GenerateHelmValues(context.Background(), nil, nil) + rc := runtimeconfig.New(nil) + + values, err := adminConsole.GenerateHelmValues(context.Background(), nil, rc, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") // Verify structure types @@ -61,7 +64,9 @@ func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { // HostCABundlePath intentionally not set } - values, err := adminConsole.GenerateHelmValues(context.Background(), nil, nil) + rc := runtimeconfig.New(nil) + + values, err := adminConsole.GenerateHelmValues(context.Background(), nil, rc, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") // Verify structure types diff --git a/pkg/addons/embeddedclusteroperator/install.go b/pkg/addons/embeddedclusteroperator/install.go index 0784d46a2..1ba2f426c 100644 --- a/pkg/addons/embeddedclusteroperator/install.go +++ b/pkg/addons/embeddedclusteroperator/install.go @@ -6,13 +6,14 @@ import ( "github.com/pkg/errors" "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" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) -func (e *EmbeddedClusterOperator) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error { - values, err := e.GenerateHelmValues(ctx, kcli, overrides) +func (e *EmbeddedClusterOperator) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string, writer *spinner.MessageWriter) error { + values, err := e.GenerateHelmValues(ctx, kcli, rc, 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 7e5dd1607..320e4998f 100644 --- a/pkg/addons/embeddedclusteroperator/integration/hostcabundle_test.go +++ b/pkg/addons/embeddedclusteroperator/integration/hostcabundle_test.go @@ -9,6 +9,7 @@ import ( "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" @@ -32,7 +33,9 @@ func TestHostCABundle(t *testing.T) { 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, nil, nil) + rc := runtimeconfig.New(nil) + + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, nil, nil) require.NoError(t, err, "embeddedclusteroperator.Install should not return an error") manifests := addon.DryRunManifests() diff --git a/pkg/addons/embeddedclusteroperator/static/metadata.yaml b/pkg/addons/embeddedclusteroperator/static/metadata.yaml index d66283855..6a1da0dc1 100644 --- a/pkg/addons/embeddedclusteroperator/static/metadata.yaml +++ b/pkg/addons/embeddedclusteroperator/static/metadata.yaml @@ -22,4 +22,4 @@ images: repo: proxy.replicated.com/anonymous/replicated/ec-utils tag: amd64: latest - arm64: latest + arm64: latest \ No newline at end of file diff --git a/pkg/addons/embeddedclusteroperator/upgrade.go b/pkg/addons/embeddedclusteroperator/upgrade.go index f1ff3042b..c1b1880c4 100644 --- a/pkg/addons/embeddedclusteroperator/upgrade.go +++ b/pkg/addons/embeddedclusteroperator/upgrade.go @@ -6,25 +6,26 @@ import ( "github.com/pkg/errors" "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" ) -func (e *EmbeddedClusterOperator) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string) error { +func (e *EmbeddedClusterOperator) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string) error { exists, err := hcli.ReleaseExists(ctx, namespace, releaseName) if err != nil { return errors.Wrap(err, "check if release exists") } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", releaseName, namespace) - if err := e.Install(ctx, logf, kcli, mcli, hcli, overrides, nil); err != nil { + if err := e.Install(ctx, logf, kcli, mcli, hcli, rc, overrides, nil); err != nil { return errors.Wrap(err, "install") } return nil } - values, err := e.GenerateHelmValues(ctx, kcli, overrides) + values, err := e.GenerateHelmValues(ctx, kcli, rc, 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 5de087eba..238ae0ca1 100644 --- a/pkg/addons/embeddedclusteroperator/values.go +++ b/pkg/addons/embeddedclusteroperator/values.go @@ -7,10 +7,11 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "sigs.k8s.io/controller-runtime/pkg/client" ) -func (e *EmbeddedClusterOperator) GenerateHelmValues(ctx context.Context, kcli client.Client, overrides []string) (map[string]interface{}, error) { +func (e *EmbeddedClusterOperator) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, 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 { diff --git a/pkg/addons/embeddedclusteroperator/values_test.go b/pkg/addons/embeddedclusteroperator/values_test.go index 0255728ce..a21cd12e2 100644 --- a/pkg/addons/embeddedclusteroperator/values_test.go +++ b/pkg/addons/embeddedclusteroperator/values_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -13,7 +14,9 @@ func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", } - values, err := e.GenerateHelmValues(context.Background(), nil, nil) + rc := runtimeconfig.New(nil) + + values, err := e.GenerateHelmValues(context.Background(), nil, rc, 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 2a18e59b5..dad9ec4fe 100644 --- a/pkg/addons/highavailability.go +++ b/pkg/addons/highavailability.go @@ -54,7 +54,7 @@ func CanEnableHA(ctx context.Context, kcli client.Client) (bool, string, error) // EnableHA enables high availability. func EnableHA( - ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, kclient kubernetes.Interface, hcli helm.Client, + ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, kclient kubernetes.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, serviceCIDR string, inSpec ecv1beta1.InstallationSpec, spinner *spinner.MessageWriter, ) error { @@ -67,7 +67,7 @@ func EnableHA( return errors.Wrap(err, "check if registry data has been migrated") } else if !hasMigrated { logrus.Debugf("Installing seaweedfs") - err = ensureSeaweedfs(ctx, logf, kcli, mcli, hcli, serviceCIDR, inSpec.Config) + err = ensureSeaweedfs(ctx, logf, kcli, mcli, hcli, rc, serviceCIDR, inSpec.Config) if err != nil { return errors.Wrap(err, "ensure seaweedfs") } @@ -91,7 +91,7 @@ func EnableHA( logrus.Debugf("Enabling high availability for the registry") spinner.Infof("Enabling high availability for the registry") - err = enableRegistryHA(ctx, logf, kcli, mcli, hcli, serviceCIDR, inSpec.Config) + err = enableRegistryHA(ctx, logf, kcli, mcli, hcli, rc, serviceCIDR, inSpec.Config) if err != nil { return errors.Wrap(err, "enable registry high availability") } @@ -101,7 +101,7 @@ func EnableHA( logrus.Debugf("Updating the Admin Console for high availability") spinner.Infof("Updating the Admin Console for high availability") - err := EnableAdminConsoleHA(ctx, logf, kcli, mcli, hcli, inSpec.AirGap, serviceCIDR, inSpec.Proxy, inSpec.Config, inSpec.LicenseInfo) + err := EnableAdminConsoleHA(ctx, logf, kcli, mcli, hcli, rc, inSpec.AirGap, serviceCIDR, inSpec.Proxy, inSpec.Config, inSpec.LicenseInfo) if err != nil { return errors.Wrap(err, "enable admin console high availability") } @@ -203,7 +203,7 @@ func migrateRegistryData(ctx context.Context, kcli client.Client, kclient kubern } // ensureSeaweedfs ensures that seaweedfs is installed. -func ensureSeaweedfs(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, serviceCIDR string, cfgspec *ecv1beta1.ConfigSpec) error { +func ensureSeaweedfs(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, serviceCIDR string, cfgspec *ecv1beta1.ConfigSpec) error { domains := runtimeconfig.GetDomains(cfgspec) // TODO (@salah): add support for end user overrides @@ -212,7 +212,7 @@ func ensureSeaweedfs(ctx context.Context, logf types.LogFunc, kcli client.Client ProxyRegistryDomain: domains.ProxyRegistryDomain, } - if err := sw.Upgrade(ctx, logf, kcli, mcli, hcli, addOnOverrides(sw, cfgspec, nil)); err != nil { + if err := sw.Upgrade(ctx, logf, kcli, mcli, hcli, rc, addOnOverrides(sw, cfgspec, nil)); err != nil { return errors.Wrap(err, "upgrade seaweedfs") } @@ -221,7 +221,7 @@ func ensureSeaweedfs(ctx context.Context, logf types.LogFunc, kcli client.Client // enableRegistryHA enables high availability for the registry and scales the registry deployment // to the desired number of replicas. -func enableRegistryHA(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, serviceCIDR string, cfgspec *ecv1beta1.ConfigSpec) error { +func enableRegistryHA(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, serviceCIDR string, cfgspec *ecv1beta1.ConfigSpec) error { domains := runtimeconfig.GetDomains(cfgspec) // TODO (@salah): add support for end user overrides @@ -230,7 +230,7 @@ func enableRegistryHA(ctx context.Context, logf types.LogFunc, kcli client.Clien ProxyRegistryDomain: domains.ProxyRegistryDomain, IsHA: true, } - if err := r.Upgrade(ctx, logf, kcli, mcli, hcli, addOnOverrides(r, cfgspec, nil)); err != nil { + if err := r.Upgrade(ctx, logf, kcli, mcli, hcli, rc, addOnOverrides(r, cfgspec, nil)); err != nil { return errors.Wrap(err, "upgrade registry") } @@ -238,7 +238,19 @@ func enableRegistryHA(ctx context.Context, logf types.LogFunc, kcli client.Clien } // EnableAdminConsoleHA enables high availability for the admin console. -func EnableAdminConsoleHA(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, isAirgap bool, serviceCIDR string, proxy *ecv1beta1.ProxySpec, cfgspec *ecv1beta1.ConfigSpec, licenseInfo *ecv1beta1.LicenseInfo) error { +func EnableAdminConsoleHA( + ctx context.Context, + logf types.LogFunc, + kcli client.Client, + mcli metadata.Interface, + hcli helm.Client, + rc runtimeconfig.RuntimeConfig, + isAirgap bool, + serviceCIDR string, + proxy *ecv1beta1.ProxySpec, + cfgspec *ecv1beta1.ConfigSpec, + licenseInfo *ecv1beta1.LicenseInfo, +) error { domains := runtimeconfig.GetDomains(cfgspec) // TODO (@salah): add support for end user overrides @@ -252,7 +264,7 @@ func EnableAdminConsoleHA(ctx context.Context, logf types.LogFunc, kcli client.C ProxyRegistryDomain: domains.ProxyRegistryDomain, ReplicatedRegistryDomain: domains.ReplicatedRegistryDomain, } - if err := ac.Upgrade(ctx, logf, kcli, mcli, hcli, addOnOverrides(ac, cfgspec, nil)); err != nil { + if err := ac.Upgrade(ctx, logf, kcli, mcli, hcli, rc, addOnOverrides(ac, cfgspec, nil)); err != nil { return errors.Wrap(err, "upgrade admin console") } diff --git a/pkg/addons/install.go b/pkg/addons/install.go index 2f7f5b682..388f1eda0 100644 --- a/pkg/addons/install.go +++ b/pkg/addons/install.go @@ -36,7 +36,7 @@ type InstallOptions struct { IsRestore bool } -func Install(ctx context.Context, logf types.LogFunc, hcli helm.Client, opts InstallOptions) error { +func Install(ctx context.Context, logf types.LogFunc, hcli helm.Client, rc runtimeconfig.RuntimeConfig, opts InstallOptions) error { kcli, err := kubeutils.KubeClient() if err != nil { return errors.Wrap(err, "create kube client") @@ -47,9 +47,9 @@ func Install(ctx context.Context, logf types.LogFunc, hcli helm.Client, opts Ins return errors.Wrap(err, "create metadata client") } - addons := getAddOnsForInstall(opts) + addons := getAddOnsForInstall(rc, opts) if opts.IsRestore { - addons = getAddOnsForRestore(opts) + addons = getAddOnsForRestore(rc, opts) } for _, addon := range addons { @@ -58,7 +58,7 @@ func Install(ctx context.Context, logf types.LogFunc, hcli helm.Client, opts Ins overrides := addOnOverrides(addon, opts.EmbeddedConfigSpec, opts.EndUserConfigSpec) - if err := addon.Install(ctx, logf, kcli, mcli, hcli, overrides, loading); err != nil { + if err := addon.Install(ctx, logf, kcli, mcli, hcli, rc, overrides, loading); err != nil { loading.ErrorClosef("Failed to install %s", addon.Name()) return errors.Wrapf(err, "install %s", addon.Name()) } @@ -69,7 +69,7 @@ func Install(ctx context.Context, logf types.LogFunc, hcli helm.Client, opts Ins return nil } -func getAddOnsForInstall(opts InstallOptions) []types.AddOn { +func getAddOnsForInstall(rc runtimeconfig.RuntimeConfig, opts InstallOptions) []types.AddOn { domains := runtimeconfig.GetDomains(opts.EmbeddedConfigSpec) addOns := []types.AddOn{ @@ -96,7 +96,7 @@ func getAddOnsForInstall(opts InstallOptions) []types.AddOn { ProxyRegistryDomain: domains.ProxyRegistryDomain, Proxy: opts.Proxy, HostCABundlePath: opts.HostCABundlePath, - EmbeddedClusterK0sSubDir: runtimeconfig.EmbeddedClusterK0sSubDir(), + EmbeddedClusterK0sSubDir: rc.EmbeddedClusterK0sSubDir(), }) } @@ -120,7 +120,7 @@ func getAddOnsForInstall(opts InstallOptions) []types.AddOn { return addOns } -func getAddOnsForRestore(opts InstallOptions) []types.AddOn { +func getAddOnsForRestore(rc runtimeconfig.RuntimeConfig, opts InstallOptions) []types.AddOn { domains := runtimeconfig.GetDomains(opts.EmbeddedConfigSpec) addOns := []types.AddOn{ @@ -131,7 +131,7 @@ func getAddOnsForRestore(opts InstallOptions) []types.AddOn { Proxy: opts.Proxy, ProxyRegistryDomain: domains.ProxyRegistryDomain, HostCABundlePath: opts.HostCABundlePath, - EmbeddedClusterK0sSubDir: runtimeconfig.EmbeddedClusterK0sSubDir(), + EmbeddedClusterK0sSubDir: rc.EmbeddedClusterK0sSubDir(), }, } return addOns diff --git a/pkg/addons/install_test.go b/pkg/addons/install_test.go index 1df61b0d4..400bd3027 100644 --- a/pkg/addons/install_test.go +++ b/pkg/addons/install_test.go @@ -4,6 +4,7 @@ import ( "testing" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" "github.com/replicatedhq/embedded-cluster/pkg/addons/embeddedclusteroperator" "github.com/replicatedhq/embedded-cluster/pkg/addons/openebs" @@ -36,7 +37,7 @@ func Test_getAddOnsForInstall(t *testing.T) { openEBS, ok := addons[0].(*openebs.OpenEBS) require.True(t, ok, "first addon should be OpenEBS") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) eco, ok := addons[1].(*embeddedclusteroperator.EmbeddedClusterOperator) require.True(t, ok, "second addon should be EmbeddedClusterOperator") @@ -47,7 +48,7 @@ func Test_getAddOnsForInstall(t *testing.T) { assert.Empty(t, eco.ImageRepoOverride, "ECO should not have an image repo override") assert.Empty(t, eco.ImageTagOverride, "ECO should not have an image tag override") assert.Empty(t, eco.UtilsImageOverride, "ECO should not have a utils image override") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) adminConsole, ok := addons[2].(*adminconsole.AdminConsole) require.True(t, ok, "third addon should be AdminConsole") @@ -56,9 +57,9 @@ func Test_getAddOnsForInstall(t *testing.T) { assert.Nil(t, adminConsole.Proxy, "AdminConsole should not have a proxy") assert.Empty(t, adminConsole.ServiceCIDR, "AdminConsole should not have a service CIDR") assert.Equal(t, "password123", adminConsole.Password) - assert.Equal(t, runtimeconfig.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) - assert.Equal(t, runtimeconfig.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) }, }, { @@ -161,7 +162,7 @@ defaultDomains: openEBS, ok := addons[0].(*openebs.OpenEBS) require.True(t, ok, "first addon should be OpenEBS") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) eco, ok := addons[1].(*embeddedclusteroperator.EmbeddedClusterOperator) require.True(t, ok, "second addon should be EmbeddedClusterOperator") @@ -172,12 +173,12 @@ defaultDomains: assert.Empty(t, eco.ImageRepoOverride, "ECO should not have an image repo override") assert.Empty(t, eco.ImageTagOverride, "ECO should not have an image tag override") assert.Empty(t, eco.UtilsImageOverride, "ECO should not have a utils image override") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) reg, ok := addons[2].(*registry.Registry) require.True(t, ok, "third addon should be Registry") assert.Equal(t, "10.96.0.0/12", reg.ServiceCIDR) - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, reg.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, reg.ProxyRegistryDomain) adminConsole, ok := addons[3].(*adminconsole.AdminConsole) require.True(t, ok, "fourth addon should be AdminConsole") @@ -186,9 +187,9 @@ defaultDomains: assert.Nil(t, adminConsole.Proxy, "AdminConsole should not have a proxy") assert.Equal(t, "10.96.0.0/12", adminConsole.ServiceCIDR) assert.Equal(t, "password123", adminConsole.Password) - assert.Equal(t, runtimeconfig.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) - assert.Equal(t, runtimeconfig.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) }, }, { @@ -204,7 +205,7 @@ defaultDomains: openEBS, ok := addons[0].(*openebs.OpenEBS) require.True(t, ok, "first addon should be OpenEBS") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) eco, ok := addons[1].(*embeddedclusteroperator.EmbeddedClusterOperator) require.True(t, ok, "second addon should be EmbeddedClusterOperator") @@ -215,12 +216,12 @@ defaultDomains: assert.Empty(t, eco.ImageRepoOverride, "ECO should not have an image repo override") assert.Empty(t, eco.ImageTagOverride, "ECO should not have an image tag override") assert.Empty(t, eco.UtilsImageOverride, "ECO should not have a utils image override") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) vel, ok := addons[2].(*velero.Velero) require.True(t, ok, "third addon should be Velero") assert.Nil(t, vel.Proxy, "Velero should not have a proxy") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, vel.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, vel.ProxyRegistryDomain) adminConsole, ok := addons[3].(*adminconsole.AdminConsole) require.True(t, ok, "fourth addon should be AdminConsole") @@ -229,9 +230,9 @@ defaultDomains: assert.Nil(t, adminConsole.Proxy, "AdminConsole should not have a proxy") assert.Equal(t, "10.96.0.0/12", adminConsole.ServiceCIDR) assert.Equal(t, "password123", adminConsole.Password) - assert.Equal(t, runtimeconfig.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) - assert.Equal(t, runtimeconfig.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) }, }, { @@ -252,7 +253,7 @@ defaultDomains: openEBS, ok := addons[0].(*openebs.OpenEBS) require.True(t, ok, "first addon should be OpenEBS") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, openEBS.ProxyRegistryDomain) eco, ok := addons[1].(*embeddedclusteroperator.EmbeddedClusterOperator) require.True(t, ok, "second addon should be EmbeddedClusterOperator") @@ -265,20 +266,20 @@ defaultDomains: assert.Empty(t, eco.ImageRepoOverride, "ECO should not have an image repo override") assert.Empty(t, eco.ImageTagOverride, "ECO should not have an image tag override") assert.Empty(t, eco.UtilsImageOverride, "ECO should not have a utils image override") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, eco.ProxyRegistryDomain) reg, ok := addons[2].(*registry.Registry) require.True(t, ok, "third addon should be Registry") assert.Equal(t, "10.96.0.0/12", reg.ServiceCIDR) assert.False(t, reg.IsHA, "Registry should not be in high availability mode") - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, reg.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, reg.ProxyRegistryDomain) vel, ok := addons[3].(*velero.Velero) require.True(t, ok, "fourth addon should be Velero") assert.Equal(t, "http://proxy.example.com", vel.Proxy.HTTPProxy) assert.Equal(t, "https://proxy.example.com", vel.Proxy.HTTPSProxy) assert.Equal(t, "localhost,127.0.0.1", vel.Proxy.NoProxy) - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, vel.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, vel.ProxyRegistryDomain) adminConsole, ok := addons[4].(*adminconsole.AdminConsole) require.True(t, ok, "fifth addon should be AdminConsole") @@ -289,9 +290,9 @@ defaultDomains: assert.Equal(t, "localhost,127.0.0.1", adminConsole.Proxy.NoProxy) assert.Equal(t, "10.96.0.0/12", adminConsole.ServiceCIDR) assert.Equal(t, "password123", adminConsole.Password) - assert.Equal(t, runtimeconfig.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) - assert.Equal(t, runtimeconfig.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) - assert.Equal(t, runtimeconfig.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedAppDomain, adminConsole.ReplicatedAppDomain) + assert.Equal(t, domains.DefaultProxyRegistryDomain, adminConsole.ProxyRegistryDomain) + assert.Equal(t, domains.DefaultReplicatedRegistryDomain, adminConsole.ReplicatedRegistryDomain) }, }, { @@ -389,7 +390,8 @@ defaultDomains: if tt.before != nil { tt.before() } - addons := getAddOnsForInstall(tt.opts) + rc := runtimeconfig.New(nil) + addons := getAddOnsForInstall(rc, tt.opts) tt.verify(t, addons) if tt.after != nil { tt.after() diff --git a/pkg/addons/openebs/install.go b/pkg/addons/openebs/install.go index 66ac529bf..d67fb1056 100644 --- a/pkg/addons/openebs/install.go +++ b/pkg/addons/openebs/install.go @@ -6,13 +6,14 @@ import ( "github.com/pkg/errors" "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" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" ) -func (o *OpenEBS) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error { - values, err := o.GenerateHelmValues(ctx, kcli, overrides) +func (o *OpenEBS) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string, writer *spinner.MessageWriter) error { + values, err := o.GenerateHelmValues(ctx, kcli, rc, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/openebs/upgrade.go b/pkg/addons/openebs/upgrade.go index 2e04fa0a5..3d2912902 100644 --- a/pkg/addons/openebs/upgrade.go +++ b/pkg/addons/openebs/upgrade.go @@ -6,25 +6,26 @@ import ( "github.com/pkg/errors" "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" ) -func (o *OpenEBS) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string) error { +func (o *OpenEBS) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string) error { exists, err := hcli.ReleaseExists(ctx, namespace, releaseName) if err != nil { return errors.Wrap(err, "check if release exists") } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", releaseName, namespace) - if err := o.Install(ctx, logf, kcli, mcli, hcli, overrides, nil); err != nil { + if err := o.Install(ctx, logf, kcli, mcli, hcli, rc, overrides, nil); err != nil { return errors.Wrap(err, "install") } return nil } - values, err := o.GenerateHelmValues(ctx, kcli, overrides) + values, err := o.GenerateHelmValues(ctx, kcli, rc, 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 38fe29f9f..0b3745d7f 100644 --- a/pkg/addons/openebs/values.go +++ b/pkg/addons/openebs/values.go @@ -10,7 +10,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func (o *OpenEBS) GenerateHelmValues(ctx context.Context, kcli client.Client, overrides []string) (map[string]interface{}, error) { +func (o *OpenEBS) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, 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 { @@ -27,7 +27,7 @@ func (o *OpenEBS) GenerateHelmValues(ctx context.Context, kcli client.Client, ov return nil, errors.Wrap(err, "unmarshal helm values") } - err = helm.SetValue(copiedValues, "localpv-provisioner.localpv.basePath", runtimeconfig.EmbeddedClusterOpenEBSLocalSubDir()) + err = helm.SetValue(copiedValues, "localpv-provisioner.localpv.basePath", rc.EmbeddedClusterOpenEBSLocalSubDir()) 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 1b4b77036..89ea72edc 100644 --- a/pkg/addons/registry/install.go +++ b/pkg/addons/registry/install.go @@ -9,6 +9,7 @@ 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" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "golang.org/x/crypto/bcrypt" corev1 "k8s.io/api/core/v1" @@ -17,7 +18,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func (r *Registry) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error { +func (r *Registry) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string, writer *spinner.MessageWriter) error { registryIP, err := GetRegistryClusterIP(r.ServiceCIDR) if err != nil { return errors.Wrap(err, "get registry cluster IP") @@ -27,7 +28,7 @@ func (r *Registry) Install(ctx context.Context, logf types.LogFunc, kcli client. return errors.Wrap(err, "create prerequisites") } - values, err := r.GenerateHelmValues(ctx, kcli, overrides) + values, err := r.GenerateHelmValues(ctx, kcli, rc, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/registry/upgrade.go b/pkg/addons/registry/upgrade.go index 0a4cec7a3..08ec964db 100644 --- a/pkg/addons/registry/upgrade.go +++ b/pkg/addons/registry/upgrade.go @@ -7,6 +7,7 @@ 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" @@ -22,14 +23,14 @@ const ( ) // Upgrade upgrades the registry chart to the latest version. -func (r *Registry) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string) error { +func (r *Registry) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string) error { exists, err := hcli.ReleaseExists(ctx, namespace, releaseName) if err != nil { return errors.Wrap(err, "check if release exists") } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", releaseName, namespace) - if err := r.Install(ctx, logf, kcli, mcli, hcli, overrides, nil); err != nil { + if err := r.Install(ctx, logf, kcli, mcli, hcli, rc, overrides, nil); err != nil { return errors.Wrap(err, "install") } return nil @@ -39,7 +40,7 @@ func (r *Registry) Upgrade(ctx context.Context, logf types.LogFunc, kcli client. return errors.Wrap(err, "create prerequisites") } - values, err := r.GenerateHelmValues(ctx, kcli, overrides) + values, err := r.GenerateHelmValues(ctx, kcli, rc, 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 c0d084bf7..064c0e739 100644 --- a/pkg/addons/registry/values.go +++ b/pkg/addons/registry/values.go @@ -7,13 +7,14 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/embedded-cluster/pkg/addons/seaweedfs" "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" k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) -func (r *Registry) GenerateHelmValues(ctx context.Context, kcli client.Client, overrides []string) (map[string]interface{}, error) { +func (r *Registry) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, 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 2b9e4a9e1..d64dc070d 100644 --- a/pkg/addons/seaweedfs/install.go +++ b/pkg/addons/seaweedfs/install.go @@ -9,6 +9,7 @@ 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" "github.com/replicatedhq/embedded-cluster/pkg/spinner" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -18,12 +19,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func (s *SeaweedFS) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error { +func (s *SeaweedFS) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string, writer *spinner.MessageWriter) error { if err := s.ensurePreRequisites(ctx, kcli); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := s.GenerateHelmValues(ctx, kcli, overrides) + values, err := s.GenerateHelmValues(ctx, kcli, rc, overrides) if err != nil { return errors.Wrap(err, "generate helm values") } diff --git a/pkg/addons/seaweedfs/upgrade.go b/pkg/addons/seaweedfs/upgrade.go index 29abbae68..31b6bbaed 100644 --- a/pkg/addons/seaweedfs/upgrade.go +++ b/pkg/addons/seaweedfs/upgrade.go @@ -6,26 +6,27 @@ import ( "github.com/pkg/errors" "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" ) -func (s *SeaweedFS) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string) error { +func (s *SeaweedFS) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string) error { exists, err := hcli.ReleaseExists(ctx, namespace, releaseName) if err != nil { return errors.Wrap(err, "check if release exists") } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", releaseName, namespace) - return s.Install(ctx, logf, kcli, mcli, hcli, overrides, nil) + return s.Install(ctx, logf, kcli, mcli, hcli, rc, overrides, nil) } if err := s.ensurePreRequisites(ctx, kcli); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := s.GenerateHelmValues(ctx, kcli, overrides) + values, err := s.GenerateHelmValues(ctx, kcli, rc, 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 a8d189f0c..b79cb7605 100644 --- a/pkg/addons/seaweedfs/values.go +++ b/pkg/addons/seaweedfs/values.go @@ -11,7 +11,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func (s *SeaweedFS) GenerateHelmValues(ctx context.Context, kcli client.Client, overrides []string) (map[string]interface{}, error) { +func (s *SeaweedFS) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, 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 { @@ -28,13 +28,13 @@ func (s *SeaweedFS) GenerateHelmValues(ctx context.Context, kcli client.Client, return nil, errors.Wrap(err, "unmarshal helm values") } - dataPath := filepath.Join(runtimeconfig.EmbeddedClusterSeaweedfsSubDir(), "ssd") + dataPath := filepath.Join(rc.EmbeddedClusterSeaweedfsSubDir(), "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(runtimeconfig.EmbeddedClusterSeaweedfsSubDir(), "storage") + logsPath := filepath.Join(rc.EmbeddedClusterSeaweedfsSubDir(), "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 c552c4686..c59169ba3 100644 --- a/pkg/addons/types/types.go +++ b/pkg/addons/types/types.go @@ -4,6 +4,7 @@ import ( "context" "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" @@ -16,7 +17,7 @@ type AddOn interface { Version() string ReleaseName() string Namespace() string - GenerateHelmValues(ctx context.Context, kcli client.Client, overrides []string) (map[string]interface{}, error) - Install(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error - Upgrade(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string) error + GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, overrides []string) (map[string]interface{}, error) + Install(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string, writer *spinner.MessageWriter) error + Upgrade(ctx context.Context, logf LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string) error } diff --git a/pkg/addons/upgrade.go b/pkg/addons/upgrade.go index bb9f4f409..012b6b832 100644 --- a/pkg/addons/upgrade.go +++ b/pkg/addons/upgrade.go @@ -24,7 +24,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func Upgrade(ctx context.Context, logf types.LogFunc, hcli helm.Client, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) error { +func Upgrade(ctx context.Context, logf types.LogFunc, hcli helm.Client, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) error { kcli, err := kubeutils.KubeClient() if err != nil { return errors.Wrap(err, "create kube client") @@ -35,12 +35,12 @@ func Upgrade(ctx context.Context, logf types.LogFunc, hcli helm.Client, in *ecv1 return errors.Wrap(err, "create metadata client") } - addons, err := getAddOnsForUpgrade(in, meta) + addons, err := getAddOnsForUpgrade(rc, in, meta) if err != nil { return errors.Wrap(err, "get addons for upgrade") } for _, addon := range addons { - if err := upgradeAddOn(ctx, logf, hcli, kcli, mcli, in, addon); err != nil { + if err := upgradeAddOn(ctx, logf, hcli, kcli, mcli, rc, in, addon); err != nil { return errors.Wrapf(err, "addon %s", addon.Name()) } } @@ -48,7 +48,7 @@ func Upgrade(ctx context.Context, logf types.LogFunc, hcli helm.Client, in *ecv1 return nil } -func getAddOnsForUpgrade(in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) ([]types.AddOn, error) { +func getAddOnsForUpgrade(rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation, meta *ectypes.ReleaseMetadata) ([]types.AddOn, error) { domains := runtimeconfig.GetDomains(in.Spec.Config) addOns := []types.AddOn{ @@ -111,7 +111,7 @@ func getAddOnsForUpgrade(in *ecv1beta1.Installation, meta *ectypes.ReleaseMetada Proxy: in.Spec.Proxy, ProxyRegistryDomain: domains.ProxyRegistryDomain, HostCABundlePath: hostCABundlePath, - EmbeddedClusterK0sSubDir: runtimeconfig.EmbeddedClusterK0sSubDir(), + EmbeddedClusterK0sSubDir: rc.EmbeddedClusterK0sSubDir(), }) } @@ -129,7 +129,7 @@ func getAddOnsForUpgrade(in *ecv1beta1.Installation, meta *ectypes.ReleaseMetada return addOns, nil } -func upgradeAddOn(ctx context.Context, logf types.LogFunc, hcli helm.Client, kcli client.Client, mcli metadata.Interface, in *ecv1beta1.Installation, addon types.AddOn) error { +func upgradeAddOn(ctx context.Context, logf types.LogFunc, hcli helm.Client, kcli client.Client, mcli metadata.Interface, rc runtimeconfig.RuntimeConfig, in *ecv1beta1.Installation, addon types.AddOn) error { // check if we already processed this addon if kubeutils.CheckInstallationConditionStatus(in.Status, conditionName(addon)) == metav1.ConditionTrue { slog.Info(addon.Name() + " is ready") @@ -146,7 +146,7 @@ func upgradeAddOn(ctx context.Context, logf types.LogFunc, hcli helm.Client, kcl // TODO (@salah): add support for end user overrides overrides := addOnOverrides(addon, in.Spec.Config, nil) - err := addon.Upgrade(ctx, logf, kcli, mcli, hcli, overrides) + err := addon.Upgrade(ctx, logf, kcli, mcli, hcli, rc, overrides) if err != nil { message := helpers.CleanErrorMessage(err) if err := setCondition(ctx, kcli, in, conditionName(addon), metav1.ConditionFalse, "UpgradeFailed", message); err != nil { diff --git a/pkg/addons/upgrade_test.go b/pkg/addons/upgrade_test.go index 15ec82e24..15bf4353b 100644 --- a/pkg/addons/upgrade_test.go +++ b/pkg/addons/upgrade_test.go @@ -12,6 +12,7 @@ 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" ) @@ -261,7 +262,8 @@ func Test_getAddOnsForUpgrade(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - addons, err := getAddOnsForUpgrade(tt.in, tt.meta) + rc := runtimeconfig.New(nil) + addons, err := getAddOnsForUpgrade(rc, tt.in, tt.meta) tt.verify(t, addons, err) }) } diff --git a/pkg/addons/velero/install.go b/pkg/addons/velero/install.go index 52eb6a321..f12a7307f 100644 --- a/pkg/addons/velero/install.go +++ b/pkg/addons/velero/install.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" "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" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -15,12 +16,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func (v *Velero) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string, writer *spinner.MessageWriter) error { +func (v *Velero) Install(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string, writer *spinner.MessageWriter) error { if err := v.createPreRequisites(ctx, kcli); err != nil { return errors.Wrap(err, "create prerequisites") } - values, err := v.GenerateHelmValues(ctx, kcli, overrides) + values, err := v.GenerateHelmValues(ctx, kcli, rc, 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 0ddf37a8f..0f1abc1ad 100644 --- a/pkg/addons/velero/integration/hostcabundle_test.go +++ b/pkg/addons/velero/integration/hostcabundle_test.go @@ -7,6 +7,7 @@ import ( "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" @@ -24,7 +25,9 @@ func TestHostCABundle(t *testing.T) { 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, nil, nil) + rc := runtimeconfig.New(nil) + + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, nil, 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 88982469c..448ad4650 100644 --- a/pkg/addons/velero/integration/k0ssubdir_test.go +++ b/pkg/addons/velero/integration/k0ssubdir_test.go @@ -8,6 +8,7 @@ import ( "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" @@ -25,7 +26,9 @@ func TestK0sDir(t *testing.T) { 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, nil, nil) + rc := runtimeconfig.New(nil) + + err = addon.Install(context.Background(), t.Logf, nil, nil, hcli, rc, nil, nil) require.NoError(t, err, "velero.Install should not return an error") manifests := addon.DryRunManifests() diff --git a/pkg/addons/velero/upgrade.go b/pkg/addons/velero/upgrade.go index f8c8f6f1e..323e846af 100644 --- a/pkg/addons/velero/upgrade.go +++ b/pkg/addons/velero/upgrade.go @@ -6,25 +6,26 @@ import ( "github.com/pkg/errors" "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" ) -func (v *Velero) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, overrides []string) error { +func (v *Velero) Upgrade(ctx context.Context, logf types.LogFunc, kcli client.Client, mcli metadata.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, overrides []string) error { exists, err := hcli.ReleaseExists(ctx, namespace, releaseName) if err != nil { return errors.Wrap(err, "check if release exists") } if !exists { logrus.Debugf("Release not found, installing release %s in namespace %s", releaseName, namespace) - if err := v.Install(ctx, logf, kcli, mcli, hcli, overrides, nil); err != nil { + if err := v.Install(ctx, logf, kcli, mcli, hcli, rc, overrides, nil); err != nil { return errors.Wrap(err, "install") } return nil } - values, err := v.GenerateHelmValues(ctx, kcli, overrides) + values, err := v.GenerateHelmValues(ctx, kcli, rc, 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 2937dbbad..a3f483fbe 100644 --- a/pkg/addons/velero/values.go +++ b/pkg/addons/velero/values.go @@ -7,10 +7,11 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "sigs.k8s.io/controller-runtime/pkg/client" ) -func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, overrides []string) (map[string]interface{}, error) { +func (v *Velero) GenerateHelmValues(ctx context.Context, kcli client.Client, rc runtimeconfig.RuntimeConfig, 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 { diff --git a/pkg/addons/velero/values_test.go b/pkg/addons/velero/values_test.go index c6204eb9d..ec19fc30c 100644 --- a/pkg/addons/velero/values_test.go +++ b/pkg/addons/velero/values_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -13,7 +14,9 @@ func TestGenerateHelmValues_HostCABundlePath(t *testing.T) { HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", } - values, err := v.GenerateHelmValues(context.Background(), nil, nil) + rc := runtimeconfig.New(nil) + + values, err := v.GenerateHelmValues(context.Background(), nil, rc, nil) require.NoError(t, err, "GenerateHelmValues should not return an error") require.NotEmpty(t, values["extraVolumes"]) diff --git a/pkg/airgap/containerd.go b/pkg/airgap/containerd.go index cb9189595..a730a100f 100644 --- a/pkg/airgap/containerd.go +++ b/pkg/airgap/containerd.go @@ -18,7 +18,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 { - parentDir := runtimeconfig.PathToK0sContainerdConfig() + parentDir := runtimeconfig.K0sContainerdConfigPath contents := fmt.Sprintf(registryConfigTemplate, registry) if err := os.MkdirAll(parentDir, 0755); err != nil { diff --git a/pkg/airgap/materialize.go b/pkg/airgap/materialize.go index b499c8a50..5ee2c8f8b 100644 --- a/pkg/airgap/materialize.go +++ b/pkg/airgap/materialize.go @@ -18,7 +18,7 @@ const K0sImagePath = "images/ec-images-amd64.tar" // MaterializeAirgap places the airgap image bundle for k0s and the embedded cluster charts on disk. // - image bundle should be located at 'images-amd64.tar' within the embedded-cluster directory within the airgap bundle. // - charts should be located at 'charts.tar.gz' within the embedded-cluster directory within the airgap bundle. -func MaterializeAirgap(airgapReader io.Reader) error { +func MaterializeAirgap(rc runtimeconfig.RuntimeConfig, airgapReader io.Reader) error { // decompress tarball ungzip, err := gzip.NewReader(airgapReader) if err != nil { @@ -40,7 +40,7 @@ func MaterializeAirgap(airgapReader io.Reader) error { } if nextFile.Name == "embedded-cluster/images-amd64.tar" { - err = writeOneFile(tarreader, filepath.Join(runtimeconfig.EmbeddedClusterK0sSubDir(), K0sImagePath), nextFile.Mode) + err = writeOneFile(tarreader, filepath.Join(rc.EmbeddedClusterK0sSubDir(), K0sImagePath), nextFile.Mode) if err != nil { return fmt.Errorf("failed to write k0s images file: %w", err) } @@ -48,7 +48,7 @@ func MaterializeAirgap(airgapReader io.Reader) error { } if nextFile.Name == "embedded-cluster/charts.tar.gz" { - err = writeChartFiles(tarreader) + err = writeChartFiles(rc, tarreader) if err != nil { return fmt.Errorf("failed to write chart files: %w", err) } @@ -63,7 +63,7 @@ func MaterializeAirgap(airgapReader io.Reader) error { // FetchAndWriteArtifacts fetches the k0s images and Helm charts from the KOTS API // and writes them to the appropriate directories -func FetchAndWriteArtifacts(ctx context.Context, kotsAPIAddress string) error { +func FetchAndWriteArtifacts(ctx context.Context, kotsAPIAddress string, rc runtimeconfig.RuntimeConfig) error { // Fetch and write k0s images imagesFile, err := kotsadm.GetK0sImagesFile(ctx, kotsAPIAddress) if err != nil { @@ -71,7 +71,7 @@ func FetchAndWriteArtifacts(ctx context.Context, kotsAPIAddress string) error { } defer imagesFile.Close() - if err := writeOneFile(imagesFile, filepath.Join(runtimeconfig.EmbeddedClusterK0sSubDir(), K0sImagePath), 0644); err != nil { + if err := writeOneFile(imagesFile, filepath.Join(rc.EmbeddedClusterK0sSubDir(), K0sImagePath), 0644); err != nil { return fmt.Errorf("failed to write k0s images file: %w", err) } @@ -82,7 +82,7 @@ func FetchAndWriteArtifacts(ctx context.Context, kotsAPIAddress string) error { } defer chartsTGZ.Close() - if err := writeChartFiles(chartsTGZ); err != nil { + if err := writeChartFiles(rc, chartsTGZ); err != nil { return fmt.Errorf("failed to write chart files: %w", err) } return nil @@ -112,7 +112,7 @@ func writeOneFile(reader io.Reader, path string, mode int64) error { } // take in a stream of a tarball and write the charts contained within to disk -func writeChartFiles(reader io.Reader) error { +func writeChartFiles(rc runtimeconfig.RuntimeConfig, reader io.Reader) error { // decompress tarball ungzip, err := gzip.NewReader(reader) if err != nil { @@ -136,7 +136,7 @@ func writeChartFiles(reader io.Reader) error { continue } - subdir := runtimeconfig.EmbeddedClusterChartsSubDir() + subdir := rc.EmbeddedClusterChartsSubDir() dst := filepath.Join(subdir, nextFile.Name) if err := writeOneFile(tarreader, dst, nextFile.Mode); err != nil { return fmt.Errorf("failed to write chart file: %w", err) diff --git a/pkg/airgap/remap.go b/pkg/airgap/remap.go deleted file mode 100644 index 194a9ad0a..000000000 --- a/pkg/airgap/remap.go +++ /dev/null @@ -1,26 +0,0 @@ -package airgap - -import ( - "fmt" - "path/filepath" - - "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" -) - -// RemapHelm removes all helm repositories from the cluster config, and changes the upstreams of all helm charts -// to paths on the host within the charts directory -func RemapHelm(cfg *v1beta1.ClusterConfig) { - // there's no upstream to reach, so we can zero out the repositories - cfg.Spec.Extensions.Helm.Repositories = nil - - // replace each chart's name with the path on the host it should be found at - // see https://docs.k0sproject.io/v1.29.2+k0s.0/helm-charts/#example - for idx := range cfg.Spec.Extensions.Helm.Charts { - cfg.Spec.Extensions.Helm.Charts[idx].ChartName = helmChartHostPath(cfg.Spec.Extensions.Helm.Charts[idx]) - } -} - -func helmChartHostPath(chart v1beta1.Chart) string { - return filepath.Join(runtimeconfig.EmbeddedClusterChartsSubDir(), fmt.Sprintf("%s-%s.tgz", chart.Name, chart.Version)) -} diff --git a/pkg/config/config.go b/pkg/config/config.go index 353e917d5..51c02c682 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -117,14 +117,14 @@ func PatchK0sConfig(config *k0sconfig.ClusterConfig, patch string, respectImmuta } // InstallFlags returns a list of default flags to be used when bootstrapping a k0s cluster. -func InstallFlags(nodeIP string) ([]string, error) { +func InstallFlags(rc runtimeconfig.RuntimeConfig, nodeIP string) ([]string, error) { flags := []string{ "install", "controller", "--labels", strings.Join(nodeLabels(), ","), "--enable-worker", "--no-taints", - "-c", runtimeconfig.PathToK0sConfig(), + "-c", runtimeconfig.K0sConfigPath, } profile, err := ProfileInstallFlag() if err != nil { @@ -133,17 +133,17 @@ func InstallFlags(nodeIP string) ([]string, error) { if profile != "" { flags = append(flags, profile) } - flags = append(flags, AdditionalInstallFlags(nodeIP)...) + flags = append(flags, AdditionalInstallFlags(rc, nodeIP)...) flags = append(flags, AdditionalInstallFlagsController()...) return flags, nil } -func AdditionalInstallFlags(nodeIP string) []string { +func AdditionalInstallFlags(rc runtimeconfig.RuntimeConfig, nodeIP string) []string { return []string{ // NOTE: quotes are not supported in older systemd // kardianos/service will escape spaces with "\x20" "--kubelet-extra-args", fmt.Sprintf("--node-ip=%s", nodeIP), - "--data-dir", runtimeconfig.EmbeddedClusterK0sSubDir(), + "--data-dir", rc.EmbeddedClusterK0sSubDir(), } } @@ -211,7 +211,7 @@ func additionalControllerLabels() map[string]string { func controllerWorkerProfile() (string, error) { // Read the k0s config file - k0sPath := runtimeconfig.PathToK0sConfig() + k0sPath := runtimeconfig.K0sConfigPath if k0sConfigPathOverride != "" { k0sPath = k0sConfigPathOverride } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 0b19d92fd..203088e88 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -9,6 +9,7 @@ import ( "testing" k0sconfig "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" @@ -100,7 +101,7 @@ func Test_extractK0sConfigPatch(t *testing.T) { } func TestRenderK0sConfig(t *testing.T) { - cfg := RenderK0sConfig(runtimeconfig.DefaultProxyRegistryDomain) + cfg := RenderK0sConfig(domains.DefaultProxyRegistryDomain) assert.Equal(t, "calico", cfg.Spec.Network.Provider) assert.Equal(t, DefaultServiceNodePortRange, cfg.Spec.API.ExtraArgs["service-node-port-range"]) @@ -138,6 +139,8 @@ func TestInstallFlags(t *testing.T) { err = os.WriteFile(profileTmpFile.Name(), k0sProfileConfigBytes, 0644) require.NoError(t, err) + rc := runtimeconfig.New(nil) + tests := []struct { name string nodeIP string @@ -158,9 +161,9 @@ func TestInstallFlags(t *testing.T) { "--labels", "kots.io/embedded-cluster-role-0=controller,kots.io/embedded-cluster-role=total-1", "--enable-worker", "--no-taints", - "-c", runtimeconfig.PathToK0sConfig(), + "-c", runtimeconfig.K0sConfigPath, "--kubelet-extra-args", "--node-ip=192.168.1.10", - "--data-dir", runtimeconfig.EmbeddedClusterK0sSubDir(), + "--data-dir", rc.EmbeddedClusterK0sSubDir(), "--disable-components", "konnectivity-server", "--enable-dynamic-config", }, @@ -190,10 +193,10 @@ spec: "--labels", "environment=test,kots.io/embedded-cluster-role-0=custom-controller,kots.io/embedded-cluster-role=total-1", "--enable-worker", "--no-taints", - "-c", runtimeconfig.PathToK0sConfig(), + "-c", runtimeconfig.K0sConfigPath, "--profile=test-profile", "--kubelet-extra-args", "--node-ip=192.168.1.10", - "--data-dir", runtimeconfig.EmbeddedClusterK0sSubDir(), + "--data-dir", rc.EmbeddedClusterK0sSubDir(), "--disable-components", "konnectivity-server", "--enable-dynamic-config", }, @@ -217,7 +220,7 @@ spec: }) // Run test - flags, err := InstallFlags(tt.nodeIP) + flags, err := InstallFlags(rc, tt.nodeIP) if tt.expectedError { require.Error(t, err) assert.Contains(t, err.Error(), tt.expectedErrMsg) diff --git a/pkg/config/images_test.go b/pkg/config/images_test.go index 02a06a759..76984730c 100644 --- a/pkg/config/images_test.go +++ b/pkg/config/images_test.go @@ -5,11 +5,11 @@ import ( "testing" "github.com/k0sproject/k0s/pkg/airgap" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "github.com/replicatedhq/embedded-cluster/pkg-new/domains" ) func TestListK0sImages(t *testing.T) { - original := airgap.GetImageURIs(RenderK0sConfig(runtimeconfig.DefaultProxyRegistryDomain).Spec, true) + original := airgap.GetImageURIs(RenderK0sConfig(domains.DefaultProxyRegistryDomain).Spec, true) if len(original) == 0 { t.Errorf("airgap.GetImageURIs() = %v, want not empty", original) } @@ -35,7 +35,7 @@ func TestListK0sImages(t *testing.T) { t.Errorf("airgap.GetImageURIs() = %v, want to contain apiserver-network-proxy-agent", original) } - filtered := ListK0sImages(RenderK0sConfig(runtimeconfig.DefaultProxyRegistryDomain)) + filtered := ListK0sImages(RenderK0sConfig(domains.DefaultProxyRegistryDomain)) if len(filtered) == 0 { t.Errorf("ListK0sImages() = %v, want not empty", filtered) } diff --git a/pkg/helpers/fs.go b/pkg/helpers/fs.go index 6fd7ddffe..902e1c03b 100644 --- a/pkg/helpers/fs.go +++ b/pkg/helpers/fs.go @@ -96,3 +96,36 @@ func RemoveAll(path string) error { } return me.ErrorOrNil() } + +// CopyFile copies a file from src to dst, creating parent directories as needed. +// The destination file will be created with the specified mode. +func CopyFile(src, dst string, mode os.FileMode) error { + if src == "" { + return fmt.Errorf("source path cannot be empty") + } + + srcinfo, err := os.Stat(src) + if err != nil { + return fmt.Errorf("stat source file: %w", err) + } + + if srcinfo.IsDir() { + return fmt.Errorf("cannot copy directory %s", src) + } + + // Create parent directories + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return fmt.Errorf("create parent directories: %w", err) + } + + data, err := os.ReadFile(src) + if err != nil { + return fmt.Errorf("read source file: %w", err) + } + + if err := os.WriteFile(dst, data, mode); err != nil { + return fmt.Errorf("write destination file: %w", err) + } + + return nil +} diff --git a/pkg/helpers/fs_test.go b/pkg/helpers/fs_test.go index 909af289f..e0c91e49b 100644 --- a/pkg/helpers/fs_test.go +++ b/pkg/helpers/fs_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -193,3 +194,123 @@ func TestRemoveAll(t *testing.T) { }) } } + +func TestCopyFile(t *testing.T) { + // Create a temporary directory for test files + tmpDir := t.TempDir() + + tests := []struct { + name string + src string + dst string + mode os.FileMode + setup func() error + wantErr bool + errContains string + }{ + { + name: "successful copy", + src: filepath.Join(tmpDir, "source.txt"), + dst: filepath.Join(tmpDir, "subdir", "dest.txt"), + mode: 0644, + setup: func() error { + return os.WriteFile(filepath.Join(tmpDir, "source.txt"), []byte("test content"), 0644) + }, + wantErr: false, + }, + { + name: "empty source", + src: "", + dst: filepath.Join(tmpDir, "dest.txt"), + mode: 0644, + setup: func() error { return nil }, + wantErr: true, + errContains: "source path cannot be empty", + }, + { + name: "source is directory", + src: filepath.Join(tmpDir, "sourcedir"), + dst: filepath.Join(tmpDir, "dest.txt"), + mode: 0644, + setup: func() error { + return os.Mkdir(filepath.Join(tmpDir, "sourcedir"), 0755) + }, + wantErr: true, + errContains: "cannot copy directory", + }, + { + name: "source does not exist", + src: filepath.Join(tmpDir, "nonexistent.txt"), + dst: filepath.Join(tmpDir, "dest.txt"), + mode: 0644, + setup: func() error { + return nil + }, + wantErr: true, + errContains: "stat source file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup test case + if err := tt.setup(); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + // Run CopyFile + err := CopyFile(tt.src, tt.dst, tt.mode) + + // Check error + if tt.wantErr { + if err == nil { + t.Error("CopyFile() error = nil, want error") + } else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("CopyFile() error = %v, want error containing %q", err, tt.errContains) + } + return + } + + if err != nil { + t.Errorf("CopyFile() error = %v, want nil", err) + return + } + + // Verify file was copied correctly + if tt.wantErr { + return + } + + // Check if destination file exists + if _, err := os.Stat(tt.dst); err != nil { + t.Errorf("Destination file does not exist: %v", err) + return + } + + // Check file mode + info, err := os.Stat(tt.dst) + if err != nil { + t.Errorf("Failed to stat destination file: %v", err) + return + } + if info.Mode() != tt.mode { + t.Errorf("Destination file mode = %v, want %v", info.Mode(), tt.mode) + } + + // Check file contents + srcContent, err := os.ReadFile(tt.src) + if err != nil { + t.Errorf("Failed to read source file: %v", err) + return + } + dstContent, err := os.ReadFile(tt.dst) + if err != nil { + t.Errorf("Failed to read destination file: %v", err) + return + } + if string(srcContent) != string(dstContent) { + t.Errorf("Destination file content = %q, want %q", string(dstContent), string(srcContent)) + } + }) + } +} diff --git a/pkg/k0s/install.go b/pkg/k0s/install.go index 0593f3148..47d205f5c 100644 --- a/pkg/k0s/install.go +++ b/pkg/k0s/install.go @@ -20,9 +20,9 @@ import ( // Install runs the k0s install command and waits for it to finish. If no configuration // is found one is generated. -func Install(networkInterface string) error { - ourbin := runtimeconfig.PathToEmbeddedClusterBinary("k0s") - hstbin := runtimeconfig.K0sBinaryPath() +func Install(rc runtimeconfig.RuntimeConfig, networkInterface string) 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) } @@ -31,7 +31,7 @@ func Install(networkInterface string) error { if err != nil { return fmt.Errorf("unable to find first valid address: %w", err) } - flags, err := config.InstallFlags(nodeIP) + flags, err := config.InstallFlags(rc, nodeIP) if err != nil { return fmt.Errorf("unable to get install flags: %w", err) } @@ -47,7 +47,7 @@ func Install(networkInterface string) error { // IsInstalled checks if the embedded cluster is already installed by looking for // the k0s configuration file existence. func IsInstalled() (bool, error) { - _, err := os.Stat(runtimeconfig.PathToK0sConfig()) + _, err := os.Stat(runtimeconfig.K0sConfigPath) if err == nil { return true, nil } else if os.IsNotExist(err) { @@ -58,10 +58,10 @@ func IsInstalled() (bool, error) { } // WriteK0sConfig creates a new k0s.yaml configuration file. The file is saved in the -// global location (as returned by runtimeconfig.PathToK0sConfig()). If a file already sits +// 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, overrides string, mutate func(*k0sv1beta1.ClusterConfig) error) (*k0sv1beta1.ClusterConfig, error) { - cfgpath := runtimeconfig.PathToK0sConfig() + cfgpath := runtimeconfig.K0sConfigPath if _, err := os.Stat(cfgpath); err == nil { return nil, fmt.Errorf("configuration file already exists") } @@ -100,9 +100,9 @@ func WriteK0sConfig(ctx context.Context, networkInterface string, airgapBundle s if airgapBundle != "" { // update the k0s config to install with airgap - airgap.RemapHelm(cfg) airgap.SetAirgapConfig(cfg) } + // This is necessary to install the previous version of k0s in e2e tests // TODO: remove this once the previous version is > 1.29 unstructured, err := helpers.K0sClusterConfigTo129Compat(cfg) diff --git a/pkg/metrics/reporter.go b/pkg/metrics/reporter.go index 66c833243..72004fbb7 100644 --- a/pkg/metrics/reporter.go +++ b/pkg/metrics/reporter.go @@ -9,9 +9,9 @@ import ( "sync" "github.com/google/uuid" + apitypes "github.com/replicatedhq/embedded-cluster/api/types" "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/replicatedhq/embedded-cluster/pkg/metrics/types" - preflightstypes "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/versions" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -171,7 +171,7 @@ func (r *Reporter) ReportJoinFailed(ctx context.Context, err error) { } // ReportPreflightsFailed reports that the preflights failed. -func (r *Reporter) ReportPreflightsFailed(ctx context.Context, output preflightstypes.Output) { +func (r *Reporter) ReportPreflightsFailed(ctx context.Context, output *apitypes.HostPreflightOutput) { outputJSON, err := json.Marshal(output) if err != nil { logrus.Warnf("unable to marshal preflight output: %s", err) @@ -187,7 +187,7 @@ func (r *Reporter) ReportPreflightsFailed(ctx context.Context, output preflights } // ReportPreflightsBypassed reports that the preflights failed but were bypassed. -func (r *Reporter) ReportPreflightsBypassed(ctx context.Context, output preflightstypes.Output) { +func (r *Reporter) ReportPreflightsBypassed(ctx context.Context, output *apitypes.HostPreflightOutput) { outputJSON, err := json.Marshal(output) if err != nil { logrus.Warnf("unable to marshal preflight output: %s", err) diff --git a/pkg/metrics/reporter_interface.go b/pkg/metrics/reporter_interface.go new file mode 100644 index 000000000..54687a35a --- /dev/null +++ b/pkg/metrics/reporter_interface.go @@ -0,0 +1,41 @@ +package metrics + +import ( + "context" + "os" + + apitypes "github.com/replicatedhq/embedded-cluster/api/types" +) + +// ReporterInterface defines the interface for reporting various events in the system. +type ReporterInterface interface { + // ReportInstallationStarted reports that the installation has started + ReportInstallationStarted(ctx context.Context, licenseID string, appSlug string) + + // ReportInstallationSucceeded reports that the installation has succeeded + ReportInstallationSucceeded(ctx context.Context) + + // ReportInstallationFailed reports that the installation has failed + ReportInstallationFailed(ctx context.Context, err error) + + // ReportJoinStarted reports that a join has started + ReportJoinStarted(ctx context.Context) + + // ReportJoinSucceeded reports that a join has finished successfully + ReportJoinSucceeded(ctx context.Context) + + // ReportJoinFailed reports that a join has failed + ReportJoinFailed(ctx context.Context, err error) + + // ReportPreflightsFailed reports that the preflights failed + ReportPreflightsFailed(ctx context.Context, output *apitypes.HostPreflightOutput) + + // ReportPreflightsBypassed reports that the preflights failed but were bypassed + ReportPreflightsBypassed(ctx context.Context, output *apitypes.HostPreflightOutput) + + // ReportSignalAborted reports that a process was terminated by a signal + ReportSignalAborted(ctx context.Context, signal os.Signal) +} + +// Ensure Reporter implements ReporterInterface +var _ ReporterInterface = (*Reporter)(nil) diff --git a/pkg/metrics/reporter_mock.go b/pkg/metrics/reporter_mock.go new file mode 100644 index 000000000..03c844466 --- /dev/null +++ b/pkg/metrics/reporter_mock.go @@ -0,0 +1,61 @@ +package metrics + +import ( + "context" + "os" + + apitypes "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/stretchr/testify/mock" +) + +var _ ReporterInterface = (*MockReporter)(nil) + +// MockReporter is a mock implementation of the ReporterInterface +type MockReporter struct { + mock.Mock +} + +// ReportInstallationStarted mocks the ReportInstallationStarted method +func (m *MockReporter) ReportInstallationStarted(ctx context.Context, licenseID string, appSlug string) { + m.Called(ctx, licenseID, appSlug) +} + +// ReportInstallationSucceeded mocks the ReportInstallationSucceeded method +func (m *MockReporter) ReportInstallationSucceeded(ctx context.Context) { + m.Called(ctx) +} + +// ReportInstallationFailed mocks the ReportInstallationFailed method +func (m *MockReporter) ReportInstallationFailed(ctx context.Context, err error) { + m.Called(ctx, err) +} + +// ReportJoinStarted mocks the ReportJoinStarted method +func (m *MockReporter) ReportJoinStarted(ctx context.Context) { + m.Called(ctx) +} + +// ReportJoinSucceeded mocks the ReportJoinSucceeded method +func (m *MockReporter) ReportJoinSucceeded(ctx context.Context) { + m.Called(ctx) +} + +// ReportJoinFailed mocks the ReportJoinFailed method +func (m *MockReporter) ReportJoinFailed(ctx context.Context, err error) { + m.Called(ctx, err) +} + +// ReportPreflightsFailed mocks the ReportPreflightsFailed method +func (m *MockReporter) ReportPreflightsFailed(ctx context.Context, output *apitypes.HostPreflightOutput) { + m.Called(ctx, output) +} + +// ReportPreflightsBypassed mocks the ReportPreflightsBypassed method +func (m *MockReporter) ReportPreflightsBypassed(ctx context.Context, output *apitypes.HostPreflightOutput) { + m.Called(ctx, output) +} + +// ReportSignalAborted mocks the ReportSignalAborted method +func (m *MockReporter) ReportSignalAborted(ctx context.Context, signal os.Signal) { + m.Called(ctx, signal) +} diff --git a/pkg/metrics/interface.go b/pkg/metrics/sender_interface.go similarity index 100% rename from pkg/metrics/interface.go rename to pkg/metrics/sender_interface.go diff --git a/pkg/metrics/sender_mock.go b/pkg/metrics/sender_mock.go new file mode 100644 index 000000000..f36b0eb17 --- /dev/null +++ b/pkg/metrics/sender_mock.go @@ -0,0 +1,20 @@ +package metrics + +import ( + "context" + + "github.com/replicatedhq/embedded-cluster/pkg/metrics/types" + "github.com/stretchr/testify/mock" +) + +var _ SenderInterface = (*MockSender)(nil) + +// MockSender is a mock implementation of the SenderInterface +type MockSender struct { + mock.Mock +} + +// Send mocks the Send method +func (m *MockSender) Send(ctx context.Context, baseURL string, ev types.Event) { + m.Called(ctx, baseURL, ev) +} diff --git a/pkg/netutil/https.go b/pkg/netutils/https.go similarity index 91% rename from pkg/netutil/https.go rename to pkg/netutils/https.go index 442792416..8861b9f21 100644 --- a/pkg/netutil/https.go +++ b/pkg/netutils/https.go @@ -1,4 +1,4 @@ -package netutil +package netutils import "strings" diff --git a/pkg/preflights/run.go b/pkg/preflights/run.go deleted file mode 100644 index 3aa45d936..000000000 --- a/pkg/preflights/run.go +++ /dev/null @@ -1,220 +0,0 @@ -package preflights - -import ( - "context" - "fmt" - "runtime" - - ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/dryrun" - "github.com/replicatedhq/embedded-cluster/pkg/metrics" - "github.com/replicatedhq/embedded-cluster/pkg/preflights/types" - "github.com/replicatedhq/embedded-cluster/pkg/prompts" - "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" - "github.com/replicatedhq/embedded-cluster/pkg/spinner" - "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" - "github.com/sirupsen/logrus" -) - -// ErrPreflightsHaveFail is an error returned when we managed to execute the host preflights but -// they contain failures. We use this to differentiate the way we provide user feedback. -var ErrPreflightsHaveFail = metrics.NewErrorNoFail(fmt.Errorf("host preflight failures detected")) - -type PrepareAndRunOptions struct { - ReplicatedAppURL string - ProxyRegistryURL string - Proxy *ecv1beta1.ProxySpec - PodCIDR string - ServiceCIDR string - GlobalCIDR *string - NodeIP string - IsAirgap bool - SkipHostPreflights bool - IgnoreHostPreflights bool - AssumeYes bool - TCPConnectionsRequired []string - MetricsReporter MetricsReporter - IsJoin bool -} - -type MetricsReporter interface { - ReportPreflightsFailed(ctx context.Context, output types.Output) - ReportPreflightsBypassed(ctx context.Context, output types.Output) -} - -func PrepareAndRun(ctx context.Context, opts PrepareAndRunOptions) error { - hpf := release.GetHostPreflights() - if hpf == nil { - hpf = &v1beta2.HostPreflightSpec{} - } - - data, err := types.TemplateData{ - ReplicatedAppURL: opts.ReplicatedAppURL, - ProxyRegistryURL: opts.ProxyRegistryURL, - IsAirgap: opts.IsAirgap, - AdminConsolePort: runtimeconfig.AdminConsolePort(), - LocalArtifactMirrorPort: runtimeconfig.LocalArtifactMirrorPort(), - DataDir: runtimeconfig.EmbeddedClusterHomeDirectory(), - K0sDataDir: runtimeconfig.EmbeddedClusterK0sSubDir(), - OpenEBSDataDir: runtimeconfig.EmbeddedClusterOpenEBSLocalSubDir(), - SystemArchitecture: runtime.GOARCH, - FromCIDR: opts.PodCIDR, - ToCIDR: opts.ServiceCIDR, - TCPConnectionsRequired: opts.TCPConnectionsRequired, - NodeIP: opts.NodeIP, - IsJoin: opts.IsJoin, - }.WithCIDRData(opts.PodCIDR, opts.ServiceCIDR, opts.GlobalCIDR) - - if err != nil { - return fmt.Errorf("get host preflights data: %w", err) - } - - if opts.Proxy != nil { - data.HTTPProxy = opts.Proxy.HTTPProxy - data.HTTPSProxy = opts.Proxy.HTTPSProxy - data.ProvidedNoProxy = opts.Proxy.ProvidedNoProxy - data.NoProxy = opts.Proxy.NoProxy - } - - chpfs, err := GetClusterHostPreflights(ctx, data) - if err != nil { - return fmt.Errorf("get cluster host preflights: %w", err) - } - - for _, h := range chpfs { - hpf.Collectors = append(hpf.Collectors, h.Spec.Collectors...) - hpf.Analyzers = append(hpf.Analyzers, h.Spec.Analyzers...) - } - - if dryrun.Enabled() { - dryrun.RecordHostPreflightSpec(hpf) - return nil - } - - return runHostPreflights(ctx, hpf, opts) -} - -func runHostPreflights(ctx context.Context, hpf *v1beta2.HostPreflightSpec, opts PrepareAndRunOptions) error { - if len(hpf.Collectors) == 0 && len(hpf.Analyzers) == 0 { - return nil - } - - spinner := spinner.Start() - - if opts.SkipHostPreflights { - spinner.Closef("Host preflights skipped") - return nil - } - - spinner.Infof("Running host preflights") - - output, stderr, err := Run(ctx, hpf, opts.Proxy) - if err != nil { - spinner.ErrorClosef("Failed to run host preflights") - return fmt.Errorf("host preflights failed to run: %w", err) - } - if stderr != "" { - logrus.Debugf("preflight stderr: %s", stderr) - } - - err = output.SaveToDisk(runtimeconfig.PathToEmbeddedClusterSupportFile("host-preflight-results.json")) - if err != nil { - logrus.Warnf("save preflights output: %v", err) - } - - err = CopyBundleToECSupportDir() - if err != nil { - logrus.Warnf("copy preflight bundle to embedded-cluster support dir: %v", err) - } - - // Failures found - if output.HasFail() { - s := "preflights" - if len(output.Fail) == 1 { - s = "preflight" - } - - if output.HasWarn() { - spinner.ErrorClosef("%d host %s failed and %d warned", len(output.Fail), s, len(output.Warn)) - } else { - spinner.ErrorClosef("%d host %s failed", len(output.Fail), s) - } - - output.PrintTableWithoutInfo() - - if opts.IgnoreHostPreflights { - if opts.AssumeYes { - if opts.MetricsReporter != nil { - opts.MetricsReporter.ReportPreflightsBypassed(ctx, *output) - } - return nil - } - confirmed, err := prompts.New().Confirm("Are you sure you want to ignore these failures and continue installing?", false) - if err != nil { - return fmt.Errorf("failed to get confirmation: %w", err) - } - if confirmed { - if opts.MetricsReporter != nil { - opts.MetricsReporter.ReportPreflightsBypassed(ctx, *output) - } - return nil // user continued after host preflights failed - } - } - - if len(output.Fail)+len(output.Warn) > 1 { - logrus.Info("\n\033[1mPlease address these issues and try again.\033[0m\n") - } else { - logrus.Info("\n\033[1mPlease address this issue and try again.\033[0m\n") - } - - if opts.MetricsReporter != nil { - opts.MetricsReporter.ReportPreflightsFailed(ctx, *output) - } - return ErrPreflightsHaveFail - } - - // Warnings found - if output.HasWarn() { - s := "preflights" - if len(output.Warn) == 1 { - s = "preflight" - } - - spinner.Warnf("%d host %s warned", len(output.Warn), s) - spinner.Close() - if opts.AssumeYes { - // We have warnings but we are not in interactive mode - // so we just print the warnings and continue - output.PrintTableWithoutInfo() - if opts.MetricsReporter != nil { - opts.MetricsReporter.ReportPreflightsBypassed(ctx, *output) - } - return nil - } - - output.PrintTableWithoutInfo() - - confirmed, err := prompts.New().Confirm("Do you want to continue?", false) - if err != nil { - return fmt.Errorf("failed to get confirmation: %w", err) - } - if !confirmed { - if opts.MetricsReporter != nil { - opts.MetricsReporter.ReportPreflightsFailed(ctx, *output) - } - return ErrPreflightsHaveFail - } - - if opts.MetricsReporter != nil { - opts.MetricsReporter.ReportPreflightsBypassed(ctx, *output) - } - return nil - } - - // No failures or warnings - spinner.Infof("Host preflights passed") - spinner.Close() - - return nil -} diff --git a/pkg/preflights/app.go b/pkg/release/ecconfig.go similarity index 74% rename from pkg/preflights/app.go rename to pkg/release/ecconfig.go index f4b53bc43..c0b26d059 100644 --- a/pkg/preflights/app.go +++ b/pkg/release/ecconfig.go @@ -1,16 +1,15 @@ -package preflights +package release import ( "fmt" - "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/sirupsen/logrus" "sigs.k8s.io/yaml" ) -// ValidateApp runs some basic checks on the embedded cluster config. -func ValidateApp() error { - cfg := release.GetEmbeddedClusterConfig() +// ValidateECConfig runs some basic checks on the embedded cluster config. +func ValidateECConfig() error { + cfg := GetEmbeddedClusterConfig() if cfg == nil || cfg.Spec.Extensions.Helm == nil { return nil } diff --git a/pkg/preflights/app_test.go b/pkg/release/ecconfig_test.go similarity index 92% rename from pkg/preflights/app_test.go rename to pkg/release/ecconfig_test.go index 5e97fb6c5..d0ff23972 100644 --- a/pkg/preflights/app_test.go +++ b/pkg/release/ecconfig_test.go @@ -1,9 +1,9 @@ -package preflights +package release import ( - "github.com/replicatedhq/embedded-cluster/pkg/release" - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestValidateApp(t *testing.T) { @@ -88,9 +88,9 @@ spec: t.Run(tt.name, func(t *testing.T) { req := require.New(t) - req.NoError(release.SetReleaseDataForTests(tt.releaseData)) + req.NoError(SetReleaseDataForTests(tt.releaseData)) - err := ValidateApp() + err := ValidateECConfig() if tt.wantErr != "" { req.Error(err) req.Equal(tt.wantErr, err.Error()) diff --git a/pkg/release/release.go b/pkg/release/release.go index e8d9fbdc2..4499b6cb8 100644 --- a/pkg/release/release.go +++ b/pkg/release/release.go @@ -8,10 +8,10 @@ import ( "io" "os" - embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/utils/pkg/embed" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" - "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "gopkg.in/yaml.v2" kruntime "k8s.io/apimachinery/pkg/runtime" @@ -27,16 +27,21 @@ var ( type ReleaseData struct { data []byte Application *kotsv1beta1.Application - HostPreflights *v1beta2.HostPreflightSpec - EmbeddedClusterConfig *embeddedclusterv1beta1.Config + HostPreflights *troubleshootv1beta2.HostPreflightSpec + EmbeddedClusterConfig *ecv1beta1.Config ChannelRelease *ChannelRelease VeleroBackup *velerov1.Backup VeleroRestore *velerov1.Restore } +// GetReleaseData returns the release data. +func GetReleaseData() *ReleaseData { + return _releaseData +} + // GetHostPreflights returns a list of HostPreflight specs that are found in the // binary. These are part of the embedded Kots Application Release. -func GetHostPreflights() *v1beta2.HostPreflightSpec { +func GetHostPreflights() *troubleshootv1beta2.HostPreflightSpec { return _releaseData.HostPreflights } @@ -49,7 +54,7 @@ func GetApplication() *kotsv1beta1.Application { // GetEmbeddedClusterConfig reads the embedded cluster config from the embedded Kots // Application Release. -func GetEmbeddedClusterConfig() *embeddedclusterv1beta1.Config { +func GetEmbeddedClusterConfig() *ecv1beta1.Config { return _releaseData.EmbeddedClusterConfig } @@ -121,7 +126,7 @@ func parseApplication(data []byte) (*kotsv1beta1.Application, error) { return &app, nil } -func parseHostPreflights(data []byte) (*v1beta2.HostPreflightSpec, error) { +func parseHostPreflights(data []byte) (*troubleshootv1beta2.HostPreflightSpec, error) { if len(data) == 0 { return nil, nil } @@ -129,24 +134,24 @@ func parseHostPreflights(data []byte) (*v1beta2.HostPreflightSpec, error) { } // unserializeHostPreflightSpec unserializes a HostPreflightSpec from a raw slice of bytes. -func unserializeHostPreflightSpec(data []byte) (*v1beta2.HostPreflightSpec, error) { +func unserializeHostPreflightSpec(data []byte) (*troubleshootv1beta2.HostPreflightSpec, error) { scheme := kruntime.NewScheme() - if err := v1beta2.AddToScheme(scheme); err != nil { + if err := troubleshootv1beta2.AddToScheme(scheme); err != nil { return nil, err } decoder := conversion.NewDecoder(scheme) - var hpf v1beta2.HostPreflight + var hpf troubleshootv1beta2.HostPreflight if err := decoder.DecodeInto(data, &hpf); err != nil { return nil, err } return &hpf.Spec, nil } -func parseEmbeddedClusterConfig(data []byte) (*embeddedclusterv1beta1.Config, error) { +func parseEmbeddedClusterConfig(data []byte) (*ecv1beta1.Config, error) { if len(data) == 0 { return nil, nil } - var cfg embeddedclusterv1beta1.Config + var cfg ecv1beta1.Config if err := kyaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("unable to unmarshal embedded cluster config: %w", err) } @@ -256,7 +261,7 @@ func (r *ReleaseData) parse() error { } if hostPreflights != nil { if r.HostPreflights == nil { - r.HostPreflights = &v1beta2.HostPreflightSpec{} + r.HostPreflights = &troubleshootv1beta2.HostPreflightSpec{} } r.HostPreflights.Collectors = append(r.HostPreflights.Collectors, hostPreflights.Collectors...) r.HostPreflights.Analyzers = append(r.HostPreflights.Analyzers, hostPreflights.Analyzers...) diff --git a/pkg/runtimeconfig/defaults.go b/pkg/runtimeconfig/defaults.go index a671537bb..be58520b0 100644 --- a/pkg/runtimeconfig/defaults.go +++ b/pkg/runtimeconfig/defaults.go @@ -6,6 +6,7 @@ import ( "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" ) @@ -20,15 +21,22 @@ var DefaultNoProxy = []string{ "169.254.169.254", } -const DefaultReplicatedAppDomain = "replicated.app" -const DefaultProxyRegistryDomain = "proxy.replicated.com" -const DefaultReplicatedRegistryDomain = "registry.replicated.com" -const KotsadmNamespace = "kotsadm" -const KotsadmServiceAccount = "kotsadm" -const SeaweedFSNamespace = "seaweedfs" -const RegistryNamespace = "registry" -const VeleroNamespace = "velero" -const EmbeddedClusterNamespace = "embedded-cluster" +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" + K0sConfigPath = "/etc/k0s/k0s.yaml" + K0sContainerdConfigPath = "/etc/k0s/containerd.d/" + ECConfigPath = "/etc/embedded-cluster/ec.yaml" +) // BinaryName returns the binary name, this is useful for places where we // need to present the name of the binary to the user (the name may vary if @@ -59,71 +67,8 @@ func PathToLog(name string) string { return filepath.Join(EmbeddedClusterLogsSubDir(), name) } -// K0sBinaryPath returns the path to the k0s binary when it is installed on the node. This -// does not return the binary just after we materialized it but the path we want it to be -// once it is installed. -func K0sBinaryPath() string { - return "/usr/local/bin/k0s" -} - -// PathToK0sStatusSocket returns the full path to the k0s status socket. -func PathToK0sStatusSocket() string { - return "/run/k0s/status.sock" -} - -// PathToK0sConfig returns the full path to the k0s configuration file. -func PathToK0sConfig() string { - return "/etc/k0s/k0s.yaml" -} - -// PathToK0sContainerdConfig returns the full path to the k0s containerd configuration directory -func PathToK0sContainerdConfig() string { - return "/etc/k0s/containerd.d/" -} - -// PathToECConfig returns the full path to the embedded cluster configuration file. -// This file is used to specify the embedded cluster data directory. -func PathToECConfig() string { - return "/etc/embedded-cluster/ec.yaml" -} - // 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 { - replicatedAppDomain := DefaultReplicatedAppDomain - proxyRegistryDomain := DefaultProxyRegistryDomain - replicatedRegistryDomain := DefaultReplicatedRegistryDomain - - // get defaults from channel release if available - rel := release.GetChannelRelease() - if rel != nil { - if rel.DefaultDomains.ReplicatedAppDomain != "" { - replicatedAppDomain = rel.DefaultDomains.ReplicatedAppDomain - } - if rel.DefaultDomains.ProxyRegistryDomain != "" { - proxyRegistryDomain = rel.DefaultDomains.ProxyRegistryDomain - } - if rel.DefaultDomains.ReplicatedRegistryDomain != "" { - replicatedRegistryDomain = rel.DefaultDomains.ReplicatedRegistryDomain - } - } - - // get overrides from config spec if available - if cfgspec != nil { - if cfgspec.Domains.ReplicatedAppDomain != "" { - replicatedAppDomain = cfgspec.Domains.ReplicatedAppDomain - } - if cfgspec.Domains.ProxyRegistryDomain != "" { - proxyRegistryDomain = cfgspec.Domains.ProxyRegistryDomain - } - if cfgspec.Domains.ReplicatedRegistryDomain != "" { - replicatedRegistryDomain = cfgspec.Domains.ReplicatedRegistryDomain - } - } - - return ecv1beta1.Domains{ - ReplicatedAppDomain: replicatedAppDomain, - ProxyRegistryDomain: proxyRegistryDomain, - ReplicatedRegistryDomain: replicatedRegistryDomain, - } + return domains.GetDomains(cfgspec, release.GetChannelRelease()) } diff --git a/pkg/runtimeconfig/interface.go b/pkg/runtimeconfig/interface.go new file mode 100644 index 000000000..c041eb7aa --- /dev/null +++ b/pkg/runtimeconfig/interface.go @@ -0,0 +1,35 @@ +package runtimeconfig + +import ( + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" +) + +// RuntimeConfig defines the interface for managing runtime configuration +type RuntimeConfig interface { + Get() *ecv1beta1.RuntimeConfigSpec + Set(spec *ecv1beta1.RuntimeConfigSpec) + Cleanup() + EmbeddedClusterHomeDirectory() string + EmbeddedClusterTmpSubDir() string + EmbeddedClusterBinsSubDir() string + EmbeddedClusterChartsSubDir() string + EmbeddedClusterChartsSubDirNoCreate() string + EmbeddedClusterImagesSubDir() string + EmbeddedClusterK0sSubDir() string + EmbeddedClusterSeaweedfsSubDir() string + EmbeddedClusterOpenEBSLocalSubDir() string + PathToEmbeddedClusterBinary(name string) string + PathToKubeConfig() string + PathToKubeletConfig() string + EmbeddedClusterSupportSubDir() string + PathToEmbeddedClusterSupportFile(name string) string + WriteToDisk() error + LocalArtifactMirrorPort() int + AdminConsolePort() int + HostCABundlePath() string + SetDataDir(dataDir string) + SetLocalArtifactMirrorPort(port int) + SetAdminConsolePort(port int) + SetManagerPort(port int) + SetHostCABundlePath(hostCABundlePath string) +} diff --git a/pkg/runtimeconfig/mock.go b/pkg/runtimeconfig/mock.go new file mode 100644 index 000000000..c5f15ed8f --- /dev/null +++ b/pkg/runtimeconfig/mock.go @@ -0,0 +1,165 @@ +package runtimeconfig + +import ( + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/stretchr/testify/mock" +) + +var _ RuntimeConfig = (*MockRuntimeConfig)(nil) + +// MockRuntimeConfig is a mock implementation of the RuntimeConfig interface +type MockRuntimeConfig struct { + mock.Mock +} + +// Get mocks the Get method +func (m *MockRuntimeConfig) Get() *ecv1beta1.RuntimeConfigSpec { + args := m.Called() + if args.Get(0) == nil { + return nil + } + return args.Get(0).(*ecv1beta1.RuntimeConfigSpec) +} + +// Set mocks the Set method +func (m *MockRuntimeConfig) Set(spec *ecv1beta1.RuntimeConfigSpec) { + m.Called(spec) +} + +// Cleanup mocks the Cleanup method +func (m *MockRuntimeConfig) Cleanup() { + m.Called() +} + +// EmbeddedClusterHomeDirectory mocks the EmbeddedClusterHomeDirectory method +func (m *MockRuntimeConfig) EmbeddedClusterHomeDirectory() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterTmpSubDir mocks the EmbeddedClusterTmpSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterTmpSubDir() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterBinsSubDir mocks the EmbeddedClusterBinsSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterBinsSubDir() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterChartsSubDir mocks the EmbeddedClusterChartsSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterChartsSubDir() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterChartsSubDirNoCreate mocks the EmbeddedClusterChartsSubDirNoCreate method +func (m *MockRuntimeConfig) EmbeddedClusterChartsSubDirNoCreate() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterImagesSubDir mocks the EmbeddedClusterImagesSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterImagesSubDir() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterK0sSubDir mocks the EmbeddedClusterK0sSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterK0sSubDir() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterSeaweedfsSubDir mocks the EmbeddedClusterSeaweedfsSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterSeaweedfsSubDir() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterOpenEBSLocalSubDir mocks the EmbeddedClusterOpenEBSLocalSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterOpenEBSLocalSubDir() string { + args := m.Called() + return args.String(0) +} + +// PathToEmbeddedClusterBinary mocks the PathToEmbeddedClusterBinary method +func (m *MockRuntimeConfig) PathToEmbeddedClusterBinary(name string) string { + args := m.Called(name) + return args.String(0) +} + +// PathToKubeConfig mocks the PathToKubeConfig method +func (m *MockRuntimeConfig) PathToKubeConfig() string { + args := m.Called() + return args.String(0) +} + +// PathToKubeletConfig mocks the PathToKubeletConfig method +func (m *MockRuntimeConfig) PathToKubeletConfig() string { + args := m.Called() + return args.String(0) +} + +// EmbeddedClusterSupportSubDir mocks the EmbeddedClusterSupportSubDir method +func (m *MockRuntimeConfig) EmbeddedClusterSupportSubDir() string { + args := m.Called() + return args.String(0) +} + +// PathToEmbeddedClusterSupportFile mocks the PathToEmbeddedClusterSupportFile method +func (m *MockRuntimeConfig) PathToEmbeddedClusterSupportFile(name string) string { + args := m.Called(name) + return args.String(0) +} + +// WriteToDisk mocks the WriteToDisk method +func (m *MockRuntimeConfig) WriteToDisk() error { + args := m.Called() + return args.Error(0) +} + +// LocalArtifactMirrorPort mocks the LocalArtifactMirrorPort method +func (m *MockRuntimeConfig) LocalArtifactMirrorPort() int { + args := m.Called() + return args.Int(0) +} + +// AdminConsolePort mocks the AdminConsolePort method +func (m *MockRuntimeConfig) AdminConsolePort() int { + args := m.Called() + return args.Int(0) +} + +// HostCABundlePath mocks the HostCABundlePath method +func (m *MockRuntimeConfig) HostCABundlePath() string { + args := m.Called() + return args.String(0) +} + +// SetDataDir mocks the SetDataDir method +func (m *MockRuntimeConfig) SetDataDir(dataDir string) { + m.Called(dataDir) +} + +// SetLocalArtifactMirrorPort mocks the SetLocalArtifactMirrorPort method +func (m *MockRuntimeConfig) SetLocalArtifactMirrorPort(port int) { + m.Called(port) +} + +// SetAdminConsolePort mocks the SetAdminConsolePort method +func (m *MockRuntimeConfig) SetAdminConsolePort(port int) { + m.Called(port) +} + +// SetManagerPort mocks the SetManagerPort method +func (m *MockRuntimeConfig) SetManagerPort(port int) { + m.Called(port) +} + +// SetHostCABundlePath mocks the SetHostCABundlePath method +func (m *MockRuntimeConfig) SetHostCABundlePath(hostCABundlePath string) { + m.Called(hostCABundlePath) +} diff --git a/pkg/runtimeconfig/runtimeconfig.go b/pkg/runtimeconfig/runtimeconfig.go index 41cfbcb83..b204b14e0 100644 --- a/pkg/runtimeconfig/runtimeconfig.go +++ b/pkg/runtimeconfig/runtimeconfig.go @@ -11,24 +11,47 @@ import ( "sigs.k8s.io/yaml" ) -var ( - runtimeConfig = ecv1beta1.GetDefaultRuntimeConfig() -) +type runtimeConfig struct { + spec *ecv1beta1.RuntimeConfigSpec +} -func Set(rc *ecv1beta1.RuntimeConfigSpec) { - if rc == nil { - // runtime config is nil in old installation objects so this keeps the default. - return +// New creates a new RuntimeConfig instance +func New(spec *ecv1beta1.RuntimeConfigSpec) RuntimeConfig { + if spec == nil { + spec = ecv1beta1.GetDefaultRuntimeConfig() } - runtimeConfig = rc + return &runtimeConfig{spec: spec} } -func Get() *ecv1beta1.RuntimeConfigSpec { - return runtimeConfig +func NewFromDisk() (RuntimeConfig, error) { + location := ECConfigPath + data, err := os.ReadFile(location) + if err != nil { + return nil, fmt.Errorf("unable to read runtime config: %w", err) + } + + var spec ecv1beta1.RuntimeConfigSpec + if err := yaml.Unmarshal(data, &spec); err != nil { + return nil, fmt.Errorf("unable to unmarshal runtime config: %w", err) + } + + return New(&spec), nil +} + +func (rc *runtimeConfig) Get() *ecv1beta1.RuntimeConfigSpec { + return rc.spec +} + +func (rc *runtimeConfig) Set(spec *ecv1beta1.RuntimeConfigSpec) { + if spec == nil { + // runtime config is nil in old installation objects so this keeps the default. + return + } + rc.spec = spec } -func Cleanup() { - tmpDir := EmbeddedClusterTmpSubDir() +func (rc *runtimeConfig) Cleanup() { + tmpDir := rc.EmbeddedClusterTmpSubDir() // We should not delete the tmp dir, rather we should empty its contents leaving // it in place. This is because commands such as `kubectl edit ` // will create files in the tmp dir @@ -39,18 +62,17 @@ func Cleanup() { // EmbeddedClusterHomeDirectory returns the parent directory. Inside this parent directory we // store all the embedded-cluster related files. -func EmbeddedClusterHomeDirectory() string { - if runtimeConfig.DataDir != "" { - return runtimeConfig.DataDir +func (rc *runtimeConfig) EmbeddedClusterHomeDirectory() string { + if rc.spec.DataDir != "" { + return rc.spec.DataDir } return ecv1beta1.DefaultDataDir } // EmbeddedClusterTmpSubDir returns the path to the tmp directory where embedded-cluster // stores temporary files. -func EmbeddedClusterTmpSubDir() string { - path := filepath.Join(EmbeddedClusterHomeDirectory(), "tmp") - +func (rc *runtimeConfig) EmbeddedClusterTmpSubDir() string { + path := filepath.Join(rc.EmbeddedClusterHomeDirectory(), "tmp") if err := os.MkdirAll(path, 0755); err != nil { logrus.Fatalf("unable to create embedded-cluster tmp dir: %s", err) } @@ -59,9 +81,8 @@ func EmbeddedClusterTmpSubDir() string { // EmbeddedClusterBinsSubDir returns the path to the directory where embedded-cluster binaries // are stored. -func EmbeddedClusterBinsSubDir() string { - path := filepath.Join(EmbeddedClusterHomeDirectory(), "bin") - +func (rc *runtimeConfig) EmbeddedClusterBinsSubDir() string { + path := filepath.Join(rc.EmbeddedClusterHomeDirectory(), "bin") if err := os.MkdirAll(path, 0755); err != nil { logrus.Fatalf("unable to create embedded-cluster bin dir: %s", err) } @@ -70,9 +91,8 @@ func EmbeddedClusterBinsSubDir() string { // EmbeddedClusterChartsSubDir returns the path to the directory where embedded-cluster helm charts // are stored. -func EmbeddedClusterChartsSubDir() string { - path := filepath.Join(EmbeddedClusterHomeDirectory(), "charts") - +func (rc *runtimeConfig) EmbeddedClusterChartsSubDir() string { + path := filepath.Join(rc.EmbeddedClusterHomeDirectory(), "charts") if err := os.MkdirAll(path, 0755); err != nil { logrus.Fatalf("unable to create embedded-cluster charts dir: %s", err) } @@ -81,13 +101,13 @@ func EmbeddedClusterChartsSubDir() string { // EmbeddedClusterChartsSubDirNoCreate returns the path to the directory where embedded-cluster helm charts // are stored without creating the directory if it does not exist. -func EmbeddedClusterChartsSubDirNoCreate() string { - return filepath.Join(EmbeddedClusterHomeDirectory(), "charts") +func (rc *runtimeConfig) EmbeddedClusterChartsSubDirNoCreate() string { + return filepath.Join(rc.EmbeddedClusterHomeDirectory(), "charts") } // EmbeddedClusterImagesSubDir returns the path to the directory where docker images are stored. -func EmbeddedClusterImagesSubDir() string { - path := filepath.Join(EmbeddedClusterHomeDirectory(), "images") +func (rc *runtimeConfig) EmbeddedClusterImagesSubDir() string { + path := filepath.Join(rc.EmbeddedClusterHomeDirectory(), "images") if err := os.MkdirAll(path, 0755); err != nil { logrus.Fatalf("unable to create embedded-cluster images dir: %s", err) } @@ -95,48 +115,48 @@ func EmbeddedClusterImagesSubDir() string { } // EmbeddedClusterK0sSubDir returns the path to the directory where k0s data is stored. -func EmbeddedClusterK0sSubDir() string { - if runtimeConfig.K0sDataDirOverride != "" { - return runtimeConfig.K0sDataDirOverride +func (rc *runtimeConfig) EmbeddedClusterK0sSubDir() string { + if rc.spec.K0sDataDirOverride != "" { + return rc.spec.K0sDataDirOverride } - return filepath.Join(EmbeddedClusterHomeDirectory(), "k0s") + return filepath.Join(rc.EmbeddedClusterHomeDirectory(), "k0s") } // EmbeddedClusterSeaweedfsSubDir returns the path to the directory where seaweedfs data is stored. -func EmbeddedClusterSeaweedfsSubDir() string { - return filepath.Join(EmbeddedClusterHomeDirectory(), "seaweedfs") +func (rc *runtimeConfig) EmbeddedClusterSeaweedfsSubDir() string { + return filepath.Join(rc.EmbeddedClusterHomeDirectory(), "seaweedfs") } // EmbeddedClusterOpenEBSLocalSubDir returns the path to the directory where OpenEBS local data is stored. -func EmbeddedClusterOpenEBSLocalSubDir() string { - if runtimeConfig.OpenEBSDataDirOverride != "" { - return runtimeConfig.OpenEBSDataDirOverride +func (rc *runtimeConfig) EmbeddedClusterOpenEBSLocalSubDir() string { + if rc.spec.OpenEBSDataDirOverride != "" { + return rc.spec.OpenEBSDataDirOverride } - return filepath.Join(EmbeddedClusterHomeDirectory(), "openebs-local") + return filepath.Join(rc.EmbeddedClusterHomeDirectory(), "openebs-local") } // PathToEmbeddedClusterBinary is an utility function that returns the full path to a // materialized binary that belongs to embedded-cluster. This function does not check // if the file exists. -func PathToEmbeddedClusterBinary(name string) string { - return filepath.Join(EmbeddedClusterBinsSubDir(), name) +func (rc *runtimeConfig) PathToEmbeddedClusterBinary(name string) string { + return filepath.Join(rc.EmbeddedClusterBinsSubDir(), name) } // PathToKubeConfig returns the path to the kubeconfig file. -func PathToKubeConfig() string { - return filepath.Join(EmbeddedClusterK0sSubDir(), "pki/admin.conf") +func (rc *runtimeConfig) PathToKubeConfig() string { + return filepath.Join(rc.EmbeddedClusterK0sSubDir(), "pki/admin.conf") } // PathToKubeletConfig returns the path to the kubelet config file. -func PathToKubeletConfig() string { - return filepath.Join(EmbeddedClusterK0sSubDir(), "kubelet.conf") +func (rc *runtimeConfig) PathToKubeletConfig() string { + return filepath.Join(rc.EmbeddedClusterK0sSubDir(), "kubelet.conf") } // EmbeddedClusterSupportSubDir returns the path to the directory where embedded-cluster // support files are stored. Things that are useful when providing end user support in // a running cluster should be stored into this directory. -func EmbeddedClusterSupportSubDir() string { - path := filepath.Join(EmbeddedClusterHomeDirectory(), "support") +func (rc *runtimeConfig) EmbeddedClusterSupportSubDir() string { + path := filepath.Join(rc.EmbeddedClusterHomeDirectory(), "support") if err := os.MkdirAll(path, 0700); err != nil { logrus.Fatalf("unable to create embedded-cluster support dir: %s", err) } @@ -145,13 +165,12 @@ func EmbeddedClusterSupportSubDir() string { // PathToEmbeddedClusterSupportFile is an utility function that returns the full path to // a materialized support file. This function does not check if the file exists. -func PathToEmbeddedClusterSupportFile(name string) string { - return filepath.Join(EmbeddedClusterSupportSubDir(), name) +func (rc *runtimeConfig) PathToEmbeddedClusterSupportFile(name string) string { + return filepath.Join(rc.EmbeddedClusterSupportSubDir(), name) } -func WriteToDisk() error { - location := PathToECConfig() - +func (rc *runtimeConfig) WriteToDisk() error { + location := ECConfigPath err := os.MkdirAll(filepath.Dir(location), 0755) if err != nil { return fmt.Errorf("unable to create runtime config directory: %w", err) @@ -163,7 +182,7 @@ func WriteToDisk() error { return fmt.Errorf("unable to remove existing runtime config: %w", err) } - yml, err := yaml.Marshal(runtimeConfig) + yml, err := yaml.Marshal(rc.spec) if err != nil { return fmt.Errorf("unable to marshal runtime config: %w", err) } @@ -176,56 +195,40 @@ func WriteToDisk() error { return nil } -func ReadFromDisk() (*ecv1beta1.RuntimeConfigSpec, error) { - location := PathToECConfig() - - data, err := os.ReadFile(location) - if err != nil { - return nil, fmt.Errorf("unable to read runtime config: %w", err) - } - - var spec ecv1beta1.RuntimeConfigSpec - if err := yaml.Unmarshal(data, &spec); err != nil { - return nil, fmt.Errorf("unable to unmarshal runtime config: %w", err) - } - - return &spec, nil -} - -func LocalArtifactMirrorPort() int { - if runtimeConfig.LocalArtifactMirror.Port > 0 { - return runtimeConfig.LocalArtifactMirror.Port +func (rc *runtimeConfig) LocalArtifactMirrorPort() int { + if rc.spec.LocalArtifactMirror.Port > 0 { + return rc.spec.LocalArtifactMirror.Port } return ecv1beta1.DefaultLocalArtifactMirrorPort } -func AdminConsolePort() int { - if runtimeConfig.AdminConsole.Port > 0 { - return runtimeConfig.AdminConsole.Port +func (rc *runtimeConfig) AdminConsolePort() int { + if rc.spec.AdminConsole.Port > 0 { + return rc.spec.AdminConsole.Port } return ecv1beta1.DefaultAdminConsolePort } -func HostCABundlePath() string { - return runtimeConfig.HostCABundlePath +func (rc *runtimeConfig) HostCABundlePath() string { + return rc.spec.HostCABundlePath } -func SetDataDir(dataDir string) { - runtimeConfig.DataDir = dataDir +func (rc *runtimeConfig) SetDataDir(dataDir string) { + rc.spec.DataDir = dataDir } -func SetLocalArtifactMirrorPort(port int) { - runtimeConfig.LocalArtifactMirror.Port = port +func (rc *runtimeConfig) SetLocalArtifactMirrorPort(port int) { + rc.spec.LocalArtifactMirror.Port = port } -func SetAdminConsolePort(port int) { - runtimeConfig.AdminConsole.Port = port +func (rc *runtimeConfig) SetAdminConsolePort(port int) { + rc.spec.AdminConsole.Port = port } -func SetManagerPort(port int) { - runtimeConfig.Manager.Port = port +func (rc *runtimeConfig) SetManagerPort(port int) { + rc.spec.Manager.Port = port } -func SetHostCABundlePath(hostCABundlePath string) { - runtimeConfig.HostCABundlePath = hostCABundlePath +func (rc *runtimeConfig) SetHostCABundlePath(hostCABundlePath string) { + rc.spec.HostCABundlePath = hostCABundlePath } diff --git a/pkg/runtimeconfig/util/util.go b/pkg/runtimeconfig/util/util.go index 22c24e2b2..43bef1ed8 100644 --- a/pkg/runtimeconfig/util/util.go +++ b/pkg/runtimeconfig/util/util.go @@ -13,35 +13,35 @@ import ( "github.com/sirupsen/logrus" ) -// InitBestRuntimeConfig initializes the runtime config from the cluster (if it's up) and will fall back to -// the /etc/embedded-cluster/ec.yaml file, the filesystem, or the default. -func InitBestRuntimeConfig(ctx context.Context) { +// InitBestRuntimeConfig returns the best runtime config available. It will try to get the runtime config from +// the cluster (if it's up) and will fall back to the /etc/embedded-cluster/ec.yaml file, the filesystem, or the default. +func InitBestRuntimeConfig(ctx context.Context) runtimeconfig.RuntimeConfig { // It's possible that the cluster is not up - if err := InitRuntimeConfigFromCluster(ctx); err == nil { - return + if rc, err := GetRuntimeConfigFromCluster(ctx); err == nil { + return rc } // There might be a runtime config file - if runtimeConfig, err := runtimeconfig.ReadFromDisk(); err == nil { - runtimeconfig.Set(runtimeConfig) - return + if rc, err := runtimeconfig.NewFromDisk(); err == nil { + return rc } // Otherwise, fall back to the filesystem - if err := InitRuntimeConfigFromFilesystem(); err == nil { - return + if rc, err := GetRuntimeConfigFromFilesystem(); err == nil { + return rc } - // If we can't find a runtime config, keep the default + // If we can't find a runtime config, return the default + return runtimeconfig.New(nil) } -// InitRuntimeConfigFromCluster discovers the runtime config from the installation object. If there is no +// GetRuntimeConfigFromCluster discovers the runtime config from the installation object. If there is no // runtime config, this is probably a prior version of EC so we will have to fall back to the // filesystem. -func InitRuntimeConfigFromCluster(ctx context.Context) error { +func GetRuntimeConfigFromCluster(ctx context.Context) (runtimeconfig.RuntimeConfig, error) { status, err := k0s.GetStatus(ctx) if err != nil { - return fmt.Errorf("get k0s status: %w", err) + return nil, fmt.Errorf("get k0s status: %w", err) } kubeconfigPath := status.Vars.AdminKubeConfigPath @@ -49,49 +49,51 @@ func InitRuntimeConfigFromCluster(ctx context.Context) error { kcli, err := kubeutils.KubeClient() if err != nil { - return fmt.Errorf("create kube client: %w", err) + return nil, fmt.Errorf("create kube client: %w", err) } in, err := kubeutils.GetLatestInstallation(ctx, kcli) if err != nil { - return fmt.Errorf("get latest installation: %w", err) + return nil, fmt.Errorf("get latest installation: %w", err) } if in.Spec.RuntimeConfig == nil || in.Spec.RuntimeConfig.DataDir == "" { // If there is no runtime config, this is probably a prior version of EC so we will have to // fall back to the filesystem. - return InitRuntimeConfigFromFilesystem() + return GetRuntimeConfigFromFilesystem() } - runtimeconfig.Set(in.Spec.RuntimeConfig) - logrus.Debugf("Got runtime config from installation with k0s data dir %s", runtimeconfig.EmbeddedClusterK0sSubDir()) + rc := runtimeconfig.New(in.Spec.RuntimeConfig) + logrus.Debugf("Got runtime config from installation with k0s data dir %s", rc.EmbeddedClusterK0sSubDir()) - return nil + return rc, nil } -// InitRuntimeConfigFromFilesystem returns initializes the runtime config from the filesystem. It supports older versions +// GetRuntimeConfigFromFilesystem returns initializes the runtime config from the filesystem. It supports older versions // of EC that used a different directory for k0s and openebs. -func InitRuntimeConfigFromFilesystem() error { +func GetRuntimeConfigFromFilesystem() (runtimeconfig.RuntimeConfig, error) { + rc := runtimeconfig.New(nil) + // ca.crt is available on both control plane and worker nodes - _, err := os.Stat(filepath.Join(runtimeconfig.EmbeddedClusterK0sSubDir(), "pki/ca.crt")) + _, err := os.Stat(filepath.Join(rc.EmbeddedClusterK0sSubDir(), "pki/ca.crt")) if err == nil { - logrus.Debugf("Got runtime config from filesystem with k0s data dir %s", runtimeconfig.EmbeddedClusterK0sSubDir()) - return nil + logrus.Debugf("Got runtime config from filesystem with k0s data dir %s", rc.EmbeddedClusterK0sSubDir()) + return rc, nil } // Handle versions prior to consolidation of data dirs - runtimeconfig.Set(&ecv1beta1.RuntimeConfigSpec{ + rc.Set(&ecv1beta1.RuntimeConfigSpec{ DataDir: ecv1beta1.DefaultDataDir, K0sDataDirOverride: "/var/lib/k0s", OpenEBSDataDirOverride: "/var/openebs", }) // ca.crt is available on both control plane and worker nodes - _, err = os.Stat(filepath.Join(runtimeconfig.EmbeddedClusterK0sSubDir(), "pki/ca.crt")) + _, err = os.Stat(filepath.Join(rc.EmbeddedClusterK0sSubDir(), "pki/ca.crt")) if err == nil { - logrus.Debugf("Got runtime config from filesystem with k0s data dir %s", runtimeconfig.EmbeddedClusterK0sSubDir()) - return nil + logrus.Debugf("Got runtime config from filesystem with k0s data dir %s", rc.EmbeddedClusterK0sSubDir()) + return rc, nil } - return fmt.Errorf("unable to discover runtime config from filesystem") + return nil, fmt.Errorf("unable to discover runtime config from filesystem") } diff --git a/pkg/support/materialize.go b/pkg/support/materialize.go index d1ab1cb16..98722ffbb 100644 --- a/pkg/support/materialize.go +++ b/pkg/support/materialize.go @@ -15,13 +15,13 @@ type TemplateData struct { OpenEBSDataDir string } -func MaterializeSupportBundleSpec() error { +func MaterializeSupportBundleSpec(rc runtimeconfig.RuntimeConfig) error { data := TemplateData{ - DataDir: runtimeconfig.EmbeddedClusterHomeDirectory(), - K0sDataDir: runtimeconfig.EmbeddedClusterK0sSubDir(), - OpenEBSDataDir: runtimeconfig.EmbeddedClusterOpenEBSLocalSubDir(), + DataDir: rc.EmbeddedClusterHomeDirectory(), + K0sDataDir: rc.EmbeddedClusterK0sSubDir(), + OpenEBSDataDir: rc.EmbeddedClusterOpenEBSLocalSubDir(), } - path := runtimeconfig.PathToEmbeddedClusterSupportFile("host-support-bundle.tmpl.yaml") + path := rc.PathToEmbeddedClusterSupportFile("host-support-bundle.tmpl.yaml") tmpl, err := os.ReadFile(path) if err != nil { return fmt.Errorf("read support bundle template: %w", err) @@ -30,7 +30,7 @@ func MaterializeSupportBundleSpec() error { if err != nil { return fmt.Errorf("render support bundle template: %w", err) } - path = runtimeconfig.PathToEmbeddedClusterSupportFile("host-support-bundle.yaml") + path = rc.PathToEmbeddedClusterSupportFile("host-support-bundle.yaml") if err := os.WriteFile(path, []byte(contents), 0644); err != nil { return fmt.Errorf("write support bundle spec: %w", err) } diff --git a/tests/dryrun/install_test.go b/tests/dryrun/install_test.go index bb99f7cde..f1a4a0ad0 100644 --- a/tests/dryrun/install_test.go +++ b/tests/dryrun/install_test.go @@ -442,11 +442,12 @@ func TestRestrictiveUmask(t *testing.T) { testDefaultInstallationImpl(t) // check that folders created in this test have the right permissions + rc := runtimeconfig.New(nil) folderList := []string{ - runtimeconfig.EmbeddedClusterHomeDirectory(), - runtimeconfig.EmbeddedClusterBinsSubDir(), - runtimeconfig.EmbeddedClusterChartsSubDir(), - runtimeconfig.PathToEmbeddedClusterBinary("kubectl-preflight"), + rc.EmbeddedClusterHomeDirectory(), + rc.EmbeddedClusterBinsSubDir(), + rc.EmbeddedClusterChartsSubDir(), + rc.PathToEmbeddedClusterBinary("kubectl-preflight"), } gotFailure := false for _, folder := range folderList { diff --git a/tests/dryrun/util.go b/tests/dryrun/util.go index e8171b522..c0ed00e5d 100644 --- a/tests/dryrun/util.go +++ b/tests/dryrun/util.go @@ -138,7 +138,7 @@ func runInstallerCmd(args ...string) error { } func readK0sConfig(t *testing.T) k0sv1beta1.ClusterConfig { - stdout, err := exec.Command("cat", runtimeconfig.PathToK0sConfig()).Output() + stdout, err := exec.Command("cat", runtimeconfig.K0sConfigPath).Output() if err != nil { t.Fatalf("fail to get k0s config: %v", err) } diff --git a/tests/integration/kind/openebs/analytics_test.go b/tests/integration/kind/openebs/analytics_test.go index 48fee1113..43857a165 100644 --- a/tests/integration/kind/openebs/analytics_test.go +++ b/tests/integration/kind/openebs/analytics_test.go @@ -4,6 +4,7 @@ import ( "testing" "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" @@ -19,10 +20,12 @@ func TestOpenEBS_AnalyticsDisabled(t *testing.T) { mcli := util.MetadataClient(t, kubeconfig) hcli := util.HelmClient(t, kubeconfig) + rc := runtimeconfig.New(nil) + addon := &openebs.OpenEBS{ ProxyRegistryDomain: "proxy.replicated.com", } - if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, nil, nil); err != nil { + if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, rc, nil, 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 60c8a4f7f..f0d6916e5 100644 --- a/tests/integration/kind/openebs/customdatadir_test.go +++ b/tests/integration/kind/openebs/customdatadir_test.go @@ -27,7 +27,8 @@ func TestOpenEBS_CustomDataDir(t *testing.T) { }) kubeconfig := util.SetupKindClusterFromConfig(t, kindConfig) - runtimeconfig.SetDataDir("/custom") + rc := runtimeconfig.New(nil) + rc.SetDataDir("/custom") kcli := util.CtrlClient(t, kubeconfig) mcli := util.MetadataClient(t, kubeconfig) @@ -36,7 +37,7 @@ func TestOpenEBS_CustomDataDir(t *testing.T) { addon := &openebs.OpenEBS{ ProxyRegistryDomain: "proxy.replicated.com", } - if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, nil, nil); err != nil { + if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, rc, nil, nil); err != nil { t.Fatalf("failed to install openebs: %v", err) } diff --git a/tests/integration/kind/registry/ha_test.go b/tests/integration/kind/registry/ha_test.go index 3aaf0720c..28620a271 100644 --- a/tests/integration/kind/registry/ha_test.go +++ b/tests/integration/kind/registry/ha_test.go @@ -22,6 +22,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/addons/registry" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/pkg/spinner" "github.com/replicatedhq/embedded-cluster/tests/integration/util" "github.com/replicatedhq/embedded-cluster/tests/integration/util/kind" @@ -69,11 +70,13 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { kclient := util.KubeClient(t, kubeconfig) hcli := util.HelmClient(t, kubeconfig) + rc := runtimeconfig.New(nil) + t.Logf("%s installing openebs", formattedTime()) addon := &openebs.OpenEBS{ ProxyRegistryDomain: "proxy.replicated.com", } - if err := addon.Install(ctx, t.Logf, kcli, mcli, hcli, nil, nil); err != nil { + if err := addon.Install(ctx, t.Logf, kcli, mcli, hcli, rc, nil, nil); err != nil { t.Fatalf("failed to install openebs: %v", err) } @@ -86,7 +89,7 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { ProxyRegistryDomain: "proxy.replicated.com", IsHA: false, } - require.NoError(t, registryAddon.Install(ctx, t.Logf, kcli, mcli, hcli, nil, nil)) + require.NoError(t, registryAddon.Install(ctx, t.Logf, kcli, mcli, hcli, rc, nil, nil)) t.Logf("%s creating hostport service", formattedTime()) registryAddr := createHostPortService(t, clusterName, kubeconfig) @@ -98,7 +101,7 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { ProxyRegistryDomain: "proxy.replicated.com", IsHA: false, } - require.NoError(t, adminConsoleAddon.Install(ctx, t.Logf, kcli, mcli, hcli, nil, nil)) + require.NoError(t, adminConsoleAddon.Install(ctx, t.Logf, kcli, mcli, hcli, rc, nil, nil)) t.Logf("%s pushing image to registry", formattedTime()) copyImageToRegistry(t, registryAddr, "docker.io/library/busybox:1.36.1") @@ -120,15 +123,15 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { }, } - enableHAAndCancelContextOnMessage(t, kcli, mcli, kclient, hcli, inSpec, + enableHAAndCancelContextOnMessage(t, kcli, mcli, kclient, hcli, rc, inSpec, regexp.MustCompile(`StatefulSet is ready: seaweedfs`), ) - enableHAAndCancelContextOnMessage(t, kcli, mcli, kclient, hcli, inSpec, + enableHAAndCancelContextOnMessage(t, kcli, mcli, kclient, hcli, rc, inSpec, regexp.MustCompile(`Migrating data for high availability \(`), ) - enableHAAndCancelContextOnMessage(t, kcli, mcli, kclient, hcli, inSpec, + enableHAAndCancelContextOnMessage(t, kcli, mcli, kclient, hcli, rc, inSpec, regexp.MustCompile(`Updating the Admin Console for high availability`), ) @@ -140,7 +143,7 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { loading := newTestingSpinner(t) func() { defer loading.Close() - err = addons.EnableHA(ctx, t.Logf, kcli, mcli, kclient, hcli, "10.96.0.0/12", inSpec, loading) + err = addons.EnableHA(ctx, t.Logf, kcli, mcli, kclient, hcli, rc, "10.96.0.0/12", inSpec, loading) require.NoError(t, err) }() @@ -155,7 +158,7 @@ func TestRegistry_EnableHAAirgap(t *testing.T) { } func enableHAAndCancelContextOnMessage( - t *testing.T, kcli client.Client, mcli metadata.Interface, kclient kubernetes.Interface, hcli helm.Client, + t *testing.T, kcli client.Client, mcli metadata.Interface, kclient kubernetes.Interface, hcli helm.Client, rc runtimeconfig.RuntimeConfig, inSpec ecv1beta1.InstallationSpec, re *regexp.Regexp, ) { @@ -202,7 +205,7 @@ func enableHAAndCancelContextOnMessage( defer loading.Close() t.Logf("%s enabling HA and cancelling context on message", formattedTime()) - err = addons.EnableHA(ctx, t.Logf, kcli, mcli, kclient, hcli, "10.96.0.0/12", inSpec, loading) + err = addons.EnableHA(ctx, t.Logf, kcli, mcli, kclient, hcli, rc, "10.96.0.0/12", inSpec, 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 e8ab5b720..9802184b4 100644 --- a/tests/integration/kind/velero/ca_test.go +++ b/tests/integration/kind/velero/ca_test.go @@ -4,6 +4,7 @@ import ( "testing" "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" @@ -21,10 +22,12 @@ func TestVelero_HostCABundle(t *testing.T) { mcli := util.MetadataClient(t, kubeconfig) hcli := util.HelmClient(t, kubeconfig) + rc := runtimeconfig.New(nil) + addon := &velero.Velero{ HostCABundlePath: "/etc/ssl/certs/ca-certificates.crt", } - if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, nil, nil); err != nil { + if err := addon.Install(t.Context(), t.Logf, kcli, mcli, hcli, rc, nil, nil); err != nil { t.Fatalf("failed to install velero: %v", err) } diff --git a/web/src/components/wizard/CompletionStep.tsx b/web/src/components/wizard/CompletionStep.tsx deleted file mode 100644 index 67b79d1e6..000000000 --- a/web/src/components/wizard/CompletionStep.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React, { useState } from "react"; -import Card from "../common/Card"; -import Button from "../common/Button"; -import { useConfig } from "../../contexts/ConfigContext"; -import { useBranding } from "../../contexts/BrandingContext"; -import { CheckCircle, ExternalLink, Copy, ClipboardCheck } from "lucide-react"; - -const CompletionStep: React.FC = () => { - const { config, prototypeSettings } = useConfig(); - const { title } = useBranding(); - const [copied, setCopied] = useState(false); - const themeColor = prototypeSettings.themeColor; - - const baseUrl = `${config.useHttps ? "https" : "http"}://${config.domain}`; - const urls = [ - { - name: "Web Interface", - url: baseUrl, - description: `Access the main ${title} interface`, - }, - { - name: "API Documentation", - url: `${baseUrl}/api/swagger`, - description: `Browse and test the ${title} API`, - }, - ]; - - const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }; - - // this component is not being used right now - return ( -
- -
-
- -
-

- Installation Complete! -

-

- {title} is installed successfully. -

- - - -
- {urls.map((item, index) => ( -
-
-

- {item.name} -

- -
- - {item.url} - - -

{item.description}

-
- ))} -
-
-
- - -
-

Next Steps

- -
-
-
-
- 1 -
-
-
-

- Log in to your {title} instance -

-

- Use the administrator credentials you provided during setup to - log in to your {title} instance. -

-
-
- -
-
-
- 2 -
-
-
-

- Configure additional settings -

-

- Visit the Admin Dashboard to configure additional settings - such as authentication providers, webhooks, and other - integrations. -

-
-
- -
-
-
- 3 -
-
-
-

- Create your first organization -

-

- Set up an organization for your team and invite members to - collaborate on repositories. -

-
-
-
-
-
-
- ); -}; - -export default CompletionStep; diff --git a/web/src/components/wizard/InstallWizard.tsx b/web/src/components/wizard/InstallWizard.tsx index 89a6dd6d0..f37f3875a 100644 --- a/web/src/components/wizard/InstallWizard.tsx +++ b/web/src/components/wizard/InstallWizard.tsx @@ -2,7 +2,8 @@ import React, { useState } from "react"; import StepNavigation from "./StepNavigation"; import WelcomeStep from "./WelcomeStep"; import SetupStep from "./SetupStep"; -import ValidationInstallStep from "./ValidationInstallStep"; +import ValidationStep from "./ValidationStep"; +import InstallationStep from "./InstallationStep"; import { WizardStep } from "../../types"; import { AppIcon } from "../common/Logo"; import { useWizardMode } from "../../contexts/WizardModeContext"; @@ -12,7 +13,7 @@ const InstallWizard: React.FC = () => { const { text } = useWizardMode(); const goToNextStep = () => { - const steps: WizardStep[] = ["welcome", "setup", "installation"]; + const steps: WizardStep[] = ["welcome", "setup", "validation", "installation"]; const currentIndex = steps.indexOf(currentStep); if (currentIndex < steps.length - 1) { setCurrentStep(steps[currentIndex + 1]); @@ -20,7 +21,7 @@ const InstallWizard: React.FC = () => { }; const goToPreviousStep = () => { - const steps: WizardStep[] = ["welcome", "setup", "installation"]; + const steps: WizardStep[] = ["welcome", "setup", "validation", "installation"]; const currentIndex = steps.indexOf(currentStep); if (currentIndex > 0) { setCurrentStep(steps[currentIndex - 1]); @@ -33,8 +34,10 @@ const InstallWizard: React.FC = () => { return ; case "setup": return ; + case "validation": + return ; case "installation": - return ; + return ; default: return null; } diff --git a/web/src/components/wizard/ValidationInstallStep.tsx b/web/src/components/wizard/InstallationStep.tsx similarity index 51% rename from web/src/components/wizard/ValidationInstallStep.tsx rename to web/src/components/wizard/InstallationStep.tsx index a1a0c4d02..b7064685e 100644 --- a/web/src/components/wizard/ValidationInstallStep.tsx +++ b/web/src/components/wizard/InstallationStep.tsx @@ -1,15 +1,18 @@ import React, { useState, useEffect } from "react"; import Card from "../common/Card"; +import Button from "../common/Button"; import { useConfig } from "../../contexts/ConfigContext"; -import { ExternalLink } from "lucide-react"; +import { CheckCircle, ExternalLink, Loader2 } from "lucide-react"; import { useQuery, Query } from "@tanstack/react-query"; +import { useWizardMode } from "../../contexts/WizardModeContext"; interface InstallStatus { state: "Succeeded" | "Failed" | "InProgress"; } -const ValidationInstallStep: React.FC = () => { +const InstallationStep: React.FC = () => { const { config } = useConfig(); + const { text } = useWizardMode(); const [showAdminLink, setShowAdminLink] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -35,10 +38,7 @@ const ValidationInstallStep: React.FC = () => { }, refetchInterval: (query: Query) => { // Continue polling until we get a final state - return query.state.data?.state === "Succeeded" || - query.state.data?.state === "Failed" - ? false - : 5000; + return query.state.data?.state === "Succeeded" || query.state.data?.state === "Failed" ? false : 5000; }, }); @@ -55,15 +55,20 @@ const ValidationInstallStep: React.FC = () => { return (
-
-

- Installing Embedded Cluster -

+ {installStatus?.state !== "Succeeded" && ( +
+

{text.installationTitle}

+

{text.installationDescription}

+
+ )} +
{isLoading && ( -

- Please wait while we complete the installation... -

+
+ +

Please wait while we complete the installation...

+

This may take a few minutes.

+
)} {error && ( @@ -74,15 +79,21 @@ const ValidationInstallStep: React.FC = () => { )} {showAdminLink && ( - - Visit Admin Console - - +
+
+ +
+

+ Visit the Admin Console to configure and install {text.installationTitle} +

+ +
)}
@@ -90,4 +101,4 @@ const ValidationInstallStep: React.FC = () => { ); }; -export default ValidationInstallStep; +export default InstallationStep; diff --git a/web/src/components/wizard/SetupStep.tsx b/web/src/components/wizard/SetupStep.tsx index d3b8b55f8..424713b9f 100644 --- a/web/src/components/wizard/SetupStep.tsx +++ b/web/src/components/wizard/SetupStep.tsx @@ -5,7 +5,6 @@ import { useConfig } from "../../contexts/ConfigContext"; import { useWizardMode } from "../../contexts/WizardModeContext"; import { ChevronLeft, ChevronRight } from "lucide-react"; import LinuxSetup from "./setup/LinuxSetup"; -import KubernetesSetup from "./setup/KubernetesSetup"; import { useQuery, useMutation } from "@tanstack/react-query"; interface SetupStepProps { @@ -23,7 +22,7 @@ const SetupStep: React.FC = ({ onNext, onBack }) => { const { isLoading: isConfigLoading } = useQuery({ queryKey: ["installConfig"], queryFn: async () => { - const response = await fetch("/api/install", { + const response = await fetch("/api/install/installation/config", { headers: { ...(localStorage.getItem("auth") && { Authorization: `Bearer ${localStorage.getItem("auth")}`, @@ -33,9 +32,9 @@ const SetupStep: React.FC = ({ onNext, onBack }) => { if (!response.ok) { throw new Error("Failed to fetch install configuration"); } - const data = await response.json(); - updateConfig(data.config); - return data; + const config = await response.json(); + updateConfig(config); + return config; }, }); @@ -64,11 +63,10 @@ const SetupStep: React.FC = ({ onNext, onBack }) => { // Mutation for submitting the configuration const { mutate: submitConfig, - isPending: isSubmitting, error: submitError, } = useMutation({ mutationFn: async (configData: typeof config) => { - const response = await fetch("/api/install/config", { + const response = await fetch("/api/install/installation/configure", { method: "POST", headers: { "Content-Type": "application/json", @@ -90,18 +88,10 @@ const SetupStep: React.FC = ({ onNext, onBack }) => { }, onError: (err: any) => { setError(err.message || "Failed to setup cluster"); + return err; }, }); - // Helper function to get field error message - const getFieldError = (fieldName: string) => { - if (!submitError?.errors) return undefined; - const fieldError = submitError.errors.find( - (err: any) => err.field === fieldName - ); - return fieldError?.message; - }; - const handleInputChange = (e: React.ChangeEvent) => { const { id, value } = e.target; if (id === "adminConsolePort" || id === "localArtifactMirrorPort") { @@ -119,7 +109,7 @@ const SetupStep: React.FC = ({ onNext, onBack }) => { updateConfig({ [id]: value }); }; - const handleNext = () => { + const handleNext = async () => { submitConfig(config); }; @@ -135,9 +125,7 @@ const SetupStep: React.FC = ({ onNext, onBack }) => { {text.setupTitle}

- {prototypeSettings.clusterMode === "embedded" - ? "Configure the installation settings." - : text.setupDescription} + Configure the installation settings.

@@ -146,7 +134,7 @@ const SetupStep: React.FC = ({ onNext, onBack }) => {

Loading configuration...

- ) : prototypeSettings?.clusterMode === "embedded" ? ( + ) : ( = ({ onNext, onBack }) => { availableNetworkInterfaces={availableNetworkInterfaces} fieldErrors={submitError?.errors || []} /> - ) : ( - )} - {submitError && ( + {error && (
Please fix the errors in the form above before proceeding.
@@ -179,9 +165,8 @@ const SetupStep: React.FC = ({ onNext, onBack }) => { diff --git a/web/src/components/wizard/StepNavigation.tsx b/web/src/components/wizard/StepNavigation.tsx index 52ef2e6ff..da2cd3b0d 100644 --- a/web/src/components/wizard/StepNavigation.tsx +++ b/web/src/components/wizard/StepNavigation.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { WizardStep } from '../../types'; -import { ClipboardList, Settings, Download } from 'lucide-react'; +import { ClipboardList, Settings, Download, CheckCircle } from 'lucide-react'; import { useWizardMode } from '../../contexts/WizardModeContext'; import { useConfig } from '../../contexts/ConfigContext'; @@ -16,6 +16,7 @@ const StepNavigation: React.FC = ({ currentStep }) => { 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 }, ]; diff --git a/web/src/components/wizard/ValidationStep.tsx b/web/src/components/wizard/ValidationStep.tsx new file mode 100644 index 000000000..0c0df35bd --- /dev/null +++ b/web/src/components/wizard/ValidationStep.tsx @@ -0,0 +1,97 @@ +import React from "react"; +import Card from "../common/Card"; +import Button from "../common/Button"; +import { useConfig } from "../../contexts/ConfigContext"; +import { useWizardMode } from "../../contexts/WizardModeContext"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import LinuxPreflightCheck from "./preflight/LinuxPreflightCheck"; +import { useMutation } from "@tanstack/react-query"; + +interface ValidationStepProps { + onNext: () => void; + onBack: () => void; +} + +const ValidationStep: React.FC = ({ onNext, onBack }) => { + const { config } = useConfig(); + const { text } = useWizardMode(); + const [preflightComplete, setPreflightComplete] = React.useState(false); + const [preflightSuccess, setPreflightSuccess] = React.useState(false); + const [error, setError] = React.useState(null); + + const handlePreflightComplete = (success: boolean) => { + setPreflightComplete(true); + setPreflightSuccess(success); + }; + + const { mutate: startInstallation } = useMutation({ + mutationFn: async () => { + const response = await fetch("/api/install/node/setup", { + method: "POST", + headers: { + ...(localStorage.getItem("auth") && { + Authorization: `Bearer ${localStorage.getItem("auth")}`, + }), + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw errorData; + } + return response.json(); + }, + onSuccess: () => { + onNext(); + }, + onError: (err: any) => { + setError(err.message || "Failed to start installation"); + return err; + }, + }); + + return ( +
+ +
+

+ {text.validationTitle} +

+

+ {text.validationDescription} +

+
+ + + + {error && ( +
+ {error} +
+ )} +
+ +
+ + +
+
+ ); +}; + +export default ValidationStep; diff --git a/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx b/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx new file mode 100644 index 000000000..8db054c9f --- /dev/null +++ b/web/src/components/wizard/preflight/LinuxPreflightCheck.tsx @@ -0,0 +1,284 @@ +import React, { useState, useEffect } from "react"; +import { ClusterConfig } from "../../../contexts/ConfigContext"; +import { XCircle, CheckCircle, Loader2, AlertTriangle } from "lucide-react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import Button from "../../common/Button"; + +interface LinuxPreflightCheckProps { + config: ClusterConfig; + onComplete: (success: boolean) => void; +} + +interface PreflightResult { + title: string; + message: string; +} + +interface PreflightOutput { + pass: PreflightResult[]; + warn: PreflightResult[]; + fail: PreflightResult[]; +} + +interface PreflightStatus { + state: string; + description: string; + lastUpdated: string; +} + +interface PreflightResponse { + titles: string[]; + output?: PreflightOutput; + status?: PreflightStatus; +} + +interface InstallationConfigResponse { + description: string; + lastUpdated: string; + state: "Failed" | "Succeeded" | "Running"; +} + +const LinuxPreflightCheck: React.FC = ({ onComplete }) => { + const [isPreflightsPolling, setIsPreflightsPolling] = useState(false); + const [isConfigPolling, setIsConfigPolling] = useState(true); + + // Mutation to run preflight checks + const { mutate: runPreflights } = useMutation({ + mutationFn: async () => { + const response = await fetch("/api/install/host-preflights/run", { + method: "POST", + headers: { + ...(localStorage.getItem("auth") && { + Authorization: `Bearer ${localStorage.getItem("auth")}`, + }), + }, + }); + if (!response.ok) { + throw new Error("Failed to run preflight checks"); + } + return response.json() as Promise; + }, + onSuccess: () => { + setIsPreflightsPolling(true); + }, + onError: () => { + setIsPreflightsPolling(false); + onComplete(false); + }, + }); + + // Query to poll installation config status + const { data: installationConfigStatus } = useQuery< + InstallationConfigResponse, + Error + >({ + queryKey: ["installationConfigStatus"], + queryFn: async () => { + const response = await fetch("/api/install/installation/status", { + headers: { + ...(localStorage.getItem("auth") && { + Authorization: `Bearer ${localStorage.getItem("auth")}`, + }), + }, + }); + if (!response.ok) { + throw new Error("Failed to get installation config status"); + } + + return response.json() as Promise; + }, + enabled: isConfigPolling, + refetchInterval: 1000, + }); + // Query to poll preflight status + const { data: preflightResponse } = useQuery({ + queryKey: ["preflightStatus"], + queryFn: async () => { + const response = await fetch("/api/install/host-preflights/status", { + headers: { + ...(localStorage.getItem("auth") && { + Authorization: `Bearer ${localStorage.getItem("auth")}`, + }), + }, + }); + if (!response.ok) { + throw new Error("Failed to get preflight status"); + } + return response.json() as Promise; + }, + enabled: isPreflightsPolling, + refetchInterval: 1000, + }); + + // Handle preflight status changes + useEffect(() => { + if (preflightResponse?.status?.state === "Succeeded" || preflightResponse?.status?.state === "Failed") { + setIsPreflightsPolling(false); + // Consider it successful if there are no failures + const hasFailures = (preflightResponse.output?.fail?.length ?? 0) > 0; + onComplete(!hasFailures); + } + }, [preflightResponse, onComplete]); + + useEffect(() => { + if (installationConfigStatus?.state === "Failed") { + setIsConfigPolling(false); + return; // Prevent running preflights if failed + } + if (installationConfigStatus?.state === "Succeeded") { + setIsConfigPolling(false); + runPreflights(); + setIsPreflightsPolling(true); + } + }, [installationConfigStatus, runPreflights]); + + const renderCheckStatus = (result: PreflightResult, type: "pass" | "warn" | "fail") => { + let Icon = Loader2; + let statusColor = "text-blue-500"; + let iconClasses = "animate-spin"; + + if (!isPreflightsPolling) { + switch (type) { + case "pass": + Icon = CheckCircle; + statusColor = "text-green-500"; + break; + case "warn": + Icon = AlertTriangle; + statusColor = "text-yellow-500"; + break; + case "fail": + Icon = XCircle; + statusColor = "text-red-500"; + break; + } + iconClasses = ""; + } + + return ( +
+
+
+ +
+
+

{result.title}

+

+ {result.message} +

+
+
+
+ ); + }; + + if (isConfigPolling) { + return ( +
+ +

Initializing...

+

Preparing the host.

+
+ ); + } + + if (isPreflightsPolling) { + return ( +
+ +

Validating host requirements...

+

Please wait while we check your system.

+
+ ); + } + + return ( +
+ {/* Header for Host Requirements Not Met */} + {(preflightResponse?.output?.fail?.length ?? 0) > 0 && ( +
+
+ + Host Requirements Not Met +
+
+ We found some issues that need to be resolved before proceeding with the installation. +
+
+ )}{" "} +
+ {preflightResponse?.output && ( + <> + {/* Failures Box */} + {preflightResponse.output.fail && preflightResponse.output.fail.length > 0 && ( +
+ {preflightResponse.output.fail.map((result: PreflightResult, index: number) => ( +
+
+ +
+
+

{result.title}

+

{result.message}

+
+
+ ))} +
+ )} + {/* Passes and Warnings */} + {preflightResponse.output.pass?.map((result: PreflightResult, index: number) => ( +
{renderCheckStatus(result, "pass")}
+ ))} + {preflightResponse.output.warn?.map((result: PreflightResult, index: number) => ( +
{renderCheckStatus(result, "warn")}
+ ))} + + )} + {installationConfigStatus?.state === "Failed" && ( +
+
+
+ +
+
+

Failed to run checks

+

Unable to complete system requirement checks

+

{installationConfigStatus?.description}

+
+
+
+ )} +
+ {/* What's Next Section - always at the bottom if there are failures */} + {preflightResponse?.output?.fail && preflightResponse.output.fail.length > 0 && ( +
+
What's Next?
+
    +
  • Review and address each failed requirement
  • +
  • Click "Back" to modify your setup if needed
  • +
  • Re-run the validation once issues are addressed
  • +
+ +
+ )} +
+ ); +}; + +export default LinuxPreflightCheck; diff --git a/web/src/components/wizard/setup/KubernetesSetup.tsx b/web/src/components/wizard/setup/KubernetesSetup.tsx deleted file mode 100644 index 97631d51b..000000000 --- a/web/src/components/wizard/setup/KubernetesSetup.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; - -interface KubernetesSetupProps { - config: { - networkInterface?: string; - globalCidr?: string; - }; - onInputChange: (e: React.ChangeEvent) => void; -} - -const KubernetesSetup: React.FC = ({ - config, - onInputChange, -}) => ( -
-
-

Network Settings

-

- Configure network settings for your Kubernetes cluster. -

-
- - -
-
-
-); - -export default KubernetesSetup; \ No newline at end of file diff --git a/web/src/contexts/WizardModeContext.tsx b/web/src/contexts/WizardModeContext.tsx index a7bbea6b6..ddd79bddf 100644 --- a/web/src/contexts/WizardModeContext.tsx +++ b/web/src/contexts/WizardModeContext.tsx @@ -1,8 +1,8 @@ -import React, { createContext, useContext } from 'react'; -import { useConfig } from './ConfigContext'; -import { useBranding } from './BrandingContext'; +import React, { createContext, useContext } from "react"; +import { useConfig } from "./ConfigContext"; +import { useBranding } from "./BrandingContext"; -export type WizardMode = 'install' | 'upgrade'; +export type WizardMode = "install" | "upgrade"; interface WizardText { title: string; @@ -11,48 +11,46 @@ interface WizardText { welcomeDescription: string; setupTitle: string; setupDescription: string; - configurationTitle: string; - configurationDescription: string; + validationTitle: string; + validationDescription: string; installationTitle: string; installationDescription: string; - completionTitle: string; - completionDescription: string; welcomeButtonText: string; nextButtonText: string; } const getTextVariations = (isEmbedded: boolean, title: string): Record => ({ install: { - title: title || '', - subtitle: 'Installation Wizard', + 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'}.`, - setupTitle: 'Setup', - setupDescription: 'Set up the hosts to use for this installation.', - configurationTitle: 'Configuration', - configurationDescription: `Configure your ${title} installation by providing the information below.`, + welcomeDescription: `This wizard will guide you through installing ${title} on your ${ + isEmbedded ? "Linux machine" : "Kubernetes cluster" + }.`, + setupTitle: "Setup", + setupDescription: "Configure the host settings for this installation.", + validationTitle: "Validation", + validationDescription: "Validate the host requirements before proceeding with installation.", installationTitle: `Installing ${title}`, - installationDescription: '', - completionTitle: 'Installation Complete!', - completionDescription: `${title} has been installed successfully.`, - welcomeButtonText: 'Start', - nextButtonText: 'Next: Start Installation', + installationDescription: "", + welcomeButtonText: "Start", + nextButtonText: "Next: Start Installation", }, upgrade: { - title: title || '', - subtitle: 'Upgrade Wizard', + title: title || "", + subtitle: "Upgrade Wizard", welcomeTitle: `Welcome to ${title}`, - welcomeDescription: `This wizard will guide you through upgrading ${title} on your ${isEmbedded ? 'Linux machine' : 'Kubernetes cluster'}.`, - setupTitle: 'Setup', - setupDescription: 'Set up the hosts to use for this installation.', - configurationTitle: 'Upgrade Configuration', - configurationDescription: `Configure your ${title} installation by providing the information below.`, + welcomeDescription: `This wizard will guide you through upgrading ${title} on your ${ + isEmbedded ? "Linux machine" : "Kubernetes cluster" + }.`, + setupTitle: "Setup", + setupDescription: "Set up the hosts to use for this upgrade.", + validationTitle: "Validation", + validationDescription: "Validate the host requirements before proceeding with the upgrade.", installationTitle: `Upgrading ${title}`, - installationDescription: '', - completionTitle: 'Upgrade Complete!', - completionDescription: `${title} has been successfully upgraded.`, - welcomeButtonText: 'Start Upgrade', - nextButtonText: 'Next: Start Upgrade', + installationDescription: "", + welcomeButtonText: "Start Upgrade", + nextButtonText: "Next: Start Upgrade", }, }); @@ -69,20 +67,16 @@ export const WizardModeProvider: React.FC<{ }> = ({ children, mode }) => { const { prototypeSettings } = useConfig(); const { title } = useBranding(); - const isEmbedded = prototypeSettings.clusterMode === 'embedded'; + const isEmbedded = prototypeSettings.clusterMode === "embedded"; const text = getTextVariations(isEmbedded, title)[mode]; - return ( - - {children} - - ); + return {children}; }; export const useWizardMode = (): WizardModeContextType => { const context = useContext(WizardModeContext); if (context === undefined) { - throw new Error('useWizardMode must be used within a WizardModeProvider'); + throw new Error("useWizardMode must be used within a WizardModeProvider"); } return context; }; diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 804f3c28c..e830dc6af 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -14,3 +14,28 @@ export interface InstallationStatus { } export type WizardStep = 'welcome' | 'setup' | 'validation' | 'installation' | 'completion'; + +export interface ValidationResult { + success: boolean; + message: string; + details?: string; +} + +export interface ValidationStatus { + kubernetes: ValidationResult | null; + helm: ValidationResult | null; + storage: ValidationResult | null; + networking: ValidationResult | null; + permissions: ValidationResult | null; +} + +export interface HostPreflightStatus { + kernelVersion: ValidationResult | null; + kernelParameters: ValidationResult | null; + dataDirectory: ValidationResult | null; + systemMemory: ValidationResult | null; + systemCPU: ValidationResult | null; + diskSpace: ValidationResult | null; + selinux: ValidationResult | null; + networkEndpoints: ValidationResult | null; +} \ No newline at end of file diff --git a/web/src/utils/validation.ts b/web/src/utils/validation.ts new file mode 100644 index 000000000..aa4eb1624 --- /dev/null +++ b/web/src/utils/validation.ts @@ -0,0 +1,143 @@ +import { ClusterConfig } from '../contexts/ConfigContext'; +import { ValidationStatus } from '../types'; + +export const validateEnvironment = async (config: ClusterConfig): Promise => { + const validationStatus: ValidationStatus = { + kubernetes: null, + helm: null, + storage: null, + networking: null, + permissions: null, + }; + + // Kubernetes check completes after 1 second + await new Promise(resolve => setTimeout(resolve, 1000)); + validationStatus.kubernetes = { + success: true, + message: 'Kubernetes cluster is accessible and running version 1.24.0', + }; + + // Other checks complete after 2 more seconds + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Get prototype settings + const prototypeSettings = JSON.parse(localStorage.getItem('gitea-prototype-settings') || '{}'); + const shouldFail = prototypeSettings.failPreflights; + + if (shouldFail) { + validationStatus.helm = { + success: true, + message: 'Helm version 3.8.0 detected', + }; + + validationStatus.storage = { + success: false, + message: `Storage class "${config.storageClass}" not found. Please create the storage class or select a different one.`, + }; + + validationStatus.networking = { + success: false, + message: 'Ingress controller not detected. Install an ingress controller (e.g., nginx-ingress) to enable external access.', + }; + + validationStatus.permissions = { + success: true, + message: 'The current user has sufficient permissions in the namespace', + }; + } else { + validationStatus.helm = { + success: true, + message: 'Helm version 3.8.0 detected', + }; + + validationStatus.storage = { + success: true, + message: `Storage class "${config.storageClass}" is available with dynamic provisioning support`, + }; + + validationStatus.networking = { + success: true, + message: 'All networking prerequisites verified successfully', + }; + + validationStatus.permissions = { + success: true, + message: 'The current user has sufficient permissions in the namespace', + }; + } + + return validationStatus; +}; + +// Add these interfaces to match the new structure +interface PreflightResult { + title: string; + message: string; +} + +interface PreflightOutput { + pass: PreflightResult[]; + warn: PreflightResult[]; + fail: PreflightResult[]; +} + +interface PreflightStatus { + state: string; + description: string; + lastUpdated: string; +} + +interface PreflightResponse { + status: PreflightStatus; + output?: PreflightOutput; +} + +export const validateHostPreflights = async (config: ClusterConfig): Promise => { + // Simulate status + const status: PreflightStatus = { + state: 'Succeeded', + description: 'Preflight checks completed', + lastUpdated: new Date().toISOString(), + }; + + // Get prototype settings + const prototypeSettings = JSON.parse(localStorage.getItem('gitea-prototype-settings') || '{}'); + const shouldFail = prototypeSettings.failHostPreflights; + + // Simulate preflight checks + await new Promise(resolve => setTimeout(resolve, 2000)); + + let output: PreflightOutput = { + pass: [], + warn: [], + fail: [], + }; + + if (shouldFail) { + output.fail.push( + { title: 'Kernel Version', message: 'Kernel version 3.10.0 is not supported. Please upgrade to kernel version 4.15.0 or later.' }, + { title: 'Kernel Parameters', message: 'Required kernel parameter net.bridge.bridge-nf-call-iptables=1 is not set. Run: sysctl -w net.bridge.bridge-nf-call-iptables=1 and add to /etc/sysctl.conf' }, + { title: 'Data Directory', message: 'Data directory is a symbolic link. Please use a real directory path for data storage.' }, + { title: 'System Memory', message: 'Insufficient memory: 4GB available, minimum 8GB required. Add more memory to meet the requirements.' }, + { title: 'CPU Resources', message: 'Insufficient CPU cores: 2 cores available, minimum 4 cores required. Add more CPU resources to meet the requirements.' }, + { title: 'Disk Space', message: 'Insufficient disk space: 5GB available, minimum 20GB required. Free up space or add more storage.' }, + { title: 'SELinux Status', message: "SELinux must be disabled or run in permissive mode. To run SELinux in permissive mode, edit /etc/selinux/config, change the line 'SELINUX=enforcing' to 'SELINUX=permissive', save the file, and reboot. You can run getenforce to verify the change." }, + { title: 'Network Connectivity', message: 'Cannot reach required network endpoints. Check firewall rules and DNS resolution for registry.gitea.com.' }, + ); + } else { + output.pass.push( + { title: 'Kernel Version', message: 'Kernel version 5.15.0 meets the minimum requirement of 4.15.0' }, + { title: 'Kernel Parameters', message: 'All required kernel parameters are configured correctly' }, + { title: 'Data Directory', message: 'Data directory is a valid path with correct permissions' }, + { title: 'System Memory', message: '16GB RAM available, exceeds minimum requirement of 8GB' }, + { title: 'CPU Resources', message: '4 CPU cores available, meets minimum requirement' }, + { title: 'Disk Space', message: '50GB disk space available, exceeds minimum requirement of 20GB' }, + { title: 'SELinux Status', message: 'SELinux is in permissive mode as required' }, + { title: 'Network Connectivity', message: 'All required network endpoints are accessible' }, + ); + } + + return { status, output }; +}; + +export type HostPreflightStatus = Record; \ No newline at end of file