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) {