allow multiple outputs for workloaddefintion

This commit is contained in:
天元
2021-01-29 11:39:44 +08:00
parent 10076f8516
commit aed2494875
6 changed files with 440 additions and 1 deletions
+14
View File
@@ -0,0 +1,14 @@
apiVersion: core.oam.dev/v1alpha2
kind: Application
metadata:
name: testapp4
spec:
components:
- name: express-server4
type: webserver
settings:
cmd:
- node
- server.js
image: oamdev/testapp:v1
port: 8080
+89
View File
@@ -0,0 +1,89 @@
apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: webserver
annotations:
definition.oam.dev/description: "webserver was composed by deployment and service"
spec:
definitionRef:
name: deployments.apps
extension:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if context["config"] != _|_ {
env: context.config
}
ports: [{
containerPort: parameter.port
}]
if parameter["cpu"] != _|_ {
resources: {
limits:
cpu: parameter.cpu
requests:
cpu: parameter.cpu
}
}
}]
}
}
}
}
// workload can have extra object composition by using 'outputs' keyword
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector: {
"app.oam.dev/component": context.name
}
ports: [
{
port: parameter.port
targetPort: parameter.port
},
]
}
}
parameter: {
image: string
cmd?: [...string]
port: *80 | int
env?: [...{
name: string
value?: string
valueFrom?: {
secretKeyRef: {
name: string
key: string
}
}
}]
cpu?: string
}
@@ -0,0 +1,123 @@
package application
import (
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/pkg/appfile/config"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/dsl/process"
"github.com/oam-dev/kubevela/pkg/oam"
)
const (
// OAMApplicationLabel is application's metadata label
OAMApplicationLabel = "application.oam.dev"
)
// GenerateApplicationConfiguration converts an appFile to applicationConfig & Components
func (p *Parser) GenerateApplicationConfiguration(app *Appfile, ns string) (*v1alpha2.ApplicationConfiguration,
[]*v1alpha2.Component, error) {
appconfig := &v1alpha2.ApplicationConfiguration{}
appconfig.SetGroupVersionKind(v1alpha2.ApplicationConfigurationGroupVersionKind)
appconfig.Name = app.Name
appconfig.Namespace = ns
appconfig.Spec.Components = []v1alpha2.ApplicationConfigurationComponent{}
if appconfig.Labels == nil {
appconfig.Labels = map[string]string{}
}
appconfig.Labels[OAMApplicationLabel] = app.Name
var components []*v1alpha2.Component
for _, wl := range app.Workloads {
pCtx := process.NewContext(wl.Name)
userConfig := wl.GetUserConfigName()
if userConfig != "" {
cg := config.Configmap{Client: p.client}
// TODO(wonderflow): envName should not be namespace when we have serverside env
var envName = ns
data, err := cg.GetConfigData(config.GenConfigMapName(app.Name, wl.Name, userConfig), envName)
if err != nil {
return nil, nil, err
}
pCtx.SetConfigs(data)
}
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 := evalWorkloadWithContext(pCtx, wl)
if err != nil {
return nil, nil, err
}
comp.Name = wl.Name
acComp.ComponentName = comp.Name
for _, sc := range wl.Scopes {
acComp.Scopes = append(acComp.Scopes, v1alpha2.ComponentScope{ScopeReference: v1alpha1.TypedReference{
APIVersion: sc.GVK.GroupVersion().String(),
Kind: sc.GVK.Kind,
Name: sc.Name,
}})
}
comp.Namespace = ns
if comp.Labels == nil {
comp.Labels = map[string]string{}
}
comp.Labels[OAMApplicationLabel] = app.Name
comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind)
components = append(components, comp)
appconfig.Spec.Components = append(appconfig.Spec.Components, *acComp)
}
return appconfig, components, nil
}
// evalWorkloadWithContext evaluate the workload's template to generate component and ACComponent
func evalWorkloadWithContext(pCtx process.Context, wl *Workload) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
base, assists := pCtx.Output()
componentWorkload, err := base.Unstructured()
if err != nil {
return nil, nil, err
}
workloadType := wl.Type
labels := componentWorkload.GetLabels()
if labels == nil {
labels = map[string]string{oam.WorkloadTypeLabel: workloadType}
} else {
labels[oam.WorkloadTypeLabel] = workloadType
}
componentWorkload.SetLabels(labels)
component := &v1alpha2.Component{}
component.Spec.Workload.Object = componentWorkload
acComponent := &v1alpha2.ApplicationConfigurationComponent{}
acComponent.Traits = []v1alpha2.ComponentTrait{}
for _, assist := range assists {
tr, err := assist.Ins.Unstructured()
if err != nil {
return nil, nil, err
}
if assist.Type != "" && assist.Type != definition.ExtraWorkloadObj {
tr.SetLabels(map[string]string{oam.TraitTypeLabel: assist.Type})
}
acComponent.Traits = append(acComponent.Traits, v1alpha2.ComponentTrait{
Trait: runtime.RawExtension{
Object: tr,
},
})
}
return component, acComponent, nil
}
@@ -25,6 +25,8 @@ import (
"net/http/httptest"
"time"
"github.com/stretchr/testify/assert"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo"
@@ -154,6 +156,9 @@ var _ = Describe("Test Application Controller", func() {
wd := &v1alpha2.WorkloadDefinition{}
wDDefJson, _ := yaml.YAMLToJSON([]byte(wDDefYaml))
webserverwd := &v1alpha2.WorkloadDefinition{}
webserverwdJson, _ := yaml.YAMLToJSON([]byte(webserverYaml))
td := &v1alpha2.TraitDefinition{}
tDDefJson, _ := yaml.YAMLToJSON([]byte(tDDefYaml))
@@ -176,6 +181,9 @@ var _ = Describe("Test Application Controller", func() {
Expect(json.Unmarshal(sdDefJson, sd)).Should(BeNil())
Expect(k8sClient.Create(ctx, sd.DeepCopy())).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
Expect(json.Unmarshal(webserverwdJson, webserverwd)).Should(BeNil())
Expect(k8sClient.Create(ctx, webserverwd.DeepCopy())).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
})
AfterEach(func() {
})
@@ -326,6 +334,94 @@ var _ = Describe("Test Application Controller", func() {
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
})
It("app-with-composedworkload-trait will create workload and trait", func() {
compName := "myweb-composed-3"
expDeployment := getExpDeployment(compName)
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-test-with-composedworkload-trait",
},
}
var appname = "app-with-composedworkload-trait"
appWithComposedWorkload := appwithNoTrait.DeepCopy()
appWithComposedWorkload.Spec.Components[0].WorkloadType = "webserver"
appWithComposedWorkload.SetName(appname)
appWithComposedWorkload.Spec.Components[0].Traits = []v1alpha2.ApplicationTrait{
{
Name: "scaler",
Properties: runtime.RawExtension{Raw: []byte(`{"replicas":2}`)},
},
}
appWithComposedWorkload.Spec.Components[0].Name = compName
appWithComposedWorkload.SetNamespace(ns.Name)
Expect(k8sClient.Create(ctx, ns)).Should(BeNil())
app := appWithComposedWorkload.DeepCopy()
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
appKey := client.ObjectKey{
Name: app.Name,
Namespace: app.Namespace,
}
reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey})
By("Check App running successfully")
checkApp := &v1alpha2.Application{}
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
Expect(checkApp.Status.Phase).Should(Equal(v1alpha2.ApplicationRunning))
By("Check AppConfig and trait created as expected")
appConfig := &v1alpha2.ApplicationConfiguration{}
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: app.Namespace,
Name: app.Name,
}, appConfig)).Should(BeNil())
Expect(len(appConfig.Spec.Components[0].Traits)).Should(BeEquivalentTo(2))
gotTrait := unstructured.Unstructured{}
By("Check the first trait should be service")
expectServiceTrait := unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"spec": map[string]interface{}{
"ports": []interface{}{
map[string]interface{}{"port": int64(80), "targetPort": int64(80)},
},
"selector": map[string]interface{}{
"app.oam.dev/component": compName,
},
},
}}
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[0].Trait.Raw, &gotTrait)).Should(BeNil())
fmt.Println(cmp.Diff(expectServiceTrait, gotTrait))
Expect(assert.ObjectsAreEqual(expectServiceTrait, gotTrait)).Should(BeTrue())
By("Check the second trait should be scaler")
gotTrait = unstructured.Unstructured{}
Expect(json.Unmarshal(appConfig.Spec.Components[0].Traits[1].Trait.Raw, &gotTrait)).Should(BeNil())
Expect(gotTrait).Should(BeEquivalentTo(expectScalerTrait))
By("Check component created as expected")
component := &v1alpha2.Component{}
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: app.Namespace,
Name: compName,
}, component)).Should(BeNil())
Expect(component.ObjectMeta.Labels).Should(BeEquivalentTo(map[string]string{"application.oam.dev": appname}))
Expect(component.ObjectMeta.OwnerReferences[0].Name).Should(BeEquivalentTo(appname))
Expect(component.ObjectMeta.OwnerReferences[0].Kind).Should(BeEquivalentTo("Application"))
Expect(component.ObjectMeta.OwnerReferences[0].APIVersion).Should(BeEquivalentTo("core.oam.dev/v1alpha2"))
Expect(component.ObjectMeta.OwnerReferences[0].Controller).Should(BeEquivalentTo(pointer.BoolPtr(true)))
gotD := &v1.Deployment{}
expDeployment.ObjectMeta.Labels["workload.oam.dev/type"] = "webserver"
expDeployment.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{{ContainerPort: 80}}
Expect(json.Unmarshal(component.Spec.Workload.Raw, gotD)).Should(BeNil())
fmt.Println(cmp.Diff(expDeployment, gotD))
Expect(gotD).Should(BeEquivalentTo(expDeployment))
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
})
It("app-with-trait-and-scope will create workload, trait and scope", func() {
expDeployment := getExpDeployment("myweb4")
ns := &corev1.Namespace{
@@ -767,6 +863,97 @@ spec:
cmd?: [...string]
}
`
webserverYaml = `apiVersion: core.oam.dev/v1alpha2
kind: WorkloadDefinition
metadata:
name: webserver
annotations:
definition.oam.dev/description: "webserver was composed by deployment and service"
spec:
definitionRef:
name: deployments.apps
extension:
template: |
output: {
apiVersion: "apps/v1"
kind: "Deployment"
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if context["config"] != _|_ {
env: context.config
}
ports: [{
containerPort: parameter.port
}]
if parameter["cpu"] != _|_ {
resources: {
limits:
cpu: parameter.cpu
requests:
cpu: parameter.cpu
}
}
}]
}
}
}
}
// workload can have extra object composition by using 'outputs' keyword
outputs: service: {
apiVersion: "v1"
kind: "Service"
spec: {
selector: {
"app.oam.dev/component": context.name
}
ports: [
{
port: parameter.port
targetPort: parameter.port
},
]
}
}
parameter: {
image: string
cmd?: [...string]
port: *80 | int
env?: [...{
name: string
value?: string
valueFrom?: {
secretKeyRef: {
name: string
key: string
}
}
}]
cpu?: string
}
`
wDDefWithHealthYaml = `
apiVersion: core.oam.dev/v1alpha2
+24
View File
@@ -29,6 +29,12 @@ const (
PatchFieldName = "patch"
)
const (
// ExtraWorkloadObj defines the extra workload obj from a workloadDefinition,
// e.g. a workload composed by deployment and service, the service will be marked as ExtraWorkloadObj
ExtraWorkloadObj = "ExtraWorkloadObj"
)
var (
metadataAccessor = meta.NewAccessor()
)
@@ -99,6 +105,24 @@ func (wd *workloadDef) Complete(ctx process.Context) error {
return errors.WithMessagef(err, "workloadDef %s new base", wd.name)
}
ctx.SetBase(base)
// we will support outputs for workload composition, and it will become trait in AppConfig.
outputs := inst.Lookup(OutputsFieldName)
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, "parse WorkloadDefinition %s outputs(%s)", wd.name, fieldInfo.Name)
}
// extra workload CR object will not have type
ctx.PutAssistants(process.Assistant{Ins: other, Type: ExtraWorkloadObj})
}
}
}
return nil
}
+3 -1
View File
@@ -20,7 +20,9 @@ type Context interface {
// Assistant are objects rendered by definition template.
type Assistant struct {
Ins model.Instance
Ins model.Instance
// Type will be used to mark definition label for OAM runtime to get the CRD
// It's now required for trait and main workload object. Extra workload CR object will not have the type.
Type string
}