mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
allow multiple outputs for workloaddefintion
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user