package cli import ( "context" "errors" "fmt" "io/ioutil" "net/http" "os" "os/exec" "os/signal" "path/filepath" "runtime" "syscall" "time" "github.com/spf13/cobra" "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/utils/system" cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" "github.com/oam-dev/kubevela/references/plugins" ) const ( // SideBar file name for docsify SideBar = "_sidebar.md" // NavBar file name for docsify NavBar = "_navbar.md" // IndexHTML file name for docsify IndexHTML = "index.html" // CSS file name for custom CSS CSS = "custom.css" // README file name for docsify README = "README.md" ) const ( // Port is the port for reference docs website Port = ":18081" ) var webSite bool // NewCapabilityShowCommand shows the reference doc for a workload type or trait func NewCapabilityShowCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "show", Short: "Show the reference doc for a workload type or trait", Long: "Show the reference doc for a workload type or trait", Example: `show webservice`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return c.SetConfig() }, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("please specify a workload type or trait") } ctx := context.Background() capabilityName := args[0] if webSite { return startReferenceDocsSite(ctx, c, ioStreams, capabilityName) } return showReferenceConsole(ctx, c, ioStreams, capabilityName) }, Annotations: map[string]string{ types.TagCommandType: types.TypeStart, }, } cmd.Flags().BoolVarP(&webSite, "web", "", false, " start web doc site") cmd.SetOut(ioStreams.Out) return cmd } func startReferenceDocsSite(ctx context.Context, c types.Args, ioStreams cmdutil.IOStreams, capabilityName string) error { home, err := system.GetVelaHomeDir() if err != nil { return err } referenceHome := filepath.Join(home, "reference") definitionPath := filepath.Join(referenceHome, "capabilities") if _, err := os.Stat(definitionPath); err != nil && os.IsNotExist(err) { if err := os.MkdirAll(definitionPath, 0750); err != nil { return err } } docsPath := filepath.Join(referenceHome, "docs") if _, err := os.Stat(docsPath); err != nil && os.IsNotExist(err) { if err := os.MkdirAll(docsPath, 0750); err != nil { return err } } capabilities, _, err := plugins.SyncDefinitionsToLocal(ctx, c, definitionPath) if err != nil { return err } // check input capability is valid var capabilityIsValid bool var capabilityType types.CapType for _, c := range capabilities { if c.Name == capabilityName { capabilityIsValid = true capabilityType = c.Type break } } if !capabilityIsValid { return fmt.Errorf("%s is not a valid workload type or trait", capabilityName) } ref := &plugins.MarkdownReference{} if err := ref.CreateMarkdown(capabilities, docsPath, plugins.ReferenceSourcePath); err != nil { return err } if err := generateSideBar(capabilities, docsPath); err != nil { return err } if err := generateNavBar(docsPath); err != nil { return err } if err := generateIndexHTML(docsPath); err != nil { return err } if err := generateCustomCSS(docsPath); err != nil { return err } if err := generateREADME(capabilities, docsPath); err != nil { return err } var capabilityPath string switch capabilityType { case types.TypeWorkload: capabilityPath = plugins.WorkloadTypePath case types.TypeTrait: capabilityPath = plugins.TraitPath case types.TypeScope: case types.TypeComponentDefinition: // TODO: add ComponentDefinition } url := fmt.Sprintf("http://127.0.0.1%s/#/%s/%s", Port, capabilityPath, capabilityName) server := &http.Server{ Addr: Port, Handler: http.FileServer(http.Dir(docsPath)), ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } server.SetKeepAlivesEnabled(true) errCh := make(chan error, 1) launch(server, errCh) select { case err = <-errCh: return err case <-time.After(time.Second): if err := OpenBrowser(url); err != nil { ioStreams.Infof("automatically invoking browser failed: %v\nPlease visit %s for reference docs", err, url) } } sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGTERM) <-sc ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() return server.Shutdown(ctx) } func launch(server *http.Server, errChan chan<- error) { go func() { err := server.ListenAndServe() if err != nil && errors.Is(err, http.ErrServerClosed) { errChan <- err } }() } func generateSideBar(capabilities []types.Capability, docsPath string) error { sideBar := filepath.Join(docsPath, SideBar) workloads, traits := getWorkloadsAndTraits(capabilities) f, err := os.Create(sideBar) if err != nil { return err } if _, err := f.WriteString("- Workload Types\n"); err != nil { return err } for _, w := range workloads { if _, err := f.WriteString(fmt.Sprintf(" - [%s](%s/%s.md)\n", w, plugins.WorkloadTypePath, w)); err != nil { return err } } if _, err := f.WriteString("- Traits\n"); err != nil { return err } for _, t := range traits { if _, err := f.WriteString(fmt.Sprintf(" - [%s](%s/%s.md)\n", t, plugins.TraitPath, t)); err != nil { return err } } return nil } func generateNavBar(docsPath string) error { sideBar := filepath.Join(docsPath, NavBar) _, err := os.Create(sideBar) if err != nil { return err } return nil } func generateIndexHTML(docsPath string) error { indexHTML := ` KubeVela Reference Docs
` return ioutil.WriteFile(filepath.Join(docsPath, IndexHTML), []byte(indexHTML), 0600) } func generateCustomCSS(docsPath string) error { css := ` body { overflow: auto !important; }` return ioutil.WriteFile(filepath.Join(docsPath, CSS), []byte(css), 0600) } func generateREADME(capabilities []types.Capability, docsPath string) error { readmeMD := filepath.Join(docsPath, README) f, err := os.Create(readmeMD) if err != nil { return err } if _, err := f.WriteString("# KubeVela Reference Docs for Workload Types and Traits\n" + "Click the navigation bar on the left or the links below to look into the detailed referennce of a Workload type or a Trait.\n"); err != nil { return err } workloads, traits := getWorkloadsAndTraits(capabilities) if _, err := f.WriteString("## Workload Types\n"); err != nil { return err } for _, w := range workloads { if _, err := f.WriteString(fmt.Sprintf(" - [%s](%s/%s.md)\n", w, plugins.WorkloadTypePath, w)); err != nil { return err } } if _, err := f.WriteString("## Traits\n"); err != nil { return err } for _, t := range traits { if _, err := f.WriteString(fmt.Sprintf(" - [%s](%s/%s.md)\n", t, plugins.TraitPath, t)); err != nil { return err } } return nil } func getWorkloadsAndTraits(capabilities []types.Capability) ([]string, []string) { var workloads, traits []string for _, c := range capabilities { switch c.Type { case types.TypeWorkload: workloads = append(workloads, c.Name) case types.TypeTrait: traits = append(traits, c.Name) case types.TypeScope: case types.TypeComponentDefinition: // TODO get ComponentDefinitions } } return workloads, traits } func showReferenceConsole(ctx context.Context, c types.Args, ioStreams cmdutil.IOStreams, capabilityName string) error { home, err := system.GetVelaHomeDir() if err != nil { return err } referenceHome := filepath.Join(home, "reference") definitionPath := filepath.Join(referenceHome, "capabilities") if _, err := os.Stat(definitionPath); err != nil && os.IsNotExist(err) { if err := os.MkdirAll(definitionPath, 0750); err != nil { return err } } capability, err := plugins.SyncDefinitionToLocal(ctx, c, definitionPath, capabilityName) if err != nil { return err } ref := &plugins.ConsoleReference{} propertyConsole, err := ref.GenerateCapabilityProperties(capability) if err != nil { return err } for _, p := range propertyConsole { ioStreams.Info(p.TableName) p.TableObject.Render() ioStreams.Info("\n") } return nil } // OpenBrowser will open browser by url in different OS system // nolint:gosec func OpenBrowser(url string) error { var err error switch runtime.GOOS { case "linux": err = exec.Command("xdg-open", url).Start() case "windows": err = exec.Command("cmd", "/C", "start", url).Run() case "darwin": err = exec.Command("open", url).Start() default: err = fmt.Errorf("unsupported platform") } return err }