mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
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:
@@ -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/"
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
chart:
|
||||
spec:
|
||||
chart: "podinfo"
|
||||
version: "1.0.0"
|
||||
url: "test.com"
|
||||
+345
@@ -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
@@ -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: {}
|
||||
+6
-1
@@ -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 {
|
||||
|
||||
+44
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user