kubevela/pkg/cue/definition/template_test.go

1695 lines
48 KiB
Go

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package definition
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
wfprocess "github.com/kubevela/workflow/pkg/cue/process"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
func TestWorkloadTemplateComplete(t *testing.T) {
testCases := map[string]struct {
workloadTemplate string
params map[string]interface{}
expectObj runtime.Object
expAssObjs map[string]runtime.Object
category types.CapabilityCategory
hasCompileErr bool
}{
"only contain an output": {
workloadTemplate: `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
spec: replicas: parameter.replicas
}
parameter: {
replicas: *1 | int
type: string
host: string
}
`,
params: map[string]interface{}{
"replicas": 2,
"type": "ClusterIP",
"host": "example.com",
},
expectObj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{"name": "test"},
"spec": map[string]interface{}{"replicas": int64(2)},
}},
hasCompileErr: false,
},
"contain output and outputs": {
workloadTemplate: `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
spec: replicas: parameter.replicas
}
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata: name: context.name
spec: type: parameter.type
}
outputs: ingress: {
apiVersion: "extensions/v1beta1"
kind: "Ingress"
metadata: name: context.name
spec: rules: [{host: parameter.host}]
}
parameter: {
replicas: *1 | int
type: string
host: string
}
`,
params: map[string]interface{}{
"replicas": 2,
"type": "ClusterIP",
"host": "example.com",
},
expectObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"},
"spec": map[string]interface{}{
"replicas": int64(2),
},
},
},
expAssObjs: map[string]runtime.Object{
"service": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"},
"spec": map[string]interface{}{"type": "ClusterIP"},
},
},
"ingress": &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{}{
"host": "example.com",
},
},
},
},
},
},
hasCompileErr: false,
},
"output needs context appRevision": {
workloadTemplate: `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: context.name
annotations: "revision.oam.dev": context.appRevision
}
spec: replicas: parameter.replicas
}
parameter: {
replicas: *1 | int
type: string
host: string
}
`,
params: map[string]interface{}{
"replicas": 2,
"type": "ClusterIP",
"host": "example.com",
},
expectObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{
"name": "test", "annotations": map[string]interface{}{
"revision.oam.dev": "myapp-v1",
},
}, "spec": map[string]interface{}{
"replicas": int64(2),
},
},
},
hasCompileErr: false,
},
"output needs context replicas": {
workloadTemplate: `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: context.name
}
spec: replicas: parameter.replicas
}
parameter: {
replicas: *1 | int
}
`,
params: nil,
expectObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"},
"spec": map[string]interface{}{
"replicas": int64(1),
},
},
},
hasCompileErr: false,
},
"parameter type doesn't match will raise error": {
workloadTemplate: `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
spec: replicas: parameter.replicas
}
parameter: {
replicas: *1 | int
type: string
host: string
}
`,
params: map[string]interface{}{
"replicas": "2",
"type": "ClusterIP",
"host": "example.com",
},
expectObj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{"name": "test"},
"spec": map[string]interface{}{"replicas": int64(2)},
}},
hasCompileErr: true,
},
"cluster version info": {
workloadTemplate: `
output:{
if context.clusterVersion.minor < 19 {
apiVersion: "networking.k8s.io/v1beta1"
}
if context.clusterVersion.minor >= 19 {
apiVersion: "networking.k8s.io/v1"
}
"kind": "Ingress",
}
`,
params: map[string]interface{}{},
expectObj: &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "networking.k8s.io/v1",
"kind": "Ingress",
}},
},
}
for _, v := range testCases {
ctx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: "test",
Namespace: "default",
AppRevisionName: "myapp-v1",
ClusterVersion: types.ClusterVersion{Minor: "19+"},
})
wt := NewWorkloadAbstractEngine("testWorkload")
err := wt.Complete(ctx, v.workloadTemplate, v.params)
hasError := err != nil
assert.Equal(t, v.hasCompileErr, hasError)
if v.hasCompileErr {
continue
}
base, assists := ctx.Output()
assert.Equal(t, len(v.expAssObjs), len(assists))
assert.NotNil(t, base)
baseObj, err := base.Unstructured()
assert.Equal(t, nil, err)
assert.Equal(t, v.expectObj, baseObj)
for _, ss := range assists {
assert.Equal(t, AuxiliaryWorkload, ss.Type)
got, err := ss.Ins.Unstructured()
assert.NoError(t, err)
assert.Equal(t, got, v.expAssObjs[ss.Name])
}
}
}
func TestTraitTemplateComplete(t *testing.T) {
tds := map[string]struct {
traitName string
traitTemplate string
params map[string]interface{}
expWorkload *unstructured.Unstructured
expAssObjs map[string]runtime.Object
hasCompileErr bool
}{
"patch trait": {
traitTemplate: `
patch: {
// +patchKey=name
spec: template: spec: containers: [parameter]
}
parameter: {
name: string
image: string
command?: [...string]
}`,
params: map[string]interface{}{
"name": "sidecar",
"image": "metrics-agent:0.2",
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}},
map[string]interface{}{"image": "metrics-agent:0.2", "name": "sidecar"}}}}}},
},
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
},
},
"patch trait with strategic merge": {
traitTemplate: `
patch: {
// +patchKey=name
spec: template: spec: {
// +patchStrategy=retainKeys
containers: [{
name: "main"
image: parameter.image
ports: [{containerPort: parameter.port}]
envFrom: [{
configMapRef: name: context.name + "game-config"
}]
if parameter["command"] != _|_ {
command: parameter.command
}
}]
}
}
parameter: {
image: string
port: int
command?: [...string]
}
`,
params: map[string]interface{}{
"image": "website:0.2",
"port": 8080,
"command": []string{"server", "start"},
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.2",
"name": "main",
"command": []interface{}{"server", "start"},
"ports": []interface{}{map[string]interface{}{"containerPort": int64(8080)}}},
}}}}},
},
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
},
},
"patch trait with json merge patch": {
traitTemplate: `
parameter: {...}
// +patchStrategy=jsonMergePatch
patch: parameter
`,
params: map[string]interface{}{
"spec": map[string]interface{}{
"replicas": 5,
"template": map[string]interface{}{
"spec": nil,
},
},
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(5),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
}}}},
},
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
},
},
"patch trait with json patch": {
traitTemplate: `
parameter: {operations: [...{...}]}
// +patchStrategy=jsonPatch
patch: parameter
`,
params: map[string]interface{}{
"operations": []map[string]interface{}{
{"op": "replace", "path": "/spec/replicas", "value": 5},
{"op": "remove", "path": "/spec/template/spec"},
},
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(5),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
}}}},
},
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
},
},
"patch trait with invalid json patch": {
traitTemplate: `
parameter: {patch: [...{...}]}
// +patchStrategy=jsonPatch
patch: parameter
`,
params: map[string]interface{}{
"patch": []map[string]interface{}{
{"op": "what", "path": "/spec/replicas", "value": 5},
},
},
hasCompileErr: true,
},
"patch trait with replace": {
traitTemplate: `
parameter: {
name: string
ports: [...int]
}
patch: spec: template: spec: {
// +patchKey=name
containers: [{
name: parameter.name
// +patchStrategy=replace
ports: [for k in parameter.ports {containerPort: k}]
}]
}
`,
params: map[string]interface{}{
"name": "main",
"ports": []int{80, 8443},
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{
map[string]interface{}{"containerPort": int64(80)},
map[string]interface{}{"containerPort": int64(8443)},
}},
}}}}},
},
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
},
},
"output trait": {
traitTemplate: `
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata: name: context.name
spec: type: parameter.type
}
parameter: {
type: string
}`,
params: map[string]interface{}{
"type": "ClusterIP",
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}},
},
traitName: "t1",
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
"t1service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"type": "ClusterIP"}}},
},
},
"outputs trait": {
traitTemplate: `
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata: name: context.name
spec: type: parameter.type
}
outputs: ingress: {
apiVersion: "extensions/v1beta1"
kind: "Ingress"
metadata: name: context.name
spec: rules: [{host: parameter.host}]
}
parameter: {
type: string
host: string
}`,
params: map[string]interface{}{
"type": "ClusterIP",
"host": "example.com",
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}},
},
traitName: "t2",
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
"t2service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test"}, "spec": map[string]interface{}{"type": "ClusterIP"}}},
"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{}{
"host": "example.com",
}}}}},
},
},
"outputs trait with context appRevision": {
traitTemplate: `
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata: {
name: context.name
annotations: "revision.oam.dev": context.appRevision
}
spec: type: parameter.type
}
outputs: ingress: {
apiVersion: "extensions/v1beta1"
kind: "Ingress"
metadata: name: context.name
spec: rules: [{host: parameter.host}]
}
parameter: {
type: string
host: string
}`,
params: map[string]interface{}{
"type": "ClusterIP",
"host": "example.com",
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}},
},
traitName: "t2",
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
"t2service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service", "metadata": map[string]interface{}{"name": "test", "annotations": map[string]interface{}{
"revision.oam.dev": "myapp-v1",
}}, "spec": map[string]interface{}{"type": "ClusterIP"}}},
"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{}{
"host": "example.com",
}}}}},
},
},
"simple data passing": {
traitTemplate: `
parameter: {
domain: string
path: string
exposePort: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector:
app: context.name
ports: [
{
port: parameter.exposePort
targetPort: context.output.spec.template.spec.containers[0].ports[0].containerPort
}
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
metadata:
name: context.name
labels: config: context.outputs.gameconfig.data.enemies
spec: {
rules: [{
host: parameter.domain
http: {
paths: [{
path: parameter.path
backend: {
serviceName: context.name
servicePort: parameter.exposePort
}
}]
}
}]
}
}`,
params: map[string]interface{}{
"domain": "example.com",
"path": "ping",
"exposePort": 1080,
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}},
},
traitName: "t3",
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
"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"}}}},
"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"}}}}}}}},
},
},
"outputs trait with schema": {
traitTemplate: `
#Service:{
apiVersion: string
kind: string
}
#Ingress:{
apiVersion: string
kind: string
}
outputs:{
service: #Service
ingress: #Ingress
}
outputs: service: {
apiVersion: "v1"
kind: "Service"
}
outputs: ingress: {
apiVersion: "extensions/v1beta1"
kind: "Ingress"
}
parameter: {
type: string
host: string
}`,
params: map[string]interface{}{
"type": "ClusterIP",
"host": "example.com",
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}},
},
traitName: "t2",
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
"t2service": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "v1", "kind": "Service"}},
"t2ingress": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "extensions/v1beta1", "kind": "Ingress"}},
},
},
"outputs trait with no params": {
traitTemplate: `
outputs: hpa: {
apiVersion: "autoscaling/v2beta2"
kind: "HorizontalPodAutoscaler"
metadata: name: context.name
spec: {
minReplicas: parameter.min
maxReplicas: parameter.max
}
}
parameter: {
min: *1 | int
max: *10 | int
}`,
params: nil,
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}},
},
traitName: "t2",
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config"}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
"t2hpa": &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "autoscaling/v2beta2", "kind": "HorizontalPodAutoscaler",
"metadata": map[string]interface{}{"name": "test"},
"spec": map[string]interface{}{"maxReplicas": int64(10), "minReplicas": int64(1)}}},
},
},
"parameter type doesn't match will raise error": {
traitTemplate: `
parameter: {
exposePort: int
}
// trait template can have multiple outputs in one trait
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector:
app: context.name
ports: [
{
port: parameter.exposePort
targetPort: parameter.exposePort
}
]
}
}
`,
params: map[string]interface{}{
"exposePort": "1080",
},
hasCompileErr: true,
},
"trait patch trait": {
traitTemplate: `
patchOutputs: {
gameconfig: {
metadata: annotations: parameter
}
}
parameter: [string]: string`,
params: map[string]interface{}{
"patch-by": "trait",
},
expWorkload: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.oam.dev/component": "test"}},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app.oam.dev/component": "test"},
},
"spec": map[string]interface{}{
"containers": []interface{}{map[string]interface{}{
"envFrom": []interface{}{map[string]interface{}{
"configMapRef": map[string]interface{}{"name": "testgame-config"},
}},
"image": "website:0.1",
"name": "main",
"ports": []interface{}{map[string]interface{}{"containerPort": int64(443)}}}}}}}},
},
expAssObjs: map[string]runtime.Object{
"AuxiliaryWorkloadgameconfig": &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": "testgame-config", "annotations": map[string]interface{}{"patch-by": "trait"}}, "data": map[string]interface{}{"enemies": "enemies-data", "lives": "lives-data"}},
},
},
},
// errors
"invalid template(space-separated labels) will raise error": {
traitTemplate: `
a b: c`,
params: map[string]interface{}{},
hasCompileErr: true,
},
"reference a non-existent variable will raise error": {
traitTemplate: `
patch: {
metadata: name: none
}
parameter: [string]: string`,
params: map[string]interface{}{},
hasCompileErr: true,
},
"out-of-scope variables in patch will raise error": {
traitTemplate: `
patchOutputs: {
x : "out of scope"
gameconfig: {
metadata: name: x
}
}
parameter: [string]: string`,
params: map[string]interface{}{},
hasCompileErr: true,
},
"using the wrong keyword in the parameter will raise error": {
traitTemplate: `
patch: {
metadata: annotations: parameter
}
parameter: [string]: string`,
params: map[string]interface{}{
"wrong-keyword": 5,
},
hasCompileErr: true,
},
"using errs": {
traitTemplate: `
errs: parameter.errs
parameter: { errs: [...string] }`,
params: map[string]interface{}{
"errs": []string{"has error"},
},
hasCompileErr: true,
},
}
for cassinfo, v := range tds {
baseTemplate := `
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
replicas: parameter.replicas
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: "main"
image: parameter.image
ports: [{containerPort: parameter.port}]
envFrom: [{
configMapRef: name: context.name + "game-config"
}]
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
outputs: gameconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: context.name + "game-config"
}
data: {
enemies: parameter.enemies
lives: parameter.lives
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: *"website:0.1" | string
// +usage=Commands to run in the container
cmd?: [...string]
replicas: *1 | int
lives: string
enemies: string
port: int
}
`
ctx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: "test",
Namespace: "default",
AppRevisionName: "myapp-v1",
})
wt := NewWorkloadAbstractEngine("-")
if err := wt.Complete(ctx, baseTemplate, map[string]interface{}{
"replicas": 2,
"enemies": "enemies-data",
"lives": "lives-data",
"port": 443,
}); err != nil {
t.Error(err)
return
}
td := NewTraitAbstractEngine(v.traitName)
r := require.New(t)
err := td.Complete(ctx, v.traitTemplate, v.params)
if v.hasCompileErr {
r.Error(err, cassinfo)
continue
}
r.NoError(err, cassinfo)
base, assists := ctx.Output()
r.Equal(len(v.expAssObjs), len(assists), cassinfo)
r.NotNil(base)
obj, err := base.Unstructured()
r.NoError(err)
r.Equal(v.expWorkload, obj, cassinfo)
for _, ss := range assists {
got, err := ss.Ins.Unstructured()
r.NoError(err, cassinfo)
r.Equal(v.expAssObjs[ss.Type+ss.Name], got, "case %s , type: %s name: %s, got: %s", cassinfo, ss.Type, ss.Name, got)
}
}
}
func TestWorkloadTemplateCompleteRenderOrder(t *testing.T) {
testcases := map[string]struct {
template string
order []struct {
name string
content string
}
}{
"dict-order": {
template: `
output: {
kind: "Deployment"
}
outputs: configMap :{
name: "test-configMap"
}
outputs: ingress :{
name: "test-ingress"
}
outputs: service :{
name: "test-service"
}
`,
order: []struct {
name string
content string
}{{
name: "configMap",
content: "name: \"test-configMap\"\n",
}, {
name: "ingress",
content: "name: \"test-ingress\"\n",
}, {
name: "service",
content: "name: \"test-service\"\n",
}},
},
"non-dict-order": {
template: `
output: {
name: "base"
}
outputs: route :{
name: "test-route"
}
outputs: service :{
name: "test-service"
}
`,
order: []struct {
name string
content string
}{{
name: "route",
content: "name: \"test-route\"\n",
}, {
name: "service",
content: "name: \"test-service\"\n",
}},
},
}
for k, v := range testcases {
wd := NewWorkloadAbstractEngine(k)
ctx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: k,
Namespace: "default",
AppRevisionName: "myapp-v1",
})
err := wd.Complete(ctx, v.template, map[string]interface{}{})
assert.NoError(t, err)
_, assists := ctx.Output()
for i, ss := range assists {
assert.Equal(t, ss.Name, v.order[i].name)
s, err := ss.Ins.String()
assert.NoError(t, err)
assert.Equal(t, s, v.order[i].content)
}
}
}
func TestTraitTemplateCompleteRenderOrder(t *testing.T) {
testcases := map[string]struct {
template string
order []struct {
name string
content string
}
}{
"dict-order": {
template: `
outputs: abc :{
name: "test-abc"
}
outputs: def :{
name: "test-def"
}
outputs: ghi :{
name: "test-ghi"
}
`,
order: []struct {
name string
content string
}{{
name: "abc",
content: "name: \"test-abc\"\n",
}, {
name: "def",
content: "name: \"test-def\"\n",
}, {
name: "ghi",
content: "name: \"test-ghi\"\n",
}},
},
"non-dict-order": {
template: `
outputs: zyx :{
name: "test-zyx"
}
outputs: lmn :{
name: "test-lmn"
}
outputs: abc :{
name: "test-abc"
}
`,
order: []struct {
name string
content string
}{{
name: "zyx",
content: "name: \"test-zyx\"\n",
}, {
name: "lmn",
content: "name: \"test-lmn\"\n",
}, {
name: "abc",
content: "name: \"test-abc\"\n",
}},
},
}
for k, v := range testcases {
td := NewTraitAbstractEngine(k)
ctx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: k,
Namespace: "default",
AppRevisionName: "myapp-v1",
})
err := td.Complete(ctx, v.template, map[string]interface{}{})
assert.NoError(t, err)
_, assists := ctx.Output()
for i, ss := range assists {
assert.Equal(t, ss.Name, v.order[i].name)
s, err := ss.Ins.String()
assert.NoError(t, err)
assert.Equal(t, s, v.order[i].content)
}
}
}
func TestTraitPatchSingleOutput(t *testing.T) {
baseTemplate := `
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: selector: matchLabels: "app.oam.dev/component": context.name
}
outputs: gameconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: name: context.name + "game-config"
data: {}
}
outputs: sideconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: name: context.name + "side-config"
data: {}
}
parameter: {}
`
traitTemplate := `
patchOutputs: sideconfig: data: key: "val"
parameter: {}
`
ctx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: "test",
Namespace: "default",
AppRevisionName: "myapp-v1",
})
wt := NewWorkloadAbstractEngine("-")
if err := wt.Complete(ctx, baseTemplate, map[string]interface{}{}); err != nil {
t.Error(err)
return
}
td := NewTraitAbstractEngine("single-patch")
r := require.New(t)
err := td.Complete(ctx, traitTemplate, map[string]string{})
r.NoError(err)
base, assists := ctx.Output()
r.NotNil(base)
r.Equal(2, len(assists))
got, err := assists[1].Ins.Unstructured()
r.NoError(err)
val, ok, err := unstructured.NestedString(got.Object, "data", "key")
r.NoError(err)
r.True(ok)
r.Equal("val", val)
}
func TestTraitCompleteErrorCases(t *testing.T) {
cases := map[string]struct {
ctx wfprocess.Context
traitName string
template string
params map[string]interface{}
err string
}{
"patch trait": {
ctx: process.NewContext(process.ContextData{}),
template: `
patch: {
// +patchKey=name
spec: template: spec: containers: [parameter]
}
parameter: {
name: string
image: string
command?: [...string]
}`,
err: "patch trait patch trait into an invalid workload",
},
}
for k, v := range cases {
td := NewTraitAbstractEngine(k)
err := td.Complete(v.ctx, v.template, v.params)
assert.Error(t, err)
assert.Contains(t, err.Error(), v.err)
}
}
func TestWorkloadGetTemplateContext(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, appsv1.AddToScheme(scheme))
require.NoError(t, corev1.AddToScheme(scheme))
workload := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-workload",
"namespace": "default",
},
},
}
auxSvc := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "test-aux-svc",
"namespace": "default",
},
},
}
baseCtx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: "test",
Namespace: "default",
AppRevisionName: "myapp-v1",
})
workloadTemplate := `
output: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: "test-workload"
namespace: "default"
}
}
outputs: service: {
apiVersion: "v1"
kind: "Service"
metadata: {
name: "test-aux-svc"
namespace: "default"
}
}
`
wt := NewWorkloadAbstractEngine("testWorkload")
err := wt.Complete(baseCtx, workloadTemplate, nil)
require.NoError(t, err)
testCases := map[string]struct {
reason string
cli client.Client
ctx wfprocess.Context
wantErr bool
checkFunc func(t *testing.T, templateContext map[string]interface{})
}{
"successfully get template context": {
reason: "Should successfully get the template context with both output and outputs.",
cli: fake.NewClientBuilder().WithScheme(scheme).WithObjects(workload, auxSvc).Build(),
ctx: baseCtx,
checkFunc: func(t *testing.T, templateContext map[string]interface{}) {
require.NotNil(t, templateContext)
output, ok := templateContext[OutputFieldName]
require.True(t, ok)
outputMap, ok := output.(map[string]interface{})
require.True(t, ok)
require.Equal(t, "test-workload", outputMap["metadata"].(map[string]interface{})["name"])
outputs, ok := templateContext[OutputsFieldName]
require.True(t, ok)
outputsMap, ok := outputs.(map[string]interface{})
require.True(t, ok)
svc, ok := outputsMap["service"]
require.True(t, ok)
svcMap, ok := svc.(map[string]interface{})
require.True(t, ok)
require.Equal(t, "test-aux-svc", svcMap["metadata"].(map[string]interface{})["name"])
},
},
"resource not found": {
reason: "Should return an error when a resource is not found in the cluster.",
cli: fake.NewClientBuilder().WithScheme(scheme).Build(),
ctx: baseCtx,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
wd := &workloadDef{def: def{name: "test"}}
accessor := util.NewApplicationResourceNamespaceAccessor("default", "")
templateContext, err := wd.GetTemplateContext(tc.ctx, tc.cli, accessor)
if tc.wantErr {
require.Error(t, err, tc.reason)
} else {
require.NoError(t, err, tc.reason)
if tc.checkFunc != nil {
tc.checkFunc(t, templateContext)
}
}
})
}
}
func TestTraitGetTemplateContext(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
traitOutput := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "test-trait-output",
"namespace": "default",
},
},
}
traitName := "my-trait"
baseCtx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: "test",
Namespace: "default",
AppRevisionName: "myapp-v1",
})
traitTemplate := `
outputs: myconfig: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: "test-trait-output"
namespace: "default"
}
}
`
td := NewTraitAbstractEngine(traitName)
err := td.Complete(baseCtx, traitTemplate, nil)
require.NoError(t, err)
emptyCtx := process.NewContext(process.ContextData{
AppName: "myapp",
CompName: "test",
Namespace: "default",
AppRevisionName: "myapp-v1",
})
testCases := map[string]struct {
reason string
cli client.Client
ctx wfprocess.Context
traitName string
wantErr bool
checkFunc func(t *testing.T, templateContext map[string]interface{})
}{
"successfully get template context for trait": {
reason: "Should successfully get the template context for a trait with outputs.",
cli: fake.NewClientBuilder().WithScheme(scheme).WithObjects(traitOutput).Build(),
ctx: baseCtx,
traitName: traitName,
checkFunc: func(t *testing.T, templateContext map[string]interface{}) {
require.NotNil(t, templateContext)
outputs, ok := templateContext[OutputsFieldName]
require.True(t, ok)
outputsMap, ok := outputs.(map[string]interface{})
require.True(t, ok)
cm, ok := outputsMap["myconfig"]
require.True(t, ok)
cmMap, ok := cm.(map[string]interface{})
require.True(t, ok)
require.Equal(t, "test-trait-output", cmMap["metadata"].(map[string]interface{})["name"])
},
},
"trait resource not found": {
reason: "Should return an error when a trait's output resource is not found.",
cli: fake.NewClientBuilder().WithScheme(scheme).Build(),
ctx: baseCtx,
traitName: traitName,
wantErr: true,
},
"trait with no outputs": {
reason: "Should successfully get a context for a trait that produces no outputs.",
cli: fake.NewClientBuilder().WithScheme(scheme).Build(),
ctx: emptyCtx,
traitName: traitName,
checkFunc: func(t *testing.T, templateContext map[string]interface{}) {
require.NotNil(t, templateContext)
_, ok := templateContext[OutputsFieldName]
require.False(t, ok)
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
traitDef := &traitDef{def: def{name: tc.traitName}}
accessor := util.NewApplicationResourceNamespaceAccessor("default", "")
templateContext, err := traitDef.GetTemplateContext(tc.ctx, tc.cli, accessor)
if tc.wantErr {
require.Error(t, err, tc.reason)
} else {
require.NoError(t, err, tc.reason)
if tc.checkFunc != nil {
tc.checkFunc(t, templateContext)
}
}
})
}
}
func TestGetCommonLabels(t *testing.T) {
type want struct {
labels map[string]string
}
cases := map[string]struct {
reason string
input map[string]string
want want
}{
"TestConvert": {
reason: "Test that context labels are correctly converted to OAM labels",
input: map[string]string{
process.ContextAppName: "my-app",
process.ContextName: "my-comp",
process.ContextAppRevision: "v1",
process.ContextReplicaKey: "rep-key",
"other-label": "other-value",
},
want: want{
labels: map[string]string{
"app.oam.dev/name": "my-app",
"app.oam.dev/component": "my-comp",
"app.oam.dev/appRevision": "v1",
"app.oam.dev/replicaKey": "rep-key",
},
},
},
"TestEmpty": {
reason: "Test that an empty input map results in an empty output map",
input: map[string]string{},
want: want{
labels: map[string]string{},
},
},
"TestNoConvert": {
reason: "Test that labels with no OAM equivalent are ignored",
input: map[string]string{
"other-label": "other-value",
},
want: want{
labels: map[string]string{},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := require.New(t)
got := GetCommonLabels(tc.input)
r.Equal(tc.want.labels, got, tc.reason)
})
}
}
func TestGetBaseContextLabels(t *testing.T) {
type want struct {
labels map[string]string
}
cases := map[string]struct {
reason string
ctx wfprocess.Context
want want
}{
"TestWithAppNameAndRevision": {
reason: "Test that app name and revision are added to the base context labels",
ctx: process.NewContext(process.ContextData{
AppName: "my-app",
AppRevisionName: "v1",
CompName: "my-comp",
}),
want: want{
labels: map[string]string{
process.ContextAppName: "my-app",
process.ContextAppRevision: "v1",
process.ContextName: "my-comp",
},
},
},
"TestWithoutAppNameAndRevision": {
reason: "Test that the base context labels are returned when app name and revision are missing",
ctx: process.NewContext(process.ContextData{
CompName: "my-comp",
}),
want: want{
labels: map[string]string{
process.ContextAppName: "",
process.ContextAppRevision: "",
process.ContextName: "my-comp",
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := require.New(t)
got := GetBaseContextLabels(tc.ctx)
r.Equal(tc.want.labels, got, tc.reason)
})
}
}