diff --git a/api/v2/helmrelease_types.go b/api/v2/helmrelease_types.go index 9495fc537..78f228d7f 100644 --- a/api/v2/helmrelease_types.go +++ b/api/v2/helmrelease_types.go @@ -810,7 +810,10 @@ type Upgrade struct { // +optional DisableSchemaValidation bool `json:"disableSchemaValidation,omitempty"` - // Force forces resource updates through a replacement strategy. + // Force forces resource updates through a replacement strategy + // that avoids 3-way merge conflicts on client-side apply. + // This field is ignored for server-side apply (which always + // forces conflicts with other field managers). // +optional Force bool `json:"force,omitempty"` @@ -1113,7 +1116,10 @@ type Rollback struct { // +optional Recreate bool `json:"recreate,omitempty"` - // Force forces resource updates through a replacement strategy. + // Force forces resource updates through a replacement strategy + // that avoids 3-way merge conflicts on client-side apply. + // This field is ignored for server-side apply (which always + // forces conflicts with other field managers). // +optional Force bool `json:"force,omitempty"` diff --git a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml index b1f6cd9c1..03d16ab31 100644 --- a/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml +++ b/config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml @@ -756,8 +756,11 @@ spec: rollback has been performed. type: boolean force: - description: Force forces resource updates through a replacement - strategy. + description: |- + Force forces resource updates through a replacement strategy + that avoids 3-way merge conflicts on client-side apply. + This field is ignored for server-side apply (which always + forces conflicts with other field managers). type: boolean recreate: description: |- @@ -961,8 +964,11 @@ spec: upgrade has been performed. type: boolean force: - description: Force forces resource updates through a replacement - strategy. + description: |- + Force forces resource updates through a replacement strategy + that avoids 3-way merge conflicts on client-side apply. + This field is ignored for server-side apply (which always + forces conflicts with other field managers). type: boolean preserveValues: description: |- diff --git a/docs/api/v2/helm.md b/docs/api/v2/helm.md index c17156d72..f49945c25 100644 --- a/docs/api/v2/helm.md +++ b/docs/api/v2/helm.md @@ -2608,7 +2608,10 @@ bool (Optional) -

Force forces resource updates through a replacement strategy.

+

Force forces resource updates through a replacement strategy +that avoids 3-way merge conflicts on client-side apply. +This field is ignored for server-side apply (which always +forces conflicts with other field managers).

@@ -3260,7 +3263,10 @@ bool (Optional) -

Force forces resource updates through a replacement strategy.

+

Force forces resource updates through a replacement strategy +that avoids 3-way merge conflicts on client-side apply. +This field is ignored for server-side apply (which always +forces conflicts with other field managers).

diff --git a/docs/spec/v2/helmreleases.md b/docs/spec/v2/helmreleases.md index 2d95676c4..3a922ea21 100644 --- a/docs/spec/v2/helmreleases.md +++ b/docs/spec/v2/helmreleases.md @@ -636,7 +636,10 @@ The field offers the following subfields: upgrading the release. Defaults to `false`. - `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete after upgrading the release. Defaults to `false`. -- `.force` (Optional): Forces resource updates through a replacement strategy. +- `.force` (Optional): Forces resource updates through a replacement strategy + that avoids 3-way merge conflicts on client-side apply. + This field is ignored for server-side apply (which always forces conflicts + with other field managers). Defaults to `false`. - `.preserveValues` (Optional): Instructs Helm to re-use the values from the last release while merging in overrides from [values](#values). Setting @@ -755,7 +758,10 @@ The field offers the following subfields: rolling back the release. Defaults to `false`. - `.disableWaitForJobs` (Optional): Disables waiting for any Jobs to complete after rolling back the release. Defaults to `false`. -- `.force` (Optional): Forces resource updates through a replacement strategy. +- `.force` (Optional): Forces resource updates through a replacement strategy + that avoids 3-way merge conflicts on client-side apply. + This field is ignored for server-side apply (which always forces conflicts + with other field managers). Defaults to `false`. - `.recreate` (Optional): Performs Pod restarts if applicable. Defaults to `false`. **Warning**: As of Flux v2.8, this option is deprecated and no diff --git a/internal/action/rollback.go b/internal/action/rollback.go index 1bca4b075..1e708ad5c 100644 --- a/internal/action/rollback.go +++ b/internal/action/rollback.go @@ -91,6 +91,7 @@ func Rollback(config *helmaction.Configuration, obj *v2.HelmRelease, rollback.ServerSideApply = fmt.Sprint(serverSideApply) } rollback.ForceConflicts = serverSideApply // We always force conflicts on server-side apply. + rollback.ForceReplace = obj.GetRollback().Force && !serverSideApply return rollback.Run(releaseName) } @@ -109,7 +110,6 @@ func newRollback(config *helmaction.Configuration, obj *v2.HelmRelease, rollback.WaitStrategy = getWaitStrategy(obj.GetWaitStrategy(), obj.GetRollback()) rollback.WaitForJobs = !obj.GetRollback().DisableWaitForJobs rollback.DisableHooks = obj.GetRollback().DisableHooks - rollback.ForceReplace = obj.GetRollback().Force rollback.CleanupOnFail = obj.GetRollback().CleanupOnFail rollback.MaxHistory = obj.GetMaxHistory() diff --git a/internal/action/rollback_test.go b/internal/action/rollback_test.go index b24fca301..0c446b1bb 100644 --- a/internal/action/rollback_test.go +++ b/internal/action/rollback_test.go @@ -48,7 +48,9 @@ func Test_newRollback(t *testing.T) { got := newRollback(&helmaction.Configuration{}, obj, 0, nil) g.Expect(got).ToNot(BeNil()) g.Expect(got.Timeout).To(Equal(obj.Spec.Rollback.Timeout.Duration)) - g.Expect(got.ForceReplace).To(Equal(obj.Spec.Rollback.Force)) + // ForceReplace is not set in the constructor; it is set after SSA resolution + // in Rollback() to avoid the Helm SDK mutual exclusivity error. + g.Expect(got.ForceReplace).To(BeFalse()) g.Expect(got.MaxHistory).To(Equal(obj.GetMaxHistory())) }) diff --git a/internal/action/upgrade.go b/internal/action/upgrade.go index 101cf9302..7027ac911 100644 --- a/internal/action/upgrade.go +++ b/internal/action/upgrade.go @@ -75,6 +75,7 @@ func Upgrade(ctx context.Context, config *helmaction.Configuration, obj *v2.Helm upgrade.ServerSideApply = fmt.Sprint(serverSideApply) } upgrade.ForceConflicts = serverSideApply // We always force conflicts on server-side apply. + upgrade.ForceReplace = obj.GetUpgrade().Force && !serverSideApply policy, err := crdPolicyOrDefault(obj.GetUpgrade().CRDs) if err != nil { @@ -116,7 +117,6 @@ func newUpgrade(config *helmaction.Configuration, obj *v2.HelmRelease, opts []Up upgrade.DisableHooks = obj.GetUpgrade().DisableHooks upgrade.DisableOpenAPIValidation = obj.GetUpgrade().DisableOpenAPIValidation upgrade.SkipSchemaValidation = obj.GetUpgrade().DisableSchemaValidation - upgrade.ForceReplace = obj.GetUpgrade().Force upgrade.CleanupOnFail = obj.GetUpgrade().CleanupOnFail upgrade.Devel = true diff --git a/internal/action/upgrade_test.go b/internal/action/upgrade_test.go index 7b1f09c2e..3e23b7d7b 100644 --- a/internal/action/upgrade_test.go +++ b/internal/action/upgrade_test.go @@ -49,7 +49,9 @@ func Test_newUpgrade(t *testing.T) { g.Expect(got).ToNot(BeNil()) g.Expect(got.Namespace).To(Equal(obj.Namespace)) g.Expect(got.Timeout).To(Equal(obj.Spec.Upgrade.Timeout.Duration)) - g.Expect(got.ForceReplace).To(Equal(obj.Spec.Upgrade.Force)) + // ForceReplace is not set in the constructor; it is set after SSA resolution + // in Upgrade() to avoid the Helm SDK mutual exclusivity error. + g.Expect(got.ForceReplace).To(BeFalse()) }) t.Run("timeout fallback", func(t *testing.T) {