github.com/oam-dev/kubevela@v1.9.11/references/docgen/markdown.go (about)

     1  /*
     2   Copyright 2022 The KubeVela Authors.
     3  
     4   Licensed under the Apache License, Version 2.0 (the "License");
     5   you may not use this file except in compliance with the License.
     6   You may obtain a copy of the License at
     7  
     8   	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10   Unless required by applicable law or agreed to in writing, software
    11   distributed under the License is distributed on an "AS IS" BASIS,
    12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   See the License for the specific language governing permissions and
    14   limitations under the License.
    15  */
    16  
    17  package docgen
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"sort"
    25  	"strings"
    26  
    27  	"github.com/kubevela/workflow/pkg/cue/packages"
    28  	"github.com/pkg/errors"
    29  	"golang.org/x/text/cases"
    30  	"golang.org/x/text/language"
    31  	"k8s.io/klog/v2"
    32  
    33  	"github.com/oam-dev/kubevela/apis/types"
    34  	"github.com/oam-dev/kubevela/pkg/cue"
    35  	"github.com/oam-dev/kubevela/pkg/utils/common"
    36  )
    37  
    38  // AllComponentTypes means trait can be applied to all component types
    39  const AllComponentTypes = "*"
    40  
    41  // MarkdownReference is the struct for capability information in
    42  type MarkdownReference struct {
    43  	Filter          func(types.Capability) bool
    44  	AllInOne        bool
    45  	ForceExample    bool
    46  	CustomDocHeader string
    47  	ParseReference
    48  }
    49  
    50  // GenerateReferenceDocs generates reference docs
    51  func (ref *MarkdownReference) GenerateReferenceDocs(ctx context.Context, c common.Args, baseRefPath string) error {
    52  	caps, err := ref.getCapabilities(ctx, c)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	var pd *packages.PackageDiscover
    57  	if ref.Remote != nil {
    58  		pd = ref.Remote.PD
    59  	}
    60  	if pd == nil {
    61  		pd = func() *packages.PackageDiscover {
    62  			rpd, err := c.GetPackageDiscover()
    63  			if err != nil {
    64  				klog.Error("fail to build package discover", err)
    65  				return nil
    66  			}
    67  			return rpd
    68  		}()
    69  	}
    70  	return ref.CreateMarkdown(ctx, caps, baseRefPath, false, pd)
    71  }
    72  
    73  // CreateMarkdown creates markdown based on capabilities
    74  func (ref *MarkdownReference) CreateMarkdown(ctx context.Context, caps []types.Capability, baseRefPath string, catalog bool, pd *packages.PackageDiscover) error {
    75  
    76  	sort.Slice(caps, func(i, j int) bool {
    77  		return caps[i].Name < caps[j].Name
    78  	})
    79  
    80  	var all string
    81  	ref.DisplayFormat = Markdown
    82  	for _, c := range caps {
    83  		if ref.Filter != nil && !ref.Filter(c) {
    84  			continue
    85  		}
    86  		capDoc, err := ref.GenerateMarkdownForCap(ctx, c, pd, ref.AllInOne)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		if baseRefPath == "" {
    91  			fmt.Println(capDoc)
    92  			continue
    93  		}
    94  		if ref.AllInOne {
    95  			all += capDoc + "\n\n"
    96  			continue
    97  		}
    98  
    99  		refPath := baseRefPath
   100  		if catalog {
   101  			// catalog by capability type with folder
   102  			refPath = filepath.Join(baseRefPath, string(c.Type))
   103  		}
   104  
   105  		if _, err := os.Stat(refPath); err != nil && os.IsNotExist(err) {
   106  			if err := os.MkdirAll(refPath, 0750); err != nil {
   107  				return err
   108  			}
   109  		}
   110  
   111  		refPath = strings.TrimSuffix(refPath, "/")
   112  		fileName := fmt.Sprintf("%s.md", c.Name)
   113  		markdownFile := filepath.Join(refPath, fileName)
   114  		f, err := os.OpenFile(filepath.Clean(markdownFile), os.O_WRONLY|os.O_CREATE, 0600)
   115  		if err != nil {
   116  			return fmt.Errorf("failed to open file %s: %w", markdownFile, err)
   117  		}
   118  		if err = os.Truncate(markdownFile, 0); err != nil {
   119  			return fmt.Errorf("failed to truncate file %s: %w", markdownFile, err)
   120  		}
   121  
   122  		if _, err := f.WriteString(capDoc); err != nil {
   123  			return err
   124  		}
   125  		if err := f.Close(); err != nil {
   126  			return err
   127  		}
   128  	}
   129  	if !ref.AllInOne {
   130  		return nil
   131  	}
   132  	all = ref.CustomDocHeader + all
   133  	if baseRefPath != "" {
   134  		return os.WriteFile(baseRefPath, []byte(all), 0600)
   135  	}
   136  	fmt.Println(all)
   137  	return nil
   138  }
   139  
   140  // GenerateMarkdownForCap will generate markdown for one capability
   141  // nolint:gocyclo
   142  func (ref *MarkdownReference) GenerateMarkdownForCap(_ context.Context, c types.Capability, pd *packages.PackageDiscover, containSuffix bool) (string, error) {
   143  	var (
   144  		description   string
   145  		base          string
   146  		sample        string
   147  		specification string
   148  		generatedDoc  string
   149  		baseDoc       string
   150  		err           error
   151  	)
   152  	switch c.Type {
   153  	case types.TypeWorkload, types.TypeComponentDefinition, types.TypeTrait, types.TypeWorkflowStep, types.TypePolicy:
   154  	default:
   155  		return "", fmt.Errorf("type(%s) of the capability(%s) is not supported for now", c.Type, c.Name)
   156  	}
   157  
   158  	capName := c.Name
   159  	lang := ref.I18N
   160  	capNameInTitle := ref.makeReadableTitle(capName)
   161  	switch c.Category {
   162  	case types.CUECategory:
   163  		cueValue, err := common.GetCUEParameterValue(c.CueTemplate, pd)
   164  		if err != nil && !errors.Is(err, cue.ErrParameterNotExist) {
   165  			return "", fmt.Errorf("failed to retrieve `parameters` value from %s with err: %w", c.Name, err)
   166  		}
   167  		var defaultDepth = 0
   168  		generatedDoc, _, err = ref.parseParameters(capName, cueValue, Specification, defaultDepth, containSuffix)
   169  		if err != nil {
   170  			return "", err
   171  		}
   172  		if c.Type == types.TypeComponentDefinition {
   173  			var warnErr error
   174  			baseDoc, warnErr = GetBaseResourceKinds(c.CueTemplate, pd, ref.Client.RESTMapper())
   175  			if warnErr != nil {
   176  				klog.Warningf("failed to get base resource kinds for %s: %v", c.Name, warnErr)
   177  			}
   178  		}
   179  	case types.TerraformCategory:
   180  		generatedDoc, err = ref.GenerateTerraformCapabilityPropertiesAndOutputs(c)
   181  		if err != nil {
   182  			return "", err
   183  		}
   184  	default:
   185  		return "", fmt.Errorf("unsupport category %s from capability %s", c.Category, capName)
   186  	}
   187  	title := fmt.Sprintf("---\ntitle:  %s\n---", capNameInTitle)
   188  	if ref.AllInOne {
   189  		title = fmt.Sprintf("## %s", capNameInTitle)
   190  	}
   191  	sampleContent := c.Example
   192  	if sampleContent == "" {
   193  		sampleContent = DefinitionDocSamples[capName]
   194  	}
   195  	descriptionI18N := DefinitionDocDescription[capName]
   196  	if descriptionI18N == "" {
   197  		descriptionI18N = c.Description
   198  	}
   199  
   200  	parameterDoc := DefinitionDocParameters[capName]
   201  	if parameterDoc == "" {
   202  		if strings.TrimSpace(generatedDoc) == "" {
   203  			generatedDoc = "This capability has no arguments."
   204  		}
   205  		parameterDoc = generatedDoc
   206  	}
   207  
   208  	var sharp = "##"
   209  	exampleTitle := lang.Get(Examples)
   210  	baseTitle := lang.Get(Base)
   211  	specificationTitle := lang.Get(Specification)
   212  	if ref.AllInOne {
   213  		sharp = "###"
   214  		exampleTitle += " (" + capName + ")"
   215  		specificationTitle += " (" + capName + ")"
   216  		baseTitle += " (" + capName + ")"
   217  	}
   218  	description = fmt.Sprintf("\n\n%s %s\n\n%s", sharp, lang.Get(Description), strings.TrimSpace(lang.Get(descriptionI18N)))
   219  	if !strings.HasSuffix(description, lang.Get(".")) {
   220  		description += lang.Get(".")
   221  	}
   222  
   223  	if c.Type == types.TypeWorkflowStep {
   224  		var scopeI18N string
   225  		switch c.Labels["custom.definition.oam.dev/scope"] {
   226  		case "":
   227  			scopeI18N = "This step type is valid in both Application and WorkflowRun"
   228  		case "Application":
   229  			scopeI18N = "This step type is only valid in Application"
   230  		case "WorkflowRun":
   231  			scopeI18N = "This step type is only valid in WorkflowRun"
   232  		}
   233  		scope := fmt.Sprintf("\n\n%s %s\n\n%s%s", sharp, lang.Get(Scope), strings.TrimSpace(lang.Get(scopeI18N)), lang.Get("."))
   234  		description += scope
   235  	}
   236  
   237  	if c.Type == types.TypeTrait {
   238  
   239  		if c.Labels[types.LabelDefinitionHidden] == "true" {
   240  			description += "\n\n> " + lang.Get("For now this trait is hidden from the VelaUX. Available when using CLI.")
   241  		}
   242  		description += "\n\n### " + lang.Get("Apply To Component Types") + "\n\n"
   243  		var applyto string
   244  		if len(c.AppliesTo) == 1 && c.AppliesTo[0] == AllComponentTypes {
   245  			applyto += lang.Get("All Component Types.")
   246  		} else {
   247  			applyto += lang.Get("Component based on the following kinds of resources:") + "\n"
   248  			for _, ap := range c.AppliesTo {
   249  				applyto += "- " + ap + "\n"
   250  			}
   251  		}
   252  		if applyto == "" {
   253  			applyto = lang.Get("All Component Types.")
   254  		}
   255  		description += applyto + "\n"
   256  	}
   257  
   258  	if sampleContent != "" {
   259  		sample = fmt.Sprintf("\n\n%s %s\n\n%s", sharp, exampleTitle, sampleContent)
   260  	} else if ref.ForceExample {
   261  		fmt.Printf("You must provide example doc for the new added definition \"%s\", place the example doc in the /refereces/docgen/def-doc folders, for more details refer to https://kubevela.io/docs/contributor/cli-ref-doc#how-the-docs-generated", capName)
   262  		os.Exit(1)
   263  	}
   264  	if c.Category == types.CUECategory && baseDoc != "" {
   265  		base = fmt.Sprintf("\n\n%s %s\n\n%s", sharp, baseTitle, baseDoc)
   266  	}
   267  	specification = fmt.Sprintf("\n\n%s %s\n%s", sharp, specificationTitle, parameterDoc)
   268  
   269  	return title + description + base + sample + specification, nil
   270  }
   271  
   272  func (ref *MarkdownReference) makeReadableTitle(title string) string {
   273  	if !strings.Contains(title, "-") {
   274  		return cases.Title(language.Und).String(title)
   275  	}
   276  	var name string
   277  	provider := strings.Split(title, "-")[0]
   278  	switch provider {
   279  	case "alibaba":
   280  		name = "AlibabaCloud"
   281  	case "aws":
   282  		name = "AWS"
   283  	case "azure":
   284  		name = "Azure"
   285  	default:
   286  		return cases.Title(language.Und).String(title)
   287  	}
   288  	cloudResource := strings.Replace(title, provider+"-", "", 1)
   289  	return fmt.Sprintf("%s %s", ref.I18N.Get(name), strings.ToUpper(cloudResource))
   290  }
   291  
   292  // getParameterString prepares the table content for each property
   293  func (ref *MarkdownReference) getParameterString(tableName string, parameterList []ReferenceParameter, category types.CapabilityCategory) string {
   294  	tab := fmt.Sprintf("\n\n%s\n\n", tableName)
   295  	if tableName == "" || tableName == Specification {
   296  		tab = "\n\n"
   297  	}
   298  	tab += fmt.Sprintf(" %s | %s | %s | %s | %s \n", ref.I18N.Get("Name"), ref.I18N.Get("Description"), ref.I18N.Get("Type"), ref.I18N.Get("Required"), ref.I18N.Get("Default"))
   299  	tab += fmt.Sprintf(" %s | %s | %s | %s | %s \n",
   300  		strings.Repeat("-", len(ref.I18N.Get("Name"))),
   301  		strings.Repeat("-", len(ref.I18N.Get("Description"))),
   302  		strings.Repeat("-", len(ref.I18N.Get("Type"))),
   303  		strings.Repeat("-", len(ref.I18N.Get("Required"))),
   304  		strings.Repeat("-", len(ref.I18N.Get("Default"))))
   305  
   306  	switch category {
   307  	case types.CUECategory:
   308  		for _, p := range parameterList {
   309  			if !p.Ignore {
   310  				printableDefaultValue := ref.getCUEPrintableDefaultValue(p.Default)
   311  				tab += fmt.Sprintf(" %s | %s | %s | %t | %s \n", p.Name, ref.prettySentence(p.Usage), ref.formatTableString(p.PrintableType), p.Required, printableDefaultValue)
   312  			}
   313  		}
   314  	case types.TerraformCategory:
   315  		// Terraform doesn't have default value
   316  		for _, p := range parameterList {
   317  			tab += fmt.Sprintf(" %s | %s | %s | %t | %s \n", p.Name, ref.prettySentence(p.Usage), ref.formatTableString(p.PrintableType), p.Required, "")
   318  		}
   319  	default:
   320  	}
   321  	return tab
   322  }
   323  
   324  // GenerateTerraformCapabilityPropertiesAndOutputs generates Capability properties and outputs for Terraform ComponentDefinition
   325  func (ref *MarkdownReference) GenerateTerraformCapabilityPropertiesAndOutputs(capability types.Capability) (string, error) {
   326  	var references string
   327  
   328  	variableTables, outputsTable, err := ref.parseTerraformCapabilityParameters(capability)
   329  	if err != nil {
   330  		return "", err
   331  	}
   332  	for _, t := range variableTables {
   333  		references += ref.getParameterString(t.Name, t.Parameters, types.CUECategory)
   334  	}
   335  	for _, t := range outputsTable {
   336  		references += ref.prepareTerraformOutputs(t.Name, t.Parameters)
   337  	}
   338  	return references, nil
   339  }
   340  
   341  // getParameterString prepares the table content for each property
   342  func (ref *MarkdownReference) prepareTerraformOutputs(tableName string, parameterList []ReferenceParameter) string {
   343  	if len(parameterList) == 0 {
   344  		return ""
   345  	}
   346  	tfdoc := fmt.Sprintf("\n\n%s\n\n", tableName)
   347  	if tableName == "" {
   348  		tfdoc = "\n\n"
   349  	}
   350  	tfdoc += fmt.Sprintf(" %s | %s \n", ref.I18N.Get("Name"), ref.I18N.Get("Description"))
   351  	tfdoc += " ------------ | ------------- \n"
   352  
   353  	for _, p := range parameterList {
   354  		tfdoc += fmt.Sprintf(" %s | %s\n", p.Name, p.Usage)
   355  	}
   356  
   357  	return tfdoc
   358  }