...

Source file src/sigs.k8s.io/cli-utils/pkg/apply/applier_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/apply

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package apply
     5  
     6  import (
     7  	"context"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  	"k8s.io/apimachinery/pkg/util/validation/field"
    15  	"k8s.io/kubectl/pkg/scheme"
    16  	"sigs.k8s.io/cli-utils/pkg/apis/actuation"
    17  	"sigs.k8s.io/cli-utils/pkg/apply/event"
    18  	"sigs.k8s.io/cli-utils/pkg/inventory"
    19  	pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
    20  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    21  	"sigs.k8s.io/cli-utils/pkg/kstatus/watcher"
    22  	"sigs.k8s.io/cli-utils/pkg/multierror"
    23  	"sigs.k8s.io/cli-utils/pkg/object"
    24  	"sigs.k8s.io/cli-utils/pkg/object/validation"
    25  	"sigs.k8s.io/cli-utils/pkg/testutil"
    26  )
    27  
    28  var (
    29  	codec     = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
    30  	resources = map[string]string{
    31  		"deployment": `
    32  apiVersion: apps/v1
    33  kind: Deployment
    34  metadata:
    35    name: foo
    36    namespace: default
    37    uid: dep-uid
    38    generation: 1
    39  spec:
    40    replicas: 1
    41  `,
    42  		"secret": `
    43  apiVersion: v1
    44  kind: Secret
    45  metadata:
    46    name: secret
    47    namespace: default
    48    uid: secret-uid
    49    generation: 1
    50  type: Opaque
    51  spec:
    52    foo: bar
    53  `,
    54  		"inventory": `
    55  apiVersion: v1
    56  kind: ConfigMap
    57  metadata:
    58    name: test-inventory-obj
    59    namespace: test-namespace
    60    labels:
    61      cli-utils.sigs.k8s.io/inventory-id: test-app-label
    62  data: {}
    63  `,
    64  		"obj1": `
    65  apiVersion: v1
    66  kind: Pod
    67  metadata:
    68    name: obj1
    69    namespace: test-namespace
    70  spec: {}
    71  `,
    72  		"obj2": `
    73  apiVersion: v1
    74  kind: Pod
    75  metadata:
    76    name: obj2
    77    namespace: test-namespace
    78  spec: {}
    79  `,
    80  		"clusterScopedObj": `
    81  apiVersion: rbac.authorization.k8s.io/v1
    82  kind: ClusterRole
    83  metadata:
    84    name: cluster-scoped-1
    85  `,
    86  	}
    87  )
    88  
    89  //nolint:dupl // event lists are very similar
    90  func TestApplier(t *testing.T) {
    91  	testCases := map[string]struct {
    92  		namespace string
    93  		// resources input to applier
    94  		resources object.UnstructuredSet
    95  		// inventory input to applier
    96  		invInfo inventoryInfo
    97  		// objects in the cluster
    98  		clusterObjs object.UnstructuredSet
    99  		// options input to applier.Run
   100  		options ApplierOptions
   101  		// fake input events from the statusWatcher
   102  		statusEvents []pollevent.Event
   103  		// expected output status events (async)
   104  		expectedStatusEvents []testutil.ExpEvent
   105  		// expected output events
   106  		expectedEvents []testutil.ExpEvent
   107  		// true if runTimeout is expected to have caused cancellation
   108  		expectRunTimeout bool
   109  		// true if testTimeout is expected to have caused cancellation
   110  		expectTestTimeout bool
   111  	}{
   112  		"initial apply without status or prune": {
   113  			namespace: "default",
   114  			resources: object.UnstructuredSet{
   115  				testutil.Unstructured(t, resources["deployment"]),
   116  			},
   117  			invInfo: inventoryInfo{
   118  				name:      "abc-123",
   119  				namespace: "default",
   120  				id:        "test",
   121  			},
   122  			clusterObjs: object.UnstructuredSet{},
   123  			options: ApplierOptions{
   124  				NoPrune:         true,
   125  				InventoryPolicy: inventory.PolicyMustMatch,
   126  			},
   127  			expectedEvents: []testutil.ExpEvent{
   128  				{
   129  					EventType: event.InitType,
   130  					InitEvent: &testutil.ExpInitEvent{},
   131  				},
   132  				{
   133  					EventType: event.ActionGroupType,
   134  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   135  						GroupName: "inventory-add-0",
   136  						Action:    event.InventoryAction,
   137  						Type:      event.Started,
   138  					},
   139  				},
   140  				{
   141  					EventType: event.ActionGroupType,
   142  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   143  						GroupName: "inventory-add-0",
   144  						Action:    event.InventoryAction,
   145  						Type:      event.Finished,
   146  					},
   147  				},
   148  
   149  				{
   150  					EventType: event.ActionGroupType,
   151  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   152  						GroupName: "apply-0",
   153  						Action:    event.ApplyAction,
   154  						Type:      event.Started,
   155  					},
   156  				},
   157  				{
   158  					EventType: event.ApplyType,
   159  					ApplyEvent: &testutil.ExpApplyEvent{
   160  						GroupName:  "apply-0",
   161  						Status:     event.ApplySuccessful, // Create new
   162  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   163  					},
   164  				},
   165  				{
   166  					EventType: event.ActionGroupType,
   167  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   168  						GroupName: "apply-0",
   169  						Action:    event.ApplyAction,
   170  						Type:      event.Finished,
   171  					},
   172  				},
   173  				{
   174  					EventType: event.ActionGroupType,
   175  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   176  						GroupName: "wait-0",
   177  						Action:    event.WaitAction,
   178  						Type:      event.Started,
   179  					},
   180  				},
   181  				{
   182  					EventType: event.WaitType,
   183  					WaitEvent: &testutil.ExpWaitEvent{
   184  						GroupName:  "wait-0",
   185  						Status:     event.ReconcilePending,
   186  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   187  					},
   188  				},
   189  				// Timeout waiting for status event saying deployment is current
   190  				// TODO: update inventory after timeout
   191  				// {
   192  				// 	EventType: event.ActionGroupType,
   193  				// 	ActionGroupEvent: &testutil.ExpActionGroupEvent{
   194  				// 		GroupName: "wait-0",
   195  				// 		Action:    event.WaitAction,
   196  				// 		Type:      event.Finished,
   197  				// 	},
   198  				// },
   199  				// {
   200  				// 	EventType: event.ActionGroupType,
   201  				// 	ActionGroupEvent: &testutil.ExpActionGroupEvent{
   202  				// 		GroupName: "inventory-set-0",
   203  				// 		Action:    event.InventoryAction,
   204  				// 		Type:      event.Started,
   205  				// 	},
   206  				// },
   207  				// {
   208  				// 	EventType: event.ActionGroupType,
   209  				// 	ActionGroupEvent: &testutil.ExpActionGroupEvent{
   210  				// 		GroupName: "inventory-set-0",
   211  				// 		Action:    event.InventoryAction,
   212  				// 		Type:      event.Finished,
   213  				// 	},
   214  				// },
   215  			},
   216  			expectTestTimeout: true,
   217  		},
   218  		"first apply multiple resources with status and prune": {
   219  			namespace: "default",
   220  			resources: object.UnstructuredSet{
   221  				testutil.Unstructured(t, resources["deployment"]),
   222  				testutil.Unstructured(t, resources["secret"]),
   223  			},
   224  			invInfo: inventoryInfo{
   225  				name:      "inv-123",
   226  				namespace: "default",
   227  				id:        "test",
   228  			},
   229  			clusterObjs: object.UnstructuredSet{},
   230  			options: ApplierOptions{
   231  				ReconcileTimeout: time.Minute,
   232  				InventoryPolicy:  inventory.PolicyMustMatch,
   233  				EmitStatusEvents: true,
   234  			},
   235  			statusEvents: []pollevent.Event{
   236  				{
   237  					Type: pollevent.ResourceUpdateEvent,
   238  					Resource: &pollevent.ResourceStatus{
   239  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   240  						Status:     status.InProgressStatus,
   241  						Resource:   testutil.Unstructured(t, resources["deployment"]),
   242  					},
   243  				},
   244  				{
   245  					Type: pollevent.ResourceUpdateEvent,
   246  					Resource: &pollevent.ResourceStatus{
   247  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   248  						Status:     status.CurrentStatus,
   249  						Resource:   testutil.Unstructured(t, resources["deployment"]),
   250  					},
   251  				},
   252  				{
   253  					Type: pollevent.ResourceUpdateEvent,
   254  					Resource: &pollevent.ResourceStatus{
   255  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   256  						Status:     status.CurrentStatus,
   257  						Resource:   testutil.Unstructured(t, resources["secret"]),
   258  					},
   259  				},
   260  			},
   261  			expectedStatusEvents: []testutil.ExpEvent{
   262  				{
   263  					EventType: event.StatusType,
   264  					StatusEvent: &testutil.ExpStatusEvent{
   265  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   266  						Status:     status.InProgressStatus,
   267  					},
   268  				},
   269  				{
   270  					EventType: event.StatusType,
   271  					StatusEvent: &testutil.ExpStatusEvent{
   272  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   273  						Status:     status.CurrentStatus,
   274  					},
   275  				},
   276  				{
   277  					EventType: event.StatusType,
   278  					StatusEvent: &testutil.ExpStatusEvent{
   279  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   280  						Status:     status.CurrentStatus,
   281  					},
   282  				},
   283  			},
   284  			expectedEvents: []testutil.ExpEvent{
   285  				{
   286  					EventType: event.InitType,
   287  					InitEvent: &testutil.ExpInitEvent{},
   288  				},
   289  				{
   290  					EventType: event.ActionGroupType,
   291  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   292  						GroupName: "inventory-add-0",
   293  						Action:    event.InventoryAction,
   294  						Type:      event.Started,
   295  					},
   296  				},
   297  				{
   298  					EventType: event.ActionGroupType,
   299  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   300  						GroupName: "inventory-add-0",
   301  						Action:    event.InventoryAction,
   302  						Type:      event.Finished,
   303  					},
   304  				},
   305  
   306  				{
   307  					EventType: event.ActionGroupType,
   308  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   309  						GroupName: "apply-0",
   310  						Action:    event.ApplyAction,
   311  						Type:      event.Started,
   312  					},
   313  				},
   314  				// Secrets applied before Deployments (see pkg/ordering)
   315  				{
   316  					EventType: event.ApplyType,
   317  					ApplyEvent: &testutil.ExpApplyEvent{
   318  						GroupName:  "apply-0",
   319  						Status:     event.ApplySuccessful, // Create new
   320  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   321  					},
   322  				},
   323  				{
   324  					EventType: event.ApplyType,
   325  					ApplyEvent: &testutil.ExpApplyEvent{
   326  						GroupName:  "apply-0",
   327  						Status:     event.ApplySuccessful, // Create new
   328  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   329  					},
   330  				},
   331  				{
   332  					EventType: event.ActionGroupType,
   333  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   334  						GroupName: "apply-0",
   335  						Action:    event.ApplyAction,
   336  						Type:      event.Finished,
   337  					},
   338  				},
   339  				{
   340  					EventType: event.ActionGroupType,
   341  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   342  						GroupName: "wait-0",
   343  						Action:    event.WaitAction,
   344  						Type:      event.Started,
   345  					},
   346  				},
   347  				// Wait events with same status sorted by Identifier (see pkg/testutil)
   348  				{
   349  					EventType: event.WaitType,
   350  					WaitEvent: &testutil.ExpWaitEvent{
   351  						GroupName:  "wait-0",
   352  						Status:     event.ReconcilePending,
   353  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   354  					},
   355  				},
   356  				{
   357  					EventType: event.WaitType,
   358  					WaitEvent: &testutil.ExpWaitEvent{
   359  						GroupName:  "wait-0",
   360  						Status:     event.ReconcilePending,
   361  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   362  					},
   363  				},
   364  				// Wait events with same status sorted by Identifier (see pkg/testutil)
   365  				{
   366  					EventType: event.WaitType,
   367  					WaitEvent: &testutil.ExpWaitEvent{
   368  						GroupName:  "wait-0",
   369  						Status:     event.ReconcileSuccessful,
   370  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   371  					},
   372  				},
   373  				{
   374  					EventType: event.WaitType,
   375  					WaitEvent: &testutil.ExpWaitEvent{
   376  						GroupName:  "wait-0",
   377  						Status:     event.ReconcileSuccessful,
   378  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   379  					},
   380  				},
   381  				{
   382  					EventType: event.ActionGroupType,
   383  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   384  						GroupName: "wait-0",
   385  						Action:    event.WaitAction,
   386  						Type:      event.Finished,
   387  					},
   388  				},
   389  				{
   390  					EventType: event.ActionGroupType,
   391  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   392  						GroupName: "inventory-set-0",
   393  						Action:    event.InventoryAction,
   394  						Type:      event.Started,
   395  					},
   396  				},
   397  				{
   398  					EventType: event.ActionGroupType,
   399  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   400  						GroupName: "inventory-set-0",
   401  						Action:    event.InventoryAction,
   402  						Type:      event.Finished,
   403  					},
   404  				},
   405  			},
   406  		},
   407  		"apply multiple existing resources with status and prune": {
   408  			namespace: "default",
   409  			resources: object.UnstructuredSet{
   410  				testutil.Unstructured(t, resources["deployment"]),
   411  				testutil.Unstructured(t, resources["secret"]),
   412  			},
   413  			invInfo: inventoryInfo{
   414  				name:      "inv-123",
   415  				namespace: "default",
   416  				id:        "test",
   417  				set: object.ObjMetadataSet{
   418  					object.UnstructuredToObjMetadata(
   419  						testutil.Unstructured(t, resources["deployment"]),
   420  					),
   421  				},
   422  			},
   423  			clusterObjs: object.UnstructuredSet{
   424  				testutil.Unstructured(t, resources["deployment"]),
   425  			},
   426  			options: ApplierOptions{
   427  				ReconcileTimeout: time.Minute,
   428  				InventoryPolicy:  inventory.PolicyAdoptIfNoInventory,
   429  				EmitStatusEvents: true,
   430  			},
   431  			statusEvents: []pollevent.Event{
   432  				{
   433  					Type: pollevent.ResourceUpdateEvent,
   434  					Resource: &pollevent.ResourceStatus{
   435  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   436  						Status:     status.CurrentStatus,
   437  						Resource:   testutil.Unstructured(t, resources["deployment"]),
   438  					},
   439  				},
   440  				{
   441  					Type: pollevent.ResourceUpdateEvent,
   442  					Resource: &pollevent.ResourceStatus{
   443  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   444  						Status:     status.CurrentStatus,
   445  						Resource:   testutil.Unstructured(t, resources["secret"]),
   446  					},
   447  				},
   448  			},
   449  			expectedStatusEvents: []testutil.ExpEvent{
   450  				{
   451  					EventType: event.StatusType,
   452  					StatusEvent: &testutil.ExpStatusEvent{
   453  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   454  						Status:     status.CurrentStatus,
   455  					},
   456  				},
   457  				{
   458  					EventType: event.StatusType,
   459  					StatusEvent: &testutil.ExpStatusEvent{
   460  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   461  						Status:     status.CurrentStatus,
   462  					},
   463  				},
   464  			},
   465  			expectedEvents: []testutil.ExpEvent{
   466  				{
   467  					EventType: event.InitType,
   468  					InitEvent: &testutil.ExpInitEvent{},
   469  				},
   470  				{
   471  					EventType: event.ActionGroupType,
   472  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   473  						GroupName: "inventory-add-0",
   474  						Action:    event.InventoryAction,
   475  						Type:      event.Started,
   476  					},
   477  				},
   478  				{
   479  					EventType: event.ActionGroupType,
   480  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   481  						GroupName: "inventory-add-0",
   482  						Action:    event.InventoryAction,
   483  						Type:      event.Finished,
   484  					},
   485  				},
   486  
   487  				{
   488  					EventType: event.ActionGroupType,
   489  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   490  						GroupName: "apply-0",
   491  						Action:    event.ApplyAction,
   492  						Type:      event.Started,
   493  					},
   494  				},
   495  				// Apply Secrets before Deployments (see ordering.SortableMetas)
   496  				{
   497  					EventType: event.ApplyType,
   498  					ApplyEvent: &testutil.ExpApplyEvent{
   499  						GroupName:  "apply-0",
   500  						Status:     event.ApplySuccessful, // Create new
   501  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   502  					},
   503  				},
   504  				{
   505  					EventType: event.ApplyType,
   506  					ApplyEvent: &testutil.ExpApplyEvent{
   507  						GroupName:  "apply-0",
   508  						Status:     event.ApplySuccessful, // Update existing
   509  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   510  					},
   511  				},
   512  				{
   513  					EventType: event.ActionGroupType,
   514  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   515  						GroupName: "apply-0",
   516  						Action:    event.ApplyAction,
   517  						Type:      event.Finished,
   518  					},
   519  				},
   520  				{
   521  					EventType: event.ActionGroupType,
   522  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   523  						GroupName: "wait-0",
   524  						Action:    event.WaitAction,
   525  						Type:      event.Started,
   526  					},
   527  				},
   528  				// Wait events with same status sorted by Identifier (see pkg/testutil)
   529  				{
   530  					EventType: event.WaitType,
   531  					WaitEvent: &testutil.ExpWaitEvent{
   532  						GroupName:  "wait-0",
   533  						Status:     event.ReconcilePending,
   534  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   535  					},
   536  				},
   537  				{
   538  					EventType: event.WaitType,
   539  					WaitEvent: &testutil.ExpWaitEvent{
   540  						GroupName:  "wait-0",
   541  						Status:     event.ReconcilePending,
   542  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   543  					},
   544  				},
   545  				// Wait events with same status sorted by Identifier (see pkg/testutil)
   546  				{
   547  					EventType: event.WaitType,
   548  					WaitEvent: &testutil.ExpWaitEvent{
   549  						GroupName:  "wait-0",
   550  						Status:     event.ReconcileSuccessful,
   551  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   552  					},
   553  				},
   554  				{
   555  					EventType: event.WaitType,
   556  					WaitEvent: &testutil.ExpWaitEvent{
   557  						GroupName:  "wait-0",
   558  						Status:     event.ReconcileSuccessful,
   559  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   560  					},
   561  				},
   562  				{
   563  					EventType: event.ActionGroupType,
   564  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   565  						GroupName: "wait-0",
   566  						Action:    event.WaitAction,
   567  						Type:      event.Finished,
   568  					},
   569  				},
   570  				{
   571  					EventType: event.ActionGroupType,
   572  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   573  						GroupName: "inventory-set-0",
   574  						Action:    event.InventoryAction,
   575  						Type:      event.Started,
   576  					},
   577  				},
   578  				{
   579  					EventType: event.ActionGroupType,
   580  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   581  						GroupName: "inventory-set-0",
   582  						Action:    event.InventoryAction,
   583  						Type:      event.Finished,
   584  					},
   585  				},
   586  			},
   587  		},
   588  		"apply no resources and prune all existing": {
   589  			namespace: "default",
   590  			resources: object.UnstructuredSet{},
   591  			invInfo: inventoryInfo{
   592  				name:      "inv-123",
   593  				namespace: "default",
   594  				id:        "test",
   595  				set: object.ObjMetadataSet{
   596  					object.UnstructuredToObjMetadata(
   597  						testutil.Unstructured(t, resources["deployment"]),
   598  					),
   599  					object.UnstructuredToObjMetadata(
   600  						testutil.Unstructured(t, resources["secret"]),
   601  					),
   602  				},
   603  			},
   604  			clusterObjs: object.UnstructuredSet{
   605  				testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
   606  				testutil.Unstructured(t, resources["secret"], testutil.AddOwningInv(t, "test")),
   607  			},
   608  			options: ApplierOptions{
   609  				InventoryPolicy:  inventory.PolicyMustMatch,
   610  				EmitStatusEvents: true,
   611  			},
   612  			statusEvents: []pollevent.Event{
   613  				{
   614  					Type: pollevent.ResourceUpdateEvent,
   615  					Resource: &pollevent.ResourceStatus{
   616  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   617  						Status:     status.InProgressStatus,
   618  					},
   619  				},
   620  				{
   621  					Type: pollevent.ResourceUpdateEvent,
   622  					Resource: &pollevent.ResourceStatus{
   623  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   624  						Status:     status.InProgressStatus,
   625  					},
   626  				},
   627  				{
   628  					Type: pollevent.ResourceUpdateEvent,
   629  					Resource: &pollevent.ResourceStatus{
   630  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   631  						Status:     status.NotFoundStatus,
   632  					},
   633  				},
   634  				{
   635  					Type: pollevent.ResourceUpdateEvent,
   636  					Resource: &pollevent.ResourceStatus{
   637  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   638  						Status:     status.NotFoundStatus,
   639  					},
   640  				},
   641  			},
   642  			expectedStatusEvents: []testutil.ExpEvent{
   643  				{
   644  					EventType: event.ActionGroupType,
   645  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   646  						GroupName: "inventory-add-0",
   647  						Action:    event.InventoryAction,
   648  						Type:      event.Started,
   649  					},
   650  				},
   651  				{
   652  					EventType: event.ActionGroupType,
   653  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   654  						GroupName: "inventory-add-0",
   655  						Action:    event.InventoryAction,
   656  						Type:      event.Finished,
   657  					},
   658  				},
   659  				{
   660  					EventType: event.StatusType,
   661  					StatusEvent: &testutil.ExpStatusEvent{
   662  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   663  						Status:     status.InProgressStatus,
   664  					},
   665  				},
   666  				{
   667  					EventType: event.StatusType,
   668  					StatusEvent: &testutil.ExpStatusEvent{
   669  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   670  						Status:     status.InProgressStatus,
   671  					},
   672  				},
   673  				{
   674  					EventType: event.StatusType,
   675  					StatusEvent: &testutil.ExpStatusEvent{
   676  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   677  						Status:     status.NotFoundStatus,
   678  					},
   679  				},
   680  				{
   681  					EventType: event.StatusType,
   682  					StatusEvent: &testutil.ExpStatusEvent{
   683  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   684  						Status:     status.NotFoundStatus,
   685  					},
   686  				},
   687  			},
   688  			expectedEvents: []testutil.ExpEvent{
   689  				{
   690  					EventType: event.InitType,
   691  					InitEvent: &testutil.ExpInitEvent{},
   692  				},
   693  				{
   694  					EventType: event.ActionGroupType,
   695  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   696  						GroupName: "prune-0",
   697  						Action:    event.PruneAction,
   698  						Type:      event.Started,
   699  					},
   700  				},
   701  				// Prune Deployments before Secrets (see ordering.SortableMetas)
   702  				{
   703  					EventType: event.PruneType,
   704  					PruneEvent: &testutil.ExpPruneEvent{
   705  						GroupName:  "prune-0",
   706  						Status:     event.PruneSuccessful,
   707  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   708  					},
   709  				},
   710  				{
   711  					EventType: event.PruneType,
   712  					PruneEvent: &testutil.ExpPruneEvent{
   713  						GroupName:  "prune-0",
   714  						Status:     event.PruneSuccessful,
   715  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   716  					},
   717  				},
   718  				{
   719  					EventType: event.ActionGroupType,
   720  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   721  						GroupName: "prune-0",
   722  						Action:    event.PruneAction,
   723  						Type:      event.Finished,
   724  					},
   725  				},
   726  				{
   727  					EventType: event.ActionGroupType,
   728  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   729  						GroupName: "wait-0",
   730  						Action:    event.WaitAction,
   731  						Type:      event.Started,
   732  					},
   733  				},
   734  				// Wait events with same status sorted by Identifier (see pkg/testutil)
   735  				{
   736  					EventType: event.WaitType,
   737  					WaitEvent: &testutil.ExpWaitEvent{
   738  						GroupName:  "wait-0",
   739  						Status:     event.ReconcilePending,
   740  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   741  					},
   742  				},
   743  				{
   744  					EventType: event.WaitType,
   745  					WaitEvent: &testutil.ExpWaitEvent{
   746  						GroupName:  "wait-0",
   747  						Status:     event.ReconcilePending,
   748  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   749  					},
   750  				},
   751  				// Wait events with same status sorted by Identifier (see pkg/testutil)
   752  				{
   753  					EventType: event.WaitType,
   754  					WaitEvent: &testutil.ExpWaitEvent{
   755  						GroupName:  "wait-0",
   756  						Status:     event.ReconcileSuccessful,
   757  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   758  					},
   759  				},
   760  				{
   761  					EventType: event.WaitType,
   762  					WaitEvent: &testutil.ExpWaitEvent{
   763  						GroupName:  "wait-0",
   764  						Status:     event.ReconcileSuccessful,
   765  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
   766  					},
   767  				},
   768  				{
   769  					EventType: event.ActionGroupType,
   770  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   771  						GroupName: "wait-0",
   772  						Action:    event.WaitAction,
   773  						Type:      event.Finished,
   774  					},
   775  				},
   776  				{
   777  					EventType: event.ActionGroupType,
   778  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   779  						GroupName: "inventory-set-0",
   780  						Action:    event.InventoryAction,
   781  						Type:      event.Started,
   782  					},
   783  				},
   784  				{
   785  					EventType: event.ActionGroupType,
   786  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   787  						GroupName: "inventory-set-0",
   788  						Action:    event.InventoryAction,
   789  						Type:      event.Finished,
   790  					},
   791  				},
   792  			},
   793  		},
   794  		"apply resource with existing object belonging to different inventory": {
   795  			namespace: "default",
   796  			resources: object.UnstructuredSet{
   797  				testutil.Unstructured(t, resources["deployment"]),
   798  			},
   799  			invInfo: inventoryInfo{
   800  				name:      "abc-123",
   801  				namespace: "default",
   802  				id:        "test",
   803  			},
   804  			clusterObjs: object.UnstructuredSet{
   805  				testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")),
   806  			},
   807  			options: ApplierOptions{
   808  				ReconcileTimeout: time.Minute,
   809  				InventoryPolicy:  inventory.PolicyMustMatch,
   810  				EmitStatusEvents: true,
   811  			},
   812  			// There could be some status events for the existing Deployment,
   813  			// but we can't always expect to receive them before the applier
   814  			// exits, because the WaitTask is skipped when the ApplyTask errors.
   815  			// So don't bother sending or expecting them.
   816  			statusEvents:         []pollevent.Event{},
   817  			expectedStatusEvents: []testutil.ExpEvent{},
   818  			expectedEvents: []testutil.ExpEvent{
   819  				{
   820  					EventType: event.InitType,
   821  					InitEvent: &testutil.ExpInitEvent{},
   822  				},
   823  				{
   824  					EventType: event.ActionGroupType,
   825  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   826  						GroupName: "inventory-add-0",
   827  						Action:    event.InventoryAction,
   828  						Type:      event.Started,
   829  					},
   830  				},
   831  				{
   832  					EventType: event.ActionGroupType,
   833  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   834  						GroupName: "inventory-add-0",
   835  						Action:    event.InventoryAction,
   836  						Type:      event.Finished,
   837  					},
   838  				},
   839  				{
   840  					EventType: event.ActionGroupType,
   841  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   842  						GroupName: "apply-0",
   843  						Action:    event.ApplyAction,
   844  						Type:      event.Started,
   845  					},
   846  				},
   847  				{
   848  					EventType: event.ApplyType,
   849  					ApplyEvent: &testutil.ExpApplyEvent{
   850  						GroupName:  "apply-0",
   851  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   852  						Status:     event.ApplySkipped,
   853  						Error: &inventory.PolicyPreventedActuationError{
   854  							Strategy: actuation.ActuationStrategyApply,
   855  							Policy:   inventory.PolicyMustMatch,
   856  							Status:   inventory.NoMatch,
   857  						},
   858  					},
   859  				},
   860  				{
   861  					EventType: event.ActionGroupType,
   862  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   863  						GroupName: "apply-0",
   864  						Action:    event.ApplyAction,
   865  						Type:      event.Finished,
   866  					},
   867  				},
   868  				{
   869  					EventType: event.ActionGroupType,
   870  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   871  						GroupName: "wait-0",
   872  						Action:    event.WaitAction,
   873  						Type:      event.Started,
   874  					},
   875  				},
   876  				{
   877  					EventType: event.WaitType,
   878  					WaitEvent: &testutil.ExpWaitEvent{
   879  						GroupName:  "wait-0",
   880  						Status:     event.ReconcileSkipped,
   881  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   882  					},
   883  				},
   884  				{
   885  					EventType: event.ActionGroupType,
   886  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   887  						GroupName: "wait-0",
   888  						Action:    event.WaitAction,
   889  						Type:      event.Finished,
   890  					},
   891  				},
   892  				{
   893  					EventType: event.ActionGroupType,
   894  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   895  						GroupName: "inventory-set-0",
   896  						Action:    event.InventoryAction,
   897  						Type:      event.Started,
   898  					},
   899  				},
   900  				{
   901  					EventType: event.ActionGroupType,
   902  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   903  						GroupName: "inventory-set-0",
   904  						Action:    event.InventoryAction,
   905  						Type:      event.Finished,
   906  					},
   907  				},
   908  			},
   909  		},
   910  		"resources belonging to a different inventory should not be pruned": {
   911  			namespace: "default",
   912  			resources: object.UnstructuredSet{},
   913  			invInfo: inventoryInfo{
   914  				name:      "abc-123",
   915  				namespace: "default",
   916  				id:        "test",
   917  				set: object.ObjMetadataSet{
   918  					object.UnstructuredToObjMetadata(
   919  						testutil.Unstructured(t, resources["deployment"]),
   920  					),
   921  				},
   922  			},
   923  			clusterObjs: object.UnstructuredSet{
   924  				testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "unmatched")),
   925  			},
   926  			options: ApplierOptions{
   927  				InventoryPolicy:  inventory.PolicyMustMatch,
   928  				EmitStatusEvents: true,
   929  			},
   930  			expectedEvents: []testutil.ExpEvent{
   931  				{
   932  					EventType: event.InitType,
   933  					InitEvent: &testutil.ExpInitEvent{},
   934  				},
   935  				{
   936  					EventType: event.ActionGroupType,
   937  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   938  						GroupName: "inventory-add-0",
   939  						Action:    event.InventoryAction,
   940  						Type:      event.Started,
   941  					},
   942  				},
   943  				{
   944  					EventType: event.ActionGroupType,
   945  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   946  						GroupName: "inventory-add-0",
   947  						Action:    event.InventoryAction,
   948  						Type:      event.Finished,
   949  					},
   950  				},
   951  				{
   952  					EventType: event.ActionGroupType,
   953  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   954  						GroupName: "prune-0",
   955  						Action:    event.PruneAction,
   956  						Type:      event.Started,
   957  					},
   958  				},
   959  				{
   960  					EventType: event.PruneType,
   961  					PruneEvent: &testutil.ExpPruneEvent{
   962  						GroupName:  "prune-0",
   963  						Status:     event.PruneSkipped,
   964  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   965  						Error: &inventory.PolicyPreventedActuationError{
   966  							Strategy: actuation.ActuationStrategyDelete,
   967  							Policy:   inventory.PolicyMustMatch,
   968  							Status:   inventory.NoMatch,
   969  						},
   970  					},
   971  				},
   972  				{
   973  					EventType: event.ActionGroupType,
   974  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   975  						GroupName: "prune-0",
   976  						Action:    event.PruneAction,
   977  						Type:      event.Finished,
   978  					},
   979  				},
   980  				{
   981  					EventType: event.ActionGroupType,
   982  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   983  						GroupName: "wait-0",
   984  						Action:    event.WaitAction,
   985  						Type:      event.Started,
   986  					},
   987  				},
   988  				{
   989  					EventType: event.WaitType,
   990  					WaitEvent: &testutil.ExpWaitEvent{
   991  						GroupName:  "wait-0",
   992  						Status:     event.ReconcileSkipped,
   993  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   994  					},
   995  				},
   996  				{
   997  					EventType: event.ActionGroupType,
   998  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   999  						GroupName: "wait-0",
  1000  						Action:    event.WaitAction,
  1001  						Type:      event.Finished,
  1002  					},
  1003  				},
  1004  				{
  1005  					EventType: event.ActionGroupType,
  1006  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1007  						GroupName: "inventory-set-0",
  1008  						Action:    event.InventoryAction,
  1009  						Type:      event.Started,
  1010  					},
  1011  				},
  1012  				{
  1013  					EventType: event.ActionGroupType,
  1014  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1015  						GroupName: "inventory-set-0",
  1016  						Action:    event.InventoryAction,
  1017  						Type:      event.Finished,
  1018  					},
  1019  				},
  1020  			},
  1021  		},
  1022  		"prune with inventory object annotation matched": {
  1023  			namespace: "default",
  1024  			resources: object.UnstructuredSet{},
  1025  			invInfo: inventoryInfo{
  1026  				name:      "abc-123",
  1027  				namespace: "default",
  1028  				id:        "test",
  1029  				set: object.ObjMetadataSet{
  1030  					object.UnstructuredToObjMetadata(
  1031  						testutil.Unstructured(t, resources["deployment"]),
  1032  					),
  1033  				},
  1034  			},
  1035  			clusterObjs: object.UnstructuredSet{
  1036  				testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
  1037  			},
  1038  			options: ApplierOptions{
  1039  				InventoryPolicy:  inventory.PolicyMustMatch,
  1040  				EmitStatusEvents: true,
  1041  			},
  1042  			statusEvents: []pollevent.Event{
  1043  				{
  1044  					Type: pollevent.ResourceUpdateEvent,
  1045  					Resource: &pollevent.ResourceStatus{
  1046  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1047  						Status:     status.InProgressStatus,
  1048  					},
  1049  				},
  1050  				{
  1051  					Type: pollevent.ResourceUpdateEvent,
  1052  					Resource: &pollevent.ResourceStatus{
  1053  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1054  						Status:     status.NotFoundStatus,
  1055  					},
  1056  				},
  1057  			},
  1058  			expectedStatusEvents: []testutil.ExpEvent{
  1059  				{
  1060  					EventType: event.StatusType,
  1061  					StatusEvent: &testutil.ExpStatusEvent{
  1062  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1063  						Status:     status.InProgressStatus,
  1064  					},
  1065  				},
  1066  				{
  1067  					EventType: event.StatusType,
  1068  					StatusEvent: &testutil.ExpStatusEvent{
  1069  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1070  						Status:     status.NotFoundStatus,
  1071  					},
  1072  				},
  1073  			},
  1074  			expectedEvents: []testutil.ExpEvent{
  1075  				{
  1076  					EventType: event.InitType,
  1077  					InitEvent: &testutil.ExpInitEvent{},
  1078  				},
  1079  				{
  1080  					EventType: event.ActionGroupType,
  1081  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1082  						GroupName: "inventory-add-0",
  1083  						Action:    event.InventoryAction,
  1084  						Type:      event.Started,
  1085  					},
  1086  				},
  1087  				{
  1088  					EventType: event.ActionGroupType,
  1089  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1090  						GroupName: "inventory-add-0",
  1091  						Action:    event.InventoryAction,
  1092  						Type:      event.Finished,
  1093  					},
  1094  				},
  1095  				{
  1096  					EventType: event.ActionGroupType,
  1097  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1098  						GroupName: "prune-0",
  1099  						Action:    event.PruneAction,
  1100  						Type:      event.Started,
  1101  					},
  1102  				},
  1103  				{
  1104  					EventType: event.PruneType,
  1105  					PruneEvent: &testutil.ExpPruneEvent{
  1106  						GroupName:  "prune-0",
  1107  						Status:     event.PruneSuccessful,
  1108  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1109  					},
  1110  				},
  1111  				{
  1112  					EventType: event.ActionGroupType,
  1113  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1114  						GroupName: "prune-0",
  1115  						Action:    event.PruneAction,
  1116  						Type:      event.Finished,
  1117  					},
  1118  				},
  1119  				{
  1120  					EventType: event.ActionGroupType,
  1121  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1122  						GroupName: "wait-0",
  1123  						Action:    event.WaitAction,
  1124  						Type:      event.Started,
  1125  					},
  1126  				},
  1127  				// Wait events sorted Pending > Successful (see pkg/testutil)
  1128  				{
  1129  					EventType: event.WaitType,
  1130  					WaitEvent: &testutil.ExpWaitEvent{
  1131  						GroupName:  "wait-0",
  1132  						Status:     event.ReconcilePending,
  1133  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1134  					},
  1135  				},
  1136  				{
  1137  					EventType: event.WaitType,
  1138  					WaitEvent: &testutil.ExpWaitEvent{
  1139  						GroupName:  "wait-0",
  1140  						Status:     event.ReconcileSuccessful,
  1141  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1142  					},
  1143  				},
  1144  				{
  1145  					EventType: event.ActionGroupType,
  1146  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1147  						GroupName: "wait-0",
  1148  						Action:    event.WaitAction,
  1149  						Type:      event.Finished,
  1150  					},
  1151  				},
  1152  				{
  1153  					EventType: event.ActionGroupType,
  1154  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1155  						GroupName: "inventory-set-0",
  1156  						Action:    event.InventoryAction,
  1157  						Type:      event.Started,
  1158  					},
  1159  				},
  1160  				{
  1161  					EventType: event.ActionGroupType,
  1162  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1163  						GroupName: "inventory-set-0",
  1164  						Action:    event.InventoryAction,
  1165  						Type:      event.Finished,
  1166  					},
  1167  				},
  1168  			},
  1169  		},
  1170  		"SkipInvalid - skip invalid objects and apply valid objects": {
  1171  			namespace: "default",
  1172  			resources: object.UnstructuredSet{
  1173  				testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1174  					"$.metadata.name", "",
  1175  				}),
  1176  				testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1177  					"$.kind", "",
  1178  				}),
  1179  				testutil.Unstructured(t, resources["secret"]),
  1180  			},
  1181  			invInfo: inventoryInfo{
  1182  				name:      "inv-123",
  1183  				namespace: "default",
  1184  				id:        "test",
  1185  			},
  1186  			clusterObjs: object.UnstructuredSet{},
  1187  			options: ApplierOptions{
  1188  				ReconcileTimeout: time.Minute,
  1189  				InventoryPolicy:  inventory.PolicyAdoptIfNoInventory,
  1190  				EmitStatusEvents: true,
  1191  				ValidationPolicy: validation.SkipInvalid,
  1192  			},
  1193  			statusEvents: []pollevent.Event{
  1194  				{
  1195  					Type: pollevent.ResourceUpdateEvent,
  1196  					Resource: &pollevent.ResourceStatus{
  1197  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
  1198  						Status:     status.CurrentStatus,
  1199  						Resource:   testutil.Unstructured(t, resources["secret"]),
  1200  					},
  1201  				},
  1202  			},
  1203  			expectedStatusEvents: []testutil.ExpEvent{
  1204  				{
  1205  					EventType: event.StatusType,
  1206  					StatusEvent: &testutil.ExpStatusEvent{
  1207  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
  1208  						Status:     status.CurrentStatus,
  1209  					},
  1210  				},
  1211  			},
  1212  			expectedEvents: []testutil.ExpEvent{
  1213  				{
  1214  					EventType: event.ValidationType,
  1215  					ValidationEvent: &testutil.ExpValidationEvent{
  1216  						Identifiers: object.ObjMetadataSet{
  1217  							object.UnstructuredToObjMetadata(
  1218  								testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1219  									"$.metadata.name", "",
  1220  								}),
  1221  							),
  1222  						},
  1223  						Error: testutil.EqualErrorString(validation.NewError(
  1224  							field.Required(field.NewPath("metadata", "name"), "name is required"),
  1225  							object.UnstructuredToObjMetadata(
  1226  								testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1227  									"$.metadata.name", "",
  1228  								}),
  1229  							),
  1230  						).Error()),
  1231  					},
  1232  				},
  1233  				{
  1234  					EventType: event.ValidationType,
  1235  					ValidationEvent: &testutil.ExpValidationEvent{
  1236  						Identifiers: object.ObjMetadataSet{
  1237  							object.UnstructuredToObjMetadata(
  1238  								testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1239  									"$.kind", "",
  1240  								}),
  1241  							),
  1242  						},
  1243  						Error: testutil.EqualErrorString(validation.NewError(
  1244  							field.Required(field.NewPath("kind"), "kind is required"),
  1245  							object.UnstructuredToObjMetadata(
  1246  								testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1247  									"$.kind", "",
  1248  								}),
  1249  							),
  1250  						).Error()),
  1251  					},
  1252  				},
  1253  				{
  1254  					EventType: event.InitType,
  1255  					InitEvent: &testutil.ExpInitEvent{},
  1256  				},
  1257  				{
  1258  					EventType: event.ActionGroupType,
  1259  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1260  						GroupName: "inventory-add-0",
  1261  						Action:    event.InventoryAction,
  1262  						Type:      event.Started,
  1263  					},
  1264  				},
  1265  				{
  1266  					EventType: event.ActionGroupType,
  1267  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1268  						GroupName: "inventory-add-0",
  1269  						Action:    event.InventoryAction,
  1270  						Type:      event.Finished,
  1271  					},
  1272  				},
  1273  
  1274  				{
  1275  					EventType: event.ActionGroupType,
  1276  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1277  						GroupName: "apply-0",
  1278  						Action:    event.ApplyAction,
  1279  						Type:      event.Started,
  1280  					},
  1281  				},
  1282  				// Secret applied
  1283  				{
  1284  					EventType: event.ApplyType,
  1285  					ApplyEvent: &testutil.ExpApplyEvent{
  1286  						GroupName:  "apply-0",
  1287  						Status:     event.ApplySuccessful, // Create new
  1288  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
  1289  					},
  1290  				},
  1291  				{
  1292  					EventType: event.ActionGroupType,
  1293  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1294  						GroupName: "apply-0",
  1295  						Action:    event.ApplyAction,
  1296  						Type:      event.Finished,
  1297  					},
  1298  				},
  1299  				{
  1300  					EventType: event.ActionGroupType,
  1301  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1302  						GroupName: "wait-0",
  1303  						Action:    event.WaitAction,
  1304  						Type:      event.Started,
  1305  					},
  1306  				},
  1307  				// Wait events sorted Pending > Successful (see pkg/testutil)
  1308  				{
  1309  					EventType: event.WaitType,
  1310  					WaitEvent: &testutil.ExpWaitEvent{
  1311  						GroupName:  "wait-0",
  1312  						Status:     event.ReconcilePending,
  1313  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
  1314  					},
  1315  				},
  1316  				{
  1317  					EventType: event.WaitType,
  1318  					WaitEvent: &testutil.ExpWaitEvent{
  1319  						GroupName:  "wait-0",
  1320  						Status:     event.ReconcileSuccessful,
  1321  						Identifier: testutil.ToIdentifier(t, resources["secret"]),
  1322  					},
  1323  				},
  1324  				{
  1325  					EventType: event.ActionGroupType,
  1326  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1327  						GroupName: "wait-0",
  1328  						Action:    event.WaitAction,
  1329  						Type:      event.Finished,
  1330  					},
  1331  				},
  1332  				{
  1333  					EventType: event.ActionGroupType,
  1334  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1335  						GroupName: "inventory-set-0",
  1336  						Action:    event.InventoryAction,
  1337  						Type:      event.Started,
  1338  					},
  1339  				},
  1340  				{
  1341  					EventType: event.ActionGroupType,
  1342  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1343  						GroupName: "inventory-set-0",
  1344  						Action:    event.InventoryAction,
  1345  						Type:      event.Finished,
  1346  					},
  1347  				},
  1348  			},
  1349  		},
  1350  		"ExitEarly - exit early on invalid objects and skip valid objects": {
  1351  			namespace: "default",
  1352  			resources: object.UnstructuredSet{
  1353  				testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1354  					"$.metadata.name", "",
  1355  				}),
  1356  				testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1357  					"$.kind", "",
  1358  				}),
  1359  				testutil.Unstructured(t, resources["secret"]),
  1360  			},
  1361  			invInfo: inventoryInfo{
  1362  				name:      "inv-123",
  1363  				namespace: "default",
  1364  				id:        "test",
  1365  			},
  1366  			clusterObjs: object.UnstructuredSet{},
  1367  			options: ApplierOptions{
  1368  				ReconcileTimeout: time.Minute,
  1369  				InventoryPolicy:  inventory.PolicyAdoptIfNoInventory,
  1370  				EmitStatusEvents: true,
  1371  				ValidationPolicy: validation.ExitEarly,
  1372  			},
  1373  			statusEvents:         []pollevent.Event{},
  1374  			expectedStatusEvents: []testutil.ExpEvent{},
  1375  			expectedEvents: []testutil.ExpEvent{
  1376  				{
  1377  					EventType: event.ErrorType,
  1378  					ErrorEvent: &testutil.ExpErrorEvent{
  1379  						Err: testutil.EqualErrorString(multierror.New(
  1380  							validation.NewError(
  1381  								field.Required(field.NewPath("metadata", "name"), "name is required"),
  1382  								object.UnstructuredToObjMetadata(
  1383  									testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1384  										"$.metadata.name", "",
  1385  									}),
  1386  								),
  1387  							),
  1388  							validation.NewError(
  1389  								field.Required(field.NewPath("kind"), "kind is required"),
  1390  								object.UnstructuredToObjMetadata(
  1391  									testutil.Unstructured(t, resources["deployment"], JSONPathSetter{
  1392  										"$.kind", "",
  1393  									}),
  1394  								),
  1395  							),
  1396  						).Error()),
  1397  					},
  1398  				},
  1399  			},
  1400  		},
  1401  	}
  1402  
  1403  	for tn, tc := range testCases {
  1404  		t.Run(tn, func(t *testing.T) {
  1405  			statusWatcher := newFakeWatcher(tc.statusEvents)
  1406  
  1407  			// Only feed valid objects into the TestApplier.
  1408  			// Invalid objects should not generate API requests.
  1409  			validObjs := object.UnstructuredSet{}
  1410  			for _, obj := range tc.resources {
  1411  				id := object.UnstructuredToObjMetadata(obj)
  1412  				if id.GroupKind.Kind == "" || id.Name == "" {
  1413  					continue
  1414  				}
  1415  				validObjs = append(validObjs, obj)
  1416  			}
  1417  
  1418  			applier := newTestApplier(t,
  1419  				tc.invInfo,
  1420  				validObjs,
  1421  				tc.clusterObjs,
  1422  				statusWatcher,
  1423  			)
  1424  
  1425  			// Context for Applier.Run
  1426  			runCtx, runCancel := context.WithCancel(context.Background())
  1427  			defer runCancel() // cleanup
  1428  
  1429  			// Context for this test (in case Applier.Run never closes the event channel)
  1430  			testTimeout := 10 * time.Second
  1431  			testCtx, testCancel := context.WithTimeout(context.Background(), testTimeout)
  1432  			defer testCancel() // cleanup
  1433  
  1434  			eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options)
  1435  
  1436  			// only start sending events once
  1437  			var once sync.Once
  1438  
  1439  			var events []event.Event
  1440  
  1441  		loop:
  1442  			for {
  1443  				select {
  1444  				case <-testCtx.Done():
  1445  					// Test timed out
  1446  					runCancel()
  1447  					if tc.expectTestTimeout {
  1448  						assert.Equal(t, context.DeadlineExceeded, testCtx.Err(), "Applier.Run failed to exit, but not because of expected timeout")
  1449  					} else {
  1450  						t.Errorf("Applier.Run failed to exit (timeout: %s)", testTimeout)
  1451  					}
  1452  					break loop
  1453  
  1454  				case e, ok := <-eventChannel:
  1455  					if !ok {
  1456  						// Event channel closed
  1457  						testCancel()
  1458  						break loop
  1459  					}
  1460  					if e.Type == event.ActionGroupType &&
  1461  						e.ActionGroupEvent.Status == event.Finished {
  1462  						// Send events after the first apply/prune task ends
  1463  						if e.ActionGroupEvent.Action == event.ApplyAction ||
  1464  							e.ActionGroupEvent.Action == event.PruneAction {
  1465  							once.Do(func() {
  1466  								// start events
  1467  								statusWatcher.Start()
  1468  							})
  1469  						}
  1470  					}
  1471  					events = append(events, e)
  1472  				}
  1473  			}
  1474  
  1475  			// Convert events to test events for comparison
  1476  			receivedEvents := testutil.EventsToExpEvents(events)
  1477  
  1478  			// Validate & remove expected status events
  1479  			for _, e := range tc.expectedStatusEvents {
  1480  				var removed int
  1481  				receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e)
  1482  				if removed < 1 {
  1483  					t.Errorf("Expected status event not received: %#v", e.StatusEvent)
  1484  				}
  1485  			}
  1486  
  1487  			// sort to allow comparison of multiple apply/prune tasks in the same task group
  1488  			testutil.SortExpEvents(receivedEvents)
  1489  
  1490  			// Validate the rest of the events
  1491  			testutil.AssertEqual(t, tc.expectedEvents, receivedEvents,
  1492  				"Actual events (%d) do not match expected events (%d)",
  1493  				len(receivedEvents), len(tc.expectedEvents))
  1494  
  1495  			// Validate that the expected timeout was the cause of the run completion.
  1496  			// just in case something else cancelled the run
  1497  			switch {
  1498  			case tc.expectRunTimeout:
  1499  				assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected context timeout")
  1500  			case tc.expectTestTimeout:
  1501  				assert.Equal(t, context.Canceled, runCtx.Err(), "Applier.Run exited, but not because of expected context cancellation")
  1502  			default:
  1503  				assert.Nil(t, runCtx.Err(), "Applier.Run exited, but context error is not nil")
  1504  			}
  1505  		})
  1506  	}
  1507  }
  1508  
  1509  func TestApplierCancel(t *testing.T) {
  1510  	testCases := map[string]struct {
  1511  		// resources input to applier
  1512  		resources object.UnstructuredSet
  1513  		// inventory input to applier
  1514  		invInfo inventoryInfo
  1515  		// objects in the cluster
  1516  		clusterObjs object.UnstructuredSet
  1517  		// options input to applier.Run
  1518  		options ApplierOptions
  1519  		// timeout for applier.Run
  1520  		runTimeout time.Duration
  1521  		// timeout for the test
  1522  		testTimeout time.Duration
  1523  		// fake input events from the statusWatcher
  1524  		statusEvents []pollevent.Event
  1525  		// expected output status events (async)
  1526  		expectedStatusEvents []testutil.ExpEvent
  1527  		// expected output events
  1528  		expectedEvents []testutil.ExpEvent
  1529  		// true if runTimeout is expected to have caused cancellation
  1530  		expectRunTimeout bool
  1531  	}{
  1532  		"cancelled by caller while waiting for reconcile": {
  1533  			expectRunTimeout: true,
  1534  			runTimeout:       2 * time.Second,
  1535  			testTimeout:      30 * time.Second,
  1536  			resources: object.UnstructuredSet{
  1537  				testutil.Unstructured(t, resources["deployment"]),
  1538  			},
  1539  			invInfo: inventoryInfo{
  1540  				name:      "abc-123",
  1541  				namespace: "test",
  1542  				id:        "test",
  1543  			},
  1544  			clusterObjs: object.UnstructuredSet{},
  1545  			options: ApplierOptions{
  1546  				// EmitStatusEvents required to test event output
  1547  				EmitStatusEvents: true,
  1548  				NoPrune:          true,
  1549  				InventoryPolicy:  inventory.PolicyMustMatch,
  1550  				// ReconcileTimeout required to enable WaitTasks
  1551  				ReconcileTimeout: 1 * time.Minute,
  1552  			},
  1553  			statusEvents: []pollevent.Event{
  1554  				{
  1555  					Type: pollevent.ResourceUpdateEvent,
  1556  					Resource: &pollevent.ResourceStatus{
  1557  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1558  						Status:     status.InProgressStatus,
  1559  						Resource:   testutil.Unstructured(t, resources["deployment"]),
  1560  					},
  1561  				},
  1562  				{
  1563  					Type: pollevent.ResourceUpdateEvent,
  1564  					Resource: &pollevent.ResourceStatus{
  1565  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1566  						Status:     status.InProgressStatus,
  1567  						Resource:   testutil.Unstructured(t, resources["deployment"]),
  1568  					},
  1569  				},
  1570  				// Resource never becomes Current, blocking applier.Run from exiting
  1571  			},
  1572  			expectedStatusEvents: []testutil.ExpEvent{
  1573  				{
  1574  					EventType: event.StatusType,
  1575  					StatusEvent: &testutil.ExpStatusEvent{
  1576  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1577  						Status:     status.InProgressStatus,
  1578  					},
  1579  				},
  1580  			},
  1581  			expectedEvents: []testutil.ExpEvent{
  1582  				{
  1583  					// InitTask
  1584  					EventType: event.InitType,
  1585  					InitEvent: &testutil.ExpInitEvent{},
  1586  				},
  1587  				{
  1588  					// InvAddTask start
  1589  					EventType: event.ActionGroupType,
  1590  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1591  						Action:    event.InventoryAction,
  1592  						GroupName: "inventory-add-0",
  1593  						Type:      event.Started,
  1594  					},
  1595  				},
  1596  				{
  1597  					// InvAddTask finished
  1598  					EventType: event.ActionGroupType,
  1599  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1600  						Action:    event.InventoryAction,
  1601  						GroupName: "inventory-add-0",
  1602  						Type:      event.Finished,
  1603  					},
  1604  				},
  1605  				{
  1606  					// ApplyTask start
  1607  					EventType: event.ActionGroupType,
  1608  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1609  						Action:    event.ApplyAction,
  1610  						GroupName: "apply-0",
  1611  						Type:      event.Started,
  1612  					},
  1613  				},
  1614  				{
  1615  					// Apply Deployment
  1616  					EventType: event.ApplyType,
  1617  					ApplyEvent: &testutil.ExpApplyEvent{
  1618  						GroupName:  "apply-0",
  1619  						Status:     event.ApplySuccessful,
  1620  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1621  					},
  1622  				},
  1623  				{
  1624  					// ApplyTask finished
  1625  					EventType: event.ActionGroupType,
  1626  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1627  						Action:    event.ApplyAction,
  1628  						GroupName: "apply-0",
  1629  						Type:      event.Finished,
  1630  					},
  1631  				},
  1632  				{
  1633  					// WaitTask start
  1634  					EventType: event.ActionGroupType,
  1635  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1636  						Action:    event.WaitAction,
  1637  						GroupName: "wait-0",
  1638  						Type:      event.Started,
  1639  					},
  1640  				},
  1641  				{
  1642  					// Deployment reconcile pending.
  1643  					EventType: event.WaitType,
  1644  					WaitEvent: &testutil.ExpWaitEvent{
  1645  						GroupName:  "wait-0",
  1646  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1647  						Status:     event.ReconcilePending,
  1648  					},
  1649  				},
  1650  				// Deployment never becomes Current.
  1651  				// WaitTask is expected to be cancelled before ReconcileTimeout.
  1652  				// Cancelled WaitTask do not sent individual timeout WaitEvents
  1653  				{
  1654  					// WaitTask finished
  1655  					EventType: event.ActionGroupType,
  1656  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1657  						Action:    event.WaitAction,
  1658  						GroupName: "wait-0",
  1659  						Type:      event.Finished, // TODO: add Cancelled event type
  1660  					},
  1661  				},
  1662  				// TODO: Update the inventory after cancellation
  1663  				// {
  1664  				// 	// InvSetTask start
  1665  				// 	EventType: event.ActionGroupType,
  1666  				// 	ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1667  				// 		Action:    event.InventoryAction,
  1668  				// 		GroupName: "inventory-set-0",
  1669  				// 		Type:      event.Started,
  1670  				// 	},
  1671  				// },
  1672  				// {
  1673  				// 	// InvSetTask finished
  1674  				// 	EventType: event.ActionGroupType,
  1675  				// 	ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1676  				// 		Action:    event.InventoryAction,
  1677  				// 		GroupName: "inventory-set-0",
  1678  				// 		Type:      event.Finished,
  1679  				// 	},
  1680  				// },
  1681  				{
  1682  					// Error
  1683  					EventType: event.ErrorType,
  1684  					ErrorEvent: &testutil.ExpErrorEvent{
  1685  						Err: context.DeadlineExceeded,
  1686  					},
  1687  				},
  1688  			},
  1689  		},
  1690  		"completed with timeout": {
  1691  			expectRunTimeout: false,
  1692  			runTimeout:       10 * time.Second,
  1693  			testTimeout:      30 * time.Second,
  1694  			resources: object.UnstructuredSet{
  1695  				testutil.Unstructured(t, resources["deployment"]),
  1696  			},
  1697  			invInfo: inventoryInfo{
  1698  				name:      "abc-123",
  1699  				namespace: "test",
  1700  				id:        "test",
  1701  			},
  1702  			clusterObjs: object.UnstructuredSet{},
  1703  			options: ApplierOptions{
  1704  				// EmitStatusEvents required to test event output
  1705  				EmitStatusEvents: true,
  1706  				NoPrune:          true,
  1707  				InventoryPolicy:  inventory.PolicyMustMatch,
  1708  				// ReconcileTimeout required to enable WaitTasks
  1709  				ReconcileTimeout: 1 * time.Minute,
  1710  			},
  1711  			statusEvents: []pollevent.Event{
  1712  				{
  1713  					Type: pollevent.ResourceUpdateEvent,
  1714  					Resource: &pollevent.ResourceStatus{
  1715  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1716  						Status:     status.InProgressStatus,
  1717  						Resource:   testutil.Unstructured(t, resources["deployment"]),
  1718  					},
  1719  				},
  1720  				{
  1721  					Type: pollevent.ResourceUpdateEvent,
  1722  					Resource: &pollevent.ResourceStatus{
  1723  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1724  						Status:     status.CurrentStatus,
  1725  						Resource:   testutil.Unstructured(t, resources["deployment"]),
  1726  					},
  1727  				},
  1728  				// Resource becoming Current should unblock applier.Run WaitTask
  1729  			},
  1730  			expectedStatusEvents: []testutil.ExpEvent{
  1731  				{
  1732  					EventType: event.StatusType,
  1733  					StatusEvent: &testutil.ExpStatusEvent{
  1734  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1735  						Status:     status.InProgressStatus,
  1736  					},
  1737  				},
  1738  				{
  1739  					EventType: event.StatusType,
  1740  					StatusEvent: &testutil.ExpStatusEvent{
  1741  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1742  						Status:     status.CurrentStatus,
  1743  					},
  1744  				},
  1745  			},
  1746  			expectedEvents: []testutil.ExpEvent{
  1747  				{
  1748  					// InitTask
  1749  					EventType: event.InitType,
  1750  					InitEvent: &testutil.ExpInitEvent{},
  1751  				},
  1752  				{
  1753  					// InvAddTask start
  1754  					EventType: event.ActionGroupType,
  1755  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1756  						Action:    event.InventoryAction,
  1757  						GroupName: "inventory-add-0",
  1758  						Type:      event.Started,
  1759  					},
  1760  				},
  1761  				{
  1762  					// InvAddTask finished
  1763  					EventType: event.ActionGroupType,
  1764  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1765  						Action:    event.InventoryAction,
  1766  						GroupName: "inventory-add-0",
  1767  						Type:      event.Finished,
  1768  					},
  1769  				},
  1770  				{
  1771  					// ApplyTask start
  1772  					EventType: event.ActionGroupType,
  1773  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1774  						Action:    event.ApplyAction,
  1775  						GroupName: "apply-0",
  1776  						Type:      event.Started,
  1777  					},
  1778  				},
  1779  				{
  1780  					// Apply Deployment
  1781  					EventType: event.ApplyType,
  1782  					ApplyEvent: &testutil.ExpApplyEvent{
  1783  						GroupName:  "apply-0",
  1784  						Status:     event.ApplySuccessful,
  1785  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1786  					},
  1787  				},
  1788  				{
  1789  					// ApplyTask finished
  1790  					EventType: event.ActionGroupType,
  1791  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1792  						Action:    event.ApplyAction,
  1793  						GroupName: "apply-0",
  1794  						Type:      event.Finished,
  1795  					},
  1796  				},
  1797  				{
  1798  					// WaitTask start
  1799  					EventType: event.ActionGroupType,
  1800  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1801  						Action:    event.WaitAction,
  1802  						GroupName: "wait-0",
  1803  						Type:      event.Started,
  1804  					},
  1805  				},
  1806  				// Wait events sorted Pending > Successful (see pkg/testutil)
  1807  				{
  1808  					// Deployment reconcile pending.
  1809  					EventType: event.WaitType,
  1810  					WaitEvent: &testutil.ExpWaitEvent{
  1811  						GroupName:  "wait-0",
  1812  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1813  						Status:     event.ReconcilePending,
  1814  					},
  1815  				},
  1816  				{
  1817  					// Deployment becomes Current.
  1818  					EventType: event.WaitType,
  1819  					WaitEvent: &testutil.ExpWaitEvent{
  1820  						GroupName:  "wait-0",
  1821  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
  1822  						Status:     event.ReconcileSuccessful,
  1823  					},
  1824  				},
  1825  				{
  1826  					// WaitTask finished
  1827  					EventType: event.ActionGroupType,
  1828  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1829  						Action:    event.WaitAction,
  1830  						GroupName: "wait-0",
  1831  						Type:      event.Finished,
  1832  					},
  1833  				},
  1834  				{
  1835  					// InvSetTask start
  1836  					EventType: event.ActionGroupType,
  1837  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1838  						Action:    event.InventoryAction,
  1839  						GroupName: "inventory-set-0",
  1840  						Type:      event.Started,
  1841  					},
  1842  				},
  1843  				{
  1844  					// InvSetTask finished
  1845  					EventType: event.ActionGroupType,
  1846  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
  1847  						Action:    event.InventoryAction,
  1848  						GroupName: "inventory-set-0",
  1849  						Type:      event.Finished,
  1850  					},
  1851  				},
  1852  			},
  1853  		},
  1854  	}
  1855  
  1856  	for tn, tc := range testCases {
  1857  		t.Run(tn, func(t *testing.T) {
  1858  			statusWatcher := newFakeWatcher(tc.statusEvents)
  1859  
  1860  			applier := newTestApplier(t,
  1861  				tc.invInfo,
  1862  				tc.resources,
  1863  				tc.clusterObjs,
  1864  				statusWatcher,
  1865  			)
  1866  
  1867  			// Context for Applier.Run
  1868  			runCtx, runCancel := context.WithTimeout(context.Background(), tc.runTimeout)
  1869  			defer runCancel() // cleanup
  1870  
  1871  			// Context for this test (in case Applier.Run never closes the event channel)
  1872  			testCtx, testCancel := context.WithTimeout(context.Background(), tc.testTimeout)
  1873  			defer testCancel() // cleanup
  1874  
  1875  			eventChannel := applier.Run(runCtx, tc.invInfo.toWrapped(), tc.resources, tc.options)
  1876  
  1877  			// only start sending events once
  1878  			var once sync.Once
  1879  
  1880  			var events []event.Event
  1881  
  1882  		loop:
  1883  			for {
  1884  				select {
  1885  				case <-testCtx.Done():
  1886  					// Test timed out
  1887  					runCancel()
  1888  					t.Errorf("Applier.Run failed to respond to cancellation (expected: %s, timeout: %s)", tc.runTimeout, tc.testTimeout)
  1889  					break loop
  1890  
  1891  				case e, ok := <-eventChannel:
  1892  					if !ok {
  1893  						// Event channel closed
  1894  						testCancel()
  1895  						break loop
  1896  					}
  1897  					events = append(events, e)
  1898  
  1899  					if e.Type == event.ActionGroupType &&
  1900  						e.ActionGroupEvent.Status == event.Finished {
  1901  						// Send events after the first apply/prune task ends
  1902  						if e.ActionGroupEvent.Action == event.ApplyAction ||
  1903  							e.ActionGroupEvent.Action == event.PruneAction {
  1904  							once.Do(func() {
  1905  								// start events
  1906  								statusWatcher.Start()
  1907  							})
  1908  						}
  1909  					}
  1910  				}
  1911  			}
  1912  
  1913  			// Convert events to test events for comparison
  1914  			receivedEvents := testutil.EventsToExpEvents(events)
  1915  
  1916  			// Validate & remove expected status events
  1917  			for _, e := range tc.expectedStatusEvents {
  1918  				var removed int
  1919  				receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e)
  1920  				if removed < 1 {
  1921  					t.Errorf("Expected status event not received: %#v", e.StatusEvent)
  1922  				}
  1923  			}
  1924  
  1925  			// sort to allow comparison of multiple wait events
  1926  			testutil.SortExpEvents(receivedEvents)
  1927  
  1928  			// Validate the rest of the events
  1929  			testutil.AssertEqual(t, tc.expectedEvents, receivedEvents,
  1930  				"Actual events (%d) do not match expected events (%d)",
  1931  				len(receivedEvents), len(tc.expectedEvents))
  1932  
  1933  			// Validate that the expected timeout was the cause of the run completion.
  1934  			// just in case something else cancelled the run
  1935  			if tc.expectRunTimeout {
  1936  				assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Applier.Run exited, but not by expected timeout")
  1937  			} else {
  1938  				assert.NoError(t, runCtx.Err(), "Applier.Run exited, but not by expected timeout")
  1939  			}
  1940  		})
  1941  	}
  1942  }
  1943  
  1944  func TestReadAndPrepareObjectsNilInv(t *testing.T) {
  1945  	applier := Applier{}
  1946  	_, _, err := applier.prepareObjects(nil, object.UnstructuredSet{}, ApplierOptions{})
  1947  	assert.Error(t, err)
  1948  }
  1949  
  1950  func TestReadAndPrepareObjects(t *testing.T) {
  1951  	inventoryObj := testutil.Unstructured(t, resources["inventory"])
  1952  	inventory := inventory.WrapInventoryInfoObj(inventoryObj)
  1953  
  1954  	obj1 := testutil.Unstructured(t, resources["obj1"])
  1955  	obj2 := testutil.Unstructured(t, resources["obj2"])
  1956  	clusterScopedObj := testutil.Unstructured(t, resources["clusterScopedObj"])
  1957  
  1958  	testCases := map[string]struct {
  1959  		// objects in the cluster
  1960  		clusterObjs object.UnstructuredSet
  1961  		// inventory input to applier
  1962  		invInfo inventoryInfo
  1963  		// resources input to applier
  1964  		resources object.UnstructuredSet
  1965  		// expected objects to apply
  1966  		applyObjs object.UnstructuredSet
  1967  		// expected objects to prune
  1968  		pruneObjs object.UnstructuredSet
  1969  		// expected error
  1970  		isError bool
  1971  	}{
  1972  		"objects include inventory": {
  1973  			invInfo: inventoryInfo{
  1974  				name:      inventory.Name(),
  1975  				namespace: inventory.Namespace(),
  1976  				id:        inventory.ID(),
  1977  			},
  1978  			resources: object.UnstructuredSet{inventoryObj},
  1979  			isError:   true,
  1980  		},
  1981  		"empty inventory, empty objects, apply none, prune none": {
  1982  			invInfo: inventoryInfo{
  1983  				name:      inventory.Name(),
  1984  				namespace: inventory.Namespace(),
  1985  				id:        inventory.ID(),
  1986  			},
  1987  		},
  1988  		"one in inventory, empty objects, prune one": {
  1989  			clusterObjs: object.UnstructuredSet{obj1},
  1990  			invInfo: inventoryInfo{
  1991  				name:      inventory.Name(),
  1992  				namespace: inventory.Namespace(),
  1993  				id:        inventory.ID(),
  1994  				set: object.ObjMetadataSet{
  1995  					object.UnstructuredToObjMetadata(obj1),
  1996  				},
  1997  			},
  1998  			pruneObjs: object.UnstructuredSet{obj1},
  1999  		},
  2000  		"all in inventory, apply all": {
  2001  			invInfo: inventoryInfo{
  2002  				name:      inventory.Name(),
  2003  				namespace: inventory.Namespace(),
  2004  				id:        inventory.ID(),
  2005  				set: object.ObjMetadataSet{
  2006  					object.UnstructuredToObjMetadata(obj1),
  2007  					object.UnstructuredToObjMetadata(clusterScopedObj),
  2008  				},
  2009  			},
  2010  			resources: object.UnstructuredSet{obj1, clusterScopedObj},
  2011  			applyObjs: object.UnstructuredSet{obj1, clusterScopedObj},
  2012  		},
  2013  		"disjoint set, apply new, prune old": {
  2014  			clusterObjs: object.UnstructuredSet{obj2},
  2015  			invInfo: inventoryInfo{
  2016  				name:      inventory.Name(),
  2017  				namespace: inventory.Namespace(),
  2018  				id:        inventory.ID(),
  2019  				set: object.ObjMetadataSet{
  2020  					object.UnstructuredToObjMetadata(obj2),
  2021  				},
  2022  			},
  2023  			resources: object.UnstructuredSet{obj1, clusterScopedObj},
  2024  			applyObjs: object.UnstructuredSet{obj1, clusterScopedObj},
  2025  			pruneObjs: object.UnstructuredSet{obj2},
  2026  		},
  2027  		"most in inventory, apply all": {
  2028  			clusterObjs: object.UnstructuredSet{obj2},
  2029  			invInfo: inventoryInfo{
  2030  				name:      inventory.Name(),
  2031  				namespace: inventory.Namespace(),
  2032  				id:        inventory.ID(),
  2033  				set: object.ObjMetadataSet{
  2034  					object.UnstructuredToObjMetadata(obj2),
  2035  				},
  2036  			},
  2037  			resources: object.UnstructuredSet{obj1, obj2, clusterScopedObj},
  2038  			applyObjs: object.UnstructuredSet{obj1, obj2, clusterScopedObj},
  2039  			pruneObjs: object.UnstructuredSet{},
  2040  		},
  2041  	}
  2042  
  2043  	for name, tc := range testCases {
  2044  		t.Run(name, func(t *testing.T) {
  2045  			applier := newTestApplier(t,
  2046  				tc.invInfo,
  2047  				tc.resources,
  2048  				tc.clusterObjs,
  2049  				// no events needed for prepareObjects
  2050  				watcher.BlindStatusWatcher{},
  2051  			)
  2052  
  2053  			applyObjs, pruneObjs, err := applier.prepareObjects(tc.invInfo.toWrapped(), tc.resources, ApplierOptions{})
  2054  			if tc.isError {
  2055  				assert.Error(t, err)
  2056  				return
  2057  			}
  2058  			require.NoError(t, err)
  2059  
  2060  			testutil.AssertEqual(t, applyObjs, tc.applyObjs,
  2061  				"Actual applied objects (%d) do not match expected applied objects (%d)",
  2062  				len(applyObjs), len(tc.applyObjs))
  2063  
  2064  			testutil.AssertEqual(t, pruneObjs, tc.pruneObjs,
  2065  				"Actual pruned objects (%d) do not match expected pruned objects (%d)",
  2066  				len(pruneObjs), len(tc.pruneObjs))
  2067  		})
  2068  	}
  2069  }
  2070  

View as plain text