From 0f7f95b24ad5d492923980888f90211835a3f98c Mon Sep 17 00:00:00 2001 From: Karl Isenberg Date: Tue, 1 Mar 2022 15:48:02 -0800 Subject: [PATCH] fix: Make DependencyFilter handle DryRun DryRun skips WaitEvents. So the DependencyFilter needs to skip checking for reconciliation if DryRun is enabled. --- pkg/apply/applier.go | 10 +- pkg/apply/destroyer.go | 5 +- pkg/apply/filter/dependency-filter.go | 28 +- pkg/apply/filter/dependency-filter_test.go | 103 +++++-- test/e2e/dry_run_test.go | 301 +++++++++++++++++++++ test/e2e/e2e_test.go | 25 ++ 6 files changed, 426 insertions(+), 46 deletions(-) create mode 100644 test/e2e/dry_run_test.go diff --git a/pkg/apply/applier.go b/pkg/apply/applier.go index 4c42d153..35c4d3ba 100644 --- a/pkg/apply/applier.go +++ b/pkg/apply/applier.go @@ -141,8 +141,9 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec InvPolicy: options.InventoryPolicy, }, filter.DependencyFilter{ - TaskContext: taskContext, - Strategy: actuation.ActuationStrategyApply, + TaskContext: taskContext, + ActuationStrategy: actuation.ActuationStrategyApply, + DryRunStrategy: options.DryRunStrategy, }, } // Build list of prune validation filters. @@ -156,8 +157,9 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec LocalNamespaces: localNamespaces(invInfo, object.UnstructuredSetToObjMetadataSet(objects)), }, filter.DependencyFilter{ - TaskContext: taskContext, - Strategy: actuation.ActuationStrategyDelete, + TaskContext: taskContext, + ActuationStrategy: actuation.ActuationStrategyDelete, + DryRunStrategy: options.DryRunStrategy, }, } // Build list of apply mutators. diff --git a/pkg/apply/destroyer.go b/pkg/apply/destroyer.go index d16c0429..42f73800 100644 --- a/pkg/apply/destroyer.go +++ b/pkg/apply/destroyer.go @@ -147,8 +147,9 @@ func (d *Destroyer) Run(ctx context.Context, invInfo inventory.Info, options Des InvPolicy: options.InventoryPolicy, }, filter.DependencyFilter{ - TaskContext: taskContext, - Strategy: actuation.ActuationStrategyDelete, + TaskContext: taskContext, + ActuationStrategy: actuation.ActuationStrategyDelete, + DryRunStrategy: options.DryRunStrategy, }, } taskBuilder := &solver.TaskQueueBuilder{ diff --git a/pkg/apply/filter/dependency-filter.go b/pkg/apply/filter/dependency-filter.go index f826b77d..fae01adb 100644 --- a/pkg/apply/filter/dependency-filter.go +++ b/pkg/apply/filter/dependency-filter.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/cli-utils/pkg/apis/actuation" "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/object" ) @@ -24,8 +25,9 @@ const ( // DependencyFilter implements ValidationFilter interface to determine if an // object can be applied or deleted based on the status of it's dependencies. type DependencyFilter struct { - TaskContext *taskrunner.TaskContext - Strategy actuation.ActuationStrategy + TaskContext *taskrunner.TaskContext + ActuationStrategy actuation.ActuationStrategy + DryRunStrategy common.DryRunStrategy } const DependencyFilterName = "DependencyFilter" @@ -40,7 +42,7 @@ func (dnrf DependencyFilter) Name() string { func (dnrf DependencyFilter) Filter(obj *unstructured.Unstructured) (bool, string, error) { id := object.UnstructuredToObjMetadata(obj) - switch dnrf.Strategy { + switch dnrf.ActuationStrategy { case actuation.ActuationStrategyApply: // For apply, check dependencies (outgoing) for _, depID := range dnrf.TaskContext.Graph().Dependencies(id) { @@ -64,7 +66,7 @@ func (dnrf DependencyFilter) Filter(obj *unstructured.Unstructured) (bool, strin } } default: - panic(fmt.Sprintf("invalid filter strategy: %q", dnrf.Strategy)) + panic(fmt.Sprintf("invalid filter strategy: %q", dnrf.ActuationStrategy)) } return false, "", nil } @@ -91,9 +93,9 @@ func (dnrf DependencyFilter) filterByRelationStatus(id object.ObjMetadata, relat // Dependencies must have the same actuation strategy. // If there is a mismatch, skip both. - if status.Strategy != dnrf.Strategy { + if status.Strategy != dnrf.ActuationStrategy { return true, fmt.Sprintf("%s skipped because %s is scheduled for %s: %q", - strings.ToLower(dnrf.Strategy.String()), + strings.ToLower(dnrf.ActuationStrategy.String()), strings.ToLower(relationship.String()), strings.ToLower(status.Strategy.String()), id), nil @@ -103,7 +105,7 @@ func (dnrf DependencyFilter) filterByRelationStatus(id object.ObjMetadata, relat case actuation.ActuationPending: // If actuation is still pending, dependency sorting is probably broken. return false, "", fmt.Errorf("premature %s: %s %s actuation %s: %q", - strings.ToLower(dnrf.Strategy.String()), + strings.ToLower(dnrf.ActuationStrategy.String()), strings.ToLower(relationship.String()), strings.ToLower(status.Strategy.String()), strings.ToLower(status.Actuation.String()), @@ -112,7 +114,7 @@ func (dnrf DependencyFilter) filterByRelationStatus(id object.ObjMetadata, relat // Skip! return true, fmt.Sprintf("%s %s actuation %s: %q", strings.ToLower(relationship.String()), - strings.ToLower(dnrf.Strategy.String()), + strings.ToLower(dnrf.ActuationStrategy.String()), strings.ToLower(status.Actuation.String()), id), nil case actuation.ActuationSucceeded: @@ -124,11 +126,17 @@ func (dnrf DependencyFilter) filterByRelationStatus(id object.ObjMetadata, relat id) } + // DryRun skips WaitTasks, so reconcile status can be ignored + if dnrf.DryRunStrategy.ClientOrServerDryRun() { + // Don't skip! + return false, "", nil + } + switch status.Reconcile { case actuation.ReconcilePending: // If reconcile is still pending, dependency sorting is probably broken. return false, "", fmt.Errorf("premature %s: %s %s reconcile %s: %q", - strings.ToLower(dnrf.Strategy.String()), + strings.ToLower(dnrf.ActuationStrategy.String()), strings.ToLower(relationship.String()), strings.ToLower(status.Strategy.String()), strings.ToLower(status.Reconcile.String()), @@ -137,7 +145,7 @@ func (dnrf DependencyFilter) filterByRelationStatus(id object.ObjMetadata, relat // Skip! return true, fmt.Sprintf("%s %s reconcile %s: %q", strings.ToLower(relationship.String()), - strings.ToLower(dnrf.Strategy.String()), + strings.ToLower(dnrf.ActuationStrategy.String()), strings.ToLower(status.Reconcile.String()), id), nil case actuation.ReconcileSucceeded: diff --git a/pkg/apply/filter/dependency-filter_test.go b/pkg/apply/filter/dependency-filter_test.go index bb9ff927..196722e1 100644 --- a/pkg/apply/filter/dependency-filter_test.go +++ b/pkg/apply/filter/dependency-filter_test.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-utils/pkg/apis/actuation" "sigs.k8s.io/cli-utils/pkg/apply/taskrunner" + "sigs.k8s.io/cli-utils/pkg/common" "sigs.k8s.io/cli-utils/pkg/inventory" "sigs.k8s.io/cli-utils/pkg/object" ) @@ -43,15 +44,16 @@ var idB = object.ObjMetadata{ func TestDependencyFilter(t *testing.T) { tests := map[string]struct { - strategy actuation.ActuationStrategy - contextSetup func(*taskrunner.TaskContext) - id object.ObjMetadata - expectedFiltered bool - expectedReason string - expectedError error + dryRunStrategy common.DryRunStrategy + actuationStrategy actuation.ActuationStrategy + contextSetup func(*taskrunner.TaskContext) + id object.ObjMetadata + expectedFiltered bool + expectedReason string + expectedError error }{ "apply A (no deps)": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.InventoryManager().AddPendingApply(idA) @@ -62,7 +64,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "apply A (A -> B) when B is invalid": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idInvalid) @@ -76,7 +78,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "apply A (A -> B) before B is applied": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -88,7 +90,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: fmt.Errorf("premature apply: dependency apply actuation pending: %q", idB), }, "apply A (A -> B) before B is reconciled": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -105,7 +107,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: fmt.Errorf("premature apply: dependency apply reconcile pending: %q", idB), }, "apply A (A -> B) after B is reconciled": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -124,7 +126,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "apply A (A -> B) after B apply failed": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -143,7 +145,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "apply A (A -> B) after B apply skipped": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -162,7 +164,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "apply A (A -> B) after B reconcile failed": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -181,7 +183,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "apply A (A -> B) after B reconcile timeout": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -201,7 +203,7 @@ func TestDependencyFilter(t *testing.T) { }, // artificial use case: reconcile should only be skipped if apply failed or was skipped "apply A (A -> B) after B reconcile skipped": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -220,7 +222,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "apply A (A -> B) when B delete pending": { - strategy: actuation.ActuationStrategyApply, + actuationStrategy: actuation.ActuationStrategyApply, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -234,7 +236,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "delete B (no deps)": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idB) taskContext.InventoryManager().AddPendingDelete(idB) @@ -245,7 +247,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "delete B (A -> B) when A is invalid": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idInvalid) taskContext.Graph().AddVertex(idB) @@ -259,7 +261,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "delete B (A -> B) before A is deleted": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -271,7 +273,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: fmt.Errorf("premature delete: dependent delete actuation pending: %q", idA), }, "delete B (A -> B) before A is reconciled": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -288,7 +290,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: fmt.Errorf("premature delete: dependent delete reconcile pending: %q", idA), }, "delete B (A -> B) after A is reconciled": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -307,7 +309,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "delete B (A -> B) after A delete failed": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -326,7 +328,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "delete B (A -> B) after A delete skipped": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -346,7 +348,7 @@ func TestDependencyFilter(t *testing.T) { }, // artificial use case: delete reconcile can't fail, only timeout "delete B (A -> B) after A reconcile failed": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -365,7 +367,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "delete B (A -> B) after A reconcile timeout": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -385,7 +387,7 @@ func TestDependencyFilter(t *testing.T) { }, // artificial use case: reconcile should only be skipped if delete failed or was skipped "delete B (A -> B) after A reconcile skipped": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -404,7 +406,7 @@ func TestDependencyFilter(t *testing.T) { expectedError: nil, }, "delete B (A -> B) when A apply succeeded": { - strategy: actuation.ActuationStrategyDelete, + actuationStrategy: actuation.ActuationStrategyDelete, contextSetup: func(taskContext *taskrunner.TaskContext) { taskContext.Graph().AddVertex(idA) taskContext.Graph().AddVertex(idB) @@ -422,6 +424,46 @@ func TestDependencyFilter(t *testing.T) { expectedReason: fmt.Sprintf("delete skipped because dependent is scheduled for apply: %q", idA), expectedError: nil, }, + "DryRun: apply A (A -> B) when B apply reconcile pending": { + dryRunStrategy: common.DryRunClient, + actuationStrategy: actuation.ActuationStrategyApply, + contextSetup: func(taskContext *taskrunner.TaskContext) { + taskContext.Graph().AddVertex(idA) + taskContext.Graph().AddVertex(idB) + taskContext.Graph().AddEdge(idA, idB) + taskContext.InventoryManager().AddPendingApply(idA) + taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ + ObjectReference: inventory.ObjectReferenceFromObjMetadata(idB), + Strategy: actuation.ActuationStrategyApply, + Actuation: actuation.ActuationSucceeded, + Reconcile: actuation.ReconcilePending, + }) + }, + id: idA, + expectedFiltered: false, + expectedReason: "", + expectedError: nil, + }, + "DryRun: delete B (A -> B) when A delete reconcile pending": { + dryRunStrategy: common.DryRunClient, + actuationStrategy: actuation.ActuationStrategyDelete, + contextSetup: func(taskContext *taskrunner.TaskContext) { + taskContext.Graph().AddVertex(idA) + taskContext.Graph().AddVertex(idB) + taskContext.Graph().AddEdge(idA, idB) + taskContext.InventoryManager().AddPendingDelete(idB) + taskContext.InventoryManager().SetObjectStatus(actuation.ObjectStatus{ + ObjectReference: inventory.ObjectReferenceFromObjMetadata(idA), + Strategy: actuation.ActuationStrategyDelete, + Actuation: actuation.ActuationSucceeded, + Reconcile: actuation.ReconcilePending, + }) + }, + id: idB, + expectedFiltered: false, + expectedReason: "", + expectedError: nil, + }, } for name, tc := range tests { @@ -430,8 +472,9 @@ func TestDependencyFilter(t *testing.T) { tc.contextSetup(taskContext) filter := DependencyFilter{ - TaskContext: taskContext, - Strategy: tc.strategy, + TaskContext: taskContext, + ActuationStrategy: tc.actuationStrategy, + DryRunStrategy: tc.dryRunStrategy, } obj := defaultObj.DeepCopy() obj.SetGroupVersionKind(tc.id.GroupKind.WithVersion("v1")) diff --git a/test/e2e/dry_run_test.go b/test/e2e/dry_run_test.go new file mode 100644 index 00000000..06705c1d --- /dev/null +++ b/test/e2e/dry_run_test.go @@ -0,0 +1,301 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/apply" + "sigs.k8s.io/cli-utils/pkg/apply/event" + "sigs.k8s.io/cli-utils/pkg/common" + "sigs.k8s.io/cli-utils/pkg/inventory" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/cli-utils/pkg/testutil" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func dryRunTest(ctx context.Context, c client.Client, invConfig InventoryConfig, inventoryName, namespaceName string) { + By("Apply with DryRun") + applier := invConfig.ApplierFactoryFunc() + inventoryID := fmt.Sprintf("%s-%s", inventoryName, namespaceName) + + inventoryInfo := createInventoryInfo(invConfig, inventoryName, namespaceName, inventoryID) + + namespace1Name := fmt.Sprintf("%s-ns1", namespaceName) + + fields := struct{ Namespace string }{Namespace: namespace1Name} + namespace1Obj := templateToUnstructured(namespaceTemplate, fields) + podBObj := templateToUnstructured(podBTemplate, fields) + + // Dependency order: podB -> namespace1 + // Apply order: namespace1, podB + resources := []*unstructured.Unstructured{ + namespace1Obj, + podBObj, + } + + applierEvents := runCollect(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ + ReconcileTimeout: 2 * time.Minute, + EmitStatusEvents: true, + DryRunStrategy: common.DryRunClient, + })) + + expEvents := []testutil.ExpEvent{ + { + // InitTask + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + // InvAddTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-add-0", + Type: event.Started, + }, + }, + { + // InvAddTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-add-0", + Type: event.Finished, + }, + }, + { + // ApplyTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-0", + Type: event.Started, + }, + }, + { + // Create namespace + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-0", + Operation: event.Created, + Identifier: object.UnstructuredToObjMetadata(namespace1Obj), + Error: nil, + }, + }, + { + // ApplyTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-0", + Type: event.Finished, + }, + }, + { + // ApplyTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-1", + Type: event.Started, + }, + }, + { + // Create pod + EventType: event.ApplyType, + ApplyEvent: &testutil.ExpApplyEvent{ + GroupName: "apply-1", + Operation: event.Created, + Identifier: object.UnstructuredToObjMetadata(podBObj), + Error: nil, + }, + }, + { + // ApplyTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.ApplyAction, + GroupName: "apply-1", + Type: event.Finished, + }, + }, + // No Wait Tasks for Dry Run + { + // InvSetTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-set-0", + Type: event.Started, + }, + }, + { + // InvSetTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "inventory-set-0", + Type: event.Finished, + }, + }, + } + received := testutil.EventsToExpEvents(applierEvents) + + // handle required async NotFound StatusEvent for pod + expected := testutil.ExpEvent{ + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: object.UnstructuredToObjMetadata(podBObj), + Status: status.NotFoundStatus, + Error: nil, + }, + } + received, matches := testutil.RemoveEqualEvents(received, expected) + Expect(matches).To(BeNumerically(">=", 1), "unexpected number of %q status events for namespace", status.NotFoundStatus) + + // handle required async NotFound StatusEvent for namespace + expected = testutil.ExpEvent{ + EventType: event.StatusType, + StatusEvent: &testutil.ExpStatusEvent{ + Identifier: object.UnstructuredToObjMetadata(namespace1Obj), + Status: status.NotFoundStatus, + Error: nil, + }, + } + received, matches = testutil.RemoveEqualEvents(received, expected) + Expect(matches).To(BeNumerically(">=", 1), "unexpected number of %q status events for pod", status.NotFoundStatus) + + Expect(received).To(testutil.Equal(expEvents)) + + By("Verify pod NotFound") + assertUnstructuredDoesNotExist(ctx, c, podBObj) + + By("Verify inventory NotFound") + invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) + + By("Apply") + runWithNoErr(applier.Run(ctx, inventoryInfo, resources, apply.ApplierOptions{ + ReconcileTimeout: 2 * time.Minute, + })) + + By("Verify pod created") + assertUnstructuredExists(ctx, c, podBObj) + + By("Verify inventory size") + invConfig.InvSizeVerifyFunc(ctx, c, inventoryName, namespaceName, inventoryID, 2) + + By("Destroy with DryRun") + destroyer := invConfig.DestroyerFactoryFunc() + + destroyerEvents := runCollect(destroyer.Run(ctx, inventoryInfo, apply.DestroyerOptions{ + InventoryPolicy: inventory.PolicyAdoptIfNoInventory, + EmitStatusEvents: true, + DryRunStrategy: common.DryRunClient, + })) + + expEvents = []testutil.ExpEvent{ + { + // InitTask + EventType: event.InitType, + InitEvent: &testutil.ExpInitEvent{}, + }, + { + // PruneTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-0", + Type: event.Started, + }, + }, + { + // Delete pod + EventType: event.DeleteType, + DeleteEvent: &testutil.ExpDeleteEvent{ + GroupName: "prune-0", + Operation: event.Deleted, + Identifier: object.UnstructuredToObjMetadata(podBObj), + Error: nil, + }, + }, + { + // PruneTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-0", + Type: event.Finished, + }, + }, + { + // PruneTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-1", + Type: event.Started, + }, + }, + { + // Delete namespace + EventType: event.DeleteType, + DeleteEvent: &testutil.ExpDeleteEvent{ + GroupName: "prune-1", + Operation: event.Deleted, + Identifier: object.UnstructuredToObjMetadata(namespace1Obj), + Error: nil, + }, + }, + { + // PruneTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.DeleteAction, + GroupName: "prune-1", + Type: event.Finished, + }, + }, + // No Wait Tasks for Dry Run + { + // DeleteInvTask start + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "delete-inventory-0", + Type: event.Started, + }, + }, + { + // DeleteInvTask finished + EventType: event.ActionGroupType, + ActionGroupEvent: &testutil.ExpActionGroupEvent{ + Action: event.InventoryAction, + GroupName: "delete-inventory-0", + Type: event.Finished, + }, + }, + } + Expect(testutil.EventsToExpEvents(destroyerEvents)).To(testutil.Equal(expEvents)) + + By("Verify pod still exists") + assertUnstructuredExists(ctx, c, podBObj) + + By("Destroy") + runWithNoErr(destroyer.Run(ctx, inventoryInfo, apply.DestroyerOptions{ + InventoryPolicy: inventory.PolicyAdoptIfNoInventory, + })) + + By("Verify pod deleted") + assertUnstructuredDoesNotExist(ctx, c, podBObj) + + By("Verify inventory deleted") + invConfig.InvNotExistsFunc(ctx, c, inventoryName, namespaceName, inventoryID) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index f0027ef1..8bf7e36f 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -36,6 +36,7 @@ type applierFactoryFunc func() *apply.Applier type destroyerFactoryFunc func() *apply.Destroyer type invSizeVerifyFunc func(ctx context.Context, c client.Client, name, namespace, id string, count int) type invCountVerifyFunc func(ctx context.Context, c client.Client, namespace string, count int) +type invNotExistsFunc func(ctx context.Context, c client.Client, name, namespace, id string) type InventoryConfig struct { Strategy inventory.Strategy @@ -45,6 +46,7 @@ type InventoryConfig struct { DestroyerFactoryFunc destroyerFactoryFunc InvSizeVerifyFunc invSizeVerifyFunc InvCountVerifyFunc invCountVerifyFunc + InvNotExistsFunc invNotExistsFunc } const ( @@ -61,6 +63,7 @@ var inventoryConfigs = map[string]InventoryConfig{ DestroyerFactoryFunc: newDefaultInvDestroyer, InvSizeVerifyFunc: defaultInvSizeVerifyFunc, InvCountVerifyFunc: defaultInvCountVerifyFunc, + InvNotExistsFunc: defaultInvNotExistsFunc, }, CustomTypeInvConfig: { Strategy: inventory.NameStrategy, @@ -70,6 +73,7 @@ var inventoryConfigs = map[string]InventoryConfig{ DestroyerFactoryFunc: newCustomInvDestroyer, InvSizeVerifyFunc: customInvSizeVerifyFunc, InvCountVerifyFunc: customInvCountVerifyFunc, + InvNotExistsFunc: customInvNotExistsFunc, }, } @@ -163,6 +167,10 @@ var _ = Describe("Applier", func() { applyAndDestroyTest(ctx, c, invConfig, inventoryName, namespace.GetName()) }) + It("DryRun", func() { + dryRunTest(ctx, c, invConfig, inventoryName, namespace.GetName()) + }) + It("Deletion Prevention", func() { deletionPreventionTest(ctx, c, invConfig, inventoryName, namespace.GetName()) }) @@ -340,6 +348,15 @@ func newDefaultInvDestroyer() *apply.Destroyer { return newDestroyerFromInvFactory(inventory.ClusterClientFactory{}) } +func defaultInvNotExistsFunc(ctx context.Context, c client.Client, name, namespace, id string) { + var cmList v1.ConfigMapList + err := c.List(ctx, &cmList, + client.MatchingLabels(map[string]string{common.InventoryLabel: id}), + client.InNamespace(namespace)) + Expect(err).ToNot(HaveOccurred()) + Expect(cmList.Items).To(HaveLen(0), "expected inventory list to be empty") +} + func defaultInvSizeVerifyFunc(ctx context.Context, c client.Client, name, namespace, id string, count int) { var cmList v1.ConfigMapList err := c.List(ctx, &cmList, @@ -376,6 +393,14 @@ func newFactory() util.Factory { return util.NewFactory(matchVersionKubeConfigFlags) } +func customInvNotExistsFunc(ctx context.Context, c client.Client, name, namespace, id string) { + var u unstructured.Unstructured + u.SetGroupVersionKind(customprovider.InventoryGVK) + u.SetName(name) + u.SetNamespace(namespace) + assertUnstructuredDoesNotExist(ctx, c, &u) +} + func customInvSizeVerifyFunc(ctx context.Context, c client.Client, name, namespace, _ string, count int) { var u unstructured.Unstructured u.SetGroupVersionKind(customprovider.InventoryGVK)