/* 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) } }