github.com/oam-dev/kubevela@v1.9.11/pkg/utils/apply/apply_test.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package apply
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/crossplane/crossplane-runtime/pkg/test"
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/pkg/errors"
    26  	"github.com/stretchr/testify/assert"
    27  	"github.com/stretchr/testify/require"
    28  	appsv1 "k8s.io/api/apps/v1"
    29  	corev1 "k8s.io/api/core/v1"
    30  	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    31  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/types"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  
    39  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    40  	"github.com/oam-dev/kubevela/pkg/oam"
    41  )
    42  
    43  var ctx = context.Background()
    44  var errFake = errors.New("fake error")
    45  
    46  type testObject struct {
    47  	runtime.Object
    48  	metav1.ObjectMeta
    49  }
    50  
    51  func (t *testObject) DeepCopyObject() runtime.Object {
    52  	return &testObject{ObjectMeta: *t.ObjectMeta.DeepCopy()}
    53  }
    54  
    55  func (t *testObject) GetObjectKind() schema.ObjectKind {
    56  	return schema.EmptyObjectKind
    57  }
    58  
    59  type testNoMetaObject struct {
    60  	runtime.Object
    61  }
    62  
    63  func TestAPIApplicator(t *testing.T) {
    64  	existing := &testObject{}
    65  	existing.SetName("existing")
    66  	desired := &testObject{}
    67  	desired.SetName("desired")
    68  	// use Deployment as a registered API sample
    69  	testDeploy := &appsv1.Deployment{}
    70  	testDeploy.SetGroupVersionKind(schema.GroupVersionKind{
    71  		Group:   "apps",
    72  		Version: "v1",
    73  		Kind:    "Deployment",
    74  	})
    75  	type args struct {
    76  		existing   client.Object
    77  		creatorErr error
    78  		patcherErr error
    79  		desired    client.Object
    80  		ao         []ApplyOption
    81  	}
    82  
    83  	cases := map[string]struct {
    84  		reason string
    85  		c      client.Client
    86  		args   args
    87  		want   error
    88  	}{
    89  		"ErrorOccursCreatOrGetExisting": {
    90  			reason: "An error should be returned if cannot create or get existing",
    91  			args: args{
    92  				creatorErr: errFake,
    93  			},
    94  			want: errFake,
    95  		},
    96  		"CreateSuccessfully": {
    97  			reason: "No error should be returned if create successfully",
    98  		},
    99  		"CannotApplyApplyOptions": {
   100  			reason: "An error should be returned if cannot apply ApplyOption",
   101  			args: args{
   102  				existing: existing,
   103  				ao: []ApplyOption{
   104  					func(_ *applyAction, existing, desired client.Object) error {
   105  						return errFake
   106  					},
   107  				},
   108  			},
   109  			want: errors.Wrap(errFake, "cannot apply ApplyOption"),
   110  		},
   111  		"CalculatePatchError": {
   112  			reason: "An error should be returned if patch failed",
   113  			args: args{
   114  				existing:   existing,
   115  				desired:    desired,
   116  				patcherErr: errFake,
   117  			},
   118  			c:    &test.MockClient{MockPatch: test.NewMockPatchFn(errFake)},
   119  			want: errors.Wrap(errFake, "cannot calculate patch by computing a three way diff"),
   120  		},
   121  		"PatchError": {
   122  			reason: "An error should be returned if patch failed",
   123  			args: args{
   124  				existing: existing,
   125  				desired:  testDeploy,
   126  			},
   127  			c:    &test.MockClient{MockPatch: test.NewMockPatchFn(errFake)},
   128  			want: errors.Wrap(errFake, "cannot patch object"),
   129  		},
   130  		"PatchingApplySuccessfully": {
   131  			reason: "No error should be returned if patch successfully",
   132  			args: args{
   133  				existing: existing,
   134  				desired:  desired,
   135  			},
   136  			c: &test.MockClient{MockPatch: test.NewMockPatchFn(nil)},
   137  		},
   138  	}
   139  
   140  	for caseName, tc := range cases {
   141  		t.Run(caseName, func(t *testing.T) {
   142  			a := &APIApplicator{
   143  				creator: creatorFn(func(_ context.Context, _ *applyAction, _ client.Client, _ client.Object, _ ...ApplyOption) (client.Object, error) {
   144  					return tc.args.existing, tc.args.creatorErr
   145  				}),
   146  				patcher: patcherFn(func(c, m client.Object, a *applyAction) (client.Patch, error) {
   147  					return client.RawPatch(types.MergePatchType, []byte(`err`)), tc.args.patcherErr
   148  				}),
   149  				c: tc.c,
   150  			}
   151  			result := a.Apply(ctx, tc.args.desired, tc.args.ao...)
   152  			if diff := cmp.Diff(tc.want, result, test.EquateErrors()); diff != "" {
   153  				t.Errorf("\n%s\nApply(...): -want , +got \n%s\n", tc.reason, diff)
   154  			}
   155  		})
   156  	}
   157  }
   158  
   159  func TestCreator(t *testing.T) {
   160  	desired := &unstructured.Unstructured{}
   161  	desired.SetName("desired")
   162  	type args struct {
   163  		desired client.Object
   164  		ao      []ApplyOption
   165  	}
   166  	type want struct {
   167  		existing client.Object
   168  		err      error
   169  	}
   170  
   171  	cases := map[string]struct {
   172  		reason string
   173  		c      client.Client
   174  		args   args
   175  		want   want
   176  	}{
   177  		"CannotCreateObjectWithoutName": {
   178  			reason: "An error should be returned if cannot create the object",
   179  			args: args{
   180  				desired: &testObject{
   181  					ObjectMeta: metav1.ObjectMeta{
   182  						GenerateName: "prefix",
   183  					},
   184  				},
   185  			},
   186  			c: &test.MockClient{MockCreate: test.NewMockCreateFn(errFake)},
   187  			want: want{
   188  				existing: nil,
   189  				err:      errors.Wrap(errFake, "cannot create object"),
   190  			},
   191  		},
   192  		"CannotCreate": {
   193  			reason: "An error should be returned if cannot create the object",
   194  			c: &test.MockClient{
   195  				MockCreate: test.NewMockCreateFn(errFake),
   196  				MockGet:    test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
   197  			args: args{
   198  				desired: desired,
   199  			},
   200  			want: want{
   201  				existing: nil,
   202  				err:      errors.Wrap(errFake, "cannot create object"),
   203  			},
   204  		},
   205  		"CannotGetExisting": {
   206  			reason: "An error should be returned if cannot get the object",
   207  			c: &test.MockClient{
   208  				MockGet: test.NewMockGetFn(errFake)},
   209  			args: args{
   210  				desired: desired,
   211  			},
   212  			want: want{
   213  				existing: nil,
   214  				err:      errors.Wrap(errFake, "cannot get object"),
   215  			},
   216  		},
   217  		"ApplyOptionErrorWhenCreatObjectWithoutName": {
   218  			reason: "An error should be returned if cannot apply ApplyOption",
   219  			args: args{
   220  				desired: &testObject{
   221  					ObjectMeta: metav1.ObjectMeta{
   222  						GenerateName: "prefix",
   223  					},
   224  				},
   225  				ao: []ApplyOption{
   226  					func(_ *applyAction, existing, desired client.Object) error {
   227  						return errFake
   228  					},
   229  				},
   230  			},
   231  			want: want{
   232  				existing: nil,
   233  				err:      errors.Wrap(errFake, "cannot apply ApplyOption"),
   234  			},
   235  		},
   236  		"ApplyOptionErrorWhenCreatObject": {
   237  			reason: "An error should be returned if cannot apply ApplyOption",
   238  			c:      &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
   239  			args: args{
   240  				desired: desired,
   241  				ao: []ApplyOption{
   242  					func(_ *applyAction, existing, desired client.Object) error {
   243  						return errFake
   244  					},
   245  				},
   246  			},
   247  			want: want{
   248  				existing: nil,
   249  				err:      errors.Wrap(errFake, "cannot apply ApplyOption"),
   250  			},
   251  		},
   252  		"CreateWithoutNameSuccessfully": {
   253  			reason: "No error and existing should be returned if create successfully",
   254  			c:      &test.MockClient{MockCreate: test.NewMockCreateFn(nil)},
   255  			args: args{
   256  				desired: &testObject{
   257  					ObjectMeta: metav1.ObjectMeta{
   258  						GenerateName: "prefix",
   259  					},
   260  				},
   261  			},
   262  			want: want{
   263  				existing: nil,
   264  				err:      nil,
   265  			},
   266  		},
   267  		"CreateSuccessfully": {
   268  			reason: "No error and existing should be returned if create successfully",
   269  			c: &test.MockClient{
   270  				MockCreate: test.NewMockCreateFn(nil),
   271  				MockGet:    test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
   272  			args: args{
   273  				desired: desired,
   274  			},
   275  			want: want{
   276  				existing: nil,
   277  				err:      nil,
   278  			},
   279  		},
   280  		"GetExistingSuccessfully": {
   281  			reason: "Existing object and no error should be returned",
   282  			c: &test.MockClient{
   283  				MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
   284  					o, _ := obj.(*unstructured.Unstructured)
   285  					*o = *desired
   286  					return nil
   287  				})},
   288  			args: args{
   289  				desired: desired,
   290  			},
   291  			want: want{
   292  				existing: desired,
   293  				err:      nil,
   294  			},
   295  		},
   296  	}
   297  
   298  	for caseName, tc := range cases {
   299  		t.Run(caseName, func(t *testing.T) {
   300  			act := new(applyAction)
   301  			result, err := createOrGetExisting(ctx, act, tc.c, tc.args.desired, tc.args.ao...)
   302  			if diff := cmp.Diff(tc.want.existing, result); diff != "" {
   303  				t.Errorf("\n%s\ncreateOrGetExisting(...): -want , +got \n%s\n", tc.reason, diff)
   304  			}
   305  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   306  				t.Errorf("\n%s\ncreateOrGetExisting(...): -want error, +got error\n%s\n", tc.reason, diff)
   307  			}
   308  		})
   309  	}
   310  
   311  }
   312  
   313  func TestMustBeControllableBy(t *testing.T) {
   314  	uid := types.UID("very-unique-string")
   315  	controller := true
   316  
   317  	cases := map[string]struct {
   318  		reason  string
   319  		current client.Object
   320  		u       types.UID
   321  		want    error
   322  	}{
   323  		"NoExistingObject": {
   324  			reason: "No error should be returned if no existing object",
   325  		},
   326  		"Adoptable": {
   327  			reason:  "A current object with no controller reference may be adopted and controlled",
   328  			u:       uid,
   329  			current: &testObject{},
   330  		},
   331  		"ControlledBySuppliedUID": {
   332  			reason: "A current object that is already controlled by the supplied UID is controllable",
   333  			u:      uid,
   334  			current: &testObject{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
   335  				UID:        uid,
   336  				Controller: &controller,
   337  			}}}},
   338  		},
   339  		"ControlledBySomeoneElse": {
   340  			reason: "A current object that is already controlled by a different UID is not controllable",
   341  			u:      uid,
   342  			current: &testObject{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
   343  				UID:        types.UID("some-other-uid"),
   344  				Controller: &controller,
   345  			}}}},
   346  			want: errors.Errorf("existing object is not controlled by UID %q", uid),
   347  		},
   348  		"cross namespace resource": {
   349  			reason: "A cross namespace resource have a resourceTracker owner, skip check UID",
   350  			u:      uid,
   351  			current: &testObject{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{
   352  				UID:        uid,
   353  				Controller: &controller,
   354  				Kind:       v1beta1.ResourceTrackerKind,
   355  			}}}},
   356  		},
   357  	}
   358  
   359  	for name, tc := range cases {
   360  		t.Run(name, func(t *testing.T) {
   361  			ao := MustBeControllableBy(tc.u)
   362  			act := new(applyAction)
   363  			err := ao(act, tc.current, nil)
   364  			if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
   365  				t.Errorf("\n%s\nMustBeControllableBy(...)(...): -want error, +got error\n%s\n", tc.reason, diff)
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  func TestMustBeControlledByApp(t *testing.T) {
   372  	app := &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "app"}}
   373  	ao := MustBeControlledByApp(app)
   374  	testCases := map[string]struct {
   375  		existing client.Object
   376  		hasError bool
   377  	}{
   378  		"no old app": {
   379  			existing: nil,
   380  			hasError: false,
   381  		},
   382  		"old app has no label": {
   383  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{ResourceVersion: "-"}},
   384  			hasError: true,
   385  		},
   386  		"old app has no app label": {
   387  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
   388  				Labels:          map[string]string{},
   389  				ResourceVersion: "-",
   390  			}},
   391  			hasError: true,
   392  		},
   393  		"old app has no app ns label": {
   394  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
   395  				Labels:          map[string]string{oam.LabelAppName: "app"},
   396  				ResourceVersion: "-",
   397  			}},
   398  			hasError: true,
   399  		},
   400  		"old app has correct label": {
   401  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
   402  				Labels: map[string]string{oam.LabelAppName: "app", oam.LabelAppNamespace: "default"},
   403  			}},
   404  			hasError: false,
   405  		},
   406  		"old app has incorrect app label": {
   407  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
   408  				Labels: map[string]string{oam.LabelAppName: "a", oam.LabelAppNamespace: "default"},
   409  			}},
   410  			hasError: true,
   411  		},
   412  		"old app has incorrect ns label": {
   413  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
   414  				Labels: map[string]string{oam.LabelAppName: "app", oam.LabelAppNamespace: "ns"},
   415  			}},
   416  			hasError: true,
   417  		},
   418  		"old app has no resource version but with bad app key": {
   419  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
   420  				Labels: map[string]string{oam.LabelAppName: "app", oam.LabelAppNamespace: "ns"},
   421  			}},
   422  			hasError: true,
   423  		},
   424  		"old app has no resource version": {
   425  			existing: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
   426  				Labels: map[string]string{},
   427  			}},
   428  			hasError: false,
   429  		},
   430  	}
   431  	for name, tc := range testCases {
   432  		t.Run(name, func(t *testing.T) {
   433  			r := require.New(t)
   434  			err := ao(&applyAction{}, tc.existing, nil)
   435  			if tc.hasError {
   436  				r.Error(err)
   437  			} else {
   438  				r.NoError(err)
   439  			}
   440  		})
   441  	}
   442  }
   443  
   444  func TestSharedByApp(t *testing.T) {
   445  	app := &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Name: "app"}}
   446  	ao := SharedByApp(app)
   447  	testCases := map[string]struct {
   448  		existing client.Object
   449  		desired  client.Object
   450  		output   client.Object
   451  		hasError bool
   452  	}{
   453  		"create new resource": {
   454  			existing: nil,
   455  			desired: &unstructured.Unstructured{Object: map[string]interface{}{
   456  				"kind": "ConfigMap",
   457  			}},
   458  			output: &unstructured.Unstructured{Object: map[string]interface{}{
   459  				"kind": "ConfigMap",
   460  				"metadata": map[string]interface{}{
   461  					"annotations": map[string]interface{}{oam.AnnotationAppSharedBy: "default/app"},
   462  				},
   463  			}},
   464  		},
   465  		"add sharer to existing resource": {
   466  			existing: &unstructured.Unstructured{Object: map[string]interface{}{
   467  				"kind": "ConfigMap",
   468  			}},
   469  			desired: &unstructured.Unstructured{Object: map[string]interface{}{
   470  				"kind": "ConfigMap",
   471  			}},
   472  			output: &unstructured.Unstructured{Object: map[string]interface{}{
   473  				"kind": "ConfigMap",
   474  				"metadata": map[string]interface{}{
   475  					"annotations": map[string]interface{}{oam.AnnotationAppSharedBy: "default/app"},
   476  				},
   477  			}},
   478  		},
   479  		"add sharer to existing sharing resource": {
   480  			existing: &unstructured.Unstructured{Object: map[string]interface{}{
   481  				"kind": "ConfigMap",
   482  				"metadata": map[string]interface{}{
   483  					"labels": map[string]interface{}{
   484  						oam.LabelAppName:      "example",
   485  						oam.LabelAppNamespace: "default",
   486  					},
   487  					"annotations": map[string]interface{}{oam.AnnotationAppSharedBy: "x/y"},
   488  				},
   489  				"data": "x",
   490  			}},
   491  			desired: &unstructured.Unstructured{Object: map[string]interface{}{
   492  				"kind": "ConfigMap",
   493  				"data": "y",
   494  			}},
   495  			output: &unstructured.Unstructured{Object: map[string]interface{}{
   496  				"kind": "ConfigMap",
   497  				"metadata": map[string]interface{}{
   498  					"labels": map[string]interface{}{
   499  						oam.LabelAppName:      "example",
   500  						oam.LabelAppNamespace: "default",
   501  					},
   502  					"annotations": map[string]interface{}{oam.AnnotationAppSharedBy: "x/y,default/app"},
   503  				},
   504  				"data": "x",
   505  			}},
   506  		},
   507  		"add sharer to existing sharing resource owned by self": {
   508  			existing: &unstructured.Unstructured{Object: map[string]interface{}{
   509  				"kind": "ConfigMap",
   510  				"metadata": map[string]interface{}{
   511  					"labels": map[string]interface{}{
   512  						oam.LabelAppName:      "app",
   513  						oam.LabelAppNamespace: "default",
   514  					},
   515  					"annotations": map[string]interface{}{oam.AnnotationAppSharedBy: "default/app,x/y"},
   516  				},
   517  				"data": "x",
   518  			}},
   519  			desired: &unstructured.Unstructured{Object: map[string]interface{}{
   520  				"kind": "ConfigMap",
   521  				"metadata": map[string]interface{}{
   522  					"labels": map[string]interface{}{
   523  						oam.LabelAppName:      "app",
   524  						oam.LabelAppNamespace: "default",
   525  					},
   526  					"annotations": map[string]interface{}{oam.AnnotationAppSharedBy: "default/app"},
   527  				},
   528  				"data": "y",
   529  			}},
   530  			output: &unstructured.Unstructured{Object: map[string]interface{}{
   531  				"kind": "ConfigMap",
   532  				"metadata": map[string]interface{}{
   533  					"labels": map[string]interface{}{
   534  						oam.LabelAppName:      "app",
   535  						oam.LabelAppNamespace: "default",
   536  					},
   537  					"annotations": map[string]interface{}{oam.AnnotationAppSharedBy: "default/app,x/y"},
   538  				},
   539  				"data": "y",
   540  			}},
   541  		},
   542  		"add sharer to existing non-sharing resource": {
   543  			existing: &unstructured.Unstructured{Object: map[string]interface{}{
   544  				"kind": "ConfigMap",
   545  				"metadata": map[string]interface{}{
   546  					"labels": map[string]interface{}{
   547  						oam.LabelAppName:      "example",
   548  						oam.LabelAppNamespace: "default",
   549  					},
   550  				},
   551  			}},
   552  			desired: &unstructured.Unstructured{Object: map[string]interface{}{
   553  				"kind": "ConfigMap",
   554  			}},
   555  			hasError: true,
   556  		},
   557  	}
   558  	for name, tc := range testCases {
   559  		t.Run(name, func(t *testing.T) {
   560  			r := require.New(t)
   561  			err := ao(&applyAction{}, tc.existing, tc.desired)
   562  			if tc.hasError {
   563  				r.Error(err)
   564  			} else {
   565  				r.NoError(err)
   566  				r.Equal(tc.output, tc.desired)
   567  			}
   568  		})
   569  	}
   570  }
   571  
   572  func TestFilterSpecialAnn(t *testing.T) {
   573  	var cm = &corev1.ConfigMap{}
   574  	var sc = &corev1.Secret{}
   575  	var dp = &appsv1.Deployment{}
   576  	var crd = &v1.CustomResourceDefinition{}
   577  	assert.Equal(t, false, trimLastAppliedConfigurationForSpecialResources(cm))
   578  	assert.Equal(t, false, trimLastAppliedConfigurationForSpecialResources(sc))
   579  	assert.Equal(t, false, trimLastAppliedConfigurationForSpecialResources(crd))
   580  	assert.Equal(t, true, trimLastAppliedConfigurationForSpecialResources(dp))
   581  
   582  	dp.Annotations = map[string]string{oam.AnnotationLastAppliedConfig: "-"}
   583  	assert.Equal(t, false, trimLastAppliedConfigurationForSpecialResources(dp))
   584  	dp.Annotations = map[string]string{oam.AnnotationLastAppliedConfig: "skip"}
   585  	assert.Equal(t, false, trimLastAppliedConfigurationForSpecialResources(dp))
   586  	dp.Annotations = map[string]string{oam.AnnotationLastAppliedConfig: "xxx"}
   587  	assert.Equal(t, true, trimLastAppliedConfigurationForSpecialResources(dp))
   588  }