github.com/oam-dev/kubevela@v1.9.11/references/cli/show.go (about) 1 /* 2 Copyright 2021 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 cli 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "os" 24 "os/exec" 25 "os/signal" 26 "path/filepath" 27 "runtime" 28 "strconv" 29 "strings" 30 "syscall" 31 "time" 32 33 "github.com/pkg/errors" 34 "github.com/spf13/cobra" 35 36 "github.com/kubevela/workflow/pkg/cue/packages" 37 38 "github.com/oam-dev/kubevela/apis/types" 39 "github.com/oam-dev/kubevela/pkg/utils/common" 40 "github.com/oam-dev/kubevela/pkg/utils/system" 41 cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" 42 "github.com/oam-dev/kubevela/references/docgen" 43 ) 44 45 const ( 46 // SideBar file name for docsify 47 SideBar = "_sidebar.md" 48 // NavBar file name for docsify 49 NavBar = "_navbar.md" 50 // IndexHTML file name for docsify 51 IndexHTML = "index.html" 52 // CSS file name for custom CSS 53 CSS = "custom.css" 54 // README file name for docsify 55 README = "README.md" 56 ) 57 58 const ( 59 // Port is the port for reference docs website 60 Port = ":18081" 61 ) 62 63 var webSite bool 64 var generateDocOnly bool 65 var showFormat string 66 67 // NewCapabilityShowCommand shows the reference doc for a component type or trait 68 func NewCapabilityShowCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command { 69 var revision, path, location, i18nPath string 70 cmd := &cobra.Command{ 71 Use: "show", 72 Short: "Show the reference doc for a component, trait, policy or workflow.", 73 Long: "Show the reference doc for component, trait, policy or workflow types. 'vela show' equals with 'vela def show'. ", 74 Example: `0. Run 'vela show' directly to start a web server for all reference docs. 75 1. Generate documentation for ComponentDefinition webservice: 76 > vela show webservice -n vela-system 77 2. Generate documentation for local CUE Definition file webservice.cue: 78 > vela show webservice.cue 79 3. Generate documentation for local Cloud Resource Definition YAML alibaba-vpc.yaml: 80 > vela show alibaba-vpc.yaml 81 4. Specify output format, markdown supported: 82 > vela show webservice --format markdown 83 5. Specify a language for output, by default, it's english. You can also load your own translation script: 84 > vela show webservice --location zh 85 > vela show webservice --location zh --i18n https://kubevela.io/reference-i18n.json 86 6. Show doc for a specified revision, it must exist in control plane cluster: 87 > vela show webservice --revision v1 88 7. Generate docs for all capabilities into folder $HOME/.vela/reference/docs/ 89 > vela show 90 8. Generate all docs and start a doc server 91 > vela show --web 92 `, 93 RunE: func(cmd *cobra.Command, args []string) error { 94 ctx := context.Background() 95 var capabilityName string 96 if len(args) > 0 { 97 capabilityName = args[0] 98 } else if !webSite { 99 cmd.Println("generating all capability docs into folder '~/.vela/reference/docs/', use '--web' to start a server for browser.") 100 generateDocOnly = true 101 } 102 namespace, err := GetFlagNamespaceOrEnv(cmd, c) 103 if err != nil { 104 return err 105 } 106 var ver int 107 if revision != "" { 108 // v1, 1, both need to work 109 version := strings.TrimPrefix(revision, "v") 110 ver, err = strconv.Atoi(version) 111 if err != nil { 112 return fmt.Errorf("invalid revision: %w", err) 113 } 114 } 115 if webSite || generateDocOnly { 116 return startReferenceDocsSite(ctx, namespace, c, ioStreams, capabilityName) 117 } 118 if path != "" || showFormat == "md" || showFormat == "markdown" { 119 return ShowReferenceMarkdown(ctx, c, ioStreams, capabilityName, path, location, i18nPath, namespace, int64(ver)) 120 } 121 return ShowReferenceConsole(ctx, c, ioStreams, capabilityName, namespace, location, i18nPath, int64(ver)) 122 }, 123 Annotations: map[string]string{ 124 types.TagCommandType: types.TypeStart, 125 types.TagCommandOrder: order, 126 }, 127 } 128 129 cmd.Flags().BoolVarP(&webSite, "web", "", false, "start web doc site") 130 cmd.Flags().StringVarP(&showFormat, "format", "", "", "specify format of output data, by default it's a pretty human readable format, you can specify markdown(md)") 131 cmd.Flags().StringVarP(&revision, "revision", "r", "", "Get the specified revision of a definition. Use def get to list revisions.") 132 cmd.Flags().StringVarP(&path, "path", "p", "", "Specify the path for of the doc generated from definition.") 133 cmd.Flags().StringVarP(&location, "location", "l", "", "specify the location for of the doc generated from definition, now supported options 'zh', 'en'. ") 134 cmd.Flags().StringVarP(&i18nPath, "i18n", "", "https://kubevela.io/reference-i18n.json", "specify the location for of the doc generated from definition, now supported options 'zh', 'en'. ") 135 136 addNamespaceAndEnvArg(cmd) 137 cmd.SetOut(ioStreams.Out) 138 return cmd 139 } 140 141 func generateWebsiteDocs(capabilities []types.Capability, docsPath string) error { 142 if err := generateSideBar(capabilities, docsPath); err != nil { 143 return err 144 } 145 146 if err := generateNavBar(docsPath); err != nil { 147 return err 148 } 149 150 if err := generateIndexHTML(docsPath); err != nil { 151 return err 152 } 153 if err := generateCustomCSS(docsPath); err != nil { 154 return err 155 } 156 157 if err := generateREADME(capabilities, docsPath); err != nil { 158 return err 159 } 160 return nil 161 } 162 163 func startReferenceDocsSite(ctx context.Context, ns string, c common.Args, ioStreams cmdutil.IOStreams, capabilityName string) error { 164 home, err := system.GetVelaHomeDir() 165 if err != nil { 166 return err 167 } 168 referenceHome := filepath.Join(home, "reference") 169 170 docsPath := filepath.Join(referenceHome, "docs") 171 if _, err := os.Stat(docsPath); err != nil && os.IsNotExist(err) { 172 if err := os.MkdirAll(docsPath, 0750); err != nil { 173 return err 174 } 175 } 176 capabilities, err := docgen.GetNamespacedCapabilitiesFromCluster(ctx, ns, c, nil) 177 if err != nil { 178 return err 179 } 180 // check whether input capability is valid 181 var capabilityIsValid bool 182 var capabilityType types.CapType 183 for _, c := range capabilities { 184 if capabilityName == "" { 185 capabilityIsValid = true 186 break 187 } 188 if c.Name == capabilityName { 189 capabilityIsValid = true 190 capabilityType = c.Type 191 break 192 } 193 } 194 if !capabilityIsValid { 195 return fmt.Errorf("%s is not a valid component, trait, policy or workflow", capabilityName) 196 } 197 198 cli, err := c.GetClient() 199 if err != nil { 200 return err 201 } 202 config, err := c.GetConfig() 203 if err != nil { 204 return err 205 } 206 pd, err := packages.NewPackageDiscover(config) 207 if err != nil { 208 return err 209 } 210 ref := &docgen.MarkdownReference{ 211 ParseReference: docgen.ParseReference{ 212 Client: cli, 213 I18N: &docgen.En, 214 }, 215 } 216 217 if err := ref.CreateMarkdown(ctx, capabilities, docsPath, true, pd); err != nil { 218 return err 219 } 220 221 if err = generateWebsiteDocs(capabilities, docsPath); err != nil { 222 return err 223 } 224 225 if generateDocOnly { 226 return nil 227 } 228 229 if capabilityType != types.TypeWorkload && capabilityType != types.TypeTrait && 230 capabilityType != types.TypeComponentDefinition && capabilityType != types.TypeWorkflowStep && capabilityType != "" { 231 return fmt.Errorf("unsupported type: %v", capabilityType) 232 } 233 var suffix = capabilityName 234 if suffix != "" { 235 suffix = "/" + suffix 236 } 237 url := fmt.Sprintf("http://127.0.0.1%s/#/%s%s", Port, capabilityType, suffix) 238 server := &http.Server{ 239 Addr: Port, 240 Handler: http.FileServer(http.Dir(docsPath)), 241 ReadTimeout: 5 * time.Second, 242 WriteTimeout: 10 * time.Second, 243 } 244 server.SetKeepAlivesEnabled(true) 245 errCh := make(chan error, 1) 246 247 launch(server, errCh) 248 249 select { 250 case err = <-errCh: 251 return err 252 case <-time.After(time.Second): 253 if err := OpenBrowser(url); err != nil { 254 ioStreams.Infof("automatically invoking browser failed: %v\nPlease visit %s for reference docs", err, url) 255 } 256 } 257 258 sc := make(chan os.Signal, 1) 259 signal.Notify(sc, syscall.SIGTERM) 260 261 <-sc 262 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 263 defer cancel() 264 return server.Shutdown(ctx) 265 } 266 267 func launch(server *http.Server, errChan chan<- error) { 268 go func() { 269 err := server.ListenAndServe() 270 if err != nil && errors.Is(err, http.ErrServerClosed) { 271 errChan <- err 272 } 273 }() 274 } 275 276 func generateSideBar(capabilities []types.Capability, docsPath string) error { 277 sideBar := filepath.Join(docsPath, SideBar) 278 components, traits, workflowSteps, policies := getDefinitions(capabilities) 279 f, err := os.Create(sideBar) // nolint 280 if err != nil { 281 return err 282 } 283 if _, err := f.WriteString("- Components Types\n"); err != nil { 284 return err 285 } 286 287 for _, c := range components { 288 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", c, types.TypeComponentDefinition, c); err != nil { 289 return err 290 } 291 } 292 if _, err := f.WriteString("- Traits\n"); err != nil { 293 return err 294 } 295 for _, t := range traits { 296 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", t, types.TypeTrait, t); err != nil { 297 return err 298 } 299 } 300 if _, err := f.WriteString("- Workflow Steps\n"); err != nil { 301 return err 302 } 303 for _, t := range workflowSteps { 304 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", t, types.TypeWorkflowStep, t); err != nil { 305 return err 306 } 307 } 308 309 if _, err := f.WriteString("- Policies\n"); err != nil { 310 return err 311 } 312 for _, t := range policies { 313 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", t, types.TypePolicy, t); err != nil { 314 return err 315 } 316 } 317 return nil 318 } 319 320 func generateNavBar(docsPath string) error { 321 sideBar := filepath.Join(docsPath, NavBar) 322 _, err := os.Create(sideBar) // nolint 323 if err != nil { 324 return err 325 } 326 return nil 327 } 328 329 func generateIndexHTML(docsPath string) error { 330 indexHTML := ` 331 <!DOCTYPE html> 332 <html lang="en"> 333 <head> 334 <meta charset="UTF-8"> 335 <title>KubeVela Reference Docs</title> 336 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 337 <meta name="description" content="Description"> 338 <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"> 339 <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css"> 340 <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify-sidebar-collapse/dist/sidebar.min.css" /> 341 <link rel="stylesheet" href="./custom.css"> 342 </head> 343 <body> 344 <div id="app"></div> 345 <script> 346 window.$docsify = { 347 name: 'KubeVela Customized Reference Docs', 348 loadSidebar: true, 349 loadNavbar: true, 350 subMaxLevel: 1, 351 alias: { 352 '/_sidebar.md': '/_sidebar.md', 353 '/_navbar.md': '/_navbar.md' 354 }, 355 formatUpdated: '{MM}/{DD}/{YYYY} {HH}:{mm}:{ss}', 356 } 357 </script> 358 <!-- Docsify v4 --> 359 <script src="//cdn.jsdelivr.net/npm/docsify@4"></script> 360 <script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script> 361 <!-- docgen --> 362 <script src="//cdn.jsdelivr.net/npm/docsify-sidebar-collapse/dist/docsify-sidebar-collapse.min.js"></script> 363 </body> 364 </html> 365 ` 366 return os.WriteFile(filepath.Join(docsPath, IndexHTML), []byte(indexHTML), 0600) 367 } 368 369 func generateCustomCSS(docsPath string) error { 370 css := ` 371 body { 372 overflow: auto !important; 373 }` 374 return os.WriteFile(filepath.Join(docsPath, CSS), []byte(css), 0600) 375 } 376 377 func generateREADME(capabilities []types.Capability, docsPath string) error { 378 readmeMD := filepath.Join(docsPath, README) 379 f, err := os.Create(readmeMD) // nolint 380 if err != nil { 381 return err 382 } 383 if _, err := f.WriteString("# KubeVela Reference Docs for Component Types, Traits and WorkflowSteps\n" + 384 "Click the navigation bar on the left or the links below to look into the detailed reference of a Workload type, Trait or Workflow Step.\n"); err != nil { 385 return err 386 } 387 388 workloads, traits, workflowSteps, policies := getDefinitions(capabilities) 389 390 if _, err := f.WriteString("## Component Types\n"); err != nil { 391 return err 392 } 393 394 for _, w := range workloads { 395 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", w, types.TypeComponentDefinition, w); err != nil { 396 return err 397 } 398 } 399 if _, err := f.WriteString("## Traits\n"); err != nil { 400 return err 401 } 402 403 for _, t := range traits { 404 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", t, types.TypeTrait, t); err != nil { 405 return err 406 } 407 } 408 409 if _, err := f.WriteString("## Workflow Steps\n"); err != nil { 410 return err 411 } 412 for _, t := range workflowSteps { 413 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", t, types.TypeWorkflowStep, t); err != nil { 414 return err 415 } 416 } 417 418 if _, err := f.WriteString("## Policies\n"); err != nil { 419 return err 420 } 421 for _, t := range policies { 422 if _, err := fmt.Fprintf(f, " - [%s](%s/%s.md)\n", t, types.TypePolicy, t); err != nil { 423 return err 424 } 425 } 426 427 return nil 428 } 429 430 func getDefinitions(capabilities []types.Capability) ([]string, []string, []string, []string) { 431 var components, traits, workflowSteps, policies []string 432 for _, c := range capabilities { 433 switch c.Type { 434 case types.TypeComponentDefinition: 435 components = append(components, c.Name) 436 case types.TypeTrait: 437 traits = append(traits, c.Name) 438 case types.TypeWorkflowStep: 439 workflowSteps = append(workflowSteps, c.Name) 440 case types.TypePolicy: 441 policies = append(policies, c.Name) 442 case types.TypeWorkload: 443 default: 444 } 445 } 446 return components, traits, workflowSteps, policies 447 } 448 449 // ShowReferenceConsole will show capability reference in console 450 func ShowReferenceConsole(ctx context.Context, c common.Args, ioStreams cmdutil.IOStreams, capabilityName string, ns, location, i18nPath string, rev int64) error { 451 cli, err := c.GetClient() 452 if err != nil { 453 return err 454 } 455 ref := &docgen.ConsoleReference{} 456 paserRef, err := genRefParser(capabilityName, ns, location, i18nPath, rev) 457 if err != nil { 458 return err 459 } 460 paserRef.Client = cli 461 ref.ParseReference = paserRef 462 return ref.Show(ctx, c, ioStreams, capabilityName, ns, rev) 463 } 464 465 // ShowReferenceMarkdown will show capability in "markdown" format 466 func ShowReferenceMarkdown(ctx context.Context, c common.Args, ioStreams cmdutil.IOStreams, capabilityNameOrPath, outputPath, location, i18nPath, ns string, rev int64) error { 467 cli, err := c.GetClient() 468 if err != nil { 469 return err 470 } 471 ref := &docgen.MarkdownReference{} 472 parseRef, err := genRefParser(capabilityNameOrPath, ns, location, i18nPath, rev) 473 if err != nil { 474 return err 475 } 476 parseRef.Client = cli 477 ref.ParseReference = parseRef 478 if err := ref.GenerateReferenceDocs(ctx, c, outputPath); err != nil { 479 return errors.Wrap(err, "failed to generate reference docs") 480 } 481 if outputPath != "" { 482 ioStreams.Infof("Generated docs in %s for %s in %s/%s.md\n", ref.I18N, capabilityNameOrPath, outputPath, ref.DefinitionName) 483 } 484 return nil 485 } 486 487 func genRefParser(capabilityNameOrPath, ns, location, i18nPath string, rev int64) (docgen.ParseReference, error) { 488 ref := docgen.ParseReference{} 489 if location != "" { 490 docgen.LoadI18nData(i18nPath) 491 } 492 if strings.HasSuffix(capabilityNameOrPath, ".yaml") || strings.HasSuffix(capabilityNameOrPath, ".cue") { 493 // read from local file 494 localFilePath := capabilityNameOrPath 495 fileName := filepath.Base(localFilePath) 496 ref.DefinitionName = strings.TrimSuffix(strings.TrimSuffix(fileName, ".yaml"), ".cue") 497 ref.Local = &docgen.FromLocal{Paths: []string{localFilePath}} 498 } else { 499 ref.DefinitionName = capabilityNameOrPath 500 ref.Remote = &docgen.FromCluster{Namespace: ns, Rev: rev} 501 } 502 switch strings.ToLower(location) { 503 case "zh", "cn", "chinese": 504 ref.I18N = &docgen.Zh 505 case "", "en", "english": 506 ref.I18N = &docgen.En 507 default: 508 return ref, fmt.Errorf("unknown location %s for i18n translation", location) 509 } 510 return ref, nil 511 } 512 513 // OpenBrowser will open browser by url in different OS system 514 // nolint:gosec 515 func OpenBrowser(url string) error { 516 var err error 517 switch runtime.GOOS { 518 case "linux": 519 err = exec.Command("xdg-open", url).Start() 520 case "windows": 521 err = exec.Command("cmd", "/C", "start", url).Run() 522 case "darwin": 523 err = exec.Command("open", url).Start() 524 default: 525 err = fmt.Errorf("unsupported platform") 526 } 527 return err 528 }