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 }