diff --git a/ssa/manager_apply.go b/ssa/manager_apply.go index 43459f029..33925e88d 100644 --- a/ssa/manager_apply.go +++ b/ssa/manager_apply.go @@ -34,11 +34,13 @@ type ApplyOptions struct { // Force configures the engine to recreate objects that contain immutable field changes. Force bool `json:"force"` - // Exclusions determines which in-cluster objects are skipped from apply - // based on the specified key-value pairs. - // A nil Exclusions map means all objects are applied - // regardless of their metadata labels and annotations. - Exclusions map[string]string `json:"exclusions"` + // ForceSelector determines which in-cluster objects are Force applied + // based on the matching labels or annotations. + ForceSelector map[string]string `json:"forceSelector"` + + // ExclusionSelector determines which in-cluster objects are skipped from apply + // based on the matching labels or annotations. + ExclusionSelector map[string]string `json:"exclusionSelector"` // WaitTimeout defines after which interval should the engine give up on waiting for // cluster scoped resources to become ready. @@ -67,9 +69,9 @@ type ApplyCleanupOptions struct { // DefaultApplyOptions returns the default apply options where force apply is disabled. func DefaultApplyOptions() ApplyOptions { return ApplyOptions{ - Force: false, - Exclusions: nil, - WaitTimeout: 60 * time.Second, + Force: false, + ExclusionSelector: nil, + WaitTimeout: 60 * time.Second, } } @@ -80,13 +82,13 @@ func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstru existingObject := object.DeepCopy() _ = m.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) - if existingObject != nil && AnyInMetadata(existingObject, opts.Exclusions) { + if m.shouldSkipApply(existingObject, opts) { return m.changeSetEntry(object, UnchangedAction), nil } dryRunObject := object.DeepCopy() if err := m.dryRunApply(ctx, dryRunObject); err != nil { - if opts.Force && IsImmutableError(err) { + if m.shouldForceApply(object, existingObject, opts, err) { if err := m.client.Delete(ctx, existingObject); err != nil { return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", FmtUnstructured(dryRunObject), err) @@ -130,14 +132,14 @@ func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured. existingObject := object.DeepCopy() _ = m.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) - if existingObject != nil && AnyInMetadata(existingObject, opts.Exclusions) { + if m.shouldSkipApply(existingObject, opts) { changeSet.Add(*m.changeSetEntry(existingObject, UnchangedAction)) continue } dryRunObject := object.DeepCopy() if err := m.dryRunApply(ctx, dryRunObject); err != nil { - if opts.Force && IsImmutableError(err) { + if m.shouldForceApply(object, existingObject, opts, err) { if err := m.client.Delete(ctx, existingObject); err != nil { return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", FmtUnstructured(dryRunObject), err) @@ -279,3 +281,25 @@ func (m *ResourceManager) cleanupMetadata(ctx context.Context, return true, m.client.Patch(ctx, existingObject, patch, client.FieldOwner(m.owner.Field)) } + +// shouldForceApply determines based on the apply error and ApplyOptions if the object should be recreated. +// An object is recreated if the apply error was due to immutable field changes and if the object +// contains a label or annotation which matches the ApplyOptions.ForceSelector. +func (m *ResourceManager) shouldForceApply(desiredObject *unstructured.Unstructured, + object *unstructured.Unstructured, opts ApplyOptions, err error) bool { + if IsImmutableError(err) { + if opts.Force || + AnyInMetadata(desiredObject, opts.ForceSelector) || + (object != nil && AnyInMetadata(object, opts.ForceSelector)) { + return true + } + } + + return false +} + +// shouldSkipApply determines based on the object metadata and ApplyOptions if the object should be skipped. +// An object is not applied if it contains a label or annotation which matches the ApplyOptions.ExclusionSelector. +func (m *ResourceManager) shouldSkipApply(object *unstructured.Unstructured, opts ApplyOptions) bool { + return object != nil && AnyInMetadata(object, opts.ExclusionSelector) +} diff --git a/ssa/manager_apply_test.go b/ssa/manager_apply_test.go index aa17f8399..7313b65f9 100644 --- a/ssa/manager_apply_test.go +++ b/ssa/manager_apply_test.go @@ -242,21 +242,26 @@ func TestApply_Force(t *testing.T) { } }) - t.Run("recreates immutable StorageClass", func(t *testing.T) { + t.Run("recreates immutable StorageClass based on metadata", func(t *testing.T) { // update parameters err = unstructured.SetNestedField(st.Object, "true", "parameters", "encrypted") if err != nil { t.Fatal(err) } + meta := map[string]string{ + "fluxcd.io/force": "true", + } + st.SetAnnotations(meta) + // apply and expect to fail _, err := manager.ApplyAllStaged(ctx, objects, DefaultApplyOptions()) if err == nil { t.Fatal("Expected error got none") } - // force apply - changeSet, err := manager.ApplyAllStaged(ctx, objects, ApplyOptions{Force: true, WaitTimeout: timeout}) + // force apply selector + changeSet, err := manager.ApplyAllStaged(ctx, objects, ApplyOptions{ForceSelector: meta, WaitTimeout: timeout}) if err != nil { t.Fatal(err) } @@ -383,9 +388,9 @@ func TestApply_Exclusions(t *testing.T) { // apply with exclusions changeSet, err := manager.ApplyAll(ctx, objects, ApplyOptions{ - Force: false, - Exclusions: meta, - WaitTimeout: time.Second, + Force: false, + ExclusionSelector: meta, + WaitTimeout: time.Second, }) if err != nil { t.Fatal(err)