Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ssa: Align ResourceManager.Diff skipping resources with ResourceManager.Apply #862

Merged
merged 2 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading