/* 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 plugins import ( "fmt" "io/ioutil" "os" "path/filepath" "strconv" "strings" "cuelang.org/go/cue" "github.com/olekukonko/tablewriter" "github.com/oam-dev/kubevela/apis/types" mycue "github.com/oam-dev/kubevela/pkg/cue" "github.com/oam-dev/kubevela/pkg/utils/common" ) const ( // BaseRefPath is the target path for reference docs BaseRefPath = "docs/en/developers/references" // ReferenceSourcePath is the location for source reference ReferenceSourcePath = "hack/references" // ComponentDefinitionTypePath is the URL path for component typed capability ComponentDefinitionTypePath = "component-types" // WorkloadTypePath is the URL path for workload typed capability WorkloadTypePath = "workload-types" // TraitPath is the URL path for trait typed capability TraitPath = "traits" ) // Int64Type is int64 type type Int64Type = int64 // StringType is string type type StringType = string // BoolType is bool type type BoolType = bool // Reference is the struct for capability information type Reference interface { prepareParameter(tableName string, parameterList []ReferenceParameter) string } // ParseReference is used to include the common function `parseParameter` type ParseReference struct { } // MarkdownReference is the struct for capability information in type MarkdownReference struct { ParseReference } // ConsoleReference is the struct for capability information in console type ConsoleReference struct { ParseReference TableName string `json:"tableName"` TableObject *tablewriter.Table `json:"tableObject"` } // ConfigurationYamlSample stores the configuration yaml sample for capabilities var ConfigurationYamlSample = map[string]string{ "autoscale": ` name: testapp services: express-server: ... autoscale: min: 1 max: 4 cron: startAt: "14:00" duration: "2h" days: "Monday, Thursday" replicas: 2 timezone: "America/Los_Angeles" cpuPercent: 10 `, "metrics": ` name: my-app-name services: my-service-name: ... metrics: format: "prometheus" port: 8080 path: "/metrics" scheme: "http" enabled: true `, "rollout": ` servcies: express-server: ... rollout: replicas: 2 stepWeight: 50 interval: "10s" `, "route": ` name: my-app-name services: my-service-name: ... route: domain: example.com issuer: tls rules: - path: /testapp rewriteTarget: / `, "scaler": ` name: my-app-name services: my-service-name: ... scaler: replicas: 100 `, "task": ` name: my-app-name services: my-service-name: type: task image: perl count: 10 cmd: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] `, "webservice": ` name: my-app-name services: my-service-name: type: webservice # could be skipped image: oamdev/testapp:v1 cmd: ["node", "server.js"] port: 8080 cpu: "0.1" env: - name: FOO value: bar - name: FOO valueFrom: secretKeyRef: name: bar key: bar `, "worker": ` name: my-app-name services: my-service-name: type: worker image: oamdev/testapp:v1 cmd: ["node", "server.js"] `, } // ReferenceParameter is the parameter section of CUE template type ReferenceParameter struct { types.Parameter `json:",inline,omitempty"` // PrintableType is same to `parameter.Type` which could be printable PrintableType string `json:"printableType"` // Depth marks the depth for calling of function `parseParameters` Depth *int `json:"depth"` } var refContent string var recurseDepth *int var propertyConsole []ConsoleReference var displayFormat *string func setDisplayFormat(format string) { displayFormat = &format } // GenerateReferenceDocs generates reference docs func (ref *MarkdownReference) GenerateReferenceDocs(baseRefPath string) error { c, err := common.InitBaseRestConfig() if err != nil { return err } caps, err := LoadAllInstalledCapability("default", c) if err != nil { return fmt.Errorf("failed to generate reference docs for all capabilities: %w", err) } if baseRefPath == "" { baseRefPath = BaseRefPath } return ref.CreateMarkdown(caps, baseRefPath, ReferenceSourcePath) } // CreateMarkdown creates markdown based on capabilities func (ref *MarkdownReference) CreateMarkdown(caps []types.Capability, baseRefPath, referenceSourcePath string) error { setDisplayFormat("markdown") var capabilityType string var specificationType string for _, c := range caps { switch c.Type { case types.TypeWorkload: capabilityType = WorkloadTypePath specificationType = "workload type" case types.TypeComponentDefinition: capabilityType = ComponentDefinitionTypePath specificationType = "component type" case types.TypeTrait: capabilityType = TraitPath specificationType = "trait" default: return fmt.Errorf("the type of the capability is not right") } fileName := fmt.Sprintf("%s.md", c.Name) filePath := filepath.Join(baseRefPath, capabilityType) if _, err := os.Stat(filePath); err != nil && os.IsNotExist(err) { if err := os.MkdirAll(filePath, 0750); err != nil { return err } } markdownFile := filepath.Join(baseRefPath, capabilityType, fileName) f, err := os.OpenFile(filepath.Clean(markdownFile), os.O_WRONLY|os.O_CREATE, 0600) if err != nil { return fmt.Errorf("failed to open file %s: %w", markdownFile, err) } if err = os.Truncate(markdownFile, 0); err != nil { return fmt.Errorf("failed to truncate file %s: %w", markdownFile, err) } capName := c.Name cueValue, err := common.GetCUEParameterValue(c.CueTemplate) if err != nil { return fmt.Errorf("failed to retrieve `parameters` value from %s with err: %w", c.Name, err) } refContent = "" var defaultDepth = 0 recurseDepth = &defaultDepth capNameInTitle := strings.Title(capName) if err := ref.parseParameters(cueValue, "Properties", defaultDepth); err != nil { return err } title := fmt.Sprintf("# %s", capNameInTitle) description := fmt.Sprintf("\n\n## Description\n\n%s", c.Description) specificationIntro := fmt.Sprintf("List of all configuration options for a `%s` %s.", capNameInTitle, specificationType) specificationContent := ref.generateSpecification(capName) if err != nil { return err } specification := fmt.Sprintf("\n\n## Specification\n\n%s\n\n%s", specificationIntro, specificationContent) // it's fine if the conflict info files not found conflictWithAndMoreSection, _ := ref.generateConflictWithAndMore(capName, referenceSourcePath) refContent = title + description + specification + refContent + conflictWithAndMoreSection if _, err := f.WriteString(refContent); err != nil { return err } if err := f.Close(); err != nil { return err } } return nil } // prepareParameter prepares the table content for each property func (ref *MarkdownReference) prepareParameter(tableName string, parameterList []ReferenceParameter) string { refContent := fmt.Sprintf("\n\n%s\n\n", tableName) refContent += "Name | Description | Type | Required | Default \n" refContent += "------------ | ------------- | ------------- | ------------- | ------------- \n" for _, p := range parameterList { printableDefaultValue := ref.getPrintableDefaultValue(p.Default) refContent += fmt.Sprintf(" %s | %s | %s | %t | %s \n", p.Name, p.Usage, p.PrintableType, p.Required, printableDefaultValue) } return refContent } // prepareParameter prepares the table content for each property func (ref *ConsoleReference) prepareParameter(tableName string, parameterList []ReferenceParameter) ConsoleReference { table := tablewriter.NewWriter(os.Stdout) table.SetColWidth(100) table.SetHeader([]string{"Name", "Description", "Type", "Required", "Default"}) for _, p := range parameterList { printableDefaultValue := ref.getPrintableDefaultValue(p.Default) table.Append([]string{p.Name, p.Usage, p.PrintableType, strconv.FormatBool(p.Required), printableDefaultValue}) } return ConsoleReference{TableName: tableName, TableObject: table} } // parseParameters parses every parameter func (ref *ParseReference) parseParameters(paraValue cue.Value, paramKey string, depth int) error { var params []ReferenceParameter *recurseDepth++ switch paraValue.Kind() { case cue.StructKind: arguments, err := paraValue.Struct() if err != nil { return fmt.Errorf("arguments not defined as struct %w", err) } 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 = mycue.GetDefault(def) } param.Short, param.Usage, param.Alias = mycue.RetrieveComments(val) param.Type = val.IncompleteKind() switch val.IncompleteKind() { case cue.StructKind: depth := *recurseDepth if subField, _ := val.Struct(); subField.Len() == 0 { // err cannot be not nil,so ignore it if mapValue, ok := val.Elem(); ok { // In the future we could recursive call to surpport complex map-value(struct or list) param.PrintableType = fmt.Sprintf("map[string]%s", mapValue.IncompleteKind().String()) } else { return fmt.Errorf("failed to got Map kind from %s", param.Name) } } else { if err := ref.parseParameters(val, name, depth); err != nil { return err } param.PrintableType = fmt.Sprintf("[%s](#%s)", name, name) } case cue.ListKind: elem, success := val.Elem() if !success { return fmt.Errorf("failed to get elements from %s", val) } switch elem.Kind() { case cue.StructKind: param.PrintableType = fmt.Sprintf("[[]%s](#%s)", name, name) depth := *recurseDepth if err := ref.parseParameters(elem, name, depth); err != nil { return err } 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 *displayFormat { case "markdown": tableName := fmt.Sprintf("%s %s", strings.Repeat("#", depth+2), paramKey) ref := MarkdownReference{} refContent = ref.prepareParameter(tableName, params) + refContent case "console": ref := ConsoleReference{} tableName := fmt.Sprintf("%s %s", strings.Repeat("#", depth+1), paramKey) console := ref.prepareParameter(tableName, params) propertyConsole = append([]ConsoleReference{console}, propertyConsole...) } return nil } // getPrintableDefaultValue converts the value in `interface{}` type to be printable func (ref *ParseReference) getPrintableDefaultValue(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 "" } // generateSpecification generates Specification part for reference docs func (ref *MarkdownReference) generateSpecification(capabilityName string) string { return fmt.Sprintf("```yaml%s```", ConfigurationYamlSample[capabilityName]) } // generateConflictWithAndMore generates Section `Conflicts With` and more like `How xxx works` in reference docs func (ref *MarkdownReference) generateConflictWithAndMore(capabilityName string, referenceSourcePath string) (string, error) { conflictWithFile, err := filepath.Abs(filepath.Join(referenceSourcePath, "conflictsWithAndMore", fmt.Sprintf("%s.md", capabilityName))) if err != nil { return "", fmt.Errorf("failed to locate conflictWith file: %w", err) } data, err := ioutil.ReadFile(filepath.Clean(conflictWithFile)) if err != nil { return "", err } return "\n" + string(data), nil } // GenerateCapabilityProperties get all properties of a capability func (ref *ConsoleReference) GenerateCapabilityProperties(capability *types.Capability) ([]ConsoleReference, error) { setDisplayFormat("console") capName := capability.Name cueValue, err := common.GetCUEParameterValue(capability.CueTemplate) if err != nil { return nil, fmt.Errorf("failed to retrieve `parameters` value from %s with err: %w", capName, err) } var defaultDepth = 0 recurseDepth = &defaultDepth if err := ref.parseParameters(cueValue, "Properties", defaultDepth); err != nil { return nil, err } return propertyConsole, nil }