Skip to content

Commit

Permalink
Add force apply selector options
Browse files Browse the repository at this point in the history
Add `ForceSelector` to `ApplyOptions` to recreate immutable objects with matching labels or annotations.

Signed-off-by: Stefan Prodan <[email protected]>
  • Loading branch information
stefanprodan committed Jan 25, 2023
1 parent d0e7e5c commit 07d48be
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 18 deletions.
48 changes: 36 additions & 12 deletions ssa/manager_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
17 changes: 11 additions & 6 deletions ssa/manager_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 07d48be

Please sign in to comment.