Files
kubevela/pkg/appfile/helm/schema.go
wyike 74a82e0397 add header-check.sh (#1310)
add ci workflow

update scripts

add missed licence  header for all files
2021-03-26 15:24:19 +08:00

217 lines
6.6 KiB
Go

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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/common"
)
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 *common.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
}
}
}