From 1a8b27d4bcc1aecb98b76639e351351bd52ea924 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Wed, 15 Oct 2025 16:23:18 -0700 Subject: [PATCH 1/6] feat(api): Lookup template function --- .github/workflows/dependencies.yaml | 8 + api/api.go | 17 ++ api/clients.go | 88 +++++++ api/controllers/app/controller.go | 9 + .../kubernetes/install/controller.go | 53 ++-- .../kubernetes/upgrade/controller.go | 36 ++- api/controllers/linux/install/controller.go | 10 + api/controllers/linux/upgrade/controller.go | 10 + api/handlers.go | 4 +- api/integration/app/install/config_test.go | 9 +- .../app/upgrade/apppreflight_test.go | 24 +- api/integration/app/upgrade/config_test.go | 23 +- api/integration/app/upgrade/upgrade_test.go | 21 +- api/integration/auth/controller_test.go | 6 +- api/integration/console/controller_test.go | 6 +- .../kubernetes/install/appconfig_test.go | 10 +- .../kubernetes/install/appinstall_test.go | 37 ++- .../kubernetes/install/apppreflight_test.go | 40 +-- .../kubernetes/install/infra_test.go | 28 +- .../kubernetes/install/installation_test.go | 20 +- .../linux/install/appconfig_test.go | 10 +- .../linux/install/appinstall_test.go | 37 ++- .../linux/install/apppreflight_test.go | 30 ++- .../linux/install/hostpreflight_test.go | 27 +- api/integration/linux/install/infra_test.go | 23 +- .../linux/install/installation_test.go | 23 +- api/integration/linux/upgrade/infra_test.go | 73 +++--- api/integration/util.go | 45 +++- api/internal/clients/kube.go | 60 ++--- api/internal/clients/kube_test.go | 12 +- .../handlers/kubernetes/kubernetes.go | 14 +- api/internal/handlers/linux/linux.go | 10 + api/internal/managers/app/release/manager.go | 12 + .../managers/app/release/template_test.go | 30 ++- .../managers/kubernetes/infra/manager.go | 56 ++-- .../managers/kubernetes/infra/manager_test.go | 158 ++--------- .../managers/kubernetes/infra/status_test.go | 3 +- api/internal/managers/linux/infra/install.go | 2 +- api/internal/managers/linux/infra/upgrade.go | 2 +- api/internal/managers/linux/infra/util.go | 30 +-- api/pkg/template/static.go | 9 +- api/routes.go | 2 +- api/types/api.go | 12 +- cmd/buildtools/metadata.go | 10 +- cmd/buildtools/openebs.go | 10 +- cmd/buildtools/registry.go | 4 +- cmd/buildtools/seaweedfs.go | 4 +- cmd/buildtools/utils.go | 27 +- cmd/buildtools/velero.go | 4 +- cmd/installer/cli/api.go | 4 +- cmd/installer/cli/api_test.go | 124 +++++++-- cmd/installer/cli/enable_ha.go | 7 +- cmd/installer/cli/install.go | 45 +--- cmd/installer/cli/join.go | 7 +- cmd/installer/cli/restore.go | 21 +- cmd/installer/cli/upgrade.go | 3 +- cmd/installer/goods/materializer.go | 24 ++ operator/pkg/cli/upgrade_job.go | 5 +- .../kubernetesinstallation/installation.go | 20 +- pkg-new/kubernetesinstallation/interface.go | 4 + pkg-new/kubernetesinstallation/mock.go | 15 ++ pkg/addons/adminconsole/install.go | 1 + pkg/addons/adminconsole/upgrade.go | 1 + pkg/addons/embeddedclusteroperator/install.go | 1 + pkg/addons/embeddedclusteroperator/upgrade.go | 1 + pkg/addons/openebs/install.go | 1 + pkg/addons/openebs/upgrade.go | 1 + pkg/addons/registry/install.go | 1 + pkg/addons/registry/upgrade.go | 1 + pkg/addons/seaweedfs/install.go | 1 + pkg/addons/seaweedfs/upgrade.go | 1 + pkg/addons/velero/install.go | 1 + pkg/addons/velero/upgrade.go | 1 + pkg/extensions/install.go | 2 +- pkg/extensions/upgrade.go | 2 +- pkg/extensions/util.go | 6 +- pkg/helm/binary_executor.go | 75 ++++++ pkg/helm/binary_executor_mock.go | 20 ++ pkg/helm/binary_executor_test.go | 248 ++++++++++++++++++ pkg/helm/client.go | 218 ++++++++++----- pkg/helm/images.go | 16 +- pkg/helm/interface.go | 14 +- pkg/helm/mock_client.go | 28 +- pkg/helm/output_parser.go | 26 ++ pkg/helm/output_parser_test.go | 150 +++++++++++ pkg/helpers/command.go | 10 + pkg/runtimeconfig/interface.go | 3 + pkg/runtimeconfig/mock.go | 10 + pkg/runtimeconfig/runtimeconfig.go | 9 + tests/integration/util/helm.go | 10 +- versions.mk | 3 + 91 files changed, 1690 insertions(+), 649 deletions(-) create mode 100644 api/clients.go create mode 100644 pkg/helm/binary_executor.go create mode 100644 pkg/helm/binary_executor_mock.go create mode 100644 pkg/helm/binary_executor_test.go create mode 100644 pkg/helm/output_parser.go create mode 100644 pkg/helm/output_parser_test.go diff --git a/.github/workflows/dependencies.yaml b/.github/workflows/dependencies.yaml index 18ef8c1005..ab1d5f2c00 100644 --- a/.github/workflows/dependencies.yaml +++ b/.github/workflows/dependencies.yaml @@ -41,6 +41,14 @@ jobs: echo "troubleshoot version: $version" sed -i "/^TROUBLESHOOT_VERSION/c\TROUBLESHOOT_VERSION = $version" versions.mk + - name: Helm + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + version=$(gh release list --repo helm/helm --json tagName,isLatest | jq -r '.[] | select(.isLatest) | .tagName') + echo "helm version: $version" + sed -i "/^HELM_VERSION/c\HELM_VERSION = $version" versions.mk + - name: FIO env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/api/api.go b/api/api.go index 864fa1254d..7ba70c1499 100644 --- a/api/api.go +++ b/api/api.go @@ -11,6 +11,7 @@ import ( linuxupgrade "github.com/replicatedhq/embedded-cluster/api/controllers/linux/upgrade" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/sirupsen/logrus" @@ -39,6 +40,7 @@ import ( type API struct { cfg types.APIConfig + hcli helm.Client logger logrus.FieldLogger metricsReporter metrics.ReporterInterface @@ -111,8 +113,19 @@ func WithMetricsReporter(metricsReporter metrics.ReporterInterface) Option { } } +// WithHelmClient configures the helm client for the API. +func WithHelmClient(hcli helm.Client) Option { + return func(a *API) { + a.hcli = hcli + } +} + // New creates a new API instance. func New(cfg types.APIConfig, opts ...Option) (*API, error) { + if cfg.InstallTarget == "" { + return nil, fmt.Errorf("target is required") + } + api := &API{ cfg: cfg, } @@ -133,6 +146,10 @@ func New(cfg types.APIConfig, opts ...Option) (*API, error) { api.logger = l } + if err := api.initClients(); err != nil { + return nil, fmt.Errorf("init clients: %w", err) + } + if err := api.initHandlers(); err != nil { return nil, fmt.Errorf("init handlers: %w", err) } diff --git a/api/clients.go b/api/clients.go new file mode 100644 index 0000000000..b7ca8bbdf4 --- /dev/null +++ b/api/clients.go @@ -0,0 +1,88 @@ +package api + +import ( + "fmt" + + "github.com/replicatedhq/embedded-cluster/api/internal/clients" + "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" + "github.com/replicatedhq/embedded-cluster/pkg/versions" +) + +func (a *API) initClients() error { + if a.hcli == nil { + if err := a.initHelmClient(); err != nil { + return fmt.Errorf("init helm client: %w", err) + } + } + return nil +} + +// initHelmClient initializes the Helm client based on the installation target +func (a *API) initHelmClient() error { + switch a.cfg.InstallTarget { + case types.InstallTargetLinux: + return a.initLinuxHelmClient() + case types.InstallTargetKubernetes: + return a.initKubernetesHelmClient() + default: + return fmt.Errorf("unsupported install target: %s", a.cfg.InstallTarget) + } +} + +// initLinuxHelmClient initializes the Helm client for Linux installations +func (a *API) initLinuxHelmClient() error { + airgapPath := "" + if a.cfg.AirgapBundle != "" { + airgapPath = a.cfg.RuntimeConfig.EmbeddedClusterChartsSubDir() + } + + hcli, err := helm.NewClient(helm.HelmOptions{ + HelmPath: a.cfg.RuntimeConfig.PathToEmbeddedClusterBinary("helm"), + KubernetesEnvSettings: a.cfg.RuntimeConfig.GetKubernetesEnvSettings(), + K8sVersion: versions.K0sVersion, + AirgapPath: airgapPath, + }) + if err != nil { + return fmt.Errorf("create linux helm client: %w", err) + } + + a.hcli = hcli + return nil +} + +// initKubernetesHelmClient initializes the Helm client for Kubernetes installations +func (a *API) initKubernetesHelmClient() error { + // get the kubernetes version + kcli, err := clients.NewDiscoveryClient(clients.KubeClientOptions{ + RESTClientGetter: a.cfg.Installation.GetKubernetesEnvSettings().RESTClientGetter(), + }) + if err != nil { + return fmt.Errorf("create discovery client: %w", err) + } + k8sVersion, err := kcli.ServerVersion() + if err != nil { + return fmt.Errorf("get server version: %w", err) + } + + // get the helm binary path + helmPath, err := a.cfg.Installation.PathToEmbeddedBinary("helm") + if err != nil { + return fmt.Errorf("get helm path: %w", err) + } + + // create the helm client + hcli, err := helm.NewClient(helm.HelmOptions{ + HelmPath: helmPath, + KubernetesEnvSettings: a.cfg.Installation.GetKubernetesEnvSettings(), + // TODO: how can we support airgap? + AirgapPath: "", + K8sVersion: k8sVersion.String(), + }) + if err != nil { + return fmt.Errorf("create kubernetes helm client: %w", err) + } + + a.hcli = hcli + return nil +} diff --git a/api/controllers/app/controller.go b/api/controllers/app/controller.go index 668fc5a246..bdee7142df 100644 --- a/api/controllers/app/controller.go +++ b/api/controllers/app/controller.go @@ -14,6 +14,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/sirupsen/logrus" @@ -46,6 +47,7 @@ type AppController struct { logger logrus.FieldLogger license []byte releaseData *release.ReleaseData + hcli helm.Client store store.Store configValues types.AppConfigValues clusterID string @@ -109,6 +111,12 @@ func WithReleaseData(releaseData *release.ReleaseData) AppControllerOption { } } +func WithHelmClient(hcli helm.Client) AppControllerOption { + return func(c *AppController) { + c.hcli = hcli + } +} + func WithConfigValues(configValues types.AppConfigValues) AppControllerOption { return func(c *AppController) { c.configValues = configValues @@ -202,6 +210,7 @@ func NewAppController(opts ...AppControllerOption) (*AppController, error) { appreleasemanager.WithLicense(license), appreleasemanager.WithIsAirgap(controller.airgapBundle != ""), appreleasemanager.WithPrivateCACertConfigMapName(controller.privateCACertConfigMapName), + appreleasemanager.WithHelmClient(controller.hcli), ) if err != nil { return nil, fmt.Errorf("create app release manager: %w", err) diff --git a/api/controllers/kubernetes/install/controller.go b/api/controllers/kubernetes/install/controller.go index 030df882c2..d8321e61c9 100644 --- a/api/controllers/kubernetes/install/controller.go +++ b/api/controllers/kubernetes/install/controller.go @@ -14,11 +14,11 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/sirupsen/logrus" helmcli "helm.sh/helm/v3/pkg/cli" - "k8s.io/cli-runtime/pkg/genericclioptions" ) type Controller interface { @@ -34,21 +34,22 @@ type Controller interface { var _ Controller = (*InstallController)(nil) type InstallController struct { - installationManager installation.InstallationManager - infraManager infra.InfraManager - metricsReporter metrics.ReporterInterface - restClientGetter genericclioptions.RESTClientGetter - releaseData *release.ReleaseData - password string - tlsConfig types.TLSConfig - license []byte - airgapBundle string - configValues types.AppConfigValues - endUserConfig *ecv1beta1.Config - store store.Store - ki kubernetesinstallation.Installation - stateMachine statemachine.Interface - logger logrus.FieldLogger + installationManager installation.InstallationManager + infraManager infra.InfraManager + metricsReporter metrics.ReporterInterface + kubernetesEnvSettings *helmcli.EnvSettings + hcli helm.Client + releaseData *release.ReleaseData + password string + tlsConfig types.TLSConfig + license []byte + airgapBundle string + configValues types.AppConfigValues + endUserConfig *ecv1beta1.Config + store store.Store + ki kubernetesinstallation.Installation + stateMachine statemachine.Interface + logger logrus.FieldLogger // App controller composition *appcontroller.AppController } @@ -73,9 +74,15 @@ func WithMetricsReporter(metricsReporter metrics.ReporterInterface) InstallContr } } -func WithRESTClientGetter(restClientGetter genericclioptions.RESTClientGetter) InstallControllerOption { +func WithHelmClient(hcli helm.Client) InstallControllerOption { return func(c *InstallController) { - c.restClientGetter = restClientGetter + c.hcli = hcli + } +} + +func WithKubernetesEnvSettings(envSettings *helmcli.EnvSettings) InstallControllerOption { + return func(c *InstallController) { + c.kubernetesEnvSettings = envSettings } } @@ -169,9 +176,9 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, controller.stateMachine = NewStateMachine(WithStateMachineLogger(controller.logger)) } - // If none is provided, use the default env settings from helm to create a RESTClientGetter - if controller.restClientGetter == nil { - controller.restClientGetter = helmcli.New().RESTClientGetter() + // If none is provided, use the default env settings from helm + if controller.kubernetesEnvSettings == nil { + controller.kubernetesEnvSettings = helmcli.New() } if controller.installationManager == nil { @@ -192,6 +199,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, appcontroller.WithConfigValues(controller.configValues), appcontroller.WithAirgapBundle(controller.airgapBundle), appcontroller.WithPrivateCACertConfigMapName(""), // Private CA ConfigMap functionality not yet implemented for Kubernetes installations + appcontroller.WithHelmClient(controller.hcli), ) if err != nil { return nil, fmt.Errorf("create app install controller: %w", err) @@ -203,13 +211,14 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, infraManager, err := infra.NewInfraManager( infra.WithLogger(controller.logger), infra.WithInfraStore(controller.store.KubernetesInfraStore()), - infra.WithRESTClientGetter(controller.restClientGetter), + infra.WithKubernetesEnvSettings(controller.kubernetesEnvSettings), infra.WithPassword(controller.password), infra.WithTLSConfig(controller.tlsConfig), infra.WithLicense(controller.license), infra.WithAirgapBundle(controller.airgapBundle), infra.WithReleaseData(controller.releaseData), infra.WithEndUserConfig(controller.endUserConfig), + infra.WithHelmClient(controller.hcli), ) if err != nil { return nil, fmt.Errorf("create infra manager: %w", err) diff --git a/api/controllers/kubernetes/upgrade/controller.go b/api/controllers/kubernetes/upgrade/controller.go index d788c20dfc..e2cc86e349 100644 --- a/api/controllers/kubernetes/upgrade/controller.go +++ b/api/controllers/kubernetes/upgrade/controller.go @@ -10,10 +10,10 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/sirupsen/logrus" helmcli "helm.sh/helm/v3/pkg/cli" - "k8s.io/cli-runtime/pkg/genericclioptions" ) type Controller interface { @@ -24,14 +24,15 @@ type Controller interface { var _ Controller = (*UpgradeController)(nil) type UpgradeController struct { - restClientGetter genericclioptions.RESTClientGetter - releaseData *release.ReleaseData - license []byte - airgapBundle string - configValues types.AppConfigValues - store store.Store - stateMachine statemachine.Interface - logger logrus.FieldLogger + kubernetesEnvSettings *helmcli.EnvSettings + hcli helm.Client + releaseData *release.ReleaseData + license []byte + airgapBundle string + configValues types.AppConfigValues + store store.Store + stateMachine statemachine.Interface + logger logrus.FieldLogger // App controller composition *appcontroller.AppController } @@ -44,9 +45,15 @@ func WithLogger(logger logrus.FieldLogger) UpgradeControllerOption { } } -func WithRESTClientGetter(restClientGetter genericclioptions.RESTClientGetter) UpgradeControllerOption { +func WithKubernetesEnvSettings(envSettings *helmcli.EnvSettings) UpgradeControllerOption { return func(c *UpgradeController) { - c.restClientGetter = restClientGetter + c.kubernetesEnvSettings = envSettings + } +} + +func WithHelmClient(hcli helm.Client) UpgradeControllerOption { + return func(c *UpgradeController) { + c.hcli = hcli } } @@ -110,9 +117,9 @@ func NewUpgradeController(opts ...UpgradeControllerOption) (*UpgradeController, controller.stateMachine = NewStateMachine(WithStateMachineLogger(controller.logger)) } - // If none is provided, use the default env settings from helm to create a RESTClientGetter - if controller.restClientGetter == nil { - controller.restClientGetter = helmcli.New().RESTClientGetter() + // If none is provided, use the default env settings from helm + if controller.kubernetesEnvSettings == nil { + controller.kubernetesEnvSettings = helmcli.New() } // Initialize the app controller with the state machine @@ -126,6 +133,7 @@ func NewUpgradeController(opts ...UpgradeControllerOption) (*UpgradeController, appcontroller.WithConfigValues(controller.configValues), appcontroller.WithAirgapBundle(controller.airgapBundle), appcontroller.WithPrivateCACertConfigMapName(""), // Private CA ConfigMap functionality not yet implemented for Kubernetes installations + appcontroller.WithHelmClient(controller.hcli), ) if err != nil { return nil, fmt.Errorf("create app controller: %w", err) diff --git a/api/controllers/linux/install/controller.go b/api/controllers/linux/install/controller.go index ab263567f4..67da48acae 100644 --- a/api/controllers/linux/install/controller.go +++ b/api/controllers/linux/install/controller.go @@ -19,6 +19,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" "github.com/replicatedhq/embedded-cluster/pkg/airgap" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -68,6 +69,7 @@ type InstallController struct { clusterID string store store.Store rc runtimeconfig.RuntimeConfig + hcli helm.Client stateMachine statemachine.Interface logger logrus.FieldLogger allowIgnoreHostPreflights bool @@ -215,6 +217,12 @@ func WithStore(store store.Store) InstallControllerOption { } } +func WithHelmClient(hcli helm.Client) InstallControllerOption { + return func(c *InstallController) { + c.hcli = hcli + } +} + func NewInstallController(opts ...InstallControllerOption) (*InstallController, error) { controller := &InstallController{ store: store.NewMemoryStore(), @@ -279,6 +287,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, appcontroller.WithClusterID(controller.clusterID), appcontroller.WithAirgapBundle(controller.airgapBundle), appcontroller.WithPrivateCACertConfigMapName(adminconsole.PrivateCASConfigMapName), // Linux installations use the ConfigMap + appcontroller.WithHelmClient(controller.hcli), ) if err != nil { return nil, fmt.Errorf("create app controller: %w", err) @@ -299,6 +308,7 @@ func NewInstallController(opts ...InstallControllerOption) (*InstallController, infra.WithReleaseData(controller.releaseData), infra.WithEndUserConfig(controller.endUserConfig), infra.WithClusterID(controller.clusterID), + infra.WithHelmClient(controller.hcli), ) } diff --git a/api/controllers/linux/upgrade/controller.go b/api/controllers/linux/upgrade/controller.go index 5d3c988c55..219d7a6acb 100644 --- a/api/controllers/linux/upgrade/controller.go +++ b/api/controllers/linux/upgrade/controller.go @@ -18,6 +18,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" "github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole" "github.com/replicatedhq/embedded-cluster/pkg/airgap" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -54,6 +55,7 @@ type UpgradeController struct { clusterID string store store.Store rc runtimeconfig.RuntimeConfig + hcli helm.Client stateMachine statemachine.Interface requiresInfraUpgrade bool logger logrus.FieldLogger @@ -173,6 +175,12 @@ func WithRuntimeConfig(rc runtimeconfig.RuntimeConfig) UpgradeControllerOption { } } +func WithHelmClient(hcli helm.Client) UpgradeControllerOption { + return func(c *UpgradeController) { + c.hcli = hcli + } +} + func WithEndUserConfig(endUserConfig *ecv1beta1.Config) UpgradeControllerOption { return func(c *UpgradeController) { c.endUserConfig = endUserConfig @@ -245,6 +253,7 @@ func NewUpgradeController(opts ...UpgradeControllerOption) (*UpgradeController, infra.WithReleaseData(controller.releaseData), infra.WithEndUserConfig(controller.endUserConfig), infra.WithClusterID(controller.clusterID), + infra.WithHelmClient(controller.hcli), ) } @@ -282,6 +291,7 @@ func NewUpgradeController(opts ...UpgradeControllerOption) (*UpgradeController, appcontroller.WithClusterID(controller.clusterID), appcontroller.WithAirgapBundle(controller.airgapBundle), appcontroller.WithPrivateCACertConfigMapName(adminconsole.PrivateCASConfigMapName), // Linux upgrades use the ConfigMap + appcontroller.WithHelmClient(controller.hcli), ) if err != nil { return nil, fmt.Errorf("create app controller: %w", err) diff --git a/api/handlers.go b/api/handlers.go index 140f370337..52be0172bc 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -50,7 +50,7 @@ func (a *API) initHandlers() error { } a.handlers.health = healthHandler - if a.cfg.Target == types.TargetLinux { + if a.cfg.InstallTarget == types.InstallTargetLinux { // Linux handler linuxHandler, err := linuxhandler.New( a.cfg, @@ -58,6 +58,7 @@ func (a *API) initHandlers() error { linuxhandler.WithMetricsReporter(a.metricsReporter), linuxhandler.WithInstallController(a.linuxInstallController), linuxhandler.WithUpgradeController(a.linuxUpgradeController), + linuxhandler.WithHelmClient(a.hcli), ) if err != nil { return fmt.Errorf("new linux handler: %w", err) @@ -70,6 +71,7 @@ func (a *API) initHandlers() error { kuberneteshandler.WithLogger(a.logger), kuberneteshandler.WithInstallController(a.kubernetesInstallController), kuberneteshandler.WithUpgradeController(a.kubernetesUpgradeController), + kuberneteshandler.WithHelmClient(a.hcli), ) if err != nil { return fmt.Errorf("new kubernetes handler: %w", err) diff --git a/api/integration/app/install/config_test.go b/api/integration/app/install/config_test.go index 261f6437d2..c20d749d9a 100644 --- a/api/integration/app/install/config_test.go +++ b/api/integration/app/install/config_test.go @@ -18,12 +18,14 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/states" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + helmcli "helm.sh/helm/v3/pkg/cli" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -1046,12 +1048,13 @@ func TestAppInstallSuite(t *testing.T) { controller, err := linuxinstall.NewInstallController( linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(initialState))), linuxinstall.WithReleaseData(rd), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithLicense([]byte("spec:\n licenseID: test-license\n")), linuxinstall.WithConfigValues(configValues), ) require.NoError(t, err) // Create the API with the install controller - return integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + return integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -1066,12 +1069,14 @@ func TestAppInstallSuite(t *testing.T) { controller, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithStateMachine(kubernetesinstall.NewStateMachine(kubernetesinstall.WithCurrentState(initialState))), kubernetesinstall.WithReleaseData(rd), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), kubernetesinstall.WithLicense([]byte("spec:\n licenseID: test-license\n")), kubernetesinstall.WithConfigValues(configValues), + kubernetesinstall.WithKubernetesEnvSettings(helmcli.New()), ) require.NoError(t, err) // Create the API with the install controller - return integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + return integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/app/upgrade/apppreflight_test.go b/api/integration/app/upgrade/apppreflight_test.go index 8f4393c052..cf6667b3c4 100644 --- a/api/integration/app/upgrade/apppreflight_test.go +++ b/api/integration/app/upgrade/apppreflight_test.go @@ -27,6 +27,7 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -35,7 +36,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "k8s.io/cli-runtime/pkg/genericclioptions" + helmcli "helm.sh/helm/v3/pkg/cli" ) type AppPreflightTestSuite struct { @@ -92,6 +93,7 @@ func (s *AppPreflightTestSuite) TestGetAppPreflightsStatus() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(s.T(), err) @@ -190,6 +192,7 @@ func (s *AppPreflightTestSuite) TestGetAppPreflightsStatus() { appcontroller.WithStateMachine(strictStateMachine), appcontroller.WithStore(mockStrictStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -293,6 +296,7 @@ func (s *AppPreflightTestSuite) TestPostRunAppPreflights() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -343,6 +347,7 @@ func (s *AppPreflightTestSuite) TestPostRunAppPreflights() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(&store.MockStore{}), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -380,6 +385,7 @@ func (s *AppPreflightTestSuite) TestPostRunAppPreflights() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(&store.MockStore{}), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -441,6 +447,7 @@ func TestAppPreflightSuite(t *testing.T) { }, AppConfig: &kotsv1beta1.Config{}, }), + linuxupgrade.WithHelmClient(&helm.MockClient{}), linuxupgrade.WithAppController(appController), linuxupgrade.WithInfraManager(mockInfraManager), ) @@ -448,17 +455,18 @@ func TestAppPreflightSuite(t *testing.T) { // Create the API with runtime config in the API config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetLinux, + Password: "password", LinuxConfig: types.LinuxConfig{ RuntimeConfig: rc, }, ReleaseData: rd, Mode: types.ModeUpgrade, - Target: types.TargetLinux, }, api.WithLinuxUpgradeController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), // Prevent permission errors from log file creation + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) return apiInstance @@ -481,24 +489,26 @@ func TestAppPreflightSuite(t *testing.T) { controller, err := kubernetesupgrade.NewUpgradeController( kubernetesupgrade.WithStateMachine(stateMachine), kubernetesupgrade.WithReleaseData(rd), + kubernetesupgrade.WithHelmClient(&helm.MockClient{}), kubernetesupgrade.WithAppController(appController), + kubernetesupgrade.WithKubernetesEnvSettings(helmcli.New()), ) require.NoError(t, err) // Create the API with kubernetes config in the API config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetKubernetes, + Password: "password", KubernetesConfig: types.KubernetesConfig{ - RESTClientGetter: &genericclioptions.ConfigFlags{}, - Installation: mockInstallation, + Installation: mockInstallation, }, ReleaseData: rd, Mode: types.ModeUpgrade, - Target: types.TargetKubernetes, }, api.WithKubernetesUpgradeController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) return apiInstance diff --git a/api/integration/app/upgrade/config_test.go b/api/integration/app/upgrade/config_test.go index 75c7a85436..40ce768929 100644 --- a/api/integration/app/upgrade/config_test.go +++ b/api/integration/app/upgrade/config_test.go @@ -18,6 +18,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/states" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" @@ -25,6 +26,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + helmcli "helm.sh/helm/v3/pkg/cli" ) type AppConfigTestSuite struct { @@ -358,19 +360,21 @@ func TestAppConfigSuite(t *testing.T) { linuxupgrade.WithStateMachine(linuxupgrade.NewStateMachine(linuxupgrade.WithCurrentState(state))), linuxupgrade.WithConfigValues(configValues), linuxupgrade.WithReleaseData(releaseData), + linuxupgrade.WithHelmClient(&helm.MockClient{}), linuxupgrade.WithInfraManager(mockInfraManager), ) require.NoError(t, err) apiInstance, err := api.New(types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: "password", + ReleaseData: releaseData, + Mode: types.ModeUpgrade, }, api.WithLinuxUpgradeController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) return apiInstance @@ -389,18 +393,21 @@ func TestAppConfigSuite(t *testing.T) { kubernetesupgrade.WithStateMachine(kubernetesupgrade.NewStateMachine(kubernetesupgrade.WithCurrentState(state))), kubernetesupgrade.WithConfigValues(configValues), kubernetesupgrade.WithReleaseData(releaseData), + kubernetesupgrade.WithHelmClient(&helm.MockClient{}), + kubernetesupgrade.WithKubernetesEnvSettings(helmcli.New()), ) require.NoError(t, err) apiInstance, err := api.New(types.APIConfig{ - Password: "password", - ReleaseData: releaseData, - Mode: types.ModeUpgrade, - Target: types.TargetKubernetes, + InstallTarget: types.InstallTargetKubernetes, + Password: "password", + ReleaseData: releaseData, + Mode: types.ModeUpgrade, }, api.WithKubernetesUpgradeController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) return apiInstance diff --git a/api/integration/app/upgrade/upgrade_test.go b/api/integration/app/upgrade/upgrade_test.go index 8adf6b435b..b82f9c24f8 100644 --- a/api/integration/app/upgrade/upgrade_test.go +++ b/api/integration/app/upgrade/upgrade_test.go @@ -24,12 +24,14 @@ import ( appupgradestore "github.com/replicatedhq/embedded-cluster/api/internal/store/app/upgrade" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + helmcli "helm.sh/helm/v3/pkg/cli" ) type AppUpgradeTestSuite struct { @@ -70,6 +72,7 @@ func (s *AppUpgradeTestSuite) TestPostUpgradeApp() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -126,6 +129,7 @@ func (s *AppUpgradeTestSuite) TestPostUpgradeApp() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(&store.MockStore{}), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -191,6 +195,7 @@ func (s *AppUpgradeTestSuite) TestPostUpgradeApp() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -266,6 +271,7 @@ func (s *AppUpgradeTestSuite) TestGetAppUpgradeStatus() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(&store.MockStore{}), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -307,6 +313,7 @@ func (s *AppUpgradeTestSuite) TestGetAppUpgradeStatus() { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(&store.MockStore{}), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -360,20 +367,22 @@ func TestAppUpgradeSuite(t *testing.T) { controller, err := linuxupgrade.NewUpgradeController( linuxupgrade.WithStateMachine(stateMachine), linuxupgrade.WithReleaseData(rd), + linuxupgrade.WithHelmClient(&helm.MockClient{}), linuxupgrade.WithAppController(appController), linuxupgrade.WithInfraManager(mockInfraManager), ) require.NoError(t, err) apiInstance, err := api.New(types.APIConfig{ - Password: "password", - ReleaseData: rd, - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: "password", + ReleaseData: rd, + Mode: types.ModeUpgrade, }, api.WithLinuxUpgradeController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) return apiInstance @@ -391,10 +400,12 @@ func TestAppUpgradeSuite(t *testing.T) { controller, err := kubernetesupgrade.NewUpgradeController( kubernetesupgrade.WithStateMachine(stateMachine), kubernetesupgrade.WithReleaseData(rd), + kubernetesupgrade.WithHelmClient(&helm.MockClient{}), kubernetesupgrade.WithAppController(appController), + kubernetesupgrade.WithKubernetesEnvSettings(helmcli.New()), ) require.NoError(t, err) - return integration.NewAPIWithReleaseData(t, types.ModeUpgrade, types.TargetKubernetes, + return integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeUpgrade, api.WithKubernetesUpgradeController(controller), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/auth/controller_test.go b/api/integration/auth/controller_test.go index cedd16acee..9cbfb10dd8 100644 --- a/api/integration/auth/controller_test.go +++ b/api/integration/auth/controller_test.go @@ -17,6 +17,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/utils" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" @@ -35,11 +36,12 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { installation.WithNetUtils(&utils.MockNetUtils{}), )), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the auth controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithAuthController(authController), api.WithLinuxInstallController(installController), api.WithLogger(logger.NewDiscardLogger()), @@ -137,7 +139,7 @@ func TestAuthLoginAndTokenValidation(t *testing.T) { func TestAPIClientLogin(t *testing.T) { // Create the API with the auth controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLogger(logger.NewDiscardLogger()), ) diff --git a/api/integration/console/controller_test.go b/api/integration/console/controller_test.go index 897084ad44..42c1c11978 100644 --- a/api/integration/console/controller_test.go +++ b/api/integration/console/controller_test.go @@ -29,7 +29,7 @@ func TestConsoleListAvailableNetworkInterfaces(t *testing.T) { require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithConsoleController(consoleController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -73,7 +73,7 @@ func TestConsoleListAvailableNetworkInterfacesUnauthorized(t *testing.T) { require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithConsoleController(consoleController), api.WithAuthController(auth.NewStaticAuthController("VALID_TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -113,7 +113,7 @@ func TestConsoleListAvailableNetworkInterfacesError(t *testing.T) { require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithConsoleController(consoleController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/kubernetes/install/appconfig_test.go b/api/integration/kubernetes/install/appconfig_test.go index b46bdac343..71756186d6 100644 --- a/api/integration/kubernetes/install/appconfig_test.go +++ b/api/integration/kubernetes/install/appconfig_test.go @@ -15,6 +15,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/states" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" @@ -65,11 +66,12 @@ func TestInstallController_PatchAppConfigValuesWithAPIClient(t *testing.T) { kubernetesinstall.WithReleaseData(&release.ReleaseData{ AppConfig: &appConfig, }), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -116,11 +118,12 @@ func TestInstallController_PatchAppConfigValuesWithAPIClient(t *testing.T) { kubernetesinstall.WithReleaseData(&release.ReleaseData{ AppConfig: &appConfig, }), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the completed install controller - completedAPIInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + completedAPIInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(completedInstallController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -219,11 +222,12 @@ func TestInstallController_GetAppConfigValuesWithAPIClient(t *testing.T) { kubernetesinstall.WithReleaseData(&release.ReleaseData{ AppConfig: &appConfig, }), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/kubernetes/install/appinstall_test.go b/api/integration/kubernetes/install/appinstall_test.go index 3fc30493b4..2f4136eb97 100644 --- a/api/integration/kubernetes/install/appinstall_test.go +++ b/api/integration/kubernetes/install/appinstall_test.go @@ -23,6 +23,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/stretchr/testify/assert" @@ -59,6 +60,7 @@ func TestGetAppInstallStatus(t *testing.T) { appcontroller.WithStateMachine(kubernetesinstall.NewStateMachine()), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -76,11 +78,12 @@ func TestGetAppInstallStatus(t *testing.T) { }, AppConfig: &kotsv1beta1.Config{}, }), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -117,11 +120,12 @@ func TestGetAppInstallStatus(t *testing.T) { // Create simple Kubernetes install controller installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -148,7 +152,7 @@ func TestGetAppInstallStatus(t *testing.T) { mockController.On("GetAppInstallStatus", mock.Anything).Return(types.AppInstall{}, assert.AnError) // Create the API with mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -204,6 +208,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -221,11 +226,12 @@ func TestPostInstallApp(t *testing.T) { }, AppConfig: &kotsv1beta1.Config{}, }), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -267,6 +273,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -275,11 +282,12 @@ func TestPostInstallApp(t *testing.T) { kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppController(appController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -328,6 +336,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -336,11 +345,12 @@ func TestPostInstallApp(t *testing.T) { kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppController(appController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -374,11 +384,12 @@ func TestPostInstallApp(t *testing.T) { // Create simple Kubernetes install controller installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -440,6 +451,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -448,11 +460,12 @@ func TestPostInstallApp(t *testing.T) { kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppController(appController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -517,6 +530,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -525,11 +539,12 @@ func TestPostInstallApp(t *testing.T) { kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppController(appController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -595,6 +610,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -603,11 +619,12 @@ func TestPostInstallApp(t *testing.T) { kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppController(appController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/kubernetes/install/apppreflight_test.go b/api/integration/kubernetes/install/apppreflight_test.go index ba50502e1e..f01e95625f 100644 --- a/api/integration/kubernetes/install/apppreflight_test.go +++ b/api/integration/kubernetes/install/apppreflight_test.go @@ -22,11 +22,11 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/helm" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "k8s.io/cli-runtime/pkg/genericclioptions" ) // Test the getAppPreflightsStatus endpoint returns app preflights status correctly @@ -73,6 +73,7 @@ func TestGetAppPreflightsStatus(t *testing.T) { appcontroller.WithStateMachine(kubernetesinstall.NewStateMachine()), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -80,11 +81,12 @@ func TestGetAppPreflightsStatus(t *testing.T) { installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithAppController(appController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -172,6 +174,7 @@ func TestGetAppPreflightsStatus(t *testing.T) { appcontroller.WithStateMachine(kubernetesinstall.NewStateMachine()), appcontroller.WithStore(mockStrictStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -179,11 +182,12 @@ func TestGetAppPreflightsStatus(t *testing.T) { strictInstallController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithAppController(strictAppController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - strictAPIInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + strictAPIInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(strictInstallController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -238,7 +242,7 @@ func TestGetAppPreflightsStatus(t *testing.T) { mockController.On("GetAppPreflightTitles", mock.Anything).Return([]string{}, assert.AnError) // Create the API with the mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -325,6 +329,7 @@ func TestPostRunAppPreflights(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -333,23 +338,24 @@ func TestPostRunAppPreflights(t *testing.T) { kubernetesinstall.WithStateMachine(stateMachine), kubernetesinstall.WithAppController(appController), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with kubernetes config in the API config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetKubernetes, + Password: "password", KubernetesConfig: types.KubernetesConfig{ - RESTClientGetter: &genericclioptions.ConfigFlags{}, - Installation: mockInstallation, + Installation: mockInstallation, }, ReleaseData: integration.DefaultReleaseData(), Mode: types.ModeInstall, - Target: types.TargetKubernetes, }, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -386,23 +392,24 @@ func TestPostRunAppPreflights(t *testing.T) { kubernetesinstall.WithCurrentState(states.StateNew), // Wrong state )), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with kubernetes config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetKubernetes, + Password: "password", KubernetesConfig: types.KubernetesConfig{ - RESTClientGetter: &genericclioptions.ConfigFlags{}, - Installation: mockInstallation, + Installation: mockInstallation, }, ReleaseData: integration.DefaultReleaseData(), Mode: types.ModeInstall, - Target: types.TargetKubernetes, }, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -433,23 +440,24 @@ func TestPostRunAppPreflights(t *testing.T) { // Create a basic install controller installController, err := kubernetesinstall.NewInstallController( kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with kubernetes config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetKubernetes, + Password: "password", KubernetesConfig: types.KubernetesConfig{ - RESTClientGetter: &genericclioptions.ConfigFlags{}, - Installation: mockInstallation, + Installation: mockInstallation, }, ReleaseData: integration.DefaultReleaseData(), Mode: types.ModeInstall, - Target: types.TargetKubernetes, }, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) diff --git a/api/integration/kubernetes/install/infra_test.go b/api/integration/kubernetes/install/infra_test.go index d001f49093..9dcea4de83 100644 --- a/api/integration/kubernetes/install/infra_test.go +++ b/api/integration/kubernetes/install/infra_test.go @@ -111,6 +111,7 @@ func TestKubernetesPostSetupInfra(t *testing.T) { }, AppConfig: &appConfig, }), + kubernetesinfra.WithHelmClient(helmMock), ) require.NoError(t, err) @@ -133,11 +134,12 @@ func TestKubernetesPostSetupInfra(t *testing.T) { }, AppConfig: &appConfig, }), + kubernetesinstall.WithHelmClient(helmMock), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -229,9 +231,25 @@ func TestKubernetesPostSetupInfra(t *testing.T) { // Test authorization t.Run("Authorization error", func(t *testing.T) { + installController, err := kubernetesinstall.NewInstallController( + kubernetesinstall.WithReleaseData(&release.ReleaseData{ + EmbeddedClusterConfig: &ecv1beta1.Config{}, + ChannelRelease: &release.ChannelRelease{ + DefaultDomains: release.Domains{ + ReplicatedAppDomain: "replicated.example.com", + ProxyRegistryDomain: "some-proxy.example.com", + }, + }, + AppConfig: &appConfig, + }), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), + ) + require.NoError(t, err) + // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithAuthController(auth.NewStaticAuthController("TOKEN")), + api.WithKubernetesInstallController(installController), api.WithLogger(logger.NewDiscardLogger()), ) @@ -253,7 +271,7 @@ func TestKubernetesPostSetupInfra(t *testing.T) { // Parse the response body var apiError types.APIError - err := json.NewDecoder(rec.Body).Decode(&apiError) + err = json.NewDecoder(rec.Body).Decode(&apiError) require.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, apiError.StatusCode) }) @@ -292,6 +310,7 @@ func TestKubernetesPostSetupInfra(t *testing.T) { }, AppConfig: &appConfig, }), + kubernetesinfra.WithHelmClient(helmMock), ) require.NoError(t, err) @@ -314,11 +333,12 @@ func TestKubernetesPostSetupInfra(t *testing.T) { }, AppConfig: &appConfig, }), + kubernetesinstall.WithHelmClient(helmMock), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/kubernetes/install/installation_test.go b/api/integration/kubernetes/install/installation_test.go index 7ae66ddedd..b05e52d3b2 100644 --- a/api/integration/kubernetes/install/installation_test.go +++ b/api/integration/kubernetes/install/installation_test.go @@ -21,6 +21,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -157,11 +158,12 @@ func TestKubernetesConfigureInstallation(t *testing.T) { kubernetesinstall.WithInstallation(ki), kubernetesinstall.WithStateMachine(kubernetesinstall.NewStateMachine(kubernetesinstall.WithCurrentState(states.StateApplicationConfigured))), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -250,11 +252,12 @@ func TestKubernetesConfigureInstallationValidation(t *testing.T) { kubernetesinstall.WithInstallation(ki), kubernetesinstall.WithStateMachine(kubernetesinstall.NewStateMachine(kubernetesinstall.WithCurrentState(states.StateApplicationConfigured))), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -308,10 +311,11 @@ func TestKubernetesConfigureInstallationBadRequest(t *testing.T) { kubernetesinstall.WithInstallation(ki), kubernetesinstall.WithStateMachine(kubernetesinstall.NewStateMachine(kubernetesinstall.WithCurrentState(states.StateApplicationConfigured))), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -343,7 +347,7 @@ func TestKubernetesConfigureInstallationControllerError(t *testing.T) { mockController.On("ConfigureInstallation", mock.Anything, mock.Anything).Return(assert.AnError) // Create the API with the mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -392,6 +396,7 @@ func TestKubernetesGetInstallationConfig(t *testing.T) { kubernetesinstall.WithInstallation(ki), kubernetesinstall.WithInstallationManager(installationManager), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -406,7 +411,7 @@ func TestKubernetesGetInstallationConfig(t *testing.T) { require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -457,11 +462,12 @@ func TestKubernetesGetInstallationConfig(t *testing.T) { kubernetesinstall.WithInstallation(ki), kubernetesinstall.WithInstallationManager(emptyInstallationManager), kubernetesinstall.WithReleaseData(integration.DefaultReleaseData()), + kubernetesinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - emptyAPI := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + emptyAPI := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(emptyInstallController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -525,7 +531,7 @@ func TestKubernetesGetInstallationConfig(t *testing.T) { mockController.On("GetInstallationConfig", mock.Anything).Return(types.KubernetesInstallationConfigResponse{}, assert.AnError) // Create the API with the mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetKubernetes, + apiInstance := integration.NewTargetKubernetesAPIWithReleaseData(t, types.ModeInstall, api.WithKubernetesInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/linux/install/appconfig_test.go b/api/integration/linux/install/appconfig_test.go index 24d55df8fc..8d3c04c264 100644 --- a/api/integration/linux/install/appconfig_test.go +++ b/api/integration/linux/install/appconfig_test.go @@ -15,6 +15,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/states" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/replicatedhq/kotskinds/multitype" @@ -65,11 +66,12 @@ func TestInstallController_PatchAppConfigValuesWithAPIClient(t *testing.T) { linuxinstall.WithReleaseData(&release.ReleaseData{ AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -116,11 +118,12 @@ func TestInstallController_PatchAppConfigValuesWithAPIClient(t *testing.T) { linuxinstall.WithReleaseData(&release.ReleaseData{ AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the completed install controller - completedAPIInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + completedAPIInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(completedInstallController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -219,11 +222,12 @@ func TestInstallController_GetAppConfigValuesWithAPIClient(t *testing.T) { linuxinstall.WithReleaseData(&release.ReleaseData{ AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/linux/install/appinstall_test.go b/api/integration/linux/install/appinstall_test.go index 63889e91fb..3fd8cc6347 100644 --- a/api/integration/linux/install/appinstall_test.go +++ b/api/integration/linux/install/appinstall_test.go @@ -23,6 +23,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -61,6 +62,7 @@ func TestGetAppInstallStatus(t *testing.T) { appcontroller.WithStateMachine(linuxinstall.NewStateMachine()), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -78,12 +80,13 @@ func TestGetAppInstallStatus(t *testing.T) { }, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(runtimeconfig.New(nil)), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -120,11 +123,12 @@ func TestGetAppInstallStatus(t *testing.T) { // Create simple Linux install controller installController, err := linuxinstall.NewInstallController( linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -151,7 +155,7 @@ func TestGetAppInstallStatus(t *testing.T) { mockController.On("GetAppInstallStatus", mock.Anything).Return(types.AppInstall{}, assert.AnError) // Create the API with mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -214,6 +218,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -232,12 +237,13 @@ func TestPostInstallApp(t *testing.T) { }, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -284,6 +290,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -292,11 +299,12 @@ func TestPostInstallApp(t *testing.T) { linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppController(appController), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -359,6 +367,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -369,12 +378,13 @@ func TestPostInstallApp(t *testing.T) { linuxinstall.WithMetricsReporter(mockMetricsReporter), linuxinstall.WithStore(mockStore), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -414,11 +424,12 @@ func TestPostInstallApp(t *testing.T) { // Create simple Linux install controller installController, err := linuxinstall.NewInstallController( linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -480,6 +491,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -488,11 +500,12 @@ func TestPostInstallApp(t *testing.T) { linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppController(appController), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -557,6 +570,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -565,11 +579,12 @@ func TestPostInstallApp(t *testing.T) { linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppController(appController), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -635,6 +650,7 @@ func TestPostInstallApp(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -643,11 +659,12 @@ func TestPostInstallApp(t *testing.T) { linuxinstall.WithStateMachine(stateMachine), linuxinstall.WithAppController(appController), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/linux/install/apppreflight_test.go b/api/integration/linux/install/apppreflight_test.go index 9cd37d004c..e9b0a92d42 100644 --- a/api/integration/linux/install/apppreflight_test.go +++ b/api/integration/linux/install/apppreflight_test.go @@ -21,6 +21,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -74,6 +75,7 @@ func TestGetAppPreflightsStatus(t *testing.T) { appcontroller.WithStateMachine(linuxinstall.NewStateMachine()), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -81,11 +83,12 @@ func TestGetAppPreflightsStatus(t *testing.T) { installController, err := linuxinstall.NewInstallController( linuxinstall.WithAppController(appController), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -173,6 +176,7 @@ func TestGetAppPreflightsStatus(t *testing.T) { appcontroller.WithStateMachine(linuxinstall.NewStateMachine()), appcontroller.WithStore(mockStrictStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -180,11 +184,12 @@ func TestGetAppPreflightsStatus(t *testing.T) { strictInstallController, err := linuxinstall.NewInstallController( linuxinstall.WithAppController(strictAppController), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - strictAPIInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + strictAPIInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(strictInstallController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -239,7 +244,7 @@ func TestGetAppPreflightsStatus(t *testing.T) { mockController.On("GetAppPreflightTitles", mock.Anything).Return([]string{}, assert.AnError) // Create the API with the mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -325,6 +330,7 @@ func TestPostRunAppPreflights(t *testing.T) { appcontroller.WithStateMachine(stateMachine), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(integration.DefaultReleaseData()), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -342,23 +348,25 @@ func TestPostRunAppPreflights(t *testing.T) { }, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with runtime config in the API config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetLinux, + Password: "password", LinuxConfig: types.LinuxConfig{ RuntimeConfig: rc, }, ReleaseData: integration.DefaultReleaseData(), Mode: types.ModeInstall, - Target: types.TargetLinux, }, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -394,23 +402,25 @@ func TestPostRunAppPreflights(t *testing.T) { linuxinstall.WithCurrentState(states.StateNew), // Wrong state )), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with runtime config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetLinux, + Password: "password", LinuxConfig: types.LinuxConfig{ RuntimeConfig: rc, }, ReleaseData: integration.DefaultReleaseData(), Mode: types.ModeInstall, - Target: types.TargetLinux, }, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -441,22 +451,24 @@ func TestPostRunAppPreflights(t *testing.T) { // Create a basic install controller installController, err := linuxinstall.NewInstallController( linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with runtime config apiInstance, err := api.New(types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetLinux, + Password: "password", LinuxConfig: types.LinuxConfig{ RuntimeConfig: rc, }, ReleaseData: integration.DefaultReleaseData(), Mode: types.ModeInstall, - Target: types.TargetLinux, }, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) diff --git a/api/integration/linux/install/hostpreflight_test.go b/api/integration/linux/install/hostpreflight_test.go index e6673f5d70..3d2ad4e9c8 100644 --- a/api/integration/linux/install/hostpreflight_test.go +++ b/api/integration/linux/install/hostpreflight_test.go @@ -22,6 +22,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/preflights" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/netutils" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -71,11 +72,12 @@ func TestGetHostPreflightsStatus(t *testing.T) { installController, err := linuxinstall.NewInstallController( linuxinstall.WithHostPreflightManager(manager), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -140,7 +142,7 @@ func TestGetHostPreflightsStatus(t *testing.T) { mockController.On("GetHostPreflightStatus", mock.Anything).Return(types.Status{}, assert.AnError) // Create the API with the mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -218,23 +220,25 @@ func TestGetHostPreflightsStatusWithIgnoreFlag(t *testing.T) { installController, err := linuxinstall.NewInstallController( linuxinstall.WithHostPreflightManager(manager), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with allow ignore host preflights flag apiInstance, err := api.New( types.APIConfig{ - Password: "password", + InstallTarget: types.InstallTargetLinux, + Password: "password", LinuxConfig: types.LinuxConfig{ AllowIgnoreHostPreflights: tt.allowIgnoreHostPreflights, }, ReleaseData: integration.DefaultReleaseData(), Mode: types.ModeInstall, - Target: types.TargetLinux, }, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -306,6 +310,7 @@ func TestPostRunHostPreflights(t *testing.T) { }, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) @@ -341,7 +346,7 @@ func TestPostRunHostPreflights(t *testing.T) { ) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -404,12 +409,13 @@ func TestPostRunHostPreflights(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -461,12 +467,13 @@ func TestPostRunHostPreflights(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -519,12 +526,13 @@ func TestPostRunHostPreflights(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -590,12 +598,13 @@ func TestPostRunHostPreflights(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &kotsv1beta1.Config{}, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/linux/install/infra_test.go b/api/integration/linux/install/infra_test.go index 492705ba19..d436c62a27 100644 --- a/api/integration/linux/install/infra_test.go +++ b/api/integration/linux/install/infra_test.go @@ -178,11 +178,12 @@ func TestLinuxPostSetupInfra(t *testing.T) { }, AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(helmMock), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -296,7 +297,7 @@ func TestLinuxPostSetupInfra(t *testing.T) { // Test authorization t.Run("Authorization error", func(t *testing.T) { // Create the API - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), ) @@ -355,11 +356,12 @@ func TestLinuxPostSetupInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -413,11 +415,12 @@ func TestLinuxPostSetupInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -478,11 +481,12 @@ func TestLinuxPostSetupInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -542,11 +546,12 @@ func TestLinuxPostSetupInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -606,11 +611,12 @@ func TestLinuxPostSetupInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -692,13 +698,14 @@ func TestLinuxPostSetupInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxinstall.WithHelmClient(&helm.MockClient{}), linuxinstall.WithRuntimeConfig(rc), linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(states.StateHostPreflightsSucceeded))), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/linux/install/installation_test.go b/api/integration/linux/install/installation_test.go index 171d7aff8b..4f4321c29e 100644 --- a/api/integration/linux/install/installation_test.go +++ b/api/integration/linux/install/installation_test.go @@ -25,6 +25,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/hostutils" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -303,11 +304,12 @@ func TestLinuxConfigureInstallation(t *testing.T) { linuxinstall.WithHostUtils(tc.mockHostUtils), linuxinstall.WithNetUtils(tc.mockNetUtils), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -399,11 +401,12 @@ func TestLinuxConfigureInstallationValidation(t *testing.T) { linuxinstall.WithRuntimeConfig(rc), linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(states.StateApplicationConfigured))), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -459,10 +462,11 @@ func TestLinuxConfigureInstallationBadRequest(t *testing.T) { linuxinstall.WithRuntimeConfig(rc), linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(states.StateHostConfigured))), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -494,7 +498,7 @@ func TestLinuxConfigureInstallationControllerError(t *testing.T) { mockController.On("ConfigureInstallation", mock.Anything, mock.Anything).Return(assert.AnError) // Create the API with the mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -549,6 +553,7 @@ func TestLinuxGetInstallationConfig(t *testing.T) { linuxinstall.WithRuntimeConfig(rc), linuxinstall.WithInstallationManager(installationManager), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -564,7 +569,7 @@ func TestLinuxGetInstallationConfig(t *testing.T) { require.NoError(t, err) // Create the API with the install controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(installController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -628,11 +633,12 @@ func TestLinuxGetInstallationConfig(t *testing.T) { linuxinstall.WithRuntimeConfig(rc), linuxinstall.WithInstallationManager(emptyInstallationManager), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) // Create the API with the install controller - emptyAPI := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + emptyAPI := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(emptyInstallController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -706,7 +712,7 @@ func TestLinuxGetInstallationConfig(t *testing.T) { mockController.On("GetInstallationConfig", mock.Anything).Return(types.LinuxInstallationConfigResponse{}, assert.AnError) // Create the API with the mock controller - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithLinuxInstallController(mockController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), @@ -760,6 +766,7 @@ func TestLinuxInstallationConfigWithAPIClient(t *testing.T) { linuxinstall.WithStateMachine(linuxinstall.NewStateMachine(linuxinstall.WithCurrentState(states.StateApplicationConfigured))), linuxinstall.WithInstallationManager(installationManager), linuxinstall.WithReleaseData(integration.DefaultReleaseData()), + linuxinstall.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -783,7 +790,7 @@ func TestLinuxInstallationConfigWithAPIClient(t *testing.T) { require.NoError(t, err) // Create the API with controllers - apiInstance := integration.NewAPIWithReleaseData(t, types.ModeInstall, types.TargetLinux, + apiInstance := integration.NewTargetLinuxAPIWithReleaseData(t, types.ModeInstall, api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLinuxInstallController(installController), api.WithLogger(logger.NewDiscardLogger()), diff --git a/api/integration/linux/upgrade/infra_test.go b/api/integration/linux/upgrade/infra_test.go index 3963b0b5b4..1c451227f7 100644 --- a/api/integration/linux/upgrade/infra_test.go +++ b/api/integration/linux/upgrade/infra_test.go @@ -168,6 +168,7 @@ func TestLinuxPostUpgradeInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxupgrade.WithHelmClient(helmMock), linuxupgrade.WithLicense(assets.LicenseData), ) require.NoError(t, err) @@ -177,16 +178,17 @@ func TestLinuxPostUpgradeInfra(t *testing.T) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) cfg := types.APIConfig{ - Password: password, - PasswordHash: passwordHash, - ReleaseData: integration.DefaultReleaseData(), - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, + ReleaseData: integration.DefaultReleaseData(), + Mode: types.ModeUpgrade, } apiInstance, err := api.New(cfg, api.WithLinuxUpgradeController(upgradeController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -309,6 +311,7 @@ func TestLinuxPostUpgradeInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxupgrade.WithHelmClient(helmMock), ) require.NoError(t, err) @@ -317,16 +320,17 @@ func TestLinuxPostUpgradeInfra(t *testing.T) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) cfg := types.APIConfig{ - Password: password, - PasswordHash: passwordHash, - ReleaseData: integration.DefaultReleaseData(), - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, + ReleaseData: integration.DefaultReleaseData(), + Mode: types.ModeUpgrade, } apiInstance, err := api.New(cfg, api.WithLinuxUpgradeController(upgradeController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -396,6 +400,7 @@ func TestLinuxPostUpgradeInfra(t *testing.T) { }, AppConfig: &appConfig, }), + linuxinfra.WithHelmClient(helmMock), ) // Create an upgrade controller @@ -415,6 +420,7 @@ func TestLinuxPostUpgradeInfra(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxupgrade.WithHelmClient(helmMock), ) require.NoError(t, err) @@ -423,16 +429,17 @@ func TestLinuxPostUpgradeInfra(t *testing.T) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) cfg := types.APIConfig{ - Password: password, - PasswordHash: passwordHash, - ReleaseData: integration.DefaultReleaseData(), - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, + ReleaseData: integration.DefaultReleaseData(), + Mode: types.ModeUpgrade, } apiInstance, err := api.New(cfg, api.WithLinuxUpgradeController(upgradeController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -522,6 +529,7 @@ func TestLinuxUpgradeProcessAirgap(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxupgrade.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -530,16 +538,17 @@ func TestLinuxUpgradeProcessAirgap(t *testing.T) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) cfg := types.APIConfig{ - Password: password, - PasswordHash: passwordHash, - ReleaseData: integration.DefaultReleaseData(), - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, + ReleaseData: integration.DefaultReleaseData(), + Mode: types.ModeUpgrade, } apiInstance, err := api.New(cfg, api.WithLinuxUpgradeController(upgradeController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -616,6 +625,7 @@ func TestLinuxUpgradeProcessAirgap(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxupgrade.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -624,16 +634,17 @@ func TestLinuxUpgradeProcessAirgap(t *testing.T) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) cfg := types.APIConfig{ - Password: password, - PasswordHash: passwordHash, - ReleaseData: integration.DefaultReleaseData(), - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, + ReleaseData: integration.DefaultReleaseData(), + Mode: types.ModeUpgrade, } apiInstance, err := api.New(cfg, api.WithLinuxUpgradeController(upgradeController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -672,6 +683,7 @@ func TestLinuxUpgradeProcessAirgap(t *testing.T) { ChannelRelease: &release.ChannelRelease{}, AppConfig: &appConfig, }), + linuxupgrade.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -680,16 +692,17 @@ func TestLinuxUpgradeProcessAirgap(t *testing.T) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) cfg := types.APIConfig{ - Password: password, - PasswordHash: passwordHash, - ReleaseData: integration.DefaultReleaseData(), - Mode: types.ModeUpgrade, - Target: types.TargetLinux, + InstallTarget: types.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, + ReleaseData: integration.DefaultReleaseData(), + Mode: types.ModeUpgrade, } apiInstance, err := api.New(cfg, api.WithLinuxUpgradeController(upgradeController), api.WithAuthController(auth.NewStaticAuthController("TOKEN")), api.WithLogger(logger.NewDiscardLogger()), + api.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) diff --git a/api/integration/util.go b/api/integration/util.go index ed1fd41365..5b0d39cace 100644 --- a/api/integration/util.go +++ b/api/integration/util.go @@ -8,6 +8,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/stretchr/testify/require" @@ -58,18 +59,48 @@ func NewTestInterceptorFuncs() interceptor.Funcs { } } -func NewAPIWithReleaseData(t *testing.T, mode types.Mode, target types.Target, opts ...api.Option) *api.API { +func NewTargetLinuxAPIWithReleaseData(t *testing.T, mode types.Mode, opts ...api.Option) *api.API { password := "password" passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) + cfg := types.APIConfig{ - Password: password, - PasswordHash: passwordHash, - ReleaseData: DefaultReleaseData(), - Mode: mode, - Target: target, + InstallTarget: types.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, + ReleaseData: DefaultReleaseData(), + Mode: mode, } - a, err := api.New(cfg, opts...) + + // Add default options + optsWithDefaults := append([]api.Option{ + api.WithHelmClient(&helm.MockClient{}), + }, opts...) + + a, err := api.New(cfg, optsWithDefaults...) + require.NoError(t, err) + return a +} + +func NewTargetKubernetesAPIWithReleaseData(t *testing.T, mode types.Mode, opts ...api.Option) *api.API { + password := "password" + passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) + require.NoError(t, err) + + cfg := types.APIConfig{ + InstallTarget: types.InstallTargetKubernetes, + Password: password, + PasswordHash: passwordHash, + ReleaseData: DefaultReleaseData(), + Mode: mode, + } + + // Add default options + optsWithDefaults := append([]api.Option{ + api.WithHelmClient(&helm.MockClient{}), + }, opts...) + + a, err := api.New(cfg, optsWithDefaults...) require.NoError(t, err) return a } diff --git a/api/internal/clients/kube.go b/api/internal/clients/kube.go index f591fe291a..b690cb3657 100644 --- a/api/internal/clients/kube.go +++ b/api/internal/clients/kube.go @@ -11,6 +11,7 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/discovery" coreclientset "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/metadata" "k8s.io/client-go/rest" @@ -44,48 +45,47 @@ func getScheme() *runtime.Scheme { // NewKubeClient returns a new kubernetes client. func NewKubeClient(opts KubeClientOptions) (client.Client, error) { - var restConfig *rest.Config - if opts.RESTClientGetter == nil && opts.KubeConfigPath == "" { - return nil, fmt.Errorf("a valid kube config is required to create a kube client") + restConfig, err := getRESTConfig(opts) + if err != nil { + return nil, err } - - if opts.RESTClientGetter == nil { - conf, err := clientcmd.BuildConfigFromFlags("", opts.KubeConfigPath) - if err != nil { - return nil, fmt.Errorf("unable to process kubernetes config for kube client: %w", err) - } - restConfig = conf - } else { - conf, err := opts.RESTClientGetter.ToRESTConfig() - if err != nil { - return nil, fmt.Errorf("unable to process rest client config for kube client: %w", err) - } - restConfig = conf - } - return client.New(restConfig, client.Options{Scheme: getScheme()}) } // NewMetadataClient returns a new kube metadata client. func NewMetadataClient(opts KubeClientOptions) (metadata.Interface, error) { - var restConfig *rest.Config - if opts.RESTClientGetter == nil && opts.KubeConfigPath == "" { - return nil, fmt.Errorf("a valid kube config is required to create a kube client") + restConfig, err := getRESTConfig(opts) + if err != nil { + return nil, err } + return metadata.NewForConfig(restConfig) +} - if opts.RESTClientGetter == nil { - conf, err := clientcmd.BuildConfigFromFlags("", opts.KubeConfigPath) +// NewDiscoveryClient returns a new kube discovery client. +func NewDiscoveryClient(opts KubeClientOptions) (discovery.DiscoveryInterface, error) { + restConfig, err := getRESTConfig(opts) + if err != nil { + return nil, err + } + return discovery.NewDiscoveryClientForConfig(restConfig) +} + +func getRESTConfig(opts KubeClientOptions) (*rest.Config, error) { + if opts.RESTClientGetter != nil { + conf, err := opts.RESTClientGetter.ToRESTConfig() if err != nil { - return nil, fmt.Errorf("unable to process kubernetes config for kube client: %w", err) + return nil, fmt.Errorf("invalid rest client getter: %w", err) } - restConfig = conf - } else { - conf, err := opts.RESTClientGetter.ToRESTConfig() + return conf, nil + } + + if opts.KubeConfigPath != "" { + conf, err := clientcmd.BuildConfigFromFlags("", opts.KubeConfigPath) if err != nil { - return nil, fmt.Errorf("unable to process rest client config for kube client: %w", err) + return nil, fmt.Errorf("invalid kubeconfig path: %w", err) } - restConfig = conf + return conf, nil } - return metadata.NewForConfig(restConfig) + return nil, fmt.Errorf("a valid kube config is required to create a kube client") } diff --git a/api/internal/clients/kube_test.go b/api/internal/clients/kube_test.go index 057e86578f..3dfa2f9ef7 100644 --- a/api/internal/clients/kube_test.go +++ b/api/internal/clients/kube_test.go @@ -117,7 +117,7 @@ func TestNewKubeClient(t *testing.T) { } }, expectedError: true, - errorContains: "unable to process kubernetes config for kube client", + errorContains: "invalid kubeconfig path", }, { name: "error with invalid kubeconfig content", @@ -128,7 +128,7 @@ func TestNewKubeClient(t *testing.T) { } }, expectedError: true, - errorContains: "unable to process kubernetes config for kube client", + errorContains: "invalid kubeconfig path", }, { name: "error with RESTClientGetter returning error", @@ -140,7 +140,7 @@ func TestNewKubeClient(t *testing.T) { } }, expectedError: true, - errorContains: "unable to process rest client config for kube client", + errorContains: "invalid rest client getter", }, } @@ -214,7 +214,7 @@ func TestNewMetadataClient(t *testing.T) { } }, expectedError: true, - errorContains: "unable to process kubernetes config for kube client", + errorContains: "invalid kubeconfig path", }, { name: "error with invalid kubeconfig content", @@ -225,7 +225,7 @@ func TestNewMetadataClient(t *testing.T) { } }, expectedError: true, - errorContains: "unable to process kubernetes config for kube client", + errorContains: "invalid kubeconfig path", }, { name: "error with RESTClientGetter returning error", @@ -237,7 +237,7 @@ func TestNewMetadataClient(t *testing.T) { } }, expectedError: true, - errorContains: "unable to process rest client config for kube client", + errorContains: "invalid rest client getter", }, } diff --git a/api/internal/handlers/kubernetes/kubernetes.go b/api/internal/handlers/kubernetes/kubernetes.go index 946e17ca5c..9cd7acc74f 100644 --- a/api/internal/handlers/kubernetes/kubernetes.go +++ b/api/internal/handlers/kubernetes/kubernetes.go @@ -9,6 +9,7 @@ import ( k8supgrade "github.com/replicatedhq/embedded-cluster/api/internal/handlers/kubernetes/upgrade" "github.com/replicatedhq/embedded-cluster/api/pkg/logger" "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/sirupsen/logrus" ) @@ -22,6 +23,7 @@ type Handler struct { upgradeController upgrade.Controller logger logrus.FieldLogger metricsReporter metrics.ReporterInterface + hcli helm.Client } type Option func(*Handler) @@ -50,6 +52,12 @@ func WithMetricsReporter(metricsReporter metrics.ReporterInterface) Option { } } +func WithHelmClient(hcli helm.Client) Option { + return func(h *Handler) { + h.hcli = hcli + } +} + func New(cfg types.APIConfig, opts ...Option) (*Handler, error) { h := &Handler{ cfg: cfg, @@ -69,13 +77,14 @@ func New(cfg types.APIConfig, opts ...Option) (*Handler, error) { installController, err := install.NewInstallController( install.WithLogger(h.logger), install.WithMetricsReporter(h.metricsReporter), - install.WithRESTClientGetter(h.cfg.RESTClientGetter), + install.WithKubernetesEnvSettings(h.cfg.Installation.GetKubernetesEnvSettings()), install.WithReleaseData(h.cfg.ReleaseData), install.WithConfigValues(h.cfg.ConfigValues), install.WithEndUserConfig(h.cfg.EndUserConfig), install.WithPassword(h.cfg.Password), //nolint:staticcheck // QF1008 this is very ambiguous, we should re-think the config struct install.WithInstallation(h.cfg.KubernetesConfig.Installation), + install.WithHelmClient(h.hcli), ) if err != nil { return nil, fmt.Errorf("new install controller: %w", err) @@ -95,11 +104,12 @@ func New(cfg types.APIConfig, opts ...Option) (*Handler, error) { if h.upgradeController == nil { upgradeController, err := upgrade.NewUpgradeController( upgrade.WithLogger(h.logger), - upgrade.WithRESTClientGetter(h.cfg.RESTClientGetter), + upgrade.WithKubernetesEnvSettings(h.cfg.Installation.GetKubernetesEnvSettings()), upgrade.WithReleaseData(h.cfg.ReleaseData), upgrade.WithLicense(h.cfg.License), upgrade.WithAirgapBundle(h.cfg.AirgapBundle), upgrade.WithConfigValues(h.cfg.ConfigValues), + upgrade.WithHelmClient(h.hcli), ) if err != nil { return nil, fmt.Errorf("new upgrade controller: %w", err) diff --git a/api/internal/handlers/linux/linux.go b/api/internal/handlers/linux/linux.go index 57c09867ed..48ee135085 100644 --- a/api/internal/handlers/linux/linux.go +++ b/api/internal/handlers/linux/linux.go @@ -10,6 +10,7 @@ import ( "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/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/sirupsen/logrus" ) @@ -24,6 +25,7 @@ type Handler struct { logger logrus.FieldLogger hostUtils hostutils.HostUtilsInterface metricsReporter metrics.ReporterInterface + hcli helm.Client } type Option func(*Handler) @@ -58,6 +60,12 @@ func WithMetricsReporter(metricsReporter metrics.ReporterInterface) Option { } } +func WithHelmClient(hcli helm.Client) Option { + return func(h *Handler) { + h.hcli = hcli + } +} + func New(cfg types.APIConfig, opts ...Option) (*Handler, error) { h := &Handler{ cfg: cfg, @@ -96,6 +104,7 @@ func New(cfg types.APIConfig, opts ...Option) (*Handler, error) { install.WithEndUserConfig(h.cfg.EndUserConfig), install.WithClusterID(h.cfg.ClusterID), install.WithAllowIgnoreHostPreflights(h.cfg.AllowIgnoreHostPreflights), + install.WithHelmClient(h.hcli), ) if err != nil { return nil, fmt.Errorf("new install controller: %w", err) @@ -130,6 +139,7 @@ func New(cfg types.APIConfig, opts ...Option) (*Handler, error) { upgrade.WithTargetVersion(h.cfg.TargetVersion), upgrade.WithInitialVersion(h.cfg.InitialVersion), upgrade.WithInfraUpgradeRequired(h.cfg.RequiresInfraUpgrade), + upgrade.WithHelmClient(h.hcli), ) if err != nil { return nil, fmt.Errorf("new upgrade controller: %w", err) diff --git a/api/internal/managers/app/release/manager.go b/api/internal/managers/app/release/manager.go index ef394296b3..2041633e7a 100644 --- a/api/internal/managers/app/release/manager.go +++ b/api/internal/managers/app/release/manager.go @@ -8,6 +8,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/pkg/template" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" @@ -27,6 +28,7 @@ type appReleaseManager struct { privateCACertConfigMapName string templateEngine *template.Engine + hcli helm.Client logger logrus.FieldLogger } @@ -50,6 +52,12 @@ func WithTemplateEngine(templateEngine *template.Engine) AppReleaseManagerOption } } +func WithHelmClient(hcli helm.Client) AppReleaseManagerOption { + return func(m *appReleaseManager) { + m.hcli = hcli + } +} + func WithLicense(license *kotsv1beta1.License) AppReleaseManagerOption { return func(m *appReleaseManager) { m.license = license @@ -82,6 +90,10 @@ func NewAppReleaseManager(config kotsv1beta1.Config, opts ...AppReleaseManagerOp return nil, fmt.Errorf("release data not found") } + if manager.hcli == nil { + return nil, fmt.Errorf("helm client is required") + } + if manager.logger == nil { manager.logger = logger.NewDiscardLogger() } diff --git a/api/internal/managers/app/release/template_test.go b/api/internal/managers/app/release/template_test.go index 73b5f38471..8deda941b4 100644 --- a/api/internal/managers/app/release/template_test.go +++ b/api/internal/managers/app/release/template_test.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" kotsv1beta2 "github.com/replicatedhq/kotskinds/apis/kots/v1beta2" @@ -328,17 +329,27 @@ spec: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Create a basic config for the template engine + config := createTestConfig() + // Create release data releaseData := &release.ReleaseData{ HelmChartCRs: tt.helmChartCRs, HelmChartArchives: tt.chartArchives, } + // Create real helm client + hcli, err := helm.NewClient(helm.HelmOptions{ + HelmPath: "helm", // use the helm binary in PATH + K8sVersion: "v1.33.0", + }) + require.NoError(t, err) + // Create manager - config := createTestConfig() manager, err := NewAppReleaseManager( config, WithReleaseData(releaseData), + WithHelmClient(hcli), ) require.NoError(t, err) @@ -997,11 +1008,19 @@ spec: HelmChartCRs: tt.helmChartCRs, } + // Create real helm client + hcli, err := helm.NewClient(helm.HelmOptions{ + HelmPath: "helm", // use the helm binary in PATH + K8sVersion: "v1.33.0", + }) + require.NoError(t, err) + // Create manager manager, err := NewAppReleaseManager( config, WithReleaseData(releaseData), WithIsAirgap(tt.isAirgap), + WithHelmClient(hcli), ) require.NoError(t, err) @@ -1274,9 +1293,18 @@ spec: releaseData := &release.ReleaseData{ HelmChartArchives: tt.helmChartArchives, } + + // Create real helm client + hcli, err := helm.NewClient(helm.HelmOptions{ + HelmPath: "helm", // use the helm binary in PATH + K8sVersion: "v1.33.0", + }) + require.NoError(t, err) + manager, err := NewAppReleaseManager( config, WithReleaseData(releaseData), + WithHelmClient(hcli), ) require.NoError(t, err) diff --git a/api/internal/managers/kubernetes/infra/manager.go b/api/internal/managers/kubernetes/infra/manager.go index 9177b64134..b420f455b4 100644 --- a/api/internal/managers/kubernetes/infra/manager.go +++ b/api/internal/managers/kubernetes/infra/manager.go @@ -14,6 +14,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/sirupsen/logrus" + helmcli "helm.sh/helm/v3/pkg/cli" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/metadata" "sigs.k8s.io/controller-runtime/pkg/client" @@ -29,19 +30,19 @@ type InfraManager interface { // infraManager is an implementation of the InfraManager interface type infraManager struct { - infraStore infrastore.Store - password string - tlsConfig types.TLSConfig - license []byte - airgapBundle string - releaseData *release.ReleaseData - endUserConfig *ecv1beta1.Config - logger logrus.FieldLogger - kcli client.Client - mcli metadata.Interface - hcli helm.Client - restClientGetter genericclioptions.RESTClientGetter - mu sync.RWMutex + infraStore infrastore.Store + password string + tlsConfig types.TLSConfig + license []byte + airgapBundle string + releaseData *release.ReleaseData + endUserConfig *ecv1beta1.Config + logger logrus.FieldLogger + kcli client.Client + mcli metadata.Interface + hcli helm.Client + kubernetesEnvSettings *helmcli.EnvSettings + mu sync.RWMutex } type InfraManagerOption func(*infraManager) @@ -112,9 +113,9 @@ func WithHelmClient(hcli helm.Client) InfraManagerOption { } } -func WithRESTClientGetter(restClientGetter genericclioptions.RESTClientGetter) InfraManagerOption { +func WithKubernetesEnvSettings(kubernetesEnvSettings *helmcli.EnvSettings) InfraManagerOption { return func(c *infraManager) { - c.restClientGetter = restClientGetter + c.kubernetesEnvSettings = kubernetesEnvSettings } } @@ -134,8 +135,18 @@ func NewInfraManager(opts ...InfraManagerOption) (*infraManager, error) { manager.infraStore = infrastore.NewMemoryStore() } + // If none is provided, use the default env settings from helm + if manager.kubernetesEnvSettings == nil { + manager.kubernetesEnvSettings = helmcli.New() + } + + var restClientGetter genericclioptions.RESTClientGetter + if manager.kubernetesEnvSettings != nil { + restClientGetter = manager.kubernetesEnvSettings.RESTClientGetter() + } + if manager.kcli == nil { - kcli, err := clients.NewKubeClient(clients.KubeClientOptions{RESTClientGetter: manager.restClientGetter}) + kcli, err := clients.NewKubeClient(clients.KubeClientOptions{RESTClientGetter: restClientGetter}) if err != nil { return nil, fmt.Errorf("create kube client: %w", err) } @@ -143,7 +154,7 @@ func NewInfraManager(opts ...InfraManagerOption) (*infraManager, error) { } if manager.mcli == nil { - mcli, err := clients.NewMetadataClient(clients.KubeClientOptions{RESTClientGetter: manager.restClientGetter}) + mcli, err := clients.NewMetadataClient(clients.KubeClientOptions{RESTClientGetter: restClientGetter}) if err != nil { return nil, fmt.Errorf("create metadata client: %w", err) } @@ -151,16 +162,7 @@ func NewInfraManager(opts ...InfraManagerOption) (*infraManager, error) { } if manager.hcli == nil { - hcli, err := helm.NewClient(helm.HelmOptions{ - RESTClientGetter: manager.restClientGetter, - // TODO: how can we support airgap? - AirgapPath: "", - LogFn: manager.logFn("helm"), - }) - if err != nil { - return nil, fmt.Errorf("create helm client: %w", err) - } - manager.hcli = hcli + return nil, fmt.Errorf("helm client is required") } return manager, nil diff --git a/api/internal/managers/kubernetes/infra/manager_test.go b/api/internal/managers/kubernetes/infra/manager_test.go index d708ca32d0..a858e126b7 100644 --- a/api/internal/managers/kubernetes/infra/manager_test.go +++ b/api/internal/managers/kubernetes/infra/manager_test.go @@ -3,12 +3,11 @@ package infra import ( "testing" - "github.com/replicatedhq/embedded-cluster/api/internal/clients" "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + helmcli "helm.sh/helm/v3/pkg/cli" metadatafake "k8s.io/client-go/metadata/fake" - "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/scheme" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -16,81 +15,34 @@ import ( func TestNewInfraManager_ClientCreation(t *testing.T) { tests := []struct { name string - setupMock func(*clients.MockRESTClientGetter) withKubeClient bool withMetadataClient bool withHelmClient bool expectError bool }{ { - name: "creates all clients when none provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // kube client and metadata client creation - mock.On("ToRESTConfig").Return(&rest.Config{}, nil).Times(2) - }, - expectError: false, + name: "fails when helm client not provided", + expectError: true, }, { - name: "creates kube and metadata clients when helm client provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // kube client and metadata client creation - mock.On("ToRESTConfig").Return(&rest.Config{}, nil).Times(2) - }, + name: "creates kube and metadata clients when only helm client provided", withHelmClient: true, expectError: false, }, { - name: "creates kube and helm clients when metadata client provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // kube client creation - mock.On("ToRESTConfig").Return(&rest.Config{}, nil).Times(1) - }, - withMetadataClient: true, - expectError: false, - }, - { - name: "creates metadata and helm clients when kube client provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // metadata client creation - mock.On("ToRESTConfig").Return(&rest.Config{}, nil).Times(1) - }, - withKubeClient: true, - expectError: false, - }, - { - name: "creates only helm client when kube and metadata clients provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // No ToRESTConfig calls expected - }, - withKubeClient: true, - withMetadataClient: true, - expectError: false, - }, - { - name: "creates only metadata client when kube and helm clients provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // metadata client creation - mock.On("ToRESTConfig").Return(&rest.Config{}, nil).Times(1) - }, + name: "creates metadata client when kube and helm clients provided", withKubeClient: true, withHelmClient: true, expectError: false, }, { - name: "creates only kube client when metadata and helm clients provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // kube client creation - mock.On("ToRESTConfig").Return(&rest.Config{}, nil).Times(1) - }, + name: "creates kube client when metadata and helm clients provided", withMetadataClient: true, withHelmClient: true, expectError: false, }, { - name: "creates no clients when all provided", - setupMock: func(mock *clients.MockRESTClientGetter) { - // No ToRESTConfig calls expected - }, + name: "uses all provided clients when all are given", withKubeClient: true, withMetadataClient: true, withHelmClient: true, @@ -100,13 +52,9 @@ func TestNewInfraManager_ClientCreation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create mock RESTClientGetter - mockRestClientGetter := &clients.MockRESTClientGetter{} - tt.setupMock(mockRestClientGetter) - // Build options opts := []InfraManagerOption{ - WithRESTClientGetter(mockRestClientGetter), + WithKubernetesEnvSettings(helmcli.New()), } // Add pre-created clients if specified @@ -117,7 +65,13 @@ func TestNewInfraManager_ClientCreation(t *testing.T) { opts = append(opts, WithMetadataClient(metadatafake.NewSimpleMetadataClient(scheme.Scheme))) } if tt.withHelmClient { - opts = append(opts, WithHelmClient(&helm.MockClient{})) + // Create real helm client + hcli, err := helm.NewClient(helm.HelmOptions{ + HelmPath: "helm", + K8sVersion: "v1.26.0", + }) + require.NoError(t, err) + opts = append(opts, WithHelmClient(hcli)) } // Create manager @@ -133,88 +87,6 @@ func TestNewInfraManager_ClientCreation(t *testing.T) { assert.NotNil(t, manager.kcli) assert.NotNil(t, manager.mcli) assert.NotNil(t, manager.hcli) - - // Verify mock expectations - mockRestClientGetter.AssertExpectations(t) - }) - } -} - -func TestNewInfraManager_ToRESTConfigError(t *testing.T) { - tests := []struct { - name string - withKubeClient bool - withMetadataClient bool - withHelmClient bool - expectedError string - }{ - { - name: "kube client creation fails", - withMetadataClient: true, - expectedError: "create kube client:", - }, - { - name: "metadata client creation fails", - withKubeClient: true, - expectedError: "create metadata client:", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create mock RESTClientGetter that returns error - mockRestClientGetter := &clients.MockRESTClientGetter{} - mockRestClientGetter.On("ToRESTConfig").Return((*rest.Config)(nil), assert.AnError) - - // Build options - opts := []InfraManagerOption{ - WithRESTClientGetter(mockRestClientGetter), - } - - // Add pre-created clients if specified - if tt.withKubeClient { - opts = append(opts, WithKubeClient(fake.NewFakeClient())) - } - if tt.withMetadataClient { - opts = append(opts, WithMetadataClient(metadatafake.NewSimpleMetadataClient(scheme.Scheme))) - } - opts = append(opts, WithHelmClient(&helm.MockClient{})) - - // Create manager - manager, err := NewInfraManager(opts...) - - require.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, manager) - - // Verify mock expectations - mockRestClientGetter.AssertExpectations(t) }) } } - -func TestNewInfraManager_WithoutRESTClientGetter(t *testing.T) { - // Test that creating manager without RESTClientGetter fails when clients need to be created - manager, err := NewInfraManager() - - require.Error(t, err) - assert.Contains(t, err.Error(), "a valid kube config is required to create a kube client") - assert.Nil(t, manager) -} - -func TestNewInfraManager_WithAllClientsProvided(t *testing.T) { - // Test that when all clients are provided, no RESTClientGetter is needed - opts := []InfraManagerOption{ - WithKubeClient(fake.NewFakeClient()), - WithMetadataClient(metadatafake.NewSimpleMetadataClient(scheme.Scheme)), - WithHelmClient(&helm.MockClient{}), - } - - manager, err := NewInfraManager(opts...) - - require.NoError(t, err) - assert.NotNil(t, manager) - assert.NotNil(t, manager.kcli) - assert.NotNil(t, manager.mcli) - assert.NotNil(t, manager.hcli) -} diff --git a/api/internal/managers/kubernetes/infra/status_test.go b/api/internal/managers/kubernetes/infra/status_test.go index e666733d3a..e3eb8f8537 100644 --- a/api/internal/managers/kubernetes/infra/status_test.go +++ b/api/internal/managers/kubernetes/infra/status_test.go @@ -3,6 +3,7 @@ package infra import ( "testing" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metadatafake "k8s.io/client-go/metadata/fake" @@ -11,7 +12,7 @@ import ( ) func TestInfraWithLogs(t *testing.T) { - manager, err := NewInfraManager(WithKubeClient(fake.NewFakeClient()), WithMetadataClient(metadatafake.NewSimpleMetadataClient(scheme.Scheme))) + manager, err := NewInfraManager(WithKubeClient(fake.NewFakeClient()), WithMetadataClient(metadatafake.NewSimpleMetadataClient(scheme.Scheme)), WithHelmClient(&helm.MockClient{})) require.NoError(t, err) // Add some logs through the internal logging mechanism diff --git a/api/internal/managers/linux/infra/install.go b/api/internal/managers/linux/infra/install.go index 8886d24572..c83af68246 100644 --- a/api/internal/managers/linux/infra/install.go +++ b/api/internal/managers/linux/infra/install.go @@ -183,7 +183,7 @@ func (m *infraManager) installK0s(ctx context.Context, rc runtimeconfig.RuntimeC } // initialize the manager's helm and kube clients - err = m.setupClients(rc.PathToKubeConfig(), rc.EmbeddedClusterChartsSubDir()) + err = m.setupClients(rc) if err != nil { return nil, fmt.Errorf("setup clients: %w", err) } diff --git a/api/internal/managers/linux/infra/upgrade.go b/api/internal/managers/linux/infra/upgrade.go index 832ce89ece..b39d9ad633 100644 --- a/api/internal/managers/linux/infra/upgrade.go +++ b/api/internal/managers/linux/infra/upgrade.go @@ -56,7 +56,7 @@ func (m *infraManager) Upgrade(ctx context.Context, rc runtimeconfig.RuntimeConf func (m *infraManager) upgrade(ctx context.Context, rc runtimeconfig.RuntimeConfig, registrySettings *types.RegistrySettings) error { if m.upgrader == nil { // ensure the manager's clients are initialized - if err := m.setupClients(rc.PathToKubeConfig(), rc.EmbeddedClusterChartsSubDir()); err != nil { + if err := m.setupClients(rc); err != nil { return fmt.Errorf("setup clients: %w", err) } diff --git a/api/internal/managers/linux/infra/util.go b/api/internal/managers/linux/infra/util.go index 9565cc9c08..bcc78d30e7 100644 --- a/api/internal/managers/linux/infra/util.go +++ b/api/internal/managers/linux/infra/util.go @@ -8,9 +8,9 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/clients" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" - "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/kubeutils" - "github.com/replicatedhq/embedded-cluster/pkg/versions" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" + "k8s.io/cli-runtime/pkg/genericclioptions" nodeutil "k8s.io/component-helpers/node/util" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -49,9 +49,14 @@ func (m *infraManager) waitForNode(ctx context.Context, kcli client.Client) erro // setupClients initializes the kube, metadata, and helm clients if they are not already set. // We need to do it after the infra manager is initialized to ensure that the runtime config is available and we already have a cluster setup -func (m *infraManager) setupClients(kubeConfigPath string, airgapChartsPath string) error { +func (m *infraManager) setupClients(rc runtimeconfig.RuntimeConfig) error { + var restClientGetter genericclioptions.RESTClientGetter + if rc.GetKubernetesEnvSettings() != nil { + restClientGetter = rc.GetKubernetesEnvSettings().RESTClientGetter() + } + if m.kcli == nil { - kcli, err := clients.NewKubeClient(clients.KubeClientOptions{KubeConfigPath: kubeConfigPath}) + kcli, err := clients.NewKubeClient(clients.KubeClientOptions{RESTClientGetter: restClientGetter}) if err != nil { return fmt.Errorf("create kube client: %w", err) } @@ -59,7 +64,7 @@ func (m *infraManager) setupClients(kubeConfigPath string, airgapChartsPath stri } if m.mcli == nil { - mcli, err := clients.NewMetadataClient(clients.KubeClientOptions{KubeConfigPath: kubeConfigPath}) + mcli, err := clients.NewMetadataClient(clients.KubeClientOptions{RESTClientGetter: restClientGetter}) if err != nil { return fmt.Errorf("create metadata client: %w", err) } @@ -67,20 +72,7 @@ func (m *infraManager) setupClients(kubeConfigPath string, airgapChartsPath stri } if m.hcli == nil { - airgapPath := "" - if m.airgapBundle != "" { - airgapPath = airgapChartsPath - } - hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: kubeConfigPath, - K0sVersion: versions.K0sVersion, - AirgapPath: airgapPath, - LogFn: m.logFn("helm"), - }) - if err != nil { - return fmt.Errorf("create helm client: %w", err) - } - m.hcli = hcli + return fmt.Errorf("helm client is required") } return nil diff --git a/api/pkg/template/static.go b/api/pkg/template/static.go index 0f98ba67fa..e0ccbd2b51 100644 --- a/api/pkg/template/static.go +++ b/api/pkg/template/static.go @@ -285,12 +285,13 @@ func (e *Engine) yamlEscape(plain string) string { return indented } -func (e *Engine) distribution() string { - return "embedded-cluster" -} - // copied from sprig func indent(spaces int, v string) string { pad := strings.Repeat(" ", spaces) return pad + strings.ReplaceAll(v, "\n", "\n"+pad) } + +func (e *Engine) distribution() string { + // TODO: support for kubernetes target + return "embedded-cluster" +} diff --git a/api/routes.go b/api/routes.go index 1997ef6924..62d1410441 100644 --- a/api/routes.go +++ b/api/routes.go @@ -28,7 +28,7 @@ func (a *API) RegisterRoutes(router *mux.Router) { authenticatedRouter := router.PathPrefix("/").Subrouter() authenticatedRouter.Use(a.handlers.auth.Middleware) - if a.cfg.Target == types.TargetLinux { + if a.cfg.InstallTarget == types.InstallTargetLinux { a.registerLinuxRoutes(authenticatedRouter) } else { a.registerKubernetesRoutes(authenticatedRouter) diff --git a/api/types/api.go b/api/types/api.go index 73120bd385..a55bb536dc 100644 --- a/api/types/api.go +++ b/api/types/api.go @@ -6,11 +6,11 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/airgap" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" - "k8s.io/cli-runtime/pkg/genericclioptions" ) // APIConfig holds the configuration for the API server type APIConfig struct { + InstallTarget InstallTarget Password string // Used by AdminConsole addon to create kotsadm-password secret and infrastructure operations in V2 PasswordHash []byte // Used by auth controller for API authentication in V3 TLSConfig TLSConfig @@ -22,7 +22,6 @@ type APIConfig struct { ReleaseData *release.ReleaseData EndUserConfig *ecv1beta1.Config ClusterID string - Target Target Mode Mode TargetVersion string // Used for upgrade metrics reporting InitialVersion string // Used for upgrade metrics reporting @@ -32,11 +31,11 @@ type APIConfig struct { KubernetesConfig } -type Target string +type InstallTarget string const ( - TargetLinux Target = "linux" - TargetKubernetes Target = "kubernetes" + InstallTargetLinux InstallTarget = "linux" + InstallTargetKubernetes InstallTarget = "kubernetes" ) type Mode string @@ -52,6 +51,5 @@ type LinuxConfig struct { } type KubernetesConfig struct { - RESTClientGetter genericclioptions.RESTClientGetter - Installation kubernetesinstallation.Installation + Installation kubernetesinstallation.Installation } diff --git a/cmd/buildtools/metadata.go b/cmd/buildtools/metadata.go index 8dfbe611c2..e3752c7163 100644 --- a/cmd/buildtools/metadata.go +++ b/cmd/buildtools/metadata.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "log" @@ -44,14 +45,15 @@ var metadataExtractHelmChartImagesCommand = &cli.Command{ charts := metadata.Configs.Charts hcli, err := helm.NewClient(helm.HelmOptions{ - K0sVersion: metadata.Versions["Kubernetes"], + HelmPath: "helm", // use the helm binary in PATH + K8sVersion: metadata.Versions["Kubernetes"], }) if err != nil { return fmt.Errorf("failed to create helm client: %w", err) } defer hcli.Close() - images, err := extractImagesFromHelmExtensions(hcli, repos, charts) + images, err := extractImagesFromHelmExtensions(c.Context, hcli, repos, charts) if err != nil { return fmt.Errorf("failed to extract images from helm extensions: %w", err) } @@ -79,7 +81,7 @@ func readMetadataFromFile(path string) (*types.ReleaseMetadata, error) { return &metadata, nil } -func extractImagesFromHelmExtensions(hcli helm.Client, repos []k0sv1beta1.Repository, charts []embeddedclusterv1beta1.Chart) ([]string, error) { +func extractImagesFromHelmExtensions(ctx context.Context, hcli helm.Client, repos []k0sv1beta1.Repository, charts []embeddedclusterv1beta1.Chart) ([]string, error) { for _, entry := range repos { log.Printf("Adding helm repository %s", entry.Name) repo := &repo.Entry{ @@ -94,7 +96,7 @@ func extractImagesFromHelmExtensions(hcli helm.Client, repos []k0sv1beta1.Reposi if entry.Insecure != nil { repo.InsecureSkipTLSverify = *entry.Insecure } - err := hcli.AddRepo(repo) + err := hcli.AddRepo(ctx, repo) if err != nil { return nil, fmt.Errorf("add helm repository %s: %w", entry.Name, err) } diff --git a/cmd/buildtools/openebs.go b/cmd/buildtools/openebs.go index ebaf4ccb25..0a9354e59c 100644 --- a/cmd/buildtools/openebs.go +++ b/cmd/buildtools/openebs.go @@ -76,7 +76,7 @@ var updateOpenEBSAddonCommand = &cli.Command{ logrus.Infof("using input override from INPUT_OPENEBS_CHART_VERSION: %s", nextChartVersion) } else { logrus.Infof("fetching the latest openebs chart version") - latest, err := LatestChartVersion(hcli, openebsRepo, "openebs") + latest, err := LatestChartVersion(c.Context, hcli, openebsRepo, "openebs") if err != nil { return fmt.Errorf("failed to get the latest openebs chart version: %v", err) } @@ -92,7 +92,7 @@ var updateOpenEBSAddonCommand = &cli.Command{ } logrus.Infof("mirroring openebs chart version %s", nextChartVersion) - if err := MirrorChart(hcli, openebsRepo, "openebs", nextChartVersion); err != nil { + if err := MirrorChart(c.Context, hcli, openebsRepo, "openebs", nextChartVersion); err != nil { return fmt.Errorf("failed to mirror openebs chart: %v", err) } @@ -100,7 +100,7 @@ var updateOpenEBSAddonCommand = &cli.Command{ upstream = addProxyAnonymousPrefix(upstream) withproto := fmt.Sprintf("oci://%s", upstream) - linuxUtilsVersion, err := findOpenEBSLinuxUtilsVersionFromChart(hcli, withproto, nextChartVersion) + linuxUtilsVersion, err := findOpenEBSLinuxUtilsVersionFromChart(c.Context, hcli, withproto, nextChartVersion) if err != nil { return fmt.Errorf("failed to find openebs linux utils version from chart: %w", err) } @@ -190,12 +190,12 @@ func updateOpenEBSAddonImages(ctx context.Context, hcli helm.Client, chartURL st var openebsLinuxUtilsRegexp = regexp.MustCompile(`openebs/linux-utils:v?[\d\.]+`) -func findOpenEBSLinuxUtilsVersionFromChart(hcli helm.Client, chartURL string, chartVersion string) (string, error) { +func findOpenEBSLinuxUtilsVersionFromChart(ctx context.Context, hcli helm.Client, chartURL string, chartVersion string) (string, error) { values, err := release.GetValuesWithOriginalImages("openebs") if err != nil { return "", fmt.Errorf("failed to get velero values: %v", err) } - images, err := helm.ExtractMatchesFromChart(hcli, chartURL, chartVersion, values, openebsLinuxUtilsRegexp) + images, err := helm.ExtractMatchesFromChart(ctx, hcli, chartURL, chartVersion, values, openebsLinuxUtilsRegexp) if err != nil { return "", fmt.Errorf("failed to get images from openebs chart: %w", err) } diff --git a/cmd/buildtools/registry.go b/cmd/buildtools/registry.go index bedc6f48fc..c0d5ed5027 100644 --- a/cmd/buildtools/registry.go +++ b/cmd/buildtools/registry.go @@ -52,7 +52,7 @@ var updateRegistryAddonCommand = &cli.Command{ logrus.Infof("using input override from INPUT_REGISTRY_CHART_VERSION: %s", nextChartVersion) } else { logrus.Infof("fetching the latest docker-registry chart version") - latest, err := LatestChartVersion(hcli, registryRepo, "docker-registry") + latest, err := LatestChartVersion(c.Context, hcli, registryRepo, "docker-registry") if err != nil { return fmt.Errorf("failed to get the latest docker-registry chart version: %v", err) } @@ -68,7 +68,7 @@ var updateRegistryAddonCommand = &cli.Command{ } logrus.Infof("mirroring docker-registry chart version %s", nextChartVersion) - if err := MirrorChart(hcli, registryRepo, "docker-registry", nextChartVersion); err != nil { + if err := MirrorChart(c.Context, hcli, registryRepo, "docker-registry", nextChartVersion); err != nil { return fmt.Errorf("failed to mirror docker-registry chart: %v", err) } diff --git a/cmd/buildtools/seaweedfs.go b/cmd/buildtools/seaweedfs.go index 004d4380ee..cc9f31e054 100644 --- a/cmd/buildtools/seaweedfs.go +++ b/cmd/buildtools/seaweedfs.go @@ -49,7 +49,7 @@ var updateSeaweedFSAddonCommand = &cli.Command{ logrus.Infof("using input override from INPUT_SEAWEEDFS_CHART_VERSION: %s", nextChartVersion) } else { logrus.Infof("fetching the latest seaweedfs chart version") - latest, err := LatestChartVersion(hcli, seaweedfsRepo, "seaweedfs") + latest, err := LatestChartVersion(c.Context, hcli, seaweedfsRepo, "seaweedfs") if err != nil { return fmt.Errorf("failed to get the latest seaweedfs chart version: %v", err) } @@ -65,7 +65,7 @@ var updateSeaweedFSAddonCommand = &cli.Command{ } logrus.Infof("mirroring seaweedfs chart version %s", nextChartVersion) - if err := MirrorChart(hcli, seaweedfsRepo, "seaweedfs", nextChartVersion); err != nil { + if err := MirrorChart(c.Context, hcli, seaweedfsRepo, "seaweedfs", nextChartVersion); err != nil { return fmt.Errorf("failed to mirror seaweedfs chart: %v", err) } diff --git a/cmd/buildtools/utils.go b/cmd/buildtools/utils.go index 4c1c4b9a8c..d65f8857be 100644 --- a/cmd/buildtools/utils.go +++ b/cmd/buildtools/utils.go @@ -24,6 +24,7 @@ import ( "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/sirupsen/logrus" + helmcli "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/repo" "oras.land/oras-go/v2/registry/remote" ) @@ -352,14 +353,14 @@ func GetGreatestTagFromRegistry(ctx context.Context, ref string, constraints *se return bestStr, nil } -func LatestChartVersion(hcli helm.Client, repo *repo.Entry, name string) (string, error) { +func LatestChartVersion(ctx context.Context, hcli helm.Client, repo *repo.Entry, name string) (string, error) { logrus.Infof("adding helm repo %s", repo.Name) - err := hcli.AddRepo(repo) + err := hcli.AddRepo(ctx, repo) if err != nil { return "", fmt.Errorf("add helm repo: %w", err) } logrus.Infof("finding latest chart version of %s/%s", repo, name) - return hcli.Latest(repo.Name, name) + return hcli.Latest(ctx, repo.Name, name) } type DockerManifestNotFoundError struct { @@ -463,29 +464,29 @@ func RemoveTagFromImage(image string) string { return location } -func MirrorChart(hcli helm.Client, repo *repo.Entry, name, ver string) error { +func MirrorChart(ctx context.Context, hcli helm.Client, repo *repo.Entry, name, ver string) error { logrus.Infof("adding helm repo %s", repo.Name) - err := hcli.AddRepo(repo) + err := hcli.AddRepo(ctx, repo) if err != nil { return fmt.Errorf("add helm repo: %w", err) } logrus.Infof("pulling %s chart version %s", name, ver) - chpath, err := hcli.Pull(repo.Name, name, ver) + chpath, err := hcli.Pull(ctx, repo.Name, name, ver) if err != nil { return fmt.Errorf("pull chart %s: %w", name, err) } logrus.Infof("downloaded %s chart: %s", name, chpath) defer os.Remove(chpath) - srcMeta, err := hcli.GetChartMetadata(chpath) + srcMeta, err := hcli.GetChartMetadata(ctx, chpath, ver) if err != nil { return fmt.Errorf("get source chart metadata: %w", err) } if val := os.Getenv("CHARTS_REGISTRY_SERVER"); val != "" { logrus.Infof("authenticating with %q", os.Getenv("CHARTS_REGISTRY_SERVER")) - if err := hcli.RegistryAuth( + if err := hcli.RegistryAuth(ctx, os.Getenv("CHARTS_REGISTRY_SERVER"), os.Getenv("CHARTS_REGISTRY_USER"), os.Getenv("CHARTS_REGISTRY_PASS"), @@ -497,7 +498,7 @@ func MirrorChart(hcli helm.Client, repo *repo.Entry, name, ver string) error { dst := fmt.Sprintf("oci://%s", os.Getenv("CHARTS_DESTINATION")) chartURL := fmt.Sprintf("%s/%s", dst, name) logrus.Infof("verifying if destination tag already exists") - dstMeta, err := helm.GetChartMetadata(hcli, chartURL, ver) + dstMeta, err := hcli.GetChartMetadata(ctx, chartURL, ver) if err != nil && !strings.HasSuffix(err.Error(), "not found") { return fmt.Errorf("verify tag exists: %w", err) } else if err == nil { @@ -511,7 +512,7 @@ func MirrorChart(hcli helm.Client, repo *repo.Entry, name, ver string) error { logrus.Infof("destination tag does not exist") logrus.Infof("pushing %s chart to %s", name, dst) - if err := hcli.Push(chpath, dst); err != nil { + if err := hcli.Push(ctx, chpath, dst); err != nil { return fmt.Errorf("push %s chart: %w", name, err) } remote := fmt.Sprintf("%s/%s:%s", dst, name, ver) @@ -531,8 +532,10 @@ func NewHelm() (helm.Client, error) { return nil, fmt.Errorf("get k0s version: %w", err) } return helm.NewClient(helm.HelmOptions{ - Writer: logrus.New().Writer(), - K0sVersion: sv.Original(), + HelmPath: "helm", // use the helm binary in PATH + KubernetesEnvSettings: helmcli.New(), // use the default env settings from helm + K8sVersion: sv.Original(), + Writer: logrus.New().Writer(), }) } diff --git a/cmd/buildtools/velero.go b/cmd/buildtools/velero.go index ec5bc9ef3d..804e0a09cd 100644 --- a/cmd/buildtools/velero.go +++ b/cmd/buildtools/velero.go @@ -73,7 +73,7 @@ var updateVeleroAddonCommand = &cli.Command{ logrus.Infof("using input override from INPUT_VELERO_CHART_VERSION: %s", nextChartVersion) } else { logrus.Infof("fetching the latest velero chart version") - latest, err := LatestChartVersion(hcli, veleroRepo, "velero") + latest, err := LatestChartVersion(c.Context, hcli, veleroRepo, "velero") if err != nil { return fmt.Errorf("failed to get the latest velero chart version: %v", err) } @@ -87,7 +87,7 @@ var updateVeleroAddonCommand = &cli.Command{ logrus.Infof("velero chart version is already up-to-date") } else { logrus.Infof("mirroring velero chart version %s", nextChartVersion) - if err := MirrorChart(hcli, veleroRepo, "velero", nextChartVersion); err != nil { + if err := MirrorChart(c.Context, hcli, veleroRepo, "velero", nextChartVersion); err != nil { return fmt.Errorf("failed to mirror velero chart: %v", err) } } diff --git a/cmd/installer/cli/api.go b/cmd/installer/cli/api.go index 520b02c8df..8b0cacffd9 100644 --- a/cmd/installer/cli/api.go +++ b/cmd/installer/cli/api.go @@ -28,8 +28,6 @@ type apiOptions struct { apitypes.APIConfig ManagerPort int - // The target of the installation, kubernetes or linux - InstallTarget string // The mode the web will be running on, install or upgrade WebMode web.Mode @@ -89,7 +87,7 @@ func serveAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, webServer, err := web.New(web.InitialState{ Title: opts.ReleaseData.Application.Spec.Title, Icon: opts.ReleaseData.Application.Spec.Icon, - InstallTarget: opts.InstallTarget, + InstallTarget: string(opts.InstallTarget), Mode: opts.WebMode, IsAirgap: opts.AirgapBundle != "", RequiresInfraUpgrade: opts.RequiresInfraUpgrade, diff --git a/cmd/installer/cli/api_test.go b/cmd/installer/cli/api_test.go index d0170fa41a..5251248602 100644 --- a/cmd/installer/cli/api_test.go +++ b/cmd/installer/cli/api_test.go @@ -4,10 +4,14 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/json" "fmt" "io" "net" "net/http" + "net/http/httptest" + "os" + "path/filepath" "strconv" "testing" "testing/fstest" @@ -15,14 +19,18 @@ import ( apilogger "github.com/replicatedhq/embedded-cluster/api/pkg/logger" apitypes "github.com/replicatedhq/embedded-cluster/api/types" + "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" "github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils" "github.com/replicatedhq/embedded-cluster/pkg/release" + "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" "github.com/replicatedhq/embedded-cluster/web" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" + helmcli "helm.sh/helm/v3/pkg/cli" + "k8s.io/apimachinery/pkg/version" ) func Test_serveAPI(t *testing.T) { @@ -63,10 +71,13 @@ func Test_serveAPI(t *testing.T) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), 10) require.NoError(t, err) + rc := setupMockRuntimeConfig(t) + config := apiOptions{ APIConfig: apitypes.APIConfig{ - Password: password, - PasswordHash: passwordHash, + InstallTarget: apitypes.InstallTargetLinux, + Password: password, + PasswordHash: passwordHash, ReleaseData: &release.ReleaseData{ Application: &kotsv1beta1.Application{ Spec: kotsv1beta1.ApplicationSpec{ @@ -78,14 +89,15 @@ func Test_serveAPI(t *testing.T) { }, }, ClusterID: "123", - Target: apitypes.TargetLinux, Mode: apitypes.ModeInstall, + LinuxConfig: apitypes.LinuxConfig{ + RuntimeConfig: rc, + }, }, - ManagerPort: portInt, - InstallTarget: "linux", - WebMode: web.ModeInstall, - Logger: apilogger.NewDiscardLogger(), - WebAssetsFS: webAssetsFS, + ManagerPort: portInt, + WebMode: web.ModeInstall, + Logger: apilogger.NewDiscardLogger(), + WebAssetsFS: webAssetsFS, } go func() { @@ -120,14 +132,14 @@ func Test_serveAPI(t *testing.T) { func Test_serveAPIHTMLInjection(t *testing.T) { tests := []struct { name string - installTarget string + installTarget apitypes.InstallTarget mode web.Mode title string }{ - {"linux install mode", "linux", web.ModeInstall, "Linux Install App"}, - {"linux upgrade mode", "linux", web.ModeUpgrade, "Linux Upgrade App"}, - {"kubernetes install mode", "kubernetes", web.ModeInstall, "K8s Install App"}, - {"kubernetes upgrade mode", "kubernetes", web.ModeUpgrade, "K8s Upgrade App"}, + {"linux install mode", apitypes.InstallTargetLinux, web.ModeInstall, "Linux Install App"}, + {"linux upgrade mode", apitypes.InstallTargetLinux, web.ModeUpgrade, "Linux Upgrade App"}, + {"kubernetes install mode", apitypes.InstallTargetKubernetes, web.ModeInstall, "K8s Install App"}, + {"kubernetes upgrade mode", apitypes.InstallTargetKubernetes, web.ModeUpgrade, "K8s Upgrade App"}, } for _, tt := range tests { @@ -171,8 +183,9 @@ func Test_serveAPIHTMLInjection(t *testing.T) { config := apiOptions{ APIConfig: apitypes.APIConfig{ - Password: password, - PasswordHash: passwordHash, + InstallTarget: tt.installTarget, + Password: password, + PasswordHash: passwordHash, ReleaseData: &release.ReleaseData{ Application: &kotsv1beta1.Application{ Spec: kotsv1beta1.ApplicationSpec{ @@ -184,14 +197,21 @@ func Test_serveAPIHTMLInjection(t *testing.T) { }, }, ClusterID: "123", - Target: apitypes.TargetLinux, Mode: apitypes.ModeInstall, }, - ManagerPort: portInt, - InstallTarget: tt.installTarget, - WebMode: tt.mode, - Logger: apilogger.NewDiscardLogger(), - WebAssetsFS: webAssetsFS, + ManagerPort: portInt, + WebMode: tt.mode, + Logger: apilogger.NewDiscardLogger(), + WebAssetsFS: webAssetsFS, + } + + if tt.installTarget == apitypes.InstallTargetKubernetes { + ki := setupMockKubernetesInstallation(t) + config.Installation = ki + } else { + // Create a runtime config with temp directory + rc := setupMockRuntimeConfig(t) + config.RuntimeConfig = rc } go func() { @@ -233,3 +253,65 @@ func Test_serveAPIHTMLInjection(t *testing.T) { }) } } + +func setupMockRuntimeConfig(t *testing.T) *runtimeconfig.MockRuntimeConfig { + // Set up mock Kubernetes API server for helm client to use + mockK8sServer := setupMockKubernetesAPI(t) + t.Cleanup(func() { + mockK8sServer.Close() + }) + t.Setenv("HELM_KUBEAPISERVER", mockK8sServer.URL) + fmt.Println("HELM_KUBEAPISERVER", mockK8sServer.URL) + + // Write the helm binary to the temp directory for helm client to use + helmPath := filepath.Join(t.TempDir(), "helm") + err := os.WriteFile(helmPath, []byte(mockK8sServer.URL), 0644) + require.NoError(t, err) + + rc := &runtimeconfig.MockRuntimeConfig{} + rc.On("GetKubernetesEnvSettings").Return(helmcli.New()) + rc.On("PathToEmbeddedClusterBinary", "helm").Return(helmPath, nil) + return rc +} + +func setupMockKubernetesInstallation(t *testing.T) *kubernetesinstallation.MockInstallation { + // Set up mock Kubernetes API server for helm client to use + mockK8sServer := setupMockKubernetesAPI(t) + t.Cleanup(func() { + mockK8sServer.Close() + }) + t.Setenv("HELM_KUBEAPISERVER", mockK8sServer.URL) + fmt.Println("HELM_KUBEAPISERVER", mockK8sServer.URL) + + // Write the helm binary to the temp directory for helm client to use + helmPath := filepath.Join(t.TempDir(), "helm") + err := os.WriteFile(helmPath, []byte(mockK8sServer.URL), 0644) + require.NoError(t, err) + + ki := &kubernetesinstallation.MockInstallation{} + ki.On("GetKubernetesEnvSettings").Return(helmcli.New()) + ki.On("PathToEmbeddedBinary", "helm").Return(helmPath, nil) + return ki +} + +// setupMockKubernetesAPI creates a mock Kubernetes API server for testing +func setupMockKubernetesAPI(_ *testing.T) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/version": + // Return a mock Kubernetes version + versionInfo := version.Info{ + Major: "1", + Minor: "28", + GitVersion: "v1.28.0", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(versionInfo) + default: + // Return 404 for other endpoints + w.WriteHeader(http.StatusNotFound) + } + })) + + return server +} diff --git a/cmd/installer/cli/enable_ha.go b/cmd/installer/cli/enable_ha.go index 6308fdcbd4..89f25c8c5a 100644 --- a/cmd/installer/cli/enable_ha.go +++ b/cmd/installer/cli/enable_ha.go @@ -80,9 +80,10 @@ func runEnableHA(ctx context.Context, rc runtimeconfig.RuntimeConfig) error { } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: rc.PathToKubeConfig(), - K0sVersion: versions.K0sVersion, - AirgapPath: airgapChartsPath, + HelmPath: rc.PathToEmbeddedClusterBinary("helm"), + KubernetesEnvSettings: rc.GetKubernetesEnvSettings(), + K8sVersion: versions.K0sVersion, + AirgapPath: airgapChartsPath, }) if err != nil { return fmt.Errorf("unable to create helm client: %w", err) diff --git a/cmd/installer/cli/install.go b/cmd/installer/cli/install.go index 3d8381174d..18f7e6d267 100644 --- a/cmd/installer/cli/install.go +++ b/cmd/installer/cli/install.go @@ -54,7 +54,6 @@ import ( "github.com/spf13/pflag" "golang.org/x/crypto/bcrypt" helmcli "helm.sh/helm/v3/pkg/cli" - "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/discovery" "k8s.io/client-go/metadata" nodeutil "k8s.io/component-helpers/node/util" @@ -102,8 +101,6 @@ type installConfig struct { tlsCert tls.Certificate tlsCertBytes []byte tlsKeyBytes []byte - - kubernetesRESTClientGetter genericclioptions.RESTClientGetter } // webAssetsFS is the filesystem to be used by the web component. Defaults to nil allowing the web server to use the default assets embedded in the binary. Useful for testing. @@ -322,27 +319,8 @@ func newKubernetesInstallFlags(flags *InstallCmdFlags, enableV3 bool) *pflag.Fla } func addKubernetesCLIFlags(flagSet *pflag.FlagSet, flags *InstallCmdFlags) { - // From helm - // https://github.com/helm/helm/blob/v3.18.3/pkg/cli/environment.go#L145-L163 - s := helmcli.New() - - flagSet.StringVar(&s.KubeConfig, "kubeconfig", "", "Path to the kubeconfig file") - flagSet.StringVar(&s.KubeContext, "kube-context", s.KubeContext, "Name of the kubeconfig context to use") - flagSet.StringVar(&s.KubeToken, "kube-token", s.KubeToken, "Bearer token used for authentication") - flagSet.StringVar(&s.KubeAsUser, "kube-as-user", s.KubeAsUser, "Username to impersonate for the operation") - flagSet.StringArrayVar(&s.KubeAsGroups, "kube-as-group", s.KubeAsGroups, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.") - flagSet.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "The address and the port for the Kubernetes API server") - flagSet.StringVar(&s.KubeCaFile, "kube-ca-file", s.KubeCaFile, "The certificate authority file for the Kubernetes API server connection") - flagSet.StringVar(&s.KubeTLSServerName, "kube-tls-server-name", s.KubeTLSServerName, "Server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used") - // flagSet.BoolVar(&s.Debug, "helm-debug", s.Debug, "enable verbose output") - flagSet.BoolVar(&s.KubeInsecureSkipTLSVerify, "kube-insecure-skip-tls-verify", s.KubeInsecureSkipTLSVerify, "If true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure") - // flagSet.StringVar(&s.RegistryConfig, "helm-registry-config", s.RegistryConfig, "Path to the Helm registry config file") - // flagSet.StringVar(&s.RepositoryConfig, "helm-repository-config", s.RepositoryConfig, "Path to the file containing Helm repository names and URLs") - // flagSet.StringVar(&s.RepositoryCache, "helm-repository-cache", s.RepositoryCache, "Path to the directory containing cached Helm repository indexes") - flagSet.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "Kubernetes API client-side default throttling limit") - flagSet.Float32Var(&s.QPS, "qps", s.QPS, "Queries per second used when communicating with the Kubernetes API, not including bursting") - + helm.AddKubernetesCLIFlags(flagSet, s) flags.kubernetesEnvSettings = s } @@ -580,7 +558,7 @@ func preRunInstallLinux(cmd *cobra.Command, flags *InstallCmdFlags, rc runtimeco return nil } -func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, _ kubernetesinstallation.Installation) error { +func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, ki kubernetesinstallation.Installation) error { // TODO: we only support amd64 clusters for target=kubernetes installs helpers.SetClusterArch("amd64") @@ -608,7 +586,7 @@ func preRunInstallKubernetes(_ *cobra.Command, flags *InstallCmdFlags, _ kuberne return fmt.Errorf("failed to connect to kubernetes api server: %w", err) } - flags.kubernetesRESTClientGetter = flags.kubernetesEnvSettings.RESTClientGetter() + ki.SetKubernetesEnvSettings(flags.kubernetesEnvSettings) return nil } @@ -699,8 +677,9 @@ func runManagerExperienceInstall( apiConfig := apiOptions{ APIConfig: apitypes.APIConfig{ - Password: flags.adminConsolePassword, - PasswordHash: passwordHash, + InstallTarget: apitypes.InstallTarget(flags.target), + Password: flags.adminConsolePassword, + PasswordHash: passwordHash, TLSConfig: apitypes.TLSConfig{ CertBytes: flags.tlsCertBytes, KeyBytes: flags.tlsKeyBytes, @@ -714,7 +693,6 @@ func runManagerExperienceInstall( ReleaseData: release.GetReleaseData(), EndUserConfig: eucfg, ClusterID: flags.clusterID, - Target: apitypes.Target(flags.target), Mode: apitypes.ModeInstall, RequiresInfraUpgrade: false, // Always false for install @@ -723,13 +701,11 @@ func runManagerExperienceInstall( AllowIgnoreHostPreflights: flags.ignoreHostPreflights, }, KubernetesConfig: apitypes.KubernetesConfig{ - RESTClientGetter: flags.kubernetesRESTClientGetter, - Installation: ki, + Installation: ki, }, }, ManagerPort: flags.managerPort, - InstallTarget: flags.target, WebMode: web.ModeInstall, MetricsReporter: metricsReporter, } @@ -810,9 +786,10 @@ func runInstall(ctx context.Context, flags InstallCmdFlags, rc runtimeconfig.Run } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: rc.PathToKubeConfig(), - K0sVersion: versions.K0sVersion, - AirgapPath: airgapChartsPath, + HelmPath: rc.PathToEmbeddedClusterBinary("helm"), + KubernetesEnvSettings: rc.GetKubernetesEnvSettings(), + K8sVersion: versions.K0sVersion, + AirgapPath: airgapChartsPath, }) if err != nil { return fmt.Errorf("failed to create helm client: %w", err) diff --git a/cmd/installer/cli/join.go b/cmd/installer/cli/join.go index 9307b831af..5fa262e752 100644 --- a/cmd/installer/cli/join.go +++ b/cmd/installer/cli/join.go @@ -616,9 +616,10 @@ func maybeEnableHA(ctx context.Context, kcli client.Client, mcli metadata.Interf airgapChartsPath = rc.EmbeddedClusterChartsSubDir() } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: rc.PathToKubeConfig(), - K0sVersion: versions.K0sVersion, - AirgapPath: airgapChartsPath, + HelmPath: rc.PathToEmbeddedClusterBinary("helm"), + KubernetesEnvSettings: rc.GetKubernetesEnvSettings(), + K8sVersion: versions.K0sVersion, + AirgapPath: airgapChartsPath, }) if err != nil { return fmt.Errorf("unable to create helm client: %w", err) diff --git a/cmd/installer/cli/restore.go b/cmd/installer/cli/restore.go index 32675a8fb0..c52ccd1157 100644 --- a/cmd/installer/cli/restore.go +++ b/cmd/installer/cli/restore.go @@ -405,9 +405,10 @@ func runRestoreStepNew(ctx context.Context, appSlug, appTitle string, flags Inst } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: rc.PathToKubeConfig(), - K0sVersion: versions.K0sVersion, - AirgapPath: airgapChartsPath, + HelmPath: rc.PathToEmbeddedClusterBinary("helm"), + KubernetesEnvSettings: rc.GetKubernetesEnvSettings(), + K8sVersion: versions.K0sVersion, + AirgapPath: airgapChartsPath, }) if err != nil { return fmt.Errorf("unable to create helm client: %w", err) @@ -612,9 +613,10 @@ func runRestoreEnableAdminConsoleHA(ctx context.Context, flags InstallCmdFlags, } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: rc.PathToKubeConfig(), - K0sVersion: versions.K0sVersion, - AirgapPath: airgapChartsPath, + HelmPath: rc.PathToEmbeddedClusterBinary("helm"), + KubernetesEnvSettings: rc.GetKubernetesEnvSettings(), + K8sVersion: versions.K0sVersion, + AirgapPath: airgapChartsPath, }) if err != nil { return fmt.Errorf("create helm client: %w", err) @@ -710,9 +712,10 @@ func runRestoreExtensions(ctx context.Context, flags InstallCmdFlags, rc runtime } hcli, err := helm.NewClient(helm.HelmOptions{ - KubeConfig: rc.PathToKubeConfig(), - K0sVersion: versions.K0sVersion, - AirgapPath: airgapChartsPath, + HelmPath: rc.PathToEmbeddedClusterBinary("helm"), + KubernetesEnvSettings: rc.GetKubernetesEnvSettings(), + K8sVersion: versions.K0sVersion, + AirgapPath: airgapChartsPath, }) if err != nil { return fmt.Errorf("unable to create helm client: %w", err) diff --git a/cmd/installer/cli/upgrade.go b/cmd/installer/cli/upgrade.go index 0e66180f73..c153fd8f4f 100644 --- a/cmd/installer/cli/upgrade.go +++ b/cmd/installer/cli/upgrade.go @@ -439,6 +439,7 @@ func runManagerExperienceUpgrade( ) (finalErr error) { apiConfig := apiOptions{ APIConfig: apitypes.APIConfig{ + InstallTarget: apitypes.InstallTarget(flags.target), Password: "", // Only PasswordHash is necessary for upgrades because the kotsadm-password secret has been created already PasswordHash: upgradeConfig.passwordHash, TLSConfig: upgradeConfig.tlsConfig, @@ -450,7 +451,6 @@ func runManagerExperienceUpgrade( ReleaseData: release.GetReleaseData(), EndUserConfig: upgradeConfig.endUserConfig, ClusterID: upgradeConfig.clusterID, - Target: apitypes.Target(flags.target), Mode: apitypes.ModeUpgrade, TargetVersion: targetVersion, InitialVersion: initialVersion, @@ -461,7 +461,6 @@ func runManagerExperienceUpgrade( }, }, ManagerPort: upgradeConfig.managerPort, - InstallTarget: flags.target, WebMode: web.ModeUpgrade, MetricsReporter: metricsReporter, } diff --git a/cmd/installer/goods/materializer.go b/cmd/installer/goods/materializer.go index 8f4fcae08b..cfb119deef 100644 --- a/cmd/installer/goods/materializer.go +++ b/cmd/installer/goods/materializer.go @@ -54,6 +54,30 @@ func InternalBinary(name string) (string, error) { return dstpath.Name(), nil } +// Binary materializes a binary from inside bins directory +// and writes it to a tmp file. It returns the path to the materialized binary. +// The binary should be deleted after it is used. +// This is primarily intended for short-lived, internal-use binaries. +func Binary(name string) (string, error) { + srcpath := fmt.Sprintf("bins/%s", name) + srcfile, err := binfs.ReadFile(srcpath) + if err != nil { + return "", fmt.Errorf("unable to read asset: %w", err) + } + dstpath, err := os.CreateTemp("", fmt.Sprintf("embedded-cluster-%s-bin-", name)) + if err != nil { + return "", fmt.Errorf("unable to create temp file: %w", err) + } + defer dstpath.Close() + if _, err := dstpath.Write(srcfile); err != nil { + return "", fmt.Errorf("unable to write file: %w", err) + } + if err := dstpath.Chmod(0755); err != nil { + return "", fmt.Errorf("unable to set executable permissions: %w", err) + } + return dstpath.Name(), nil +} + // LocalArtifactMirrorUnitFile writes to disk the local-artifact-mirror systemd unit file. func (m *Materializer) LocalArtifactMirrorUnitFile() error { content, err := systemdfs.ReadFile("systemd/local-artifact-mirror.service") diff --git a/operator/pkg/cli/upgrade_job.go b/operator/pkg/cli/upgrade_job.go index b4fe2a32bc..7be1e0edab 100644 --- a/operator/pkg/cli/upgrade_job.go +++ b/operator/pkg/cli/upgrade_job.go @@ -64,11 +64,8 @@ func UpgradeJobCmd() *cobra.Command { } hcli, err := helm.NewClient(helm.HelmOptions{ - K0sVersion: versions.K0sVersion, + K8sVersion: versions.K0sVersion, AirgapPath: airgapChartsPath, - LogFn: func(format string, v ...interface{}) { - logger.WithField("component", "helm").Infof(format, v...) - }, }) if err != nil { return fmt.Errorf("failed to create helm client: %w", err) diff --git a/pkg-new/kubernetesinstallation/installation.go b/pkg-new/kubernetesinstallation/installation.go index 0e86e6b3d4..6940d5a2ab 100644 --- a/pkg-new/kubernetesinstallation/installation.go +++ b/pkg-new/kubernetesinstallation/installation.go @@ -5,6 +5,7 @@ import ( "github.com/replicatedhq/embedded-cluster/cmd/installer/goods" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + helmcli "helm.sh/helm/v3/pkg/cli" ) var _ Installation = &kubernetesInstallation{} @@ -16,8 +17,9 @@ type EnvSetter interface { } type kubernetesInstallation struct { - installation *ecv1beta1.KubernetesInstallation - envSetter EnvSetter + installation *ecv1beta1.KubernetesInstallation + envSetter EnvSetter + kubernetesEnvSettings *helmcli.EnvSettings } type osEnvSetter struct{} @@ -128,7 +130,17 @@ func (ki *kubernetesInstallation) SetProxySpec(proxySpec *ecv1beta1.ProxySpec) { ki.installation.Spec.Proxy = proxySpec } -// PathToEmbeddedBinary returns the path to an embedded binary by materializing it from the embedded assets. +// PathToEmbeddedBinary returns the path to the embedded binary. func (ki *kubernetesInstallation) PathToEmbeddedBinary(binaryName string) (string, error) { - return goods.InternalBinary(binaryName) + return goods.Binary(binaryName) +} + +// SetKubernetesEnvSettings sets the helm environment settings. +func (ki *kubernetesInstallation) SetKubernetesEnvSettings(envSettings *helmcli.EnvSettings) { + ki.kubernetesEnvSettings = envSettings +} + +// GetKubernetesEnvSettings returns the helm environment settings. +func (ki *kubernetesInstallation) GetKubernetesEnvSettings() *helmcli.EnvSettings { + return ki.kubernetesEnvSettings } diff --git a/pkg-new/kubernetesinstallation/interface.go b/pkg-new/kubernetesinstallation/interface.go index 73ab30670f..1147eb2700 100644 --- a/pkg-new/kubernetesinstallation/interface.go +++ b/pkg-new/kubernetesinstallation/interface.go @@ -2,6 +2,7 @@ package kubernetesinstallation import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + helmcli "helm.sh/helm/v3/pkg/cli" ) // Installation defines the interface for managing kubernetes installation @@ -24,4 +25,7 @@ type Installation interface { SetProxySpec(proxySpec *ecv1beta1.ProxySpec) PathToEmbeddedBinary(binaryName string) (string, error) + + SetKubernetesEnvSettings(envSettings *helmcli.EnvSettings) + GetKubernetesEnvSettings() *helmcli.EnvSettings } diff --git a/pkg-new/kubernetesinstallation/mock.go b/pkg-new/kubernetesinstallation/mock.go index 4a99037958..522cf42c12 100644 --- a/pkg-new/kubernetesinstallation/mock.go +++ b/pkg-new/kubernetesinstallation/mock.go @@ -3,6 +3,7 @@ package kubernetesinstallation import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/stretchr/testify/mock" + helmcli "helm.sh/helm/v3/pkg/cli" ) var _ Installation = (*MockInstallation)(nil) @@ -86,3 +87,17 @@ func (m *MockInstallation) PathToEmbeddedBinary(binaryName string) (string, erro args := m.Called(binaryName) return args.String(0), args.Error(1) } + +// SetKubernetesEnvSettings mocks the SetKubernetesEnvSettings method +func (m *MockInstallation) SetKubernetesEnvSettings(envSettings *helmcli.EnvSettings) { + m.Called(envSettings) +} + +// GetKubernetesEnvSettings mocks the GetKubernetesEnvSettings method +func (m *MockInstallation) GetKubernetesEnvSettings() *helmcli.EnvSettings { + args := m.Called() + if args.Get(0) == nil { + return nil + } + return args.Get(0).(*helmcli.EnvSettings) +} diff --git a/pkg/addons/adminconsole/install.go b/pkg/addons/adminconsole/install.go index 6c4d3789eb..8b621bdf99 100644 --- a/pkg/addons/adminconsole/install.go +++ b/pkg/addons/adminconsole/install.go @@ -57,6 +57,7 @@ func (a *AdminConsole) Install( Values: values, Namespace: a.Namespace(), Labels: getBackupLabels(), + LogFn: helm.LogFn(logf), } if a.DryRun { diff --git a/pkg/addons/adminconsole/upgrade.go b/pkg/addons/adminconsole/upgrade.go index ffd85b0a86..45b095d595 100644 --- a/pkg/addons/adminconsole/upgrade.go +++ b/pkg/addons/adminconsole/upgrade.go @@ -45,6 +45,7 @@ func (a *AdminConsole) Upgrade( Namespace: a.Namespace(), Labels: getBackupLabels(), Force: false, + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm upgrade") diff --git a/pkg/addons/embeddedclusteroperator/install.go b/pkg/addons/embeddedclusteroperator/install.go index 5e0e1f5909..394526f8c9 100644 --- a/pkg/addons/embeddedclusteroperator/install.go +++ b/pkg/addons/embeddedclusteroperator/install.go @@ -28,6 +28,7 @@ func (e *EmbeddedClusterOperator) Install( Values: values, Namespace: e.Namespace(), Labels: getBackupLabels(), + LogFn: helm.LogFn(logf), } if e.DryRun { diff --git a/pkg/addons/embeddedclusteroperator/upgrade.go b/pkg/addons/embeddedclusteroperator/upgrade.go index cbc2668076..7ec902295d 100644 --- a/pkg/addons/embeddedclusteroperator/upgrade.go +++ b/pkg/addons/embeddedclusteroperator/upgrade.go @@ -42,6 +42,7 @@ func (e *EmbeddedClusterOperator) Upgrade( Namespace: e.Namespace(), Labels: getBackupLabels(), Force: false, + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm upgrade") diff --git a/pkg/addons/openebs/install.go b/pkg/addons/openebs/install.go index d5752ba942..59db665e5d 100644 --- a/pkg/addons/openebs/install.go +++ b/pkg/addons/openebs/install.go @@ -27,6 +27,7 @@ func (o *OpenEBS) Install( ChartVersion: Metadata.Version, Values: values, Namespace: o.Namespace(), + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm install") diff --git a/pkg/addons/openebs/upgrade.go b/pkg/addons/openebs/upgrade.go index bd2fe28f73..385a5380ff 100644 --- a/pkg/addons/openebs/upgrade.go +++ b/pkg/addons/openebs/upgrade.go @@ -51,6 +51,7 @@ func (o *OpenEBS) Upgrade( Values: values, Namespace: o.Namespace(), Force: false, + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm upgrade") diff --git a/pkg/addons/registry/install.go b/pkg/addons/registry/install.go index d27030517f..0268c1f2ec 100644 --- a/pkg/addons/registry/install.go +++ b/pkg/addons/registry/install.go @@ -44,6 +44,7 @@ func (r *Registry) Install( Values: values, Namespace: r.Namespace(), Labels: getBackupLabels(), + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm install") diff --git a/pkg/addons/registry/upgrade.go b/pkg/addons/registry/upgrade.go index 6de7be4884..ad4f712089 100644 --- a/pkg/addons/registry/upgrade.go +++ b/pkg/addons/registry/upgrade.go @@ -57,6 +57,7 @@ func (r *Registry) Upgrade( Namespace: r.Namespace(), Labels: getBackupLabels(), Force: false, + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm upgrade") diff --git a/pkg/addons/seaweedfs/install.go b/pkg/addons/seaweedfs/install.go index 79dd480287..d8b1037209 100644 --- a/pkg/addons/seaweedfs/install.go +++ b/pkg/addons/seaweedfs/install.go @@ -44,6 +44,7 @@ func (s *SeaweedFS) Install( Values: values, Namespace: s.Namespace(), Labels: getBackupLabels(), + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm install") diff --git a/pkg/addons/seaweedfs/upgrade.go b/pkg/addons/seaweedfs/upgrade.go index 903e8830de..2667d2c7b1 100644 --- a/pkg/addons/seaweedfs/upgrade.go +++ b/pkg/addons/seaweedfs/upgrade.go @@ -78,6 +78,7 @@ func (s *SeaweedFS) Upgrade( Namespace: s.Namespace(), Labels: getBackupLabels(), Force: false, + LogFn: helm.LogFn(logf), }) if err != nil { return fmt.Errorf("helm upgrade: %w", err) diff --git a/pkg/addons/velero/install.go b/pkg/addons/velero/install.go index dce9b5f2ea..51206b7dd8 100644 --- a/pkg/addons/velero/install.go +++ b/pkg/addons/velero/install.go @@ -35,6 +35,7 @@ func (v *Velero) Install( ChartVersion: Metadata.Version, Values: values, Namespace: v.Namespace(), + LogFn: helm.LogFn(logf), } if v.DryRun { diff --git a/pkg/addons/velero/upgrade.go b/pkg/addons/velero/upgrade.go index 00560814ca..e73ed739fe 100644 --- a/pkg/addons/velero/upgrade.go +++ b/pkg/addons/velero/upgrade.go @@ -41,6 +41,7 @@ func (v *Velero) Upgrade( Values: values, Namespace: v.Namespace(), Force: false, + LogFn: helm.LogFn(logf), }) if err != nil { return errors.Wrap(err, "helm upgrade") diff --git a/pkg/extensions/install.go b/pkg/extensions/install.go index e3e3a28612..30ebfe4e24 100644 --- a/pkg/extensions/install.go +++ b/pkg/extensions/install.go @@ -20,7 +20,7 @@ func Install(ctx context.Context, hcli helm.Client, progressChan chan<- Extensio return nil } - if err := addRepos(hcli, config.AdditionalRepositories()); err != nil { + if err := addRepos(ctx, hcli, config.AdditionalRepositories()); err != nil { return errors.Wrap(err, "add additional helm repositories") } diff --git a/pkg/extensions/upgrade.go b/pkg/extensions/upgrade.go index d793af3a72..ba8857ea14 100644 --- a/pkg/extensions/upgrade.go +++ b/pkg/extensions/upgrade.go @@ -25,7 +25,7 @@ type helmAction string func Upgrade(ctx context.Context, kcli client.Client, hcli helm.Client, prev *ecv1beta1.Installation, in *ecv1beta1.Installation, logger logrus.FieldLogger) error { // add new helm repos if in.Spec.Config.Extensions.Helm != nil { - if err := addRepos(hcli, in.Spec.Config.Extensions.Helm.Repositories); err != nil { + if err := addRepos(ctx, hcli, in.Spec.Config.Extensions.Helm.Repositories); err != nil { return errors.Wrap(err, "add repos") } } diff --git a/pkg/extensions/util.go b/pkg/extensions/util.go index 16767f0fc4..dba038655e 100644 --- a/pkg/extensions/util.go +++ b/pkg/extensions/util.go @@ -14,7 +14,7 @@ import ( helmrepo "helm.sh/helm/v3/pkg/repo" ) -func addRepos(hcli helm.Client, repos []k0sv1beta1.Repository) error { +func addRepos(ctx context.Context, hcli helm.Client, repos []k0sv1beta1.Repository) error { for _, r := range repos { logrus.Debugf("Adding helm repository %s", r.Name) @@ -30,7 +30,7 @@ func addRepos(hcli helm.Client, repos []k0sv1beta1.Repository) error { if r.Insecure != nil { helmRepo.InsecureSkipTLSverify = *r.Insecure } - if err := hcli.AddRepo(helmRepo); err != nil { + if err := hcli.AddRepo(ctx, helmRepo); err != nil { return errors.Wrapf(err, "add helm repository %s", r.Name) } } @@ -51,6 +51,7 @@ func install(ctx context.Context, hcli helm.Client, ext ecv1beta1.Chart) error { Values: values, Namespace: ext.TargetNS, Timeout: ext.Timeout.Duration, + // TODO: Do we need to set LogFn? }) if err != nil { return errors.Wrap(err, "helm install") @@ -73,6 +74,7 @@ func upgrade(ctx context.Context, hcli helm.Client, ext ecv1beta1.Chart) error { Namespace: ext.TargetNS, Timeout: ext.Timeout.Duration, Force: true, // this was the default in k0s + // TODO: Do we need to set LogFn? } if ext.ForceUpgrade != nil { opts.Force = *ext.ForceUpgrade diff --git a/pkg/helm/binary_executor.go b/pkg/helm/binary_executor.go new file mode 100644 index 0000000000..3d39c3c8e1 --- /dev/null +++ b/pkg/helm/binary_executor.go @@ -0,0 +1,75 @@ +package helm + +import ( + "bytes" + "context" + "io" + "maps" + "path/filepath" + "regexp" + "strings" + + "github.com/replicatedhq/embedded-cluster/pkg/helpers" +) + +// BinaryExecutor is an interface for executing helm binary commands. +// This interface is mockable for testing purposes. +type BinaryExecutor interface { + // ExecuteCommand runs a command and returns stdout, stderr, and error + ExecuteCommand(ctx context.Context, env map[string]string, logFn LogFn, args ...string) (stdout string, stderr string, err error) +} + +// binaryExecutor implements BinaryExecutor using helpers.RunCommandWithOptions +type binaryExecutor struct { + bin string // Path to the binary to execute + defaultEnv map[string]string // Default environment variables to set for all commands +} + +// newBinaryExecutor creates a new binaryExecutor with the specified binary path and optional default environment +func newBinaryExecutor(bin string, homeDir string) BinaryExecutor { + // Configure helm environment variables for tmpdir isolation + helmEnv := map[string]string{ + "HELM_CACHE_HOME": filepath.Join(homeDir, ".cache"), + "HELM_CONFIG_HOME": filepath.Join(homeDir, ".config"), + "HELM_DATA_HOME": filepath.Join(homeDir, ".local"), + } + return &binaryExecutor{bin: bin, defaultEnv: helmEnv} +} + +// ExecuteCommand runs a command using helpers.RunCommandWithOptions and returns stdout, stderr, and error +func (c *binaryExecutor) ExecuteCommand(ctx context.Context, env map[string]string, logFn LogFn, args ...string) (string, string, error) { + var stdout, stderr bytes.Buffer + logWriter := &logWriter{logFn: logFn} + + // Merge default environment with provided environment (provided env takes precedence) + mergedEnv := make(map[string]string) + maps.Copy(mergedEnv, c.defaultEnv) + maps.Copy(mergedEnv, env) + + err := helpers.RunCommandWithOptions(helpers.RunCommandOptions{ + Context: ctx, + Stdout: &stdout, + Stderr: io.MultiWriter(&stderr, logWriter), // Helm uses stderr for debug logging and progress + Env: mergedEnv, + }, c.bin, args...) + + return stdout.String(), stderr.String(), err +} + +// logWriter wraps a logFn as an io.Writer +type logWriter struct { + logFn LogFn +} + +// match log lines that come from go files to reduce noise and keep the logs relevant and readable to the user +var goFilePattern = regexp.MustCompile(`^\w+\.go:\d+:`) + +func (lw *logWriter) Write(p []byte) (n int, err error) { + if lw.logFn != nil && len(p) > 0 { + line := strings.TrimSpace(string(p)) + if line != "" && goFilePattern.MatchString(line) { + lw.logFn("helm: %s", line) + } + } + return len(p), nil +} diff --git a/pkg/helm/binary_executor_mock.go b/pkg/helm/binary_executor_mock.go new file mode 100644 index 0000000000..0453f064e4 --- /dev/null +++ b/pkg/helm/binary_executor_mock.go @@ -0,0 +1,20 @@ +package helm + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +var _ BinaryExecutor = (*MockBinaryExecutor)(nil) + +// MockBinaryExecutor is a mock implementation of BinaryExecutor for testing +type MockBinaryExecutor struct { + mock.Mock +} + +// ExecuteCommand mocks the ExecuteCommand method +func (m *MockBinaryExecutor) ExecuteCommand(ctx context.Context, env map[string]string, logFn LogFn, args ...string) (string, string, error) { + callArgs := m.Called(ctx, env, logFn, args) + return callArgs.String(0), callArgs.String(1), callArgs.Error(2) +} diff --git a/pkg/helm/binary_executor_test.go b/pkg/helm/binary_executor_test.go new file mode 100644 index 0000000000..676c57cad4 --- /dev/null +++ b/pkg/helm/binary_executor_test.go @@ -0,0 +1,248 @@ +package helm + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_binaryExecutor_ExecuteCommand(t *testing.T) { + tests := []struct { + name string + bin string + args []string + wantErr bool + }{ + { + name: "echo command", + bin: "echo", + args: []string{"hello", "world"}, + wantErr: false, + }, + { + name: "invalid command", + bin: "nonexistent-command", + args: []string{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + executor := newBinaryExecutor(tt.bin, t.TempDir()) + stdout, stderr, err := executor.ExecuteCommand(t.Context(), nil, nil, tt.args...) + + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Empty(t, stderr) + if tt.bin == "echo" { + assert.Contains(t, stdout, "hello world") + } + }) + } +} + +func Test_binaryExecutor_ExecuteCommand_WithLogging(t *testing.T) { + tests := []struct { + name string + bin string + args []string + wantErr bool + expectedStdout string + expectedStderr string + expectedLogs []string + }{ + { + name: "echo command with logging", + bin: "echo", + args: []string{"hello", "world"}, + wantErr: false, + expectedStdout: "hello world\n", + expectedStderr: "", + expectedLogs: []string{}, // No logs expected since echo only writes to stdout + }, + { + name: "command with stderr", + bin: "sh", + args: []string{"-c", "echo 'stdout message'; echo 'stderr message' >&2"}, + wantErr: false, + expectedStdout: "stdout message\n", + expectedStderr: "stderr message\n", + expectedLogs: []string{}, // No logs expected since stderr doesn't match .go file pattern + }, + { + name: "command with go file pattern in stderr", + bin: "sh", + args: []string{"-c", "echo 'stdout message'; echo 'install.go:225: debug message' >&2"}, + wantErr: false, + expectedStdout: "stdout message\n", + expectedStderr: "install.go:225: debug message\n", + expectedLogs: []string{"helm: install.go:225: debug message"}, // Go file pattern should be logged with helm prefix + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var logs []string + logFn := func(format string, v ...any) { + logs = append(logs, fmt.Sprintf(format, v...)) + } + + executor := newBinaryExecutor(tt.bin, t.TempDir()) + stdout, stderr, err := executor.ExecuteCommand(t.Context(), nil, logFn, tt.args...) + + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + + // Verify output is captured in buffers + assert.Equal(t, tt.expectedStdout, stdout) + assert.Equal(t, tt.expectedStderr, stderr) + + // Verify logging occurred with expected messages + assert.ElementsMatch(t, tt.expectedLogs, logs) + }) + } +} + +func Test_logWriter_Write(t *testing.T) { + var loggedMessages []string + logFn := func(format string, v ...any) { + loggedMessages = append(loggedMessages, fmt.Sprintf(format, v...)) + } + + writer := &logWriter{logFn: logFn} + + // Test writing data that matches .go file pattern + n, err := writer.Write([]byte("install.go:225: test message")) + assert.NoError(t, err) + assert.Equal(t, 28, n) + assert.Len(t, loggedMessages, 1) + assert.Equal(t, "helm: install.go:225: test message", loggedMessages[0]) + + // Test writing data that doesn't match .go file pattern (should be filtered out) + loggedMessages = nil + n, err = writer.Write([]byte("verbose debug message")) + assert.NoError(t, err) + assert.Equal(t, 21, n) + assert.Len(t, loggedMessages, 0) // Should be filtered out + + // Test writing empty data + loggedMessages = nil + n, err = writer.Write([]byte{}) + assert.NoError(t, err) + assert.Equal(t, 0, n) + assert.Len(t, loggedMessages, 0) + + // Test with nil logFn + writer = &logWriter{logFn: nil} + n, err = writer.Write([]byte("test")) + assert.NoError(t, err) + assert.Equal(t, 4, n) +} + +func Test_binaryExecutor_EnvironmentMerging(t *testing.T) { + tmpDir := t.TempDir() + executor := newBinaryExecutor("sh", tmpDir) + + // Create a command that outputs all environment variables containing our test vars + providedEnv := map[string]string{ + "PROVIDED_VAR": "provided_value", + "HELM_DATA_HOME": "overridden_value", // This should override the default + } + + // Use a shell command to check if our environment variables are set + stdout, _, err := executor.ExecuteCommand( + t.Context(), + providedEnv, + nil, + "-c", "echo HELM_CACHE_HOME=$HELM_CACHE_HOME HELM_CONFIG_HOME=$HELM_CONFIG_HOME HELM_DATA_HOME=$HELM_DATA_HOME PROVIDED_VAR=$PROVIDED_VAR", + ) + + require.NoError(t, err) + + // Verify that: + // 1. Default env var is present + assert.Contains(t, stdout, "HELM_CACHE_HOME="+tmpDir) + assert.Contains(t, stdout, "HELM_CONFIG_HOME="+tmpDir) + // 2. Provided env var is present + assert.Contains(t, stdout, "PROVIDED_VAR=provided_value") + // 3. Provided env var overrides default + assert.Contains(t, stdout, "HELM_DATA_HOME=overridden_value") + assert.NotContains(t, stdout, "HELM_DATA_HOME="+tmpDir) +} + +func Test_MockBinaryExecutor_ExecuteCommand(t *testing.T) { + tests := []struct { + name string + setupMock func(*MockBinaryExecutor) + env map[string]string + args []string + expectedStdout string + expectedStderr string + expectedErr error + }{ + { + name: "successful command", + setupMock: func(m *MockBinaryExecutor) { + m.On("ExecuteCommand", + mock.Anything, + map[string]string{"TEST": "value"}, + mock.Anything, // LogFn + []string{"version"}, + ).Return("v3.12.0", "", nil) + }, + env: map[string]string{"TEST": "value"}, + args: []string{"version"}, + expectedStdout: "v3.12.0", + expectedStderr: "", + expectedErr: nil, + }, + { + name: "command with error", + setupMock: func(m *MockBinaryExecutor) { + m.On("ExecuteCommand", + mock.Anything, + mock.Anything, + mock.Anything, // LogFn + []string{"invalid"}, + ).Return("", "command not found", assert.AnError) + }, + env: nil, + args: []string{"invalid"}, + expectedStdout: "", + expectedStderr: "command not found", + expectedErr: assert.AnError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &MockBinaryExecutor{} + tt.setupMock(mock) + + stdout, stderr, err := mock.ExecuteCommand(t.Context(), tt.env, nil, tt.args...) + + if tt.expectedErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedStderr, stderr) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedStdout, stdout) + assert.Equal(t, tt.expectedStderr, stderr) + } + + mock.AssertExpectations(t) + }) + } +} diff --git a/pkg/helm/client.go b/pkg/helm/client.go index dcac3ad162..5ebead5ea6 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -13,17 +13,18 @@ import ( "github.com/Masterminds/semver/v3" "github.com/sirupsen/logrus" + "github.com/spf13/pflag" "go.yaml.in/yaml/v3" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" + helmcli "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/pusher" "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v3/pkg/storage/driver" "helm.sh/helm/v3/pkg/uploader" @@ -60,52 +61,51 @@ var ( var _ Client = (*HelmClient)(nil) func newClient(opts HelmOptions) (*HelmClient, error) { - tmpdir, err := os.MkdirTemp(os.TempDir(), "helm-cache-*") + tmpdir, err := os.MkdirTemp(os.TempDir(), "helm-*") if err != nil { return nil, err } - registryOpts := []registry.ClientOption{} - if opts.Writer != nil { - registryOpts = append(registryOpts, registry.ClientOptWriter(opts.Writer)) - } + var kversion *semver.Version - if opts.K0sVersion != "" { - sv, err := semver.NewVersion(opts.K0sVersion) + if opts.K8sVersion != "" { + sv, err := semver.NewVersion(opts.K8sVersion) if err != nil { return nil, fmt.Errorf("parse k0s version: %w", err) } kversion = sv } + + registryOpts := []registry.ClientOption{} + if opts.Writer != nil { + registryOpts = append(registryOpts, registry.ClientOptWriter(opts.Writer)) + } regcli, err := registry.NewClient(registryOpts...) if err != nil { return nil, fmt.Errorf("create registry client: %w", err) } - if opts.RESTClientGetter == nil { - cfgFlags := &genericclioptions.ConfigFlags{} - if opts.KubeConfig != "" { - cfgFlags.KubeConfig = &opts.KubeConfig - } - opts.RESTClientGetter = cfgFlags - } + return &HelmClient{ - tmpdir: tmpdir, - kversion: kversion, - restClientGetter: opts.RESTClientGetter, - regcli: regcli, - logFn: opts.LogFn, - airgapPath: opts.AirgapPath, + helmPath: opts.HelmPath, + executor: newBinaryExecutor(opts.HelmPath, tmpdir), + tmpdir: tmpdir, + kversion: kversion, + kubernetesEnvSettings: opts.KubernetesEnvSettings, + regcli: regcli, + airgapPath: opts.AirgapPath, + repositories: []*repo.Entry{}, }, nil } type HelmOptions struct { - KubeConfig string - RESTClientGetter genericclioptions.RESTClientGetter - K0sVersion string - AirgapPath string - Writer io.Writer - LogFn action.DebugLog + HelmPath string // Required: Path to the helm binary + KubernetesEnvSettings *helmcli.EnvSettings + K8sVersion string + AirgapPath string + Writer io.Writer } +type LogFn func(format string, args ...interface{}) + type InstallOptions struct { ReleaseName string ChartPath string @@ -114,6 +114,7 @@ type InstallOptions struct { Namespace string Labels map[string]string Timeout time.Duration + LogFn LogFn // Log function override to use for install command } type UpgradeOptions struct { @@ -125,6 +126,7 @@ type UpgradeOptions struct { Labels map[string]string Timeout time.Duration Force bool + LogFn LogFn // Log function override to use for upgrade command } type UninstallOptions struct { @@ -132,21 +134,33 @@ type UninstallOptions struct { Namespace string Wait bool IgnoreNotFound bool + LogFn LogFn // Log function override to use for uninstall command +} + +type RollbackOptions struct { + ReleaseName string + Namespace string + Revision int // Target revision to rollback to, 0 for automatic + Timeout time.Duration + Force bool + LogFn LogFn // Log function override to use for rollback command } type HelmClient struct { - tmpdir string - kversion *semver.Version - restClientGetter genericclioptions.RESTClientGetter - regcli *registry.Client - repocfg string - repos []*repo.Entry - reposChanged bool - logFn action.DebugLog - airgapPath string -} - -func (h *HelmClient) prepare() error { + helmPath string // Path to helm binary + executor BinaryExecutor // Mockable executor + tmpdir string // Temporary directory for helm + kversion *semver.Version // Kubernetes version for template rendering + kubernetesEnvSettings *helmcli.EnvSettings // Kubernetes environment settings + regcli *registry.Client + repocfg string + repos []*repo.Entry + reposChanged bool + airgapPath string // Airgap path where charts are stored + repositories []*repo.Entry // Repository entries for helm repo commands +} + +func (h *HelmClient) prepare(_ context.Context) error { // NOTE: this is a hack and should be refactored if !h.reposChanged { return nil @@ -184,13 +198,13 @@ func (h *HelmClient) Close() error { return os.RemoveAll(h.tmpdir) } -func (h *HelmClient) AddRepo(repo *repo.Entry) error { +func (h *HelmClient) AddRepo(_ context.Context, repo *repo.Entry) error { h.repos = append(h.repos, repo) h.reposChanged = true return nil } -func (h *HelmClient) Latest(reponame, chart string) (string, error) { +func (h *HelmClient) Latest(_ context.Context, reponame, chart string) (string, error) { stableConstraint, err := semver.NewConstraint(">0.0.0") // search only for stable versions if err != nil { return "", fmt.Errorf("create stable constraint: %w", err) @@ -242,7 +256,7 @@ func (h *HelmClient) Latest(reponame, chart string) (string, error) { func (h *HelmClient) PullByRefWithRetries(ctx context.Context, ref string, version string, tries int) (string, error) { for i := 0; ; i++ { - localPath, err := h.PullByRef(ref, version) + localPath, err := h.PullByRef(ctx, ref, version) if err == nil { return localPath, nil } @@ -258,14 +272,14 @@ func (h *HelmClient) PullByRefWithRetries(ctx context.Context, ref string, versi } } -func (h *HelmClient) Pull(reponame, chart string, version string) (string, error) { +func (h *HelmClient) Pull(ctx context.Context, reponame, chart string, version string) (string, error) { ref := fmt.Sprintf("%s/%s", reponame, chart) - return h.PullByRef(ref, version) + return h.PullByRef(ctx, ref, version) } -func (h *HelmClient) PullByRef(ref string, version string) (string, error) { +func (h *HelmClient) PullByRef(ctx context.Context, ref string, version string) (string, error) { if !isOCIChart(ref) { - if err := h.prepare(); err != nil { + if err := h.prepare(ctx); err != nil { return "", fmt.Errorf("prepare: %w", err) } } @@ -286,11 +300,11 @@ func (h *HelmClient) PullByRef(ref string, version string) (string, error) { return dst, nil } -func (h *HelmClient) RegistryAuth(server, user, pass string) error { +func (h *HelmClient) RegistryAuth(_ context.Context, server, user, pass string) error { return h.regcli.Login(server, registry.LoginOptBasicAuth(user, pass)) } -func (h *HelmClient) Push(path, dst string) error { +func (h *HelmClient) Push(_ context.Context, path, dst string) error { up := uploader.ChartUploader{ Out: os.Stdout, Pushers: pushers, @@ -300,7 +314,7 @@ func (h *HelmClient) Push(path, dst string) error { return up.UploadTo(path, dst) } -func (h *HelmClient) GetChartMetadata(chartPath string) (*chart.Metadata, error) { +func (h *HelmClient) GetChartMetadata(_ context.Context, chartPath string, version string) (*chart.Metadata, error) { chartRequested, err := loader.Load(chartPath) if err != nil { return nil, fmt.Errorf("load chart: %w", err) @@ -311,7 +325,7 @@ func (h *HelmClient) GetChartMetadata(chartPath string) (*chart.Metadata, error) // reference: https://github.com/helm/helm/blob/0d66425d9a745d8a289b1a5ebb6ccc744436da95/cmd/helm/upgrade.go#L122-L125 func (h *HelmClient) ReleaseExists(ctx context.Context, namespace string, releaseName string) (bool, error) { - cfg, err := h.getActionCfg(namespace) + cfg, err := h.getActionCfg(namespace, nil) if err != nil { return false, fmt.Errorf("get action configuration: %w", err) } @@ -335,7 +349,7 @@ func isReleaseUninstalled(versions []*release.Release) bool { } func (h *HelmClient) Install(ctx context.Context, opts InstallOptions) (*release.Release, error) { - cfg, err := h.getActionCfg(opts.Namespace) + cfg, err := h.getActionCfg(opts.Namespace, opts.LogFn) if err != nil { return nil, fmt.Errorf("get action configuration: %w", err) } @@ -382,7 +396,7 @@ func (h *HelmClient) Install(ctx context.Context, opts InstallOptions) (*release } func (h *HelmClient) Upgrade(ctx context.Context, opts UpgradeOptions) (*release.Release, error) { - cfg, err := h.getActionCfg(opts.Namespace) + cfg, err := h.getActionCfg(opts.Namespace, opts.LogFn) if err != nil { return nil, fmt.Errorf("get action configuration: %w", err) } @@ -426,7 +440,7 @@ func (h *HelmClient) Upgrade(ctx context.Context, opts UpgradeOptions) (*release } func (h *HelmClient) Uninstall(ctx context.Context, opts UninstallOptions) error { - cfg, err := h.getActionCfg(opts.Namespace) + cfg, err := h.getActionCfg(opts.Namespace, opts.LogFn) if err != nil { return fmt.Errorf("get action configuration: %w", err) } @@ -495,29 +509,29 @@ func (h *HelmClient) Render(ctx context.Context, opts InstallOptions) ([][]byte, fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest) } - resources := [][]byte{} - splitManifests := releaseutil.SplitManifests(manifests.String()) - for _, manifest := range splitManifests { - manifest = strings.TrimSpace(manifest) - resources = append(resources, []byte(manifest)) + splitManifests, err := splitManifests(manifests.String()) + if err != nil { + return nil, fmt.Errorf("split manifests: %w", err) } - - return resources, nil + return splitManifests, nil } -func (h *HelmClient) getActionCfg(namespace string) (*action.Configuration, error) { +func (h *HelmClient) getActionCfg(namespace string, logFn LogFn) (*action.Configuration, error) { cfg := &action.Configuration{} - var logFn action.DebugLog - if h.logFn != nil { - logFn = h.logFn - } else { + if logFn == nil { logFn = _logFn } - restClientGetter := &namespacedRESTClientGetter{ - RESTClientGetter: h.restClientGetter, + var restClientGetter genericclioptions.RESTClientGetter + if h.kubernetesEnvSettings != nil { + restClientGetter = h.kubernetesEnvSettings.RESTClientGetter() + } else { + restClientGetter = helmcli.New().RESTClientGetter() // use the default env settings from helm + } + restClientGetter = &namespacedRESTClientGetter{ + RESTClientGetter: restClientGetter, namespace: namespace, } - if err := cfg.Init(restClientGetter, namespace, "secret", logFn); err != nil { + if err := cfg.Init(restClientGetter, namespace, "secret", action.DebugLog(logFn)); err != nil { return nil, fmt.Errorf("init helm configuration: %w", err) } return cfg, nil @@ -610,3 +624,71 @@ func (n *namespacedClientConfig) Namespace() (string, bool, error) { func (n *namespacedClientConfig) ConfigAccess() clientcmd.ConfigAccess { return n.cfg.ConfigAccess() } + +// addKubernetesEnvArgs adds kubernetes environment arguments to the helm command +func (h *HelmClient) addKubernetesEnvArgs(args []string) []string { + if h.kubernetesEnvSettings == nil { + return args + } + + // Add all helm CLI flags from kubernetesEnvSettings + // Based on addKubernetesCLIFlags function below + if h.kubernetesEnvSettings.KubeConfig != "" { + args = append(args, "--kubeconfig", h.kubernetesEnvSettings.KubeConfig) + } + if h.kubernetesEnvSettings.KubeContext != "" { + args = append(args, "--kube-context", h.kubernetesEnvSettings.KubeContext) + } + if h.kubernetesEnvSettings.KubeToken != "" { + args = append(args, "--kube-token", h.kubernetesEnvSettings.KubeToken) + } + if h.kubernetesEnvSettings.KubeAsUser != "" { + args = append(args, "--kube-as-user", h.kubernetesEnvSettings.KubeAsUser) + } + for _, group := range h.kubernetesEnvSettings.KubeAsGroups { + args = append(args, "--kube-as-group", group) + } + if h.kubernetesEnvSettings.KubeAPIServer != "" { + args = append(args, "--kube-apiserver", h.kubernetesEnvSettings.KubeAPIServer) + } + if h.kubernetesEnvSettings.KubeCaFile != "" { + args = append(args, "--kube-ca-file", h.kubernetesEnvSettings.KubeCaFile) + } + if h.kubernetesEnvSettings.KubeTLSServerName != "" { + args = append(args, "--kube-tls-server-name", h.kubernetesEnvSettings.KubeTLSServerName) + } + if h.kubernetesEnvSettings.KubeInsecureSkipTLSVerify { + args = append(args, "--kube-insecure-skip-tls-verify") + } + if h.kubernetesEnvSettings.BurstLimit != 0 { + args = append(args, "--burst-limit", fmt.Sprintf("%d", h.kubernetesEnvSettings.BurstLimit)) + } + if h.kubernetesEnvSettings.QPS != 0 { + args = append(args, "--qps", fmt.Sprintf("%.2f", h.kubernetesEnvSettings.QPS)) + } + + return args +} + +// AddKubernetesCLIFlags adds Kubernetes-related CLI flags to a pflag.FlagSet +// This function is used to configure Kubernetes environment settings +func AddKubernetesCLIFlags(flagSet *pflag.FlagSet, kubernetesEnvSettings *helmcli.EnvSettings) { + // From helm + // https://github.com/helm/helm/blob/v3.18.3/pkg/cli/environment.go#L145-L163 + + flagSet.StringVar(&kubernetesEnvSettings.KubeConfig, "kubeconfig", "", "Path to the kubeconfig file") + flagSet.StringVar(&kubernetesEnvSettings.KubeContext, "kube-context", kubernetesEnvSettings.KubeContext, "Name of the kubeconfig context to use") + flagSet.StringVar(&kubernetesEnvSettings.KubeToken, "kube-token", kubernetesEnvSettings.KubeToken, "Bearer token used for authentication") + flagSet.StringVar(&kubernetesEnvSettings.KubeAsUser, "kube-as-user", kubernetesEnvSettings.KubeAsUser, "Username to impersonate for the operation") + flagSet.StringArrayVar(&kubernetesEnvSettings.KubeAsGroups, "kube-as-group", kubernetesEnvSettings.KubeAsGroups, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.") + flagSet.StringVar(&kubernetesEnvSettings.KubeAPIServer, "kube-apiserver", kubernetesEnvSettings.KubeAPIServer, "The address and the port for the Kubernetes API server") + flagSet.StringVar(&kubernetesEnvSettings.KubeCaFile, "kube-ca-file", kubernetesEnvSettings.KubeCaFile, "The certificate authority file for the Kubernetes API server connection") + flagSet.StringVar(&kubernetesEnvSettings.KubeTLSServerName, "kube-tls-server-name", kubernetesEnvSettings.KubeTLSServerName, "Server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used") + // flagSet.BoolVar(&kubernetesEnvSettings.Debug, "helm-debug", kubernetesEnvSettings.Debug, "enable verbose output") + flagSet.BoolVar(&kubernetesEnvSettings.KubeInsecureSkipTLSVerify, "kube-insecure-skip-tls-verify", kubernetesEnvSettings.KubeInsecureSkipTLSVerify, "If true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure") + // flagSet.StringVar(&kubernetesEnvSettings.RegistryConfig, "helm-registry-config", kubernetesEnvSettings.RegistryConfig, "Path to the Helm registry config file") + // flagSet.StringVar(&kubernetesEnvSettings.RepositoryConfig, "helm-repository-config", kubernetesEnvSettings.RepositoryConfig, "Path to the file containing Helm repository names and URLs") + // flagSet.StringVar(&kubernetesEnvSettings.RepositoryCache, "helm-repository-cache", kubernetesEnvSettings.RepositoryCache, "Path to the directory containing cached Helm repository indexes") + flagSet.IntVar(&kubernetesEnvSettings.BurstLimit, "burst-limit", kubernetesEnvSettings.BurstLimit, "Kubernetes API client-side default throttling limit") + flagSet.Float32Var(&kubernetesEnvSettings.QPS, "qps", kubernetesEnvSettings.QPS, "Queries per second used when communicating with the Kubernetes API, not including bursting") +} diff --git a/pkg/helm/images.go b/pkg/helm/images.go index 7903f361f8..fb9852d5cd 100644 --- a/pkg/helm/images.go +++ b/pkg/helm/images.go @@ -3,7 +3,6 @@ package helm import ( "context" "fmt" - "os" "regexp" "slices" "sort" @@ -11,7 +10,6 @@ import ( "github.com/distribution/reference" "github.com/replicatedhq/embedded-cluster/pkg/helpers" - "helm.sh/helm/v3/pkg/chart" k8syaml "sigs.k8s.io/yaml" ) @@ -69,11 +67,11 @@ func ExtractImagesFromChart(hcli Client, ref string, version string, values map[ return images, nil } -func ExtractMatchesFromChart(hcli Client, ref string, version string, values map[string]interface{}, matchRegexp *regexp.Regexp) ([]string, error) { +func ExtractMatchesFromChart(ctx context.Context, hcli Client, ref string, version string, values map[string]interface{}, matchRegexp *regexp.Regexp) ([]string, error) { parts := strings.Split(ref, "/") name := parts[len(parts)-1] - manifests, err := hcli.Render(context.Background(), InstallOptions{ + manifests, err := hcli.Render(ctx, InstallOptions{ ReleaseName: name, ChartPath: ref, ChartVersion: version, @@ -96,16 +94,6 @@ func ExtractMatchesFromChart(hcli Client, ref string, version string, values map return images, nil } -func GetChartMetadata(hcli Client, ref string, version string) (*chart.Metadata, error) { - chartPath, err := hcli.PullByRef(ref, version) - if err != nil { - return nil, fmt.Errorf("pull: %w", err) - } - defer os.RemoveAll(chartPath) - - return hcli.GetChartMetadata(chartPath) -} - func extractImagesFromK8sManifest(resource []byte) ([]string, error) { images := []string{} diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 5f90ba4aea..8ee55991f3 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -14,13 +14,13 @@ var ( type Client interface { Close() error - AddRepo(repo *repo.Entry) error - Latest(reponame, chart string) (string, error) - Pull(reponame, chart string, version string) (string, error) - PullByRef(ref string, version string) (string, error) - RegistryAuth(server, user, pass string) error - Push(path, dst string) error - GetChartMetadata(chartPath string) (*chart.Metadata, error) + AddRepo(ctx context.Context, repo *repo.Entry) error + Latest(ctx context.Context, reponame, chart string) (string, error) + Pull(ctx context.Context, reponame, chart string, version string) (string, error) + PullByRef(ctx context.Context, ref string, version string) (string, error) + RegistryAuth(ctx context.Context, server, user, pass string) error + Push(ctx context.Context, path, dst string) error + GetChartMetadata(ctx context.Context, chartPath string, version string) (*chart.Metadata, error) ReleaseExists(ctx context.Context, namespace string, releaseName string) (bool, error) Install(ctx context.Context, opts InstallOptions) (*release.Release, error) Upgrade(ctx context.Context, opts UpgradeOptions) (*release.Release, error) diff --git a/pkg/helm/mock_client.go b/pkg/helm/mock_client.go index deeef6d68c..fabe523121 100644 --- a/pkg/helm/mock_client.go +++ b/pkg/helm/mock_client.go @@ -20,38 +20,38 @@ func (m *MockClient) Close() error { return args.Error(0) } -func (m *MockClient) AddRepo(repo *repo.Entry) error { - args := m.Called(repo) +func (m *MockClient) AddRepo(ctx context.Context, repo *repo.Entry) error { + args := m.Called(ctx, repo) return args.Error(0) } -func (m *MockClient) Latest(reponame, chart string) (string, error) { - args := m.Called(reponame, chart) +func (m *MockClient) Latest(ctx context.Context, reponame, chart string) (string, error) { + args := m.Called(ctx, reponame, chart) return args.String(0), args.Error(1) } -func (m *MockClient) Pull(reponame, chart string, version string) (string, error) { - args := m.Called(reponame, chart, version) +func (m *MockClient) Pull(ctx context.Context, reponame, chart string, version string) (string, error) { + args := m.Called(ctx, reponame, chart, version) return args.String(0), args.Error(1) } -func (m *MockClient) PullByRef(ref string, version string) (string, error) { - args := m.Called(ref, version) +func (m *MockClient) PullByRef(ctx context.Context, ref string, version string) (string, error) { + args := m.Called(ctx, ref, version) return args.String(0), args.Error(1) } -func (m *MockClient) RegistryAuth(server, user, pass string) error { - args := m.Called(server, user, pass) +func (m *MockClient) RegistryAuth(ctx context.Context, server, user, pass string) error { + args := m.Called(ctx, server, user, pass) return args.Error(0) } -func (m *MockClient) Push(path, dst string) error { - args := m.Called(path, dst) +func (m *MockClient) Push(ctx context.Context, path, dst string) error { + args := m.Called(ctx, path, dst) return args.Error(0) } -func (m *MockClient) GetChartMetadata(chartPath string) (*chart.Metadata, error) { - args := m.Called(chartPath) +func (m *MockClient) GetChartMetadata(ctx context.Context, chartPath string, version string) (*chart.Metadata, error) { + args := m.Called(ctx, chartPath, version) if args.Get(0) == nil { return nil, args.Error(1) } diff --git a/pkg/helm/output_parser.go b/pkg/helm/output_parser.go new file mode 100644 index 0000000000..3245815284 --- /dev/null +++ b/pkg/helm/output_parser.go @@ -0,0 +1,26 @@ +package helm + +import ( + "regexp" + "strings" +) + +var separator = regexp.MustCompile(`(?:^|\n)\s*---\s*(?:\n|$)`) + +// splitManifests parses multi-doc YAML manifests and returns them as byte slices +func splitManifests(yamlOutput string) ([][]byte, error) { + result := [][]byte{} + + // Make sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly. + manifests := separator.Split(strings.TrimSpace(yamlOutput), -1) + + for _, manifest := range manifests { + manifest = strings.TrimSpace(manifest) + if manifest == "" { + continue + } + result = append(result, []byte(manifest)) + } + + return result, nil +} diff --git a/pkg/helm/output_parser_test.go b/pkg/helm/output_parser_test.go new file mode 100644 index 0000000000..86be09497a --- /dev/null +++ b/pkg/helm/output_parser_test.go @@ -0,0 +1,150 @@ +package helm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_splitManifests(t *testing.T) { + tests := []struct { + name string + yamlInput string + want [][]byte + wantErr bool + }{ + { + name: "multiple YAML documents", + yamlInput: `apiVersion: v1 +kind: Service +metadata: + name: test-service +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment`, + want: [][]byte{ + []byte("apiVersion: v1\nkind: Service\nmetadata:\n name: test-service"), + []byte("apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test-deployment"), + }, + wantErr: false, + }, + { + name: "single YAML document", + yamlInput: `apiVersion: v1 +kind: Service +metadata: + name: test-service`, + want: [][]byte{ + []byte("apiVersion: v1\nkind: Service\nmetadata:\n name: test-service"), + }, + wantErr: false, + }, + { + name: "empty input", + yamlInput: "", + want: [][]byte{}, + wantErr: false, + }, + { + name: "documents with whitespace around separators", + yamlInput: `apiVersion: v1 +kind: ConfigMap +metadata: + name: config1 + + --- + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config2`, + want: [][]byte{ + []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: config1"), + []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: config2"), + }, + wantErr: false, + }, + { + name: "document starting with separator", + yamlInput: `--- +apiVersion: v1 +kind: Service +metadata: + name: test-service +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment`, + want: [][]byte{ + []byte("apiVersion: v1\nkind: Service\nmetadata:\n name: test-service"), + []byte("apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test-deployment"), + }, + wantErr: false, + }, + { + name: "yaml content containing triple dash", + yamlInput: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + message: "This contains --- in the middle but should not split here" +--- +apiVersion: v1 +kind: Secret +metadata: + name: test-secret`, + want: [][]byte{ + []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-config\ndata:\n message: \"This contains --- in the middle but should not split here\""), + []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: test-secret"), + }, + wantErr: false, + }, + { + name: "complex whitespace variations", + yamlInput: ` apiVersion: v1 +kind: ConfigMap +metadata: + name: config1 + + --- + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config2 + + --- + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config3 `, + want: [][]byte{ + []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: config1"), + []byte("apiVersion: v1\nkind: ConfigMap \nmetadata:\n name: config2"), + []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: config3"), + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := splitManifests(tt.yamlInput) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, len(tt.want), len(got)) + for i, expected := range tt.want { + assert.Equal(t, string(expected), string(got[i])) + } + }) + } +} diff --git a/pkg/helpers/command.go b/pkg/helpers/command.go index 8d76079b3d..5ed456b9e9 100644 --- a/pkg/helpers/command.go +++ b/pkg/helpers/command.go @@ -23,26 +23,36 @@ func (h *Helpers) RunCommandWithOptions(opts RunCommandOptions, bin string, args stderr := bytes.NewBuffer(nil) stdout := bytes.NewBuffer(nil) cmd := exec.CommandContext(ctx, bin, args...) + cmd.Stdout = stdout if opts.Stdout != nil { cmd.Stdout = io.MultiWriter(opts.Stdout, stdout) } + if opts.Stdin != nil { cmd.Stdin = opts.Stdin } + cmd.Stderr = stderr if opts.Stderr != nil { cmd.Stderr = io.MultiWriter(opts.Stderr, stderr) } + cmdEnv := cmd.Environ() for k, v := range opts.Env { cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v)) } cmd.Env = cmdEnv + if err := cmd.Run(); err != nil { logrus.Debugf("failed to run command:") logrus.Debugf("stdout: %s", stdout.String()) logrus.Debugf("stderr: %s", stderr.String()) + + // Check if it's a context error and return it instead + if ctx.Err() != nil { + return ctx.Err() + } if stderr.String() != "" { return fmt.Errorf("%w: %s", err, stderr.String()) } diff --git a/pkg/runtimeconfig/interface.go b/pkg/runtimeconfig/interface.go index fd98530474..8db028f027 100644 --- a/pkg/runtimeconfig/interface.go +++ b/pkg/runtimeconfig/interface.go @@ -2,6 +2,7 @@ package runtimeconfig import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + helmcli "helm.sh/helm/v3/pkg/cli" ) // RuntimeConfig defines the interface for managing runtime configuration @@ -47,4 +48,6 @@ type RuntimeConfig interface { SetProxySpec(proxySpec *ecv1beta1.ProxySpec) SetNetworkSpec(networkSpec ecv1beta1.NetworkSpec) SetHostCABundlePath(hostCABundlePath string) + + GetKubernetesEnvSettings() *helmcli.EnvSettings } diff --git a/pkg/runtimeconfig/mock.go b/pkg/runtimeconfig/mock.go index 36c3753d9a..035bf61441 100644 --- a/pkg/runtimeconfig/mock.go +++ b/pkg/runtimeconfig/mock.go @@ -3,6 +3,7 @@ package runtimeconfig import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/stretchr/testify/mock" + helmcli "helm.sh/helm/v3/pkg/cli" ) var _ RuntimeConfig = (*MockRuntimeConfig)(nil) @@ -221,3 +222,12 @@ func (m *MockRuntimeConfig) SetNetworkSpec(networkSpec ecv1beta1.NetworkSpec) { func (m *MockRuntimeConfig) SetHostCABundlePath(hostCABundlePath string) { m.Called(hostCABundlePath) } + +// GetKubernetesEnvSettings mocks the GetKubernetesEnvSettings method +func (m *MockRuntimeConfig) GetKubernetesEnvSettings() *helmcli.EnvSettings { + args := m.Called() + if args.Get(0) == nil { + return nil + } + return args.Get(0).(*helmcli.EnvSettings) +} diff --git a/pkg/runtimeconfig/runtimeconfig.go b/pkg/runtimeconfig/runtimeconfig.go index 9b67097af8..440813f63b 100644 --- a/pkg/runtimeconfig/runtimeconfig.go +++ b/pkg/runtimeconfig/runtimeconfig.go @@ -8,6 +8,7 @@ import ( ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg/helpers" "github.com/sirupsen/logrus" + helmcli "helm.sh/helm/v3/pkg/cli" "sigs.k8s.io/yaml" ) @@ -333,6 +334,14 @@ func (rc *runtimeConfig) SetHostCABundlePath(hostCABundlePath string) { rc.spec.HostCABundlePath = hostCABundlePath } +// GetKubernetesEnvSettings returns a minimal helm environment settings with just the kubeconfig path. +// For Linux target, this builds the settings from the runtime config kubeconfig path. +func (rc *runtimeConfig) GetKubernetesEnvSettings() *helmcli.EnvSettings { + envSettings := helmcli.New() + envSettings.KubeConfig = rc.PathToKubeConfig() + return envSettings +} + func mkdirAll(path string) error { return os.MkdirAll(path, 0755) } diff --git a/tests/integration/util/helm.go b/tests/integration/util/helm.go index d6b8a06c30..1e82a7d3fe 100644 --- a/tests/integration/util/helm.go +++ b/tests/integration/util/helm.go @@ -5,10 +5,18 @@ import ( "testing" "github.com/replicatedhq/embedded-cluster/pkg/helm" + helmcli "helm.sh/helm/v3/pkg/cli" ) func HelmClient(t *testing.T, kubeconfig string) helm.Client { - hcli, err := helm.NewClient(helm.HelmOptions{KubeConfig: kubeconfig}) + envSettings := helmcli.New() + envSettings.KubeConfig = kubeconfig + + hcli, err := helm.NewClient(helm.HelmOptions{ + HelmPath: "helm", // use the helm binary in PATH + KubernetesEnvSettings: envSettings, + K8sVersion: "v1.26.0", + }) if err != nil { t.Fatalf("failed to create helm client: %s", err) } diff --git a/versions.mk b/versions.mk index 2f1da1dc31..b3b2c57067 100644 --- a/versions.mk +++ b/versions.mk @@ -19,6 +19,9 @@ K0S_GO_VERSION = $(K0S_VERSION_1_$(K0S_MINOR_VERSION)) # Troubleshoot Version TROUBLESHOOT_VERSION = v0.122.0 +# Helm Version +HELM_VERSION = v3.19.0 + # FIO Version (for performance testing) FIO_VERSION = 3.41 From acaa1b9a6cf3096937a8fe138f9ae34b714914c7 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 16 Oct 2025 11:43:51 -0700 Subject: [PATCH 2/6] f --- .../kubernetes/install/controller_test.go | 7 +++++++ api/controllers/linux/install/controller_test.go | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/api/controllers/kubernetes/install/controller_test.go b/api/controllers/kubernetes/install/controller_test.go index 5a46fd519f..6d5a4839ff 100644 --- a/api/controllers/kubernetes/install/controller_test.go +++ b/api/controllers/kubernetes/install/controller_test.go @@ -15,6 +15,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" "github.com/replicatedhq/embedded-cluster/pkg-new/kubernetesinstallation" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" @@ -111,6 +112,7 @@ func TestGetInstallationConfig(t *testing.T) { WithInstallation(ki), WithInstallationManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -228,6 +230,7 @@ func TestConfigureInstallation(t *testing.T) { WithStore(mockStore), WithMetricsReporter(mockMetricsReporter), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -299,6 +302,7 @@ func TestGetInstallationStatus(t *testing.T) { controller, err := NewInstallController( WithInstallationManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -425,6 +429,7 @@ func TestSetupInfra(t *testing.T) { appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(getTestReleaseData(&appConfig)), appcontroller.WithAppConfigManager(mockAppConfigManager), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -437,6 +442,7 @@ func TestSetupInfra(t *testing.T) { WithMetricsReporter(mockMetricsReporter), WithReleaseData(getTestReleaseData(&appConfig)), WithStore(mockStore), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -538,6 +544,7 @@ func TestGetInfra(t *testing.T) { controller, err := NewInstallController( WithInfraManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) diff --git a/api/controllers/linux/install/controller_test.go b/api/controllers/linux/install/controller_test.go index 1dfb2f4c29..8d3db7ba2f 100644 --- a/api/controllers/linux/install/controller_test.go +++ b/api/controllers/linux/install/controller_test.go @@ -18,6 +18,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -153,6 +154,7 @@ func TestGetInstallationConfig(t *testing.T) { WithRuntimeConfig(rc), WithInstallationManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -408,6 +410,7 @@ func TestConfigureInstallation(t *testing.T) { WithStore(mockStore), WithMetricsReporter(mockMetricsReporter), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -736,6 +739,7 @@ func TestRunHostPreflights(t *testing.T) { WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), WithMetricsReporter(mockMetricsReporter), WithStore(mockStore), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -808,6 +812,7 @@ func TestGetHostPreflightStatus(t *testing.T) { controller, err := NewInstallController( WithHostPreflightManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -860,6 +865,7 @@ func TestGetHostPreflightOutput(t *testing.T) { controller, err := NewInstallController( WithHostPreflightManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -912,6 +918,7 @@ func TestGetHostPreflightTitles(t *testing.T) { controller, err := NewInstallController( WithHostPreflightManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -968,6 +975,7 @@ func TestGetInstallationStatus(t *testing.T) { controller, err := NewInstallController( WithInstallationManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -1158,6 +1166,7 @@ func TestSetupInfra(t *testing.T) { appcontroller.WithAppConfigManager(mockAppConfigManager), appcontroller.WithAppPreflightManager(mockAppPreflightManager), appcontroller.WithAppReleaseManager(mockAppReleaseManager), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -1173,6 +1182,7 @@ func TestSetupInfra(t *testing.T) { WithReleaseData(getTestReleaseData(&appConfig)), WithLicense([]byte("spec:\n licenseID: test-license\n")), WithStore(mockStore), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -1276,6 +1286,7 @@ func TestGetInfra(t *testing.T) { controller, err := NewInstallController( WithInfraManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -1393,6 +1404,7 @@ func TestProcessAirgap(t *testing.T) { WithStore(mockStore), WithInstallationManager(mockInstallationManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -1463,6 +1475,7 @@ func TestGetAirgapStatus(t *testing.T) { controller, err := NewInstallController( WithAirgapManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) From 810efdcca774aaccb2991741481e443998f8aed8 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 16 Oct 2025 11:49:30 -0700 Subject: [PATCH 3/6] f --- api/controllers/linux/upgrade/controller_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/controllers/linux/upgrade/controller_test.go b/api/controllers/linux/upgrade/controller_test.go index a37026e573..bb72fdd270 100644 --- a/api/controllers/linux/upgrade/controller_test.go +++ b/api/controllers/linux/upgrade/controller_test.go @@ -15,6 +15,7 @@ import ( "github.com/replicatedhq/embedded-cluster/api/internal/store" "github.com/replicatedhq/embedded-cluster/api/types" ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/embedded-cluster/pkg/helm" "github.com/replicatedhq/embedded-cluster/pkg/metrics" "github.com/replicatedhq/embedded-cluster/pkg/release" "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig" @@ -94,6 +95,7 @@ func TestUpgradeInfra(t *testing.T) { appcontroller.WithStateMachine(sm), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -105,6 +107,7 @@ func TestUpgradeInfra(t *testing.T) { WithAppController(appController), WithStore(mockStore), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -192,6 +195,7 @@ func TestGetInfra(t *testing.T) { WithInfraManager(mockInfraManager), WithStore(mockStore), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -336,6 +340,7 @@ func TestReportingHandlers(t *testing.T) { appcontroller.WithStateMachine(sm), appcontroller.WithStore(mockStore), appcontroller.WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + appcontroller.WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -348,6 +353,7 @@ func TestReportingHandlers(t *testing.T) { WithTargetVersion(tt.targetVersion), WithInitialVersion(tt.initialVersion), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -453,6 +459,7 @@ func TestProcessAirgap(t *testing.T) { WithStore(mockStore), WithInstallationManager(mockInstallationManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) @@ -523,6 +530,7 @@ func TestGetAirgapStatus(t *testing.T) { controller, err := NewUpgradeController( WithAirgapManager(mockManager), WithReleaseData(getTestReleaseData(&kotsv1beta1.Config{})), + WithHelmClient(&helm.MockClient{}), ) require.NoError(t, err) From 508a1b0ce460c6c4c6adeeaf5b9985040d341aa7 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 16 Oct 2025 12:20:23 -0700 Subject: [PATCH 4/6] f --- Makefile | 19 ++++++ pkg/helm/client.go | 80 +++++++++------------- pkg/helm/client_test.go | 147 ++++++++++++++++++++++++++++++++++++++++ pkg/helm/interface.go | 2 +- pkg/helm/mock_client.go | 4 +- 5 files changed, 202 insertions(+), 50 deletions(-) diff --git a/Makefile b/Makefile index ca5612f992..c5a72cb313 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,23 @@ output/bins/k0s-override: chmod +x $@ touch $@ +.PHONY: cmd/installer/goods/bins/helm +cmd/installer/goods/bins/helm: + $(MAKE) output/bins/helm-$(HELM_VERSION)-$(ARCH) + cp output/bins/helm-$(HELM_VERSION)-$(ARCH) $@ + touch $@ + +output/bins/helm-%: + mkdir -p output/bins + mkdir -p output/tmp + curl --retry 5 --retry-all-errors -fL -o output/tmp/helm.tar.gz \ + https://get.helm.sh/helm-$(call split-hyphen,$*,1)-$(OS)-$(call split-hyphen,$*,2).tar.gz + tar -xzf output/tmp/helm.tar.gz -C output/tmp + mv output/tmp/$(OS)-$(call split-hyphen,$*,2)/helm $@ + rm -rf output/tmp + chmod +x $@ + touch $@ + .PHONY: cmd/installer/goods/bins/kubectl-support_bundle cmd/installer/goods/bins/kubectl-support_bundle: $(MAKE) output/bins/kubectl-support_bundle-$(TROUBLESHOOT_VERSION)-$(ARCH) @@ -217,6 +234,7 @@ static: cmd/installer/goods/bins/k0s \ cmd/installer/goods/bins/kubectl-support_bundle \ cmd/installer/goods/bins/local-artifact-mirror \ cmd/installer/goods/bins/fio \ + cmd/installer/goods/bins/helm \ cmd/installer/goods/internal/bins/kubectl-kots .PHONY: static-dryrun @@ -226,6 +244,7 @@ static-dryrun: cmd/installer/goods/bins/kubectl-support_bundle \ cmd/installer/goods/bins/local-artifact-mirror \ cmd/installer/goods/bins/fio \ + cmd/installer/goods/bins/helm \ cmd/installer/goods/internal/bins/kubectl-kots .PHONY: embedded-cluster-linux-amd64 diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 5ebead5ea6..405dd8405e 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -3,6 +3,7 @@ package helm import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -204,54 +205,29 @@ func (h *HelmClient) AddRepo(_ context.Context, repo *repo.Entry) error { return nil } -func (h *HelmClient) Latest(_ context.Context, reponame, chart string) (string, error) { - stableConstraint, err := semver.NewConstraint(">0.0.0") // search only for stable versions +func (h *HelmClient) Latest(ctx context.Context, reponame, chart string) (string, error) { + // Use helm search repo with JSON output to find the latest version + args := []string{"search", "repo", fmt.Sprintf("%s/%s", reponame, chart), "--version", ">0.0.0", "--versions", "--output", "json"} + + stdout, _, err := h.executor.ExecuteCommand(ctx, nil, nil, args...) if err != nil { - return "", fmt.Errorf("create stable constraint: %w", err) + return "", fmt.Errorf("helm search repo: %w", err) } - for _, repository := range h.repos { - if repository.Name != reponame { - continue - } - chrepo, err := repo.NewChartRepository(repository, getters) - if err != nil { - return "", fmt.Errorf("create chart repo: %w", err) - } - chrepo.CachePath = h.tmpdir - idx, err := chrepo.DownloadIndexFile() - if err != nil { - return "", fmt.Errorf("download index file: %w", err) - } - - repoidx, err := repo.LoadIndexFile(idx) - if err != nil { - return "", fmt.Errorf("load index file: %w", err) - } - - versions, ok := repoidx.Entries[chart] - if !ok { - return "", fmt.Errorf("chart %s not found", chart) - } - - if len(versions) == 0 { - return "", fmt.Errorf("chart %s has no versions", chart) - } - - for _, version := range versions { - v, err := semver.NewVersion(version.Version) - if err != nil { - continue - } - - if stableConstraint.Check(v) { - return version.Version, nil - } - } + // Parse JSON output + var results []struct { + Version string `json:"version"` + } + if err := json.Unmarshal([]byte(stdout), &results); err != nil { + return "", fmt.Errorf("parse helm search json output: %w", err) + } - return "", fmt.Errorf("no stable version found for chart %s", chart) + if len(results) == 0 { + return "", fmt.Errorf("no charts found for %s/%s", reponame, chart) } - return "", fmt.Errorf("repository %s not found", reponame) + + // Return the version of the first result (latest version due to --versions flag) + return results[0].Version, nil } func (h *HelmClient) PullByRefWithRetries(ctx context.Context, ref string, version string, tries int) (string, error) { @@ -314,13 +290,23 @@ func (h *HelmClient) Push(_ context.Context, path, dst string) error { return up.UploadTo(path, dst) } -func (h *HelmClient) GetChartMetadata(_ context.Context, chartPath string, version string) (*chart.Metadata, error) { - chartRequested, err := loader.Load(chartPath) +func (h *HelmClient) GetChartMetadata(ctx context.Context, ref string, version string) (*chart.Metadata, error) { + // Use helm show chart to get chart metadata + args := []string{"show", "chart", ref} + if version != "" { + args = append(args, "--version", version) + } + + stdout, _, err := h.executor.ExecuteCommand(ctx, nil, nil, args...) if err != nil { - return nil, fmt.Errorf("load chart: %w", err) + return nil, fmt.Errorf("helm show chart: %w", err) } - return chartRequested.Metadata, nil + var metadata chart.Metadata + if err := k8syaml.Unmarshal([]byte(stdout), &metadata); err != nil { + return nil, fmt.Errorf("parse chart metadata YAML: %w", err) + } + return &metadata, nil } // reference: https://github.com/helm/helm/blob/0d66425d9a745d8a289b1a5ebb6ccc744436da95/cmd/helm/upgrade.go#L122-L125 diff --git a/pkg/helm/client_test.go b/pkg/helm/client_test.go index 7e3e7f6ece..e30266d717 100644 --- a/pkg/helm/client_test.go +++ b/pkg/helm/client_test.go @@ -3,10 +3,157 @@ package helm import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" k8syaml "sigs.k8s.io/yaml" ) +func TestHelmClient_Latest(t *testing.T) { + tests := []struct { + name string + reponame string + chart string + setupMock func(*MockBinaryExecutor) + want string + wantErr bool + }{ + { + name: "valid JSON response", + reponame: "myrepo", + chart: "mychart", + setupMock: func(m *MockBinaryExecutor) { + jsonOutput := `[ + { + "name": "myrepo/mychart", + "version": "1.2.3", + "app_version": "1.2.3", + "description": "A test chart" + } + ]` + m.On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, + []string{"search", "repo", "myrepo/mychart", "--version", ">0.0.0", "--versions", "--output", "json"}). + Return(jsonOutput, "", nil) + }, + want: "1.2.3", + wantErr: false, + }, + { + name: "empty results", + reponame: "myrepo", + chart: "nonexistent", + setupMock: func(m *MockBinaryExecutor) { + m.On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, + []string{"search", "repo", "myrepo/nonexistent", "--version", ">0.0.0", "--versions", "--output", "json"}). + Return("[]", "", nil) + }, + want: "", + wantErr: true, + }, + { + name: "helm command fails", + reponame: "myrepo", + chart: "mychart", + setupMock: func(m *MockBinaryExecutor) { + m.On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, + []string{"search", "repo", "myrepo/mychart", "--version", ">0.0.0", "--versions", "--output", "json"}). + Return("", "repo not found", assert.AnError) + }, + want: "", + wantErr: true, + }, + { + name: "invalid JSON response", + reponame: "myrepo", + chart: "mychart", + setupMock: func(m *MockBinaryExecutor) { + m.On("ExecuteCommand", mock.Anything, mock.Anything, mock.Anything, + []string{"search", "repo", "myrepo/mychart", "--version", ">0.0.0", "--versions", "--output", "json"}). + Return("invalid json", "", nil) + }, + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockExec := &MockBinaryExecutor{} + tt.setupMock(mockExec) + + client := &HelmClient{ + helmPath: "/usr/local/bin/helm", + executor: mockExec, + } + + got, err := client.Latest(t.Context(), tt.reponame, tt.chart) + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + mockExec.AssertExpectations(t) + }) + } +} + +func TestHelmClient_GetChartMetadata(t *testing.T) { + tests := []struct { + name string + setupMock func(*MockBinaryExecutor) + chartPath string + version string + wantErr bool + }{ + { + name: "successful metadata retrieval", + setupMock: func(m *MockBinaryExecutor) { + m.On("ExecuteCommand", + mock.Anything, // context + mock.Anything, // env + mock.Anything, // LogFn + []string{"show", "chart", "/path/to/chart", "--version", "1.0.0"}, + ).Return(`apiVersion: v2 +name: test-chart +description: A test chart +type: application +version: 1.0.0 +appVersion: "1.0.0"`, "", nil) + }, + chartPath: "/path/to/chart", + version: "1.0.0", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockExec := &MockBinaryExecutor{} + tt.setupMock(mockExec) + + client := &HelmClient{ + helmPath: "/usr/local/bin/helm", + executor: mockExec, + } + + metadata, err := client.GetChartMetadata(t.Context(), tt.chartPath, tt.version) + + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, "test-chart", metadata.Name) + assert.Equal(t, "1.0.0", metadata.Version) + assert.Equal(t, "1.0.0", metadata.AppVersion) + mockExec.AssertExpectations(t) + }) + } +} + func Test_cleanUpGenericMap(t *testing.T) { tests := []struct { name string diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 8ee55991f3..61a13b1c79 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -20,7 +20,7 @@ type Client interface { PullByRef(ctx context.Context, ref string, version string) (string, error) RegistryAuth(ctx context.Context, server, user, pass string) error Push(ctx context.Context, path, dst string) error - GetChartMetadata(ctx context.Context, chartPath string, version string) (*chart.Metadata, error) + GetChartMetadata(ctx context.Context, ref string, version string) (*chart.Metadata, error) ReleaseExists(ctx context.Context, namespace string, releaseName string) (bool, error) Install(ctx context.Context, opts InstallOptions) (*release.Release, error) Upgrade(ctx context.Context, opts UpgradeOptions) (*release.Release, error) diff --git a/pkg/helm/mock_client.go b/pkg/helm/mock_client.go index fabe523121..b107dc8993 100644 --- a/pkg/helm/mock_client.go +++ b/pkg/helm/mock_client.go @@ -50,8 +50,8 @@ func (m *MockClient) Push(ctx context.Context, path, dst string) error { return args.Error(0) } -func (m *MockClient) GetChartMetadata(ctx context.Context, chartPath string, version string) (*chart.Metadata, error) { - args := m.Called(ctx, chartPath, version) +func (m *MockClient) GetChartMetadata(ctx context.Context, ref string, version string) (*chart.Metadata, error) { + args := m.Called(ctx, ref, version) if args.Get(0) == nil { return nil, args.Error(1) } From b6b4f7f4a9bf03702887c275301c9bbef844c595 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 16 Oct 2025 12:22:14 -0700 Subject: [PATCH 5/6] f --- proposals/helm_binary_migration.md | 365 +++++++++++++++++ proposals/helm_binary_migration_research.md | 427 ++++++++++++++++++++ 2 files changed, 792 insertions(+) create mode 100644 proposals/helm_binary_migration.md create mode 100644 proposals/helm_binary_migration_research.md diff --git a/proposals/helm_binary_migration.md b/proposals/helm_binary_migration.md new file mode 100644 index 0000000000..f152e2a772 --- /dev/null +++ b/proposals/helm_binary_migration.md @@ -0,0 +1,365 @@ +# Helm Binary Migration Proposal + +## Executive Summary + +Replace the Helm Go SDK with direct helm binary execution for **all Embedded Cluster installs (V2 and V3)**. This approach aligns with KOTS' existing helm binary usage, reducing migration complexity and potential regressions when porting functionality from KOTS. + +## Problem Statement + +The current Helm Go SDK integration presents several challenges: +- **Migration Complexity**: Using the SDK instead of the binary adds complexity and potential for regressions when migrating from KOTS, which uses the helm binary directly. +- **Compatibility Issues**: SDK behavior may diverge from CLI behavior in edge cases. +- **Debugging Complexity**: SDK errors are harder to diagnose than CLI output. +- **Stability**: The Helm CLI interface seems to be more commonly used and robust than the SDK + +## Proposed Solution + +### Architecture Overview + +This proposal replaces the Helm Go SDK with direct binary execution while maintaining the exact same API interface. The change is transparent to all consumers and only affects the internal implementation. + +#### Current State (SDK-based) +``` +App/Installer → pkg/helm/interface.go → pkg/helm/client.go → Helm Go SDK → Kubernetes API +``` + +#### Proposed State (Binary-based) +``` +App/Installer → pkg/helm/interface.go → pkg/helm/client.go → helm binary → Kubernetes API +``` + +### Implementation Architecture + +**Application Layer (No Changes)** +• api/, cmd/embedded-cluster/, etc. +• All existing code continues to work unchanged + +↓ + +**Helm Interface (No Changes)** +• pkg/helm/interface.go maintains same Client interface +• Same method signatures, return types, and error handling + +↓ + +**Unified Binary Implementation:** +• pkg/helm/client.go (refactored to use helm binary) +• HelmClient struct (same name, different implementation) +• Command execution via helpers.RunCommand +• JSON output parsing with stdout/stderr capture +• Error handling and logging +• binaryExecutor interface (mockable for tests) +• Uses helm binary from cmd/installer/goods/materializer.go + +### Migration Strategy +**Single-phase migration**: Refactor existing `pkg/helm/client.go` to use binary execution instead of Go SDK for **both V2 and V3** installs. + +- Replace SDK calls with helm binary execution via helpers.RunCommand +- Maintain exact same public interface and behavior +- Helm binary availability handled by existing materializer functionality + +### Key Components + +#### 1. binaryExecutor Interface (Mockable) +```go +type binaryExecutor interface { + // ExecuteCommand runs a command and returns stdout, stderr, and error + ExecuteCommand(ctx context.Context, env map[string]string, bin string, args ...string) (stdout string, stderr string, err error) +} + +type commandExecutor struct{} + +func (c *commandExecutor) ExecuteCommand(ctx context.Context, env map[string]string, bin string, args ...string) (string, string, error) { + var stdoutBuf, stderrBuf bytes.Buffer + + err := helpers.RunCommandWithOptions(helpers.RunCommandOptions{ + Context: ctx, + Stdout: &stdoutBuf, + Stderr: &stderrBuf, + Env: env, + }, bin, args...) + + return stdoutBuf.String(), stderrBuf.String(), err +} +``` + +#### 2. HelmClient Structure (Refactored) +```go +type HelmClient struct { + helmPath string // Path to helm binary + executor binaryExecutor // Mockable executor + tmpdir string // Temporary directory for helm + kversion *semver.Version // Kubernetes version + restClientGetter genericclioptions.RESTClientGetter // REST client getter + registryConfig string // Registry config path for OCI + repositories []*repo.Entry // Repository entries + logFn action.DebugLog // Debug logging function + airgapPath string // Airgap path where charts are stored +} +``` + +## New Subagents / Commands + +**No new subagents or commands will be created.** This proposal only changes the internal implementation of the existing Helm client. + +## Database + +**No database changes required.** This proposal only affects in-memory operations and command execution. + +## Implementation plan + +### Files to Create/Modify + +#### New Files: +- `pkg/helm/binary_executor.go` - Executor interface and implementation (~100 lines) +- `pkg/helm/binary_executor_mock.go` - Generated mock for testing (~50 lines) +- `pkg/helm/output_parser.go` - Parse helm command outputs (~300 lines) +- `pkg/helm/output_parser_test.go` - Parser tests (~200 lines) + +#### Modified Files: +- `pkg/helm/client.go` - Complete refactor from SDK to binary execution (~800 lines, replacing 613 existing) +- `pkg/helm/client_test.go` - Update tests to use mock executor (~300 lines modified) +- `pkg/helm/values_test.go` - Update for binary client (~50 lines modified) +- `pkg/helm/interface.go` - No changes (same interface) + +#### Files Using Helm Client (No Changes Required): +- **70+ files** across codebase continue to work unchanged +- All addons, API managers, CLI commands, extensions maintain compatibility + +### Function to Binary Command Mapping + +| SDK Function | Helm Binary Command | Options Preserved | Output Parsing Required | +|--------------|-------------------|-------------------|------------------------| +| `Install()` | `helm install` | ✓ All | Release JSON | +| `Upgrade()` | `helm upgrade` | ✓ All including `--force` | Release JSON | +| `Uninstall()` | `helm uninstall` | ✓ `--wait`, `--no-hooks` | Success message | +| `ReleaseExists()` | `helm list` | `--namespace`, `--filter` | JSON list | +| `Render()` | `helm template` | ✓ All options | YAML manifests | +| `Pull()` | `helm pull` | `--version`, `--repo` | File path | +| `PullByRef()` | `helm pull` | `--version` for OCI | File path | +| `Push()` | `helm push` | OCI destination | Success message | +| `RegistryAuth()` | `helm registry login` | `--username`, `--password` | Success message | +| `AddRepo()` | `helm repo add` | `--force-update`, auth | Success message | +| `Latest()` | `helm search repo` | `--version ">0.0.0"` | Version string | +| `GetChartMetadata()` | `helm show chart` | Chart path | Chart.yaml parsing | + +### Detailed Option Preservation + +#### Install Options +```bash +helm install [NAME] [CHART] \ + --namespace \ + --create-namespace \ + --wait \ + --wait-for-jobs \ + --timeout \ + --values \ + --set key=value \ + --atomic=false \ # Explicitly false for install + --replace \ + --output json +``` + +#### Upgrade Options +```bash +helm upgrade [NAME] [CHART] \ + --namespace \ + --wait \ + --wait-for-jobs \ + --timeout \ + --values \ + --set key=value \ + --atomic \ + --force \ # Critical: User noticed this was missing + --output json +``` + +#### Uninstall Options +```bash +helm uninstall [NAME] \ + --namespace \ + --wait \ + --timeout \ + --ignore-not-found +``` + +### Implementation + +```go +// Example: Install implementation +func (c *HelmClient) Install(ctx context.Context, opts InstallOptions) (*release.Release, error) { + args := []string{"install", opts.ReleaseName} + + // Handle chart source + if c.airgapPath != "" { + // Use chart from airgap path + } else if !strings.HasPrefix(opts.ChartPath, "/") { + // Pull chart with retries (includes oci:// prefix) + } else { + // Use local chart path + } + + // Add all helm install flags: --namespace, --create-namespace, --wait, etc. + // Add values file if provided + // Add labels if provided + + // Execute helm command + stdout, stderr, err := c.executor.ExecuteCommand(ctx, nil, c.helmPath, args...) + + // Parse release from JSON output + return &release, nil +} + +// Example: ReleaseExists implementation +func (c *HelmClient) ReleaseExists(ctx context.Context, namespace, name string) (bool, error) { + // Build: helm list --namespace X --filter "^name$" --output json + // Execute command and parse JSON list + // Check if release exists and is not uninstalled + return exists, nil +} +``` + +### External Contracts + +No changes to external APIs. The binary implementation maintains exact compatibility with existing interface. + +## Testing + +### Unit Tests +```go +// Using mockery-generated mock +func TestHelmClient_Install(t *testing.T) { + mockExec := new(MockBinaryExecutor) + client := &HelmClient{ + helmPath: "/usr/local/bin/helm", + executor: mockExec, + } + + mockExec.On("ExecuteCommand", + mock.Anything, // context + mock.Anything, // env + "/usr/local/bin/helm", + "install", "myrelease", "/path/to/chart", + "--namespace", "default", + "--create-namespace", + "--wait", + "--wait-for-jobs", + "--timeout", "5m0s", + "--replace", + "--output", "json", + ).Return(testReleaseJSON, "", nil) + + release, err := client.Install(context.Background(), InstallOptions{ + ReleaseName: "myrelease", + ChartPath: "/path/to/chart", + Namespace: "default", + Timeout: 5 * time.Minute, + }) + + require.NoError(t, err) + assert.Equal(t, "myrelease", release.Name) + mockExec.AssertExpectations(t) +} +``` + +### Integration Tests +- Execution with SDK and binary implementations +- Output comparison for all operations +- Airgap mode testing + +### Test Data and Fixtures +- Sample chart archives +- Mock release JSON outputs +- Error response samples +- Repository index files + +## Backward compatibility + +### Full API Compatibility +- Exact same Client interface maintained +- All return types preserved +- No changes to function signatures + +### Data Format Compatibility +- JSON output parsing for structured data +- YAML manifest compatibility for Render() +- Repository cache format unchanged + +## Migrations + +**Helm binary must be embedded in installer.** The existing materializer functionality in `cmd/installer/goods/materializer.go` will handle helm binary availability similar to other binaries. + +### Required Changes: +1. **Embed helm binary** in the embedded-cluster installer binary +2. **Materialize helm binary** during installation to same directory we materialize other embedded binaries +3. **Enable binary client** for all installs (v2 and v3) +4. **Maintain exact same interface** for all consuming code + +### Implementation: +- Verify helm binary is materialized during installation +- Replace all SDK calls with `helpers.RunCommand` execution +- Parse command outputs to maintain existing return types + +## Trade-offs + +### Optimizing For: +- **Maintainability**: Simpler codebase without SDK dependencies +- **Compatibility**: Guaranteed parity with helm CLI behavior +- **Debuggability**: Clear command output in logs + +## Alternative solutions considered + +### 1. Upgrade Helm SDK to Latest Version +- **Rejected**: Continues maintenance burden, doesn't solve core issues +- **Risk**: Breaking changes in SDK API + +### 2. Fork Helm SDK +- **Rejected**: Massive maintenance burden +- **Risk**: Divergence from upstream + +### 4. Hybrid Approach (SDK for some, binary for others) +- **Rejected**: Would require maintaining both SDK and binary implementations +- **Complexity**: + - Need to carefully track which functions use which implementation + - More complex testing matrix to validate both paths + - Increased cognitive load for developers to remember which path to use + - Potential for subtle bugs when functions interact across implementations + +## Research + +### Prior Art in Codebase +- [Helm Binary Migration Research](./helm_binary_migration_research.md) +- `pkg/helpers/RunCommand` - Established pattern for command execution +- `pkg/helpers/firewalld/client.go` - Example of binary wrapper pattern +- Mock patterns in `pkg/helpers/mock.go` + +### External References +- [Helm CLI Documentation](https://helm.sh/docs/helm/) +- [Kubernetes SIG-Apps Helm discussions](https://github.com/kubernetes/community/tree/master/sig-apps) +- [ArgoCD Helm Binary Integration](https://github.com/argoproj/argo-cd/tree/master/util/helm) +- [Flux Helm Controller](https://github.com/fluxcd/helm-controller) - Uses helm SDK but considering binary + +### Prototypes and Learnings +- Spike: JSON output parsing - All commands support --output json +- Spike: Concurrent execution - No file lock issues with separate processes +- Test: Repository cache compatibility verified between SDK and binary + +## Checkpoints (PR plan) + +### PR 1: Foundation & Utilities +- `pkg/helm/binary_executor.go` - Interface and implementation +- Generate `pkg/helm/binary_executor_mock.go` using github.com/stretchr/testify/mock +- `pkg/helm/output_parser.go` - Parse JSON and YAML outputs from helm commands +- Unit tests for executor and parser components + +### PR 2: Client Refactor +- Complete refactor of `pkg/helm/client.go` - replace SDK with binary execution +- All 13 interface methods implemented with binary commands +- Comprehensive error handling with stdout/stderr capture and logging +- Update `pkg/helm/client_test.go` to use mock executor +- Update `pkg/helm/values_test.go` for binary client +- Remove unused Helm Go SDK imports and dependencies + +Each PR will include: +- Complete implementation for its scope +- Unit and integration tests diff --git a/proposals/helm_binary_migration_research.md b/proposals/helm_binary_migration_research.md new file mode 100644 index 0000000000..44d92915f4 --- /dev/null +++ b/proposals/helm_binary_migration_research.md @@ -0,0 +1,427 @@ +--- +date: 2025-08-28T21:30:00-07:00 +researcher: claude-code +git_commit: 7e03295e +branch: salah/sc-128060/add-missing-functionality-for-the-image-pull +repository: replicatedhq/embedded-cluster +topic: "Helm Client Usage Analysis for Go SDK to Binary Migration" +tags: [research, codebase, helm, migration, v2, v3] +status: complete +last_updated: 2025-08-28 +last_updated_by: claude-code +--- + +# Helm Binary Migration Research + +**Date**: 2025-08-28T21:30:00-07:00 +**Researcher**: claude-code +**Git Commit**: 7e03295e +**Branch**: salah/sc-128060/add-missing-functionality-for-the-image-pull +**Repository**: replicatedhq/embedded-cluster + +## Research Question +Analyze the current Helm client usage across the entire embedded-cluster codebase to understand the scope of migrating from Helm Go SDK to Helm binary for both v2 and v3. Focus on understanding what needs to change when we refactor the existing client.go to use binary execution instead of the Go SDK. + +## Executive Summary +The embedded-cluster codebase has extensive Helm usage across **70 files** with a well-defined interface and complex dependency patterns. The migration scope includes **613 lines** in the core client implementation, **32 test files** with mocking, and critical usage across all major components including addons, extensions, API managers, and CLI operations. The analysis reveals clear v2/v3 usage patterns and identifies **3 critical Helm Go SDK types** that must be preserved in the interface. + +## Core Implementation Analysis + +### pkg/helm/client.go (613 lines) +**Primary implementation**: Complete Helm v3 Go SDK wrapper +- **Interface**: `pkg/helm/interface.go` defines the `Client` interface with 13 methods +- **Dependencies**: 70 files across the codebase depend on the Helm package + +**Key Helm SDK dependencies** (15 imports from `helm.sh/helm/v3/pkg/*`): +- `action` - Install, Upgrade, Uninstall, History, Configuration +- `chart` - Chart metadata and loading (`chart.Metadata`, `chart.Chart`) +- `release` - Release management (`release.Release`, `release.Status`) +- `repo` - Repository management (`repo.Entry`, `repo.File`) +- `downloader` - Chart downloading (`downloader.ChartDownloader`) +- `registry` - OCI registry support (`registry.Client`) +- `getter` - Chart fetching (`getter.Providers`) +- `pusher` - Chart uploading (`pusher.Providers`) + +### pkg/helm/interface.go (43 lines) +**Client interface**: 13 methods defining the complete Helm contract +- **Factory pattern**: ClientFactory with SetClientFactory for dependency injection +- **Critical method signatures**: + - `Install(ctx, InstallOptions) (*release.Release, error)` + - `Upgrade(ctx, UpgradeOptions) (*release.Release, error)` + - `Render(ctx, InstallOptions) ([][]byte, error)` + - `GetChartMetadata(chartPath) (*chart.Metadata, error)` + +## File Usage Distribution + +### Direct Helm Package Consumers (70 files) + +#### Addons (30 files): All infrastructure components +- **Components**: openebs, velero, seaweedfs, registry, embeddedclusteroperator, adminconsole +- **Pattern**: Each addon has install.go, upgrade.go, metadata.go, values.go +- **Usage**: Direct calls to helm.Client for installing/upgrading cluster components + +#### API Managers (8 files): V3 application deployment and infrastructure +- **Location**: `api/internal/managers/app/` +- **Purpose**: Deploy customer applications via Helm charts +- **Features**: Template rendering, install manager, release management + +#### CLI Commands (4 files): install, join, restore, enable_ha +- **Install Command**: `cmd/installer/cli/install.go` +- **Join Command**: `cmd/installer/cli/join.go` +- **Restore Command**: `cmd/installer/cli/restore.go` +- **Enable HA**: `cmd/installer/cli/enable_ha.go` + +#### Extensions (3 files): Third-party extension management +- **Location**: `pkg/extensions/` +- **Purpose**: Install and upgrade third-party extensions + +#### Build Tools (7 files): Chart packaging for airgap bundles +- **Location**: `cmd/buildtools/` +- **Purpose**: Pull and package charts for airgap bundles +- **Components**: velero.go, seaweedfs.go, registry.go, openebs.go, embeddedclusteroperator.go, adminconsole.go + +#### Operator (2 files): Automated upgrade jobs +- **Location**: `operator/pkg/upgrade/upgrade.go`, `operator/pkg/cli/upgrade_job.go` +- **Purpose**: Automated upgrades of cluster components + +#### Tests (32 files): Integration and dryrun tests +- **Unit Tests**: Mock implementations in tests/dryrun/ +- **Integration Tests**: tests/integration/util/helm.go +- **Test Patterns**: Heavy use of mock.Mock for helm.Client + +### Helm SDK Direct Imports (16 files) +Key files that directly import `helm.sh/helm/v3/pkg/*`: +- `pkg/helm/client.go` - Core implementation +- `pkg/helm/interface.go` - Type definitions +- `pkg/helm/mock_client.go` - Test mocking +- `api/internal/managers/app/release/util.go` - Release utilities +- `cmd/buildtools/*.go` - Chart build tools + +## Helm Operations Analysis + +### Current SDK Operations +The current implementation uses Helm v3 Go SDK for: + +#### 1. Release Management Operations +- **Install** - 30+ usage sites across addons and applications + - Pattern: `hcli.Install(ctx, helm.InstallOptions{...})` + - Return: `*release.Release` with complete release metadata + +- **Upgrade** - 25+ usage sites for component updates + - Pattern: `hcli.Upgrade(ctx, helm.UpgradeOptions{...})` + - Critical option: `Force: true` for upgrades + +- **Uninstall** - 10+ usage sites for cleanup operations + - Pattern: `hcli.Uninstall(ctx, helm.UninstallOptions{...})` + - Options: `Wait`, `IgnoreNotFound` + +- **ReleaseExists** - 15+ usage sites for state checking + - Pattern: `exists, err := hcli.ReleaseExists(ctx, namespace, name)` + - Critical for upgrade/install decision logic + +#### 2. Chart Management Operations +- **Pull/PullByRef** - 20+ usage sites for chart downloading + - Supports both traditional repos and OCI registries + - Retry logic with `PullByRefWithRetries` + +- **Render** - 10+ usage sites for template rendering + - Pattern: `manifests, err := hcli.Render(ctx, opts)` + - Returns `[][]byte` of rendered YAML manifests + +- **GetChartMetadata** - 8+ usage sites for metadata extraction + - Returns `*chart.Metadata` with version, dependencies info + +#### 3. Repository Management +- **AddRepo** - Add Helm repositories +- **RegistryAuth** - Authenticate to OCI registries +- **Latest** - Find latest stable chart version + +## Critical Use Cases + +### 1. Addon Installation (Core Infrastructure) +**Files**: All addon packages (openebs, velero, seaweedfs, registry, embeddedclusteroperator, adminconsole) +- **Pattern**: Each addon has install.go, upgrade.go, metadata.go, values.go +- **Usage**: Direct calls to helm.Client for installing/upgrading cluster components + +### 2. Application Deployment (V3 API) +**Location**: `api/internal/managers/app/` +- **Purpose**: Deploy customer applications via Helm charts +- **Features**: Template rendering, install manager, release management + +### 3. Build Tools +**Location**: `cmd/buildtools/` +- **Purpose**: Pull and package charts for airgap bundles +- **Components**: velero.go, seaweedfs.go, registry.go, openebs.go, embeddedclusteroperator.go, adminconsole.go + +### 4. CLI Operations +- **Install Command**: `cmd/installer/cli/install.go` +- **Join Command**: `cmd/installer/cli/join.go` +- **Restore Command**: `cmd/installer/cli/restore.go` +- **Enable HA**: `cmd/installer/cli/enable_ha.go` + +### 5. Operator Upgrade Jobs +**Location**: `operator/pkg/upgrade/upgrade.go`, `operator/pkg/cli/upgrade_job.go` +- **Purpose**: Automated upgrades of cluster components + +### 6. Extensions System +**Location**: `pkg/extensions/` +- **Purpose**: Install and upgrade third-party extensions + +## V2 vs V3 Usage Patterns + +### V3-Specific Features +- **Environment variable**: `ENABLE_V3=1` controls V3 feature activation +- **Usage locations**: + - `cmd/installer/cli/flags.go` - V3 feature flag detection + - `cmd/installer/cli/install.go` - V3 manager experience defaults +- **V3 components**: + - API managers for kubernetes/linux infrastructure + - Application deployment managers + - New manager experience vs legacy installer flow + +### V2/Legacy Pattern +- **Traditional workflow**: Direct CLI-driven installation without API managers +- **Addon installation**: Same Helm client usage for both V2 and V3 +- **Backwards compatibility**: All existing Helm operations work in both modes + +## Critical Dependencies on Helm Go SDK Types + +### Return Value Dependencies +1. **`*release.Release`** - Used by Install() and Upgrade() + - Contains: Name, Namespace, Version, Status, Manifest, Hooks + - **Usage**: Status checking, rollback decisions, manifest extraction + +2. **`*chart.Metadata`** - Used by GetChartMetadata() + - Contains: Name, Version, Dependencies, Annotations + - **Usage**: Version validation, dependency checking + +3. **`[][]byte`** - Used by Render() + - Contains: Rendered YAML manifests as byte slices + - **Usage**: Template processing, manifest application + +### Parameter Dependencies +1. **`*repo.Entry`** - Used by AddRepo() + - Contains: Name, URL, Username, Password, CertFile, KeyFile + - **Usage**: Repository configuration, authentication + +## Special Implementation Considerations + +### Airgap Support +- **Pattern**: `airgapPath` field enables offline chart loading +- **Logic**: Load from `{airgapPath}/{releaseName}-{chartVersion}.tgz` +- **Scope**: All addons and application deployments support airgap +- Current implementation handles airgap via `airgapPath` field in HelmClient +- Charts are loaded from local filesystem in airgap mode + +### Registry Authentication +- **OCI support**: Full OCI registry integration via `registry.Client` +- **Authentication**: Basic auth, registry login support +- **Usage**: Private chart repositories, enterprise scenarios +- Uses registry.Client for OCI authentication +- Supports basic auth via `RegistryAuth()` method +- Critical for private registry scenarios + +### Kubernetes Version Compatibility +- **K0s integration**: `kversion` field for template rendering compatibility +- **Template context**: Correct API versions based on cluster version +- K0s version awareness via `kversion` field +- Used for proper template rendering with correct API versions + +### Error Handling & Retry Logic +- **Retry pattern**: `PullByRefWithRetries(ctx, ref, version, 3)` +- **Error wrapping**: Comprehensive error context throughout +- **Debug logging**: Configurable debug output via `LogFn` +- Retry logic for chart pulls (`PullByRefWithRetries`) +- Detailed error wrapping throughout +- Debug logging via customizable LogFn + +## Test Infrastructure Analysis + +### Mock Usage (32 test files) +- **Primary mock**: `pkg/helm/mock_client.go` (94 lines) +- **Test pattern**: `testify/mock` based mocking +- **Critical mocked operations**: + - Install/Upgrade returning mock `*release.Release` + - Render returning mock `[][]byte` manifests + - GetChartMetadata returning mock `*chart.Metadata` + +### Integration Tests +- **Utility**: `tests/integration/util/helm.go` - HelmClient factory for tests +- **Addon integration tests**: 8 files testing real Helm operations +- **Dryrun tests**: 5 files using mocked clients + +## Architecture Insights + +### Interface Stability Requirements +- **13 method signatures** must remain unchanged for 70+ consuming files +- **3 critical return types** (`*release.Release`, `*chart.Metadata`, `[][]byte`) must be preserved +- **Factory pattern** with `SetClientFactory` enables testing and dependency injection +- Must maintain exact same Client interface +- 70+ files depend on this interface +- Breaking changes would cascade throughout codebase + +### Component Dependencies +``` +CLI Commands → Helm Interface ← API Managers + ↓ ↓ ↓ + Addons → Helm Client ← Extensions + ↓ ↓ ↓ +Build Tools → SDK Implementation ← Tests +``` + +### Operation Flow Patterns +1. **Installation Flow**: NewClient → AddRepo → Pull → Install → Close +2. **Upgrade Flow**: NewClient → ReleaseExists → Pull → Upgrade → Close +3. **Template Flow**: NewClient → Pull → Render → Close +4. **Metadata Flow**: NewClient → Pull → GetChartMetadata → Close + +## Interface Consumers + +### Direct Consumers (via helm.NewClient) +1. CLI commands (install, join, restore, enable_ha) +2. Operator upgrade jobs +3. Integration test utilities +4. Build tools + +### Indirect Consumers (via dependency injection) +1. Addons package (receives helm.Client) +2. Extensions package +3. App managers +4. Infrastructure managers + +## Code References + +### Core Files (Migration Critical) +- `pkg/helm/client.go:1-613` - Complete SDK implementation to replace +- `pkg/helm/interface.go:15-29` - Client interface definition (must preserve) +- `pkg/helm/mock_client.go:1-94` - Mock implementation to update + +### High-Impact Usage Sites +- `pkg/addons/*/install.go` - All addon installation logic (30 files) +- `pkg/extensions/util.go:41-89` - Extension install/upgrade/uninstall +- `api/internal/managers/app/install/install.go` - V3 application deployment +- `cmd/installer/cli/install.go:200+` - CLI installation workflow + +### Test Coverage +- `tests/dryrun/*_test.go` - 5 files with extensive mock usage +- `pkg/addons/*/integration/*_test.go` - 8 files with real Helm operations +- `api/integration/*/install/*_test.go` - 4 files testing install managers + +## Migration Complexity Assessment + +### Binary Management Challenges +1. **Distribution**: How to package/ship helm binary +2. **Versioning**: Ensure consistent helm version +3. **Platform Support**: Linux/Darwin compatibility +4. **Airgap**: Binary must be available offline + +### Operation Translation Complexity +1. **Simple Operations**: Pull, Push, AddRepo (straightforward CLI mapping) +2. **Complex Operations**: Render (requires --dry-run with parsing) +3. **State Operations**: ReleaseExists (requires history parsing) +4. **Value Handling**: Complex value merging and YAML processing + +### Testing Impact +- All existing mocks would need updating +- Integration tests need binary availability +- Build process changes for binary inclusion + +### Performance Considerations +- Process spawning overhead for each operation +- Increased memory usage (separate process) +- Potential for zombie processes +- File descriptor limits with concurrent operations + +## Affected Workflows + +### Critical Paths +1. **Initial Cluster Installation** + - All addon installations + - Registry setup for airgap + - Admin console deployment + +2. **Cluster Upgrades** + - Operator-driven upgrades + - Extension updates + - Application updates + +3. **HA Enablement** + - Scaling critical components + - Reconfiguring services + +4. **Disaster Recovery** + - Restore operations + - Reinstalling components + +### Build and Release Process +- Chart packaging for airgap +- Binary inclusion in releases +- Version compatibility matrix + +## Risk Areas + +### High Impact Components +- **Addon installation** (all cluster infrastructure) +- **Application deployment** (customer workloads) +- **Upgrade operations** (cluster stability) + +### Complex Operations +- Template rendering with value merging +- Chart dependency resolution +- Release rollback on failure +- Concurrent operations handling + +### State Management +- Repository cache management +- Temporary file handling +- Release state tracking + +## Migration Scope Estimates + +### Implementation Requirements +- **Core refactor**: `pkg/helm/client.go` (~800 lines replacing 613 existing) +- **New files**: ~650 lines across 3 new files + - `binary_executor.go` (~100 lines) + - `output_parser.go` (~300 lines) + - Test files (~250 lines) + +### Testing Requirements +- **Mock updates**: 32 test files need mock client updates +- **Integration tests**: Verify binary vs SDK output compatibility +- **Regression testing**: All 70 consuming files need validation + +## Open Questions + +1. **Binary distribution**: How to embed and materialize helm binary via materializer? +2. **Version compatibility**: Which helm binary version to embed for maximum compatibility? +3. **Performance impact**: Process spawning overhead vs in-memory SDK operations? +4. **Error translation**: Mapping CLI error messages to structured error types? +5. **Concurrent operations**: File locking and process management for parallel operations? + +## Recommendations for Migration + +### Critical Success Factors +1. Perfect interface compatibility +2. Comprehensive error handling +3. Binary distribution strategy +4. Rollback capability +5. Performance benchmarking +6. Extended testing period + +### Risk Mitigation +1. Comprehensive testing of all 70 consumer files +2. Binary availability validation in all environments +3. Error handling compatibility with existing patterns +4. Performance monitoring during migration +5. Rollback plan if critical issues arise + +## Key Dependencies +- helm.sh/helm/v3/pkg/* - Core Helm SDK packages (TO BE REMOVED) +- k8s.io/cli-runtime - Kubernetes client configuration +- sigs.k8s.io/controller-runtime - Controller client +- gopkg.in/yaml.v3 - YAML marshaling +- github.com/replicatedhq/embedded-cluster/pkg/helpers - RunCommand functionality + +## Related Research +- **Migration proposal**: `proposals/helm_binary_migration.md` +- **V3 transition**: `proposals/v3_app_deployment_transition.md` From 17632c8ec2d367284c3b46cdc0e7edc5ed7cad85 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Thu, 16 Oct 2025 12:33:36 -0700 Subject: [PATCH 6/6] f --- dev/dockerfiles/operator/Dockerfile.local | 2 +- dev/dockerfiles/operator/Dockerfile.ttlsh | 3 +++ operator/deploy/apko.tmpl.yaml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dev/dockerfiles/operator/Dockerfile.local b/dev/dockerfiles/operator/Dockerfile.local index 8981e9475b..f0fa09a30a 100644 --- a/dev/dockerfiles/operator/Dockerfile.local +++ b/dev/dockerfiles/operator/Dockerfile.local @@ -1,5 +1,5 @@ FROM golang:1.25.2-alpine AS build -RUN apk add --no-cache ca-certificates curl git make bash +RUN apk add --no-cache ca-certificates curl git make bash helm WORKDIR /replicatedhq/embedded-cluster/operator diff --git a/dev/dockerfiles/operator/Dockerfile.ttlsh b/dev/dockerfiles/operator/Dockerfile.ttlsh index feb669b0ee..b08f560387 100644 --- a/dev/dockerfiles/operator/Dockerfile.ttlsh +++ b/dev/dockerfiles/operator/Dockerfile.ttlsh @@ -25,12 +25,15 @@ ENV K0S_VERSION=${K0S_VERSION} ENV GOCACHE=/root/.cache/go-build RUN --mount=type=cache,target="/root/.cache/go-build" make -C operator build +RUN curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY --from=build /app/operator/bin/manager /manager +COPY --from=build /usr/local/bin/helm /usr/local/bin/helm RUN groupadd -r manager && useradd -r -u 1000 -g manager manager USER 1000 diff --git a/operator/deploy/apko.tmpl.yaml b/operator/deploy/apko.tmpl.yaml index d36d38ba0e..86b331045f 100644 --- a/operator/deploy/apko.tmpl.yaml +++ b/operator/deploy/apko.tmpl.yaml @@ -8,6 +8,7 @@ contents: packages: - ec-operator # This is expected to be built locally by `melange`. - ca-certificates-bundle + - helm accounts: groups: