Files
kubevela/references/plugins/parser.go
Jianbo Sun b24e7523d8 Feat: generate docs for reference automatically (#4377)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Feat: refactor hardcode example to embd.FS

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: refactor doc gen for general types

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: update generate format

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: generate terraform reference docs

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Feat: add definition reference generate script

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: refine output format

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: remove dup annotation

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: update doc

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: add i18n support

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Feat: add translation

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Feat: add policy definition gen

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: add compatibility for lable Annotation change

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: add more tests

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Feat: allow mark example doc url on annotation

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>

Fix: align vela show with vela def doc-gen, add vela def show equals with vela show

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-07-18 19:22:55 +08:00

537 lines
18 KiB
Go

/*
Copyright 2022 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 plugins
import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"github.com/getkin/kin-openapi/openapi3"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/controller/utils"
velacue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/cue/model"
"github.com/oam-dev/kubevela/pkg/cue/packages"
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
pkgUtils "github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/terraform"
)
// ParseReference is used to include the common function `parseParameter`
type ParseReference struct {
Client client.Client
I18N *I18n `json:"i18n"`
Remote *FromCluster `json:"remote"`
Local *FromLocal `json:"local"`
DefinitionName string `json:"definitionName"`
DisplayFormat string
}
func (ref *ParseReference) getCapabilities(ctx context.Context, c common.Args) ([]types.Capability, error) {
var (
caps []types.Capability
pd *packages.PackageDiscover
)
switch {
case ref.Local != nil:
lcap, err := ParseLocalFile(ref.Local.Path, c)
if err != nil {
return nil, fmt.Errorf("failed to get capability from local file %s: %w", ref.DefinitionName, err)
}
caps = append(caps, *lcap)
case ref.Remote != nil:
config, err := c.GetConfig()
if err != nil {
return nil, err
}
pd, err = packages.NewPackageDiscover(config)
if err != nil {
return nil, err
}
ref.Remote.PD = pd
if ref.DefinitionName == "" {
caps, err = LoadAllInstalledCapability("default", c)
if err != nil {
return nil, fmt.Errorf("failed to get all capabilityes: %w", err)
}
} else {
var rcap *types.Capability
if ref.Remote.Rev == 0 {
rcap, err = GetCapabilityByName(ctx, c, ref.DefinitionName, ref.Remote.Namespace, pd)
if err != nil {
return nil, fmt.Errorf("failed to get capability %s: %w", ref.DefinitionName, err)
}
} else {
rcap, err = GetCapabilityFromDefinitionRevision(ctx, c, pd, ref.Remote.Namespace, ref.DefinitionName, ref.Remote.Rev)
if err != nil {
return nil, fmt.Errorf("failed to get revision %v of capability %s: %w", ref.Remote.Rev, ref.DefinitionName, err)
}
}
caps = []types.Capability{*rcap}
}
default:
return nil, fmt.Errorf("failed to get capability %s without namespace or local filepath", ref.DefinitionName)
}
return caps, nil
}
func (ref *ParseReference) prettySentence(s string) string {
if strings.TrimSpace(s) == "" {
return ""
}
return ref.I18N.Get(s) + ref.I18N.Get(".")
}
// prepareConsoleParameter prepares the table content for each property
func (ref *ParseReference) prepareConsoleParameter(tableName string, parameterList []ReferenceParameter, category types.CapabilityCategory) ConsoleReference {
table := tablewriter.NewWriter(os.Stdout)
table.SetColWidth(100)
table.SetHeader([]string{ref.I18N.Get("Name"), ref.I18N.Get("Description"), ref.I18N.Get("Type"), ref.I18N.Get("Required"), ref.I18N.Get("Default")})
switch category {
case types.CUECategory:
for _, p := range parameterList {
if !p.Ignore {
printableDefaultValue := ref.getCUEPrintableDefaultValue(p.Default)
table.Append([]string{ref.I18N.Get(p.Name), ref.prettySentence(p.Usage), ref.I18N.Get(p.PrintableType), ref.I18N.Get(strconv.FormatBool(p.Required)), ref.I18N.Get(printableDefaultValue)})
}
}
case types.HelmCategory, types.KubeCategory:
for _, p := range parameterList {
printableDefaultValue := ref.getJSONPrintableDefaultValue(p.JSONType, p.Default)
table.Append([]string{ref.I18N.Get(p.Name), ref.prettySentence(p.Usage), ref.I18N.Get(p.PrintableType), ref.I18N.Get(strconv.FormatBool(p.Required)), ref.I18N.Get(printableDefaultValue)})
}
case types.TerraformCategory:
// Terraform doesn't have default value
for _, p := range parameterList {
table.Append([]string{ref.I18N.Get(p.Name), ref.prettySentence(p.Usage), ref.I18N.Get(p.PrintableType), ref.I18N.Get(strconv.FormatBool(p.Required)), ""})
}
default:
}
return ConsoleReference{TableName: tableName, TableObject: table}
}
// parseParameters parses every parameter
func (ref *ParseReference) parseParameters(capName string, paraValue cue.Value, paramKey string, depth int, containSuffix bool) (string, []ConsoleReference, error) {
var doc string
var console []ConsoleReference
var params []ReferenceParameter
var suffixTitle = " (" + capName + ")"
var suffixRef = "-" + strings.ToLower(capName)
if !containSuffix || capName == "" {
suffixTitle = ""
suffixRef = ""
}
switch paraValue.Kind() {
case cue.StructKind:
arguments, err := paraValue.Struct()
if err != nil {
return "", nil, fmt.Errorf("arguments not defined as struct %w", err)
}
if arguments.Len() == 0 {
var param ReferenceParameter
param.Name = "\\-"
param.Required = true
tl := paraValue.Template()
if tl != nil { // is map type
param.PrintableType = fmt.Sprintf("map[string]%s", tl("").IncompleteKind().String())
} else {
param.PrintableType = "{}"
}
params = append(params, param)
}
for i := 0; i < arguments.Len(); i++ {
var param ReferenceParameter
fi := arguments.Field(i)
if fi.IsDefinition {
continue
}
val := fi.Value
name := fi.Name
param.Name = name
param.Required = !fi.IsOptional
if def, ok := val.Default(); ok && def.IsConcrete() {
param.Default = velacue.GetDefault(def)
}
param.Short, param.Usage, param.Alias, param.Ignore = velacue.RetrieveComments(val)
param.Type = val.IncompleteKind()
switch val.IncompleteKind() {
case cue.StructKind:
if subField, err := val.Struct(); err == nil && subField.Len() == 0 { // err cannot be not nil,so ignore it
if mapValue, ok := val.Elem(); ok {
// In the future we could recursively call to support complex map-value(struct or list)
source, converted := mapValue.Source().(*ast.Ident)
if converted && len(source.Name) != 0 {
param.PrintableType = fmt.Sprintf("map[string]%s", source.Name)
} else {
param.PrintableType = fmt.Sprintf("map[string]%s", mapValue.IncompleteKind().String())
}
} else {
return "", nil, fmt.Errorf("failed to got Map kind from %s", param.Name)
}
} else {
subDoc, subConsole, err := ref.parseParameters(capName, val, name, depth+1, containSuffix)
if err != nil {
return "", nil, err
}
param.PrintableType = fmt.Sprintf("[%s](#%s%s)", name, strings.ToLower(name), suffixRef)
doc += subDoc
console = append(console, subConsole...)
}
case cue.ListKind:
elem, success := val.Elem()
if !success {
// fail to get elements, use the value of ListKind to be the type
param.Type = val.Kind()
param.PrintableType = val.IncompleteKind().String()
break
}
switch elem.Kind() {
case cue.StructKind:
param.PrintableType = fmt.Sprintf("[[]%s](#%s%s)", name, strings.ToLower(name), suffixRef)
subDoc, subConsole, err := ref.parseParameters(capName, elem, name, depth+1, containSuffix)
if err != nil {
return "", nil, err
}
doc += subDoc
console = append(console, subConsole...)
default:
param.Type = elem.Kind()
param.PrintableType = fmt.Sprintf("[]%s", elem.IncompleteKind().String())
}
default:
param.PrintableType = param.Type.String()
}
params = append(params, param)
}
default:
//
}
switch ref.DisplayFormat {
case Markdown, "":
// markdown defines the contents that display in web
var tableName string
if paramKey != Specification {
length := depth + 3
if length >= 5 {
length = 5
}
tableName = fmt.Sprintf("%s %s%s", strings.Repeat("#", length), paramKey, suffixTitle)
}
mref := MarkdownReference{}
mref.I18N = ref.I18N
doc = mref.getParameterString(tableName, params, types.CUECategory) + doc
case Console:
length := depth + 1
if length >= 3 {
length = 3
}
cref := ConsoleReference{}
tableName := fmt.Sprintf("%s %s", strings.Repeat("#", length), paramKey)
console = append([]ConsoleReference{cref.prepareConsoleParameter(tableName, params, types.CUECategory)}, console...)
}
return doc, console, nil
}
// getCUEPrintableDefaultValue converts the value in `interface{}` type to be printable
func (ref *ParseReference) getCUEPrintableDefaultValue(v interface{}) string {
if v == nil {
return ""
}
switch value := v.(type) {
case Int64Type:
return strconv.FormatInt(value, 10)
case StringType:
if v == "" {
return "empty"
}
return value
case BoolType:
return strconv.FormatBool(value)
}
return ""
}
func (ref *ParseReference) getJSONPrintableDefaultValue(dataType string, value interface{}) string {
if value != nil {
return strings.TrimSpace(fmt.Sprintf("%v", value))
}
defaultValueMap := map[string]string{
"number": "0",
"boolean": "false",
"string": "\"\"",
"object": "{}",
"array": "[]",
}
return defaultValueMap[dataType]
}
// CommonReference contains parameters info of HelmCategory and KubuCategory type capability at present
type CommonReference struct {
Name string
Parameters []ReferenceParameter
Depth int
}
// CommonSchema is a struct contains *openapi3.Schema style parameter
type CommonSchema struct {
Name string
Schemas *openapi3.Schema
}
// GenerateHelmAndKubeProperties get all properties of a Helm/Kube Category type capability
func (ref *ParseReference) GenerateHelmAndKubeProperties(ctx context.Context, capability *types.Capability) ([]CommonReference, []ConsoleReference, error) {
cmName := fmt.Sprintf("%s%s", types.CapabilityConfigMapNamePrefix, capability.Name)
switch capability.Type {
case types.TypeComponentDefinition:
cmName = fmt.Sprintf("component-%s", cmName)
case types.TypeTrait:
cmName = fmt.Sprintf("trait-%s", cmName)
default:
}
var cm v1.ConfigMap
commonRefs = make([]CommonReference, 0)
if err := ref.Client.Get(ctx, client.ObjectKey{Namespace: capability.Namespace, Name: cmName}, &cm); err != nil {
return nil, nil, err
}
data, ok := cm.Data[types.OpenapiV3JSONSchema]
if !ok {
return nil, nil, errors.Errorf("configMap doesn't have openapi-v3-json-schema data")
}
parameterJSON := fmt.Sprintf(BaseOpenAPIV3Template, data)
swagger, err := openapi3.NewLoader().LoadFromData(json.RawMessage(parameterJSON))
if err != nil {
return nil, nil, err
}
parameters := swagger.Components.Schemas[model.ParameterFieldName].Value
WalkParameterSchema(parameters, Specification, 0)
var consoleRefs []ConsoleReference
for _, item := range commonRefs {
consoleRefs = append(consoleRefs, ref.prepareConsoleParameter(item.Name, item.Parameters, types.HelmCategory))
}
return commonRefs, consoleRefs, err
}
// GenerateTerraformCapabilityProperties generates Capability properties for Terraform ComponentDefinition
func (ref *ParseReference) parseTerraformCapabilityParameters(capability types.Capability) ([]ReferenceParameterTable, []ReferenceParameterTable, error) {
var (
tables []ReferenceParameterTable
refParameterList []ReferenceParameter
writeConnectionSecretToRefReferenceParameter ReferenceParameter
configuration string
err error
outputsList []ReferenceParameter
outputsTables []ReferenceParameterTable
outputsTableName string
)
outputsTableName = fmt.Sprintf("%s %s\n\n%s", strings.Repeat("#", 3), ref.I18N.Get("Outputs"), ref.I18N.Get("WriteConnectionSecretToRefIntroduction"))
writeConnectionSecretToRefReferenceParameter.Name = terraform.TerraformWriteConnectionSecretToRefName
writeConnectionSecretToRefReferenceParameter.PrintableType = terraform.TerraformWriteConnectionSecretToRefType
writeConnectionSecretToRefReferenceParameter.Required = false
writeConnectionSecretToRefReferenceParameter.Usage = terraform.TerraformWriteConnectionSecretToRefDescription
if capability.ConfigurationType == "remote" {
configuration, err = utils.GetTerraformConfigurationFromRemote(capability.Name, capability.TerraformConfiguration, capability.Path)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve Terraform configuration from %s: %w", capability.Name, err)
}
} else {
configuration = capability.TerraformConfiguration
}
variables, outputs, err := common.ParseTerraformVariables(configuration)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to generate capability properties")
}
for _, v := range variables {
var refParam ReferenceParameter
refParam.Name = v.Name
refParam.PrintableType = strings.ReplaceAll(v.Type, "\n", `\n`)
refParam.Usage = strings.ReplaceAll(v.Description, "\n", `\n`)
refParam.Required = v.Required
refParameterList = append(refParameterList, refParam)
}
refParameterList = append(refParameterList, writeConnectionSecretToRefReferenceParameter)
sort.SliceStable(refParameterList, func(i, j int) bool {
return refParameterList[i].Name < refParameterList[j].Name
})
tables = append(tables, ReferenceParameterTable{
Name: "",
Parameters: refParameterList,
})
var (
writeSecretRefNameParam ReferenceParameter
writeSecretRefNameSpaceParam ReferenceParameter
)
// prepare `## writeConnectionSecretToRef`
writeSecretRefNameParam.Name = "name"
writeSecretRefNameParam.PrintableType = "string"
writeSecretRefNameParam.Required = true
writeSecretRefNameParam.Usage = terraform.TerraformSecretNameDescription
writeSecretRefNameSpaceParam.Name = "namespace"
writeSecretRefNameSpaceParam.PrintableType = "string"
writeSecretRefNameSpaceParam.Required = false
writeSecretRefNameSpaceParam.Usage = terraform.TerraformSecretNamespaceDescription
writeSecretRefParameterList := []ReferenceParameter{writeSecretRefNameParam, writeSecretRefNameSpaceParam}
writeSecretTableName := fmt.Sprintf("%s %s", strings.Repeat("#", 4), terraform.TerraformWriteConnectionSecretToRefName)
sort.SliceStable(writeSecretRefParameterList, func(i, j int) bool {
return writeSecretRefParameterList[i].Name < writeSecretRefParameterList[j].Name
})
tables = append(tables, ReferenceParameterTable{
Name: writeSecretTableName,
Parameters: writeSecretRefParameterList,
})
// outputs
for _, v := range outputs {
var refParam ReferenceParameter
refParam.Name = v.Name
refParam.Usage = v.Description
outputsList = append(outputsList, refParam)
}
sort.SliceStable(outputsList, func(i, j int) bool {
return outputsList[i].Name < outputsList[j].Name
})
outputsTables = append(outputsTables, ReferenceParameterTable{
Name: outputsTableName,
Parameters: outputsList,
})
return tables, outputsTables, nil
}
// ParseLocalFile parse the local file and get name, configuration from local ComponentDefinition file
func ParseLocalFile(localFilePath string, c common.Args) (*types.Capability, error) {
data, err := pkgUtils.ReadRemoteOrLocalPath(localFilePath, false)
if err != nil {
return nil, errors.Wrap(err, "failed to read local file or url")
}
if strings.HasSuffix(localFilePath, "yaml") {
jsonData, err := yaml.YAMLToJSON(data)
if err != nil {
return nil, errors.Wrap(err, "failed to convert yaml data into k8s valid json format")
}
var localDefinition v1beta1.ComponentDefinition
if err = json.Unmarshal(jsonData, &localDefinition); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal data into componentDefinition")
}
desc := localDefinition.ObjectMeta.Annotations["definition.oam.dev/description"]
lcap := &types.Capability{
Name: localDefinition.ObjectMeta.Name,
Description: desc,
TerraformConfiguration: localDefinition.Spec.Schematic.Terraform.Configuration,
ConfigurationType: localDefinition.Spec.Schematic.Terraform.Type,
Path: localDefinition.Spec.Schematic.Terraform.Path,
}
lcap.Type = types.TypeComponentDefinition
lcap.Category = types.TerraformCategory
return lcap, nil
}
// local definition for general definition in CUE format
def := pkgdef.Definition{Unstructured: unstructured.Unstructured{}}
config, err := c.GetConfig()
if err != nil {
return nil, errors.Wrap(err, "get kubeconfig")
}
if err = def.FromCUEString(string(data), config); err != nil {
return nil, errors.Wrapf(err, "failed to parse CUE for definition")
}
lcap, err := ParseCapabilityFromUnstructured(nil, def.Unstructured)
if err != nil {
return nil, errors.Wrapf(err, "fail to parse definition to capability")
}
return &lcap, nil
}
// WalkParameterSchema will extract properties from *openapi3.Schema
func WalkParameterSchema(parameters *openapi3.Schema, name string, depth int) {
if parameters == nil {
return
}
var schemas []CommonSchema
var commonParameters []ReferenceParameter
for k, v := range parameters.Properties {
p := ReferenceParameter{
Parameter: types.Parameter{
Name: k,
Default: v.Value.Default,
Usage: v.Value.Description,
JSONType: v.Value.Type,
},
PrintableType: v.Value.Type,
}
required := false
for _, requiredType := range parameters.Required {
if k == requiredType {
required = true
break
}
}
p.Required = required
if v.Value.Type == "object" {
if v.Value.Properties != nil {
schemas = append(schemas, CommonSchema{
Name: k,
Schemas: v.Value,
})
}
p.PrintableType = fmt.Sprintf("[%s](#%s)", k, k)
}
commonParameters = append(commonParameters, p)
}
commonRefs = append(commonRefs, CommonReference{
Name: fmt.Sprintf("%s %s", strings.Repeat("#", depth+1), name),
Parameters: commonParameters,
Depth: depth + 1,
})
for _, schema := range schemas {
WalkParameterSchema(schema.Schemas, schema.Name, depth+1)
}
}