generate OpenAPI schema for Helm schematic compDef (#1250)

* get JSON schema for Helm schematic ComponentDefinition

Signed-off-by: roy wang <seiwy2010@gmail.com>

* add e2e test

update doc

add unit tests

Signed-off-by: roywang <seiwy2010@gmail.com>
This commit is contained in:
Yue Wang
2021-03-23 00:16:17 +08:00
committed by GitHub
parent dbc4593b23
commit f554523196
15 changed files with 1053 additions and 25 deletions
+14 -2
View File
@@ -68,8 +68,20 @@ spec:
tag: "5.1.2"
```
Helm module workload will use data in `settings` as [Helm chart values](https://github.com/captainroy-hy/podinfo/blob/master/charts/podinfo/values.yaml).
Currently, you can learn the schema of settings by reading the `README.md` of the Helm chart, and the schema are totally align with [`values.yaml`](https://github.com/captainroy-hy/podinfo/blob/master/charts/podinfo/values.yaml) of the chart.
We will integerate the values and generate it's [openapi-v3-json-schema](https://kubevela.io/#/en/platform-engineers/openapi-v3-json-schema.md) soon.
You can learn the schema of settings by reading the `README.md` of the Helm
chart, and the schema are totally align with
[`values.yaml`](https://github.com/captainroy-hy/podinfo/blob/master/charts/podinfo/values.yaml)
of the chart.
Helm v3 has [support to validate
values](https://helm.sh/docs/topics/charts/#schema-files) in a chart's
values.yaml file with JSON schemas.
Vela will try to fetch the `values.schema.json` file from the Chart archive and
[save the schema into a
ConfigMap](https://kubevela.io/#/en/platform-engineers/openapi-v3-json-schema.md)
which can be consumed latter through UI or CLI.
If `values.schema.json` is not provided by the Chart author, Vela will generate a
OpenAPI-v3 JSON schema based on the `values.yaml` file automatically.
Deploy the application and after several minutes (it takes time to fetch Helm chart from the repo, render and install), you can check the Helm release is installed.
```shell
@@ -0,0 +1,21 @@
apiVersion: core.oam.dev/v1alpha2
kind: ComponentDefinition
metadata:
name: webapp-chart
namespace: vela-system
annotations:
definition.oam.dev/description: helm chart for webapp
spec:
workload:
definition:
apiVersion: apps/v1
kind: Deployment
schematic:
helm:
release:
chart:
spec:
chart: "podinfo"
version: "5.1.4"
repository:
url: "http://oam.dev/catalog/"
+16 -9
View File
@@ -5,13 +5,12 @@ import (
"fmt"
"time"
"github.com/pkg/errors"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/pkg/errors"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
)
@@ -23,17 +22,13 @@ var (
// RenderHelmReleaseAndHelmRepo constructs HelmRelease and HelmRepository in unstructured format
func RenderHelmReleaseAndHelmRepo(helmSpec *v1alpha2.Helm, compName, appName, ns string, values map[string]interface{}) (*unstructured.Unstructured, *unstructured.Unstructured, error) {
releaseSpec := &helmapi.HelmReleaseSpec{}
if err := json.Unmarshal(helmSpec.Release.Raw, releaseSpec); err != nil {
return nil, nil, err
releaseSpec, repoSpec, err := decodeHelmSpec(helmSpec)
if err != nil {
return nil, nil, errors.WithMessage(err, "Helm spec is invalid")
}
if releaseSpec.Interval == nil {
releaseSpec.Interval = DefaultIntervalDuration
}
repoSpec := &helmapi.HelmRepositorySpec{}
if err := json.Unmarshal(helmSpec.Repository.Raw, repoSpec); err != nil {
return nil, nil, err
}
if repoSpec.Interval == nil {
repoSpec.Interval = DefaultIntervalDuration
}
@@ -102,3 +97,15 @@ func setSpecObjIntoUnstructuredObj(spec interface{}, u *unstructured.Unstructure
_ = unstructured.SetNestedMap(u.Object, data, "spec")
return nil
}
func decodeHelmSpec(h *v1alpha2.Helm) (*helmapi.HelmReleaseSpec, *helmapi.HelmRepositorySpec, error) {
releaseSpec := &helmapi.HelmReleaseSpec{}
if err := json.Unmarshal(h.Release.Raw, releaseSpec); err != nil {
return nil, nil, errors.Wrap(err, "Helm release spec is invalid")
}
repoSpec := &helmapi.HelmRepositorySpec{}
if err := json.Unmarshal(h.Repository.Raw, repoSpec); err != nil {
return nil, nil, errors.Wrap(err, "Helm repository spec is invalid")
}
return releaseSpec, repoSpec, nil
}
+7 -6
View File
@@ -1,6 +1,7 @@
package helm
import (
"fmt"
"testing"
"github.com/ghodss/yaml"
@@ -12,7 +13,7 @@ import (
)
func TestRenderHelmReleaseAndHelmRepo(t *testing.T) {
h := testData()
h := testData("podinfo", "1.0.0", "test.com")
chartValues := map[string]interface{}{
"image": map[string]interface{}{
"tag": "1.0.1",
@@ -61,13 +62,13 @@ func TestRenderHelmReleaseAndHelmRepo(t *testing.T) {
}
}
func testData() *v1alpha2.Helm {
rlsStr :=
func testData(chart, version, repoURL string) *v1alpha2.Helm {
rlsStr := fmt.Sprintf(
`chart:
spec:
chart: "podinfo"
version: "1.0.0"`
repoStr := `url: "test.com"`
chart: "%s"
version: "%s"`, chart, version)
repoStr := fmt.Sprintf(`url: "%s"`, repoURL)
rlsJson, _ := yaml.YAMLToJSON([]byte(rlsStr))
repoJson, _ := yaml.YAMLToJSON([]byte(repoStr))
+200
View File
@@ -0,0 +1,200 @@
package helm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"cuelang.org/go/cue"
"cuelang.org/go/cue/format"
"cuelang.org/go/encoding/openapi"
"cuelang.org/go/encoding/yaml"
"github.com/getkin/kin-openapi/openapi3"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
)
var (
getters = getter.Providers{
getter.Provider{
Schemes: []string{"http", "https"},
New: getter.NewHTTPGetter,
},
}
)
// GetChartValuesJSONSchema fetched the Chart bundle and get JSON schema of Values
// file. If the Chart provides a 'values.json.schema' file, use it directly.
// Otherwise, try to generate a JSON schema based on the Values file.
func GetChartValuesJSONSchema(ctx context.Context, h *v1alpha2.Helm) ([]byte, error) {
releaseSpec, repoSpec, err := decodeHelmSpec(h)
if err != nil {
return nil, errors.WithMessage(err, "Helm spec is invalid")
}
chartSpec := releaseSpec.Chart.Spec
files, err := loadChartFiles(ctx, repoSpec.URL, chartSpec.Chart, chartSpec.Version)
if err != nil {
return nil, errors.WithMessage(err, "cannot load Chart files")
}
var values *loader.BufferedFile
for _, f := range files {
switch f.Name {
case "values.yaml", "values.yml":
values = f
case "values.schema.json":
// use the JSON schema file if exists
return f.Data, nil
default:
continue
}
}
if values == nil {
return nil, errors.New("cannot find 'values.schema.json' or 'values.yaml' file in the Chart")
}
// try to generate a schema based on Values file
generatedSchema, err := generateSchemaFromValues(values.Data)
if err != nil {
return nil, errors.WithMessage(err, "cannot generate schema from Values file")
}
return generatedSchema, nil
}
// generateSchemaFromValues generate OpenAPIv3 schema based on Chart Values
// file.
func generateSchemaFromValues(values []byte) ([]byte, error) {
valuesIdentifier := "values"
r := cue.Runtime{}
// convert Values yaml to CUE
ins, err := yaml.Decode(&r, "", string(values))
if err != nil {
return nil, errors.Wrap(err, "cannot decode Values.yaml to CUE")
}
// get the streamed CUE including the comments which will be used as
// 'description' in the schema
c, err := format.Node(ins.Value().Syntax(cue.Docs(true)), format.Simplify())
if err != nil {
return nil, errors.Wrap(err, "cannot format CUE generated from Values.yaml")
}
// cue openapi encoder only works on top-level identifier, we have to add
// an identifier manually
valuesStr := fmt.Sprintf("#%s:{\n%s\n}", valuesIdentifier, string(c))
r = cue.Runtime{}
ins, err = r.Compile("-", valuesStr)
if err != nil {
return nil, errors.Wrap(err, "cannot compile CUE generated from Values.yaml")
}
if ins.Err != nil {
return nil, errors.Wrap(ins.Err, "cannot compile CUE generated from Values.yaml")
}
// generate OpenAPIv3 schema through cue openapi encoder
rawSchema, err := openapi.Gen(ins, &openapi.Config{})
if err != nil {
return nil, errors.Wrap(ins.Err, "cannot generate OpenAPIv3 schema")
}
rawSchema, err = makeSwaggerCompatible(rawSchema)
if err != nil {
return nil, errors.WithMessage(err, "cannot make CUE-generated schema compatible with Swagger")
}
var out = &bytes.Buffer{}
_ = json.Indent(out, rawSchema, "", " ")
// load schema into Swagger to validate it compatible with Swagger OpenAPIv3
fullSchemaBySwagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(out.Bytes())
if err != nil {
return nil, errors.Wrap(err, "cannot load schema by SwaggerLoader")
}
valuesSchema := fullSchemaBySwagger.Components.Schemas[valuesIdentifier].Value
changeEnumToDefault(valuesSchema)
b, err := valuesSchema.MarshalJSON()
if err != nil {
return nil, errors.Wrap(err, "cannot marshall Values schema")
}
return b, nil
}
func loadChartFiles(ctx context.Context, repoURL, chart, version string) ([]*loader.BufferedFile, error) {
url, err := repo.FindChartInRepoURL(repoURL, chart, version, "", "", "", getters)
if err != nil {
return nil, errors.Wrap(err, "cannot find Chart URL")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "cannot fetch Chart from remote URL:%s", url)
}
//nolint:errcheck
defer resp.Body.Close()
files, err := loader.LoadArchiveFiles(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "cannot load Chart files")
}
return files, nil
}
// cue openapi encoder converts default in Chart Values as enum in schema
// changing enum to default makes the schema consistent with Chart Values
func changeEnumToDefault(schema *openapi3.Schema) {
t := schema.Type
switch t {
case "object":
for _, v := range schema.Properties {
s := v.Value
changeEnumToDefault(s)
}
case "array":
if schema.Items != nil {
changeEnumToDefault(schema.Items.Value)
}
}
// change enum to default
if len(schema.Enum) > 0 {
schema.Default = schema.Enum[0]
schema.Enum = nil
}
// remove all required fields, because fields in Values.yml are all optional
schema.Required = nil
}
// cue openapi encoder converts 'items' field in an array type field into array,
// that's not compatible with OpenAPIv3. 'items' field should be an object.
func makeSwaggerCompatible(d []byte) ([]byte, error) {
m := map[string]interface{}{}
err := json.Unmarshal(d, &m)
if err != nil {
return nil, errors.Wrap(err, "cannot unmarshall schema")
}
handleItemsOfArrayType(m)
b, err := json.Marshal(m)
if err != nil {
return nil, errors.Wrap(err, "cannot marshall schema")
}
return b, nil
}
// handleItemsOfArrayType will convert all 'items' of array type from array to object
// and remove enum in the items
func handleItemsOfArrayType(t map[string]interface{}) {
for _, v := range t {
if next, ok := v.(map[string]interface{}); ok {
handleItemsOfArrayType(next)
}
}
if t["type"] == "array" {
if i, ok := t["items"].([]interface{}); ok {
itemSpec, _ := i[0].(map[string]interface{})
itemSpec["enum"] = nil
t["items"] = itemSpec
}
}
}
+248
View File
@@ -0,0 +1,248 @@
package helm
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"github.com/google/go-cmp/cmp"
)
func TestGenerateSchemaFromValues(t *testing.T) {
testValues, err := ioutil.ReadFile("./testdata/values.yaml")
if err != nil {
t.Error(err, "cannot load test data")
}
wantSchema, err := ioutil.ReadFile("./testdata/values.schema.json")
if err != nil {
t.Error(err, "cannot load expected data")
}
wantSchemaMap := map[string]interface{}{}
// convert bytes to map for diff converience
_ = json.Unmarshal(wantSchema, &wantSchemaMap)
result, err := generateSchemaFromValues(testValues)
if err != nil {
t.Error(err, "failed generate schema from values")
}
resultMap := map[string]interface{}{}
if err := json.Unmarshal(result, &resultMap); err != nil {
t.Error(err, "cannot unmarshal result bytes")
}
if diff := cmp.Diff(resultMap, wantSchemaMap); diff != "" {
t.Fatalf("\ngenerateSchemaFromValues(...)(...) -want +get \n%s", diff)
}
}
func TestGetChartValuesJSONSchema(t *testing.T) {
testHelm := testData("podinfo", "5.1.4", "http://oam.dev/catalog")
wantSchema, err := ioutil.ReadFile("./testdata/values.schema.json")
if err != nil {
t.Error(err, "cannot load expected data")
}
wantSchemaMap := map[string]interface{}{}
// convert bytes to map for diff converience
_ = json.Unmarshal(wantSchema, &wantSchemaMap)
result, err := GetChartValuesJSONSchema(context.Background(), testHelm)
if err != nil {
t.Error(err, "failed get schema")
}
resultMap := map[string]interface{}{}
if err := json.Unmarshal(result, &resultMap); err != nil {
t.Error(err, "cannot unmarshal result bytes")
}
if diff := cmp.Diff(resultMap, wantSchemaMap); diff != "" {
t.Fatalf("\nGetChartValuesJSONSchema(...)(...) -want +get \n%s", diff)
}
}
func TestChangeEnumToDefault(t *testing.T) {
// testData contains object, string, integer, bool, and array type fields
// with enum and required values
testData := `{"properties":{"array":{"enum":[["a","b","c"]],"items":{"type":"string"},"type":"array"},"bool":{"enum":[false],"type":"boolean"},"integer":{"enum":[1],"type":"integer"},"obj":{"properties":{"f0":{"enum":["v0"],"type":"string"},"f1":{"enum":["v1"],"type":"string"},"f2":{"enum":["v2"],"type":"string"}},"required":["f0","f1","f2"],"type":"object"},"string":{"enum":["a"],"type":"string"}},"required":["bool","string","obj","array","integer"],"type":"object"}`
s := fmt.Sprintf(`{"components":{"schemas":{"values":%s}}}`, testData)
testSwagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData([]byte(s))
if err != nil {
t.Error(err)
}
testSchema := testSwagger.Components.Schemas["values"].Value
changeEnumToDefault(testSchema)
result, err := testSchema.MarshalJSON()
if err != nil {
t.Error(err)
}
resultMap := map[string]interface{}{}
err = json.Unmarshal(result, &resultMap)
if err != nil {
t.Error(err)
}
want := `{"properties":{"array":{"default":["a","b","c"],"items":{"type":"string"},"type":"array"},"bool":{"default":false,"type":"boolean"},"integer":{"default":1,"type":"integer"},"obj":{"properties":{"f0":{"default":"v0","type":"string"},"f1":{"default":"v1","type":"string"},"f2":{"default":"v2","type":"string"}},"type":"object"},"string":{"default":"a","type":"string"}},"type":"object"}`
wantMap := map[string]interface{}{}
_ = json.Unmarshal([]byte(want), &wantMap)
if diff := cmp.Diff(resultMap, wantMap); diff != "" {
t.Fatalf("\nchangeEnumToDefault(...) -want +get %s\n", diff)
}
}
func TestMakeSwaggerCompatible(t *testing.T) {
tests := []struct {
caseName string
testdata string
want string
}{
{
caseName: "integer type array",
testdata: `{"integerArray": {
"type": "array",
"items": [
{
"type": "integer",
"enum": [
0
]
},
{
"type": "integer",
"enum": [
1
]
},
{
"type": "integer",
"enum": [
2
]
}
],
"enum": [
[
0,
1,
2
]
]
}}`,
want: `{"integerArray":{"enum":[[0,1,2]],"items":{"enum":null,"type":"integer"},"type":"array"}}`,
},
{
caseName: "string type array",
testdata: `{"stringArray": {
"type": "array",
"items": [
{
"type": "string",
"enum": [
"a"
]
},
{
"type": "string",
"enum": [
"b"
]
},
{
"type": "string",
"enum": [
"c"
]
}
],
"enum": [
[
"a",
"b",
"c"
]
]
}}`,
want: `{"stringArray":{"enum":[["a","b","c"]],"items":{"enum":null,"type":"string"},"type":"array"}}`,
},
{
caseName: "bool type array",
testdata: `{"boolArray": {
"type": "array",
"items": [
{
"type": "boolean",
"enum": [
true
]
},
{
"type": "boolean",
"enum": [
false
]
}
],
"enum": [
[
true,
false
]
]
}}`,
want: `{"boolArray":{"enum":[[true,false]],"items":{"enum":null,"type":"boolean"},"type":"array"}}`,
},
{
caseName: "object type array",
testdata: `{"objectArray": {
"type": "array",
"items": [
{
"type": "object",
"required": [
"f0",
"f1",
"f2"
],
"properties": {
"f0": {
"type": "string",
"enum": [
"v0"
]
},
"f1": {
"type": "string",
"enum": [
"v1"
]
},
"f2": {
"type": "string",
"enum": [
"v2"
]
}
}
}
]
}}`,
want: `{"objectArray":{"items":{"enum":null,"properties":{"f0":{"enum":["v0"],"type":"string"},"f1":{"enum":["v1"],"type":"string"},"f2":{"enum":["v2"],"type":"string"}},"required":["f0","f1","f2"],"type":"object"},"type":"array"}}`,
},
}
for _, tc := range tests {
t.Run(tc.caseName, func(t *testing.T) {
result, err := makeSwaggerCompatible([]byte(tc.testdata))
if err != nil {
t.Error(err)
}
resultMap := map[string]interface{}{}
err = json.Unmarshal(result, &resultMap)
if err != nil {
t.Error(err)
}
wantMap := map[string]interface{}{}
_ = json.Unmarshal([]byte(tc.want), &wantMap)
if diff := cmp.Diff(resultMap, wantMap); diff != "" {
t.Fatalf("\nmakeSwaggerCompatible(...) -want +get %s\n", diff)
}
})
}
}
-5
View File
@@ -1,5 +0,0 @@
chart:
spec:
chart: "podinfo"
version: "1.0.0"
url: "test.com"
+345
View File
@@ -0,0 +1,345 @@
{
"properties": {
"affinity": {
"type": "object"
},
"backend": {
"nullable": true
},
"backends": {
"default": [],
"type": "array"
},
"cache": {
"default": "",
"description": "Redis address in the format <host>:<port>",
"type": "string"
},
"certificate": {
"description": "create a certificate manager certificate",
"properties": {
"create": {
"default": false,
"type": "boolean"
},
"dnsNames": {
"default": [
"podinfo"
],
"description": "the hostname / subject alternative names for the certificate",
"items": {
"type": "string"
},
"type": "array"
},
"issuerRef": {
"description": "the issuer used to issue the certificate",
"properties": {
"kind": {
"default": "ClusterIssuer",
"type": "string"
},
"name": {
"default": "self-signed",
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
},
"faults": {
"properties": {
"delay": {
"default": false,
"type": "boolean"
},
"error": {
"default": false,
"type": "boolean"
},
"testFail": {
"default": false,
"type": "boolean"
},
"testTimeout": {
"default": false,
"type": "boolean"
},
"unhealthy": {
"default": false,
"type": "boolean"
},
"unready": {
"default": false,
"type": "boolean"
}
},
"type": "object"
},
"h2c": {
"properties": {
"enabled": {
"default": false,
"type": "boolean"
}
},
"type": "object"
},
"hpa": {
"description": "metrics-server add-on required",
"properties": {
"cpu": {
"description": "average total CPU usage per pod (1-100)",
"nullable": true
},
"enabled": {
"default": false,
"type": "boolean"
},
"maxReplicas": {
"default": 10,
"type": "integer"
},
"memory": {
"description": "average memory usage per pod (100Mi-1Gi)",
"nullable": true
},
"requests": {
"description": "average http requests per second per pod (k8s-prometheus-adapter)",
"nullable": true
}
},
"type": "object"
},
"image": {
"properties": {
"pullPolicy": {
"default": "IfNotPresent",
"type": "string"
},
"repository": {
"default": "ghcr.io/stefanprodan/podinfo",
"type": "string"
},
"tag": {
"default": "5.1.4",
"type": "string"
}
},
"type": "object"
},
"ingress": {
"properties": {
"annotations": {
"type": "object"
},
"enabled": {
"default": false,
"type": "boolean"
},
"hosts": {
"default": [],
"type": "array"
},
"path": {
"default": "/*",
"description": "kubernetes.io/ingress.class: nginx\nkubernetes.io/tls-acme: \"true\"",
"type": "string"
},
"tls": {
"default": [],
"description": "- podinfo.local",
"type": "array"
}
},
"type": "object"
},
"linkerd": {
"properties": {
"profile": {
"properties": {
"enabled": {
"default": false,
"type": "boolean"
}
},
"type": "object"
}
},
"type": "object"
},
"logLevel": {
"default": "info",
"type": "string"
},
"nodeSelector": {
"type": "object"
},
"podAnnotations": {
"type": "object"
},
"redis": {
"description": "Redis deployment",
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"repository": {
"default": "redis",
"type": "string"
},
"tag": {
"default": "6.0.8",
"type": "string"
}
},
"type": "object"
},
"replicaCount": {
"default": 1,
"type": "integer"
},
"resources": {
"properties": {
"limits": {
"nullable": true
},
"requests": {
"properties": {
"cpu": {
"default": "1m",
"type": "string"
},
"memory": {
"default": "16Mi",
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
},
"service": {
"properties": {
"enabled": {
"default": true,
"type": "boolean"
},
"externalPort": {
"default": 9898,
"type": "integer"
},
"grpcPort": {
"default": 9999,
"type": "integer"
},
"grpcService": {
"default": "podinfo",
"type": "string"
},
"hostPort": {
"description": "the port used to bind the http port to the host\nNOTE: requires privileged container with NET_BIND_SERVICE capability -- this is useful for testing\nin local clusters such as kind without port forwarding",
"nullable": true
},
"httpPort": {
"default": 9898,
"type": "integer"
},
"metricsPort": {
"default": 9797,
"type": "integer"
},
"nodePort": {
"default": 31198,
"type": "integer"
},
"type": {
"default": "ClusterIP",
"type": "string"
}
},
"type": "object"
},
"serviceAccount": {
"properties": {
"enabled": {
"default": false,
"description": "Specifies whether a service account should be created",
"type": "boolean"
},
"name": {
"description": "The name of the service account to use.\nIf not set and create is true, a name is generated using the fullname template",
"nullable": true
}
},
"type": "object"
},
"serviceMonitor": {
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"interval": {
"default": "15s",
"type": "string"
}
},
"type": "object"
},
"tls": {
"description": "enable tls on the podinfo service",
"properties": {
"certPath": {
"default": "/data/cert",
"description": "the path where the certificate key pair will be mounted",
"type": "string"
},
"enabled": {
"default": false,
"type": "boolean"
},
"hostPort": {
"description": "the port used to bind the tls port to the host\nNOTE: requires privileged container with NET_BIND_SERVICE capability -- this is useful for testing\nin local clusters such as kind without port forwarding",
"nullable": true
},
"port": {
"default": 9899,
"description": "the port used to host the tls endpoint on the service",
"type": "integer"
},
"secretName": {
"description": "the name of the secret used to mount the certificate key pair",
"nullable": true
}
},
"type": "object"
},
"tolerations": {
"default": [],
"type": "array"
},
"ui": {
"properties": {
"color": {
"default": "#34577c",
"type": "string"
},
"logo": {
"default": "",
"type": "string"
},
"message": {
"default": "",
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}
+127
View File
@@ -0,0 +1,127 @@
# Default values for podinfo.
replicaCount: 1
logLevel: info
backend: #http://backend-podinfo:9898/echo
backends: []
ui:
color: "#34577c"
message: ""
logo: ""
faults:
delay: false
error: false
unhealthy: false
unready: false
testFail: false
testTimeout: false
h2c:
enabled: false
image:
repository: ghcr.io/stefanprodan/podinfo
tag: 5.1.4
pullPolicy: IfNotPresent
service:
enabled: true
type: ClusterIP
metricsPort: 9797
httpPort: 9898
externalPort: 9898
grpcPort: 9999
grpcService: podinfo
nodePort: 31198
# the port used to bind the http port to the host
# NOTE: requires privileged container with NET_BIND_SERVICE capability -- this is useful for testing
# in local clusters such as kind without port forwarding
hostPort:
# enable tls on the podinfo service
tls:
enabled: false
# the name of the secret used to mount the certificate key pair
secretName:
# the path where the certificate key pair will be mounted
certPath: /data/cert
# the port used to host the tls endpoint on the service
port: 9899
# the port used to bind the tls port to the host
# NOTE: requires privileged container with NET_BIND_SERVICE capability -- this is useful for testing
# in local clusters such as kind without port forwarding
hostPort:
# create a certificate manager certificate
certificate:
create: false
# the issuer used to issue the certificate
issuerRef:
kind: ClusterIssuer
name: self-signed
# the hostname / subject alternative names for the certificate
dnsNames:
- podinfo
# metrics-server add-on required
hpa:
enabled: false
maxReplicas: 10
# average total CPU usage per pod (1-100)
cpu:
# average memory usage per pod (100Mi-1Gi)
memory:
# average http requests per second per pod (k8s-prometheus-adapter)
requests:
# Redis address in the format <host>:<port>
cache: ""
# Redis deployment
redis:
enabled: false
repository: redis
tag: 6.0.8
serviceAccount:
# Specifies whether a service account should be created
enabled: false
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name:
linkerd:
profile:
enabled: false
serviceMonitor:
enabled: false
interval: 15s
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
path: /*
hosts: []
# - podinfo.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources:
limits:
requests:
cpu: 1m
memory: 16Mi
nodeSelector: {}
tolerations: []
affinity: {}
podAnnotations: {}
@@ -83,8 +83,13 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
var def utils.CapabilityComponentDefinition
def.Name = req.NamespacedName.Name
def.WorkloadType = workloadType
if workloadType == util.ReferWorkload {
switch workloadType {
case util.ReferWorkload:
def.WorkloadDefName = componentDefinition.Spec.Workload.Type
case util.HELMDef:
def.Helm = componentDefinition.Spec.Schematic.HELM
def.ComponentDefinition = componentDefinition
default:
}
err = def.StoreOpenAPISchema(ctx, r, req.Namespace, req.Name)
if err != nil {
@@ -479,6 +479,49 @@ spec:
})
})
Context("When the ComponentDefinition contains Helm schematic", func() {
var componentDefinitionName = "cd-with-helm-schematic"
var namespace = "default"
req := reconcile.Request{NamespacedName: client.ObjectKey{Name: componentDefinitionName, Namespace: namespace}}
It("Applying ComponentDefinition with Helm schematic", func() {
cd := v1alpha2.ComponentDefinition{}
cd.SetName(componentDefinitionName)
cd.SetNamespace(namespace)
cd.Spec.Workload.Definition = v1alpha2.WorkloadGVK{APIVersion: "apps/v1", Kind: "Deployment"}
cd.Spec.Schematic = &v1alpha2.Schematic{
HELM: &v1alpha2.Helm{
Release: util.Object2RawExtension(map[string]interface{}{
"chart": map[string]interface{}{
"spec": map[string]interface{}{
"chart": "podinfo",
"version": "5.1.4",
},
},
}),
Repository: util.Object2RawExtension(map[string]interface{}{
"url": "http://oam.dev/catalog/",
}),
},
}
By("Create ComponentDefinition")
Expect(k8sClient.Create(ctx, &cd)).Should(Succeed())
By("Check whether WorkloadDefinition is created")
reconcileRetry(&r, req)
var wd v1alpha2.WorkloadDefinition
var wdName = componentDefinitionName
Eventually(func() bool {
err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: wdName}, &wd)
return err == nil
}, 10*time.Second, time.Second).Should(BeTrue())
Expect(wd.Name).Should(Equal(cd.Name))
Expect(wd.Namespace).Should(Equal(cd.Namespace))
Expect(wd.Annotations).Should(Equal(cd.Annotations))
Expect(wd.Spec.Schematic).Should(Equal(cd.Spec.Schematic))
})
})
Context("When the ComponentDefinition contain Workload.Type, shouldn't create a WorkloadDefinition", func() {
var componentDefinitionName = "cd-with-workload-type"
var namespace = "default"
@@ -576,4 +619,5 @@ spec:
}, 10*time.Second, time.Second).Should(Equal(name))
})
})
})
+9 -1
View File
@@ -33,6 +33,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile/helm"
mycue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
@@ -105,7 +106,14 @@ func (def *CapabilityComponentDefinition) GetOpenAPISchema(ctx context.Context,
// StoreOpenAPISchema stores OpenAPI v3 schema in ConfigMap from WorkloadDefinition
func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name string) error {
jsonSchema, err := def.GetOpenAPISchema(ctx, k8sClient, namespace, name)
var jsonSchema []byte
var err error
switch def.WorkloadType {
case util.HELMDef:
jsonSchema, err = helm.GetChartValuesJSONSchema(ctx, def.Helm)
default:
jsonSchema, err = def.GetOpenAPISchema(ctx, k8sClient, namespace, name)
}
if err != nil {
return fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err)
}
-1
View File
@@ -90,7 +90,6 @@ const (
ComponentDef WorkloadType = "ComponentDef"
// HELMDef describe a workload refer to HELM
// TODO(yangsoon): we need store helm capability schema in configMap
HELMDef WorkloadType = "HelmDef"
// ReferWorkload describe an existing workload
+1
View File
@@ -340,6 +340,7 @@ var _ = Describe("Versioning mechanism of components", func() {
var w1 unstructured.Unstructured
Eventually(
func() error {
reconcileAppConfigNow(ctx, &appconfig)
w1.SetAPIVersion("example.com/v1")
w1.SetKind("Bar")
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: revisionNameV1}, &w1)
+15
View File
@@ -2,6 +2,7 @@ package controllers_test
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -285,4 +286,18 @@ var _ = Describe("Test application containing helm module", func() {
}, 120*time.Second, 10*time.Second).Should(BeTrue())
})
It("Test store JSON schema of Helm Chart in ConfigMap", func() {
By("Get the ConfigMap")
cmName := fmt.Sprintf("schema-%s", cdName)
Eventually(func() error {
cm := &corev1.ConfigMap{}
if err := k8sClient.Get(ctx, client.ObjectKey{Name: cmName, Namespace: namespace}, cm); err != nil {
return err
}
if cm.Data["openapi-v3-json-schema"] == "" {
return errors.New("json schema is not found in the ConfigMap")
}
return nil
}, 60*time.Second, 5*time.Second).Should(Succeed())
})
})