diff --git a/e2e/default_ordering_test.go b/e2e/default_ordering_test.go new file mode 100644 index 00000000..8df90c32 --- /dev/null +++ b/e2e/default_ordering_test.go @@ -0,0 +1,225 @@ +package e2e + +import ( + "context" + "testing" + "time" + + flow "github.com/Azure/go-workflow" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + apiv1 "github.com/Azure/eno/api/v1" + fw "github.com/Azure/eno/e2e/framework" +) + +// TestResourceDependencyOrdering validates that Eno correctly handles the +// dependency ordering between a Secret and a Deployment that mounts it, +// WITHOUT explicit eno.azure.io/readiness-group annotations. +// +// The synthesizer outputs both a Secret and a Deployment (which mounts +// the Secret as a volume). The test verifies: +// - Both resources are created successfully +// - The Deployment becomes available (at least one ready pod) +// - Pods have zero restarts (no CrashLoopBackOff) +// - No container errors (waiting/terminated with failure) +func TestResourceDependencyOrdering(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + cli := fw.NewClient(t) + + synthName := fw.UniqueName("dep-order-synth") + compName := fw.UniqueName("dep-order-comp") + secretName := fw.UniqueName("dep-order-secret") + deployName := fw.UniqueName("dep-order-deploy") + + // Secret with NO readiness-group annotation. + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: "default"}, + StringData: map[string]string{"token": "test-value-123"}, + } + + // Deployment that mounts the Secret as a volume — NO readiness-group annotation. + replicas := int32(1) + deploy := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"}, + ObjectMeta: metav1.ObjectMeta{Name: deployName, Namespace: "default"}, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": deployName}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": deployName}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Image: "docker.io/busybox:1.36.1", + Command: []string{"sh", "-c", "cat /etc/secret-vol/token && sleep 3600"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "secret-vol", + MountPath: "/etc/secret-vol", + ReadOnly: true, + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "secret-vol", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + }}, + }, + }, + }, + } + + // Synthesizer outputs both Secret and Deployment. + // Eno should automatically resolve the dependency ordering. + synth := fw.NewMinimalSynthesizer(synthName, fw.WithCommand(fw.ToCommand(secret, deploy))) + comp := fw.NewComposition(compName, "default", + fw.WithSynthesizerRefs(apiv1.SynthesizerRef{Name: synthName})) + compKey := types.NamespacedName{Name: compName, Namespace: "default"} + + // --- Workflow steps --- + + createSynthesizer := fw.CreateStep(t, "createSynthesizer", cli, synth) + + createComposition := fw.CreateStep(t, "createComposition", cli, comp) + + waitCompositionReady := flow.Func("waitCompositionReady", func(ctx context.Context) error { + fw.WaitForCompositionReady(t, ctx, cli, compKey, 4*time.Minute) + t.Log("composition is Ready") + return nil + }) + + verifySecret := flow.Func("verifySecret", func(ctx context.Context) error { + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: "default"}, + } + fw.WaitForResourceExists(t, ctx, cli, s, 30*time.Second) + assert.Equal(t, "test-value-123", string(s.Data["token"]), + "secret should contain expected data") + t.Logf("secret %s exists with correct data", secretName) + return nil + }) + + verifyDeploymentReady := flow.Func("verifyDeploymentReady", func(ctx context.Context) error { + err := wait.PollUntilContextTimeout(ctx, 3*time.Second, 3*time.Minute, true, + func(ctx context.Context) (bool, error) { + d := &appsv1.Deployment{} + if err := cli.Get(ctx, types.NamespacedName{ + Name: deployName, Namespace: "default", + }, d); err != nil { + return false, nil + } + t.Logf("deployment %s: replicas=%d available=%d ready=%d", + deployName, + d.Status.Replicas, + d.Status.AvailableReplicas, + d.Status.ReadyReplicas) + return d.Status.AvailableReplicas >= 1, nil + }) + require.NoError(t, err, + "timed out waiting for deployment %s to have available replicas", deployName) + t.Logf("deployment %s is available", deployName) + return nil + }) + + verifyZeroRestarts := flow.Func("verifyZeroRestarts", func(ctx context.Context) error { + podList := &corev1.PodList{} + require.NoError(t, cli.List(ctx, podList, + client.InNamespace("default"), + client.MatchingLabels{"app": deployName})) + require.NotEmpty(t, podList.Items, + "expected at least one pod for deployment %s", deployName) + + for _, pod := range podList.Items { + for _, cs := range pod.Status.ContainerStatuses { + assert.Equal(t, int32(0), cs.RestartCount, + "pod %s container %s should have 0 restarts", pod.Name, cs.Name) + assert.True(t, cs.Ready, + "pod %s container %s should be ready", pod.Name, cs.Name) + t.Logf("pod %s container %s: restarts=%d ready=%v", + pod.Name, cs.Name, cs.RestartCount, cs.Ready) + } + } + return nil + }) + + verifyNoContainerErrors := flow.Func("verifyNoContainerErrors", func(ctx context.Context) error { + podList := &corev1.PodList{} + require.NoError(t, cli.List(ctx, podList, + client.InNamespace("default"), + client.MatchingLabels{"app": deployName})) + require.NotEmpty(t, podList.Items) + + for _, pod := range podList.Items { + for _, cs := range pod.Status.ContainerStatuses { + if cs.State.Waiting != nil { + t.Errorf("pod %s container %s is waiting: %s - %s", + pod.Name, cs.Name, cs.State.Waiting.Reason, cs.State.Waiting.Message) + } + if cs.State.Terminated != nil && cs.State.Terminated.ExitCode != 0 { + t.Errorf("pod %s container %s terminated with exit code %d: %s", + pod.Name, cs.Name, cs.State.Terminated.ExitCode, cs.State.Terminated.Reason) + } + } + } + t.Log("no container errors found") + return nil + }) + + deleteComposition := fw.DeleteStep(t, "deleteComposition", cli, comp) + + verifyResourcesDeleted := flow.Func("verifyResourcesDeleted", func(ctx context.Context) error { + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: deployName, Namespace: "default"}, + } + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: "default"}, + } + fw.WaitForResourceDeleted(t, ctx, cli, d, 90*time.Second) + fw.WaitForResourceDeleted(t, ctx, cli, s, 90*time.Second) + t.Log("managed resources (deployment + secret) deleted") + return nil + }) + + cleanupSynthesizer := fw.CleanupStep(t, "cleanupSynthesizer", cli, synth) + + // --- Wire the workflow DAG --- + + w := new(flow.Workflow) + w.Add( + flow.Step(createComposition).DependsOn(createSynthesizer), + flow.Step(waitCompositionReady).DependsOn(createComposition), + + // Parallel verification after composition is ready + flow.Step(verifySecret).DependsOn(waitCompositionReady), + flow.Step(verifyDeploymentReady).DependsOn(waitCompositionReady), + + // Pod-level checks after deployment is verified ready + flow.Step(verifyZeroRestarts).DependsOn(verifyDeploymentReady), + flow.Step(verifyNoContainerErrors).DependsOn(verifyDeploymentReady), + + // Cleanup after all verifications pass + flow.Step(deleteComposition).DependsOn( + verifySecret, verifyZeroRestarts, verifyNoContainerErrors), + flow.Step(verifyResourcesDeleted).DependsOn(deleteComposition), + flow.Step(cleanupSynthesizer).DependsOn(verifyResourcesDeleted), + ) + + require.NoError(t, w.Do(ctx)) +} diff --git a/internal/controllers/reconciliation/edgecase_test.go b/internal/controllers/reconciliation/edgecase_test.go index b3c28934..c7f11863 100644 --- a/internal/controllers/reconciliation/edgecase_test.go +++ b/internal/controllers/reconciliation/edgecase_test.go @@ -161,6 +161,10 @@ func TestLargeNamespaceDeletion(t *testing.T) { "kind": "Namespace", "metadata": map[string]any{ "name": "test", + "annotations": map[string]any{ + "eno.azure.io/readiness-group": "0", + "eno.azure.io/deletion-group": "0", + }, }, }, } @@ -169,6 +173,9 @@ func TestLargeNamespaceDeletion(t *testing.T) { output.Items = []*unstructured.Unstructured{ns} for i := 0; i < 500; i++ { + // Explicit readiness/deletion groups: Namespace/Secret/ConfigMap now + // default into the reserved [-100,-81] range, but this test wants the + // legacy "everything in group 0" behavior. cm := &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "v1", @@ -176,6 +183,10 @@ func TestLargeNamespaceDeletion(t *testing.T) { "metadata": map[string]any{ "name": fmt.Sprintf("test-%d", i), "namespace": ns.GetName(), + "annotations": map[string]any{ + "eno.azure.io/readiness-group": "0", + "eno.azure.io/deletion-group": "0", + }, }, }, } diff --git a/internal/controllers/reconciliation/ordering_test.go b/internal/controllers/reconciliation/ordering_test.go index c29509c3..9fb82b48 100644 --- a/internal/controllers/reconciliation/ordering_test.go +++ b/internal/controllers/reconciliation/ordering_test.go @@ -58,6 +58,11 @@ func TestReadinessGroups(t *testing.T) { "metadata": map[string]any{ "name": "test-obj-1", "namespace": "default", + // Explicit readiness-group: ConfigMap now defaults into the reserved + // [-100,-81] range; this test wants the legacy group-0 behavior. + "annotations": map[string]string{ + "eno.azure.io/readiness-group": "0", + }, }, "data": map[string]any{"image": s.Spec.Image}, }, diff --git a/internal/resource/cache_test.go b/internal/resource/cache_test.go index 7526ffd8..d4409e4a 100644 --- a/internal/resource/cache_test.go +++ b/internal/resource/cache_test.go @@ -212,8 +212,10 @@ func TestCacheResourceFilter(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "slice-1"}, Spec: apiv1.ResourceSliceSpec{ Resources: []apiv1.Manifest{ - {Manifest: `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "allowed", "namespace": "default", "labels": {"env": "prod"}}}`}, - {Manifest: `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "filtered", "namespace": "default", "labels": {"env": "dev"}}}`}, + // Explicit readiness-group: ConfigMap now defaults into the reserved + // [-100,-81] range; this test wants group-0 behavior. + {Manifest: `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "allowed", "namespace": "default", "labels": {"env": "prod"}, "annotations": {"eno.azure.io/readiness-group": "0"}}}`}, + {Manifest: `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "filtered", "namespace": "default", "labels": {"env": "dev"}, "annotations": {"eno.azure.io/readiness-group": "0"}}}`}, {Manifest: `{"apiVersion": "v1", "kind": "Pod", "metadata": {"name": "pod-prod", "namespace": "default", "labels": {"env": "prod"}}}`}, }, }, @@ -385,7 +387,9 @@ func TestCacheResourceFilterAlwaysTrue(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "slice-1"}, Spec: apiv1.ResourceSliceSpec{ Resources: []apiv1.Manifest{ - {Manifest: `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "resource-1", "namespace": "default"}}`}, + // Explicit readiness-group: ConfigMap now defaults into the reserved + // [-100,-81] range; this test wants group-0 behavior. + {Manifest: `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "resource-1", "namespace": "default", "annotations": {"eno.azure.io/readiness-group": "0"}}}`}, {Manifest: `{"apiVersion": "v1", "kind": "Pod", "metadata": {"name": "resource-2", "namespace": "default"}}`}, }, }, diff --git a/internal/resource/kind_ordering.go b/internal/resource/kind_ordering.go new file mode 100644 index 00000000..406923fa --- /dev/null +++ b/internal/resource/kind_ordering.go @@ -0,0 +1,38 @@ +package resource + +// managedCreateOrder maps Kind names (group-insensitive) to reserved +// readiness groups. We intentionally key on Kind alone: any resource +// whose Kind matches one of these names is treated as infrastructure +// and reconciled first, regardless of its API group. +// +// User-supplied readiness/deletion groups must be >= -60. +// Values in [-100, -60] are reserved for Eno-managed infrastructure defaults. +// Deletion groups are the negation of the create groups. +// Order derived from Helm's InstallOrder/UninstallOrder: +// https://github.com/helm/helm/blob/main/pkg/release/v1/util/kind_sorter.go +var managedCreateOrder = map[string]int{ + "PriorityClass": -100, + "Namespace": -100, + "NetworkPolicy": -99, + "ResourceQuota": -99, + "LimitRange": -99, + "PodSecurityPolicy": -98, + "PodDisruptionBudget": -98, + "ServiceAccount": -97, + "Secret": -96, + "SecretList": -96, + "ConfigMap": -96, + "StorageClass": -95, + "PersistentVolume": -94, + "PersistentVolumeClaim": -93, + "CustomResourceDefinition": -92, + "ClusterRole": -91, + "ClusterRoleList": -91, + "ClusterRoleBinding": -91, + "ClusterRoleBindingList": -91, + "Role": -90, + "RoleList": -90, + "RoleBinding": -90, + "RoleBindingList": -90, + "Service": -89, +} diff --git a/internal/resource/kind_ordering_test.go b/internal/resource/kind_ordering_test.go new file mode 100644 index 00000000..eb9c4216 --- /dev/null +++ b/internal/resource/kind_ordering_test.go @@ -0,0 +1,150 @@ +package resource + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestManagedCreateOrderCoversExpectedKinds(t *testing.T) { + expectedKinds := []string{ + "PriorityClass", "Namespace", "NetworkPolicy", "ResourceQuota", + "LimitRange", "PodSecurityPolicy", "PodDisruptionBudget", + "ServiceAccount", "Secret", "SecretList", "ConfigMap", + "StorageClass", "PersistentVolume", "PersistentVolumeClaim", + "CustomResourceDefinition", "ClusterRole", "ClusterRoleList", + "ClusterRoleBinding", "ClusterRoleBindingList", + "Role", "RoleList", "RoleBinding", "RoleBindingList", "Service", + } + for _, kind := range expectedKinds { + _, ok := managedCreateOrder[kind] + assert.True(t, ok, "expected kind %q to be in managedCreateOrder", kind) + } +} + +func TestManagedCreateOrderGroupRange(t *testing.T) { + for kind, grp := range managedCreateOrder { + assert.GreaterOrEqual(t, grp, -100, "kind %q group %d below minimum", kind, grp) + assert.LessOrEqual(t, grp, -81, "kind %q group %d above reserved max", kind, grp) + } +} + +func TestApplyDefaultOrdering_UnmanagedKindNotInMap(t *testing.T) { + unmanagedKinds := []string{ + "Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob", + "Ingress", "IngressClass", "HorizontalPodAutoscaler", + "MutatingWebhookConfiguration", "ValidatingWebhookConfiguration", + "APIService", "Pod", "ReplicaSet", "ReplicationController", + } + for _, kind := range unmanagedKinds { + t.Run(kind, func(t *testing.T) { + _, ok := managedCreateOrder[kind] + assert.False(t, ok, "kind %q should not be in managedCreateOrder", kind) + }) + } +} + +func TestNewResource_DefaultOrderingForManagedKind(t *testing.T) { + cases := []struct { + name string + manifest string + wantReadiness int + wantDeletion *int + }{ + { + name: "managed kind without annotations gets defaults", + manifest: `{"apiVersion":"v1","kind":"Secret", + "metadata":{"name":"s","namespace":"default"}}`, + wantReadiness: -96, + wantDeletion: intPtr(96), + }, + { + name: "user readiness annotation wins; deletion still defaulted", + manifest: `{"apiVersion":"v1","kind":"Secret", + "metadata":{"name":"s","namespace":"default", + "annotations":{"eno.azure.io/readiness-group":"5"}}}`, + wantReadiness: 5, + wantDeletion: intPtr(96), + }, + { + name: "user deletion annotation wins; readiness still defaulted", + manifest: `{"apiVersion":"v1","kind":"Secret", + "metadata":{"name":"s","namespace":"default", + "annotations":{"eno.azure.io/deletion-group":"10"}}}`, + wantReadiness: -96, + wantDeletion: intPtr(10), + }, + { + name: "user sets both annotations; both win", + manifest: `{"apiVersion":"v1","kind":"Secret", + "metadata":{"name":"s","namespace":"default", + "annotations":{"eno.azure.io/readiness-group":"5","eno.azure.io/deletion-group":"10"}}}`, + wantReadiness: 5, + wantDeletion: intPtr(10), + }, + { + name: "unmanaged kind untouched", + manifest: `{"apiVersion":"apps/v1","kind":"Deployment", + "metadata":{"name":"d","namespace":"default"}}`, + wantReadiness: 0, + wantDeletion: nil, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + u := &unstructured.Unstructured{} + require.NoError(t, u.UnmarshalJSON([]byte(tc.manifest))) + r, err := newResource(context.Background(), u, false) + require.NoError(t, err) + assert.Equal(t, tc.wantReadiness, r.readinessGroup) + if tc.wantDeletion == nil { + assert.Nil(t, r.deletionGroup) + } else { + require.NotNil(t, r.deletionGroup) + assert.Equal(t, *tc.wantDeletion, *r.deletionGroup) + } + }) + } +} + +func intPtr(i int) *int { return &i } + +func TestManagedOrderingPrecedence(t *testing.T) { + // Namespace/PriorityClass (-100) before everything + assert.Less(t, managedCreateOrder["Namespace"], managedCreateOrder["ServiceAccount"]) + // ServiceAccount (-97) before Secret/ConfigMap (-96) + assert.Less(t, managedCreateOrder["ServiceAccount"], managedCreateOrder["Secret"]) + assert.Less(t, managedCreateOrder["ServiceAccount"], managedCreateOrder["ConfigMap"]) + // StorageClass (-95) before PV (-94) before PVC (-93) + assert.Less(t, managedCreateOrder["StorageClass"], managedCreateOrder["PersistentVolume"]) + assert.Less(t, managedCreateOrder["PersistentVolume"], managedCreateOrder["PersistentVolumeClaim"]) + // PVC (-93) before CRD (-92) + assert.Less(t, managedCreateOrder["PersistentVolumeClaim"], managedCreateOrder["CustomResourceDefinition"]) + // CRD (-92) before ClusterRole (-91) + assert.Less(t, managedCreateOrder["CustomResourceDefinition"], managedCreateOrder["ClusterRole"]) + // ClusterRole (-91) before Role (-90) + assert.Less(t, managedCreateOrder["ClusterRole"], managedCreateOrder["Role"]) + // Role (-90) before Service (-89) + assert.Less(t, managedCreateOrder["Role"], managedCreateOrder["Service"]) +} + +func TestManagedOrderingDeletionPrecedence(t *testing.T) { + // Deletion group is negation of create group, so deletion precedence is reversed. + delGrp := func(kind string) int { return -managedCreateOrder[kind] } + + // Service (+89) < Role (+90) < ClusterRole (+91) < CRD (+92) < ... < Namespace (+100) + assert.Less(t, delGrp("Service"), delGrp("Role")) + assert.Less(t, delGrp("Role"), delGrp("ClusterRole")) + assert.Less(t, delGrp("ClusterRole"), delGrp("CustomResourceDefinition")) + assert.Less(t, delGrp("CustomResourceDefinition"), delGrp("PersistentVolumeClaim")) + assert.Less(t, delGrp("PersistentVolumeClaim"), delGrp("PersistentVolume")) + assert.Less(t, delGrp("PersistentVolume"), delGrp("StorageClass")) + assert.Less(t, delGrp("StorageClass"), delGrp("ConfigMap")) + assert.Less(t, delGrp("ConfigMap"), delGrp("ServiceAccount")) + assert.Less(t, delGrp("ServiceAccount"), delGrp("Namespace")) +} + + diff --git a/internal/resource/resource.go b/internal/resource/resource.go index 2c664ba2..8c248525 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -24,6 +24,11 @@ import ( "k8s.io/apimachinery/pkg/types" ) +const ( + ReservedReadinessGroupLowerBound = -100 + ReservedReadinessGroupUpperBound = -60 +) + var patchGVK = schema.GroupVersionKind{ Group: "eno.azure.io", Version: "v1", @@ -198,6 +203,11 @@ func newResource(ctx context.Context, parsed *unstructured.Unstructured, strict if err != nil { logger.Info("invalid readiness group - ignoring") } else { + if rg >= ReservedReadinessGroupLowerBound && rg <= ReservedReadinessGroupUpperBound { + logger.Info(fmt.Sprintf("WARNING: user-supplied readiness-group is in Eno reserved range [%d, %d]", + ReservedReadinessGroupLowerBound, ReservedReadinessGroupUpperBound), + "kind", res.GVK.Kind, "readinessGroup", rg) + } res.readinessGroup = rg } } @@ -213,6 +223,10 @@ func newResource(ctx context.Context, parsed *unstructured.Unstructured, strict if err != nil { logger.Info("invalid deletion group - ignoring") } else { + if rg >= -ReservedReadinessGroupUpperBound && rg <= -ReservedReadinessGroupLowerBound { + logger.Info(fmt.Sprintf("WARNING: user-supplied deletion-group is in Eno reserved range [%d, %d]", -ReservedReadinessGroupUpperBound, -ReservedReadinessGroupLowerBound), + "kind", res.GVK.Kind, "deletionGroup", rg) + } res.deletionGroup = &rg } } @@ -241,6 +255,28 @@ func newResource(ctx context.Context, parsed *unstructured.Unstructured, strict res.ReadinessChecks = append(res.ReadinessChecks, check) } sort.Slice(res.ReadinessChecks, func(i, j int) bool { return res.ReadinessChecks[i].Name < res.ReadinessChecks[j].Name }) + // This indicates that this is an infrastructure Kind + if defaultGrp, ok := managedCreateOrder[res.GVK.Kind]; ok { + if _, ok := anno[readinessGroupKey]; !ok { + logger.Info("assigning default readiness group to managed kind", + "kind", res.GVK.Kind, "defaultGroup", defaultGrp) + res.readinessGroup = defaultGrp + } else { + logger.Info("user-specified readiness group present, skipping default for managed kind", + "kind", res.GVK.Kind, "readinessGroup", res.readinessGroup) + } + + if _, ok := anno[deletionGroupKey]; !ok { + logger.Info("assigning default deletion group to managed kind", + "kind", res.GVK.Kind, "defaultGroup", defaultGrp) + delGroup := -defaultGrp + res.deletionGroup = &delGroup + } else { + logger.Info("user-specified deletion group present, skipping default for managed kind", + "kind", res.GVK.Kind, "deletionGroup", *res.deletionGroup) + } + } + logger.Info("resource created successfully") return res, nil } diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go index e559b06d..35f7fe5a 100644 --- a/internal/resource/resource_test.go +++ b/internal/resource/resource_test.go @@ -30,8 +30,8 @@ var newResourceTests = []struct { { Name: "configmap", Manifest: `{ - "apiVersion": "v1", - "kind": "ConfigMap", + "apiVersion": "example.io/v1", + "kind": "Widget", "metadata": { "name": "foo", "annotations": { @@ -51,14 +51,14 @@ var newResourceTests = []struct { } }`, Assert: func(t *testing.T, r *Snapshot) { - assert.Equal(t, schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, r.GVK) + assert.Equal(t, schema.GroupVersionKind{Group: "example.io", Version: "v1", Kind: "Widget"}, r.GVK) assert.Len(t, r.ReadinessChecks, 2) assert.Equal(t, time.Second*10, r.ReconcileInterval.Duration) assert.Equal(t, Ref{ Name: "foo", Namespace: "", - Group: "", - Kind: "ConfigMap", + Group: "example.io", + Kind: "Widget", }, r.Ref) assert.True(t, r.Disable) assert.True(t, r.DisableUpdates) @@ -110,8 +110,8 @@ var newResourceTests = []struct { { Name: "zero-readiness-group", Manifest: `{ - "apiVersion": "v1", - "kind": "ConfigMap", + "apiVersion": "example.io/v1", + "kind": "Widget", "metadata": { "name": "foo", "annotations": { @@ -213,8 +213,8 @@ var newResourceTests = []struct { { Name: "negative-readiness-group", Manifest: `{ - "apiVersion": "v1", - "kind": "ConfigMap", + "apiVersion": "example.io/v1", + "kind": "Widget", "metadata": { "name": "foo", "annotations": {