diff --git a/docs/helm-charts.md b/docs/helm-charts.md index a27167f2a61a..618a58272965 100644 --- a/docs/helm-charts.md +++ b/docs/helm-charts.md @@ -35,15 +35,16 @@ It is possible to customize the timeout by using the `timeout' field. ### Chart configuration -| Field | Default value | Description | -|-----------|---------------|----------------------------------------------------------------------| -| name | - | Release name | -| chartname | - | chartname in form "repository/chartname" or path to tgz file | -| version | - | version to install | -| timeout | - | timeout to wait for release install | -| values | - | yaml as a string, custom chart values | -| namespace | - | namespace to install chart into | -| order | 0 | order to apply manifest. For equal values, alphanum ordering is used | +| Field | Default value | Description | +|--------------|---------------|----------------------------------------------------------------------------------------| +| name | - | Release name | +| chartname | - | chartname in form "repository/chartname" or path to tgz file | +| version | - | version to install | +| timeout | - | timeout to wait for release install | +| values | - | yaml as a string, custom chart values | +| namespace | - | namespace to install chart into | +| forceUpgrade | true | when set to false, disables the use of the "--force" flag when upgrading the the chart | +| order | 0 | order to apply manifest. For equal values, alphanum ordering is used | ## Example diff --git a/inttest/addons/addons_test.go b/inttest/addons/addons_test.go index 85071123a98a..0e8ebc4d3c5d 100644 --- a/inttest/addons/addons_test.go +++ b/inttest/addons/addons_test.go @@ -36,6 +36,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" k8s "k8s.io/client-go/kubernetes" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" crlog "sigs.k8s.io/controller-runtime/pkg/log" @@ -374,17 +375,19 @@ func (as *AddonsSuite) doTestAddonUpdate(addonName string, values map[string]int Name: "testChartUpdate", Template: chartCrdTemplate, Data: struct { - Name string - ChartName string - Values string - Version string - TargetNS string + Name string + ChartName string + Values string + Version string + TargetNS string + ForceUpgrade *bool }{ - Name: "test-addon", - ChartName: "ealenn/echo-server", - Values: string(valuesBytes), - Version: "0.5.0", - TargetNS: "default", + Name: "test-addon", + ChartName: "ealenn/echo-server", + Values: string(valuesBytes), + Version: "0.5.0", + TargetNS: "default", + ForceUpgrade: ptr.To(false), }, } buf := bytes.NewBuffer([]byte{}) @@ -429,6 +432,7 @@ spec: version: "0.0.1" values: "" namespace: kube-system + forceUpgrade: false ` // TODO: this actually duplicates logic from the controller code @@ -448,4 +452,7 @@ spec: {{ .Values | nindent 4 }} version: {{ .Version }} namespace: {{ .TargetNS }} +{{- if ne .ForceUpgrade nil }} + forceUpgrade: {{ .ForceUpgrade }} +{{- end }} ` diff --git a/pkg/apis/helm/v1beta1/chart_types.go b/pkg/apis/helm/v1beta1/chart_types.go index b94004bc5993..f6832fc19b5e 100644 --- a/pkg/apis/helm/v1beta1/chart_types.go +++ b/pkg/apis/helm/v1beta1/chart_types.go @@ -33,7 +33,11 @@ type ChartSpec struct { Version string `json:"version,omitempty"` Namespace string `json:"namespace,omitempty"` Timeout string `json:"timeout,omitempty"` - Order int `json:"order,omitempty"` + // ForceUpgrade when set to false, disables the use of the "--force" flag when upgrading the the chart (default: true). + // +kubebuilder:default=true + // +optional + ForceUpgrade *bool `json:"forceUpgrade,omitempty"` + Order int `json:"order,omitempty"` } // YamlValues returns values as map @@ -54,6 +58,13 @@ func (cs ChartSpec) HashValues() string { return fmt.Sprintf("%x", h.Sum(nil)) } +// ShouldForceUpgrade returns true if the chart should be force upgraded +func (cs ChartSpec) ShouldForceUpgrade() bool { + // This defaults to true when not explicitly set to false. + // Better have this the other way round in the next API version. + return cs.ForceUpgrade == nil || *cs.ForceUpgrade +} + // ChartStatus defines the observed state of Chart type ChartStatus struct { ReleaseName string `json:"releaseName,omitempty"` diff --git a/pkg/apis/helm/v1beta1/zz_generated.deepcopy.go b/pkg/apis/helm/v1beta1/zz_generated.deepcopy.go index 447f2669c4c1..c6b29424a13e 100644 --- a/pkg/apis/helm/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/helm/v1beta1/zz_generated.deepcopy.go @@ -29,7 +29,7 @@ func (in *Chart) DeepCopyInto(out *Chart) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -86,6 +86,11 @@ func (in *ChartList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ChartSpec) DeepCopyInto(out *ChartSpec) { *out = *in + if in.ForceUpgrade != nil { + in, out := &in.ForceUpgrade, &out.ForceUpgrade + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChartSpec. diff --git a/pkg/apis/k0s/v1beta1/extensions.go b/pkg/apis/k0s/v1beta1/extensions.go index 2986de8f38cf..41d2df0556cc 100644 --- a/pkg/apis/k0s/v1beta1/extensions.go +++ b/pkg/apis/k0s/v1beta1/extensions.go @@ -96,7 +96,11 @@ type Chart struct { Values string `json:"values"` TargetNS string `json:"namespace"` Timeout time.Duration `json:"timeout"` - Order int `json:"order"` + // ForceUpgrade when set to false, disables the use of the "--force" flag when upgrading the the chart (default: true). + // +kubebuilder:default=true + // +optional + ForceUpgrade *bool `json:"forceUpgrade,omitempty"` + Order int `json:"order"` } // Validate performs validation diff --git a/pkg/apis/k0s/v1beta1/zz_generated.deepcopy.go b/pkg/apis/k0s/v1beta1/zz_generated.deepcopy.go index 20b1bfcafc39..bada13d7b048 100644 --- a/pkg/apis/k0s/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/k0s/v1beta1/zz_generated.deepcopy.go @@ -129,6 +129,11 @@ func (in *CalicoImageSpec) DeepCopy() *CalicoImageSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Chart) DeepCopyInto(out *Chart) { *out = *in + if in.ForceUpgrade != nil { + in, out := &in.ForceUpgrade, &out.ForceUpgrade + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Chart. @@ -146,7 +151,9 @@ func (in ChartsSettings) DeepCopyInto(out *ChartsSettings) { { in := &in *out = make(ChartsSettings, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } @@ -583,7 +590,9 @@ func (in *HelmExtensions) DeepCopyInto(out *HelmExtensions) { if in.Charts != nil { in, out := &in.Charts, &out.Charts *out = make(ChartsSettings, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } diff --git a/pkg/component/controller/extensions_controller.go b/pkg/component/controller/extensions_controller.go index e1ec7948af50..256d6f0e40f1 100644 --- a/pkg/component/controller/extensions_controller.go +++ b/pkg/component/controller/extensions_controller.go @@ -191,24 +191,13 @@ func (ec *ExtensionsController) reconcileHelmExtensions(helmSpec *k0sv1beta1.Hel fileName := chartManifestFileName(&chart) fileNamesToKeep = append(fileNamesToKeep, fileName) - tw := templatewriter.TemplateWriter{ - Path: filepath.Join(ec.manifestsDir, fileName), - Name: "addon_crd_manifest", - Template: chartCrdTemplate, - Data: struct { - k0sv1beta1.Chart - Finalizer string - }{ - Chart: chart, - Finalizer: finalizerName, - }, - } - if err := tw.Write(); err != nil { + path, err := ec.writeChartManifestFile(chart, fileName) + if err != nil { errs = append(errs, fmt.Errorf("can't write file for Helm chart manifest %q: %w", chart.ChartName, err)) continue } - ec.L.Infof("Wrote Helm chart manifest file %q", tw.Path) + ec.L.Infof("Wrote Helm chart manifest file %q", path) } if err := filepath.WalkDir(ec.manifestsDir, func(path string, entry fs.DirEntry, err error) error { @@ -237,6 +226,25 @@ func (ec *ExtensionsController) reconcileHelmExtensions(helmSpec *k0sv1beta1.Hel return errors.Join(errs...) } +func (ec *ExtensionsController) writeChartManifestFile(chart k0sv1beta1.Chart, fileName string) (string, error) { + tw := templatewriter.TemplateWriter{ + Path: filepath.Join(ec.manifestsDir, fileName), + Name: "addon_crd_manifest", + Template: chartCrdTemplate, + Data: struct { + k0sv1beta1.Chart + Finalizer string + }{ + Chart: chart, + Finalizer: finalizerName, + }, + } + if err := tw.Write(); err != nil { + return "", err + } + return tw.Path, nil +} + // Determines the file name to use when storing a chart as a manifest on disk. func chartManifestFileName(c *k0sv1beta1.Chart) string { return fmt.Sprintf("%d_helm_extension_%s.yaml", c.Order, c.Name) @@ -375,6 +383,7 @@ func (cr *ChartReconciler) updateOrInstallChart(ctx context.Context, chart helmv chart.Status.Namespace, chart.Spec.YamlValues(), timeout, + chart.Spec.ShouldForceUpgrade(), ) if err != nil { return fmt.Errorf("can't reconcile upgrade for %q: %w", chart.GetName(), err) @@ -447,6 +456,9 @@ spec: {{ .Values | nindent 4 }} version: {{ .Version }} namespace: {{ .TargetNS }} +{{- if ne .ForceUpgrade nil }} + forceUpgrade: {{ .ForceUpgrade }} +{{- end }} ` const finalizerName = "helm.k0sproject.io/uninstall-helm-release" diff --git a/pkg/component/controller/extensions_controller_test.go b/pkg/component/controller/extensions_controller_test.go index dc4da3b246d1..4ccda431909d 100644 --- a/pkg/component/controller/extensions_controller_test.go +++ b/pkg/component/controller/extensions_controller_test.go @@ -17,11 +17,16 @@ limitations under the License. package controller import ( + "os" + "strings" "testing" + "time" "github.com/k0sproject/k0s/pkg/apis/helm/v1beta1" k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" ) func TestChartNeedsUpgrade(t *testing.T) { @@ -225,3 +230,92 @@ func TestChartManifestFileName(t *testing.T) { assert.Equal(t, chartManifestFileName(&chart2), "2_helm_extension_release.yaml") assert.True(t, isChartManifestFileName("0_helm_extension_release.yaml")) } + +func TestExtensionsController_writeChartManifestFile(t *testing.T) { + type args struct { + chart k0sv1beta1.Chart + fileName string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "forceUpgrade is nil should omit from manifest", + args: args{ + chart: k0sv1beta1.Chart{ + Name: "release", + ChartName: "k0s/chart", + Version: "0.0.1", + Values: "values", + TargetNS: "default", + Timeout: 5 * time.Minute, + }, + fileName: "0_helm_extension_release.yaml", + }, + want: `apiVersion: helm.k0sproject.io/v1beta1 +kind: Chart +metadata: + name: k0s-addon-chart-release + namespace: "kube-system" + finalizers: + - helm.k0sproject.io/uninstall-helm-release +spec: + chartName: k0s/chart + releaseName: release + timeout: 5m0s + values: | + + values + version: 0.0.1 + namespace: default +`, + }, + { + name: "forceUpgrade is false should be included in manifest", + args: args{ + chart: k0sv1beta1.Chart{ + Name: "release", + ChartName: "k0s/chart", + Version: "0.0.1", + Values: "values", + TargetNS: "default", + Timeout: 5 * time.Minute, + ForceUpgrade: ptr.To(false), + }, + fileName: "0_helm_extension_release.yaml", + }, + want: `apiVersion: helm.k0sproject.io/v1beta1 +kind: Chart +metadata: + name: k0s-addon-chart-release + namespace: "kube-system" + finalizers: + - helm.k0sproject.io/uninstall-helm-release +spec: + chartName: k0s/chart + releaseName: release + timeout: 5m0s + values: | + + values + version: 0.0.1 + namespace: default + forceUpgrade: false +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ec := &ExtensionsController{ + manifestsDir: t.TempDir(), + } + path, err := ec.writeChartManifestFile(tt.args.chart, tt.args.fileName) + require.NoError(t, err) + contents, err := os.ReadFile(path) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.want), strings.TrimSpace(string(contents))) + }) + } +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 1084c5c53efe..ad78aff45665 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -270,7 +270,7 @@ func (hc *Commands) InstallChart(ctx context.Context, chartName string, version // UpgradeChart upgrades a helm chart. // InstallChart, UpgradeChart and UninstallRelease(releaseName are *NOT* thread-safe -func (hc *Commands) UpgradeChart(ctx context.Context, chartName string, version string, releaseName string, namespace string, values map[string]interface{}, timeout time.Duration) (*release.Release, error) { +func (hc *Commands) UpgradeChart(ctx context.Context, chartName string, version string, releaseName string, namespace string, values map[string]interface{}, timeout time.Duration, force bool) (*release.Release, error) { cfg, err := hc.getActionCfg(namespace) if err != nil { return nil, fmt.Errorf("can't create action configuration: %v", err) @@ -280,7 +280,7 @@ func (hc *Commands) UpgradeChart(ctx context.Context, chartName string, version upgrade.Wait = true upgrade.WaitForJobs = true upgrade.Install = true - upgrade.Force = true + upgrade.Force = force upgrade.Atomic = true upgrade.Timeout = timeout chartDir, err := hc.locateChart(chartName, version) diff --git a/static/manifests/helm/CustomResourceDefinition/helm.k0sproject.io_charts.yaml b/static/manifests/helm/CustomResourceDefinition/helm.k0sproject.io_charts.yaml index f467b42c39cf..86edc7c5cef4 100644 --- a/static/manifests/helm/CustomResourceDefinition/helm.k0sproject.io_charts.yaml +++ b/static/manifests/helm/CustomResourceDefinition/helm.k0sproject.io_charts.yaml @@ -41,6 +41,11 @@ spec: properties: chartName: type: string + forceUpgrade: + default: true + description: 'ForceUpgrade when set to false, disables the use of + the "--force" flag when upgrading the the chart (default: true).' + type: boolean namespace: type: string order: diff --git a/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml b/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml index 6ff2e0f7ab3d..12bd86263d1e 100644 --- a/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml +++ b/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml @@ -94,6 +94,12 @@ spec: properties: chartname: type: string + forceUpgrade: + default: true + description: 'ForceUpgrade when set to false, disables + the use of the "--force" flag when upgrading the the + chart (default: true).' + type: boolean name: type: string namespace: