Merge pull request #555 from karlkfi/karl-dep-filter2

feat: Add dependency filter
This commit is contained in:
Kubernetes Prow Robot 2022-02-28 19:27:47 -08:00 committed by GitHub
commit 5c6134aeac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 3054 additions and 95 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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)
})
}
}

View File

@ -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]]
}

View File

@ -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

View File

@ -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)
})
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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.

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -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))
})
}
}

View File

@ -192,3 +192,10 @@ spec:
- name: tcp
containerPort: 80
`
var namespaceTemplate = `
apiVersion: v1
kind: Namespace
metadata:
name: {{.Namespace}}
`

View File

@ -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)
}

View File

@ -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())
})

View File

@ -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)
}