...

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

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

     1  // Copyright 2020 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package task
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  	"testing"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	"k8s.io/apimachinery/pkg/types"
    16  	"k8s.io/cli-runtime/pkg/resource"
    17  	"k8s.io/client-go/discovery"
    18  	"k8s.io/client-go/dynamic"
    19  	"sigs.k8s.io/cli-utils/pkg/apply/cache"
    20  	"sigs.k8s.io/cli-utils/pkg/apply/event"
    21  	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
    22  	"sigs.k8s.io/cli-utils/pkg/common"
    23  	"sigs.k8s.io/cli-utils/pkg/object"
    24  	"sigs.k8s.io/cli-utils/pkg/testutil"
    25  )
    26  
    27  type resourceInfo struct {
    28  	group      string
    29  	apiVersion string
    30  	kind       string
    31  	name       string
    32  	namespace  string
    33  	uid        types.UID
    34  	generation int64
    35  }
    36  
    37  // Tests that the correct "applied" objects are sent
    38  // to the TaskContext correctly, since these are the
    39  // applied objects added to the final inventory.
    40  func TestApplyTask_BasicAppliedObjects(t *testing.T) {
    41  	testCases := map[string]struct {
    42  		applied []resourceInfo
    43  	}{
    44  		"apply single namespaced resource": {
    45  			applied: []resourceInfo{
    46  				{
    47  					group:      "apps",
    48  					apiVersion: "apps/v1",
    49  					kind:       "Deployment",
    50  					name:       "foo",
    51  					namespace:  "default",
    52  					uid:        types.UID("my-uid"),
    53  					generation: int64(42),
    54  				},
    55  			},
    56  		},
    57  		"apply multiple clusterscoped resources": {
    58  			applied: []resourceInfo{
    59  				{
    60  					group:      "custom.io",
    61  					apiVersion: "custom.io/v1beta1",
    62  					kind:       "Custom",
    63  					name:       "bar",
    64  					uid:        types.UID("uid-1"),
    65  					generation: int64(32),
    66  				},
    67  				{
    68  					group:      "custom2.io",
    69  					apiVersion: "custom2.io/v1",
    70  					kind:       "Custom2",
    71  					name:       "foo",
    72  					uid:        types.UID("uid-2"),
    73  					generation: int64(1),
    74  				},
    75  			},
    76  		},
    77  	}
    78  
    79  	for tn, tc := range testCases {
    80  		t.Run(tn, func(t *testing.T) {
    81  			eventChannel := make(chan event.Event)
    82  			defer close(eventChannel)
    83  			resourceCache := cache.NewResourceCacheMap()
    84  			taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
    85  
    86  			objs := toUnstructureds(tc.applied)
    87  
    88  			oldAO := applyOptionsFactoryFunc
    89  			applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
    90  				dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
    91  				return &fakeApplyOptions{}
    92  			}
    93  			defer func() { applyOptionsFactoryFunc = oldAO }()
    94  
    95  			restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{
    96  				Group:   "apps",
    97  				Version: "v1",
    98  				Kind:    "Deployment",
    99  			}, schema.GroupVersionKind{
   100  				Group:   "anothercustom.io",
   101  				Version: "v2",
   102  				Kind:    "AnotherCustom",
   103  			})
   104  
   105  			applyTask := &ApplyTask{
   106  				Objects:    objs,
   107  				Mapper:     restMapper,
   108  				InfoHelper: &fakeInfoHelper{},
   109  			}
   110  
   111  			applyTask.Start(taskContext)
   112  			<-taskContext.TaskChannel()
   113  
   114  			// The applied resources should be stored in the TaskContext
   115  			// for the final inventory.
   116  			expectedIDs := object.UnstructuredSetToObjMetadataSet(objs)
   117  			actual := taskContext.InventoryManager().SuccessfulApplies()
   118  			if !actual.Equal(expectedIDs) {
   119  				t.Errorf("expected (%s) inventory resources, got (%s)", expectedIDs, actual)
   120  			}
   121  
   122  			im := taskContext.InventoryManager()
   123  
   124  			for _, id := range expectedIDs {
   125  				assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id)
   126  				assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id)
   127  			}
   128  		})
   129  	}
   130  }
   131  
   132  func TestApplyTask_FetchGeneration(t *testing.T) {
   133  	testCases := map[string]struct {
   134  		rss []resourceInfo
   135  	}{
   136  		"single namespaced resource": {
   137  			rss: []resourceInfo{
   138  				{
   139  					group:      "apps",
   140  					apiVersion: "apps/v1",
   141  					kind:       "Deployment",
   142  					name:       "foo",
   143  					namespace:  "default",
   144  					uid:        types.UID("my-uid"),
   145  					generation: int64(42),
   146  				},
   147  			},
   148  		},
   149  		"multiple clusterscoped resources": {
   150  			rss: []resourceInfo{
   151  				{
   152  					group:      "custom.io",
   153  					apiVersion: "custom.io/v1beta1",
   154  					kind:       "Custom",
   155  					name:       "bar",
   156  					uid:        types.UID("uid-1"),
   157  					generation: int64(32),
   158  				},
   159  				{
   160  					group:      "custom2.io",
   161  					apiVersion: "custom2.io/v1",
   162  					kind:       "Custom2",
   163  					name:       "foo",
   164  					uid:        types.UID("uid-2"),
   165  					generation: int64(1),
   166  				},
   167  			},
   168  		},
   169  	}
   170  
   171  	for tn, tc := range testCases {
   172  		t.Run(tn, func(t *testing.T) {
   173  			eventChannel := make(chan event.Event)
   174  			defer close(eventChannel)
   175  			resourceCache := cache.NewResourceCacheMap()
   176  			taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
   177  
   178  			objs := toUnstructureds(tc.rss)
   179  
   180  			oldAO := applyOptionsFactoryFunc
   181  			applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
   182  				dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
   183  				return &fakeApplyOptions{}
   184  			}
   185  			defer func() { applyOptionsFactoryFunc = oldAO }()
   186  			applyTask := &ApplyTask{
   187  				Objects:    objs,
   188  				InfoHelper: &fakeInfoHelper{},
   189  			}
   190  			applyTask.Start(taskContext)
   191  
   192  			<-taskContext.TaskChannel()
   193  
   194  			for _, info := range tc.rss {
   195  				id := object.ObjMetadata{
   196  					GroupKind: schema.GroupKind{
   197  						Group: info.group,
   198  						Kind:  info.kind,
   199  					},
   200  					Name:      info.name,
   201  					Namespace: info.namespace,
   202  				}
   203  				uid, _ := taskContext.InventoryManager().AppliedResourceUID(id)
   204  				assert.Equal(t, info.uid, uid)
   205  
   206  				gen, _ := taskContext.InventoryManager().AppliedGeneration(id)
   207  				assert.Equal(t, info.generation, gen)
   208  			}
   209  		})
   210  	}
   211  }
   212  
   213  func TestApplyTask_DryRun(t *testing.T) {
   214  	testCases := map[string]struct {
   215  		objs            []*unstructured.Unstructured
   216  		expectedObjects []object.ObjMetadata
   217  		expectedEvents  []event.Event
   218  	}{
   219  		"simple dry run": {
   220  			objs: []*unstructured.Unstructured{
   221  				toUnstructured(map[string]interface{}{
   222  					"apiVersion": "apps/v1",
   223  					"kind":       "Deployment",
   224  					"metadata": map[string]interface{}{
   225  						"name":      "foo",
   226  						"namespace": "default",
   227  					},
   228  				}),
   229  			},
   230  			expectedObjects: []object.ObjMetadata{
   231  				{
   232  					GroupKind: schema.GroupKind{
   233  						Group: "apps",
   234  						Kind:  "Deployment",
   235  					},
   236  					Name:      "foo",
   237  					Namespace: "default",
   238  				},
   239  			},
   240  			expectedEvents: []event.Event{},
   241  		},
   242  		"dry run with CRD and CR": {
   243  			objs: []*unstructured.Unstructured{
   244  				toUnstructured(map[string]interface{}{
   245  					"apiVersion": "apiextensions.k8s.io/v1",
   246  					"kind":       "CustomResourceDefinition",
   247  					"metadata": map[string]interface{}{
   248  						"name": "foo",
   249  					},
   250  					"spec": map[string]interface{}{
   251  						"group": "custom.io",
   252  						"names": map[string]interface{}{
   253  							"kind": "Custom",
   254  						},
   255  						"versions": []interface{}{
   256  							map[string]interface{}{
   257  								"name": "v1alpha1",
   258  							},
   259  						},
   260  					},
   261  				}),
   262  				toUnstructured(map[string]interface{}{
   263  					"apiVersion": "custom.io/v1alpha1",
   264  					"kind":       "Custom",
   265  					"metadata": map[string]interface{}{
   266  						"name": "bar",
   267  					},
   268  				}),
   269  			},
   270  			expectedObjects: []object.ObjMetadata{
   271  				{
   272  					GroupKind: schema.GroupKind{
   273  						Group: "custom.io",
   274  						Kind:  "Custom",
   275  					},
   276  					Name: "bar",
   277  				},
   278  			},
   279  			expectedEvents: []event.Event{},
   280  		},
   281  	}
   282  
   283  	for tn, tc := range testCases {
   284  		for i := range common.Strategies {
   285  			drs := common.Strategies[i]
   286  			t.Run(tn, func(t *testing.T) {
   287  				eventChannel := make(chan event.Event)
   288  				resourceCache := cache.NewResourceCacheMap()
   289  				taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
   290  
   291  				restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{
   292  					Group:   "apps",
   293  					Version: "v1",
   294  					Kind:    "Deployment",
   295  				}, schema.GroupVersionKind{
   296  					Group:   "anothercustom.io",
   297  					Version: "v2",
   298  					Kind:    "AnotherCustom",
   299  				})
   300  
   301  				ao := &fakeApplyOptions{}
   302  				oldAO := applyOptionsFactoryFunc
   303  				applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
   304  					dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
   305  					return ao
   306  				}
   307  				defer func() { applyOptionsFactoryFunc = oldAO }()
   308  
   309  				applyTask := &ApplyTask{
   310  					Objects:        tc.objs,
   311  					InfoHelper:     &fakeInfoHelper{},
   312  					Mapper:         restMapper,
   313  					DryRunStrategy: drs,
   314  				}
   315  
   316  				var events []event.Event
   317  				var wg sync.WaitGroup
   318  				wg.Add(1)
   319  				go func() {
   320  					defer wg.Done()
   321  					for msg := range eventChannel {
   322  						events = append(events, msg)
   323  					}
   324  				}()
   325  
   326  				applyTask.Start(taskContext)
   327  				<-taskContext.TaskChannel()
   328  				close(eventChannel)
   329  				wg.Wait()
   330  
   331  				assert.Equal(t, len(tc.expectedObjects), len(ao.objects))
   332  				for i, obj := range ao.objects {
   333  					actual, err := object.InfoToObjMeta(obj)
   334  					if err != nil {
   335  						continue
   336  					}
   337  					assert.Equal(t, tc.expectedObjects[i], actual)
   338  				}
   339  
   340  				assert.Equal(t, len(tc.expectedEvents), len(events))
   341  				for i, e := range events {
   342  					assert.Equal(t, tc.expectedEvents[i].Type, e.Type)
   343  				}
   344  			})
   345  		}
   346  	}
   347  }
   348  
   349  func TestApplyTaskWithError(t *testing.T) {
   350  	testCases := map[string]struct {
   351  		objs            []*unstructured.Unstructured
   352  		expectedObjects object.ObjMetadataSet
   353  		expectedEvents  []event.Event
   354  		expectedSkipped object.ObjMetadataSet
   355  		expectedFailed  object.ObjMetadataSet
   356  	}{
   357  		"some resources have apply error": {
   358  			objs: []*unstructured.Unstructured{
   359  				toUnstructured(map[string]interface{}{
   360  					"apiVersion": "apiextensions.k8s.io/v1",
   361  					"kind":       "CustomResourceDefinition",
   362  					"metadata": map[string]interface{}{
   363  						"name": "foo",
   364  					},
   365  					"spec": map[string]interface{}{
   366  						"group": "anothercustom.io",
   367  						"names": map[string]interface{}{
   368  							"kind": "AnotherCustom",
   369  						},
   370  						"versions": []interface{}{
   371  							map[string]interface{}{
   372  								"name": "v2",
   373  							},
   374  						},
   375  					},
   376  				}),
   377  				toUnstructured(map[string]interface{}{
   378  					"apiVersion": "anothercustom.io/v2",
   379  					"kind":       "AnotherCustom",
   380  					"metadata": map[string]interface{}{
   381  						"name":      "bar",
   382  						"namespace": "barbar",
   383  					},
   384  				}),
   385  				toUnstructured(map[string]interface{}{
   386  					"apiVersion": "anothercustom.io/v2",
   387  					"kind":       "AnotherCustom",
   388  					"metadata": map[string]interface{}{
   389  						"name":      "bar-with-failure",
   390  						"namespace": "barbar",
   391  					},
   392  				}),
   393  			},
   394  			expectedObjects: object.ObjMetadataSet{
   395  				{
   396  					GroupKind: schema.GroupKind{
   397  						Group: "apiextensions.k8s.io",
   398  						Kind:  "CustomResourceDefinition",
   399  					},
   400  					Name: "foo",
   401  				},
   402  				{
   403  					GroupKind: schema.GroupKind{
   404  						Group: "anothercustom.io",
   405  						Kind:  "AnotherCustom",
   406  					},
   407  					Name:      "bar",
   408  					Namespace: "barbar",
   409  				},
   410  			},
   411  			expectedEvents: []event.Event{
   412  				{
   413  					Type: event.ApplyType,
   414  					ApplyEvent: event.ApplyEvent{
   415  						Status: event.ApplyFailed,
   416  						Error:  fmt.Errorf("expected apply error"),
   417  					},
   418  				},
   419  			},
   420  			expectedFailed: object.ObjMetadataSet{
   421  				{
   422  					GroupKind: schema.GroupKind{
   423  						Group: "anothercustom.io",
   424  						Kind:  "AnotherCustom",
   425  					},
   426  					Name:      "bar-with-failure",
   427  					Namespace: "barbar",
   428  				},
   429  			},
   430  		},
   431  	}
   432  
   433  	for tn, tc := range testCases {
   434  		drs := common.DryRunNone
   435  		t.Run(tn, func(t *testing.T) {
   436  			eventChannel := make(chan event.Event)
   437  			resourceCache := cache.NewResourceCacheMap()
   438  			taskContext := taskrunner.NewTaskContext(eventChannel, resourceCache)
   439  
   440  			restMapper := testutil.NewFakeRESTMapper(schema.GroupVersionKind{
   441  				Group:   "apps",
   442  				Version: "v1",
   443  				Kind:    "Deployment",
   444  			}, schema.GroupVersionKind{
   445  				Group:   "anothercustom.io",
   446  				Version: "v2",
   447  				Kind:    "AnotherCustom",
   448  			})
   449  
   450  			ao := &fakeApplyOptions{}
   451  			oldAO := applyOptionsFactoryFunc
   452  			applyOptionsFactoryFunc = func(string, chan<- event.Event, common.ServerSideOptions, common.DryRunStrategy,
   453  				dynamic.Interface, discovery.OpenAPISchemaInterface) applyOptions {
   454  				return ao
   455  			}
   456  			defer func() { applyOptionsFactoryFunc = oldAO }()
   457  
   458  			applyTask := &ApplyTask{
   459  				Objects:        tc.objs,
   460  				InfoHelper:     &fakeInfoHelper{},
   461  				Mapper:         restMapper,
   462  				DryRunStrategy: drs,
   463  			}
   464  
   465  			var events []event.Event
   466  			var wg sync.WaitGroup
   467  			wg.Add(1)
   468  			go func() {
   469  				defer wg.Done()
   470  				for msg := range eventChannel {
   471  					events = append(events, msg)
   472  				}
   473  			}()
   474  
   475  			applyTask.Start(taskContext)
   476  			<-taskContext.TaskChannel()
   477  			close(eventChannel)
   478  			wg.Wait()
   479  
   480  			assert.Equal(t, len(tc.expectedObjects), len(ao.passedObjects))
   481  			for i, obj := range ao.passedObjects {
   482  				actual, err := object.InfoToObjMeta(obj)
   483  				if err != nil {
   484  					continue
   485  				}
   486  				assert.Equal(t, tc.expectedObjects[i], actual)
   487  			}
   488  
   489  			assert.Equal(t, len(tc.expectedEvents), len(events))
   490  			for i, e := range events {
   491  				assert.Equal(t, tc.expectedEvents[i].Type, e.Type)
   492  				assert.Equal(t, tc.expectedEvents[i].ApplyEvent.Error.Error(), e.ApplyEvent.Error.Error())
   493  			}
   494  
   495  			applyIds := object.UnstructuredSetToObjMetadataSet(tc.objs)
   496  
   497  			im := taskContext.InventoryManager()
   498  
   499  			// validate record of failed prunes
   500  			for _, id := range tc.expectedFailed {
   501  				assert.Truef(t, im.IsFailedApply(id), "ApplyTask should mark object as failed: %s", id)
   502  			}
   503  			for _, id := range applyIds.Diff(tc.expectedFailed) {
   504  				assert.Falsef(t, im.IsFailedApply(id), "ApplyTask should NOT mark object as failed: %s", id)
   505  			}
   506  			// validate record of skipped prunes
   507  			for _, id := range tc.expectedSkipped {
   508  				assert.Truef(t, im.IsSkippedApply(id), "ApplyTask should mark object as skipped: %s", id)
   509  			}
   510  			for _, id := range applyIds.Diff(tc.expectedSkipped) {
   511  				assert.Falsef(t, im.IsSkippedApply(id), "ApplyTask should NOT mark object as skipped: %s", id)
   512  			}
   513  		})
   514  	}
   515  }
   516  
   517  func toUnstructured(obj map[string]interface{}) *unstructured.Unstructured {
   518  	return &unstructured.Unstructured{
   519  		Object: obj,
   520  	}
   521  }
   522  
   523  func toUnstructureds(rss []resourceInfo) []*unstructured.Unstructured {
   524  	var objs []*unstructured.Unstructured
   525  
   526  	for _, rs := range rss {
   527  		objs = append(objs, &unstructured.Unstructured{
   528  			Object: map[string]interface{}{
   529  				"apiVersion": rs.apiVersion,
   530  				"kind":       rs.kind,
   531  				"metadata": map[string]interface{}{
   532  					"name":       rs.name,
   533  					"namespace":  rs.namespace,
   534  					"uid":        string(rs.uid),
   535  					"generation": rs.generation,
   536  					"annotations": map[string]interface{}{
   537  						"config.k8s.io/owning-inventory": "id",
   538  					},
   539  				},
   540  			},
   541  		})
   542  	}
   543  	return objs
   544  }
   545  
   546  type fakeApplyOptions struct {
   547  	objects       []*resource.Info
   548  	passedObjects []*resource.Info
   549  }
   550  
   551  func (f *fakeApplyOptions) Run() error {
   552  	var err error
   553  	for _, obj := range f.objects {
   554  		if strings.Contains(obj.Name, "failure") {
   555  			err = fmt.Errorf("expected apply error")
   556  		} else {
   557  			f.passedObjects = append(f.passedObjects, obj)
   558  		}
   559  	}
   560  	return err
   561  }
   562  
   563  func (f *fakeApplyOptions) SetObjects(objects []*resource.Info) {
   564  	f.objects = objects
   565  }
   566  
   567  type fakeInfoHelper struct{}
   568  
   569  func (f *fakeInfoHelper) UpdateInfo(*resource.Info) error {
   570  	return nil
   571  }
   572  
   573  func (f *fakeInfoHelper) BuildInfos(objs []*unstructured.Unstructured) ([]*resource.Info, error) {
   574  	return object.UnstructuredsToInfos(objs)
   575  }
   576  
   577  func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) {
   578  	return object.UnstructuredToInfo(obj)
   579  }
   580  

View as plain text