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  }