diff --git a/docs/en/application.md b/docs/en/application.md index 90f820874..b360e57d1 100644 --- a/docs/en/application.md +++ b/docs/en/application.md @@ -86,7 +86,50 @@ Hence, the `settings` section of `backend` only supports two parameters: `image` The similar extensible abstraction mechanism also applies to traits. For example, `name: autoscaler` in `frontend` means its trait specification (i.e. `properties` section) will be enforced by a `TraitDefinition` object named `autoscaler` as below: -> TBD: a autoscaler TraitDefinition (HPA) +```yaml +apiVersion: core.oam.dev/v1alpha2 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "configure k8s HPA for Deployment" + name: hpa +spec: + appliesToWorkloads: + - webservice + - worker + schematic: + cue: + template: | + outputs: hpa: { + apiVersion: "autoscaling/v2beta2" + kind: "HorizontalPodAutoscaler" + metadata: name: context.name + spec: { + scaleTargetRef: { + apiVersion: "apps/v1" + kind: "Deployment" + name: context.name + } + minReplicas: parameter.min + maxReplicas: parameter.max + metrics: [{ + type: "Resource" + resource: { + name: "cpu" + target: { + type: "Utilization" + averageUtilization: parameter.cpuUtil + } + } + }] + } + } + parameter: { + min: *1 | int + max: *10 | int + cpuUtil: *50 | int + } +``` All the definition objects are expected to be defined and installed by platform team. The end users will only focus on `Application` resource (either render it by tools or author it manually). diff --git a/docs/examples/registry/hpa.cue b/docs/examples/registry/hpa.cue new file mode 100644 index 000000000..c24b9f080 --- /dev/null +++ b/docs/examples/registry/hpa.cue @@ -0,0 +1,29 @@ +outputs: hpa: { + apiVersion: "autoscaling/v2beta2" + kind: "HorizontalPodAutoscaler" + metadata: name: context.name + spec: { + scaleTargetRef: { + apiVersion: "apps/v1" + kind: "Deployment" + name: context.name + } + minReplicas: parameter.min + maxReplicas: parameter.max + metrics: [{ + type: "Resource" + resource: { + name: "cpu" + target: { + type: "Utilization" + averageUtilization: parameter.cpuUtil + } + } + }] + } +} +parameter: { + min: *1 | int + max: *10 | int + cpuUtil: *50 | int +} diff --git a/docs/examples/registry/hpa.yaml b/docs/examples/registry/hpa.yaml new file mode 100644 index 000000000..7f174692c --- /dev/null +++ b/docs/examples/registry/hpa.yaml @@ -0,0 +1,43 @@ +apiVersion: core.oam.dev/v1alpha2 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: "configure k8s HPA for Deployment" + name: hpa +spec: + appliesToWorkloads: + - webservice + - worker + schematic: + cue: + template: | + outputs: hpa: { + apiVersion: "autoscaling/v2beta2" + kind: "HorizontalPodAutoscaler" + metadata: name: context.name + spec: { + scaleTargetRef: { + apiVersion: "apps/v1" + kind: "Deployment" + name: context.name + } + minReplicas: parameter.min + maxReplicas: parameter.max + metrics: [{ + type: "Resource" + resource: { + name: "cpu" + target: { + type: "Utilization" + averageUtilization: parameter.cpuUtil + } + } + }] + } + } + parameter: { + min: *1 | int + max: *10 | int + cpuUtil: *50 | int + } + diff --git a/pkg/dsl/definition/template.go b/pkg/dsl/definition/template.go index 0df1ea882..5d0445922 100644 --- a/pkg/dsl/definition/template.go +++ b/pkg/dsl/definition/template.go @@ -69,15 +69,19 @@ func (wd *workloadDef) Complete(ctx process.Context, abstractTemplate string, pa if err := bi.AddFile("-", abstractTemplate); err != nil { return errors.WithMessagef(err, "invalid cue template of workload %s", wd.name) } + var paramFile = "parameter: {}" if params != nil { bt, err := json.Marshal(params) if err != nil { return errors.WithMessagef(err, "marshal parameter of workload %s", wd.name) } - if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil { - return errors.WithMessagef(err, "invalid parameter of workload %s", wd.name) + if string(bt) != "null" { + paramFile = fmt.Sprintf("parameter: %s", string(bt)) } } + if err := bi.AddFile("parameter", paramFile); err != nil { + return errors.WithMessagef(err, "invalid parameter of workload %s", wd.name) + } if err := bi.AddFile("-", ctx.BaseContextFile()); err != nil { return err @@ -248,16 +252,19 @@ func (td *traitDef) Complete(ctx process.Context, abstractTemplate string, param if err := bi.AddFile("-", abstractTemplate); err != nil { return errors.WithMessagef(err, "invalid template of trait %s", td.name) } + var paramFile = "parameter: {}" if params != nil { bt, err := json.Marshal(params) if err != nil { return errors.WithMessagef(err, "marshal parameter of trait %s", td.name) } - if err := bi.AddFile("parameter", fmt.Sprintf("parameter: %s", string(bt))); err != nil { - return errors.WithMessagef(err, "invalid parameter of trait %s", td.name) + if string(bt) != "null" { + paramFile = fmt.Sprintf("parameter: %s", string(bt)) } } - + if err := bi.AddFile("parameter", paramFile); err != nil { + return errors.WithMessagef(err, "invalid parameter of trait %s", td.name) + } if err := bi.AddFile("context", ctx.BaseContextFile()); err != nil { return errors.WithMessagef(err, "invalid context of trait %s", td.name) } diff --git a/pkg/dsl/definition/template_test.go b/pkg/dsl/definition/template_test.go index 28dd9ab32..efba6172d 100644 --- a/pkg/dsl/definition/template_test.go +++ b/pkg/dsl/definition/template_test.go @@ -103,6 +103,23 @@ parameter: { }, 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)}}}, }, + { + 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)}}}, + }, } for _, v := range testCases { @@ -496,6 +513,57 @@ parameter: { "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)}}}, + }, + }, } for cassinfo, v := range tds {