Implement API for generating OpenAPI schema for definition

Fetched cue parameters from workloaddefinition/traitdefinition, generated
OpenAPI v3 schema, fixed problems of tainted `description` filed, added filed
"title" required by form-render component, added it in API server router

To fix #680
This commit is contained in:
zzxwill
2021-01-05 21:57:38 +08:00
parent 810cbcaae4
commit 53164a4d2f
22 changed files with 972 additions and 34 deletions
+1 -1
View File
@@ -15,11 +15,11 @@ require (
github.com/deckarep/golang-set v1.7.1
github.com/fatih/color v1.9.0
github.com/gertd/go-pluralize v0.1.7
github.com/getkin/kin-openapi v0.34.0
github.com/ghodss/yaml v1.0.0
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1
github.com/gin-gonic/gin v1.6.3
github.com/go-logr/logr v0.1.0
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/spec v0.19.8 // indirect
github.com/go-openapi/swag v0.19.11 // indirect
+4
View File
@@ -543,6 +543,8 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwL
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/gertd/go-pluralize v0.1.7 h1:RgvJTJ5W7olOoAks97BOwOlekBFsLEyh00W48Z6ZEZY=
github.com/gertd/go-pluralize v0.1.7/go.mod h1:O4eNeeIf91MHh1GJ2I47DNtaesm66NYvjYgAahcqSDQ=
github.com/getkin/kin-openapi v0.34.0 h1:B/YcH4BXcEzQfpCv7WvdPubb3EnMuwEcI3O+l6GpiXc=
github.com/getkin/kin-openapi v0.34.0/go.mod h1:ZJSfy1PxJv2QQvH9EdBj3nupRTVvV42mkW6zKUlRBwk=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -1219,6 +1221,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v0.0.0-20160406211939-eadb3ce320cb/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyoh86/exportloopref v0.1.7/go.mod h1:h1rDl2Kdj97+Kwh4gdz3ujE7XHmH51Q0lUiZ1z4NLj8=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
@@ -1884,6 +1887,7 @@ go.etcd.io/bbolt v1.3.1-etcd.7/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XK
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20181031231232-83304cfc808c/go.mod h1:weASp41xM3dk0YHg1s/W8ecdGP5G4teSTMBPpYAaUgA=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
+2 -31
View File
@@ -1,7 +1,6 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
@@ -13,6 +12,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
mycue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/plugins"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
const BaseRefPath = "docs/en/developers/references"
@@ -70,7 +70,7 @@ func main() {
CapabilityType: capabilityType,
}
cueValue, err := getCUEParameterValue(c.CueTemplate)
cueValue, err := common.GetCUEParameterValue(c.CueTemplate)
if err != nil {
fmt.Printf("failed to retrieve `parameters` value from %s with err: %s", c.Name, err)
os.Exit(1)
@@ -114,35 +114,6 @@ func (ref *ReferenceMarkdown) prepareParameterTable(tableName string, parameterL
return refContent
}
// getCUEParameterValue converts definitions to cue format
func getCUEParameterValue(cueStr string) (cue.Value, error) {
r := cue.Runtime{}
template, err := r.Compile("", cueStr+mycue.BaseTemplate)
if err != nil {
return cue.Value{}, err
}
tempStruct, err := template.Value().Struct()
if err != nil {
return cue.Value{}, err
}
// find the parameter definition
var paraDef cue.FieldInfo
var found bool
for i := 0; i < tempStruct.Len(); i++ {
paraDef = tempStruct.Field(i)
if paraDef.Name == "parameter" {
found = true
break
}
}
if !found {
return cue.Value{}, errors.New("arguments not exist")
}
arguments := paraDef.Value
return arguments, nil
}
// parseParameters parses every parameter
func (ref *ReferenceMarkdown) parseParameters(paraValue cue.Value, paramKey string, depth int) error {
var params []Parameter
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/oam-dev/kubevela/pkg/oam"
)
// Service defines the service spec for AppFile, it will contain all information a service realted including OAM component, traits, source to image, etc...
// Service defines the service spec for AppFile, it will contain all information a service related including OAM component, traits, source to image, etc...
type Service map[string]interface{}
// DefaultWorkloadType defines the default service type if no type specified in Appfile
+21
View File
@@ -1 +1,22 @@
package appfile
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetType(t *testing.T) {
svc1 := Service{}
got := svc1.GetType()
assert.Equal(t, DefaultWorkloadType, got)
var workload2 = "W2"
map2 := map[string]interface{}{
"type": workload2,
"cpu": "0.5",
}
svc2 := Service(map2)
got = svc2.GetType()
assert.Equal(t, workload2, got)
}
+19
View File
@@ -0,0 +1,19 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/oam-dev/kubevela/pkg/server/util"
"github.com/oam-dev/kubevela/pkg/serverlib"
)
// GetDefinition gets OpenAPI schema from Cue section of a WorkloadDefinition/TraitDefinition
func (s *APIServer) GetDefinition(c *gin.Context) {
definitionName := c.Param("name")
parameter, err := serverlib.GetDefinition(definitionName)
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err)
return
}
util.AssembleResponse(c, string(parameter), nil)
}
+6
View File
@@ -149,6 +149,12 @@ func (s *APIServer) setupRoute(staticPath string) http.Handler {
caps.GET("", s.ListCapabilities)
}
// Definition related api
defs := api.Group(util.Definition)
{
defs.GET("/:name", s.GetDefinition)
}
// version
api.GET(util.VersionPath, s.GetVersion)
+1
View File
@@ -53,6 +53,7 @@ const (
CapabilityPath = "/capabilities"
CapabilityCenterPath = "/capability-centers"
VersionPath = "/version"
Definition = "/definitions"
)
// NoRoute is a handler which is invoked when there is no route matches.
+213
View File
@@ -0,0 +1,213 @@
package serverlib
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/getkin/kin-openapi/openapi3"
mycue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/system"
)
const (
// OpenAPISchemaDir is the folder name under ~/.vela/capabilities
OpenAPISchemaDir = "openapi"
// UsageTag is usage comment annotation
UsageTag = "+usage="
// ShortTag is the short alias annotation
ShortTag = "\\n+short"
)
// OpenAPISchema is the struct for OpenAPI Schema generated by Cue OpenAPI
type OpenAPISchema struct {
OpenAPI string `json:"openapi"`
Components Components `json:"components"`
}
// Components is the struct filed of OpenAPISchema
type Components struct {
Schemas Schemas `json:"schemas"`
}
// Schemas is the struct filed of Components
type Schemas struct {
Parameter map[string]interface{} `json:"parameter"`
}
// GetDefinition is the main function for GetDefinition API
func GetDefinition(name string) ([]byte, error) {
openAPISchema, err := generateOpenAPISchemaFromCapabilityParameter(name)
if err != nil {
return nil, err
}
swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(openAPISchema)
if err != nil {
return nil, err
}
schemaRef := swagger.Components.Schemas["parameter"]
schema := schemaRef.Value
fixOpenAPISchema("", schema)
parameter, err := schema.MarshalJSON()
if err != nil {
return nil, err
}
return parameter, nil
}
// generateOpenAPISchemaFromCapabilityParameter returns the parameter of a definition in cue.Value format
func generateOpenAPISchemaFromCapabilityParameter(name string) ([]byte, error) {
dir, err := system.GetCapabilityDir()
if err != nil {
return nil, err
}
definitionCueName := fmt.Sprintf("%s.cue", name)
schemaDir := filepath.Join(dir, OpenAPISchemaDir)
if err = prepareParameterCue(dir, definitionCueName, schemaDir); err != nil {
return nil, err
}
if err = appendCueReference(filepath.Join(dir, OpenAPISchemaDir, definitionCueName)); err != nil {
return nil, err
}
filename := filepath.FromSlash(definitionCueName)
return common.GenOpenAPIFromFile(filepath.Join(dir, OpenAPISchemaDir), filename)
}
// prepareParameterCue cuts `parameter` section form definition .cue file
func prepareParameterCue(fileDir, fileName string, targetSchemaDir string) error {
if _, err := os.Stat(targetSchemaDir); err != nil && os.IsNotExist(err) {
if err := os.Mkdir(targetSchemaDir, 0750); err != nil {
return err
}
}
cueFile := filepath.Join(fileDir, fileName)
f, err := os.Open(filepath.Clean(cueFile))
if err != nil {
return err
}
//nolint
defer f.Close()
schemaFile := filepath.Join(targetSchemaDir, fileName)
_, err = os.Stat(schemaFile)
if err == nil {
if err = os.Truncate(schemaFile, 0); err != nil {
return err
}
}
targetFile, err := os.OpenFile(filepath.Clean(schemaFile), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
//nolint
defer targetFile.Close()
scanner := bufio.NewScanner(f)
var withParameterFlag bool
r := regexp.MustCompile("[[:space:]]*parameter:[[:space:]]*{.*")
for scanner.Scan() {
text := scanner.Text()
if r.MatchString(text) {
// a variable has to be refined as a definition which starts with "#"
text = fmt.Sprintf("parameter: #parameter\n#%s", text)
withParameterFlag = true
}
if _, err := targetFile.WriteString(fmt.Sprintf("%s\n", text)); err != nil {
return err
}
}
if !withParameterFlag {
return fmt.Errorf("cue file %s doesn't contain section `parmeter`", cueFile)
}
return nil
}
// appendCueReference appends `context` filed to parameter .cue file
func appendCueReference(cueFile string) error {
f, err := os.OpenFile(filepath.Clean(cueFile), os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return err
}
//nolint
defer f.Close()
if _, err := f.WriteString(mycue.BaseTemplate); err != nil {
return err
}
return nil
}
// fixOpenAPISchema fixes tainted `description` filed, missing of title `field`.
func fixOpenAPISchema(name string, schema *openapi3.Schema) {
t := schema.Type
switch t {
case "object":
for k, v := range schema.Properties {
s := v.Value
fixOpenAPISchema(k, s)
}
case "array":
if name != "" {
schema.Title = name
}
fixOpenAPISchema("", schema.Items.Value)
default:
if name != "" {
schema.Title = name
}
}
line := schema.Description
if strings.Contains(line, UsageTag) {
newDescription := strings.ReplaceAll(line, UsageTag, "")
if strings.Contains(newDescription, ShortTag) {
newDescription = strings.ReplaceAll(newDescription, ShortTag, "")
}
schema.Description = newDescription
}
}
// addTitleField adds title field
func addTitleField(line, propertyName string) string {
blanks := strings.Split(line, "\"")[0]
title := fmt.Sprintf("%s\"title\": \"%s\"", blanks, propertyName)
return title + ",\n"
}
// getParameterItemName gets the name of a parameter item
func getParameterItemName(previousLine string) (string, error) {
// parse property name, like from `"cmd": {\n`
tempPropertyName := strings.Split(previousLine, "\"")
if len(tempPropertyName) <= 1 {
return "", fmt.Errorf("could not get property name from: %s", previousLine)
}
propertyName := strings.Split(tempPropertyName[1], "\"")[0]
return propertyName, nil
}
// getParameterFromOpenAPISchema retrieves needs section from OpenAPI schema for front-end requirements
func getParameterFromOpenAPISchema(openAPISchema []byte) ([]byte, error) {
var schema OpenAPISchema
var parameterJSON []byte
var err error
if err = json.Unmarshal(openAPISchema, &schema); err != nil {
return nil, err
}
parameter := schema.Components.Schemas.Parameter
if parameterJSON, err = json.Marshal(parameter); err != nil {
return nil, err
}
return parameterJSON, nil
}
+207
View File
@@ -0,0 +1,207 @@
package serverlib
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/oam-dev/kubevela/pkg/utils/system"
)
const TestDir = "testdata/definition"
func TestGenerateOpenAPISchemaFromCapabilityParameter(t *testing.T) {
var invalidWorkloadName = "IAmAnInvalidWorkloadDefinition"
capabilityDir, _ := system.GetCapabilityDir()
if _, err := os.Stat(capabilityDir); err != nil && os.IsNotExist(err) {
os.Mkdir(capabilityDir, 0755)
}
invalidWorkloadPath := filepath.Join(capabilityDir, fmt.Sprintf("%s.cue", invalidWorkloadName))
type want struct {
data []byte
err error
}
cases := map[string]struct {
reason string
name string
want want
}{
"GenerateOpenAPISchemaFromInvalidCapability": {
reason: "generate OpenAPI schema for an invalid Workload/Trait",
name: invalidWorkloadName,
want: want{data: nil, err: &os.PathError{Op: "open", Path: invalidWorkloadPath, Err: fmt.Errorf("no such file or directory")}},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := generateOpenAPISchemaFromCapabilityParameter(tc.name)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\ngetDefinition(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.data, got); diff != "" {
t.Errorf("\n%s\ngetDefinition(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestPrepareParameterCue(t *testing.T) {
targetSchemaDir := filepath.Join(TestDir, "openapi")
var noParameterCueName = "workloadNoParameter.cue"
var err = fmt.Errorf("cue file %s doesn't contain section `parmeter`", filepath.Join(TestDir, noParameterCueName))
var invalidDir = "IAmAnInvalidDirectory/openapi"
var invalidDirErr = &os.PathError{Op: "mkdir", Path: invalidDir, Err: fmt.Errorf("no such file or directory")}
var invalidSourceFile = "IAmAnInvalidFile"
invalidSourceFilePath := filepath.Join(TestDir, invalidSourceFile)
var invalidSourceFileErr = &os.PathError{Op: "open", Path: invalidSourceFilePath, Err: fmt.Errorf("no such file or directory")} //
// fmt.Errorf("open %s: no such file or directory", )
cases := map[string]struct {
reason string
fileDir string
fileName string
targetSchemaDir string
want error
}{
"PrepareANormalParameterCueFile": {
reason: "Prepare a normal parameter cue file",
fileDir: TestDir,
fileName: "workload1.cue",
targetSchemaDir: targetSchemaDir,
want: nil,
},
"CueFileNotContainParameter": {
reason: "Prepare a cue file which doesn't contain `parameter` section",
fileDir: TestDir,
fileName: noParameterCueName,
targetSchemaDir: targetSchemaDir,
want: err,
},
"InvalidTargetSchemaDir": {
reason: "target schema directory is invalid",
fileDir: "",
fileName: "",
targetSchemaDir: invalidDir,
want: invalidDirErr,
},
"InvalidSourceCueFile": {
reason: "source cue file is invalid",
fileDir: TestDir,
fileName: invalidSourceFile,
targetSchemaDir: TestDir,
want: invalidSourceFileErr,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
err := prepareParameterCue(tc.fileDir, tc.fileName, tc.targetSchemaDir)
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nprepareParameterCue(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
os.RemoveAll(targetSchemaDir)
}
func TestAppendCueReference(t *testing.T) {
var cueStr = `
#parameter: {
min: int
}
`
temporaryDir := filepath.Join(TestDir, "temp")
os.Mkdir(temporaryDir, 0750)
var cueFile = filepath.Join(temporaryDir, "workloadPureParameter.cue")
ioutil.WriteFile(cueFile, []byte(cueStr), 0750)
cases := map[string]struct {
reason string
cueFile string
want error
}{
"AppendCueReference": {
reason: "Append Cue Reference",
cueFile: cueFile,
want: nil,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
err := appendCueReference(tc.cueFile)
if (err == nil && tc.want != nil) || (err != nil && tc.want == nil) {
t.Errorf("%s\nappendCueReference(...): -want %s, +got %s", tc.reason, tc.want, err)
}
data, err := ioutil.ReadFile(tc.cueFile)
if err != nil {
t.Errorf("%s\nappendCueReference(...): target file %s could not be read: %s", tc.reason, tc.cueFile, err)
}
if strings.HasSuffix(string(data), "context: {") {
t.Errorf("%s\nappendCueReference(...): target file %s doesn't contain `parameter` section", tc.reason, tc.cueFile)
}
})
}
os.RemoveAll(temporaryDir)
}
func TestAddTitleField(t *testing.T) {
got := addTitleField(" \"description\": \"cpu core\"", "cpu")
title := " \"title\": \"cpu\",\n"
assert.Equal(t, title, got)
}
func TestGetParameterItemName(t *testing.T) {
got, err := getParameterItemName(" \"cmd\": {\n")
name := "cmd"
assert.NoError(t, err)
assert.Equal(t, name, got)
}
func TestGetParameterFromOpenAPISchema(t *testing.T) {
cases := map[string]struct {
reason string
fileDir string
fileName string
targetFile string
want error
}{
"GetParameterFromOpenAPISchema": {
reason: "get parameter from OpenAPI schema",
fileDir: TestDir,
fileName: "normalOpenAPISchemaFixed.json",
targetFile: "normalOpenAPISchemaParameter.json",
want: nil,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rawFile := filepath.Join(tc.fileDir, tc.fileName)
raw, err := ioutil.ReadFile(rawFile)
got, err := getParameterFromOpenAPISchema(raw)
if (err == nil && tc.want != nil) || (err != nil && tc.want == nil) {
t.Errorf("%s\ngetParameterFromOpenAPISchema(...): -want %s, +got %s", tc.reason, tc.want, err)
}
targetFile := filepath.Join(tc.fileDir, tc.targetFile)
expect, err := ioutil.ReadFile(targetFile)
if err != nil {
t.Errorf("%s\ngetParameterFromOpenAPISchema(...): target file %s could not be read: %s", tc.reason, targetFile, err)
}
if strings.Compare(string(got), string(expect)) != 0 {
t.Errorf("%s\ngetParameterFromOpenAPISchema(...): pure parameter schema %s isn't retrieved", tc.reason, targetFile)
}
})
}
}
@@ -0,0 +1,34 @@
{
"openapi": "3.0.0",
"info": {
"title": "Generated by cue.",
"version": "no version"
},
"paths": {},
"components": {
"schemas": {
"parameter": {
"type": "object",
"required": [
"image"
],
"properties": {
"cmd": {
"description": "+usage=Commands to run in the container",
"type": "array",
"items": {
"type": "string"
}
},
"image": {
"description": "+usage=Which image would you like to use for your service\n+short=i",
"type": "string"
},
"cpu": {
"type": "string"
}
}
}
}
}
}
@@ -0,0 +1,38 @@
{
"openapi": "3.0.0",
"info": {
"title": "Generated by cue.",
"version": "no version"
},
"paths": {},
"components": {
"schemas": {
"parameter": {
"type": "object",
"required": [
"image"
],
"properties": {
"cmd": {
"description": "Commands to run in the container",
"title": "cmd",
"type": "array",
"items": {
"type": "string"
}
},
"image": {
"description": "Which image would you like to use for your service",
"title": "image",
"type": "string"
},
"cpu": {
"title": "cpu",
"type": "string"
}
}
}
}
}
}
@@ -0,0 +1 @@
{"properties":{"cmd":{"description":"Commands to run in the container","items":{"type":"string"},"title":"cmd","type":"array"},"cpu":{"title":"cpu","type":"string"},"image":{"description":"Which image would you like to use for your service","title":"image","type":"string"}},"required":["image"],"type":"object"}
+88
View File
@@ -0,0 +1,88 @@
{
"openapi": "3.0.0",
"info": {
"title": "Generated by cue.",
"version": "no version"
},
"paths": {},
"components": {
"schemas": {
"parameter": {
"type": "object",
"required": [
"image",
"port"
],
"properties": {
"cmd": {
"description": "+usage=Commands to run in the container",
"type": "array",
"items": {
"type": "string"
}
},
"env": {
"description": "+usage=Define arguments by using environment variables",
"type": "array",
"items": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"description": "+usage=Environment variable name",
"type": "string"
},
"value": {
"description": "+usage=The value of the environment variable",
"type": "string"
},
"valueFrom": {
"description": "+usage=Specifies a source the value of this var should come from",
"type": "object",
"required": [
"secretKeyRef"
],
"properties": {
"secretKeyRef": {
"description": "+usage=Selects a key of a secret in the pod's namespace",
"type": "object",
"required": [
"name",
"key"
],
"properties": {
"name": {
"description": "+usage=The name of the secret in the pod's namespace to select from",
"type": "string"
},
"key": {
"description": "+usage=The key of the secret to select from. Must be a valid secret key",
"type": "string"
}
}
}
}
}
}
}
},
"image": {
"description": "+usage=Which image would you like to use for your service\n+short=i",
"type": "string"
},
"port": {
"description": "+usage=Which port do you want customer traffic sent to\n+short=p",
"type": "integer",
"default": 80
},
"cpu": {
"description": "+usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)",
"type": "string"
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
{"properties":{"cmd":{"description":"Commands to run in the container","items":{"type":"string"},"title":"cmd","type":"array"},"cpu":{"description":"Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)","title":"cpu","type":"string"},"env":{"description":"Define arguments by using environment variables","items":{"properties":{"name":{"description":"Environment variable name","title":"name","type":"string"},"value":{"description":"The value of the environment variable","title":"value","type":"string"},"valueFrom":{"description":"Specifies a source the value of this var should come from","properties":{"secretKeyRef":{"description":"Selects a key of a secret in the pod's namespace","properties":{"key":{"description":"The key of the secret to select from. Must be a valid secret key","title":"key","type":"string"},"name":{"description":"The name of the secret in the pod's namespace to select from","title":"name","type":"string"}},"required":["name","key"],"title":"secretKeyRef","type":"object"}},"required":["secretKeyRef"],"title":"valueFrom","type":"object"}},"required":["name"],"type":"object"},"title":"env","type":"array"},"image":{"description":"Which image would you like to use for your service","title":"image","type":"string"},"port":{"default":80,"description":"Which port do you want customer traffic sent to","title":"port","type":"integer"}},"required":["image","port"],"type":"object"}
+7
View File
@@ -0,0 +1,7 @@
project: {
name: string
}
parameter: {
min: int
}
@@ -0,0 +1,7 @@
project: {
name: string
}
noParameter: {
min: int
}
+64 -1
View File
@@ -1,21 +1,28 @@
package common
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
"cuelang.org/go/encoding/openapi"
certmanager "github.com/wonderflow/cert-manager-api/pkg/apis/certmanager/v1"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/config"
core "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/types"
mycue "github.com/oam-dev/kubevela/pkg/cue"
)
var (
@@ -60,3 +67,59 @@ func HTTPGet(ctx context.Context, url string) ([]byte, error) {
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
// GetCUEParameterValue converts definitions to cue format
func GetCUEParameterValue(cueStr string) (cue.Value, error) {
r := cue.Runtime{}
template, err := r.Compile("", cueStr+mycue.BaseTemplate)
if err != nil {
return cue.Value{}, err
}
tempStruct, err := template.Value().Struct()
if err != nil {
return cue.Value{}, err
}
// find the parameter definition
var paraDef cue.FieldInfo
var found bool
for i := 0; i < tempStruct.Len(); i++ {
paraDef = tempStruct.Field(i)
if paraDef.Name == "parameter" {
found = true
break
}
}
if !found {
return cue.Value{}, errors.New("parameter not exist")
}
arguments := paraDef.Value
return arguments, nil
}
// GenOpenAPI generates OpenAPI json schema from cue.Instance
func GenOpenAPI(inst *cue.Instance) ([]byte, error) {
if inst.Err != nil {
return nil, inst.Err
}
defaultConfig := &openapi.Config{}
b, err := openapi.Gen(inst, defaultConfig)
if err != nil {
return nil, err
}
var out = &bytes.Buffer{}
_ = json.Indent(out, b, "", " ")
return out.Bytes(), nil
}
// GenOpenAPIFromFile generates OpenAPI json schema from cue file
func GenOpenAPIFromFile(filePath string, fileName string) ([]byte, error) {
filename := filepath.FromSlash(fileName)
inst := cue.Build(load.Instances([]string{filename}, &load.Config{
Dir: filePath,
}))[0]
if inst.Err != nil {
return nil, inst.Err
}
return GenOpenAPI(inst)
}
+208
View File
@@ -0,0 +1,208 @@
package common
import (
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"testing"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
)
func TestGetCUEParameterValue(t *testing.T) {
type want struct {
err error
}
var validCueStr = `
parameter: {
min: int
}
`
var CueStrNotContainParameter = `
output: {
min: int
}
`
cases := map[string]struct {
reason string
cueStr string
want want
}{
"GetCUEParameterValue": {
reason: "cue string is valid",
cueStr: validCueStr,
want: want{
err: nil,
},
},
"CUEStringNotContainParameter": {
reason: "cue string doesn't contain Parameter",
cueStr: CueStrNotContainParameter,
want: want{
err: fmt.Errorf("parameter not exist"),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
_, err := GetCUEParameterValue(tc.cueStr)
if tc.want.err != nil {
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nGenOpenAPIFromFile(...): -want error, +got error:\n%s", tc.reason, diff)
}
}
})
}
}
func TestGetCUEParameterValue4RareCases(t *testing.T) {
type want struct {
errMsg string
}
var invalidCueStr = `
name
`
cases := map[string]struct {
reason string
cueStr string
want want
}{
"CUEStringInvalid": {
reason: "cue string is invalid",
cueStr: invalidCueStr,
want: want{
errMsg: "reference \"name\" not found",
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
_, err := GetCUEParameterValue(tc.cueStr)
if diff := cmp.Diff(tc.want.errMsg, err.Error(), test.EquateConditions()); diff != "" {
t.Errorf("\n%s\nGenOpenAPIFromFile(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
func TestGenOpenAPIFromFile(t *testing.T) {
type want struct {
data []byte
err error
}
var dir = "testdata"
var validCueFile = "workload1.cue"
var validTargetSchema = "workload1.json"
targetFile := filepath.Join(dir, validTargetSchema)
expect, _ := ioutil.ReadFile(targetFile)
normalWant := want{
data: expect,
err: nil,
}
cases := map[string]struct {
reason string
fileDir string
fileName string
targetSchema string
want want
}{
"GenOpenAPIFromFile": {
reason: "generate OpenAPI schema from a cue file",
fileDir: dir,
fileName: validCueFile,
targetSchema: validTargetSchema,
want: normalWant,
},
"CueFileIsNotValid": {
reason: "generate OpenAPI schema from a cue file which is invalid",
fileDir: dir,
fileName: "workload2Invalid.cue",
want: want{
data: nil,
err: errors.New("expected 'EOF', found '}'"),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := GenOpenAPIFromFile(tc.fileDir, tc.fileName)
if tc.want.err != nil {
if diff := cmp.Diff(tc.want.err, errors.New(err.Error()), test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nGenOpenAPIFromFile(...): -want error, +got error:\n%s", tc.reason, diff)
}
}
if diff := cmp.Diff(tc.want.data, got); diff != "" {
t.Errorf("\n%s\nGenOpenAPIFromFile(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestGenOpenAPI(t *testing.T) {
type want struct {
data []byte
err error
}
var dir = "testdata"
var validCueFile = "workload1.cue"
var validTargetSchema = "workload1.json"
targetFile := filepath.Join(dir, validTargetSchema)
expect, _ := ioutil.ReadFile(targetFile)
normalWant := want{
data: expect,
err: nil,
}
f := filepath.FromSlash(validCueFile)
inst := cue.Build(load.Instances([]string{f}, &load.Config{
Dir: dir,
}))[0]
cases := map[string]struct {
reason string
fileDir string
fileName string
targetSchema string
want want
}{
"GenOpenAPI": {
reason: "generate OpenAPI schema",
fileDir: dir,
fileName: validCueFile,
targetSchema: validTargetSchema,
want: normalWant,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := GenOpenAPI(inst)
if tc.want.err != nil {
if diff := cmp.Diff(tc.want.err, errors.New(err.Error()), test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nGenOpenAPIFromFile(...): -want error, +got error:\n%s", tc.reason, diff)
}
}
if diff := cmp.Diff(tc.want.data, got); diff != "" {
t.Errorf("\n%s\nGenOpenAPIFromFile(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
+10
View File
@@ -0,0 +1,10 @@
#parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
cpu?: string
}
+34
View File
@@ -0,0 +1,34 @@
{
"openapi": "3.0.0",
"info": {
"title": "Generated by cue.",
"version": "no version"
},
"paths": {},
"components": {
"schemas": {
"parameter": {
"type": "object",
"required": [
"image"
],
"properties": {
"cmd": {
"description": "+usage=Commands to run in the container",
"type": "array",
"items": {
"type": "string"
}
},
"image": {
"description": "+usage=Which image would you like to use for your service\n+short=i",
"type": "string"
},
"cpu": {
"type": "string"
}
}
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
#parameter:
// +usage=Which image would you like to use for your service
// +short=i
image: string
}