mirror of https://github.com/kubevela/kubevela.git
1695 lines
48 KiB
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)
|
|
})
|
|
}
|
|
}
|