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 }