github.com/oam-dev/kubevela@v1.9.11/pkg/cue/definition/template_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 definition 18 19 import ( 20 "testing" 21 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 25 "k8s.io/apimachinery/pkg/runtime" 26 27 "github.com/kubevela/workflow/pkg/cue/packages" 28 wfprocess "github.com/kubevela/workflow/pkg/cue/process" 29 30 "github.com/oam-dev/kubevela/apis/types" 31 "github.com/oam-dev/kubevela/pkg/cue/process" 32 ) 33 34 func TestWorkloadTemplateComplete(t *testing.T) { 35 testCases := map[string]struct { 36 workloadTemplate string 37 params map[string]interface{} 38 expectObj runtime.Object 39 expAssObjs map[string]runtime.Object 40 category types.CapabilityCategory 41 hasCompileErr bool 42 }{ 43 "only contain an output": { 44 workloadTemplate: ` 45 output:{ 46 apiVersion: "apps/v1" 47 kind: "Deployment" 48 metadata: name: context.name 49 spec: replicas: parameter.replicas 50 } 51 parameter: { 52 replicas: *1 | int 53 type: string 54 host: string 55 } 56 `, 57 params: map[string]interface{}{ 58 "replicas": 2, 59 "type": "ClusterIP", 60 "host": "example.com", 61 }, 62 expectObj: &unstructured.Unstructured{Object: map[string]interface{}{ 63 "apiVersion": "apps/v1", 64 "kind": "Deployment", 65 "metadata": map[string]interface{}{"name": "test"}, 66 "spec": map[string]interface{}{"replicas": int64(2)}, 67 }}, 68 hasCompileErr: false, 69 }, 70 "contain output and outputs": { 71 workloadTemplate: ` 72 output:{ 73 apiVersion: "apps/v1" 74 kind: "Deployment" 75 metadata: name: context.name 76 spec: replicas: parameter.replicas 77 } 78 outputs: service: { 79 apiVersion: "v1" 80 kind: "Service" 81 metadata: name: context.name 82 spec: type: parameter.type 83 } 84 outputs: ingress: { 85 apiVersion: "extensions/v1beta1" 86 kind: "Ingress" 87 metadata: name: context.name 88 spec: rules: [{host: parameter.host}] 89 } 90 91 parameter: { 92 replicas: *1 | int 93 type: string 94 host: string 95 } 96 `, 97 params: map[string]interface{}{ 98 "replicas": 2, 99 "type": "ClusterIP", 100 "host": "example.com", 101 }, 102 expectObj: &unstructured.Unstructured{ 103 Object: map[string]interface{}{ 104 "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"}, 105 "spec": map[string]interface{}{ 106 "replicas": int64(2), 107 }, 108 }, 109 }, 110 expAssObjs: map[string]runtime.Object{ 111 "service": &unstructured.Unstructured{ 112 Object: map[string]interface{}{ 113 "apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, 114 "spec": map[string]interface{}{"type": "ClusterIP"}, 115 }, 116 }, 117 "ingress": &unstructured.Unstructured{ 118 Object: map[string]interface{}{ 119 "apiVersion": "extensions/v1beta1", "kind": "Ingress", "metadata": map[string]interface{}{ 120 "name": "test", 121 }, "spec": map[string]interface{}{ 122 "rules": []interface{}{ 123 map[string]interface{}{ 124 "host": "example.com", 125 }, 126 }, 127 }, 128 }, 129 }, 130 }, 131 hasCompileErr: false, 132 }, 133 "output needs context appRevision": { 134 workloadTemplate: ` 135 output:{ 136 apiVersion: "apps/v1" 137 kind: "Deployment" 138 metadata: { 139 name: context.name 140 annotations: "revision.oam.dev": context.appRevision 141 } 142 spec: replicas: parameter.replicas 143 } 144 parameter: { 145 replicas: *1 | int 146 type: string 147 host: string 148 } 149 `, 150 params: map[string]interface{}{ 151 "replicas": 2, 152 "type": "ClusterIP", 153 "host": "example.com", 154 }, 155 expectObj: &unstructured.Unstructured{ 156 Object: map[string]interface{}{ 157 "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ 158 "name": "test", "annotations": map[string]interface{}{ 159 "revision.oam.dev": "myapp-v1", 160 }, 161 }, "spec": map[string]interface{}{ 162 "replicas": int64(2), 163 }, 164 }, 165 }, 166 hasCompileErr: false, 167 }, 168 "output needs context replicas": { 169 workloadTemplate: ` 170 output:{ 171 apiVersion: "apps/v1" 172 kind: "Deployment" 173 metadata: { 174 name: context.name 175 } 176 spec: replicas: parameter.replicas 177 } 178 parameter: { 179 replicas: *1 | int 180 } 181 `, 182 params: nil, 183 expectObj: &unstructured.Unstructured{ 184 Object: map[string]interface{}{ 185 "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"}, 186 "spec": map[string]interface{}{ 187 "replicas": int64(1), 188 }, 189 }, 190 }, 191 hasCompileErr: false, 192 }, 193 "parameter type doesn't match will raise error": { 194 workloadTemplate: ` 195 output:{ 196 apiVersion: "apps/v1" 197 kind: "Deployment" 198 metadata: name: context.name 199 spec: replicas: parameter.replicas 200 } 201 parameter: { 202 replicas: *1 | int 203 type: string 204 host: string 205 } 206 `, 207 params: map[string]interface{}{ 208 "replicas": "2", 209 "type": "ClusterIP", 210 "host": "example.com", 211 }, 212 expectObj: &unstructured.Unstructured{Object: map[string]interface{}{ 213 "apiVersion": "apps/v1", 214 "kind": "Deployment", 215 "metadata": map[string]interface{}{"name": "test"}, 216 "spec": map[string]interface{}{"replicas": int64(2)}, 217 }}, 218 hasCompileErr: true, 219 }, 220 "cluster version info": { 221 workloadTemplate: ` 222 output:{ 223 if context.clusterVersion.minor < 19 { 224 apiVersion: "networking.k8s.io/v1beta1" 225 } 226 if context.clusterVersion.minor >= 19 { 227 apiVersion: "networking.k8s.io/v1" 228 } 229 "kind": "Ingress", 230 } 231 `, 232 params: map[string]interface{}{}, 233 expectObj: &unstructured.Unstructured{Object: map[string]interface{}{ 234 "apiVersion": "networking.k8s.io/v1", 235 "kind": "Ingress", 236 }}, 237 }, 238 } 239 240 for _, v := range testCases { 241 ctx := process.NewContext(process.ContextData{ 242 AppName: "myapp", 243 CompName: "test", 244 Namespace: "default", 245 AppRevisionName: "myapp-v1", 246 ClusterVersion: types.ClusterVersion{Minor: "19+"}, 247 }) 248 wt := NewWorkloadAbstractEngine("testWorkload", &packages.PackageDiscover{}) 249 err := wt.Complete(ctx, v.workloadTemplate, v.params) 250 hasError := err != nil 251 assert.Equal(t, v.hasCompileErr, hasError) 252 if v.hasCompileErr { 253 continue 254 } 255 base, assists := ctx.Output() 256 assert.Equal(t, len(v.expAssObjs), len(assists)) 257 assert.NotNil(t, base) 258 baseObj, err := base.Unstructured() 259 assert.Equal(t, nil, err) 260 assert.Equal(t, v.expectObj, baseObj) 261 for _, ss := range assists { 262 assert.Equal(t, AuxiliaryWorkload, ss.Type) 263 got, err := ss.Ins.Unstructured() 264 assert.NoError(t, err) 265 assert.Equal(t, got, v.expAssObjs[ss.Name]) 266 } 267 } 268 269 } 270 271 func TestTraitTemplateComplete(t *testing.T) { 272 273 tds := map[string]struct { 274 traitName string 275 traitTemplate string 276 params map[string]interface{} 277 expWorkload *unstructured.Unstructured 278 expAssObjs map[string]runtime.Object 279 hasCompileErr bool 280 }{ 281 "patch trait": { 282 traitTemplate: ` 283 patch: { 284 // +patchKey=name 285 spec: template: spec: containers: [parameter] 286 } 287 288 parameter: { 289 name: string 290 image: string 291 command?: [...string] 292 }`, 293 params: map[string]interface{}{ 294 "name": "sidecar", 295 "image": "metrics-agent:0.2", 296 }, 297 expWorkload: &unstructured.Unstructured{ 298 Object: map[string]interface{}{ 299 "apiVersion": "apps/v1", 300 "kind": "Deployment", 301 "spec": map[string]interface{}{ 302 "replicas": int64(2), 303 "selector": map[string]interface{}{ 304 "matchLabels": map[string]interface{}{ 305 "app.oam.dev/component": "test"}}, 306 "template": map[string]interface{}{ 307 "metadata": map[string]interface{}{ 308 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 309 }, 310 "spec": map[string]interface{}{ 311 "containers": []interface{}{map[string]interface{}{ 312 "envFrom": []interface{}{map[string]interface{}{ 313 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 314 }}, 315 "image": "website:0.1", 316 "name": "main", 317 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}, 318 map[string]interface{}{"image": "metrics-agent:0.2", "name": "sidecar"}}}}}}, 319 }, 320 expAssObjs: map[string]runtime.Object{ 321 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 322 Object: map[string]interface{}{ 323 "apiVersion": "v1", 324 "kind": "ConfigMap", 325 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 326 }, 327 }, 328 }, 329 330 "patch trait with strategic merge": { 331 traitTemplate: ` 332 patch: { 333 // +patchKey=name 334 spec: template: spec: { 335 // +patchStrategy=retainKeys 336 containers: [{ 337 name: "main" 338 image: parameter.image 339 ports: [{containerPort: parameter.port}] 340 envFrom: [{ 341 configMapRef: name: context.name + "game-config" 342 }] 343 if parameter["command"] != _|_ { 344 command: parameter.command 345 } 346 }] 347 } 348 } 349 350 parameter: { 351 image: string 352 port: int 353 command?: [...string] 354 } 355 `, 356 params: map[string]interface{}{ 357 "image": "website:0.2", 358 "port": 8080, 359 "command": []string{"server", "start"}, 360 }, 361 expWorkload: &unstructured.Unstructured{ 362 Object: map[string]interface{}{ 363 "apiVersion": "apps/v1", 364 "kind": "Deployment", 365 "spec": map[string]interface{}{ 366 "replicas": int64(2), 367 "selector": map[string]interface{}{ 368 "matchLabels": map[string]interface{}{ 369 "app.oam.dev/component": "test"}}, 370 "template": map[string]interface{}{ 371 "metadata": map[string]interface{}{ 372 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 373 }, 374 "spec": map[string]interface{}{ 375 "containers": []interface{}{map[string]interface{}{ 376 "envFrom": []interface{}{map[string]interface{}{ 377 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 378 }}, 379 "image": "website:0.2", 380 "name": "main", 381 "command": []interface{}{"server", "start"}, 382 "ports": []interface{}{map[string]interface{}{"containerPort": int64(8080)}}}, 383 }}}}}, 384 }, 385 expAssObjs: map[string]runtime.Object{ 386 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 387 Object: map[string]interface{}{ 388 "apiVersion": "v1", 389 "kind": "ConfigMap", 390 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 391 }, 392 }, 393 }, 394 "patch trait with json merge patch": { 395 traitTemplate: ` 396 parameter: {...} 397 // +patchStrategy=jsonMergePatch 398 patch: parameter 399 `, 400 params: map[string]interface{}{ 401 "spec": map[string]interface{}{ 402 "replicas": 5, 403 "template": map[string]interface{}{ 404 "spec": nil, 405 }, 406 }, 407 }, 408 expWorkload: &unstructured.Unstructured{ 409 Object: map[string]interface{}{ 410 "apiVersion": "apps/v1", 411 "kind": "Deployment", 412 "spec": map[string]interface{}{ 413 "replicas": int64(5), 414 "selector": map[string]interface{}{ 415 "matchLabels": map[string]interface{}{ 416 "app.oam.dev/component": "test"}}, 417 "template": map[string]interface{}{ 418 "metadata": map[string]interface{}{ 419 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 420 }}}}, 421 }, 422 expAssObjs: map[string]runtime.Object{ 423 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 424 Object: map[string]interface{}{ 425 "apiVersion": "v1", 426 "kind": "ConfigMap", 427 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 428 }, 429 }, 430 }, 431 "patch trait with json patch": { 432 traitTemplate: ` 433 parameter: {operations: [...{...}]} 434 // +patchStrategy=jsonPatch 435 patch: parameter 436 `, 437 params: map[string]interface{}{ 438 "operations": []map[string]interface{}{ 439 {"op": "replace", "path": "/spec/replicas", "value": 5}, 440 {"op": "remove", "path": "/spec/template/spec"}, 441 }, 442 }, 443 expWorkload: &unstructured.Unstructured{ 444 Object: map[string]interface{}{ 445 "apiVersion": "apps/v1", 446 "kind": "Deployment", 447 "spec": map[string]interface{}{ 448 "replicas": int64(5), 449 "selector": map[string]interface{}{ 450 "matchLabels": map[string]interface{}{ 451 "app.oam.dev/component": "test"}}, 452 "template": map[string]interface{}{ 453 "metadata": map[string]interface{}{ 454 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 455 }}}}, 456 }, 457 expAssObjs: map[string]runtime.Object{ 458 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 459 Object: map[string]interface{}{ 460 "apiVersion": "v1", 461 "kind": "ConfigMap", 462 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 463 }, 464 }, 465 }, 466 "patch trait with invalid json patch": { 467 traitTemplate: ` 468 parameter: {patch: [...{...}]} 469 // +patchStrategy=jsonPatch 470 patch: parameter 471 `, 472 params: map[string]interface{}{ 473 "patch": []map[string]interface{}{ 474 {"op": "what", "path": "/spec/replicas", "value": 5}, 475 }, 476 }, 477 hasCompileErr: true, 478 }, 479 "patch trait with replace": { 480 traitTemplate: ` 481 parameter: { 482 name: string 483 ports: [...int] 484 } 485 patch: spec: template: spec: { 486 // +patchKey=name 487 containers: [{ 488 name: parameter.name 489 // +patchStrategy=replace 490 ports: [for k in parameter.ports {containerPort: k}] 491 }] 492 } 493 `, 494 params: map[string]interface{}{ 495 "name": "main", 496 "ports": []int{80, 8443}, 497 }, 498 expWorkload: &unstructured.Unstructured{ 499 Object: map[string]interface{}{ 500 "apiVersion": "apps/v1", 501 "kind": "Deployment", 502 "spec": map[string]interface{}{ 503 "replicas": int64(2), 504 "selector": map[string]interface{}{ 505 "matchLabels": map[string]interface{}{ 506 "app.oam.dev/component": "test"}}, 507 "template": map[string]interface{}{ 508 "metadata": map[string]interface{}{ 509 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 510 }, 511 "spec": map[string]interface{}{ 512 "containers": []interface{}{map[string]interface{}{ 513 "envFrom": []interface{}{map[string]interface{}{ 514 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 515 }}, 516 "image": "website:0.1", 517 "name": "main", 518 "ports": []interface{}{ 519 map[string]interface{}{"containerPort": int64(80)}, 520 map[string]interface{}{"containerPort": int64(8443)}, 521 }}, 522 }}}}}, 523 }, 524 expAssObjs: map[string]runtime.Object{ 525 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 526 Object: map[string]interface{}{ 527 "apiVersion": "v1", 528 "kind": "ConfigMap", 529 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 530 }, 531 }, 532 }, 533 "output trait": { 534 traitTemplate: ` 535 outputs: service: { 536 apiVersion: "v1" 537 kind: "Service" 538 metadata: name: context.name 539 spec: type: parameter.type 540 } 541 parameter: { 542 type: string 543 }`, 544 params: map[string]interface{}{ 545 "type": "ClusterIP", 546 }, 547 expWorkload: &unstructured.Unstructured{ 548 Object: map[string]interface{}{ 549 "apiVersion": "apps/v1", 550 "kind": "Deployment", 551 "spec": map[string]interface{}{ 552 "replicas": int64(2), 553 "selector": map[string]interface{}{ 554 "matchLabels": map[string]interface{}{ 555 "app.oam.dev/component": "test"}}, 556 "template": map[string]interface{}{ 557 "metadata": map[string]interface{}{ 558 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 559 }, 560 "spec": map[string]interface{}{ 561 "containers": []interface{}{map[string]interface{}{ 562 "envFrom": []interface{}{map[string]interface{}{ 563 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 564 }}, 565 "image": "website:0.1", 566 "name": "main", 567 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}}, 568 }, 569 traitName: "t1", 570 expAssObjs: map[string]runtime.Object{ 571 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 572 Object: map[string]interface{}{ 573 "apiVersion": "v1", 574 "kind": "ConfigMap", 575 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 576 }, 577 "t1service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"type": "ClusterIP"}}}, 578 }, 579 }, 580 "outputs trait": { 581 traitTemplate: ` 582 outputs: service: { 583 apiVersion: "v1" 584 kind: "Service" 585 metadata: name: context.name 586 spec: type: parameter.type 587 } 588 outputs: ingress: { 589 apiVersion: "extensions/v1beta1" 590 kind: "Ingress" 591 metadata: name: context.name 592 spec: rules: [{host: parameter.host}] 593 } 594 parameter: { 595 type: string 596 host: string 597 }`, 598 params: map[string]interface{}{ 599 "type": "ClusterIP", 600 "host": "example.com", 601 }, 602 expWorkload: &unstructured.Unstructured{ 603 Object: map[string]interface{}{ 604 "apiVersion": "apps/v1", 605 "kind": "Deployment", 606 "spec": map[string]interface{}{ 607 "replicas": int64(2), 608 "selector": map[string]interface{}{ 609 "matchLabels": map[string]interface{}{ 610 "app.oam.dev/component": "test"}}, 611 "template": map[string]interface{}{ 612 "metadata": map[string]interface{}{ 613 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 614 }, 615 "spec": map[string]interface{}{ 616 "containers": []interface{}{map[string]interface{}{ 617 "envFrom": []interface{}{map[string]interface{}{ 618 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 619 }}, 620 "image": "website:0.1", 621 "name": "main", 622 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}}, 623 }, 624 traitName: "t2", 625 expAssObjs: map[string]runtime.Object{ 626 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 627 Object: map[string]interface{}{ 628 "apiVersion": "v1", 629 "kind": "ConfigMap", 630 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 631 }, 632 "t2service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"type": "ClusterIP"}}}, 633 "t2ingress": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "Ingress", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"rules": []interface{}{map[string]interface{}{ 634 "host": "example.com", 635 }}}}}, 636 }, 637 }, 638 "outputs trait with context appRevision": { 639 traitTemplate: ` 640 outputs: service: { 641 apiVersion: "v1" 642 kind: "Service" 643 metadata: { 644 name: context.name 645 annotations: "revision.oam.dev": context.appRevision 646 } 647 spec: type: parameter.type 648 } 649 outputs: ingress: { 650 apiVersion: "extensions/v1beta1" 651 kind: "Ingress" 652 metadata: name: context.name 653 spec: rules: [{host: parameter.host}] 654 } 655 parameter: { 656 type: string 657 host: string 658 }`, 659 params: map[string]interface{}{ 660 "type": "ClusterIP", 661 "host": "example.com", 662 }, 663 expWorkload: &unstructured.Unstructured{ 664 Object: map[string]interface{}{ 665 "apiVersion": "apps/v1", 666 "kind": "Deployment", 667 "spec": map[string]interface{}{ 668 "replicas": int64(2), 669 "selector": map[string]interface{}{ 670 "matchLabels": map[string]interface{}{ 671 "app.oam.dev/component": "test"}}, 672 "template": map[string]interface{}{ 673 "metadata": map[string]interface{}{ 674 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 675 }, 676 "spec": map[string]interface{}{ 677 "containers": []interface{}{map[string]interface{}{ 678 "envFrom": []interface{}{map[string]interface{}{ 679 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 680 }}, 681 "image": "website:0.1", 682 "name": "main", 683 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}}, 684 }, 685 traitName: "t2", 686 expAssObjs: map[string]runtime.Object{ 687 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 688 Object: map[string]interface{}{ 689 "apiVersion": "v1", 690 "kind": "ConfigMap", 691 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 692 }, 693 "t2service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test", "annotations": map[string]interface{}{ 694 "revision.oam.dev": "myapp-v1", 695 }}, "spec": map[string]interface{}{"type": "ClusterIP"}}}, 696 "t2ingress": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "Ingress", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"rules": []interface{}{map[string]interface{}{ 697 "host": "example.com", 698 }}}}}, 699 }, 700 }, 701 "simple data passing": { 702 traitTemplate: ` 703 parameter: { 704 domain: string 705 path: string 706 exposePort: int 707 } 708 // trait template can have multiple outputs in one trait 709 outputs: service: { 710 apiVersion: "v1" 711 kind: "Service" 712 spec: { 713 selector: 714 app: context.name 715 ports: [ 716 { 717 port: parameter.exposePort 718 targetPort: context.output.spec.template.spec.containers[0].ports[0].containerPort 719 } 720 ] 721 } 722 } 723 outputs: ingress: { 724 apiVersion: "networking.k8s.io/v1beta1" 725 kind: "Ingress" 726 metadata: 727 name: context.name 728 labels: config: context.outputs.gameconfig.data.enemies 729 spec: { 730 rules: [{ 731 host: parameter.domain 732 http: { 733 paths: [{ 734 path: parameter.path 735 backend: { 736 serviceName: context.name 737 servicePort: parameter.exposePort 738 } 739 }] 740 } 741 }] 742 } 743 }`, 744 params: map[string]interface{}{ 745 "domain": "example.com", 746 "path": "ping", 747 "exposePort": 1080, 748 }, 749 expWorkload: &unstructured.Unstructured{ 750 Object: map[string]interface{}{ 751 "apiVersion": "apps/v1", 752 "kind": "Deployment", 753 "spec": map[string]interface{}{ 754 "replicas": int64(2), 755 "selector": map[string]interface{}{ 756 "matchLabels": map[string]interface{}{ 757 "app.oam.dev/component": "test"}}, 758 "template": map[string]interface{}{ 759 "metadata": map[string]interface{}{ 760 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 761 }, 762 "spec": map[string]interface{}{ 763 "containers": []interface{}{map[string]interface{}{ 764 "envFrom": []interface{}{map[string]interface{}{ 765 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 766 }}, 767 "image": "website:0.1", 768 "name": "main", 769 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}}, 770 }, 771 traitName: "t3", 772 expAssObjs: map[string]runtime.Object{ 773 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 774 Object: map[string]interface{}{ 775 "apiVersion": "v1", 776 "kind": "ConfigMap", 777 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 778 }, 779 "t3service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "spec": map[string]interface{}{"ports": []interface{}{map[string]interface{}{"port": int64(1080), "targetPort": int64(443)}}, "selector": map[string]interface{}{"app": "test"}}}}, 780 "t3ingress": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "networking.k8s.io/v1beta1", "kind": "Ingress", "labels": map[string]interface{}{"config": "enemies-data"}, "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"rules": []interface{}{map[string]interface{}{"host": "example.com", "http": map[string]interface{}{"paths": []interface{}{map[string]interface{}{"backend": map[string]interface{}{"serviceName": "test", "servicePort": int64(1080)}, "path": "ping"}}}}}}}}, 781 }, 782 }, 783 "outputs trait with schema": { 784 traitTemplate: ` 785 #Service:{ 786 apiVersion: string 787 kind: string 788 } 789 #Ingress:{ 790 apiVersion: string 791 kind: string 792 } 793 outputs:{ 794 service: #Service 795 ingress: #Ingress 796 } 797 outputs: service: { 798 apiVersion: "v1" 799 kind: "Service" 800 } 801 outputs: ingress: { 802 apiVersion: "extensions/v1beta1" 803 kind: "Ingress" 804 } 805 parameter: { 806 type: string 807 host: string 808 }`, 809 params: map[string]interface{}{ 810 "type": "ClusterIP", 811 "host": "example.com", 812 }, 813 expWorkload: &unstructured.Unstructured{ 814 Object: map[string]interface{}{ 815 "apiVersion": "apps/v1", 816 "kind": "Deployment", 817 "spec": map[string]interface{}{ 818 "replicas": int64(2), 819 "selector": map[string]interface{}{ 820 "matchLabels": map[string]interface{}{ 821 "app.oam.dev/component": "test"}}, 822 "template": map[string]interface{}{ 823 "metadata": map[string]interface{}{ 824 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 825 }, 826 "spec": map[string]interface{}{ 827 "containers": []interface{}{map[string]interface{}{ 828 "envFrom": []interface{}{map[string]interface{}{ 829 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 830 }}, 831 "image": "website:0.1", 832 "name": "main", 833 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}}, 834 }, 835 traitName: "t2", 836 expAssObjs: map[string]runtime.Object{ 837 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 838 Object: map[string]interface{}{ 839 "apiVersion": "v1", 840 "kind": "ConfigMap", 841 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 842 }, 843 "t2service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service"}}, 844 "t2ingress": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "Ingress"}}, 845 }, 846 }, 847 "outputs trait with no params": { 848 traitTemplate: ` 849 outputs: hpa: { 850 apiVersion: "autoscaling/v2beta2" 851 kind: "HorizontalPodAutoscaler" 852 metadata: name: context.name 853 spec: { 854 minReplicas: parameter.min 855 maxReplicas: parameter.max 856 } 857 } 858 parameter: { 859 min: *1 | int 860 max: *10 | int 861 }`, 862 params: nil, 863 expWorkload: &unstructured.Unstructured{ 864 Object: map[string]interface{}{ 865 "apiVersion": "apps/v1", 866 "kind": "Deployment", 867 "spec": map[string]interface{}{ 868 "replicas": int64(2), 869 "selector": map[string]interface{}{ 870 "matchLabels": map[string]interface{}{ 871 "app.oam.dev/component": "test"}}, 872 "template": map[string]interface{}{ 873 "metadata": map[string]interface{}{ 874 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 875 }, 876 "spec": map[string]interface{}{ 877 "containers": []interface{}{map[string]interface{}{ 878 "envFrom": []interface{}{map[string]interface{}{ 879 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 880 }}, 881 "image": "website:0.1", 882 "name": "main", 883 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}}, 884 }, 885 traitName: "t2", 886 expAssObjs: map[string]runtime.Object{ 887 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 888 Object: map[string]interface{}{ 889 "apiVersion": "v1", 890 "kind": "ConfigMap", 891 "metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 892 }, 893 "t2hpa": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "autoscaling/v2beta2", "kind": "HorizontalPodAutoscaler", 894 "metadata": map[string]interface{}{"name": "test"}, 895 "spec": map[string]interface{}{"maxReplicas": int64(10), "minReplicas": int64(1)}}}, 896 }, 897 }, 898 "parameter type doesn't match will raise error": { 899 traitTemplate: ` 900 parameter: { 901 exposePort: int 902 } 903 // trait template can have multiple outputs in one trait 904 outputs: service: { 905 apiVersion: "v1" 906 kind: "Service" 907 spec: { 908 selector: 909 app: context.name 910 ports: [ 911 { 912 port: parameter.exposePort 913 targetPort: parameter.exposePort 914 } 915 ] 916 } 917 } 918 `, 919 params: map[string]interface{}{ 920 "exposePort": "1080", 921 }, 922 hasCompileErr: true, 923 }, 924 925 "trait patch trait": { 926 traitTemplate: ` 927 patchOutputs: { 928 gameconfig: { 929 metadata: annotations: parameter 930 } 931 } 932 933 parameter: [string]: string`, 934 params: map[string]interface{}{ 935 "patch-by": "trait", 936 }, 937 expWorkload: &unstructured.Unstructured{ 938 Object: map[string]interface{}{ 939 "apiVersion": "apps/v1", 940 "kind": "Deployment", 941 "spec": map[string]interface{}{ 942 "replicas": int64(2), 943 "selector": map[string]interface{}{ 944 "matchLabels": map[string]interface{}{ 945 "app.oam.dev/component": "test"}}, 946 "template": map[string]interface{}{ 947 "metadata": map[string]interface{}{ 948 "labels": map[string]interface{}{"app.oam.dev/component": "test"}, 949 }, 950 "spec": map[string]interface{}{ 951 "containers": []interface{}{map[string]interface{}{ 952 "envFrom": []interface{}{map[string]interface{}{ 953 "configMapRef": map[string]interface{}{"name": "testgame-config"}, 954 }}, 955 "image": "website:0.1", 956 "name": "main", 957 "ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}}, 958 }, 959 expAssObjs: map[string]runtime.Object{ 960 "AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{ 961 Object: map[string]interface{}{ 962 "apiVersion": "v1", 963 "kind": "ConfigMap", 964 "metadata": map[string]interface{}{"name": "testgame-config", "annotations": map[string]interface{}{"patch-by": "trait"}}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}}, 965 }, 966 }, 967 }, 968 969 // errors 970 "invalid template(space-separated labels) will raise error": { 971 traitTemplate: ` 972 a b: c`, 973 params: map[string]interface{}{}, 974 hasCompileErr: true, 975 }, 976 "reference a non-existent variable will raise error": { 977 traitTemplate: ` 978 patch: { 979 metadata: name: none 980 } 981 982 parameter: [string]: string`, 983 params: map[string]interface{}{}, 984 hasCompileErr: true, 985 }, 986 "out-of-scope variables in patch will raise error": { 987 traitTemplate: ` 988 patchOutputs: { 989 x : "out of scope" 990 gameconfig: { 991 metadata: name: x 992 } 993 } 994 995 parameter: [string]: string`, 996 params: map[string]interface{}{}, 997 hasCompileErr: true, 998 }, 999 "using the wrong keyword in the parameter will raise error": { 1000 traitTemplate: ` 1001 patch: { 1002 metadata: annotations: parameter 1003 } 1004 1005 parameter: [string]: string`, 1006 params: map[string]interface{}{ 1007 "wrong-keyword": 5, 1008 }, 1009 hasCompileErr: true, 1010 }, 1011 "using errs": { 1012 traitTemplate: ` 1013 errs: parameter.errs 1014 parameter: { errs: [...string] }`, 1015 params: map[string]interface{}{ 1016 "errs": []string{"has error"}, 1017 }, 1018 hasCompileErr: true, 1019 }, 1020 } 1021 1022 for cassinfo, v := range tds { 1023 baseTemplate := ` 1024 output: { 1025 apiVersion: "apps/v1" 1026 kind: "Deployment" 1027 spec: { 1028 selector: matchLabels: { 1029 "app.oam.dev/component": context.name 1030 } 1031 replicas: parameter.replicas 1032 template: { 1033 metadata: labels: { 1034 "app.oam.dev/component": context.name 1035 } 1036 spec: { 1037 containers: [{ 1038 name: "main" 1039 image: parameter.image 1040 ports: [{containerPort: parameter.port}] 1041 envFrom: [{ 1042 configMapRef: name: context.name + "game-config" 1043 }] 1044 if parameter["cmd"] != _|_ { 1045 command: parameter.cmd 1046 } 1047 }] 1048 } 1049 } 1050 } 1051 } 1052 1053 outputs: gameconfig: { 1054 apiVersion: "v1" 1055 kind: "ConfigMap" 1056 metadata: { 1057 name: context.name + "game-config" 1058 } 1059 data: { 1060 enemies: parameter.enemies 1061 lives: parameter.lives 1062 } 1063 } 1064 1065 parameter: { 1066 // +usage=Which image would you like to use for your service 1067 // +short=i 1068 image: *"website:0.1" | string 1069 // +usage=Commands to run in the container 1070 cmd?: [...string] 1071 replicas: *1 | int 1072 lives: string 1073 enemies: string 1074 port: int 1075 } 1076 1077 ` 1078 ctx := process.NewContext(process.ContextData{ 1079 AppName: "myapp", 1080 CompName: "test", 1081 Namespace: "default", 1082 AppRevisionName: "myapp-v1", 1083 }) 1084 wt := NewWorkloadAbstractEngine("-", &packages.PackageDiscover{}) 1085 if err := wt.Complete(ctx, baseTemplate, map[string]interface{}{ 1086 "replicas": 2, 1087 "enemies": "enemies-data", 1088 "lives": "lives-data", 1089 "port": 443, 1090 }); err != nil { 1091 t.Error(err) 1092 return 1093 } 1094 td := NewTraitAbstractEngine(v.traitName, &packages.PackageDiscover{}) 1095 r := require.New(t) 1096 err := td.Complete(ctx, v.traitTemplate, v.params) 1097 if v.hasCompileErr { 1098 r.Error(err, cassinfo) 1099 continue 1100 } 1101 r.NoError(err, cassinfo) 1102 base, assists := ctx.Output() 1103 r.Equal(len(v.expAssObjs), len(assists), cassinfo) 1104 r.NotNil(base) 1105 obj, err := base.Unstructured() 1106 r.NoError(err) 1107 r.Equal(v.expWorkload, obj, cassinfo) 1108 for _, ss := range assists { 1109 got, err := ss.Ins.Unstructured() 1110 r.NoError(err, cassinfo) 1111 r.Equal(v.expAssObjs[ss.Type+ss.Name], got, "case %s , type: %s name: %s, got: %s", cassinfo, ss.Type, ss.Name, got) 1112 } 1113 } 1114 } 1115 1116 func TestWorkloadTemplateCompleteRenderOrder(t *testing.T) { 1117 testcases := map[string]struct { 1118 template string 1119 order []struct { 1120 name string 1121 content string 1122 } 1123 }{ 1124 "dict-order": { 1125 template: ` 1126 output: { 1127 kind: "Deployment" 1128 } 1129 1130 outputs: configMap :{ 1131 name: "test-configMap" 1132 } 1133 1134 outputs: ingress :{ 1135 name: "test-ingress" 1136 } 1137 1138 outputs: service :{ 1139 name: "test-service" 1140 } 1141 `, 1142 order: []struct { 1143 name string 1144 content string 1145 }{{ 1146 name: "configMap", 1147 content: "name: \"test-configMap\"\n", 1148 }, { 1149 name: "ingress", 1150 content: "name: \"test-ingress\"\n", 1151 }, { 1152 name: "service", 1153 content: "name: \"test-service\"\n", 1154 }}, 1155 }, 1156 "non-dict-order": { 1157 template: ` 1158 output: { 1159 name: "base" 1160 } 1161 outputs: route :{ 1162 name: "test-route" 1163 } 1164 1165 outputs: service :{ 1166 name: "test-service" 1167 } 1168 `, 1169 order: []struct { 1170 name string 1171 content string 1172 }{{ 1173 name: "route", 1174 content: "name: \"test-route\"\n", 1175 }, { 1176 name: "service", 1177 content: "name: \"test-service\"\n", 1178 }}, 1179 }, 1180 } 1181 for k, v := range testcases { 1182 wd := NewWorkloadAbstractEngine(k, &packages.PackageDiscover{}) 1183 ctx := process.NewContext(process.ContextData{ 1184 AppName: "myapp", 1185 CompName: k, 1186 Namespace: "default", 1187 AppRevisionName: "myapp-v1", 1188 }) 1189 err := wd.Complete(ctx, v.template, map[string]interface{}{}) 1190 assert.NoError(t, err) 1191 _, assists := ctx.Output() 1192 for i, ss := range assists { 1193 assert.Equal(t, ss.Name, v.order[i].name) 1194 s, err := ss.Ins.String() 1195 assert.NoError(t, err) 1196 assert.Equal(t, s, v.order[i].content) 1197 } 1198 } 1199 } 1200 1201 func TestTraitTemplateCompleteRenderOrder(t *testing.T) { 1202 testcases := map[string]struct { 1203 template string 1204 order []struct { 1205 name string 1206 content string 1207 } 1208 }{ 1209 "dict-order": { 1210 template: ` 1211 outputs: abc :{ 1212 name: "test-abc" 1213 } 1214 1215 outputs: def :{ 1216 name: "test-def" 1217 } 1218 1219 outputs: ghi :{ 1220 name: "test-ghi" 1221 } 1222 `, 1223 order: []struct { 1224 name string 1225 content string 1226 }{{ 1227 name: "abc", 1228 content: "name: \"test-abc\"\n", 1229 }, { 1230 name: "def", 1231 content: "name: \"test-def\"\n", 1232 }, { 1233 name: "ghi", 1234 content: "name: \"test-ghi\"\n", 1235 }}, 1236 }, 1237 "non-dict-order": { 1238 template: ` 1239 outputs: zyx :{ 1240 name: "test-zyx" 1241 } 1242 1243 outputs: lmn :{ 1244 name: "test-lmn" 1245 } 1246 1247 outputs: abc :{ 1248 name: "test-abc" 1249 } 1250 `, 1251 order: []struct { 1252 name string 1253 content string 1254 }{{ 1255 name: "zyx", 1256 content: "name: \"test-zyx\"\n", 1257 }, { 1258 name: "lmn", 1259 content: "name: \"test-lmn\"\n", 1260 }, { 1261 name: "abc", 1262 content: "name: \"test-abc\"\n", 1263 }}, 1264 }, 1265 } 1266 for k, v := range testcases { 1267 td := NewTraitAbstractEngine(k, &packages.PackageDiscover{}) 1268 ctx := process.NewContext(process.ContextData{ 1269 AppName: "myapp", 1270 CompName: k, 1271 Namespace: "default", 1272 AppRevisionName: "myapp-v1", 1273 }) 1274 err := td.Complete(ctx, v.template, map[string]interface{}{}) 1275 assert.NoError(t, err) 1276 _, assists := ctx.Output() 1277 for i, ss := range assists { 1278 assert.Equal(t, ss.Name, v.order[i].name) 1279 s, err := ss.Ins.String() 1280 assert.NoError(t, err) 1281 assert.Equal(t, s, v.order[i].content) 1282 } 1283 } 1284 } 1285 1286 func TestCheckHealth(t *testing.T) { 1287 cases := map[string]struct { 1288 tpContext map[string]interface{} 1289 healthTemp string 1290 parameter interface{} 1291 exp bool 1292 }{ 1293 "normal-equal": { 1294 tpContext: map[string]interface{}{ 1295 "output": map[string]interface{}{ 1296 "status": map[string]interface{}{ 1297 "readyReplicas": 4, 1298 "replicas": 4, 1299 }, 1300 }, 1301 }, 1302 healthTemp: "isHealth: context.output.status.readyReplicas == context.output.status.replicas", 1303 parameter: nil, 1304 exp: true, 1305 }, 1306 "normal-false": { 1307 tpContext: map[string]interface{}{ 1308 "output": map[string]interface{}{ 1309 "status": map[string]interface{}{ 1310 "readyReplicas": 4, 1311 "replicas": 5, 1312 }, 1313 }, 1314 }, 1315 healthTemp: "isHealth: context.output.status.readyReplicas == context.output.status.replicas", 1316 parameter: nil, 1317 exp: false, 1318 }, 1319 "array-case-equal": { 1320 tpContext: map[string]interface{}{ 1321 "output": map[string]interface{}{ 1322 "status": map[string]interface{}{ 1323 "conditions": []interface{}{ 1324 map[string]interface{}{"status": "True"}, 1325 }, 1326 }, 1327 }, 1328 }, 1329 healthTemp: `isHealth: context.output.status.conditions[0].status == "True"`, 1330 parameter: nil, 1331 exp: true, 1332 }, 1333 "parameter-false": { 1334 tpContext: map[string]interface{}{ 1335 "output": map[string]interface{}{ 1336 "status": map[string]interface{}{ 1337 "replicas": 4, 1338 }, 1339 }, 1340 "outputs": map[string]interface{}{ 1341 "my": map[string]interface{}{ 1342 "status": map[string]interface{}{ 1343 "readyReplicas": 4, 1344 }, 1345 }, 1346 }, 1347 }, 1348 healthTemp: "isHealth: context.outputs[parameter.res].status.readyReplicas == context.output.status.replicas", 1349 parameter: map[string]string{ 1350 "res": "my", 1351 }, 1352 exp: true, 1353 }, 1354 } 1355 for message, ca := range cases { 1356 healthy, err := checkHealth(ca.tpContext, ca.healthTemp, ca.parameter) 1357 assert.NoError(t, err, message) 1358 assert.Equal(t, ca.exp, healthy, message) 1359 } 1360 } 1361 1362 func TestGetStatus(t *testing.T) { 1363 cases := map[string]struct { 1364 tpContext map[string]interface{} 1365 parameter interface{} 1366 statusTemp string 1367 expMessage string 1368 }{ 1369 "field-with-array-and-outputs": { 1370 tpContext: map[string]interface{}{ 1371 "outputs": map[string]interface{}{ 1372 "service": map[string]interface{}{ 1373 "spec": map[string]interface{}{ 1374 "type": "NodePort", 1375 "clusterIP": "10.0.0.1", 1376 "ports": []interface{}{ 1377 map[string]interface{}{ 1378 "port": 80, 1379 }, 1380 }, 1381 }, 1382 }, 1383 "ingress": map[string]interface{}{ 1384 "rules": []interface{}{ 1385 map[string]interface{}{ 1386 "host": "example.com", 1387 }, 1388 }, 1389 }, 1390 }, 1391 }, 1392 statusTemp: `message: "type: " + context.outputs.service.spec.type + " clusterIP:" + context.outputs.service.spec.clusterIP + " ports:" + "\(context.outputs.service.spec.ports[0].port)" + " domain:" + context.outputs.ingress.rules[0].host`, 1393 expMessage: "type: NodePort clusterIP:10.0.0.1 ports:80 domain:example.com", 1394 }, 1395 "complex status": { 1396 tpContext: map[string]interface{}{ 1397 "outputs": map[string]interface{}{ 1398 "ingress": map[string]interface{}{ 1399 "spec": map[string]interface{}{ 1400 "rules": []interface{}{ 1401 map[string]interface{}{ 1402 "host": "example.com", 1403 }, 1404 }, 1405 }, 1406 "status": map[string]interface{}{ 1407 "loadBalancer": map[string]interface{}{ 1408 "ingress": []interface{}{ 1409 map[string]interface{}{ 1410 "ip": "10.0.0.1", 1411 }, 1412 }, 1413 }, 1414 }, 1415 }, 1416 }, 1417 }, 1418 statusTemp: `if len(context.outputs.ingress.status.loadBalancer.ingress) > 0 { 1419 message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + context.outputs.ingress.status.loadBalancer.ingress[0].ip 1420 } 1421 if len(context.outputs.ingress.status.loadBalancer.ingress) == 0 { 1422 message: "No loadBalancer found, visiting by using 'vela port-forward " + context.appName + " --route'\n" 1423 }`, 1424 expMessage: "Visiting URL: example.com, IP: 10.0.0.1", 1425 }, 1426 "status use parameter field": { 1427 tpContext: map[string]interface{}{ 1428 "outputs": map[string]interface{}{ 1429 "test-name": map[string]interface{}{ 1430 "spec": map[string]interface{}{ 1431 "type": "NodePort", 1432 "clusterIP": "10.0.0.1", 1433 "ports": []interface{}{ 1434 map[string]interface{}{ 1435 "port": 80, 1436 }, 1437 }, 1438 }, 1439 }, 1440 }, 1441 }, 1442 parameter: map[string]interface{}{ 1443 "configInfo": map[string]string{ 1444 "name": "test-name", 1445 }, 1446 }, 1447 statusTemp: `message: parameter.configInfo.name + ".type: " + context.outputs["\(parameter.configInfo.name)"].spec.type`, 1448 expMessage: "test-name.type: NodePort", 1449 }, 1450 "import package in template": { 1451 tpContext: map[string]interface{}{ 1452 "outputs": map[string]interface{}{ 1453 "service": map[string]interface{}{ 1454 "spec": map[string]interface{}{ 1455 "type": "NodePort", 1456 "clusterIP": "10.0.0.1", 1457 "ports": []interface{}{ 1458 map[string]interface{}{ 1459 "port": 80, 1460 }, 1461 }, 1462 }, 1463 }, 1464 "ingress": map[string]interface{}{ 1465 "rules": []interface{}{ 1466 map[string]interface{}{ 1467 "host": "example.com", 1468 }, 1469 }, 1470 }, 1471 }, 1472 }, 1473 statusTemp: `import "strconv" 1474 message: "ports: " + strconv.FormatInt(context.outputs.service.spec.ports[0].port,10)`, 1475 expMessage: "ports: 80", 1476 }, 1477 } 1478 for message, ca := range cases { 1479 gotMessage, err := getStatusMessage(&packages.PackageDiscover{}, ca.tpContext, ca.statusTemp, ca.parameter) 1480 assert.NoError(t, err, message) 1481 assert.Equal(t, ca.expMessage, gotMessage, message) 1482 } 1483 } 1484 1485 func TestTraitPatchSingleOutput(t *testing.T) { 1486 baseTemplate := ` 1487 output: { 1488 apiVersion: "apps/v1" 1489 kind: "Deployment" 1490 spec: selector: matchLabels: "app.oam.dev/component": context.name 1491 } 1492 1493 outputs: gameconfig: { 1494 apiVersion: "v1" 1495 kind: "ConfigMap" 1496 metadata: name: context.name + "game-config" 1497 data: {} 1498 } 1499 1500 outputs: sideconfig: { 1501 apiVersion: "v1" 1502 kind: "ConfigMap" 1503 metadata: name: context.name + "side-config" 1504 data: {} 1505 } 1506 1507 parameter: {} 1508 ` 1509 traitTemplate := ` 1510 patchOutputs: sideconfig: data: key: "val" 1511 parameter: {} 1512 ` 1513 ctx := process.NewContext(process.ContextData{ 1514 AppName: "myapp", 1515 CompName: "test", 1516 Namespace: "default", 1517 AppRevisionName: "myapp-v1", 1518 }) 1519 wt := NewWorkloadAbstractEngine("-", &packages.PackageDiscover{}) 1520 if err := wt.Complete(ctx, baseTemplate, map[string]interface{}{}); err != nil { 1521 t.Error(err) 1522 return 1523 } 1524 td := NewTraitAbstractEngine("single-patch", &packages.PackageDiscover{}) 1525 r := require.New(t) 1526 err := td.Complete(ctx, traitTemplate, map[string]string{}) 1527 r.NoError(err) 1528 base, assists := ctx.Output() 1529 r.NotNil(base) 1530 r.Equal(2, len(assists)) 1531 got, err := assists[1].Ins.Unstructured() 1532 r.NoError(err) 1533 val, ok, err := unstructured.NestedString(got.Object, "data", "key") 1534 r.NoError(err) 1535 r.True(ok) 1536 r.Equal("val", val) 1537 } 1538 1539 func TestTraitCompleteErrorCases(t *testing.T) { 1540 cases := map[string]struct { 1541 ctx wfprocess.Context 1542 traitName string 1543 template string 1544 params map[string]interface{} 1545 err string 1546 }{ 1547 "patch trait": { 1548 ctx: process.NewContext(process.ContextData{}), 1549 template: ` 1550 patch: { 1551 // +patchKey=name 1552 spec: template: spec: containers: [parameter] 1553 } 1554 parameter: { 1555 name: string 1556 image: string 1557 command?: [...string] 1558 }`, 1559 err: "patch trait patch trait into an invalid workload", 1560 }, 1561 } 1562 for k, v := range cases { 1563 td := NewTraitAbstractEngine(k, &packages.PackageDiscover{}) 1564 err := td.Complete(v.ctx, v.template, v.params) 1565 assert.Error(t, err) 1566 assert.Contains(t, err.Error(), v.err) 1567 } 1568 }