mirror of https://github.com/fluxcd/cli-utils.git
				
				
				
			Merge pull request #555 from karlkfi/karl-dep-filter2
feat: Add dependency filter
This commit is contained in:
		
						commit
						5c6134aeac
					
				|  | @ -14,6 +14,7 @@ import ( | |||
| 	"k8s.io/client-go/discovery" | ||||
| 	"k8s.io/client-go/dynamic" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apis/actuation" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/cache" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/event" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/filter" | ||||
|  | @ -125,6 +126,10 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec | |||
| 		} | ||||
| 		klog.V(4).Infof("calculated %d apply objs; %d prune objs", len(applyObjs), len(pruneObjs)) | ||||
| 
 | ||||
| 		// Build a TaskContext for passing info between tasks
 | ||||
| 		resourceCache := cache.NewResourceCacheMap() | ||||
| 		taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) | ||||
| 
 | ||||
| 		// Fetch the queue (channel) of tasks that should be executed.
 | ||||
| 		klog.V(4).Infoln("applier building task queue...") | ||||
| 		// Build list of apply validation filters.
 | ||||
|  | @ -135,6 +140,10 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec | |||
| 				Inv:       invInfo, | ||||
| 				InvPolicy: options.InventoryPolicy, | ||||
| 			}, | ||||
| 			filter.DependencyFilter{ | ||||
| 				TaskContext: taskContext, | ||||
| 				Strategy:    actuation.ActuationStrategyApply, | ||||
| 			}, | ||||
| 		} | ||||
| 		// Build list of prune validation filters.
 | ||||
| 		pruneFilters := []filter.ValidationFilter{ | ||||
|  | @ -146,9 +155,12 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec | |||
| 			filter.LocalNamespacesFilter{ | ||||
| 				LocalNamespaces: localNamespaces(invInfo, object.UnstructuredSetToObjMetadataSet(objects)), | ||||
| 			}, | ||||
| 			filter.DependencyFilter{ | ||||
| 				TaskContext: taskContext, | ||||
| 				Strategy:    actuation.ActuationStrategyDelete, | ||||
| 			}, | ||||
| 		} | ||||
| 		// Build list of apply mutators.
 | ||||
| 		resourceCache := cache.NewResourceCacheMap() | ||||
| 		applyMutators := []mutator.Interface{ | ||||
| 			&mutator.ApplyTimeMutator{ | ||||
| 				Client:        a.client, | ||||
|  | @ -184,7 +196,7 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec | |||
| 			WithApplyObjects(applyObjs). | ||||
| 			WithPruneObjects(pruneObjs). | ||||
| 			WithInventory(invInfo). | ||||
| 			Build(opts) | ||||
| 			Build(taskContext, opts) | ||||
| 
 | ||||
| 		klog.V(4).Infof("validation errors: %d", len(vCollector.Errors)) | ||||
| 		klog.V(4).Infof("invalid objects: %d", len(vCollector.InvalidIds)) | ||||
|  | @ -206,9 +218,6 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.Info, objects objec | |||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// Build a TaskContext for passing info between tasks
 | ||||
| 		taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) | ||||
| 
 | ||||
| 		// Register invalid objects to be retained in the inventory, if present.
 | ||||
| 		for _, id := range vCollector.InvalidIds { | ||||
| 			taskContext.AddInvalidObject(id) | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import ( | |||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	cmdutil "k8s.io/kubectl/pkg/cmd/util" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apis/actuation" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/cache" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/event" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/filter" | ||||
|  | @ -129,6 +130,10 @@ func (d *Destroyer) Run(ctx context.Context, invInfo inventory.Info, options Des | |||
| 		} | ||||
| 		validator.Validate(deleteObjs) | ||||
| 
 | ||||
| 		// Build a TaskContext for passing info between tasks
 | ||||
| 		resourceCache := cache.NewResourceCacheMap() | ||||
| 		taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) | ||||
| 
 | ||||
| 		klog.V(4).Infoln("destroyer building task queue...") | ||||
| 		dynamicClient, err := d.factory.DynamicClient() | ||||
| 		if err != nil { | ||||
|  | @ -141,6 +146,10 @@ func (d *Destroyer) Run(ctx context.Context, invInfo inventory.Info, options Des | |||
| 				Inv:       invInfo, | ||||
| 				InvPolicy: options.InventoryPolicy, | ||||
| 			}, | ||||
| 			filter.DependencyFilter{ | ||||
| 				TaskContext: taskContext, | ||||
| 				Strategy:    actuation.ActuationStrategyDelete, | ||||
| 			}, | ||||
| 		} | ||||
| 		taskBuilder := &solver.TaskQueueBuilder{ | ||||
| 			Pruner:        d.pruner, | ||||
|  | @ -165,7 +174,7 @@ func (d *Destroyer) Run(ctx context.Context, invInfo inventory.Info, options Des | |||
| 		taskQueue := taskBuilder. | ||||
| 			WithPruneObjects(deleteObjs). | ||||
| 			WithInventory(invInfo). | ||||
| 			Build(opts) | ||||
| 			Build(taskContext, opts) | ||||
| 
 | ||||
| 		klog.V(4).Infof("validation errors: %d", len(vCollector.Errors)) | ||||
| 		klog.V(4).Infof("invalid objects: %d", len(vCollector.InvalidIds)) | ||||
|  | @ -187,10 +196,6 @@ func (d *Destroyer) Run(ctx context.Context, invInfo inventory.Info, options Des | |||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// Build a TaskContext for passing info between tasks
 | ||||
| 		resourceCache := cache.NewResourceCacheMap() | ||||
| 		taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache) | ||||
| 
 | ||||
| 		// Register invalid objects to be retained in the inventory, if present.
 | ||||
| 		for _, id := range vCollector.InvalidIds { | ||||
| 			taskContext.AddInvalidObject(id) | ||||
|  |  | |||
|  | @ -0,0 +1,152 @@ | |||
| // Copyright 2020 The Kubernetes Authors.
 | ||||
| // SPDX-License-Identifier: Apache-2.0
 | ||||
| 
 | ||||
| package filter | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"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/object" | ||||
| ) | ||||
| 
 | ||||
| //go:generate stringer -type=Relationship -linecomment
 | ||||
| type Relationship int | ||||
| 
 | ||||
| const ( | ||||
| 	RelationshipDependent  Relationship = iota // Dependent
 | ||||
| 	RelationshipDependency                     // Dependency
 | ||||
| ) | ||||
| 
 | ||||
| // 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 | ||||
| } | ||||
| 
 | ||||
| const DependencyFilterName = "DependencyFilter" | ||||
| 
 | ||||
| // Name returns the name of the filter for logs and events.
 | ||||
| func (dnrf DependencyFilter) Name() string { | ||||
| 	return DependencyFilterName | ||||
| } | ||||
| 
 | ||||
| // Filter returns true if the specified object should be skipped because at
 | ||||
| // least one of its dependencies is Not Found or Not Reconciled.
 | ||||
| func (dnrf DependencyFilter) Filter(obj *unstructured.Unstructured) (bool, string, error) { | ||||
| 	id := object.UnstructuredToObjMetadata(obj) | ||||
| 
 | ||||
| 	switch dnrf.Strategy { | ||||
| 	case actuation.ActuationStrategyApply: | ||||
| 		// For apply, check dependencies (outgoing)
 | ||||
| 		for _, depID := range dnrf.TaskContext.Graph().Dependencies(id) { | ||||
| 			skip, reason, err := dnrf.filterByRelationStatus(depID, RelationshipDependency) | ||||
| 			if err != nil { | ||||
| 				return false, "", err | ||||
| 			} | ||||
| 			if skip { | ||||
| 				return skip, reason, nil | ||||
| 			} | ||||
| 		} | ||||
| 	case actuation.ActuationStrategyDelete: | ||||
| 		// For delete, check dependents (incoming)
 | ||||
| 		for _, depID := range dnrf.TaskContext.Graph().Dependents(id) { | ||||
| 			skip, reason, err := dnrf.filterByRelationStatus(depID, RelationshipDependent) | ||||
| 			if err != nil { | ||||
| 				return false, "", err | ||||
| 			} | ||||
| 			if skip { | ||||
| 				return skip, reason, nil | ||||
| 			} | ||||
| 		} | ||||
| 	default: | ||||
| 		panic(fmt.Sprintf("invalid filter strategy: %q", dnrf.Strategy)) | ||||
| 	} | ||||
| 	return false, "", nil | ||||
| } | ||||
| 
 | ||||
| func (dnrf DependencyFilter) filterByRelationStatus(id object.ObjMetadata, relationship Relationship) (bool, string, error) { | ||||
| 	// Dependency on an invalid object is considered an invalid dependency, making both objects invalid.
 | ||||
| 	// For applies: don't prematurely apply something that depends on something that hasn't been applied (because invalid).
 | ||||
| 	// For deletes: don't prematurely delete something that depends on something that hasn't been deleted (because invalid).
 | ||||
| 	// These can't be caught be subsequent checks, because invalid objects aren't in the inventory.
 | ||||
| 	if dnrf.TaskContext.IsInvalidObject(id) { | ||||
| 		// Skip!
 | ||||
| 		return true, fmt.Sprintf("%s invalid: %q", | ||||
| 			strings.ToLower(relationship.String()), | ||||
| 			id), nil | ||||
| 	} | ||||
| 
 | ||||
| 	status, found := dnrf.TaskContext.InventoryManager().ObjectStatus(id) | ||||
| 	if !found { | ||||
| 		// Status is registered during planning.
 | ||||
| 		// So if status is not found, the object is external (NYI) or invalid.
 | ||||
| 		return false, "", fmt.Errorf("unknown %s actuation strategy: %v", | ||||
| 			strings.ToLower(relationship.String()), id) | ||||
| 	} | ||||
| 
 | ||||
| 	// Dependencies must have the same actuation strategy.
 | ||||
| 	// If there is a mismatch, skip both.
 | ||||
| 	if status.Strategy != dnrf.Strategy { | ||||
| 		return true, fmt.Sprintf("%s skipped because %s is scheduled for %s: %q", | ||||
| 			strings.ToLower(dnrf.Strategy.String()), | ||||
| 			strings.ToLower(relationship.String()), | ||||
| 			strings.ToLower(status.Strategy.String()), | ||||
| 			id), nil | ||||
| 	} | ||||
| 
 | ||||
| 	switch status.Actuation { | ||||
| 	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(relationship.String()), | ||||
| 			strings.ToLower(status.Strategy.String()), | ||||
| 			strings.ToLower(status.Actuation.String()), | ||||
| 			id) | ||||
| 	case actuation.ActuationSkipped, actuation.ActuationFailed: | ||||
| 		// Skip!
 | ||||
| 		return true, fmt.Sprintf("%s %s actuation %s: %q", | ||||
| 			strings.ToLower(relationship.String()), | ||||
| 			strings.ToLower(dnrf.Strategy.String()), | ||||
| 			strings.ToLower(status.Actuation.String()), | ||||
| 			id), nil | ||||
| 	case actuation.ActuationSucceeded: | ||||
| 		// Don't skip!
 | ||||
| 	default: | ||||
| 		return false, "", fmt.Errorf("invalid %s apply status %q: %q", | ||||
| 			strings.ToLower(relationship.String()), | ||||
| 			strings.ToLower(status.Actuation.String()), | ||||
| 			id) | ||||
| 	} | ||||
| 
 | ||||
| 	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(relationship.String()), | ||||
| 			strings.ToLower(status.Strategy.String()), | ||||
| 			strings.ToLower(status.Reconcile.String()), | ||||
| 			id) | ||||
| 	case actuation.ReconcileSkipped, actuation.ReconcileFailed, actuation.ReconcileTimeout: | ||||
| 		// Skip!
 | ||||
| 		return true, fmt.Sprintf("%s %s reconcile %s: %q", | ||||
| 			strings.ToLower(relationship.String()), | ||||
| 			strings.ToLower(dnrf.Strategy.String()), | ||||
| 			strings.ToLower(status.Reconcile.String()), | ||||
| 			id), nil | ||||
| 	case actuation.ReconcileSucceeded: | ||||
| 		// Don't skip!
 | ||||
| 	default: | ||||
| 		return false, "", fmt.Errorf("invalid dependency reconcile status %q: %q", | ||||
| 			strings.ToLower(status.Reconcile.String()), id) | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't skip!
 | ||||
| 	return false, "", nil | ||||
| } | ||||
|  | @ -0,0 +1,451 @@ | |||
| // Copyright 2019 The Kubernetes Authors.
 | ||||
| // SPDX-License-Identifier: Apache-2.0
 | ||||
| 
 | ||||
| package filter | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"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/inventory" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/object" | ||||
| ) | ||||
| 
 | ||||
| var idInvalid = object.ObjMetadata{ | ||||
| 	GroupKind: schema.GroupKind{ | ||||
| 		Kind: "", // required
 | ||||
| 	}, | ||||
| 	Name: "invalid", // required
 | ||||
| } | ||||
| 
 | ||||
| var idA = object.ObjMetadata{ | ||||
| 	GroupKind: schema.GroupKind{ | ||||
| 		Group: "group-a", | ||||
| 		Kind:  "kind-a", | ||||
| 	}, | ||||
| 	Name:      "name-a", | ||||
| 	Namespace: "namespace-a", | ||||
| } | ||||
| 
 | ||||
| var idB = object.ObjMetadata{ | ||||
| 	GroupKind: schema.GroupKind{ | ||||
| 		Group: "group-b", | ||||
| 		Kind:  "kind-b", | ||||
| 	}, | ||||
| 	Name:      "name-b", | ||||
| 	Namespace: "namespace-b", | ||||
| } | ||||
| 
 | ||||
| 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 | ||||
| 	}{ | ||||
| 		"apply A (no deps)": { | ||||
| 			strategy: actuation.ActuationStrategyApply, | ||||
| 			contextSetup: func(taskContext *taskrunner.TaskContext) { | ||||
| 				taskContext.Graph().AddVertex(idA) | ||||
| 				taskContext.InventoryManager().AddPendingApply(idA) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: false, | ||||
| 			expectedReason:   "", | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"apply A (A -> B) when B is invalid": { | ||||
| 			strategy: actuation.ActuationStrategyApply, | ||||
| 			contextSetup: func(taskContext *taskrunner.TaskContext) { | ||||
| 				taskContext.Graph().AddVertex(idA) | ||||
| 				taskContext.Graph().AddVertex(idInvalid) | ||||
| 				taskContext.Graph().AddEdge(idA, idInvalid) | ||||
| 				taskContext.InventoryManager().AddPendingApply(idA) | ||||
| 				taskContext.AddInvalidObject(idInvalid) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependency invalid: %q", idInvalid), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"apply A (A -> B) before B is applied": { | ||||
| 			strategy: 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().AddPendingApply(idB) | ||||
| 			}, | ||||
| 			id:            idA, | ||||
| 			expectedError: fmt.Errorf("premature apply: dependency apply actuation pending: %q", idB), | ||||
| 		}, | ||||
| 		"apply A (A -> B) before B is reconciled": { | ||||
| 			strategy: 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, | ||||
| 			expectedError: fmt.Errorf("premature apply: dependency apply reconcile pending: %q", idB), | ||||
| 		}, | ||||
| 		"apply A (A -> B) after B is reconciled": { | ||||
| 			strategy: 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.ReconcileSucceeded, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: false, | ||||
| 			expectedReason:   "", | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"apply A (A -> B) after B apply failed": { | ||||
| 			strategy: 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.ActuationFailed, | ||||
| 					Reconcile:       actuation.ReconcilePending, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependency apply actuation failed: %q", idB), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"apply A (A -> B) after B apply skipped": { | ||||
| 			strategy: 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.ActuationSkipped, | ||||
| 					Reconcile:       actuation.ReconcileSkipped, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependency apply actuation skipped: %q", idB), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"apply A (A -> B) after B reconcile failed": { | ||||
| 			strategy: 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.ReconcileFailed, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependency apply reconcile failed: %q", idB), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"apply A (A -> B) after B reconcile timeout": { | ||||
| 			strategy: 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.ReconcileTimeout, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependency apply reconcile timeout: %q", idB), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		// 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, | ||||
| 			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.ReconcileSkipped, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependency apply reconcile skipped: %q", idB), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"apply A (A -> B) when B delete pending": { | ||||
| 			strategy: 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().AddPendingDelete(idB) | ||||
| 			}, | ||||
| 			id:               idA, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("apply skipped because dependency is scheduled for delete: %q", idB), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"delete B (no deps)": { | ||||
| 			strategy: actuation.ActuationStrategyDelete, | ||||
| 			contextSetup: func(taskContext *taskrunner.TaskContext) { | ||||
| 				taskContext.Graph().AddVertex(idB) | ||||
| 				taskContext.InventoryManager().AddPendingDelete(idB) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: false, | ||||
| 			expectedReason:   "", | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"delete B (A -> B) when A is invalid": { | ||||
| 			strategy: actuation.ActuationStrategyDelete, | ||||
| 			contextSetup: func(taskContext *taskrunner.TaskContext) { | ||||
| 				taskContext.Graph().AddVertex(idInvalid) | ||||
| 				taskContext.Graph().AddVertex(idB) | ||||
| 				taskContext.Graph().AddEdge(idInvalid, idB) | ||||
| 				taskContext.InventoryManager().AddPendingDelete(idB) | ||||
| 				taskContext.AddInvalidObject(idInvalid) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependent invalid: %q", idInvalid), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"delete B (A -> B) before A is deleted": { | ||||
| 			strategy: 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().AddPendingDelete(idA) | ||||
| 			}, | ||||
| 			id:            idB, | ||||
| 			expectedError: fmt.Errorf("premature delete: dependent delete actuation pending: %q", idA), | ||||
| 		}, | ||||
| 		"delete B (A -> B) before A is reconciled": { | ||||
| 			strategy: 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, | ||||
| 			expectedError: fmt.Errorf("premature delete: dependent delete reconcile pending: %q", idA), | ||||
| 		}, | ||||
| 		"delete B (A -> B) after A is reconciled": { | ||||
| 			strategy: 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.ReconcileSucceeded, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: false, | ||||
| 			expectedReason:   "", | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"delete B (A -> B) after A delete failed": { | ||||
| 			strategy: 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.ActuationFailed, | ||||
| 					Reconcile:       actuation.ReconcilePending, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependent delete actuation failed: %q", idA), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"delete B (A -> B) after A delete skipped": { | ||||
| 			strategy: 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.ActuationSkipped, | ||||
| 					Reconcile:       actuation.ReconcileSkipped, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependent delete actuation skipped: %q", idA), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		// artificial use case: delete reconcile can't fail, only timeout
 | ||||
| 		"delete B (A -> B) after A reconcile failed": { | ||||
| 			strategy: 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.ReconcileFailed, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependent delete reconcile failed: %q", idA), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"delete B (A -> B) after A reconcile timeout": { | ||||
| 			strategy: 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.ReconcileTimeout, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependent delete reconcile timeout: %q", idA), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		// 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, | ||||
| 			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.ReconcileSkipped, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("dependent delete reconcile skipped: %q", idA), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 		"delete B (A -> B) when A apply succeeded": { | ||||
| 			strategy: 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.ActuationStrategyApply, | ||||
| 					Actuation:       actuation.ActuationSucceeded, | ||||
| 					Reconcile:       actuation.ReconcileSucceeded, | ||||
| 				}) | ||||
| 			}, | ||||
| 			id:               idB, | ||||
| 			expectedFiltered: true, | ||||
| 			expectedReason:   fmt.Sprintf("delete skipped because dependent is scheduled for apply: %q", idA), | ||||
| 			expectedError:    nil, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for name, tc := range tests { | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			taskContext := taskrunner.NewTaskContext(nil, nil) | ||||
| 			tc.contextSetup(taskContext) | ||||
| 
 | ||||
| 			filter := DependencyFilter{ | ||||
| 				TaskContext: taskContext, | ||||
| 				Strategy:    tc.strategy, | ||||
| 			} | ||||
| 			obj := defaultObj.DeepCopy() | ||||
| 			obj.SetGroupVersionKind(tc.id.GroupKind.WithVersion("v1")) | ||||
| 			obj.SetName(tc.id.Name) | ||||
| 			obj.SetNamespace(tc.id.Namespace) | ||||
| 
 | ||||
| 			filtered, reason, err := filter.Filter(obj) | ||||
| 			if tc.expectedError != nil { | ||||
| 				require.EqualError(t, err, tc.expectedError.Error()) | ||||
| 				return | ||||
| 			} | ||||
| 			require.NoError(t, err) | ||||
| 			assert.Equal(t, tc.expectedFiltered, filtered) | ||||
| 			assert.Equal(t, tc.expectedReason, reason) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| // Code generated by "stringer -type=Relationship -linecomment"; DO NOT EDIT.
 | ||||
| 
 | ||||
| package filter | ||||
| 
 | ||||
| import "strconv" | ||||
| 
 | ||||
| func _() { | ||||
| 	// An "invalid array index" compiler error signifies that the constant values have changed.
 | ||||
| 	// Re-run the stringer command to generate them again.
 | ||||
| 	var x [1]struct{} | ||||
| 	_ = x[RelationshipDependent-0] | ||||
| 	_ = x[RelationshipDependency-1] | ||||
| } | ||||
| 
 | ||||
| const _Relationship_name = "DependentDependency" | ||||
| 
 | ||||
| var _Relationship_index = [...]uint8{0, 9, 19} | ||||
| 
 | ||||
| func (i Relationship) String() string { | ||||
| 	if i < 0 || i >= Relationship(len(_Relationship_index)-1) { | ||||
| 		return "Relationship(" + strconv.FormatInt(int64(i), 10) + ")" | ||||
| 	} | ||||
| 	return _Relationship_name[_Relationship_index[i]:_Relationship_index[i+1]] | ||||
| } | ||||
|  | @ -119,7 +119,7 @@ func (t *TaskQueueBuilder) WithPruneObjects(pruneObjs object.UnstructuredSet) *T | |||
| } | ||||
| 
 | ||||
| // Build returns the queue of tasks that have been created
 | ||||
| func (t *TaskQueueBuilder) Build(o Options) *TaskQueue { | ||||
| func (t *TaskQueueBuilder) Build(taskContext *taskrunner.TaskContext, o Options) *TaskQueue { | ||||
| 	var tasks []taskrunner.Task | ||||
| 
 | ||||
| 	// reset counters
 | ||||
|  | @ -127,11 +127,40 @@ func (t *TaskQueueBuilder) Build(o Options) *TaskQueue { | |||
| 	t.pruneCounter = 0 | ||||
| 	t.waitCounter = 0 | ||||
| 
 | ||||
| 	// Filter objects that failed earlier validation
 | ||||
| 	applyObjs := t.Collector.FilterInvalidObjects(t.applyObjs) | ||||
| 	pruneObjs := t.Collector.FilterInvalidObjects(t.pruneObjs) | ||||
| 
 | ||||
| 	// Merge applyObjs & pruneObjs and graph them together.
 | ||||
| 	// This detects implicit and explicit dependencies.
 | ||||
| 	// Invalid dependency annotations will be treated as validation errors.
 | ||||
| 	allObjs := make(object.UnstructuredSet, 0, len(applyObjs)+len(pruneObjs)) | ||||
| 	allObjs = append(allObjs, applyObjs...) | ||||
| 	allObjs = append(allObjs, pruneObjs...) | ||||
| 	g, err := graph.DependencyGraph(allObjs) | ||||
| 	if err != nil { | ||||
| 		t.Collector.Collect(err) | ||||
| 	} | ||||
| 	// Store graph for use by DependencyFilter
 | ||||
| 	taskContext.SetGraph(g) | ||||
| 	// Sort objects into phases (apply order).
 | ||||
| 	// Cycles will be treated as validation errors.
 | ||||
| 	idSetList, err := g.Sort() | ||||
| 	if err != nil { | ||||
| 		t.Collector.Collect(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Filter objects with cycles or invalid dependency annotations
 | ||||
| 	applyObjs = t.Collector.FilterInvalidObjects(applyObjs) | ||||
| 	pruneObjs = t.Collector.FilterInvalidObjects(pruneObjs) | ||||
| 
 | ||||
| 	if len(applyObjs) > 0 { | ||||
| 		klog.V(2).Infoln("adding inventory add task (%d objects)", len(applyObjs)) | ||||
| 		// Register actuation plan in the inventory
 | ||||
| 		for _, id := range object.UnstructuredSetToObjMetadataSet(applyObjs) { | ||||
| 			taskContext.InventoryManager().AddPendingApply(id) | ||||
| 		} | ||||
| 
 | ||||
| 		klog.V(2).Infof("adding inventory add task (%d objects)", len(applyObjs)) | ||||
| 		tasks = append(tasks, &task.InvAddTask{ | ||||
| 			TaskName:  "inventory-add-0", | ||||
| 			InvClient: t.InvClient, | ||||
|  | @ -140,18 +169,10 @@ func (t *TaskQueueBuilder) Build(o Options) *TaskQueue { | |||
| 			DryRun:    o.DryRunStrategy, | ||||
| 		}) | ||||
| 
 | ||||
| 		// Create a dependency graph, sort, and flatten into phases.
 | ||||
| 		applySets, err := graph.SortObjs(applyObjs) | ||||
| 		if err != nil { | ||||
| 			t.Collector.Collect(err) | ||||
| 		} | ||||
| 		// Filter idSetList down to just apply objects
 | ||||
| 		applySets := graph.HydrateSetList(idSetList, applyObjs) | ||||
| 
 | ||||
| 		for _, applySet := range applySets { | ||||
| 			// filter again, because sorting may have added more invalid objects.
 | ||||
| 			applySet = t.Collector.FilterInvalidObjects(applySet) | ||||
| 			if len(applySet) == 0 { | ||||
| 				continue | ||||
| 			} | ||||
| 			tasks = append(tasks, | ||||
| 				t.newApplyTask(applySet, t.ApplyFilters, t.ApplyMutators, o)) | ||||
| 			// dry-run skips wait tasks
 | ||||
|  | @ -164,18 +185,18 @@ func (t *TaskQueueBuilder) Build(o Options) *TaskQueue { | |||
| 	} | ||||
| 
 | ||||
| 	if o.Prune && len(pruneObjs) > 0 { | ||||
| 		// Create a dependency graph, sort (in reverse), and flatten into phases.
 | ||||
| 		pruneSets, err := graph.ReverseSortObjs(pruneObjs) | ||||
| 		if err != nil { | ||||
| 			t.Collector.Collect(err) | ||||
| 		// Register actuation plan in the inventory
 | ||||
| 		for _, id := range object.UnstructuredSetToObjMetadataSet(pruneObjs) { | ||||
| 			taskContext.InventoryManager().AddPendingDelete(id) | ||||
| 		} | ||||
| 
 | ||||
| 		// Filter idSetList down to just prune objects
 | ||||
| 		pruneSets := graph.HydrateSetList(idSetList, pruneObjs) | ||||
| 
 | ||||
| 		// Reverse apply order to get prune order
 | ||||
| 		graph.ReverseSetList(pruneSets) | ||||
| 
 | ||||
| 		for _, pruneSet := range pruneSets { | ||||
| 			// filter again, because sorting may have added more invalid objects.
 | ||||
| 			pruneSet = t.Collector.FilterInvalidObjects(pruneSet) | ||||
| 			if len(pruneSet) == 0 { | ||||
| 				continue | ||||
| 			} | ||||
| 			tasks = append(tasks, | ||||
| 				t.newPruneTask(pruneSet, t.PruneFilters, o)) | ||||
| 			// dry-run skips wait tasks
 | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import ( | |||
| 	"github.com/google/go-cmp/cmp/cmpopts" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apis/actuation" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/prune" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/task" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner" | ||||
|  | @ -133,10 +134,11 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 		"abc-123", "default", "test")) | ||||
| 
 | ||||
| 	testCases := map[string]struct { | ||||
| 		applyObjs     []*unstructured.Unstructured | ||||
| 		options       Options | ||||
| 		expectedTasks []taskrunner.Task | ||||
| 		expectedError error | ||||
| 		applyObjs      []*unstructured.Unstructured | ||||
| 		options        Options | ||||
| 		expectedTasks  []taskrunner.Task | ||||
| 		expectedError  error | ||||
| 		expectedStatus []actuation.ObjectStatus | ||||
| 	}{ | ||||
| 		"no resources, no apply or wait tasks": { | ||||
| 			applyObjs: []*unstructured.Unstructured{}, | ||||
|  | @ -184,6 +186,16 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resource with no timeout": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -226,6 +238,24 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resources with reconcile timeout": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -272,6 +302,24 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resources with reconcile timeout and dryrun": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -313,6 +361,24 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					DryRun: common.DryRunClient, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resources with reconcile timeout and server-dryrun": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -354,6 +420,24 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					DryRun: common.DryRunServer, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["default-pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resources including CRD": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -413,6 +497,32 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"no wait with CRDs if it is a dryrun": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -463,6 +573,32 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					DryRun: common.DryRunClient, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"resources in namespace creates multiple apply tasks": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -522,6 +658,32 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"deployment depends on secret creates multiple tasks": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -579,6 +741,24 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"cyclic dependency returns error": { | ||||
| 			applyObjs: []*unstructured.Unstructured{ | ||||
|  | @ -629,9 +809,10 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 				InvClient: fakeInvClient, | ||||
| 				Collector: vCollector, | ||||
| 			} | ||||
| 			taskContext := taskrunner.NewTaskContext(nil, nil) | ||||
| 			tq := tqb.WithInventory(invInfo). | ||||
| 				WithApplyObjects(tc.applyObjs). | ||||
| 				Build(tc.options) | ||||
| 				Build(taskContext, tc.options) | ||||
| 			err := vCollector.ToError() | ||||
| 			if tc.expectedError != nil { | ||||
| 				assert.EqualError(t, err, tc.expectedError.Error()) | ||||
|  | @ -639,6 +820,9 @@ func TestTaskQueueBuilder_ApplyBuild(t *testing.T) { | |||
| 			} | ||||
| 			assert.NoError(t, err) | ||||
| 			asserter.Equal(t, tc.expectedTasks, tq.tasks) | ||||
| 
 | ||||
| 			actualStatus := taskContext.InventoryManager().Inventory().Status.Objects | ||||
| 			testutil.AssertEqual(t, tc.expectedStatus, actualStatus) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | @ -656,10 +840,11 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 		"abc-123", "default", "test")) | ||||
| 
 | ||||
| 	testCases := map[string]struct { | ||||
| 		pruneObjs     []*unstructured.Unstructured | ||||
| 		options       Options | ||||
| 		expectedTasks []taskrunner.Task | ||||
| 		expectedError error | ||||
| 		pruneObjs      []*unstructured.Unstructured | ||||
| 		options        Options | ||||
| 		expectedTasks  []taskrunner.Task | ||||
| 		expectedError  error | ||||
| 		expectedStatus []actuation.ObjectStatus | ||||
| 	}{ | ||||
| 		"no resources, no apply or prune tasks": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{}, | ||||
|  | @ -701,6 +886,16 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["default-pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resources, one prune task, one wait task": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -734,6 +929,24 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["default-pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"dependent resources, two prune tasks, two wait tasks": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -781,6 +994,24 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"single resource with prune timeout has wait task": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -814,6 +1045,16 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resources with prune timeout and server-dryrun": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -846,6 +1087,24 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					DryRun: common.DryRunServer, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["default-pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"multiple resources including CRD": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -895,6 +1154,32 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"no wait with CRDs if it is a dryrun": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -935,6 +1220,32 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					DryRun: common.DryRunClient, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"resources in namespace creates multiple apply tasks": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -983,6 +1294,32 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"cyclic dependency": { | ||||
| 			pruneObjs: []*unstructured.Unstructured{ | ||||
|  | @ -1084,9 +1421,10 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 				InvClient: fakeInvClient, | ||||
| 				Collector: vCollector, | ||||
| 			} | ||||
| 			taskContext := taskrunner.NewTaskContext(nil, nil) | ||||
| 			tq := tqb.WithInventory(invInfo). | ||||
| 				WithPruneObjects(tc.pruneObjs). | ||||
| 				Build(tc.options) | ||||
| 				Build(taskContext, tc.options) | ||||
| 			err := vCollector.ToError() | ||||
| 			if tc.expectedError != nil { | ||||
| 				assert.EqualError(t, err, tc.expectedError.Error()) | ||||
|  | @ -1094,6 +1432,365 @@ func TestTaskQueueBuilder_PruneBuild(t *testing.T) { | |||
| 			} | ||||
| 			assert.NoError(t, err) | ||||
| 			asserter.Equal(t, tc.expectedTasks, tq.tasks) | ||||
| 
 | ||||
| 			actualStatus := taskContext.InventoryManager().Inventory().Status.Objects | ||||
| 			testutil.AssertEqual(t, tc.expectedStatus, actualStatus) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestTaskQueueBuilder_ApplyPruneBuild(t *testing.T) { | ||||
| 	// Use a custom Asserter to customize the comparison options
 | ||||
| 	asserter := testutil.NewAsserter( | ||||
| 		cmpopts.EquateErrors(), | ||||
| 		waitTaskComparer(), | ||||
| 		fakeClientComparer(), | ||||
| 		inventoryInfoComparer(), | ||||
| 	) | ||||
| 
 | ||||
| 	invInfo := inventory.WrapInventoryInfoObj(newInvObject( | ||||
| 		"abc-123", "default", "test")) | ||||
| 
 | ||||
| 	testCases := map[string]struct { | ||||
| 		inventoryIDs   object.ObjMetadataSet | ||||
| 		applyObjs      object.UnstructuredSet | ||||
| 		pruneObjs      object.UnstructuredSet | ||||
| 		options        Options | ||||
| 		expectedTasks  []taskrunner.Task | ||||
| 		expectedError  error | ||||
| 		expectedStatus []actuation.ObjectStatus | ||||
| 	}{ | ||||
| 		"two resources, one apply, one prune": { | ||||
| 			inventoryIDs: object.ObjMetadataSet{ | ||||
| 				testutil.ToIdentifier(t, resources["secret"]), | ||||
| 			}, | ||||
| 			applyObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 			}, | ||||
| 			pruneObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 			}, | ||||
| 			options: Options{Prune: true}, | ||||
| 			expectedTasks: []taskrunner.Task{ | ||||
| 				&task.InvAddTask{ | ||||
| 					TaskName:  "inventory-add-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					Objects: object.UnstructuredSet{ | ||||
| 						testutil.Unstructured(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&task.ApplyTask{ | ||||
| 					TaskName: "apply-0", | ||||
| 					Objects: []*unstructured.Unstructured{ | ||||
| 						testutil.Unstructured(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&taskrunner.WaitTask{ | ||||
| 					TaskName: "wait-0", | ||||
| 					Ids: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 					Condition: taskrunner.AllCurrent, | ||||
| 				}, | ||||
| 				&task.PruneTask{ | ||||
| 					TaskName: "prune-0", | ||||
| 					Objects: []*unstructured.Unstructured{ | ||||
| 						testutil.Unstructured(t, resources["secret"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&taskrunner.WaitTask{ | ||||
| 					TaskName: "wait-1", | ||||
| 					Ids: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					Condition: taskrunner.AllNotFound, | ||||
| 				}, | ||||
| 				&task.InvSetTask{ | ||||
| 					TaskName:  "inventory-set-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					PrevInventory: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"prune disabled": { | ||||
| 			inventoryIDs: object.ObjMetadataSet{ | ||||
| 				testutil.ToIdentifier(t, resources["secret"]), | ||||
| 			}, | ||||
| 			applyObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 			}, | ||||
| 			pruneObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 			}, | ||||
| 			options: Options{Prune: false}, | ||||
| 			expectedTasks: []taskrunner.Task{ | ||||
| 				&task.InvAddTask{ | ||||
| 					TaskName:  "inventory-add-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					Objects: object.UnstructuredSet{ | ||||
| 						testutil.Unstructured(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&task.ApplyTask{ | ||||
| 					TaskName: "apply-0", | ||||
| 					Objects: []*unstructured.Unstructured{ | ||||
| 						testutil.Unstructured(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&taskrunner.WaitTask{ | ||||
| 					TaskName: "wait-0", | ||||
| 					Ids: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 					Condition: taskrunner.AllCurrent, | ||||
| 				}, | ||||
| 				&task.InvSetTask{ | ||||
| 					TaskName:  "inventory-set-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					PrevInventory: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		// This use case returns in a task plan that would cause a dependency
 | ||||
| 		// to be deleted. This is remediated by the DependencyFilter at
 | ||||
| 		// apply-time, by skipping both the apply and prune.
 | ||||
| 		// This test does not verify the DependencyFilter tho, just that the
 | ||||
| 		// dependency was discovered between apply & prune objects.
 | ||||
| 		"dependency: apply -> prune": { | ||||
| 			inventoryIDs: object.ObjMetadataSet{ | ||||
| 				testutil.ToIdentifier(t, resources["secret"]), | ||||
| 			}, | ||||
| 			applyObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 			}, | ||||
| 			pruneObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 			}, | ||||
| 			options: Options{Prune: true}, | ||||
| 			expectedTasks: []taskrunner.Task{ | ||||
| 				&task.InvAddTask{ | ||||
| 					TaskName:  "inventory-add-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					Objects: object.UnstructuredSet{ | ||||
| 						testutil.Unstructured(t, resources["deployment"], | ||||
| 							testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&task.ApplyTask{ | ||||
| 					TaskName: "apply-0", | ||||
| 					Objects: []*unstructured.Unstructured{ | ||||
| 						testutil.Unstructured(t, resources["deployment"], | ||||
| 							testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&taskrunner.WaitTask{ | ||||
| 					TaskName: "wait-0", | ||||
| 					Ids: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 					Condition: taskrunner.AllCurrent, | ||||
| 				}, | ||||
| 				&task.PruneTask{ | ||||
| 					TaskName: "prune-0", | ||||
| 					Objects: []*unstructured.Unstructured{ | ||||
| 						testutil.Unstructured(t, resources["secret"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&taskrunner.WaitTask{ | ||||
| 					TaskName: "wait-1", | ||||
| 					Ids: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					Condition: taskrunner.AllNotFound, | ||||
| 				}, | ||||
| 				&task.InvSetTask{ | ||||
| 					TaskName:  "inventory-set-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					PrevInventory: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		// This use case returns in a task plan that would cause a dependency
 | ||||
| 		// to be applied. This is fine.
 | ||||
| 		// This test just verifies that the  dependency was discovered between
 | ||||
| 		// prune & apply objects.
 | ||||
| 		"dependency: prune -> apply": { | ||||
| 			inventoryIDs: object.ObjMetadataSet{ | ||||
| 				testutil.ToIdentifier(t, resources["secret"]), | ||||
| 			}, | ||||
| 			applyObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 			}, | ||||
| 			pruneObjs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["secret"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), | ||||
| 			}, | ||||
| 			options: Options{Prune: true}, | ||||
| 			expectedTasks: []taskrunner.Task{ | ||||
| 				&task.InvAddTask{ | ||||
| 					TaskName:  "inventory-add-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					Objects: object.UnstructuredSet{ | ||||
| 						testutil.Unstructured(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&task.ApplyTask{ | ||||
| 					TaskName: "apply-0", | ||||
| 					Objects: []*unstructured.Unstructured{ | ||||
| 						testutil.Unstructured(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&taskrunner.WaitTask{ | ||||
| 					TaskName: "wait-0", | ||||
| 					Ids: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 					Condition: taskrunner.AllCurrent, | ||||
| 				}, | ||||
| 				&task.PruneTask{ | ||||
| 					TaskName: "prune-0", | ||||
| 					Objects: []*unstructured.Unstructured{ | ||||
| 						testutil.Unstructured(t, resources["secret"], | ||||
| 							testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), | ||||
| 					}, | ||||
| 				}, | ||||
| 				&taskrunner.WaitTask{ | ||||
| 					TaskName: "wait-1", | ||||
| 					Ids: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					Condition: taskrunner.AllNotFound, | ||||
| 				}, | ||||
| 				&task.InvSetTask{ | ||||
| 					TaskName:  "inventory-set-0", | ||||
| 					InvClient: &inventory.FakeClient{}, | ||||
| 					InvInfo:   invInfo, | ||||
| 					PrevInventory: object.ObjMetadataSet{ | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			expectedStatus: []actuation.ObjectStatus{ | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyApply, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 				{ | ||||
| 					ObjectReference: inventory.ObjectReferenceFromObjMetadata( | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					), | ||||
| 					Strategy:  actuation.ActuationStrategyDelete, | ||||
| 					Actuation: actuation.ActuationPending, | ||||
| 					Reconcile: actuation.ReconcilePending, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for tn, tc := range testCases { | ||||
| 		t.Run(tn, func(t *testing.T) { | ||||
| 			mapper := testutil.NewFakeRESTMapper() | ||||
| 			// inject mapper & pruner for equality comparison
 | ||||
| 			for _, t := range tc.expectedTasks { | ||||
| 				switch typedTask := t.(type) { | ||||
| 				case *task.ApplyTask: | ||||
| 					typedTask.Mapper = mapper | ||||
| 				case *task.PruneTask: | ||||
| 					typedTask.Pruner = &prune.Pruner{} | ||||
| 				case *taskrunner.WaitTask: | ||||
| 					typedTask.Mapper = mapper | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			fakeInvClient := inventory.NewFakeClient(tc.inventoryIDs) | ||||
| 			vCollector := &validation.Collector{} | ||||
| 			tqb := TaskQueueBuilder{ | ||||
| 				Pruner:    pruner, | ||||
| 				Mapper:    mapper, | ||||
| 				InvClient: fakeInvClient, | ||||
| 				Collector: vCollector, | ||||
| 			} | ||||
| 			taskContext := taskrunner.NewTaskContext(nil, nil) | ||||
| 			tq := tqb.WithInventory(invInfo). | ||||
| 				WithApplyObjects(tc.applyObjs). | ||||
| 				WithPruneObjects(tc.pruneObjs). | ||||
| 				Build(taskContext, tc.options) | ||||
| 
 | ||||
| 			err := vCollector.ToError() | ||||
| 			if tc.expectedError != nil { | ||||
| 				assert.EqualError(t, err, tc.expectedError.Error()) | ||||
| 				return | ||||
| 			} | ||||
| 			assert.NoError(t, err) | ||||
| 
 | ||||
| 			asserter.Equal(t, tc.expectedTasks, tq.tasks) | ||||
| 
 | ||||
| 			actualStatus := taskContext.InventoryManager().Inventory().Status.Objects | ||||
| 			testutil.AssertEqual(t, tc.expectedStatus, actualStatus) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import ( | |||
| 	"sigs.k8s.io/cli-utils/pkg/apply/event" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/inventory" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/object" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/object/graph" | ||||
| ) | ||||
| 
 | ||||
| // NewTaskContext returns a new TaskContext
 | ||||
|  | @ -20,6 +21,7 @@ func NewTaskContext(eventChannel chan event.Event, resourceCache cache.ResourceC | |||
| 		inventoryManager: inventory.NewManager(), | ||||
| 		abandonedObjects: make(map[object.ObjMetadata]struct{}), | ||||
| 		invalidObjects:   make(map[object.ObjMetadata]struct{}), | ||||
| 		graph:            graph.New(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -32,6 +34,7 @@ type TaskContext struct { | |||
| 	inventoryManager *inventory.Manager | ||||
| 	abandonedObjects map[object.ObjMetadata]struct{} | ||||
| 	invalidObjects   map[object.ObjMetadata]struct{} | ||||
| 	graph            *graph.Graph | ||||
| } | ||||
| 
 | ||||
| func (tc *TaskContext) TaskChannel() chan TaskResult { | ||||
|  | @ -50,6 +53,14 @@ func (tc *TaskContext) InventoryManager() *inventory.Manager { | |||
| 	return tc.inventoryManager | ||||
| } | ||||
| 
 | ||||
| func (tc *TaskContext) Graph() *graph.Graph { | ||||
| 	return tc.graph | ||||
| } | ||||
| 
 | ||||
| func (tc *TaskContext) SetGraph(g *graph.Graph) { | ||||
| 	tc.graph = g | ||||
| } | ||||
| 
 | ||||
| // SendEvent sends an event on the event channel
 | ||||
| func (tc *TaskContext) SendEvent(e event.Event) { | ||||
| 	klog.V(5).Infof("sending event: %s", e) | ||||
|  |  | |||
|  | @ -393,3 +393,55 @@ func (tc *Manager) SetPendingReconcile(id object.ObjMetadata) error { | |||
| func (tc *Manager) PendingReconciles() object.ObjMetadataSet { | ||||
| 	return tc.ObjectsWithReconcileStatus(actuation.ReconcilePending) | ||||
| } | ||||
| 
 | ||||
| // IsPendingApply returns true if the object pending apply
 | ||||
| func (tc *Manager) IsPendingApply(id object.ObjMetadata) bool { | ||||
| 	objStatus, found := tc.ObjectStatus(id) | ||||
| 	if !found { | ||||
| 		return false | ||||
| 	} | ||||
| 	return objStatus.Strategy == actuation.ActuationStrategyApply && | ||||
| 		objStatus.Actuation == actuation.ActuationPending | ||||
| } | ||||
| 
 | ||||
| // AddPendingApply registers that the object is pending apply
 | ||||
| func (tc *Manager) AddPendingApply(id object.ObjMetadata) { | ||||
| 	tc.SetObjectStatus(actuation.ObjectStatus{ | ||||
| 		ObjectReference: ObjectReferenceFromObjMetadata(id), | ||||
| 		Strategy:        actuation.ActuationStrategyApply, | ||||
| 		Actuation:       actuation.ActuationPending, | ||||
| 		Reconcile:       actuation.ReconcilePending, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // PendingApplies returns all the objects that are pending apply
 | ||||
| func (tc *Manager) PendingApplies() object.ObjMetadataSet { | ||||
| 	return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyApply, | ||||
| 		actuation.ActuationPending) | ||||
| } | ||||
| 
 | ||||
| // IsPendingDelete returns true if the object pending delete
 | ||||
| func (tc *Manager) IsPendingDelete(id object.ObjMetadata) bool { | ||||
| 	objStatus, found := tc.ObjectStatus(id) | ||||
| 	if !found { | ||||
| 		return false | ||||
| 	} | ||||
| 	return objStatus.Strategy == actuation.ActuationStrategyDelete && | ||||
| 		objStatus.Actuation == actuation.ActuationPending | ||||
| } | ||||
| 
 | ||||
| // AddPendingDelete registers that the object is pending delete
 | ||||
| func (tc *Manager) AddPendingDelete(id object.ObjMetadata) { | ||||
| 	tc.SetObjectStatus(actuation.ObjectStatus{ | ||||
| 		ObjectReference: ObjectReferenceFromObjMetadata(id), | ||||
| 		Strategy:        actuation.ActuationStrategyDelete, | ||||
| 		Actuation:       actuation.ActuationPending, | ||||
| 		Reconcile:       actuation.ReconcilePending, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // PendingDeletes returns all the objects that are pending delete
 | ||||
| func (tc *Manager) PendingDeletes() object.ObjMetadataSet { | ||||
| 	return tc.ObjectsWithActuationStatus(actuation.ActuationStrategyDelete, | ||||
| 		actuation.ActuationPending) | ||||
| } | ||||
|  |  | |||
|  | @ -19,25 +19,19 @@ import ( | |||
| 	"sigs.k8s.io/cli-utils/pkg/ordering" | ||||
| ) | ||||
| 
 | ||||
| // SortObjs returns a slice of the sets of objects to apply (in order).
 | ||||
| // Each of the objects in an apply set is applied together. The order of
 | ||||
| // the returned applied sets is a topological ordering of the sets to apply.
 | ||||
| // Returns an single empty apply set if there are no objects to apply.
 | ||||
| func SortObjs(objs object.UnstructuredSet) ([]object.UnstructuredSet, error) { | ||||
| 	var objSets []object.UnstructuredSet | ||||
| // DependencyGraph returns a new graph, populated with the supplied objects as
 | ||||
| // vetices and edges built from their dependencies.
 | ||||
| func DependencyGraph(objs object.UnstructuredSet) (*Graph, error) { | ||||
| 	g := New() | ||||
| 	if len(objs) == 0 { | ||||
| 		return objSets, nil | ||||
| 		return g, nil | ||||
| 	} | ||||
| 	var errors []error | ||||
| 
 | ||||
| 	// Convert to IDs (same length & order as objs)
 | ||||
| 	// This is simply an optimiation to avoid repeating obj -> id conversion.
 | ||||
| 	ids := object.UnstructuredSetToObjMetadataSet(objs) | ||||
| 	// Create the graph, and build a map of object metadata to the object (Unstructured).
 | ||||
| 	g := New() | ||||
| 	objToUnstructured := map[object.ObjMetadata]*unstructured.Unstructured{} | ||||
| 	for i, obj := range objs { | ||||
| 		id := ids[i] | ||||
| 		objToUnstructured[id] = obj | ||||
| 	} | ||||
| 
 | ||||
| 	// Add objects as graph vertices
 | ||||
| 	addVertices(g, ids) | ||||
| 	// Add dependencies as graph edges
 | ||||
|  | @ -49,14 +43,29 @@ func SortObjs(objs object.UnstructuredSet) ([]object.UnstructuredSet, error) { | |||
| 	if err := addApplyTimeMutationEdges(g, objs, ids); err != nil { | ||||
| 		errors = append(errors, err) | ||||
| 	} | ||||
| 	// Run topological sort on the graph.
 | ||||
| 	sortedObjSets, err := g.Sort() | ||||
| 	if err != nil { | ||||
| 		errors = append(errors, err) | ||||
| 	if len(errors) > 0 { | ||||
| 		return g, multierror.Wrap(errors...) | ||||
| 	} | ||||
| 	return g, nil | ||||
| } | ||||
| 
 | ||||
| // HydrateSetList takes a list of sets of ids and a set of objects and returns
 | ||||
| // a list of set of objects. The output set list will be the same order as the
 | ||||
| // input set list, but with IDs converted into Objects. Any IDs that do not
 | ||||
| // match objects in the provided object set will be skipped (filtered) in the
 | ||||
| // output.
 | ||||
| func HydrateSetList(idSetList []object.ObjMetadataSet, objs object.UnstructuredSet) []object.UnstructuredSet { | ||||
| 	var objSetList []object.UnstructuredSet | ||||
| 
 | ||||
| 	// Build a map of id -> obj.
 | ||||
| 	objToUnstructured := map[object.ObjMetadata]*unstructured.Unstructured{} | ||||
| 	for _, obj := range objs { | ||||
| 		objToUnstructured[object.UnstructuredToObjMetadata(obj)] = obj | ||||
| 	} | ||||
| 
 | ||||
| 	// Map the object metadata back to the sorted sets of unstructured objects.
 | ||||
| 	// Ignore any edges that aren't part of the input set (don't wait for them).
 | ||||
| 	for _, objSet := range sortedObjSets { | ||||
| 	for _, objSet := range idSetList { | ||||
| 		currentSet := object.UnstructuredSet{} | ||||
| 		for _, id := range objSet { | ||||
| 			var found bool | ||||
|  | @ -65,14 +74,43 @@ func SortObjs(objs object.UnstructuredSet) ([]object.UnstructuredSet, error) { | |||
| 				currentSet = append(currentSet, obj) | ||||
| 			} | ||||
| 		} | ||||
| 		// Sort each set in apply order
 | ||||
| 		sort.Sort(ordering.SortableUnstructureds(currentSet)) | ||||
| 		objSets = append(objSets, currentSet) | ||||
| 		if len(currentSet) > 0 { | ||||
| 			// Sort each set in apply order
 | ||||
| 			sort.Sort(ordering.SortableUnstructureds(currentSet)) | ||||
| 			objSetList = append(objSetList, currentSet) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return objSetList | ||||
| } | ||||
| 
 | ||||
| // SortObjs returns a slice of the sets of objects to apply (in order).
 | ||||
| // Each of the objects in an apply set is applied together. The order of
 | ||||
| // the returned applied sets is a topological ordering of the sets to apply.
 | ||||
| // Returns an single empty apply set if there are no objects to apply.
 | ||||
| func SortObjs(objs object.UnstructuredSet) ([]object.UnstructuredSet, error) { | ||||
| 	var errors []error | ||||
| 	if len(objs) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	g, err := DependencyGraph(objs) | ||||
| 	if err != nil { | ||||
| 		// collect and continue
 | ||||
| 		errors = multierror.Unwrap(err) | ||||
| 	} | ||||
| 
 | ||||
| 	idSetList, err := g.Sort() | ||||
| 	if err != nil { | ||||
| 		errors = append(errors, err) | ||||
| 	} | ||||
| 
 | ||||
| 	objSetList := HydrateSetList(idSetList, objs) | ||||
| 
 | ||||
| 	if len(errors) > 0 { | ||||
| 		return objSets, multierror.Wrap(errors...) | ||||
| 		return objSetList, multierror.Wrap(errors...) | ||||
| 	} | ||||
| 	return objSets, nil | ||||
| 	return objSetList, nil | ||||
| } | ||||
| 
 | ||||
| // ReverseSortObjs is the same as SortObjs but using reverse ordering.
 | ||||
|  | @ -82,17 +120,22 @@ func ReverseSortObjs(objs object.UnstructuredSet) ([]object.UnstructuredSet, err | |||
| 	if err != nil { | ||||
| 		return s, err | ||||
| 	} | ||||
| 	ReverseSetList(s) | ||||
| 	return s, nil | ||||
| } | ||||
| 
 | ||||
| // ReverseSetList deep reverses of a list of object lists
 | ||||
| func ReverseSetList(setList []object.UnstructuredSet) { | ||||
| 	// Reverse the ordering of the object sets using swaps.
 | ||||
| 	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { | ||||
| 		s[i], s[j] = s[j], s[i] | ||||
| 	for i, j := 0, len(setList)-1; i < j; i, j = i+1, j-1 { | ||||
| 		setList[i], setList[j] = setList[j], setList[i] | ||||
| 	} | ||||
| 	// Reverse the ordering of the objects in each set using swaps.
 | ||||
| 	for _, c := range s { | ||||
| 		for i, j := 0, len(c)-1; i < j; i, j = i+1, j-1 { | ||||
| 			c[i], c[j] = c[j], c[i] | ||||
| 	for _, set := range setList { | ||||
| 		for i, j := 0, len(set)-1; i < j; i, j = i+1, j-1 { | ||||
| 			set[i], set[j] = set[j], set[i] | ||||
| 		} | ||||
| 	} | ||||
| 	return s, nil | ||||
| } | ||||
| 
 | ||||
| // addVertices adds all the IDs in the set as graph vertices.
 | ||||
|  |  | |||
|  | @ -7,7 +7,10 @@ import ( | |||
| 	"errors" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/google/go-cmp/cmp/cmpopts" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||||
| 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/multierror" | ||||
|  | @ -374,6 +377,495 @@ func TestReverseSortObjs(t *testing.T) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestDependencyGraph(t *testing.T) { | ||||
| 	// Use a custom Asserter to customize the graph options
 | ||||
| 	asserter := testutil.NewAsserter( | ||||
| 		cmpopts.EquateErrors(), | ||||
| 		graphComparer(), | ||||
| 	) | ||||
| 
 | ||||
| 	testCases := map[string]struct { | ||||
| 		objs          object.UnstructuredSet | ||||
| 		graph         *Graph | ||||
| 		expectedError error | ||||
| 	}{ | ||||
| 		"no objects": { | ||||
| 			objs:  object.UnstructuredSet{}, | ||||
| 			graph: New(), | ||||
| 		}, | ||||
| 		"one object no dependencies": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two unrelated objects": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]):     {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]):     {}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two objects one dependency": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"three objects two dependencies": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 				testutil.Unstructured(t, resources["secret"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))), | ||||
| 				testutil.Unstructured(t, resources["pod"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["pod"]): {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["pod"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"three objects two dependencies on the same object": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 				testutil.Unstructured(t, resources["pod"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["pod"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["pod"]):        {}, | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two objects and their namespace": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 				testutil.Unstructured(t, resources["namespace"]), | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["namespace"]): {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["namespace"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]):     {}, | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): {}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two custom resources and their CRD": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["crontab1"]), | ||||
| 				testutil.Unstructured(t, resources["crontab2"]), | ||||
| 				testutil.Unstructured(t, resources["crd"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["crontab1"]): { | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["crontab2"]): { | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["crd"]): {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["crd"]): { | ||||
| 						testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 						testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["crontab1"]): {}, | ||||
| 					testutil.ToIdentifier(t, resources["crontab2"]): {}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two custom resources with their CRD and namespace": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["crontab1"]), | ||||
| 				testutil.Unstructured(t, resources["crontab2"]), | ||||
| 				testutil.Unstructured(t, resources["namespace"]), | ||||
| 				testutil.Unstructured(t, resources["crd"]), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["crontab1"]): { | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 						testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["crontab2"]): { | ||||
| 						testutil.ToIdentifier(t, resources["crd"]), | ||||
| 						testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["crd"]):       {}, | ||||
| 					testutil.ToIdentifier(t, resources["namespace"]): {}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["crd"]): { | ||||
| 						testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 						testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["namespace"]): { | ||||
| 						testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 						testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["crontab1"]): {}, | ||||
| 					testutil.ToIdentifier(t, resources["crontab2"]): {}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two object cyclic dependency": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 				testutil.Unstructured(t, resources["secret"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"three object cyclic dependency": { | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["secret"]))), | ||||
| 				testutil.Unstructured(t, resources["secret"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["pod"]))), | ||||
| 				testutil.Unstructured(t, resources["pod"], | ||||
| 					testutil.AddDependsOn(t, testutil.ToIdentifier(t, resources["deployment"]))), | ||||
| 			}, | ||||
| 			graph: &Graph{ | ||||
| 				edges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["pod"]): { | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 				reverseEdges: map[object.ObjMetadata]object.ObjMetadataSet{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]): { | ||||
| 						testutil.ToIdentifier(t, resources["pod"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["pod"]): { | ||||
| 						testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					}, | ||||
| 					testutil.ToIdentifier(t, resources["secret"]): { | ||||
| 						testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for tn, tc := range testCases { | ||||
| 		t.Run(tn, func(t *testing.T) { | ||||
| 			g, err := DependencyGraph(tc.objs) | ||||
| 			if tc.expectedError != nil { | ||||
| 				require.EqualError(t, err, tc.expectedError.Error()) | ||||
| 				return | ||||
| 			} | ||||
| 			assert.NoError(t, err) | ||||
| 			asserter.Equal(t, tc.graph, g) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestHydrateSetList(t *testing.T) { | ||||
| 	testCases := map[string]struct { | ||||
| 		idSetList []object.ObjMetadataSet | ||||
| 		objs      object.UnstructuredSet | ||||
| 		expected  []object.UnstructuredSet | ||||
| 	}{ | ||||
| 		"no object sets": { | ||||
| 			idSetList: []object.ObjMetadataSet{}, | ||||
| 			expected:  nil, | ||||
| 		}, | ||||
| 		"one object set": { | ||||
| 			idSetList: []object.ObjMetadataSet{ | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two out of three": { | ||||
| 			idSetList: []object.ObjMetadataSet{ | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["secret"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["pod"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 				testutil.Unstructured(t, resources["pod"]), | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["pod"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two uneven sets": { | ||||
| 			idSetList: []object.ObjMetadataSet{ | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["secret"]), | ||||
| 					testutil.ToIdentifier(t, resources["deployment"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["namespace"]), | ||||
| 				testutil.Unstructured(t, resources["deployment"]), | ||||
| 				testutil.Unstructured(t, resources["secret"]), | ||||
| 				testutil.Unstructured(t, resources["pod"]), | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["secret"]), | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["namespace"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"one of two sets": { | ||||
| 			idSetList: []object.ObjMetadataSet{ | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["namespace"]), | ||||
| 					testutil.ToIdentifier(t, resources["crd"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.ToIdentifier(t, resources["crontab1"]), | ||||
| 					testutil.ToIdentifier(t, resources["crontab2"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			objs: object.UnstructuredSet{ | ||||
| 				testutil.Unstructured(t, resources["namespace"]), | ||||
| 				testutil.Unstructured(t, resources["crd"]), | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["namespace"]), | ||||
| 					testutil.Unstructured(t, resources["crd"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for tn, tc := range testCases { | ||||
| 		t.Run(tn, func(t *testing.T) { | ||||
| 			objSetList := HydrateSetList(tc.idSetList, tc.objs) | ||||
| 			assert.Equal(t, tc.expected, objSetList) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestReverseSetList(t *testing.T) { | ||||
| 	testCases := map[string]struct { | ||||
| 		setList  []object.UnstructuredSet | ||||
| 		expected []object.UnstructuredSet | ||||
| 	}{ | ||||
| 		"no object sets": { | ||||
| 			setList:  []object.UnstructuredSet{}, | ||||
| 			expected: []object.UnstructuredSet{}, | ||||
| 		}, | ||||
| 		"one object set": { | ||||
| 			setList: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"three object sets": { | ||||
| 			setList: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["secret"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["pod"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["pod"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["secret"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two uneven sets": { | ||||
| 			setList: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["secret"]), | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["namespace"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["namespace"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["deployment"]), | ||||
| 					testutil.Unstructured(t, resources["secret"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"two even sets": { | ||||
| 			setList: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["crontab1"]), | ||||
| 					testutil.Unstructured(t, resources["crontab2"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["crd"]), | ||||
| 					testutil.Unstructured(t, resources["namespace"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 			expected: []object.UnstructuredSet{ | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["namespace"]), | ||||
| 					testutil.Unstructured(t, resources["crd"]), | ||||
| 				}, | ||||
| 				{ | ||||
| 					testutil.Unstructured(t, resources["crontab2"]), | ||||
| 					testutil.Unstructured(t, resources["crontab1"]), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for tn, tc := range testCases { | ||||
| 		t.Run(tn, func(t *testing.T) { | ||||
| 			ReverseSetList(tc.setList) | ||||
| 			assert.Equal(t, tc.expected, tc.setList) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestApplyTimeMutationEdges(t *testing.T) { | ||||
| 	testCases := map[string]struct { | ||||
| 		objs          []*unstructured.Unstructured | ||||
|  | @ -646,7 +1138,7 @@ func TestApplyTimeMutationEdges(t *testing.T) { | |||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 			actual := g.GetEdges() | ||||
| 			actual := edgeMapToList(g.edges) | ||||
| 			verifyEdges(t, tc.expected, actual) | ||||
| 		}) | ||||
| 	} | ||||
|  | @ -939,7 +1431,7 @@ func TestAddDependsOnEdges(t *testing.T) { | |||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 			actual := g.GetEdges() | ||||
| 			actual := edgeMapToList(g.edges) | ||||
| 			verifyEdges(t, tc.expected, actual) | ||||
| 		}) | ||||
| 	} | ||||
|  | @ -1016,7 +1508,7 @@ func TestAddNamespaceEdges(t *testing.T) { | |||
| 			g := New() | ||||
| 			ids := object.UnstructuredSetToObjMetadataSet(tc.objs) | ||||
| 			addNamespaceEdges(g, tc.objs, ids) | ||||
| 			actual := g.GetEdges() | ||||
| 			actual := edgeMapToList(g.edges) | ||||
| 			verifyEdges(t, tc.expected, actual) | ||||
| 		}) | ||||
| 	} | ||||
|  | @ -1068,7 +1560,7 @@ func TestAddCRDEdges(t *testing.T) { | |||
| 			g := New() | ||||
| 			ids := object.UnstructuredSetToObjMetadataSet(tc.objs) | ||||
| 			addCRDEdges(g, tc.objs, ids) | ||||
| 			actual := g.GetEdges() | ||||
| 			actual := edgeMapToList(g.edges) | ||||
| 			verifyEdges(t, tc.expected, actual) | ||||
| 		}) | ||||
| 	} | ||||
|  | @ -1136,3 +1628,17 @@ func containsEdge(edges []Edge, edge Edge) bool { | |||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // waitTaskComparer allows comparion of WaitTasks, ignoring private fields.
 | ||||
| func graphComparer() cmp.Option { | ||||
| 	return cmp.Comparer(func(x, y *Graph) bool { | ||||
| 		if x == nil { | ||||
| 			return y == nil | ||||
| 		} | ||||
| 		if y == nil { | ||||
| 			return false | ||||
| 		} | ||||
| 		return cmp.Equal(x.edges, y.edges) && | ||||
| 			cmp.Equal(x.reverseEdges, y.reverseEdges) | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
|  | @ -20,12 +20,15 @@ import ( | |||
| type Graph struct { | ||||
| 	// map "from" vertex -> list of "to" vertices
 | ||||
| 	edges map[object.ObjMetadata]object.ObjMetadataSet | ||||
| 	// map "to" vertex -> list of "from" vertices
 | ||||
| 	reverseEdges map[object.ObjMetadata]object.ObjMetadataSet | ||||
| } | ||||
| 
 | ||||
| // New returns a pointer to an empty Graph data structure.
 | ||||
| func New() *Graph { | ||||
| 	g := &Graph{} | ||||
| 	g.edges = make(map[object.ObjMetadata]object.ObjMetadataSet) | ||||
| 	g.reverseEdges = make(map[object.ObjMetadata]object.ObjMetadataSet) | ||||
| 	return g | ||||
| } | ||||
| 
 | ||||
|  | @ -35,13 +38,16 @@ func (g *Graph) AddVertex(v object.ObjMetadata) { | |||
| 	if _, exists := g.edges[v]; !exists { | ||||
| 		g.edges[v] = object.ObjMetadataSet{} | ||||
| 	} | ||||
| 	if _, exists := g.reverseEdges[v]; !exists { | ||||
| 		g.reverseEdges[v] = object.ObjMetadataSet{} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetVertices returns a sorted set of unique vertices in the graph.
 | ||||
| func (g *Graph) GetVertices() object.ObjMetadataSet { | ||||
| 	keys := make(object.ObjMetadataSet, len(g.edges)) | ||||
| // edgeMapKeys returns a sorted set of unique vertices in the graph.
 | ||||
| func edgeMapKeys(edgeMap map[object.ObjMetadata]object.ObjMetadataSet) object.ObjMetadataSet { | ||||
| 	keys := make(object.ObjMetadataSet, len(edgeMap)) | ||||
| 	i := 0 | ||||
| 	for k := range g.edges { | ||||
| 	for k := range edgeMap { | ||||
| 		keys[i] = k | ||||
| 		i++ | ||||
| 	} | ||||
|  | @ -56,21 +62,28 @@ func (g *Graph) AddEdge(from object.ObjMetadata, to object.ObjMetadata) { | |||
| 	if _, exists := g.edges[from]; !exists { | ||||
| 		g.edges[from] = object.ObjMetadataSet{} | ||||
| 	} | ||||
| 	if _, exists := g.reverseEdges[from]; !exists { | ||||
| 		g.reverseEdges[from] = object.ObjMetadataSet{} | ||||
| 	} | ||||
| 	// Add "to" vertex if it doesn't already exist.
 | ||||
| 	if _, exists := g.edges[to]; !exists { | ||||
| 		g.edges[to] = object.ObjMetadataSet{} | ||||
| 	} | ||||
| 	if _, exists := g.reverseEdges[to]; !exists { | ||||
| 		g.reverseEdges[to] = object.ObjMetadataSet{} | ||||
| 	} | ||||
| 	// Add edge "from" -> "to" if it doesn't already exist
 | ||||
| 	// into the adjacency list.
 | ||||
| 	if !g.isAdjacent(from, to) { | ||||
| 		g.edges[from] = append(g.edges[from], to) | ||||
| 		g.reverseEdges[to] = append(g.reverseEdges[to], from) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetEdges returns a sorted slice of directed graph edges (vertex pairs).
 | ||||
| func (g *Graph) GetEdges() []Edge { | ||||
| // edgeMapToList returns a sorted slice of directed graph edges (vertex pairs).
 | ||||
| func edgeMapToList(edgeMap map[object.ObjMetadata]object.ObjMetadataSet) []Edge { | ||||
| 	edges := []Edge{} | ||||
| 	for from, toList := range g.edges { | ||||
| 	for from, toList := range edgeMap { | ||||
| 		for _, to := range toList { | ||||
| 			edge := Edge{From: from, To: to} | ||||
| 			edges = append(edges, edge) | ||||
|  | @ -101,25 +114,53 @@ func (g *Graph) Size() int { | |||
| 	return len(g.edges) | ||||
| } | ||||
| 
 | ||||
| // removeVertex removes the passed vertex as well as any edges
 | ||||
| // into the vertex.
 | ||||
| func (g *Graph) removeVertex(r object.ObjMetadata) { | ||||
| // removeVertex removes the passed vertex as well as any edges into the vertex.
 | ||||
| func removeVertex(edges map[object.ObjMetadata]object.ObjMetadataSet, r object.ObjMetadata) { | ||||
| 	// First, remove the object from all adjacency lists.
 | ||||
| 	for v, adj := range g.edges { | ||||
| 		g.edges[v] = adj.Remove(r) | ||||
| 	for v, adj := range edges { | ||||
| 		edges[v] = adj.Remove(r) | ||||
| 	} | ||||
| 	// Finally, remove the vertex
 | ||||
| 	delete(g.edges, r) | ||||
| 	delete(edges, r) | ||||
| } | ||||
| 
 | ||||
| // Sort returns the ordered set of vertices after
 | ||||
| // a topological sort.
 | ||||
| // Dependencies returns the objects that this object depends on.
 | ||||
| func (g *Graph) Dependencies(from object.ObjMetadata) object.ObjMetadataSet { | ||||
| 	edgesFrom, exists := g.edges[from] | ||||
| 	if !exists { | ||||
| 		return nil | ||||
| 	} | ||||
| 	c := make(object.ObjMetadataSet, len(edgesFrom)) | ||||
| 	copy(c, edgesFrom) | ||||
| 	return c | ||||
| } | ||||
| 
 | ||||
| // Dependents returns the objects that depend on this object.
 | ||||
| func (g *Graph) Dependents(to object.ObjMetadata) object.ObjMetadataSet { | ||||
| 	edgesTo, exists := g.reverseEdges[to] | ||||
| 	if !exists { | ||||
| 		return nil | ||||
| 	} | ||||
| 	c := make(object.ObjMetadataSet, len(edgesTo)) | ||||
| 	copy(c, edgesTo) | ||||
| 	return c | ||||
| } | ||||
| 
 | ||||
| // Sort returns the ordered set of vertices after a topological sort.
 | ||||
| func (g *Graph) Sort() ([]object.ObjMetadataSet, error) { | ||||
| 	// deep copy edge map to avoid destructive sorting
 | ||||
| 	edges := make(map[object.ObjMetadata]object.ObjMetadataSet, len(g.edges)) | ||||
| 	for vertex, deps := range g.edges { | ||||
| 		c := make(object.ObjMetadataSet, len(deps)) | ||||
| 		copy(c, deps) | ||||
| 		edges[vertex] = c | ||||
| 	} | ||||
| 
 | ||||
| 	sorted := []object.ObjMetadataSet{} | ||||
| 	for g.Size() > 0 { | ||||
| 	for len(edges) > 0 { | ||||
| 		// Identify all the leaf vertices.
 | ||||
| 		leafVertices := object.ObjMetadataSet{} | ||||
| 		for v, adj := range g.edges { | ||||
| 		for v, adj := range edges { | ||||
| 			if len(adj) == 0 { | ||||
| 				leafVertices = append(leafVertices, v) | ||||
| 			} | ||||
|  | @ -129,12 +170,12 @@ func (g *Graph) Sort() ([]object.ObjMetadataSet, error) { | |||
| 		if len(leafVertices) == 0 { | ||||
| 			// Error can be ignored, so return the full set list
 | ||||
| 			return sorted, validation.NewError(CyclicDependencyError{ | ||||
| 				Edges: g.GetEdges(), | ||||
| 			}, g.GetVertices()...) | ||||
| 				Edges: edgeMapToList(edges), | ||||
| 			}, edgeMapKeys(edges)...) | ||||
| 		} | ||||
| 		// Remove all edges to leaf vertices.
 | ||||
| 		for _, v := range leafVertices { | ||||
| 			g.removeVertex(v) | ||||
| 			removeVertex(edges, v) | ||||
| 		} | ||||
| 		sorted = append(sorted, leafVertices) | ||||
| 	} | ||||
|  |  | |||
|  | @ -138,6 +138,119 @@ func TestObjectGraphSort(t *testing.T) { | |||
| 			} | ||||
| 			assert.NoError(t, err) | ||||
| 			testutil.AssertEqual(t, tc.expected, actual) | ||||
| 
 | ||||
| 			// verify sort is repeatable & non-destructive
 | ||||
| 			actual, err = g.Sort() | ||||
| 			assert.NoError(t, err) | ||||
| 			testutil.AssertEqual(t, tc.expected, actual) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGraphDependencies(t *testing.T) { | ||||
| 	testCases := map[string]struct { | ||||
| 		vertices object.ObjMetadataSet | ||||
| 		edges    []Edge | ||||
| 		from     object.ObjMetadata | ||||
| 		expected object.ObjMetadataSet | ||||
| 	}{ | ||||
| 		"no dependencies": { | ||||
| 			vertices: object.ObjMetadataSet{o1, o2, o3}, | ||||
| 			edges: []Edge{ | ||||
| 				{From: o1, To: o2}, | ||||
| 				{From: o1, To: o3}, | ||||
| 				{From: o2, To: o3}, | ||||
| 			}, | ||||
| 			from:     o3, | ||||
| 			expected: object.ObjMetadataSet{}, | ||||
| 		}, | ||||
| 		"one dependency": { | ||||
| 			vertices: object.ObjMetadataSet{o1, o2, o3}, | ||||
| 			edges: []Edge{ | ||||
| 				{From: o1, To: o2}, | ||||
| 				{From: o1, To: o3}, | ||||
| 				{From: o2, To: o3}, | ||||
| 			}, | ||||
| 			from:     o2, | ||||
| 			expected: object.ObjMetadataSet{o3}, | ||||
| 		}, | ||||
| 		"two dependencies": { | ||||
| 			vertices: object.ObjMetadataSet{o1, o2, o3}, | ||||
| 			edges: []Edge{ | ||||
| 				{From: o1, To: o2}, | ||||
| 				{From: o1, To: o3}, | ||||
| 				{From: o2, To: o3}, | ||||
| 			}, | ||||
| 			from:     o1, | ||||
| 			expected: object.ObjMetadataSet{o2, o3}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for tn, tc := range testCases { | ||||
| 		t.Run(tn, func(t *testing.T) { | ||||
| 			g := New() | ||||
| 			for _, vertex := range tc.vertices { | ||||
| 				g.AddVertex(vertex) | ||||
| 			} | ||||
| 			for _, edge := range tc.edges { | ||||
| 				g.AddEdge(edge.From, edge.To) | ||||
| 			} | ||||
| 
 | ||||
| 			testutil.AssertEqual(t, tc.expected, g.Dependencies(tc.from)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGraphDependents(t *testing.T) { | ||||
| 	testCases := map[string]struct { | ||||
| 		vertices object.ObjMetadataSet | ||||
| 		edges    []Edge | ||||
| 		to       object.ObjMetadata | ||||
| 		expected object.ObjMetadataSet | ||||
| 	}{ | ||||
| 		"no dependents": { | ||||
| 			vertices: object.ObjMetadataSet{o1, o2, o3}, | ||||
| 			edges: []Edge{ | ||||
| 				{From: o1, To: o2}, | ||||
| 				{From: o1, To: o3}, | ||||
| 				{From: o2, To: o3}, | ||||
| 			}, | ||||
| 			to:       o1, | ||||
| 			expected: object.ObjMetadataSet{}, | ||||
| 		}, | ||||
| 		"one dependent": { | ||||
| 			vertices: object.ObjMetadataSet{o1, o2, o3}, | ||||
| 			edges: []Edge{ | ||||
| 				{From: o1, To: o2}, | ||||
| 				{From: o1, To: o3}, | ||||
| 				{From: o2, To: o3}, | ||||
| 			}, | ||||
| 			to:       o2, | ||||
| 			expected: object.ObjMetadataSet{o1}, | ||||
| 		}, | ||||
| 		"two dependents": { | ||||
| 			vertices: object.ObjMetadataSet{o1, o2, o3}, | ||||
| 			edges: []Edge{ | ||||
| 				{From: o1, To: o2}, | ||||
| 				{From: o1, To: o3}, | ||||
| 				{From: o2, To: o3}, | ||||
| 			}, | ||||
| 			to:       o3, | ||||
| 			expected: object.ObjMetadataSet{o1, o2}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for tn, tc := range testCases { | ||||
| 		t.Run(tn, func(t *testing.T) { | ||||
| 			g := New() | ||||
| 			for _, vertex := range tc.vertices { | ||||
| 				g.AddVertex(vertex) | ||||
| 			} | ||||
| 			for _, edge := range tc.edges { | ||||
| 				g.AddEdge(edge.From, edge.To) | ||||
| 			} | ||||
| 
 | ||||
| 			testutil.AssertEqual(t, tc.expected, g.Dependents(tc.to)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -192,3 +192,10 @@ spec: | |||
|     - name: tcp | ||||
|       containerPort: 80 | ||||
| ` | ||||
| 
 | ||||
| var namespaceTemplate = ` | ||||
| apiVersion: v1 | ||||
| kind: Namespace | ||||
| metadata: | ||||
|   name: {{.Namespace}} | ||||
| ` | ||||
|  |  | |||
|  | @ -0,0 +1,411 @@ | |||
| // Copyright 2020 The Kubernetes Authors.
 | ||||
| // SPDX-License-Identifier: Apache-2.0
 | ||||
| 
 | ||||
| package e2e | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	. "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/object" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/object/validation" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/testutil" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| ) | ||||
| 
 | ||||
| //nolint:dupl // expEvents similar to other tests
 | ||||
| func dependencyFilterTest(ctx context.Context, c client.Client, invConfig InventoryConfig, inventoryName, namespaceName string) { | ||||
| 	By("apply resources in order based on depends-on annotation") | ||||
| 	applier := invConfig.ApplierFactoryFunc() | ||||
| 
 | ||||
| 	inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) | ||||
| 
 | ||||
| 	pod1Obj := withDependsOn(withNamespace(manifestToUnstructured(pod1), namespaceName), fmt.Sprintf("/namespaces/%s/Pod/pod2", namespaceName)) | ||||
| 	pod2Obj := withNamespace(manifestToUnstructured(pod2), namespaceName) | ||||
| 
 | ||||
| 	// Dependency order: pod1 -> pod2
 | ||||
| 	// Apply order: pod2, pod1
 | ||||
| 	resources := []*unstructured.Unstructured{ | ||||
| 		pod1Obj, | ||||
| 		pod2Obj, | ||||
| 	} | ||||
| 
 | ||||
| 	// Cleanup
 | ||||
| 	defer func(ctx context.Context, c client.Client) { | ||||
| 		deleteUnstructuredIfExists(ctx, c, pod1Obj) | ||||
| 		deleteUnstructuredIfExists(ctx, c, pod2Obj) | ||||
| 	}(ctx, c) | ||||
| 
 | ||||
| 	applierEvents := runCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ | ||||
| 		EmitStatusEvents: false, | ||||
| 	})) | ||||
| 
 | ||||
| 	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, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Apply pod2 first
 | ||||
| 			EventType: event.ApplyType, | ||||
| 			ApplyEvent: &testutil.ExpApplyEvent{ | ||||
| 				GroupName:  "apply-0", | ||||
| 				Operation:  event.Created, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod2Obj), | ||||
| 				Error:      nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// ApplyTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.ApplyAction, | ||||
| 				GroupName: "apply-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// pod2 reconcile Pending.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-0", | ||||
| 				Operation:  event.ReconcilePending, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod2Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// pod2 confirmed Current.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-0", | ||||
| 				Operation:  event.Reconciled, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod2Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// ApplyTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.ApplyAction, | ||||
| 				GroupName: "apply-1", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Apply pod1 second
 | ||||
| 			EventType: event.ApplyType, | ||||
| 			ApplyEvent: &testutil.ExpApplyEvent{ | ||||
| 				GroupName:  "apply-1", | ||||
| 				Operation:  event.Created, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod1Obj), | ||||
| 				Error:      nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// ApplyTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.ApplyAction, | ||||
| 				GroupName: "apply-1", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// pod1 reconcile Pending.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-1", | ||||
| 				Operation:  event.ReconcilePending, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod1Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// pod1 confirmed Current.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-1", | ||||
| 				Operation:  event.Reconciled, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod1Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// 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, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) | ||||
| 
 | ||||
| 	By("verify pod1 created and ready") | ||||
| 	result := assertUnstructuredExists(ctx, c, pod1Obj) | ||||
| 	podIP, found, err := object.NestedField(result.Object, "status", "podIP") | ||||
| 	Expect(err).NotTo(HaveOccurred()) | ||||
| 	Expect(found).To(BeTrue()) | ||||
| 	Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness
 | ||||
| 
 | ||||
| 	By("verify pod2 created and ready") | ||||
| 	result = assertUnstructuredExists(ctx, c, pod2Obj) | ||||
| 	podIP, found, err = object.NestedField(result.Object, "status", "podIP") | ||||
| 	Expect(err).NotTo(HaveOccurred()) | ||||
| 	Expect(found).To(BeTrue()) | ||||
| 	Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness
 | ||||
| 
 | ||||
| 	// Attempt to Prune pod2
 | ||||
| 	resources = []*unstructured.Unstructured{ | ||||
| 		pod1Obj, | ||||
| 	} | ||||
| 
 | ||||
| 	applierEvents = runCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ | ||||
| 		EmitStatusEvents: false, | ||||
| 		ValidationPolicy: validation.SkipInvalid, | ||||
| 	})) | ||||
| 
 | ||||
| 	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, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Apply pod1 Skipped (dependency actuation strategy mismatch)
 | ||||
| 			EventType: event.ApplyType, | ||||
| 			ApplyEvent: &testutil.ExpApplyEvent{ | ||||
| 				GroupName:  "apply-0", | ||||
| 				Operation:  event.Unchanged, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod1Obj), | ||||
| 				Error:      nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// ApplyTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.ApplyAction, | ||||
| 				GroupName: "apply-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// pod1 reconcile Skipped (because apply skipped)
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-0", | ||||
| 				Operation:  event.ReconcileSkipped, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod1Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// PruneTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.PruneAction, | ||||
| 				GroupName: "prune-0", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Prune pod2 Skipped (dependency actuation strategy mismatch)
 | ||||
| 			EventType: event.PruneType, | ||||
| 			PruneEvent: &testutil.ExpPruneEvent{ | ||||
| 				GroupName:  "prune-0", | ||||
| 				Operation:  event.PruneSkipped, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod2Obj), | ||||
| 				Error:      nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// PruneTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.PruneAction, | ||||
| 				GroupName: "prune-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// pod2 reconcile Skipped (because prune skipped)
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-1", | ||||
| 				Operation:  event.ReconcileSkipped, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(pod2Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// 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, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) | ||||
| 
 | ||||
| 	By("verify pod1 not deleted") | ||||
| 	result = assertUnstructuredExists(ctx, c, pod1Obj) | ||||
| 	ts, found, err := object.NestedField(result.Object, "metadata", "deletionTimestamp") | ||||
| 	Expect(err).NotTo(HaveOccurred()) | ||||
| 	Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) | ||||
| 
 | ||||
| 	By("verify pod2 not deleted") | ||||
| 	result = assertUnstructuredExists(ctx, c, pod2Obj) | ||||
| 	ts, found, err = object.NestedField(result.Object, "metadata", "deletionTimestamp") | ||||
| 	Expect(err).NotTo(HaveOccurred()) | ||||
| 	Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) | ||||
| } | ||||
|  | @ -187,6 +187,14 @@ var _ = Describe("Applier", func() { | |||
| 					mutationTest(ctx, c, invConfig, inventoryName, namespace.GetName()) | ||||
| 				}) | ||||
| 
 | ||||
| 				It("DependencyFilter", func() { | ||||
| 					dependencyFilterTest(ctx, c, invConfig, inventoryName, namespace.GetName()) | ||||
| 				}) | ||||
| 
 | ||||
| 				It("LocalNamespacesFilter", func() { | ||||
| 					namespaceFilterTest(ctx, c, invConfig, inventoryName, namespace.GetName()) | ||||
| 				}) | ||||
| 
 | ||||
| 				It("Prune retrieval error correctly handled", func() { | ||||
| 					pruneRetrieveErrorTest(ctx, c, invConfig, inventoryName, namespace.GetName()) | ||||
| 				}) | ||||
|  |  | |||
|  | @ -0,0 +1,408 @@ | |||
| // Copyright 2020 The Kubernetes Authors.
 | ||||
| // SPDX-License-Identifier: Apache-2.0
 | ||||
| 
 | ||||
| package e2e | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	. "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/object" | ||||
| 	"sigs.k8s.io/cli-utils/pkg/testutil" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| ) | ||||
| 
 | ||||
| //nolint:dupl // expEvents similar to other tests
 | ||||
| func namespaceFilterTest(ctx context.Context, c client.Client, invConfig InventoryConfig, inventoryName, namespaceName string) { | ||||
| 	By("apply resources in order based on depends-on annotation") | ||||
| 	applier := invConfig.ApplierFactoryFunc() | ||||
| 
 | ||||
| 	inv := invConfig.InvWrapperFunc(invConfig.FactoryFunc(inventoryName, namespaceName, "test")) | ||||
| 
 | ||||
| 	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, | ||||
| 	} | ||||
| 
 | ||||
| 	// Cleanup
 | ||||
| 	defer func(ctx context.Context, c client.Client) { | ||||
| 		deleteUnstructuredIfExists(ctx, c, podBObj) | ||||
| 		deleteUnstructuredIfExists(ctx, c, namespace1Obj) | ||||
| 	}(ctx, c) | ||||
| 
 | ||||
| 	applierEvents := runCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ | ||||
| 		EmitStatusEvents: false, | ||||
| 	})) | ||||
| 
 | ||||
| 	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, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Apply namespace1 first
 | ||||
| 			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, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// namespace1 reconcile Pending.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-0", | ||||
| 				Operation:  event.ReconcilePending, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(namespace1Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// namespace1 confirmed Current.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-0", | ||||
| 				Operation:  event.Reconciled, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(namespace1Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// ApplyTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.ApplyAction, | ||||
| 				GroupName: "apply-1", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Apply podB second
 | ||||
| 			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, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// podB reconcile Pending.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-1", | ||||
| 				Operation:  event.ReconcilePending, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(podBObj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// podB confirmed Current.
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-1", | ||||
| 				Operation:  event.Reconciled, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(podBObj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// 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, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) | ||||
| 
 | ||||
| 	By("verify namespace1 created") | ||||
| 	assertUnstructuredExists(ctx, c, namespace1Obj) | ||||
| 
 | ||||
| 	By("verify podB created and ready") | ||||
| 	result := assertUnstructuredExists(ctx, c, podBObj) | ||||
| 	podIP, found, err := object.NestedField(result.Object, "status", "podIP") | ||||
| 	Expect(err).NotTo(HaveOccurred()) | ||||
| 	Expect(found).To(BeTrue()) | ||||
| 	Expect(podIP).NotTo(BeEmpty()) // use podIP as proxy for readiness
 | ||||
| 
 | ||||
| 	// Attempt to Prune namespace
 | ||||
| 	resources = []*unstructured.Unstructured{ | ||||
| 		podBObj, | ||||
| 	} | ||||
| 
 | ||||
| 	applierEvents = runCollect(applier.Run(ctx, inv, resources, apply.ApplierOptions{ | ||||
| 		EmitStatusEvents: false, | ||||
| 	})) | ||||
| 
 | ||||
| 	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, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Apply podB Skipped (because depends on namespace being deleted)
 | ||||
| 			EventType: event.ApplyType, | ||||
| 			ApplyEvent: &testutil.ExpApplyEvent{ | ||||
| 				GroupName:  "apply-0", | ||||
| 				Operation:  event.Unchanged, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(podBObj), | ||||
| 				Error:      nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// ApplyTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.ApplyAction, | ||||
| 				GroupName: "apply-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// podB Reconcile Skipped (because apply skipped)
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-0", | ||||
| 				Operation:  event.ReconcileSkipped, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(podBObj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// PruneTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.PruneAction, | ||||
| 				GroupName: "prune-0", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// Prune namespace1 Skipped (because namespace still in use)
 | ||||
| 			EventType: event.PruneType, | ||||
| 			PruneEvent: &testutil.ExpPruneEvent{ | ||||
| 				GroupName:  "prune-0", | ||||
| 				Operation:  event.PruneSkipped, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(namespace1Obj), | ||||
| 				Error:      nil, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// PruneTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.PruneAction, | ||||
| 				GroupName: "prune-0", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask start
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Started, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// namespace1 reconcile Skipped (because prune skipped).
 | ||||
| 			EventType: event.WaitType, | ||||
| 			WaitEvent: &testutil.ExpWaitEvent{ | ||||
| 				GroupName:  "wait-1", | ||||
| 				Operation:  event.ReconcileSkipped, | ||||
| 				Identifier: object.UnstructuredToObjMetadata(namespace1Obj), | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// WaitTask finished
 | ||||
| 			EventType: event.ActionGroupType, | ||||
| 			ActionGroupEvent: &testutil.ExpActionGroupEvent{ | ||||
| 				Action:    event.WaitAction, | ||||
| 				GroupName: "wait-1", | ||||
| 				Type:      event.Finished, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			// 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, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	Expect(testutil.EventsToExpEvents(applierEvents)).To(testutil.Equal(expEvents)) | ||||
| 
 | ||||
| 	By("verify namespace1 not deleted") | ||||
| 	result = assertUnstructuredExists(ctx, c, namespace1Obj) | ||||
| 	ts, found, err := object.NestedField(result.Object, "metadata", "deletionTimestamp") | ||||
| 	Expect(err).NotTo(HaveOccurred()) | ||||
| 	Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) | ||||
| 
 | ||||
| 	By("verify podB not deleted") | ||||
| 	result = assertUnstructuredExists(ctx, c, podBObj) | ||||
| 	ts, found, err = object.NestedField(result.Object, "metadata", "deletionTimestamp") | ||||
| 	Expect(err).NotTo(HaveOccurred()) | ||||
| 	Expect(found).To(BeFalse(), "deletionTimestamp found: ", ts) | ||||
| } | ||||
		Loading…
	
		Reference in New Issue