...

Source file src/sigs.k8s.io/cli-utils/pkg/apply/destroyer_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  	"sigs.k8s.io/cli-utils/pkg/apply/event"
    14  	"sigs.k8s.io/cli-utils/pkg/inventory"
    15  	pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
    16  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    17  	"sigs.k8s.io/cli-utils/pkg/object"
    18  	"sigs.k8s.io/cli-utils/pkg/testutil"
    19  )
    20  
    21  func TestDestroyerCancel(t *testing.T) {
    22  	testCases := map[string]struct {
    23  		// inventory input to destroyer
    24  		invInfo inventoryInfo
    25  		// objects in the cluster
    26  		clusterObjs object.UnstructuredSet
    27  		// options input to destroyer.Run
    28  		options DestroyerOptions
    29  		// timeout for destroyer.Run
    30  		runTimeout time.Duration
    31  		// timeout for the test
    32  		testTimeout time.Duration
    33  		// fake input events from the status poller
    34  		statusEvents []pollevent.Event
    35  		// expected output status events (async)
    36  		expectedStatusEvents []testutil.ExpEvent
    37  		// expected output events
    38  		expectedEvents []testutil.ExpEvent
    39  		// true if runTimeout is expected to have caused cancellation
    40  		expectRunTimeout bool
    41  	}{
    42  		"cancelled by caller while waiting for deletion": {
    43  			expectRunTimeout: true,
    44  			runTimeout:       2 * time.Second,
    45  			testTimeout:      30 * time.Second,
    46  			invInfo: inventoryInfo{
    47  				name:      "abc-123",
    48  				namespace: "test",
    49  				id:        "test",
    50  				set: object.ObjMetadataSet{
    51  					testutil.ToIdentifier(t, resources["deployment"]),
    52  				},
    53  			},
    54  			clusterObjs: object.UnstructuredSet{
    55  				testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
    56  			},
    57  			options: DestroyerOptions{
    58  				EmitStatusEvents: true,
    59  				// DeleteTimeout needs to block long enough to cancel the run,
    60  				// otherwise the WaitTask is skipped.
    61  				DeleteTimeout: 1 * time.Minute,
    62  			},
    63  			statusEvents: []pollevent.Event{
    64  				{
    65  					Type: pollevent.ResourceUpdateEvent,
    66  					Resource: &pollevent.ResourceStatus{
    67  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
    68  						Status:     status.InProgressStatus,
    69  						Resource:   testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
    70  					},
    71  				},
    72  				{
    73  					Type: pollevent.ResourceUpdateEvent,
    74  					Resource: &pollevent.ResourceStatus{
    75  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
    76  						Status:     status.InProgressStatus,
    77  						Resource:   testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
    78  					},
    79  				},
    80  				// Resource never becomes NotFound, blocking destroyer.Run from exiting
    81  			},
    82  			expectedStatusEvents: []testutil.ExpEvent{
    83  				{
    84  					EventType: event.StatusType,
    85  					StatusEvent: &testutil.ExpStatusEvent{
    86  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
    87  						Status:     status.InProgressStatus,
    88  					},
    89  				},
    90  			},
    91  			expectedEvents: []testutil.ExpEvent{
    92  				{
    93  					// InitTask
    94  					EventType: event.InitType,
    95  					InitEvent: &testutil.ExpInitEvent{},
    96  				},
    97  				{
    98  					// PruneTask start
    99  					EventType: event.ActionGroupType,
   100  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   101  						Action:    event.DeleteAction,
   102  						GroupName: "prune-0",
   103  						Type:      event.Started,
   104  					},
   105  				},
   106  				{
   107  					// Delete Deployment
   108  					EventType: event.DeleteType,
   109  					DeleteEvent: &testutil.ExpDeleteEvent{
   110  						GroupName:  "prune-0",
   111  						Status:     event.DeleteSuccessful,
   112  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   113  						Error:      nil,
   114  					},
   115  				},
   116  				{
   117  					// PruneTask finished
   118  					EventType: event.ActionGroupType,
   119  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   120  						Action:    event.DeleteAction,
   121  						GroupName: "prune-0",
   122  						Type:      event.Finished,
   123  					},
   124  				},
   125  				{
   126  					// WaitTask start
   127  					EventType: event.ActionGroupType,
   128  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   129  						Action:    event.WaitAction,
   130  						GroupName: "wait-0",
   131  						Type:      event.Started,
   132  					},
   133  				},
   134  				{
   135  					// Deployment reconcile pending.
   136  					EventType: event.WaitType,
   137  					WaitEvent: &testutil.ExpWaitEvent{
   138  						GroupName:  "wait-0",
   139  						Status:     event.ReconcilePending,
   140  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   141  					},
   142  				},
   143  				// Deployment never becomes NotFound.
   144  				// WaitTask is expected to be cancelled before DeleteTimeout.
   145  				{
   146  					// WaitTask finished
   147  					EventType: event.ActionGroupType,
   148  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   149  						Action:    event.WaitAction,
   150  						GroupName: "wait-0",
   151  						Type:      event.Finished, // TODO: add Cancelled event type
   152  					},
   153  				},
   154  				// Inventory cannot be deleted, because the objects still exist,
   155  				// even tho they've been deleted (ex: blocked by finalizer).
   156  				{
   157  					// Error
   158  					EventType: event.ErrorType,
   159  					ErrorEvent: &testutil.ExpErrorEvent{
   160  						Err: context.DeadlineExceeded,
   161  					},
   162  				},
   163  			},
   164  		},
   165  		"completed with timeout": {
   166  			expectRunTimeout: false,
   167  			runTimeout:       10 * time.Second,
   168  			testTimeout:      30 * time.Second,
   169  			invInfo: inventoryInfo{
   170  				name:      "abc-123",
   171  				namespace: "test",
   172  				id:        "test",
   173  				set: object.ObjMetadataSet{
   174  					testutil.ToIdentifier(t, resources["deployment"]),
   175  				},
   176  			},
   177  			clusterObjs: object.UnstructuredSet{
   178  				testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
   179  			},
   180  			options: DestroyerOptions{
   181  				EmitStatusEvents: true,
   182  				// DeleteTimeout needs to block long enough for completion
   183  				DeleteTimeout: 1 * time.Minute,
   184  			},
   185  			statusEvents: []pollevent.Event{
   186  				{
   187  					Type: pollevent.ResourceUpdateEvent,
   188  					Resource: &pollevent.ResourceStatus{
   189  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   190  						Status:     status.InProgressStatus,
   191  						Resource:   testutil.Unstructured(t, resources["deployment"], testutil.AddOwningInv(t, "test")),
   192  					},
   193  				},
   194  				{
   195  					Type: pollevent.ResourceUpdateEvent,
   196  					Resource: &pollevent.ResourceStatus{
   197  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   198  						Status:     status.NotFoundStatus,
   199  					},
   200  				},
   201  				// Resource becoming NotFound should unblock destroyer.Run WaitTask
   202  			},
   203  			expectedStatusEvents: []testutil.ExpEvent{
   204  				{
   205  					EventType: event.StatusType,
   206  					StatusEvent: &testutil.ExpStatusEvent{
   207  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   208  						Status:     status.InProgressStatus,
   209  					},
   210  				},
   211  				{
   212  					EventType: event.StatusType,
   213  					StatusEvent: &testutil.ExpStatusEvent{
   214  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   215  						Status:     status.NotFoundStatus,
   216  					},
   217  				},
   218  			},
   219  			expectedEvents: []testutil.ExpEvent{
   220  				{
   221  					// InitTask
   222  					EventType: event.InitType,
   223  					InitEvent: &testutil.ExpInitEvent{},
   224  				},
   225  				{
   226  					// PruneTask start
   227  					EventType: event.ActionGroupType,
   228  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   229  						Action:    event.DeleteAction,
   230  						GroupName: "prune-0",
   231  						Type:      event.Started,
   232  					},
   233  				},
   234  				{
   235  					// Delete Deployment
   236  					EventType: event.DeleteType,
   237  					DeleteEvent: &testutil.ExpDeleteEvent{
   238  						GroupName:  "prune-0",
   239  						Status:     event.DeleteSuccessful,
   240  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   241  						Error:      nil,
   242  					},
   243  				},
   244  				{
   245  					// PruneTask finished
   246  					EventType: event.ActionGroupType,
   247  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   248  						Action:    event.DeleteAction,
   249  						GroupName: "prune-0",
   250  						Type:      event.Finished,
   251  					},
   252  				},
   253  				{
   254  					// WaitTask start
   255  					EventType: event.ActionGroupType,
   256  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   257  						Action:    event.WaitAction,
   258  						GroupName: "wait-0",
   259  						Type:      event.Started,
   260  					},
   261  				},
   262  				// Wait events sorted Pending > Successful (see pkg/testutil)
   263  				{
   264  					// Deployment reconcile pending.
   265  					EventType: event.WaitType,
   266  					WaitEvent: &testutil.ExpWaitEvent{
   267  						GroupName:  "wait-0",
   268  						Status:     event.ReconcilePending,
   269  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   270  					},
   271  				},
   272  				{
   273  					// Deployment confirmed NotFound.
   274  					EventType: event.WaitType,
   275  					WaitEvent: &testutil.ExpWaitEvent{
   276  						GroupName:  "wait-0",
   277  						Status:     event.ReconcileSuccessful,
   278  						Identifier: testutil.ToIdentifier(t, resources["deployment"]),
   279  					},
   280  				},
   281  				{
   282  					// WaitTask finished
   283  					EventType: event.ActionGroupType,
   284  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   285  						Action:    event.WaitAction,
   286  						GroupName: "wait-0",
   287  						Type:      event.Finished,
   288  					},
   289  				},
   290  				{
   291  					// DeleteInvTask start
   292  					EventType: event.ActionGroupType,
   293  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   294  						Action:    event.InventoryAction,
   295  						GroupName: "delete-inventory-0",
   296  						Type:      event.Started,
   297  					},
   298  				},
   299  				{
   300  					// DeleteInvTask finished
   301  					EventType: event.ActionGroupType,
   302  					ActionGroupEvent: &testutil.ExpActionGroupEvent{
   303  						Action:    event.InventoryAction,
   304  						GroupName: "delete-inventory-0",
   305  						Type:      event.Finished,
   306  					},
   307  				},
   308  			},
   309  		},
   310  	}
   311  
   312  	for tn, tc := range testCases {
   313  		t.Run(tn, func(t *testing.T) {
   314  			statusWatcher := newFakeWatcher(tc.statusEvents)
   315  
   316  			invInfo := tc.invInfo.toWrapped()
   317  
   318  			destroyer := newTestDestroyer(t,
   319  				tc.invInfo,
   320  				// Add the inventory to the cluster (to allow deletion)
   321  				append(tc.clusterObjs, inventory.InvInfoToConfigMap(invInfo)),
   322  				statusWatcher,
   323  			)
   324  
   325  			// Context for Destroyer.Run
   326  			runCtx, runCancel := context.WithTimeout(context.Background(), tc.runTimeout)
   327  			defer runCancel() // cleanup
   328  
   329  			// Context for this test (in case Destroyer.Run never closes the event channel)
   330  			testCtx, testCancel := context.WithTimeout(context.Background(), tc.testTimeout)
   331  			defer testCancel() // cleanup
   332  
   333  			eventChannel := destroyer.Run(runCtx, invInfo, tc.options)
   334  
   335  			// only start poller once per run
   336  			var once sync.Once
   337  			var events []event.Event
   338  
   339  		loop:
   340  			for {
   341  				select {
   342  				case <-testCtx.Done():
   343  					// Test timed out
   344  					runCancel()
   345  					t.Errorf("Destroyer.Run failed to respond to cancellation (expected: %s, timeout: %s)", tc.runTimeout, tc.testTimeout)
   346  					break loop
   347  
   348  				case e, ok := <-eventChannel:
   349  					if !ok {
   350  						// Event channel closed
   351  						testCancel()
   352  						break loop
   353  					}
   354  					events = append(events, e)
   355  
   356  					if e.Type == event.ActionGroupType &&
   357  						e.ActionGroupEvent.Action == event.WaitAction {
   358  						once.Do(func() {
   359  							// Start sending status events after waiting starts
   360  							statusWatcher.Start()
   361  						})
   362  					}
   363  				}
   364  			}
   365  
   366  			// Convert events to test events for comparison
   367  			receivedEvents := testutil.EventsToExpEvents(events)
   368  
   369  			// Validate & remove expected status events
   370  			for _, e := range tc.expectedStatusEvents {
   371  				var removed int
   372  				receivedEvents, removed = testutil.RemoveEqualEvents(receivedEvents, e)
   373  				if removed < 1 {
   374  					t.Errorf("Expected status event not received: %#v", e)
   375  				}
   376  			}
   377  
   378  			// sort to allow comparison of multiple wait events
   379  			testutil.SortExpEvents(receivedEvents)
   380  
   381  			// Validate the rest of the events
   382  			testutil.AssertEqual(t, tc.expectedEvents, receivedEvents,
   383  				"Actual events (%d) do not match expected events (%d)",
   384  				len(receivedEvents), len(tc.expectedEvents))
   385  
   386  			// Validate that the expected timeout was the cause of the run completion.
   387  			// just in case something else cancelled the run
   388  			if tc.expectRunTimeout {
   389  				assert.Equal(t, context.DeadlineExceeded, runCtx.Err(), "Destroyer.Run exited, but not by expected timeout")
   390  			} else {
   391  				assert.Nil(t, runCtx.Err(), "Destroyer.Run exited, but not by expected timeout")
   392  			}
   393  		})
   394  	}
   395  }
   396  

View as plain text