Skip to content

Commit

Permalink
Merge pull request #862 from YvanGuidoin/rework-ssa-diffoptions
Browse files Browse the repository at this point in the history
ssa: Align ResourceManager.Diff skipping resources with ResourceManager.Apply
  • Loading branch information
stefanprodan authored Feb 6, 2025
2 parents e454462 + 88a752e commit 7c90c14
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 4 deletions.
29 changes: 25 additions & 4 deletions ssa/manager_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ import (
// DiffOptions contains options for server-side dry-run apply requests.
type DiffOptions struct {
// Exclusions determines which in-cluster objects are skipped from dry-run apply
// based on the specified key-value pairs.
// A nil Exclusions map means all objects are applied
// regardless of their metadata labels and annotations.
// based on the matching labels or annotations.
Exclusions map[string]string `json:"exclusions"`

// IfNotPresentSelector determines which in-cluster objects are skipped from dry-run apply
// based on the matching labels or annotations.
IfNotPresentSelector map[string]string `json:"ifNotPresentSelector"`
}

// DefaultDiffOptions returns the default dry-run apply options.
Expand All @@ -57,7 +59,7 @@ func (m *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstruc
existingObject.SetGroupVersionKind(object.GroupVersionKind())
_ = m.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject)

if existingObject != nil && utils.AnyInMetadata(existingObject, opts.Exclusions) {
if m.shouldSkipDiff(object, existingObject, opts) {
return m.changeSetEntry(existingObject, SkippedAction), nil, nil, nil
}

Expand Down Expand Up @@ -123,3 +125,22 @@ func prepareObjectForDiff(object *unstructured.Unstructured) *unstructured.Unstr
}
return deepCopy
}

// shouldSkipDiff determines based on the object metadata and DiffOptions if the object should be skipped.
// An object is not applied if it contains a label or annotation
// which matches the DiffOptions.Exclusions or DiffOptions.IfNotPresentSelector.
func (m *ResourceManager) shouldSkipDiff(desiredObject *unstructured.Unstructured,
existingObject *unstructured.Unstructured, opts DiffOptions) bool {
if utils.AnyInMetadata(desiredObject, opts.Exclusions) ||
(existingObject != nil && utils.AnyInMetadata(existingObject, opts.Exclusions)) {
return true
}

if existingObject != nil &&
existingObject.GetUID() != "" &&
utils.AnyInMetadata(desiredObject, opts.IfNotPresentSelector) {
return true
}

return false
}
168 changes: 168 additions & 0 deletions ssa/manager_diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,174 @@ func TestDiff_Exclusions(t *testing.T) {
})
}

func TestDiff_IfNotPresent_OnExisting(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

id := generateName("ifnotpresentonexisting")
objects, err := readManifest("testdata/test1.yaml", id)
if err != nil {
t.Fatal(err)
}

meta := map[string]string{
"fluxcd.io/ignore": "true",
}

_, configMap := getFirstObject(objects, "ConfigMap", id)
configMap.SetAnnotations(meta)

if _, err = manager.ApplyAllStaged(ctx, objects, DefaultApplyOptions()); err != nil {
t.Fatal(err)
}

opts := DefaultDiffOptions()
opts.IfNotPresentSelector = meta

t.Run("diffs skips", func(t *testing.T) {
entry, _, _, err := manager.Diff(ctx, configMap, opts)
if err != nil {
t.Fatal(err)
}

if entry.Action != SkippedAction && entry.Subject == utils.FmtUnstructured(configMap) {
t.Errorf("Expected %s, got %s", SkippedAction, entry.Action)
}
})

t.Run("diffs applies without meta", func(t *testing.T) {
// mutate in-cluster object
configMapClone := configMap.DeepCopy()
err = manager.client.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone)
if err != nil {
t.Fatal(err)
}

err = unstructured.SetNestedField(configMapClone.Object, "public-second-key", "data", "secondKey")
if err != nil {
t.Fatal(err)
}
configMapClone.SetAnnotations(map[string]string{"fluxcd.io/ignore": ""})
configMapClone.SetManagedFields(nil)
entry, _, _, err := manager.Diff(ctx, configMapClone, opts)
if err != nil {
t.Fatal(err)
}

if entry.Action != ConfiguredAction && entry.Subject == utils.FmtUnstructured(configMapClone) {
t.Errorf("Expected %s, got %s", ConfiguredAction, entry.Action)
}
})

t.Run("diffs skips with meta", func(t *testing.T) {
// mutate in-cluster object
configMapClone := configMap.DeepCopy()
err = manager.client.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone)
if err != nil {
t.Fatal(err)
}

err = unstructured.SetNestedField(configMapClone.Object, "public-second-key", "data", "secondKey")
if err != nil {
t.Fatal(err)
}
configMapClone.SetManagedFields(nil)

entry, _, _, err := manager.Diff(ctx, configMapClone, opts)
if err != nil {
t.Fatal(err)
}

if entry.Action != SkippedAction && entry.Subject == utils.FmtUnstructured(configMapClone) {
t.Errorf("Expected %s, got %s", SkippedAction, entry.Action)
}
})
}

func TestDiff_IfNotPresent_OnObject(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

id := generateName("ifnotpresentonobject")
objects, err := readManifest("testdata/test1.yaml", id)
if err != nil {
t.Fatal(err)
}

_, configMap := getFirstObject(objects, "ConfigMap", id)

if _, err = manager.ApplyAllStaged(ctx, objects, DefaultApplyOptions()); err != nil {
t.Fatal(err)
}

meta := map[string]string{
"fluxcd.io/ignore": "true",
}
opts := DefaultDiffOptions()
opts.IfNotPresentSelector = meta

t.Run("diffs unchanged", func(t *testing.T) {
entry, _, _, err := manager.Diff(ctx, configMap, opts)
if err != nil {
t.Fatal(err)
}

if entry.Action != UnchangedAction && entry.Subject == utils.FmtUnstructured(configMap) {
t.Errorf("Expected %s, got %s", UnchangedAction, entry.Action)
}
})

t.Run("diffs skips with meta", func(t *testing.T) {
// mutate in-cluster object
configMapClone := configMap.DeepCopy()
err = manager.client.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone)
if err != nil {
t.Fatal(err)
}

configMapClone.SetAnnotations(meta)
err = unstructured.SetNestedField(configMapClone.Object, "public-second-key", "data", "secondKey")
if err != nil {
t.Fatal(err)
}

entry, _, _, err := manager.Diff(ctx, configMapClone, opts)
if err != nil {
t.Fatal(err)
}

if entry.Action != SkippedAction && entry.Subject == utils.FmtUnstructured(configMapClone) {
t.Errorf("Expected %s, got %s", SkippedAction, entry.Action)
}
})

t.Run("diffs configures without meta", func(t *testing.T) {
// mutate in-cluster object
configMapClone := configMap.DeepCopy()
err = manager.client.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone)
if err != nil {
t.Fatal(err)
}

err = unstructured.SetNestedField(configMapClone.Object, "public-second-key", "data", "secondKey")
if err != nil {
t.Fatal(err)
}
configMapClone.SetManagedFields(nil)

entry, _, _, err := manager.Diff(ctx, configMapClone, opts)
if err != nil {
t.Fatal(err)
}

if entry.Action != ConfiguredAction && entry.Subject == utils.FmtUnstructured(configMapClone) {
t.Errorf("Expected %s, got %s", ConfiguredAction, entry.Action)
}
})
}

func TestDiff_Removals(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
Expand Down

0 comments on commit 7c90c14

Please sign in to comment.