Extend application's capabilities by trait definition (#742)

* add dsl pkg

* realize context capabilities for application

* fix golint

* fix check-diff

* upgrade vela server sample

* comment group

* Definition Reference be Optional & fix  three-part trait crd apply without namespace

* Improve samples
This commit is contained in:
Jian.Li
2020-12-13 20:00:06 -08:00
committed by GitHub
parent ef9c26ffec
commit e882a650ae
17 changed files with 1293 additions and 142 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ type WorkloadDefinitionList struct {
// A TraitDefinitionSpec defines the desired state of a TraitDefinition.
type TraitDefinitionSpec struct {
// Reference to the CustomResourceDefinition that defines this trait kind.
Reference DefinitionReference `json:"definitionRef"`
Reference DefinitionReference `json:"definitionRef,omitempty"`
// Revision indicates whether a trait is aware of component revision
// +optional
@@ -66,8 +66,6 @@ spec:
workloadRefPath:
description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object
type: string
required:
- definitionRef
type: object
type: object
served: true
+144
View File
@@ -0,0 +1,144 @@
# Application Example
In this Demo, Application application-sample will be converted to appconfig and component
The fields in the application spec come from the parametes defined in the definition template
, so we must install Definition at first
Step 1: Install Workload Definition & Trait Definition
```
kubectl apply -f template.yaml
```
Step 2: Create a sample application in the cluster
```
kubectl apply -f application-sample.yaml
```
Step 3: View the application status
```
kubectl get -f application-sample.yaml -oyaml
// You can see the following
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"core.oam.dev/v1alpha2","kind":"Application","metadata":{"annotations":{},"name":"application-sample","namespace":"oam-test"},"spec":{"template":"services:\n myweb:\n type: worker\n image: \"busybox\"\n cmd:\n - sleep\n - \"1000\"\n scaler:\n replicas: 10"}}
name: application-sample
namespace: oam-test
spec:
services:
myweb:
cmd:
- sleep
- "1000"
image: busybox
scaler:
replicas: 10
service:
http:
server: 80
sidecar:
command:
- sleep
- "1000"
image: busybox
name: test-sidecar
type: worker
status:
conditions:
- lastTransitionTime: "2020-12-02T12:12:52Z"
reason: Available
status: "True"
type: Parsed
- lastTransitionTime: "2020-12-02T12:12:52Z"
reason: Available
status: "True"
type: Built
- lastTransitionTime: "2020-12-02T12:12:52Z"
reason: Available
status: "True"
type: Applied
status: running
```
Step 4: View the oam CR generated by application
```
kubectl get appconfig/application-sample -oyaml
// appconfig is as follows
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
metadata:
labels:
application.oam.dev: application-sample
name: application-sample
namespace: oam-test
ownerReferences:
- apiVersion: core.oam.dev/v1alpha2
controller: true
kind: Application
name: application-sample
uid: dca7acc3-664c-422b-aa52-4fe012e37974
spec:
components:
- componentName: myweb
traits:
- trait:
apiVersion: v1
kind: Service
metadata:
name: myweb
spec:
ports:
- port: 80
targetPort: 80
selector:
app: myweb
kubectl get component/myweb -oyaml
// component is as follows
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
labels:
application.oam.dev: application-sample
name: myweb
namespace: oam-test
ownerReferences:
- apiVersion: core.oam.dev/v1alpha2
controller: true
kind: Application
name: application-sample
uid: dca7acc3-664c-422b-aa52-4fe012e37974
spec:
workload:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 10
selector:
matchLabels:
app.oam.dev/component: myweb
template:
metadata:
labels:
app: myweb
app.oam.dev/component: myweb
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb
- command:
- sleep
- "1000"
image: busybox
name: test-sidecar
```
+311 -125
View File
@@ -1,146 +1,332 @@
# Vela Server
## example
# Definition Docs
In this Demo, Application application-sample will be converted to appconfig and component
## Reserved word
### patch
Perform the CUE AND operation with the content declared by 'patch' and workload cr
The fields in the application spec come from the parametes defined in the definition template
, so we must install Definition at first
### output
Generate a new cr, which is generally associated with workload cr
Step 1: Install Workload Definition & Trait Definition
## Workload Definition
The following workload definition is to generate a deployment
```
kubectl apply -f template.yaml
```
Step 2: Create a sample application in the cluster
```
kubectl apply -f application-sample.yaml
```
Step 3: View the application status
```
kubectl get -f application-sample.yaml -oyaml
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: worker
annotations:
definition.oam.dev/description: "Long-running scalable backend worker without network endpoint"
spec:
definitionRef:
name: deployments.apps
extension:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
// You can see the following
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
cmd?: [...string]
}
```
If defined an application as follows
```
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"core.oam.dev/v1alpha2","kind":"Application","metadata":{"annotations":{},"name":"application-sample","namespace":"oam-test"},"spec":{"template":"services:\n myweb:\n type: worker\n image: \"busybox\"\n cmd:\n - sleep\n - \"1000\"\n scaler:\n replicas: 10"}}
name: application-sample
namespace: oam-test
spec:
services:
myweb:
type: worker
image: "busybox"
cmd:
- sleep
- "1000"
image: busybox
scaler:
replicas: 10
type: worker
status:
conditions:
- lastTransitionTime: "2020-12-02T12:12:52Z"
reason: Available
status: "True"
type: Parsed
- lastTransitionTime: "2020-12-02T12:12:52Z"
reason: Available
status: "True"
type: Built
- lastTransitionTime: "2020-12-02T12:12:52Z"
reason: Available
status: "True"
type: Applied
status: running
```
Step 4: View the oam CR generated by application
we will get a deployment
```
kubectl get appconfig/application-sample -oyaml
// appconfig is as follows
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
metadata:
labels:
application.oam.dev: application-sample
name: application-sample
namespace: oam-test
ownerReferences:
- apiVersion: core.oam.dev/v1alpha2
controller: true
kind: Application
name: application-sample
uid: dca7acc3-664c-422b-aa52-4fe012e37974
apiVersion: apps/v1
kind: Deployment
spec:
components:
- componentName: myweb
traits:
- trait:
apiVersion: core.oam.dev/v1alpha2
kind: ManualScalerTrait
spec:
replicaCount: 10
status:
conditions:
- lastTransitionTime: "2020-12-02T12:12:52Z"
reason: Successfully reconciled resource
status: "True"
type: Synced
dependency: {}
workloads:
- componentName: myweb
componentRevisionName: myweb-v1
traits:
- traitRef:
apiVersion: core.oam.dev/v1alpha2
kind: ManualScalerTrait
name: myweb-trait-78fdd467d6
workloadRef:
apiVersion: apps/v1
kind: Deployment
name: myweb
kubectl get component/myweb -oyaml
// component is as follows
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
labels:
application.oam.dev: application-sample
name: myweb
namespace: oam-test
ownerReferences:
- apiVersion: core.oam.dev/v1alpha2
controller: true
kind: Application
name: application-sample
uid: dca7acc3-664c-422b-aa52-4fe012e37974
spec:
workload:
apiVersion: apps/v1
kind: Deployment
selector:
matchLabels:
app.oam.dev/component: myweb
template:
metadata:
labels:
app.oam.dev/component: myweb
spec:
selector:
matchLabels:
app.oam.dev/component: myweb
template:
metadata:
labels:
app.oam.dev/component: myweb
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb
status:
latestRevision:
name: myweb-v1
revision: 1
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb
```
## Service Trait Definition
Define a trait Definition that appends service to workload(worker) , as shown below
```
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "service the app"
name: service
spec:
appliesToWorkloads:
- webservice
- worker
definitionRef:
name: service.v1
extension:
template: |-
patch: {spec: template: metadata: labels: app: context.name}
output: {
apiVersion: "v1"
kind: "Service"
metadata: name: context.name
spec: {
selector: app: context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
}
]
}
}
parameter: {
http: [string]: int
}
```
If add service capability to the application, as follows
```
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: application-sample
spec:
services:
myweb:
type: worker
image: "busybox"
cmd:
- sleep
- "1000"
service:
http:
server: 80
```
we will get a new deployment and service
```
// origin deployment template add labels
apiVersion: apps/v1
kind: Deployment
spec:
selector:
matchLabels:
app.oam.dev/component: myweb
template:
metadata:
labels:
// add label app
app: myweb
app.oam.dev/component: myweb
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb
---
apiVersion: v1
kind: Service
metadata:
name: myweb
spec:
ports:
- port: 80
targetPort: 80
selector:
app: myweb
```
## Scaler Trait Definition
Define a trait Definition that scale workload(worker) replicas
```
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "Manually scale the app"
name: scaler
spec:
appliesToWorkloads:
- webservice
- worke
extension:
template: |-
patch: {
spec: replicas: parameter.replicas
}
parameter: {
//+short=r
replicas: *1 | int
}
```
If add scaler capability to the application, as follows
```
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: application-sample
spec:
services:
myweb:
type: worker
image: "busybox"
cmd:
- sleep
- "1000"
service:
http:
server: 80
scaler:
replicas: 10
```
The deployment replicas will be scale to 10
```
apiVersion: apps/v1
kind: Deployment
spec:
selector:
matchLabels:
app.oam.dev/component: myweb
// scale to 10
replicas: 10
template:
metadata:
labels:
// add label app
app: myweb
app.oam.dev/component: myweb
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb
```
## Sidecar Trait Definition
Define a trait Definition that append containers to workload(worker)
```
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "add sidecar to the app"
name: sidecar
spec:
appliesToWorkloads:
- webservice
- worke
extension:
template: |-
_containers: context.input.spec.template.spec.containers+[parameter]
patch: {
spec: template: spec: containers: _containers
}
parameter: {
name: string
image: string
command?: [...string]
}
```
If add sidercar capability to the application, as follows
```
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: application-sample
spec:
services:
myweb:
type: worker
image: "busybox"
cmd:
- sleep
- "1000"
service:
http:
server: 80
scaler:
replicas: 10
sidercar:
name: "sidecar-test"
image: "nginx"
```
The deployment updated as follows
```
apiVersion: apps/v1
kind: Deployment
spec:
selector:
matchLabels:
app.oam.dev/component: myweb
// scale to 10
replicas: 10
template:
metadata:
labels:
// add label app
app: myweb
app.oam.dev/component: myweb
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb
- name: sidecar-test
image: nginx
```
@@ -11,4 +11,10 @@ spec:
- sleep
- "1000"
scaler:
replicas: 10
replicas: 10
sidercar:
name: "sidecar-test"
image: "nginx"
service:
http:
server: 80
+66 -9
View File
@@ -58,19 +58,76 @@ spec:
appliesToWorkloads:
- webservice
- worker
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
extension:
template: |-
output: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
patch: {
spec: replicas: parameter.replicas
}
parameter: {
//+short=r
replicas: *1 | int
}
---
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "add sidecar to the app"
name: sidecar
spec:
appliesToWorkloads:
- webservice
- worker
extension:
template: |-
_containers: context.input.spec.template.spec.containers+[parameter]
patch: {
spec: template: spec: containers: _containers
}
parameter: {
name: string
image: string
command?: [...string]
}
---
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "service the app"
name: service
spec:
appliesToWorkloads:
- webservice
- worker
definitionRef:
name: services
extension:
template: |-
patch: {spec: template: metadata: labels: app: context.name}
output: {
apiVersion: "v1"
kind: "Service"
metadata: name: context.name
spec: {
selector: app: context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
}
]
}
}
parameter: {
http: [string]: int
}
---
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
name: services
namespace: default
spec:
definitionRef:
name: services
@@ -65,8 +65,6 @@ spec:
workloadRefPath:
description: WorkloadRefPath indicates where/if a trait accepts a workloadRef object
type: string
required:
- definitionRef
type: object
type: object
version: v1alpha2
@@ -19,6 +19,7 @@ package application
import (
"context"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/go-logr/logr"
kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -69,6 +70,8 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (result ctrl.Result, gerr error
app.Status.Phase = v1alpha2.ApplicationRendering
handler := &reter{r, app, applog}
app.Status.Conditions = []v1alpha1.Condition{}
applog.Info("parse template")
// parse template
appParser := parser.NewParser(template.GetHanler(fclient.NewDefinitionClient(r.Client)))
@@ -9,9 +9,11 @@ import (
"cuelang.org/go/cue/build"
cueparser "cuelang.org/go/cue/parser"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/parser"
"github.com/oam-dev/kubevela/pkg/dsl/process"
)
type builder struct {
@@ -26,7 +28,7 @@ const (
// Build template to applicationConfig & Component
func Build(ns string, app *parser.Appfile) (*v1alpha2.ApplicationConfiguration, []*v1alpha2.Component, error) {
b := &builder{app}
return b.Complete(ns)
return b.CompleteWithContext(ns)
}
// Complete: builder complete rendering
@@ -44,7 +46,6 @@ func (b *builder) Complete(ns string) (*v1alpha2.ApplicationConfiguration, []*v1
componets := []*v1alpha2.Component{}
for _, wl := range b.app.Services() {
compCtx := map[string]string{"name": wl.Name()}
component, err := wl.Eval(newLoader(compCtx))
@@ -78,6 +79,75 @@ func (b *builder) Complete(ns string) (*v1alpha2.ApplicationConfiguration, []*v1
return appconfig, componets, nil
}
func (b *builder) CompleteWithContext(ns string) (*v1alpha2.ApplicationConfiguration, []*v1alpha2.Component, error) {
appconfig := &v1alpha2.ApplicationConfiguration{}
appconfig.SetGroupVersionKind(v1alpha2.ApplicationConfigurationGroupVersionKind)
appconfig.Name = b.app.Name()
appconfig.Namespace = ns
appconfig.Spec.Components = []v1alpha2.ApplicationConfigurationComponent{}
if appconfig.Labels == nil {
appconfig.Labels = map[string]string{}
}
appconfig.Labels[OamApplicationLabel] = b.app.Name()
componets := []*v1alpha2.Component{}
for _, wl := range b.app.Services() {
pCtx := process.NewContext(wl.Name())
if err := wl.EvalContext(pCtx); err != nil {
return nil, nil, err
}
for _, tr := range wl.Traits() {
if err := tr.EvalContext(pCtx); err != nil {
return nil, nil, err
}
}
comp, acComp, err := generateOAM(pCtx)
if err != nil {
return nil, nil, err
}
comp.Name = wl.Name()
acComp.ComponentName = comp.Name
comp.Namespace = ns
if comp.Labels == nil {
comp.Labels = map[string]string{}
}
comp.Labels[OamApplicationLabel] = b.app.Name()
comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind)
componets = append(componets, comp)
appconfig.Spec.Components = append(appconfig.Spec.Components, *acComp)
}
return appconfig, componets, nil
}
func generateOAM(pCtx process.Context) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
base, assists := pCtx.Output()
componetWorkload, err := base.Object(nil)
if err != nil {
return nil, nil, err
}
component := &v1alpha2.Component{}
component.Spec.Workload.Object = componetWorkload
acComponent := &v1alpha2.ApplicationConfigurationComponent{}
acComponent.Traits = []v1alpha2.ComponentTrait{}
for _, assist := range assists {
traitRef, err := assist.Object(nil)
if err != nil {
return nil, nil, err
}
acComponent.Traits = append(acComponent.Traits, v1alpha2.ComponentTrait{
Trait: runtime.RawExtension{
Object: traitRef,
},
})
}
return component, acComponent, nil
}
type loader struct {
files map[string]*ast.File
err error
@@ -11,6 +11,8 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/template"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/dsl/process"
)
// Render is cue render
@@ -67,6 +69,11 @@ func (wl *Workload) Eval(render Render) (*v1alpha2.Component, error) {
return component, nil
}
// EvalContext eval workload template and set result to context
func (wl *Workload) EvalContext(ctx process.Context) error {
return definition.NewWDTemplater("-", wl.template).Params(wl.params).Complete(ctx)
}
// Trait is ComponentTrait
type Trait struct {
name string
@@ -120,6 +127,11 @@ func (trait *Trait) Eval(render Render) ([]v1alpha2.ComponentTrait, error) {
return compTraits, nil
}
// EvalContext eval trait template and set result to context
func (trait *Trait) EvalContext(ctx process.Context) error {
return definition.NewTDTemplater("-", trait.template).Params(trait.params).Complete(ctx)
}
// Appfile describle application
type Appfile struct {
name string
@@ -229,6 +229,7 @@ func (r *components) renderTrait(ctx context.Context, ct v1alpha2.ComponentTrait
traitDef, err := util.FetchTraitDefinition(ctx, r.client, r.dm, t)
if err != nil {
if apierrors.IsNotFound(err) {
t.SetNamespace(ac.GetNamespace())
return t, util.GetDummyTraitDefinition(t), nil
}
return nil, nil, errors.Wrapf(err, errFmtGetTraitDefinition, t.GetAPIVersion(), t.GetKind(), t.GetName())
+162
View File
@@ -0,0 +1,162 @@
package definition
import (
"encoding/json"
"fmt"
"cuelang.org/go/cue"
"cuelang.org/go/cue/build"
"github.com/pkg/errors"
"github.com/oam-dev/kubevela/pkg/dsl/model"
"github.com/oam-dev/kubevela/pkg/dsl/process"
)
// Template defines Definition's Render interface
type Template interface {
Params(params interface{}) Template
Complete(ctx process.Context) error
}
type def struct {
name string
templ string
params interface{}
}
type workloadDef struct {
def
}
// NewWDTemplater create Workload Definition templater
func NewWDTemplater(name, templ string) Template {
return &workloadDef{
def: def{
name: name,
templ: templ,
params: nil,
},
}
}
// Params set definition's params
func (wd *workloadDef) Params(params interface{}) Template {
wd.params = params
return wd
}
// Complete do workload definition's rendering
func (wd *workloadDef) Complete(ctx process.Context) error {
bi := build.NewContext().NewInstance("", nil)
if err := bi.AddFile("-", wd.templ); err != nil {
return err
}
if wd.params != nil {
bt, _ := json.Marshal(wd.params)
if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil {
return err
}
}
if err := bi.AddFile("-", ctx.Compile("context")); err != nil {
return err
}
insts := cue.Build([]*build.Instance{bi})
for _, inst := range insts {
if err := inst.Value().Err(); err != nil {
return errors.WithMessagef(err, "workloadDef %s eval", wd.name)
}
output := inst.Lookup("output")
base, err := model.NewBase(output)
if err != nil {
return errors.WithMessagef(err, "workloadDef %s new base", wd.name)
}
ctx.SetBase(base)
}
return nil
}
type traitDef struct {
def
}
// NewTDTemplater create Trait Definition templater
func NewTDTemplater(name, templ string) Template {
return &traitDef{
def: def{
name: name,
templ: templ,
},
}
}
// Params set definition's params
func (td *traitDef) Params(params interface{}) Template {
td.params = params
return td
}
// Complete do trait definition's rendering
func (td *traitDef) Complete(ctx process.Context) error {
bi := build.NewContext().NewInstance("", nil)
if err := bi.AddFile("-", td.templ); err != nil {
return err
}
if td.params != nil {
bt, _ := json.Marshal(td.params)
if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil {
return err
}
}
if err := bi.AddFile("f", ctx.Compile("context")); err != nil {
return err
}
insts := cue.Build([]*build.Instance{bi})
for _, inst := range insts {
if err := inst.Value().Err(); err != nil {
return errors.WithMessagef(err, "traitDef %s build", td.name)
}
output := inst.Lookup("output")
if output.Exists() {
other, err := model.NewOther(output)
if err != nil {
return errors.WithMessagef(err, "traitDef %s new Assist", td.name)
}
ctx.PutAssistants(other)
}
outputs := inst.Lookup("outputs")
st, err := outputs.Struct()
if err == nil {
for i := 0; i < st.Len(); i++ {
fieldInfo := st.Field(i)
if fieldInfo.IsDefinition || fieldInfo.IsHidden || fieldInfo.IsOptional {
continue
}
other, err := model.NewOther(fieldInfo.Value)
if err != nil {
return errors.WithMessagef(err, "traitDef %s new Assists(%s)", td.name, fieldInfo.Name)
}
ctx.PutAssistants(other)
}
}
patcher := inst.Lookup("patch")
if patcher.Exists() {
base, _ := ctx.Output()
p, err := model.NewOther(patcher)
if err != nil {
return errors.WithMessagef(err, "traitDef %s patcher NewOther", td.name)
}
if err := base.Unity(p); err != nil {
return err
}
}
}
return nil
}
+124
View File
@@ -0,0 +1,124 @@
package definition
import (
"testing"
"github.com/bmizerany/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/pkg/dsl/process"
)
func TestWDTemplate(t *testing.T) {
testCases := []struct {
templ string
params map[string]interface{}
expectObj runtime.Object
}{
{
templ: `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
spec: replicas: parameter.replicas
}
parameter: {
replicas: *1 | int
}
`,
params: map[string]interface{}{
"replicas": 2,
},
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)}}},
},
}
for _, v := range testCases {
ctx := process.NewContext("test")
wt := NewWDTemplater("-", v.templ)
if err := wt.Params(v.params).Complete(ctx); err != nil {
t.Error(err)
return
}
base, assists := ctx.Output()
assert.Equal(t, 0, len(assists))
assert.Equal(t, false, base == nil)
baseObj, err := base.Object(nil)
assert.Equal(t, nil, err)
assert.Equal(t, v.expectObj, baseObj)
}
}
func TestTDTemplate(t *testing.T) {
baseTemplate := `
output:{
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
spec: {
replicas: parameter.replicas
template: spec: {
containers: [{image: "website:0.1",name:"main"}]
}
}
}
parameter: {
replicas: *1 | int
}
`
ctx := process.NewContext("test")
wt := NewWDTemplater("-", baseTemplate)
if err := wt.Params(map[string]interface{}{
"replicas": 2,
}).Complete(ctx); err != nil {
t.Error(err)
return
}
tds := []struct {
templ string
params map[string]interface{}
}{
{
templ: `
patch: {
_containers: context.input.spec.template.spec.containers+[parameter]
sepc: template: spec: containers: _containers
}
parameter: {
name: string
image: string
command?: [...string]
}
`,
params: map[string]interface{}{
"name": "sidecar",
"image": "metrics-agent:0.2",
},
},
}
for _, v := range tds {
td := NewTDTemplater("-", v.templ)
if err := td.Params(v.params).Complete(ctx); err != nil {
t.Error(err)
return
}
}
base, assists := ctx.Output()
assert.Equal(t, 0, len(assists))
assert.Equal(t, false, base == nil)
obj, err := base.Object(nil)
assert.Equal(t, nil, err)
expect := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{"name": "test"}, "sepc": map[string]interface{}{"template": map[string]interface{}{"spec": map[string]interface{}{"containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"}, map[string]interface{}{"image": "metrics-agent:0.2", "name": "sidecar"}}}}}, "spec": map[string]interface{}{"replicas": int64(2), "template": map[string]interface{}{"spec": map[string]interface{}{"containers": []interface{}{map[string]interface{}{"image": "website:0.1", "name": "main"}}}}}}}
assert.Equal(t, expect, obj)
}
+195
View File
@@ -0,0 +1,195 @@
package model
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/token"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
// Instance defines Model Interface
type Instance interface {
String() string
Object(m *runtime.Scheme) (runtime.Object, error)
IsBase() bool
Unity(other Instance) error
}
type instance struct {
v string
base bool
}
// String return instance's cue format string
func (inst *instance) String() string {
return inst.v
}
// IsBase indicate whether the instance is base model
func (inst *instance) IsBase() bool {
return inst.base
}
// Object convert to runtime.Object
func (inst *instance) Object(m *runtime.Scheme) (runtime.Object, error) {
var r cue.Runtime
cueInst, err := r.Compile("-", inst.v)
if err != nil {
return nil, err
}
o := new(unstructured.Unstructured)
jsonv, err := cueInst.Value().MarshalJSON()
if err != nil {
return nil, err
}
if err := o.UnmarshalJSON(jsonv); err != nil {
return nil, err
}
if m != nil {
object, err := m.New(o.GetObjectKind().GroupVersionKind())
if err == nil {
if err := json.Unmarshal(jsonv, object); err != nil {
return nil, err
}
return object, nil
}
if !runtime.IsNotRegisteredError(err) {
return nil, err
}
}
return o, nil
}
// Unity implement unity operations between instances
func (inst *instance) Unity(other Instance) error {
var r cue.Runtime
raw, err := r.Compile("-", inst.v)
if err != nil {
return err
}
o, err := r.Compile("-", other.String())
if err != nil {
return err
}
pv, err := print(raw.Value().Unify(o.Value()))
if err != nil {
return err
}
inst.v = pv
return nil
}
// NewBase create a base instance
func NewBase(v cue.Value) (Instance, error) {
vs, err := openPrint(v)
if err != nil {
return nil, err
}
return &instance{
v: vs,
base: true,
}, nil
}
// NewOther create a non-base instance
func NewOther(v cue.Value) (Instance, error) {
vs, err := openPrint(v)
if err != nil {
return nil, err
}
return &instance{
v: vs,
}, nil
}
func print(v cue.Value) (string, error) {
v = v.Eval()
syopts := []cue.Option{cue.All(), cue.DisallowCycles(true), cue.ResolveReferences(true)}
var w bytes.Buffer
useSep := false
format := func(name string, n ast.Node) error {
if name != "" {
// TODO: make this relative to DIR
fmt.Fprintf(&w, "// %s\n", filepath.Base(name))
} else if useSep {
fmt.Println("// ---")
}
useSep = true
b, err := format.Node(toFile(n))
if err != nil {
return err
}
_, err = w.Write(b)
return err
}
if err := format("", v.Syntax(syopts...)); err != nil {
return "", err
}
instStr := w.String()
return instStr, nil
}
func toFile(n ast.Node) *ast.File {
switch x := n.(type) {
case nil:
return nil
case *ast.StructLit:
return &ast.File{Decls: x.Elts}
case ast.Expr:
ast.SetRelPos(x, token.NoSpace)
return &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: x}}}
case *ast.File:
return x
default:
panic(fmt.Sprintf("Unsupported node type %T", x))
}
}
func openPrint(v cue.Value) (string, error) {
sysopts := []cue.Option{cue.All(), cue.DisallowCycles(true), cue.ResolveReferences(true)}
f := toFile(v.Syntax(sysopts...))
for _, decl := range f.Decls {
listOpen(decl)
}
ret, err := format.Node(f)
return string(ret), err
}
func listOpen(expr ast.Node) {
switch v := expr.(type) {
case *ast.Field:
listOpen(v.Value)
case *ast.StructLit:
for _, elt := range v.Elts {
listOpen(elt)
}
case *ast.BinaryExpr:
listOpen(v.X)
listOpen(v.Y)
case *ast.EmbedDecl:
listOpen(v.Expr)
case *ast.Comprehension:
listOpen(v.Value)
case *ast.ListLit:
for _, elt := range v.Elts {
listOpen(elt)
}
if len(v.Elts) > 0 {
if _, ok := v.Elts[len(v.Elts)-1].(*ast.Ellipsis); !ok {
v.Elts = append(v.Elts, &ast.Ellipsis{})
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@
package model
import (
"testing"
"cuelang.org/go/cue"
"github.com/bmizerany/assert"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func TestInstance(t *testing.T) {
testCases := []struct {
src string
gvk schema.GroupVersionKind
}{{
src: `apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: "test"
`,
gvk: schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}},
}
for _, v := range testCases {
var r cue.Runtime
inst, err := r.Compile("-", v.src)
if err != nil {
t.Error(err)
return
}
base, err := NewBase(inst.Value())
if err != nil {
t.Error(err)
return
}
baseObj, err := base.Object(nil)
if err != nil {
t.Error(err)
return
}
assert.Equal(t, v.gvk, baseObj.GetObjectKind().GroupVersionKind())
assert.Equal(t, true, base.IsBase())
other, err := NewOther(inst.Value())
if err != nil {
t.Error(err)
return
}
otherObj, err := other.Object(nil)
if err != nil {
t.Error(err)
return
}
assert.Equal(t, v.gvk, otherObj.GetObjectKind().GroupVersionKind())
assert.Equal(t, false, other.IsBase())
}
}
+89
View File
@@ -0,0 +1,89 @@
package process
import (
"encoding/json"
"fmt"
"strings"
"unicode"
"github.com/oam-dev/kubevela/pkg/dsl/model"
)
// Context defines Rendering Context Interface
type Context interface {
SetBase(base model.Instance)
PutAssistants(insts ...model.Instance)
Output() (model.Instance, []model.Instance)
Compile(label string) string
}
type context struct {
name string
configs map[string]interface{}
base model.Instance
assistants []model.Instance
}
// NewContext create render context
func NewContext(name string) Context {
return &context{
name: name,
configs: map[string]interface{}{},
assistants: []model.Instance{},
}
}
// SetBase set context base model
func (ctx *context) SetBase(base model.Instance) {
ctx.base = base
}
// PutAssistants add Assist model to context
func (ctx *context) PutAssistants(insts ...model.Instance) {
ctx.assistants = append(ctx.assistants, insts...)
}
// Compile return cue format string of context
func (ctx *context) Compile(label string) string {
var buff string
buff += fmt.Sprintf("name: \"%s\"\n", ctx.name)
if ctx.base != nil {
buff += fmt.Sprintf("input: %s\n", structMashal(ctx.base.String()))
}
if len(ctx.configs) > 0 {
bt, _ := json.Marshal(ctx.configs)
buff += "configs: " + string(bt)
}
if label != "" {
buff = fmt.Sprintf("%s: %s", label, structMashal(buff))
}
return buff
}
// Output return models of context
func (ctx *context) Output() (model.Instance, []model.Instance) {
return ctx.base, ctx.assistants
}
func structMashal(v string) string {
skip := false
v = strings.TrimFunc(v, func(r rune) bool {
if !skip {
if unicode.IsSpace(r) {
return true
}
skip = true
}
return false
})
if strings.HasPrefix(v, "{") {
return v
}
return fmt.Sprintf("{%s}", v)
}
+43
View File
@@ -0,0 +1,43 @@
package process
import (
"testing"
"cuelang.org/go/cue"
"github.com/bmizerany/assert"
"github.com/oam-dev/kubevela/pkg/dsl/model"
)
func TestContext(t *testing.T) {
baseTemplate := `
image: "myserver"
`
var r cue.Runtime
inst, err := r.Compile("-", baseTemplate)
if err != nil {
t.Error(err)
return
}
base, err := model.NewBase(inst.Value())
if err != nil {
t.Error(err)
return
}
ctx := NewContext("myctx")
ctx.SetBase(base)
ctxInst, err := r.Compile("-", ctx.Compile("context"))
if err != nil {
t.Error(err)
return
}
gName, err := ctxInst.Lookup("context", "name").String()
assert.Equal(t, nil, err)
assert.Equal(t, "myctx", gName)
inputJs, err := ctxInst.Lookup("context", "input").MarshalJSON()
assert.Equal(t, nil, err)
assert.Equal(t, `{"image":"myserver"}`, string(inputJs))
}