cli-utils/pkg/apply/applier_test.go

2082 lines
59 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package apply
import (
"context"
"fmt"
"sort"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubectl/pkg/scheme"
"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/inventory"
pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/multierror"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/object/validation"
"sigs.k8s.io/cli-utils/pkg/testutil"
)
var (
codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
resources = map[string]string{
"deployment": `
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
uid: dep-uid
generation: 1
spec:
replicas: 1
`,
"secret": `
apiVersion: v1
kind: Secret
metadata:
name: secret
namespace: default
uid: secret-uid
generation: 1
type: Opaque
spec:
foo: bar
`,
"inventory": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test-inventory-obj
namespace: test-namespace
labels:
cli-utils.sigs.k8s.io/inventory-id: test-app-label
data: {}
`,
"obj1": `
apiVersion: v1
kind: Pod
metadata:
name: obj1
namespace: test-namespace
spec: {}
`,
"obj2": `
apiVersion: v1
kind: Pod
metadata:
name: obj2
namespace: test-namespace
spec: {}
`,
"clusterScopedObj": `
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-scoped-1
`,
}
)
//nolint:dupl // event lists are very similar
func TestApplier(t *testing.T) {
testCases := map[string]struct {
namespace string
// resources input to applier
resources object.UnstructuredSet
// inventory input to applier
invInfo inventoryInfo
// objects in the cluster
clusterObjs object.UnstructuredSet
// options input to applier.Run
options Options
// fake input events from the status poller
statusEvents []pollevent.Event
// expected output status events (async)
expectedStatusEvents []testutil.ExpEvent
// expected output events
expectedEvents []testutil.ExpEvent
// true if runTimeout is expected to have caused cancellation
expectRunTimeout bool
// true if testTimeout is expected to have caused cancellation
expectTestTimeout bool
}{
"initial apply without status or prune": {
namespace: "default",
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
},
invInfo: inventoryInfo{
name: "abc-123",
namespace: "default",
id: "test",
},
clusterObjs: object.UnstructuredSet{},
options: Options{
NoPrune: true,
InventoryPolicy: inventory.InventoryPolicyMustMatch,
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Started,
},
},
{
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Created, // Create new
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
// Timeout waiting for status event saying deployment is current
// TODO: update inventory after timeout
// {
// EventType: event.ActionGroupType,
// ActionGroupEvent: &testutil.ExpActionGroupEvent{
// GroupName: "wait-0",
// Action: event.WaitAction,
// Type: event.Finished,
// },
// },
// {
// EventType: event.ActionGroupType,
// ActionGroupEvent: &testutil.ExpActionGroupEvent{
// GroupName: "inventory-set-0",
// Action: event.InventoryAction,
// Type: event.Started,
// },
// },
// {
// EventType: event.ActionGroupType,
// ActionGroupEvent: &testutil.ExpActionGroupEvent{
// GroupName: "inventory-set-0",
// Action: event.InventoryAction,
// Type: event.Finished,
// },
// },
},
expectTestTimeout: true,
},
"first apply multiple resources with status and prune": {
namespace: "default",
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
invInfo: inventoryInfo{
name: "inv-123",
namespace: "default",
id: "test",
},
clusterObjs: object.UnstructuredSet{},
options: Options{
ReconcileTimeout: time.Minute,
InventoryPolicy: inventory.InventoryPolicyMustMatch,
EmitStatusEvents: true,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
Resource: testutil.Unstructured(t, resources["secret"]),
},
},
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
},
},
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Started,
},
},
// Secrets applied before Deployments (see pkg/ordering)
{
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Created, // Create new
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Created, // Create new
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
// Secrets before Deployments (see pkg/ordering)
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
// Deployment before Secret (see statusEvents)
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
},
},
"apply multiple existing resources with status and prune": {
namespace: "default",
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
testutil.Unstructured(t, resources["secret"]),
},
invInfo: inventoryInfo{
name: "inv-123",
namespace: "default",
id: "test",
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"]),
),
},
},
clusterObjs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
},
options: Options{
ReconcileTimeout: time.Minute,
InventoryPolicy: inventory.AdoptIfNoInventory,
EmitStatusEvents: true,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
Resource: testutil.Unstructured(t, resources["secret"]),
},
},
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
},
},
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Started,
},
},
// Apply Secrets before Deployments (see ordering.SortableMetas)
{
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Created, // Create new
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Configured, // Update existing
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
// Apply Secrets before Deployments (see ordering.SortableMetas)
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
// Wait Deployments before Secrets (see testutil.GroupedEventsByID)
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
},
},
"apply no resources and prune all existing": {
namespace: "default",
resources: object.UnstructuredSet{},
invInfo: inventoryInfo{
name: "inv-123",
namespace: "default",
id: "test",
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"]),
),
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["secret"]),
),
},
},
clusterObjs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
testutil.Unstructured(t, resources["secret"], testutil.AddOwningInv(t, "test")),
},
options: Options{
InventoryPolicy: inventory.InventoryPolicyMustMatch,
EmitStatusEvents: true,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.InProgressStatus,
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.NotFoundStatus,
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.NotFoundStatus,
},
},
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.InProgressStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.NotFoundStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.NotFoundStatus,
},
},
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "prune-0",
Action: event.PruneAction,
Type: event.Started,
},
},
// Prune Deployments before Secrets (see ordering.SortableMetas)
{
EventType: event.PruneType,
PruneEvent: &testutil.ExpPruneEvent{
GroupName: "prune-0",
Operation: event.Pruned,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.PruneType,
PruneEvent: &testutil.ExpPruneEvent{
GroupName: "prune-0",
Operation: event.Pruned,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "prune-0",
Action: event.PruneAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
// Prune Deployments before Secrets (see ordering.SortableMetas)
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
// Wait Deployments before Secrets (see testutil.GroupedEventsByID)
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
},
},
"apply resource with existing object belonging to different inventory": {
namespace: "default",
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
},
invInfo: inventoryInfo{
name: "abc-123",
namespace: "default",
id: "test",
},
clusterObjs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")),
},
options: Options{
ReconcileTimeout: time.Minute,
InventoryPolicy: inventory.InventoryPolicyMustMatch,
EmitStatusEvents: true,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
},
},
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
},
},
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Started,
},
},
{
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Error: testutil.EqualErrorType(inventory.NewInventoryOverlapError(fmt.Errorf(""))),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcileSkipped,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
},
},
"resources belonging to a different inventory should not be pruned": {
namespace: "default",
resources: object.UnstructuredSet{},
invInfo: inventoryInfo{
name: "abc-123",
namespace: "default",
id: "test",
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"]),
),
},
},
clusterObjs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")),
},
options: Options{
InventoryPolicy: inventory.InventoryPolicyMustMatch,
EmitStatusEvents: true,
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "prune-0",
Action: event.PruneAction,
Type: event.Started,
},
},
{
EventType: event.PruneType,
PruneEvent: &testutil.ExpPruneEvent{
GroupName: "prune-0",
Operation: event.PruneSkipped,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "prune-0",
Action: event.PruneAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcileSkipped,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
},
},
"prune with inventory object annotation matched": {
namespace: "default",
resources: object.UnstructuredSet{},
invInfo: inventoryInfo{
name: "abc-123",
namespace: "default",
id: "test",
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"]),
),
},
},
clusterObjs: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
},
options: Options{
InventoryPolicy: inventory.InventoryPolicyMustMatch,
EmitStatusEvents: true,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.NotFoundStatus,
},
},
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.NotFoundStatus,
},
},
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "prune-0",
Action: event.PruneAction,
Type: event.Started,
},
},
{
EventType: event.PruneType,
PruneEvent: &testutil.ExpPruneEvent{
GroupName: "prune-0",
Operation: event.Pruned,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "prune-0",
Action: event.PruneAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
},
},
"SkipInvalid - skip invalid objects and apply valid objects": {
namespace: "default",
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.metadata.name", "",
}),
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.kind", "",
}),
testutil.Unstructured(t, resources["secret"]),
},
invInfo: inventoryInfo{
name: "inv-123",
namespace: "default",
id: "test",
},
clusterObjs: object.UnstructuredSet{},
options: Options{
ReconcileTimeout: time.Minute,
InventoryPolicy: inventory.AdoptIfNoInventory,
EmitStatusEvents: true,
ValidationPolicy: validation.SkipInvalid,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
Resource: testutil.Unstructured(t, resources["secret"]),
},
},
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["secret"]),
Status: status.CurrentStatus,
},
},
},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.ValidationType,
ValidationEvent: &testutil.ExpValidationEvent{
Identifiers: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.kind", "",
}),
),
},
Error: testutil.EqualErrorString(validation.NewError(
field.Required(field.NewPath("kind"), "kind is required"),
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.kind", "",
}),
),
).Error()),
},
},
{
EventType: event.ValidationType,
ValidationEvent: &testutil.ExpValidationEvent{
Identifiers: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.metadata.name", "",
}),
),
},
Error: testutil.EqualErrorString(validation.NewError(
field.Required(field.NewPath("metadata", "name"), "name is required"),
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.metadata.name", "",
}),
),
).Error()),
},
},
{
EventType: event.InitType,
InitEvent: &testutil.ExpInitEvent{},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-add-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Started,
},
},
// Secret applied
{
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Created, // Create new
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "apply-0",
Action: event.ApplyAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Started,
},
},
// Secret pending
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.ReconcilePending,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
// Secret reconciled
{
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Operation: event.Reconciled,
Identifier: testutil.ToIdentifier(t, resources["secret"]),
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "wait-0",
Action: event.WaitAction,
Type: event.Finished,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Started,
},
},
{
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
GroupName: "inventory-set-0",
Action: event.InventoryAction,
Type: event.Finished,
},
},
},
},
"ExitEarly - exit early on invalid objects and skip valid objects": {
namespace: "default",
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.metadata.name", "",
}),
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.kind", "",
}),
testutil.Unstructured(t, resources["secret"]),
},
invInfo: inventoryInfo{
name: "inv-123",
namespace: "default",
id: "test",
},
clusterObjs: object.UnstructuredSet{},
options: Options{
ReconcileTimeout: time.Minute,
InventoryPolicy: inventory.AdoptIfNoInventory,
EmitStatusEvents: true,
ValidationPolicy: validation.ExitEarly,
},
statusEvents: []pollevent.Event{},
expectedStatusEvents: []testutil.ExpEvent{},
expectedEvents: []testutil.ExpEvent{
{
EventType: event.ErrorType,
ErrorEvent: &testutil.ExpErrorEvent{
Err: testutil.EqualErrorString(multierror.New(
validation.NewError(
field.Required(field.NewPath("metadata", "name"), "name is required"),
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.metadata.name", "",
}),
),
),
validation.NewError(
field.Required(field.NewPath("kind"), "kind is required"),
object.UnstructuredToObjMetadata(
testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
"$.kind", "",
}),
),
),
).Error()),
},
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
poller := newFakePoller(tc.statusEvents)
// Only feed valid objects into the TestApplier.
// Invalid objects should not generate API requests.
validObjs := object.UnstructuredSet{}
for _, obj := range tc.resources {
id := object.UnstructuredToObjMetadata(obj)
if id.GroupKind.Kind == "" || id.Name == "" {
continue
}
validObjs = append(validObjs, obj)
}
applier := newTestApplier(t,
tc.invInfo,
validObjs,
tc.clusterObjs,
poller,
)
// Context for Applier.Run
runCtx, runCancel := context.WithCancel(context.Background())
defer runCancel() // cleanup
// Context for this test (in case Applier.Run never closes the event channel)
testTimeout := 10 * time.Second
testCtx, testCancel := context.WithTimeout(context.Background(), testTimeout)
defer testCancel() // cleanup
eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options)
// only start sending events once
var once sync.Once
var events []event.Event
loop:
for {
select {
case <-testCtx.Done():
// Test timed out
runCancel()
if tc.expectTestTimeout {
assert.Equal(t, context.DeadlineExceeded, testCtx.Err(), "Applier.Run failed to exit, but not because of expected timeout")
} else {
t.Errorf("Applier.Run failed to exit (timeout: %s)", testTimeout)
}
break loop
case e, ok := <-eventChannel:
if !ok {
// Event channel closed
testCancel()
break loop
}
if e.Type == event.ActionGroupType &&
e.ActionGroupEvent.Type == event.Finished {
// Send events after the first apply/prune task ends
if e.ActionGroupEvent.Action == event.ApplyAction ||
e.ActionGroupEvent.Action == event.PruneAction {
once.Do(func() {
// start events
poller.Start()
})
}
}
events = append(events, e)
}
}
// Convert events to test events for comparison
receivedEvents := testutil.EventsToExpEvents(events)
// Validate & remove expected status events
for _, e := range tc.expectedStatusEvents {
var removed int
receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e)
if removed < 1 {
t.Fatalf("Expected status event not received: %#v", e.StatusEvent)
}
}
// sort to allow comparison of multiple apply/prune tasks in the same task group
sort.Sort(testutil.GroupedEventsByID(receivedEvents))
// Validate the rest of the events
testutil.AssertEqual(t, tc.expectedEvents, receivedEvents,
"Actual events (%d) do not match expected events (%d)",
len(receivedEvents), len(tc.expectedEvents))
// Validate that the expected timeout was the cause of the run completion.
// just in case something else cancelled the run
switch {
case tc.expectRunTimeout:
assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected context timeout")
case tc.expectTestTimeout:
assert.Equal(t, context.Canceled, runCtx.Err(), "Applier.Run exited, but not because of expected context cancellation")
default:
assert.Nil(t, runCtx.Err(), "Applier.Run exited, but context error is not nil")
}
})
}
}
func TestApplierCancel(t *testing.T) {
testCases := map[string]struct {
// resources input to applier
resources object.UnstructuredSet
// inventory input to applier
invInfo inventoryInfo
// objects in the cluster
clusterObjs object.UnstructuredSet
// options input to applier.Run
options Options
// timeout for applier.Run
runTimeout time.Duration
// timeout for the test
testTimeout time.Duration
// fake input events from the status poller
statusEvents []pollevent.Event
// expected output status events (async)
expectedStatusEvents []testutil.ExpEvent
// expected output events
expectedEvents []testutil.ExpEvent
// true if runTimeout is expected to have caused cancellation
expectRunTimeout bool
}{
"cancelled by caller while waiting for reconcile": {
expectRunTimeout: true,
runTimeout: 2 * time.Second,
testTimeout: 30 * time.Second,
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
},
invInfo: inventoryInfo{
name: "abc-123",
namespace: "test",
id: "test",
},
clusterObjs: object.UnstructuredSet{},
options: Options{
// EmitStatusEvents required to test event output
EmitStatusEvents: true,
NoPrune: true,
InventoryPolicy: inventory.InventoryPolicyMustMatch,
// ReconcileTimeout required to enable WaitTasks
ReconcileTimeout: 1 * time.Minute,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
// Resource never becomes Current, blocking applier.Run from exiting
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
},
expectedEvents: []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 Deployment
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Created,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
// 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,
},
},
{
// Deployment reconcile pending.
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Operation: event.ReconcilePending,
},
},
// Deployment never becomes Current.
// WaitTask is expected to be cancelled before ReconcileTimeout.
// Cancelled WaitTask do not sent individual timeout WaitEvents
{
// WaitTask finished
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
Action: event.WaitAction,
GroupName: "wait-0",
Type: event.Finished, // TODO: add Cancelled event type
},
},
// TODO: Update the inventory after cancellation
// {
// // 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,
// },
// },
{
// Error
EventType: event.ErrorType,
ErrorEvent: &testutil.ExpErrorEvent{
Err: context.DeadlineExceeded,
},
},
},
},
"completed with timeout": {
expectRunTimeout: false,
runTimeout: 10 * time.Second,
testTimeout: 30 * time.Second,
resources: object.UnstructuredSet{
testutil.Unstructured(t, resources["deployment"]),
},
invInfo: inventoryInfo{
name: "abc-123",
namespace: "test",
id: "test",
},
clusterObjs: object.UnstructuredSet{},
options: Options{
// EmitStatusEvents required to test event output
EmitStatusEvents: true,
NoPrune: true,
InventoryPolicy: inventory.InventoryPolicyMustMatch,
// ReconcileTimeout required to enable WaitTasks
ReconcileTimeout: 1 * time.Minute,
},
statusEvents: []pollevent.Event{
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
{
EventType: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
Resource: testutil.Unstructured(t, resources["deployment"]),
},
},
// Resource becoming Current should unblock applier.Run WaitTask
},
expectedStatusEvents: []testutil.ExpEvent{
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.InProgressStatus,
},
},
{
EventType: event.StatusType,
StatusEvent: &testutil.ExpStatusEvent{
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Status: status.CurrentStatus,
},
},
},
expectedEvents: []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 Deployment
EventType: event.ApplyType,
ApplyEvent: &testutil.ExpApplyEvent{
GroupName: "apply-0",
Operation: event.Created,
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
},
},
{
// 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,
},
},
{
// Deployment reconcile pending.
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Operation: event.ReconcilePending,
},
},
{
// Deployment becomes Current.
EventType: event.WaitType,
WaitEvent: &testutil.ExpWaitEvent{
GroupName: "wait-0",
Identifier: testutil.ToIdentifier(t, resources["deployment"]),
Operation: event.Reconciled,
},
},
{
// WaitTask finished
EventType: event.ActionGroupType,
ActionGroupEvent: &testutil.ExpActionGroupEvent{
Action: event.WaitAction,
GroupName: "wait-0",
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,
},
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
poller := newFakePoller(tc.statusEvents)
applier := newTestApplier(t,
tc.invInfo,
tc.resources,
tc.clusterObjs,
poller,
)
// Context for Applier.Run
runCtx, runCancel := context.WithTimeout(context.Background(), tc.runTimeout)
defer runCancel() // cleanup
// Context for this test (in case Applier.Run never closes the event channel)
testCtx, testCancel := context.WithTimeout(context.Background(), tc.testTimeout)
defer testCancel() // cleanup
eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options)
// only start sending events once
var once sync.Once
var events []event.Event
loop:
for {
select {
case <-testCtx.Done():
// Test timed out
runCancel()
t.Errorf("Applier.Run failed to respond to cancellation (expected: %s, timeout: %s)", tc.runTimeout, tc.testTimeout)
break loop
case e, ok := <-eventChannel:
if !ok {
// Event channel closed
testCancel()
break loop
}
events = append(events, e)
if e.Type == event.ActionGroupType &&
e.ActionGroupEvent.Type == event.Finished {
// Send events after the first apply/prune task ends
if e.ActionGroupEvent.Action == event.ApplyAction ||
e.ActionGroupEvent.Action == event.PruneAction {
once.Do(func() {
// start events
poller.Start()
})
}
}
}
}
// Convert events to test events for comparison
receivedEvents := testutil.EventsToExpEvents(events)
// Validate & remove expected status events
for _, e := range tc.expectedStatusEvents {
var removed int
receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e)
if removed < 1 {
t.Fatalf("Expected status event not received: %#v", e.StatusEvent)
}
}
// Validate the rest of the events
testutil.AssertEqual(t, tc.expectedEvents, receivedEvents,
"Actual events (%d) do not match expected events (%d)",
len(receivedEvents), len(tc.expectedEvents))
// Validate that the expected timeout was the cause of the run completion.
// just in case something else cancelled the run
if tc.expectRunTimeout {
assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected timeout")
} else {
assert.Nil(t, runCtx.Err(), "Applier.Run exited, but not by expected timeout")
}
})
}
}
func TestReadAndPrepareObjectsNilInv(t *testing.T) {
applier := Applier{}
_, _, err := applier.prepareObjects(nil, object.UnstructuredSet{}, Options{})
assert.Error(t, err)
}
func TestReadAndPrepareObjects(t *testing.T) {
inventoryObj := testutil.Unstructured(t, resources["inventory"])
inventory := inventory.WrapInventoryInfoObj(inventoryObj)
obj1 := testutil.Unstructured(t, resources["obj1"])
obj2 := testutil.Unstructured(t, resources["obj2"])
clusterScopedObj := testutil.Unstructured(t, resources["clusterScopedObj"])
testCases := map[string]struct {
// objects in the cluster
clusterObjs object.UnstructuredSet
// inventory input to applier
invInfo inventoryInfo
// resources input to applier
resources object.UnstructuredSet
// expected objects to apply
applyObjs object.UnstructuredSet
// expected objects to prune
pruneObjs object.UnstructuredSet
// expected error
isError bool
}{
"objects include inventory": {
invInfo: inventoryInfo{
name: inventory.Name(),
namespace: inventory.Namespace(),
id: inventory.ID(),
},
resources: object.UnstructuredSet{inventoryObj},
isError: true,
},
"empty inventory, empty objects, apply none, prune none": {
invInfo: inventoryInfo{
name: inventory.Name(),
namespace: inventory.Namespace(),
id: inventory.ID(),
},
},
"one in inventory, empty objects, prune one": {
clusterObjs: object.UnstructuredSet{obj1},
invInfo: inventoryInfo{
name: inventory.Name(),
namespace: inventory.Namespace(),
id: inventory.ID(),
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(obj1),
},
},
pruneObjs: object.UnstructuredSet{obj1},
},
"all in inventory, apply all": {
invInfo: inventoryInfo{
name: inventory.Name(),
namespace: inventory.Namespace(),
id: inventory.ID(),
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(obj1),
object.UnstructuredToObjMetadata(clusterScopedObj),
},
},
resources: object.UnstructuredSet{obj1, clusterScopedObj},
applyObjs: object.UnstructuredSet{obj1, clusterScopedObj},
},
"disjoint set, apply new, prune old": {
clusterObjs: object.UnstructuredSet{obj2},
invInfo: inventoryInfo{
name: inventory.Name(),
namespace: inventory.Namespace(),
id: inventory.ID(),
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(obj2),
},
},
resources: object.UnstructuredSet{obj1, clusterScopedObj},
applyObjs: object.UnstructuredSet{obj1, clusterScopedObj},
pruneObjs: object.UnstructuredSet{obj2},
},
"most in inventory, apply all": {
clusterObjs: object.UnstructuredSet{obj2},
invInfo: inventoryInfo{
name: inventory.Name(),
namespace: inventory.Namespace(),
id: inventory.ID(),
set: object.ObjMetadataSet{
object.UnstructuredToObjMetadata(obj2),
},
},
resources: object.UnstructuredSet{obj1, obj2, clusterScopedObj},
applyObjs: object.UnstructuredSet{obj1, obj2, clusterScopedObj},
pruneObjs: object.UnstructuredSet{},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
applier := newTestApplier(t,
tc.invInfo,
tc.resources,
tc.clusterObjs,
// no events needed for prepareObjects
newFakePoller([]pollevent.Event{}),
)
applyObjs, pruneObjs, err := applier.prepareObjects(tc.invInfo.toWrapped(), tc.resources, Options{})
if tc.isError {
assert.Error(t, err)
return
}
require.NoError(t, err)
testutil.AssertEqual(t, applyObjs, tc.applyObjs,
"Actual applied objects (%d) do not match expected applied objects (%d)",
len(applyObjs), len(tc.applyObjs))
testutil.AssertEqual(t, pruneObjs, tc.pruneObjs,
"Actual pruned objects (%d) do not match expected pruned objects (%d)",
len(pruneObjs), len(tc.pruneObjs))
})
}
}