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  }