diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml index b15a8037e8..0156f59630 100644 --- a/charts/fleet-crd/templates/crds.yaml +++ b/charts/fleet-crd/templates/crds.yaml @@ -128,6 +128,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: @@ -495,6 +512,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: @@ -1002,6 +1036,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: @@ -1145,6 +1196,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: @@ -2799,6 +2867,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: @@ -3166,6 +3251,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: @@ -3674,6 +3776,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: @@ -3817,6 +3936,23 @@ spec: type: string takeOwnership: type: boolean + test: + properties: + enabled: + type: boolean + filters: + additionalProperties: + items: + nullable: true + type: string + nullable: true + type: array + nullable: true + type: object + timeout: + nullable: true + type: string + type: object timeoutSeconds: type: integer values: diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go index 711a097538..7bf652fab6 100644 --- a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go +++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go @@ -233,6 +233,7 @@ type HelmOptions struct { TakeOwnership bool `json:"takeOwnership,omitempty"` MaxHistory int `json:"maxHistory,omitempty"` ValuesFiles []string `json:"valuesFiles,omitempty"` + Test HelmTest `json:"test,omitempty"` } // Define helm values that can come from configmap, secret or external. Credit: https://github.com/fluxcd/helm-operator/blob/0cfea875b5d44bea995abe7324819432070dfbdc/pkg/apis/helm.fluxcd.io/v1/types_helmrelease.go#L439 @@ -245,6 +246,20 @@ type ValuesFrom struct { SecretKeyRef *SecretKeySelector `json:"secretKeyRef,omitempty"` } +// Configure the helm test run for the release. +type HelmTest struct { + // Enable helm test for this release or not. + Enabled bool `json:"enabled"` + // Timeout passed to helm test. If absent a default helm test timeout is applied. + // +optional + Timeout *metav1.Duration `json:"timeout,omitempty"` + // Filters for tests to be run during helm test. See: https://helm.sh/docs/helm/helm_test/ for syntax. + // +optional + Filters HelmFilters `json:"filters,omitempty"` +} + +type HelmFilters map[string][]string + type ConfigMapKeySelector struct { LocalObjectReference `json:",inline"` // +optional diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go index 392323a7db..00cc7b576a 100644 --- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go +++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go @@ -1502,6 +1502,36 @@ func (in *GitTarget) DeepCopy() *GitTarget { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in HelmFilters) DeepCopyInto(out *HelmFilters) { + { + in := &in + *out = make(HelmFilters, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmFilters. +func (in HelmFilters) DeepCopy() HelmFilters { + if in == nil { + return nil + } + out := new(HelmFilters) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmOptions) DeepCopyInto(out *HelmOptions) { *out = *in @@ -1521,6 +1551,7 @@ func (in *HelmOptions) DeepCopyInto(out *HelmOptions) { *out = make([]string, len(*in)) copy(*out, *in) } + in.Test.DeepCopyInto(&out.Test) return } @@ -1534,6 +1565,42 @@ func (in *HelmOptions) DeepCopy() *HelmOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmTest) DeepCopyInto(out *HelmTest) { + *out = *in + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make(HelmFilters, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmTest. +func (in *HelmTest) DeepCopy() *HelmTest { + if in == nil { + return nil + } + out := new(HelmTest) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImagePolicyChoice) DeepCopyInto(out *ImagePolicyChoice) { *out = *in diff --git a/pkg/helmdeployer/deployer.go b/pkg/helmdeployer/deployer.go index 868c3e8a22..2e1d4b2e79 100644 --- a/pkg/helmdeployer/deployer.go +++ b/pkg/helmdeployer/deployer.go @@ -24,6 +24,7 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/kube" + "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" corev1 "k8s.io/api/core/v1" @@ -66,6 +67,7 @@ type helm struct { defaultNamespace string labelPrefix string labelSuffix string + defaultTestTimeout time.Duration } func NewHelm(namespace, defaultNamespace, labelPrefix, labelSuffix string, getter genericclioptions.RESTClientGetter, @@ -79,6 +81,7 @@ func NewHelm(namespace, defaultNamespace, labelPrefix, labelSuffix string, gette secretCache: secretCache, labelPrefix: labelPrefix, labelSuffix: labelSuffix, + defaultTestTimeout: 120 * time.Second, } if err := h.globalCfg.Init(getter, "", "secrets", logrus.Infof); err != nil { return nil, err @@ -269,6 +272,9 @@ func (h *helm) getCfg(namespace, serviceAccountName string) (action.Configuratio } func (h *helm) install(bundleID string, manifest *manifest.Manifest, chart *chart.Chart, options fleet.BundleDeploymentOptions, dryRun bool) (*release.Release, error) { + + defer logrus.Infof("exiting install %s", bundleID) + timeout, defaultNamespace, releaseName := h.getOpts(bundleID, options) values, err := h.getValues(options, defaultNamespace) @@ -317,30 +323,59 @@ func (h *helm) install(bundleID string, manifest *manifest.Manifest, chart *char pr.mapper = mapper } + var readyRelease *release.Release if install { - u := action.NewInstall(&cfg) - u.ClientOnly = h.template || dryRun - u.ForceAdopt = options.Helm.TakeOwnership - u.Replace = true - u.ReleaseName = releaseName - u.CreateNamespace = true - u.Namespace = defaultNamespace - u.Timeout = timeout - u.DryRun = dryRun - u.PostRenderer = pr - if u.Timeout > 0 { - u.Wait = true - } + u := h.newInstallAction(releaseName, &cfg, *options.Helm, defaultNamespace, timeout, dryRun, pr) if !dryRun { logrus.Infof("Helm: Installing %s", bundleID) } - return u.Run(chart, values) + readyRelease, err = u.Run(chart, values) + if err != nil { + return nil, err + } + } else { + u := h.newUpgradeAction(&cfg, *options.Helm, defaultNamespace, timeout, dryRun, pr) + if !dryRun { + logrus.Infof("Helm: Upgrading %s", bundleID) + } + readyRelease, err = u.Run(releaseName, chart, values) + if err != nil { + return nil, err + } + } + helmTest := options.Helm.Test + if !dryRun && helmTest.Enabled { + t := h.newReleaseTestingAction(&cfg, helmTest) + logrus.Infof("Helm: Release Testing %s", bundleID) + return t.Run(releaseName) } + return readyRelease, nil +} - u := action.NewUpgrade(&cfg) +func (h *helm) newInstallAction(releaseName string, cfg *action.Configuration, helmOptions fleet.HelmOptions, + defaultNamespace string, timeout time.Duration, dryRun bool, pr postrender.PostRenderer) *action.Install { + u := action.NewInstall(cfg) + u.ClientOnly = h.template || dryRun + u.ForceAdopt = helmOptions.TakeOwnership + u.Replace = true + u.ReleaseName = releaseName + u.CreateNamespace = true + u.Namespace = defaultNamespace + u.Timeout = timeout + u.DryRun = dryRun + u.PostRenderer = pr + if u.Timeout > 0 { + u.Wait = true + } + return u +} + +func (h *helm) newUpgradeAction(cfg *action.Configuration, helmOptions fleet.HelmOptions, + defaultNamespace string, timeout time.Duration, dryRun bool, pr postrender.PostRenderer) *action.Upgrade { + u := action.NewUpgrade(cfg) u.Adopt = true - u.Force = options.Helm.Force - u.MaxHistory = options.Helm.MaxHistory + u.Force = helmOptions.Force + u.MaxHistory = helmOptions.MaxHistory if u.MaxHistory == 0 { u.MaxHistory = 10 } @@ -352,10 +387,18 @@ func (h *helm) install(bundleID string, manifest *manifest.Manifest, chart *char if u.Timeout > 0 { u.Wait = true } - if !dryRun { - logrus.Infof("Helm: Upgrading %s", bundleID) + return u +} + +func (h *helm) newReleaseTestingAction(cfg *action.Configuration, helmTest fleet.HelmTest) *action.ReleaseTesting { + t := action.NewReleaseTesting(cfg) + t.Filters = helmTest.Filters + if helmTest.Timeout == nil { + t.Timeout = h.defaultTestTimeout + } else { + t.Timeout = helmTest.Timeout.Duration } - return u.Run(releaseName, chart, values) + return t } func (h *helm) getValues(options fleet.BundleDeploymentOptions, defaultNamespace string) (map[string]interface{}, error) {