github.com/oam-dev/kubevela@v1.9.11/pkg/appfile/parser_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 appfile 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "strings" 24 "testing" 25 26 "github.com/crossplane/crossplane-runtime/pkg/test" 27 . "github.com/onsi/ginkgo/v2" 28 . "github.com/onsi/gomega" 29 "github.com/stretchr/testify/assert" 30 errors2 "k8s.io/apimachinery/pkg/api/errors" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 "sigs.k8s.io/controller-runtime/pkg/client/fake" 35 "sigs.k8s.io/yaml" 36 37 workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" 38 39 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 40 "github.com/oam-dev/kubevela/apis/types" 41 42 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 43 "github.com/oam-dev/kubevela/pkg/oam/util" 44 common2 "github.com/oam-dev/kubevela/pkg/utils/common" 45 ) 46 47 var expectedExceptApp = &Appfile{ 48 Name: "application-sample", 49 ParsedComponents: []*Component{ 50 { 51 Name: "myweb", 52 Type: "worker", 53 Params: map[string]interface{}{ 54 "image": "busybox", 55 "cmd": []interface{}{"sleep", "1000"}, 56 }, 57 FullTemplate: &Template{ 58 TemplateStr: ` 59 output: { 60 apiVersion: "apps/v1" 61 kind: "Deployment" 62 spec: { 63 selector: matchLabels: { 64 "app.oam.dev/component": context.name 65 } 66 67 template: { 68 metadata: labels: { 69 "app.oam.dev/component": context.name 70 } 71 72 spec: { 73 containers: [{ 74 name: context.name 75 image: parameter.image 76 77 if parameter["cmd"] != _|_ { 78 command: parameter.cmd 79 } 80 }] 81 } 82 } 83 84 selector: 85 matchLabels: 86 "app.oam.dev/component": context.name 87 } 88 } 89 90 parameter: { 91 // +usage=Which image would you like to use for your service 92 // +short=i 93 image: string 94 95 cmd?: [...string] 96 }`, 97 }, 98 }, 99 }, 100 WorkflowSteps: []workflowv1alpha1.WorkflowStep{ 101 { 102 WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{ 103 Name: "suspend", 104 Type: "suspend", 105 }, 106 }, 107 }, 108 } 109 110 const componentDefinition = ` 111 apiVersion: core.oam.dev/v1beta1 112 kind: ComponentDefinition 113 metadata: 114 name: worker 115 annotations: 116 definition.oam.dev/description: "Long-running scalable backend worker without network endpoint" 117 spec: 118 workload: 119 definition: 120 apiVersion: apps/v1 121 kind: Deployment 122 extension: 123 template: | 124 output: { 125 apiVersion: "apps/v1" 126 kind: "Deployment" 127 spec: { 128 selector: matchLabels: { 129 "app.oam.dev/component": context.name 130 } 131 132 template: { 133 metadata: labels: { 134 "app.oam.dev/component": context.name 135 } 136 137 spec: { 138 containers: [{ 139 name: context.name 140 image: parameter.image 141 142 if parameter["cmd"] != _|_ { 143 command: parameter.cmd 144 } 145 }] 146 } 147 } 148 149 selector: 150 matchLabels: 151 "app.oam.dev/component": context.name 152 } 153 } 154 155 parameter: { 156 // +usage=Which image would you like to use for your service 157 // +short=i 158 image: string 159 160 cmd?: [...string] 161 }` 162 163 const policyDefinition = ` 164 # Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. 165 # Definition source cue file: vela-templates/definitions/internal/topology.cue 166 apiVersion: core.oam.dev/v1beta1 167 kind: PolicyDefinition 168 metadata: 169 annotations: 170 definition.oam.dev/description: Determining the destination where components should be deployed to. 171 name: topology 172 namespace: {{ include "systemDefinitionNamespace" . }} 173 spec: 174 schematic: 175 cue: 176 template: | 177 parameter: { 178 // +usage=Specify the names of the clusters to select. 179 cluster?: [...string] 180 // +usage=Specify the label selector for clusters 181 clusterLabelSelector?: [string]: string 182 // +usage=Deprecated: Use clusterLabelSelector instead. 183 clusterSelector?: [string]: string 184 // +usage=Specify the target namespace to deploy in the selected clusters, default inherit the original namespace. 185 namespace?: string 186 } 187 ` 188 189 const appfileYaml = ` 190 apiVersion: core.oam.dev/v1beta1 191 kind: Application 192 metadata: 193 name: application-sample 194 namespace: default 195 spec: 196 components: 197 - name: myweb 198 type: worker 199 properties: 200 image: "busybox" 201 cmd: 202 - sleep 203 - "1000" 204 workflow: 205 steps: 206 - name: "suspend" 207 type: "suspend" 208 ` 209 210 const appfileYaml2 = ` 211 apiVersion: core.oam.dev/v1beta1 212 kind: Application 213 metadata: 214 name: application-sample 215 namespace: default 216 spec: 217 components: 218 - name: myweb 219 type: worker-notexist 220 properties: 221 image: "busybox" 222 ` 223 224 const appfileYamlEmptyPolicy = ` 225 apiVersion: core.oam.dev/v1beta1 226 kind: Application 227 metadata: 228 name: application-sample 229 namespace: default 230 spec: 231 components: [] 232 policies: 233 - type: garbage-collect 234 name: somename 235 properties: 236 ` 237 238 var _ = Describe("Test application parser", func() { 239 It("Test parse an application", func() { 240 o := v1beta1.Application{} 241 err := yaml.Unmarshal([]byte(appfileYaml), &o) 242 Expect(err).ShouldNot(HaveOccurred()) 243 244 // Create a mock client 245 tclient := test.MockClient{ 246 MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { 247 if strings.Contains(key.Name, "notexist") { 248 return &errors2.StatusError{ErrStatus: metav1.Status{Reason: "NotFound", Message: "not found"}} 249 } 250 switch o := obj.(type) { 251 case *v1beta1.ComponentDefinition: 252 wd, err := util.UnMarshalStringToComponentDefinition(componentDefinition) 253 if err != nil { 254 return err 255 } 256 *o = *wd 257 case *v1beta1.PolicyDefinition: 258 ppd, err := util.UnMarshalStringToPolicyDefinition(policyDefinition) 259 if err != nil { 260 return err 261 } 262 *o = *ppd 263 } 264 return nil 265 }, 266 } 267 268 appfile, err := NewApplicationParser(&tclient, pd).GenerateAppFile(context.TODO(), &o) 269 Expect(err).ShouldNot(HaveOccurred()) 270 Expect(equal(expectedExceptApp, appfile)).Should(BeTrue()) 271 272 notfound := v1beta1.Application{} 273 err = yaml.Unmarshal([]byte(appfileYaml2), ¬found) 274 Expect(err).ShouldNot(HaveOccurred()) 275 _, err = NewApplicationParser(&tclient, pd).GenerateAppFile(context.TODO(), ¬found) 276 Expect(err).Should(HaveOccurred()) 277 278 By("app with empty policy") 279 emptyPolicy := v1beta1.Application{} 280 err = yaml.Unmarshal([]byte(appfileYamlEmptyPolicy), &emptyPolicy) 281 Expect(err).ShouldNot(HaveOccurred()) 282 _, err = NewApplicationParser(&tclient, pd).GenerateAppFile(context.TODO(), &emptyPolicy) 283 Expect(err).Should(HaveOccurred()) 284 Expect(err.Error()).Should(ContainSubstring("have empty properties")) 285 }) 286 }) 287 288 func equal(af, dest *Appfile) bool { 289 if af.Name != dest.Name || len(af.ParsedComponents) != len(dest.ParsedComponents) { 290 return false 291 } 292 for i, wd := range af.ParsedComponents { 293 destWd := dest.ParsedComponents[i] 294 if wd.Name != destWd.Name || len(wd.Traits) != len(destWd.Traits) { 295 return false 296 } 297 if !reflect.DeepEqual(wd.Params, destWd.Params) { 298 fmt.Printf("%#v | %#v\n", wd.Params, destWd.Params) 299 return false 300 } 301 for j, td := range wd.Traits { 302 destTd := destWd.Traits[j] 303 if td.Name != destTd.Name { 304 fmt.Printf("td:%s dest%s", td.Name, destTd.Name) 305 return false 306 } 307 if !reflect.DeepEqual(td.Params, destTd.Params) { 308 fmt.Printf("%#v | %#v\n", td.Params, destTd.Params) 309 return false 310 } 311 } 312 } 313 return true 314 } 315 316 var _ = Describe("Test application parser", func() { 317 var app v1beta1.Application 318 var apprev v1beta1.ApplicationRevision 319 var wsd v1beta1.WorkflowStepDefinition 320 var expectedExceptAppfile *Appfile 321 var mockClient test.MockClient 322 323 BeforeEach(func() { 324 // prepare WorkflowStepDefinition 325 Expect(common2.ReadYamlToObject("testdata/backport-1-2/wsd.yaml", &wsd)).Should(BeNil()) 326 327 // prepare verify data 328 expectedExceptAppfile = &Appfile{ 329 Name: "backport-1-2-test-demo", 330 ParsedComponents: []*Component{ 331 { 332 Name: "backport-1-2-test-demo", 333 Type: "webservice", 334 Params: map[string]interface{}{ 335 "image": "nginx", 336 }, 337 FullTemplate: &Template{ 338 TemplateStr: ` 339 output: { 340 apiVersion: "apps/v1" 341 kind: "Deployment" 342 spec: { 343 selector: matchLabels: { 344 "app.oam.dev/component": context.name 345 } 346 347 template: { 348 metadata: labels: { 349 "app.oam.dev/component": context.name 350 } 351 352 spec: { 353 containers: [{ 354 name: context.name 355 image: parameter.image 356 357 if parameter["cmd"] != _|_ { 358 command: parameter.cmd 359 } 360 }] 361 } 362 } 363 364 selector: 365 matchLabels: 366 "app.oam.dev/component": context.name 367 } 368 } 369 370 parameter: { 371 // +usage=Which image would you like to use for your service 372 // +short=i 373 image: string 374 375 cmd?: [...string] 376 }`, 377 }, 378 Traits: []*Trait{ 379 { 380 Name: "scaler", 381 Params: map[string]interface{}{ 382 "replicas": float64(1), 383 }, 384 Template: ` 385 parameter: { 386 // +usage=Specify the number of workload 387 replicas: *1 | int 388 } 389 // +patchStrategy=retainKeys 390 patch: spec: replicas: parameter.replicas 391 392 `, 393 }, 394 }, 395 }, 396 }, 397 WorkflowSteps: []workflowv1alpha1.WorkflowStep{ 398 { 399 WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{ 400 Name: "apply", 401 Type: "apply-application", 402 }, 403 }, 404 }, 405 } 406 407 // Create mock client 408 mockClient = test.MockClient{ 409 MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { 410 if strings.Contains(key.Name, "unknown") { 411 return &errors2.StatusError{ErrStatus: metav1.Status{Reason: "NotFound", Message: "not found"}} 412 } 413 switch o := obj.(type) { 414 case *v1beta1.ComponentDefinition: 415 wd, err := util.UnMarshalStringToComponentDefinition(componentDefinition) 416 if err != nil { 417 return err 418 } 419 *o = *wd 420 case *v1beta1.WorkflowStepDefinition: 421 *o = wsd 422 case *v1beta1.ApplicationRevision: 423 *o = apprev 424 default: 425 // skip 426 } 427 return nil 428 }, 429 } 430 }) 431 432 When("with apply-application workflowStep", func() { 433 BeforeEach(func() { 434 // prepare application 435 Expect(common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app)).Should(BeNil()) 436 // prepare application revision 437 Expect(common2.ReadYamlToObject("testdata/backport-1-2/apprev1.yaml", &apprev)).Should(BeNil()) 438 }) 439 440 It("Test we can parse an application revision to an appFile 1", func() { 441 442 appfile, err := NewApplicationParser(&mockClient, pd).GenerateAppFile(context.TODO(), &app) 443 Expect(err).ShouldNot(HaveOccurred()) 444 Expect(equal(expectedExceptAppfile, appfile)).Should(BeTrue()) 445 Expect(len(appfile.WorkflowSteps) > 0 && 446 len(appfile.RelatedWorkflowStepDefinitions) == len(appfile.AppRevision.Spec.WorkflowStepDefinitions)).Should(BeTrue()) 447 448 Expect(len(appfile.WorkflowSteps) > 0 && func() bool { 449 this := appfile.RelatedWorkflowStepDefinitions 450 that := appfile.AppRevision.Spec.WorkflowStepDefinitions 451 for i, w := range this { 452 thatW := that[i] 453 if !reflect.DeepEqual(w, thatW) { 454 return false 455 } 456 } 457 return true 458 }()).Should(BeTrue()) 459 }) 460 }) 461 462 When("with apply-application and apply-component build-in workflowStep", func() { 463 BeforeEach(func() { 464 // prepare application 465 Expect(common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app)).Should(BeNil()) 466 // prepare application revision 467 Expect(common2.ReadYamlToObject("testdata/backport-1-2/apprev2.yaml", &apprev)).Should(BeNil()) 468 }) 469 470 It("Test we can parse an application revision to an appFile 2", func() { 471 472 appfile, err := NewApplicationParser(&mockClient, pd).GenerateAppFile(context.TODO(), &app) 473 Expect(err).ShouldNot(HaveOccurred()) 474 Expect(equal(expectedExceptAppfile, appfile)).Should(BeTrue()) 475 Expect(len(appfile.WorkflowSteps) > 0 && 476 len(appfile.RelatedWorkflowStepDefinitions) == len(appfile.AppRevision.Spec.WorkflowStepDefinitions)).Should(BeTrue()) 477 478 Expect(len(appfile.WorkflowSteps) > 0 && func() bool { 479 this := appfile.RelatedWorkflowStepDefinitions 480 that := appfile.AppRevision.Spec.WorkflowStepDefinitions 481 for i, w := range this { 482 thatW := that[i] 483 if !reflect.DeepEqual(w, thatW) { 484 fmt.Printf("appfile wsd:%s apprev wsd%s", (*w).Name, thatW.Name) 485 return false 486 } 487 } 488 return true 489 }()).Should(BeTrue()) 490 }) 491 }) 492 493 When("with unknown workflowStep", func() { 494 BeforeEach(func() { 495 // prepare application 496 Expect(common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app)).Should(BeNil()) 497 // prepare application revision 498 Expect(common2.ReadYamlToObject("testdata/backport-1-2/apprev3.yaml", &apprev)).Should(BeNil()) 499 }) 500 501 It("Test we can parse an application revision to an appFile 3", func() { 502 503 _, err := NewApplicationParser(&mockClient, pd).GenerateAppFile(context.TODO(), &app) 504 Expect(err).Should(HaveOccurred()) 505 Expect(err.Error()).Should(SatisfyAll( 506 ContainSubstring("failed to get workflow step definition apply-application-unknown: not found"), 507 ContainSubstring("failed to parseWorkflowStepsForLegacyRevision")), 508 ) 509 }) 510 }) 511 }) 512 513 func TestParser_parseTraits(t *testing.T) { 514 type args struct { 515 workload *Component 516 comp common.ApplicationComponent 517 } 518 tests := []struct { 519 name string 520 args args 521 wantErr assert.ErrorAssertionFunc 522 mockTemplateLoaderFn TemplateLoaderFn 523 validateFunc func(w *Component) bool 524 }{ 525 { 526 name: "test empty traits", 527 args: args{ 528 comp: common.ApplicationComponent{}, 529 }, 530 wantErr: assert.NoError, 531 }, 532 { 533 name: "test parse trait properties error", 534 args: args{ 535 comp: common.ApplicationComponent{ 536 Traits: []common.ApplicationTrait{ 537 { 538 Type: "expose", 539 Properties: &runtime.RawExtension{ 540 Raw: []byte("invalid properties"), 541 }, 542 }, 543 }, 544 }, 545 }, 546 wantErr: assert.Error, 547 }, 548 { 549 name: "test parse trait error", 550 args: args{ 551 comp: common.ApplicationComponent{ 552 Traits: []common.ApplicationTrait{ 553 { 554 Type: "expose", 555 Properties: &runtime.RawExtension{ 556 Raw: []byte(`{"unsupported": "{\"key\":\"value\"}"}`), 557 }, 558 }, 559 }, 560 }, 561 }, 562 mockTemplateLoaderFn: func(context.Context, client.Client, string, types.CapType) (*Template, error) { 563 return nil, fmt.Errorf("unsupported key not found") 564 }, 565 wantErr: assert.Error, 566 }, 567 { 568 name: "test parse trait success", 569 args: args{ 570 comp: common.ApplicationComponent{ 571 Traits: []common.ApplicationTrait{ 572 { 573 Type: "expose", 574 Properties: &runtime.RawExtension{ 575 Raw: []byte(`{"annotation": "{\"key\":\"value\"}"}`), 576 }, 577 }, 578 }, 579 }, 580 workload: &Component{}, 581 }, 582 wantErr: assert.NoError, 583 mockTemplateLoaderFn: func(ctx context.Context, reader client.Client, s string, capType types.CapType) (*Template, error) { 584 return &Template{ 585 TemplateStr: "template", 586 CapabilityCategory: "network", 587 Health: "true", 588 CustomStatus: "healthy", 589 }, nil 590 }, 591 validateFunc: func(w *Component) bool { 592 return w != nil && len(w.Traits) != 0 && w.Traits[0].Name == "expose" && w.Traits[0].Template == "template" 593 }, 594 }, 595 } 596 597 p := NewApplicationParser(nil, pd) 598 for _, tt := range tests { 599 t.Run(tt.name, func(t *testing.T) { 600 p.tmplLoader = tt.mockTemplateLoaderFn 601 err := p.parseTraits(context.Background(), tt.args.workload, tt.args.comp) 602 tt.wantErr(t, err, fmt.Sprintf("parseTraits(%v, %v)", tt.args.workload, tt.args.comp)) 603 if tt.validateFunc != nil { 604 assert.True(t, tt.validateFunc(tt.args.workload)) 605 } 606 }) 607 } 608 } 609 610 func TestParser_parseTraitsFromRevision(t *testing.T) { 611 type args struct { 612 comp common.ApplicationComponent 613 appRev *v1beta1.ApplicationRevision 614 workload *Component 615 } 616 tests := []struct { 617 name string 618 args args 619 validateFunc func(w *Component) bool 620 wantErr assert.ErrorAssertionFunc 621 }{ 622 { 623 name: "test empty traits", 624 args: args{ 625 comp: common.ApplicationComponent{}, 626 }, 627 wantErr: assert.NoError, 628 }, 629 { 630 name: "test parse traits properties error", 631 args: args{ 632 comp: common.ApplicationComponent{ 633 Traits: []common.ApplicationTrait{ 634 { 635 Type: "expose", 636 Properties: &runtime.RawExtension{Raw: []byte("invalid")}, 637 }, 638 }, 639 }, 640 workload: &Component{}, 641 }, 642 wantErr: assert.Error, 643 }, 644 { 645 name: "test parse traits from revision failed", 646 args: args{ 647 comp: common.ApplicationComponent{ 648 Traits: []common.ApplicationTrait{ 649 { 650 Type: "expose", 651 Properties: &runtime.RawExtension{Raw: []byte(`{"appRevisionName": "appRevName"}`)}, 652 }, 653 }, 654 }, 655 appRev: &v1beta1.ApplicationRevision{ 656 Spec: v1beta1.ApplicationRevisionSpec{ 657 ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{ 658 TraitDefinitions: map[string]*v1beta1.TraitDefinition{}, 659 }, 660 }, 661 }, 662 workload: &Component{}, 663 }, 664 wantErr: assert.Error, 665 }, 666 { 667 name: "test parse traits from revision success", 668 args: args{ 669 comp: common.ApplicationComponent{ 670 Traits: []common.ApplicationTrait{ 671 { 672 Type: "expose", 673 Properties: &runtime.RawExtension{Raw: []byte(`{"appRevisionName": "appRevName"}`)}, 674 }, 675 }, 676 }, 677 appRev: &v1beta1.ApplicationRevision{ 678 Spec: v1beta1.ApplicationRevisionSpec{ 679 ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{ 680 TraitDefinitions: map[string]*v1beta1.TraitDefinition{ 681 "expose": { 682 Spec: v1beta1.TraitDefinitionSpec{ 683 RevisionEnabled: true, 684 AppliesToWorkloads: []string{"*"}, 685 }, 686 }, 687 }, 688 }, 689 }, 690 }, 691 workload: &Component{}, 692 }, 693 wantErr: assert.NoError, 694 validateFunc: func(w *Component) bool { 695 return w != nil && len(w.Traits) == 1 && w.Traits[0].Name == "expose" 696 }, 697 }, 698 } 699 p := NewApplicationParser(fake.NewClientBuilder().Build(), pd) 700 for _, tt := range tests { 701 t.Run(tt.name, func(t *testing.T) { 702 tt.wantErr(t, p.parseTraitsFromRevision(tt.args.comp, tt.args.appRev, tt.args.workload), fmt.Sprintf("parseTraitsFromRevision(%v, %v, %v)", tt.args.comp, tt.args.appRev, tt.args.workload)) 703 if tt.validateFunc != nil { 704 assert.True(t, tt.validateFunc(tt.args.workload)) 705 } 706 }) 707 } 708 }