diff --git a/ssa/manager_wait.go b/ssa/manager_wait.go index 36f44bce..fdcf4a7f 100644 --- a/ssa/manager_wait.go +++ b/ssa/manager_wait.go @@ -136,6 +136,12 @@ func (m *ResourceManager) WaitForSet(set object.ObjMetadataSet, opts WaitOptions errs = append(errs, builder.String()) case errors.Is(ctx.Err(), context.DeadlineExceeded) && lastStatus[id].Status != status.CurrentStatus: var builder strings.Builder + if utils.IsSuspended(lastStatus[id].Resource) { + // skip suspended resources that are not in a failed state + // as they are not expected to be reconciled and + // their observed generation will always be behind + continue + } builder.WriteString(fmt.Sprintf("%s status: '%s'", utils.FmtObjMetadata(rs.Identifier), lastStatus[id].Status)) if rs.Error != nil { diff --git a/ssa/manager_wait_test.go b/ssa/manager_wait_test.go index 29726753..8b0355ae 100644 --- a/ssa/manager_wait_test.go +++ b/ssa/manager_wait_test.go @@ -42,6 +42,9 @@ func TestWaitForSet(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() + waitOps := DefaultWaitOptions() + waitOps.Timeout = timeout + id := generateName("wait") objects, err := readManifest("testdata/test5.yaml", id) if err != nil { @@ -99,7 +102,41 @@ func TestWaitForSet(t *testing.T) { t.Fatal(err) } - if err := manager.WaitForSet(changeSet.ToObjMetadataSet(), DefaultWaitOptions()); err != nil { + if err := manager.WaitForSet(changeSet.ToObjMetadataSet(), waitOps); err != nil { + t.Errorf("wait error: %v", err) + } + }) + + t.Run("skips suspended CRs", func(t *testing.T) { + clusterCR := &unstructured.Unstructured{} + clusterCR.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "testing.fluxcd.io", + Kind: "ClusterTest", + Version: "v1", + }) + if err := manager.client.Get(ctx, client.ObjectKeyFromObject(cr), clusterCR); err != nil { + t.Fatal(err) + } + + clusterCR.SetManagedFields(nil) + err = unstructured.SetNestedField(clusterCR.Object, true, "spec", "suspend") + if err != nil { + t.Fatal(err) + } + + opts := &client.SubResourcePatchOptions{ + PatchOptions: client.PatchOptions{ + FieldManager: manager.owner.Field, + }, + } + + // Suspend the CR and thus bumping the generation. + if err := manager.client.Patch(ctx, clusterCR, client.Apply, opts); err != nil { + t.Fatal(err) + } + + metaSet := object.UnstructuredSetToObjMetadataSet([]*unstructured.Unstructured{clusterCR}) + if err := manager.WaitForSet(metaSet, waitOps); err != nil { t.Errorf("wait error: %v", err) } }) diff --git a/ssa/testdata/test5.yaml b/ssa/testdata/test5.yaml index 6dc9f213..a1f8245d 100644 --- a/ssa/testdata/test5.yaml +++ b/ssa/testdata/test5.yaml @@ -31,6 +31,8 @@ spec: spec: description: TestSpec defines the desired state of a test run properties: + suspend: + type: boolean type: description: Type of test type: string diff --git a/ssa/utils/is.go b/ssa/utils/is.go index 38d90b1d..72b56d0f 100644 --- a/ssa/utils/is.go +++ b/ssa/utils/is.go @@ -63,3 +63,21 @@ func IsKustomization(object *unstructured.Unstructured) bool { func IsSecret(object *unstructured.Unstructured) bool { return strings.ToLower(object.GetKind()) == "secret" && object.GetAPIVersion() == "v1" } + +// IsSuspended returns true if the given Flux object has '.spec.suspend' set to true. +func IsSuspended(object *unstructured.Unstructured) bool { + if object == nil { + return false + } + + if !strings.Contains(object.GetAPIVersion(), "fluxcd") { + return false + } + + suspended, found, err := unstructured.NestedBool(object.Object, "spec", "suspend") + if err != nil || !found { + return false + } + + return suspended +}