feat: Add dependency filter

- Pass TaskContext into TaskBuilder.Build
- Combine dependency graph for apply and prune objects.
  This is required to catch dependencies that would have been deleted.
- Replace graph.SortObjs into DependencyGraph + Sort + HydrateSetList
- Replace graph.ReverseSortObjs with ReverseSetList to perform on the
  combined (apply + prune) set list.
- Add planned pending applies and prune to the InventoryManager
  before executing the task queue.
  This allows the DependencyFilter to validate against the planned
  actuation strategy of objects that haven't been applied/pruned yet.
- Add the dependency graph to the TaskContext, for the
  DependencyFilter to use.
  This can be removed in the future if the filters are managed by the
  solver.
- Make Graph.Sort non-destructive, so the graph can be re-used by the
  DependencyFilter.
- Add Graph.EdgesFrom and EdgesTo for the DependencyFilter to use.
  This requires storing the reverse edge list.
- Add an e2e test for the DependencyFilter
- Add an e2e test for the LocalNamespaceFilter

Fixes https://github.com/kubernetes-sigs/cli-utils/issues/526
Fixes https://github.com/kubernetes-sigs/cli-utils/issues/528
This commit is contained in:
Karl Isenberg 2022-02-25 14:23:17 -08:00
parent bd20d622f6
commit 4599e4fb00
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)
}