diff --git a/.github/workflows/godev.yml b/.github/workflows/godev.yml index 681f974..125e37e 100644 --- a/.github/workflows/godev.yml +++ b/.github/workflows/godev.yml @@ -9,6 +9,7 @@ on: jobs: update: + if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 042eb2a..7c1a821 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,3 +41,20 @@ jobs: uses: codecov/codecov-action@v2 with: file: ./coverage.txt + + example: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + - + name: Run + run: | + go run main.go + working-directory: ./example diff --git a/README.md b/README.md index a7f82b4..3dc8aa9 100644 --- a/README.md +++ b/README.md @@ -41,61 +41,10 @@ require ( ) ``` -Next, create a file named `docgen.go` inside that directory containing the -following Go code: +Next, create a file named `main.go` inside that directory containing the +following Go code from [`example/main.go`](example/main.go). -```go -package main - -import ( - "log" - "os" - "path/filepath" - - "github.com/docker/buildx/commands" - "github.com/docker/cli/cli/command" - clidocstool "github.com/docker/cli-docs-tool" - "github.com/spf13/cobra" -) - -const sourcePath = "docs/" - -func main() { - log.SetFlags(0) - - dockerCLI, err := command.NewDockerCli() - if err != nil { - log.Printf("ERROR: %+v", err) - } - - cmd := &cobra.Command{ - Use: "docker [OPTIONS] COMMAND [ARG...]", - Short: "The base command for the Docker CLI.", - DisableAutoGenTag: true, - } - - cmd.AddCommand(commands.NewRootCmd("buildx", true, dockerCLI)) - clidocstool.DisableFlagsInUseLine(cmd) - - cwd, _ := os.Getwd() - source := filepath.Join(cwd, sourcePath) - - // Make sure "source" folder is created first - if err = os.MkdirAll(source, 0755); err != nil { - log.Printf("ERROR: %+v", err) - } - - // Generate Markdown and YAML documentation to "source" folder - if err = clidocstool.GenTree(cmd, source); err != nil { - log.Printf("ERROR: %+v", err) - } -} -``` - -Here we create a new instance of Docker CLI with `command.NewDockerCli` and a -subcommand `commands.NewRootCmd` for `buildx`. - -Finally, we generate Markdown and YAML documentation with `clidocstool.GenTree`. +Running this example should produce the following output: ```console $ go run main.go diff --git a/clidocstool.go b/clidocstool.go index b353f59..6bcadcb 100644 --- a/clidocstool.go +++ b/clidocstool.go @@ -15,38 +15,108 @@ package clidocstool import ( + "io" + "os" + + "github.com/pkg/errors" "github.com/spf13/cobra" ) -// GenTree creates yaml and markdown structured ref files for this command -// and all descendants in the directory given. This function will just -// call GenMarkdownTree and GenYamlTree functions successively. -func GenTree(cmd *cobra.Command, dir string) error { +// Options defines options for cli-docs-tool +type Options struct { + Root *cobra.Command + SourceDir string + TargetDir string + Plugin bool +} + +// Client represents an active cli-docs-tool object +type Client struct { + root *cobra.Command + source string + target string + plugin bool +} + +// New initializes a new cli-docs-tool client +func New(opts Options) (*Client, error) { + if opts.Root == nil { + return nil, errors.New("root cmd required") + } + if len(opts.SourceDir) == 0 { + return nil, errors.New("source dir required") + } + c := &Client{ + root: opts.Root, + source: opts.SourceDir, + plugin: opts.Plugin, + } + if len(opts.TargetDir) == 0 { + c.target = c.source + } else { + c.target = opts.TargetDir + } + if err := os.MkdirAll(c.target, 0755); err != nil { + return nil, err + } + return c, nil +} + +// GenAllTree creates all structured ref files for this command and +// all descendants in the directory given. +func (c *Client) GenAllTree() error { var err error - if err = GenMarkdownTree(cmd, dir); err != nil { + if err = c.GenMarkdownTree(c.root); err != nil { return err } - if err = GenYamlTree(cmd, dir); err != nil { + if err = c.GenYamlTree(c.root); err != nil { return err } return nil } -// VisitAll will traverse all commands from the root. -// This is different from the VisitAll of cobra.Command where only parents -// are checked. -func VisitAll(root *cobra.Command, fn func(*cobra.Command)) { - for _, cmd := range root.Commands() { - VisitAll(cmd, fn) - } - fn(root) -} - // DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all // commands within the tree rooted at cmd. -func DisableFlagsInUseLine(cmd *cobra.Command) { - VisitAll(cmd, func(ccmd *cobra.Command) { +func (c *Client) DisableFlagsInUseLine() { + visitAll(c.root, func(ccmd *cobra.Command) { // do not add a `[flags]` to the end of the usage line. ccmd.DisableFlagsInUseLine = true }) } + +// visitAll traverses all commands from the root. +// This is different from the VisitAll of cobra.Command where only parents +// are checked. +func visitAll(root *cobra.Command, fn func(*cobra.Command)) { + for _, cmd := range root.Commands() { + visitAll(cmd, fn) + } + fn(root) +} + +func fileExists(f string) bool { + info, err := os.Stat(f) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func copyFile(src string, dest string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + destFile, err := os.Create(dest) + if err != nil { + return err + } + defer destFile.Close() + + if _, err = io.Copy(destFile, srcFile); err != nil { + return err + } + return destFile.Sync() +} diff --git a/clidocstool_md.go b/clidocstool_md.go index dc2723a..c95177a 100644 --- a/clidocstool_md.go +++ b/clidocstool_md.go @@ -31,21 +31,25 @@ import ( // GenMarkdownTree will generate a markdown page for this command and all // descendants in the directory given. -func GenMarkdownTree(cmd *cobra.Command, dir string) error { - for _, c := range cmd.Commands() { - if err := GenMarkdownTree(c, dir); err != nil { +func (c *Client) GenMarkdownTree(cmd *cobra.Command) error { + for _, sc := range cmd.Commands() { + if err := c.GenMarkdownTree(sc); err != nil { return err } } - if !cmd.HasParent() { + + // Skip the root command altogether, to prevent generating a useless + // md file for plugins. + if c.plugin && !cmd.HasParent() { return nil } log.Printf("INFO: Generating Markdown for %q", cmd.CommandPath()) mdFile := mdFilename(cmd) - fullPath := filepath.Join(dir, mdFile) + sourcePath := filepath.Join(c.source, mdFile) + targetPath := filepath.Join(c.target, mdFile) - if _, err := os.Stat(fullPath); os.IsNotExist(err) { + if !fileExists(sourcePath) { var icBuf bytes.Buffer icTpl, err := template.New("ic").Option("missingkey=error").Parse(`# {{ .Command }} @@ -63,12 +67,14 @@ func GenMarkdownTree(cmd *cobra.Command, dir string) error { }); err != nil { return err } - if err = ioutil.WriteFile(fullPath, icBuf.Bytes(), 0644); err != nil { + if err = ioutil.WriteFile(targetPath, icBuf.Bytes(), 0644); err != nil { return err } + } else if err := copyFile(sourcePath, targetPath); err != nil { + return err } - content, err := ioutil.ReadFile(fullPath) + content, err := ioutil.ReadFile(targetPath) if err != nil { return err } @@ -91,12 +97,12 @@ func GenMarkdownTree(cmd *cobra.Command, dir string) error { } cont := cs[:start] + "" + "\n" + out + "\n" + cs[end:] - fi, err := os.Stat(fullPath) + fi, err := os.Stat(targetPath) if err != nil { return err } - if err := ioutil.WriteFile(fullPath, []byte(cont), fi.Mode()); err != nil { - return errors.Wrapf(err, "failed to write %s", fullPath) + if err = ioutil.WriteFile(targetPath, []byte(cont), fi.Mode()); err != nil { + return errors.Wrapf(err, "failed to write %s", targetPath) } return nil diff --git a/clidocstool_md_test.go b/clidocstool_md_test.go index 90d2b5d..9708541 100644 --- a/clidocstool_md_test.go +++ b/clidocstool_md_test.go @@ -17,67 +17,39 @@ package clidocstool import ( "io/ioutil" "os" + "path" "path/filepath" "testing" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) //nolint:errcheck func TestGenMarkdownTree(t *testing.T) { - c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} - s := &cobra.Command{Use: "sub [OPTIONS] arg1 arg2"} - - flags := s.Flags() - _ = flags.Bool("push", false, "Shorthand for --output=type=registry") - _ = flags.Bool("load", false, "Shorthand for --output=type=docker") - _ = flags.StringArrayP("tag", "t", []string{}, "Name and optionally a tag in the 'name:tag' format") - flags.SetAnnotation("tag", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#tag-an-image--t"}) - _ = flags.StringArray("build-arg", []string{}, "Set build-time variables") - flags.SetAnnotation("build-arg", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg"}) - _ = flags.StringP("file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") - flags.SetAnnotation("file", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#specify-a-dockerfile--f"}) - _ = flags.StringArray("label", []string{}, "Set metadata for an image") - _ = flags.StringArray("cache-from", []string{}, "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)") - _ = flags.StringArray("cache-to", []string{}, "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)") - _ = flags.String("target", "", "Set the target build stage to build.") - flags.SetAnnotation("target", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#specifying-target-build-stage---target"}) - _ = flags.StringSlice("allow", []string{}, "Allow extra privileged entitlement, e.g. network.host, security.insecure") - _ = flags.StringArray("platform", []string{}, "Set target platform for build") - _ = flags.StringArray("secret", []string{}, "Secret file to expose to the build: id=mysecret,src=/local/secret") - _ = flags.StringArray("ssh", []string{}, "SSH agent socket or keys to expose to the build (format: `default|[=|[,]]`)") - _ = flags.StringArrayP("output", "o", []string{}, "Output destination (format: type=local,dest=path)") - // not implemented - _ = flags.String("network", "default", "Set the networking mode for the RUN instructions during build") - _ = flags.StringSlice("add-host", []string{}, "Add a custom host-to-IP mapping (host:ip)") - _ = flags.SetAnnotation("add-host", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#add-entries-to-container-hosts-file---add-host"}) - _ = flags.String("iidfile", "", "Write the image ID to the file") - // hidden flags - _ = flags.BoolP("quiet", "q", false, "Suppress the build output and print image ID on success") - flags.MarkHidden("quiet") - _ = flags.Bool("squash", false, "Squash newly built layers into a single new layer") - flags.MarkHidden("squash") - _ = flags.String("ulimit", "", "Ulimit options") - flags.MarkHidden("ulimit") - _ = flags.StringSlice("security-opt", []string{}, "Security options") - flags.MarkHidden("security-opt") - _ = flags.Bool("compress", false, "Compress the build context using gzip") - - c.AddCommand(s) - tmpdir, err := ioutil.TempDir("", "test-gen-markdown-tree") require.NoError(t, err) - defer os.RemoveAll(tmpdir) - require.NoError(t, GenMarkdownTree(c, tmpdir)) - fres := filepath.Join(tmpdir, "sub.md") - require.FileExists(t, fres) - bres, err := ioutil.ReadFile(fres) + c, err := New(Options{ + Root: buildxCmd, + SourceDir: tmpdir, + Plugin: true, + }) require.NoError(t, err) - bexc, err := ioutil.ReadFile("fixtures/sub.md") - require.NoError(t, err) - assert.Equal(t, string(bres), string(bexc)) + require.NoError(t, c.GenMarkdownTree(buildxCmd)) + + for _, tt := range []string{"buildx.md", "buildx_build.md"} { + tt := tt + t.Run(tt, func(t *testing.T) { + fres := filepath.Join(tmpdir, tt) + require.FileExists(t, fres) + bres, err := ioutil.ReadFile(fres) + require.NoError(t, err) + + bexc, err := ioutil.ReadFile(path.Join("fixtures", tt)) + require.NoError(t, err) + assert.Equal(t, string(bexc), string(bres)) + }) + } } diff --git a/clidocstool_test.go b/clidocstool_test.go new file mode 100644 index 0000000..f47a610 --- /dev/null +++ b/clidocstool_test.go @@ -0,0 +1,124 @@ +// Copyright 2021 cli-docs-tool authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package clidocstool + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dockerCmd *cobra.Command + buildxCmd *cobra.Command + buildxBuildCmd *cobra.Command +) + +//nolint:errcheck +func init() { + dockerCmd = &cobra.Command{ + Use: "docker [OPTIONS] COMMAND [ARG...]", + Short: "A self-sufficient runtime for containers", + SilenceUsage: true, + SilenceErrors: true, + TraverseChildren: true, + Run: func(cmd *cobra.Command, args []string) {}, + Version: "20.10.8", + DisableFlagsInUseLine: true, + } + buildxCmd = &cobra.Command{ + Use: "buildx", + Short: "Build with BuildKit", + } + buildxBuildCmd = &cobra.Command{ + Use: "build [OPTIONS] PATH | URL | -", + Aliases: []string{"b"}, + Short: "Start a build", + Run: func(cmd *cobra.Command, args []string) {}, + } + + flags := buildxBuildCmd.Flags() + flags.Bool("push", false, "Shorthand for --output=type=registry") + flags.Bool("load", false, "Shorthand for --output=type=docker") + flags.StringArrayP("tag", "t", []string{}, "Name and optionally a tag in the 'name:tag' format") + flags.SetAnnotation("tag", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#tag-an-image--t"}) + flags.StringArray("build-arg", []string{}, "Set build-time variables") + flags.SetAnnotation("build-arg", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg"}) + flags.StringP("file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") + flags.SetAnnotation("file", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#specify-a-dockerfile--f"}) + flags.StringArray("label", []string{}, "Set metadata for an image") + flags.StringArray("cache-from", []string{}, "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)") + flags.StringArray("cache-to", []string{}, "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)") + flags.String("target", "", "Set the target build stage to build.") + flags.SetAnnotation("target", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#specifying-target-build-stage---target"}) + flags.StringSlice("allow", []string{}, "Allow extra privileged entitlement, e.g. network.host, security.insecure") + flags.StringArray("platform", []string{}, "Set target platform for build") + flags.StringArray("secret", []string{}, "Secret file to expose to the build: id=mysecret,src=/local/secret") + flags.StringArray("ssh", []string{}, "SSH agent socket or keys to expose to the build (format: `default|[=|[,]]`)") + flags.StringArrayP("output", "o", []string{}, "Output destination (format: type=local,dest=path)") + // not implemented + flags.String("network", "default", "Set the networking mode for the RUN instructions during build") + flags.StringSlice("add-host", []string{}, "Add a custom host-to-IP mapping (host:ip)") + flags.SetAnnotation("add-host", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#add-entries-to-container-hosts-file---add-host"}) + flags.String("iidfile", "", "Write the image ID to the file") + // hidden flags + flags.BoolP("quiet", "q", false, "Suppress the build output and print image ID on success") + flags.MarkHidden("quiet") + flags.Bool("squash", false, "Squash newly built layers into a single new layer") + flags.MarkHidden("squash") + flags.String("ulimit", "", "Ulimit options") + flags.MarkHidden("ulimit") + flags.StringSlice("security-opt", []string{}, "Security options") + flags.MarkHidden("security-opt") + flags.Bool("compress", false, "Compress the build context using gzip") + + buildxCmd.AddCommand(buildxBuildCmd) + dockerCmd.AddCommand(buildxCmd) +} + +//nolint:errcheck +func TestGenAllTree(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "test-gen-all-tree") + require.NoError(t, err) + defer os.RemoveAll(tmpdir) + + c, err := New(Options{ + Root: buildxCmd, + SourceDir: tmpdir, + Plugin: true, + }) + require.NoError(t, err) + require.NoError(t, c.GenAllTree()) + + for _, tt := range []string{"buildx.md", "buildx_build.md", "docker_buildx.yaml", "docker_buildx_build.yaml"} { + tt := tt + t.Run(tt, func(t *testing.T) { + fres := filepath.Join(tmpdir, tt) + require.FileExists(t, fres) + bres, err := ioutil.ReadFile(fres) + require.NoError(t, err) + + bexc, err := ioutil.ReadFile(path.Join("fixtures", tt)) + require.NoError(t, err) + assert.Equal(t, string(bexc), string(bres)) + }) + } +} diff --git a/clidocstool_yaml.go b/clidocstool_yaml.go index bd46f81..e11815d 100644 --- a/clidocstool_yaml.go +++ b/clidocstool_yaml.go @@ -74,62 +74,58 @@ type cmdDoc struct { // correctly if your command names have `-` in them. If you have `cmd` with two // subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` // it is undefined which help output will be in the file `cmd-sub-third.1`. -func GenYamlTree(cmd *cobra.Command, dir string) error { +func (c *Client) GenYamlTree(cmd *cobra.Command) error { emptyStr := func(s string) string { return "" } - if err := loadLongDescription(cmd, dir); err != nil { + if err := c.loadLongDescription(cmd); err != nil { return err } - return GenYamlTreeCustom(cmd, dir, emptyStr) + return c.genYamlTreeCustom(cmd, emptyStr) } -// GenYamlTreeCustom creates yaml structured ref files. -func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string) error { - for _, c := range cmd.Commands() { - if !c.Runnable() && !c.HasAvailableSubCommands() { +// genYamlTreeCustom creates yaml structured ref files. +func (c *Client) genYamlTreeCustom(cmd *cobra.Command, filePrepender func(string) string) error { + for _, sc := range cmd.Commands() { + if !sc.Runnable() && !sc.HasAvailableSubCommands() { // skip non-runnable commands without subcommands // but *do* generate YAML for hidden and deprecated commands // the YAML will have those included as metadata, so that the // documentation repository can decide whether or not to present them continue } - if err := GenYamlTreeCustom(c, dir, filePrepender); err != nil { + if err := c.genYamlTreeCustom(sc, filePrepender); err != nil { return err } } - // TODO: conditionally skip the root command (for plugins) - // // The "root" command used in the generator is just a "stub", and only has a // list of subcommands, but not (e.g.) global options/flags. We should fix // that, so that the YAML file for the docker "root" command contains the // global flags. - // - // If we're using this code to generate YAML docs for a plugin, the root- - // command is even less useful; in that case, the root command represents - // the "docker" command, and is a "dummy" with no flags, and only a single - // subcommand (the plugin's top command). For plugins, we should skip the - // root command altogether, to prevent generating a useless YAML file. - if !cmd.HasParent() { + + // Skip the root command altogether, to prevent generating a useless + // YAML file for plugins. + if c.plugin && !cmd.HasParent() { return nil } + log.Printf("INFO: Generating YAML for %q", cmd.CommandPath()) basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".yaml" - filename := filepath.Join(dir, basename) - f, err := os.Create(filename) + target := filepath.Join(c.target, basename) + f, err := os.Create(target) if err != nil { return err } defer f.Close() - if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + if _, err := io.WriteString(f, filePrepender(target)); err != nil { return err } - return GenYamlCustom(cmd, f) + return c.genYamlCustom(cmd, f) } -// GenYamlCustom creates custom yaml output. +// genYamlCustom creates custom yaml output. // nolint: gocyclo -func GenYamlCustom(cmd *cobra.Command, w io.Writer) error { +func (c *Client) genYamlCustom(cmd *cobra.Command, w io.Writer) error { const ( // shortMaxWidth is the maximum width for the "Short" description before // we force YAML to use multi-line syntax. The goal is to make the total @@ -338,15 +334,15 @@ func hasSeeAlso(cmd *cobra.Command) bool { } // loadLongDescription gets long descriptions and examples from markdown. -func loadLongDescription(parentCmd *cobra.Command, path string) error { +func (c *Client) loadLongDescription(parentCmd *cobra.Command) error { for _, cmd := range parentCmd.Commands() { if cmd.HasSubCommands() { - if err := loadLongDescription(cmd, path); err != nil { + if err := c.loadLongDescription(cmd); err != nil { return err } } name := cmd.CommandPath() - if i := strings.Index(name, " "); i >= 0 { + if i := strings.Index(name, " "); c.plugin && i >= 0 { // remove root command / binary name name = name[i+1:] } @@ -354,8 +350,8 @@ func loadLongDescription(parentCmd *cobra.Command, path string) error { continue } mdFile := strings.ReplaceAll(name, " ", "_") + ".md" - fullPath := filepath.Join(path, mdFile) - content, err := ioutil.ReadFile(fullPath) + sourcePath := filepath.Join(c.source, mdFile) + content, err := ioutil.ReadFile(sourcePath) if os.IsNotExist(err) { log.Printf("WARN: %s does not exist, skipping Markdown examples for YAML doc\n", mdFile) continue diff --git a/clidocstool_yaml_test.go b/clidocstool_yaml_test.go index 6f067d4..5c238ba 100644 --- a/clidocstool_yaml_test.go +++ b/clidocstool_yaml_test.go @@ -17,67 +17,39 @@ package clidocstool import ( "io/ioutil" "os" + "path" "path/filepath" "testing" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) //nolint:errcheck func TestGenYamlTree(t *testing.T) { - c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"} - s := &cobra.Command{Use: "sub [OPTIONS] arg1 arg2", Run: func(cmd *cobra.Command, args []string) {}} - - flags := s.Flags() - _ = flags.Bool("push", false, "Shorthand for --output=type=registry") - _ = flags.Bool("load", false, "Shorthand for --output=type=docker") - _ = flags.StringArrayP("tag", "t", []string{}, "Name and optionally a tag in the 'name:tag' format") - flags.SetAnnotation("tag", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#tag-an-image--t"}) - _ = flags.StringArray("build-arg", []string{}, "Set build-time variables") - flags.SetAnnotation("build-arg", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg"}) - _ = flags.StringP("file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") - flags.SetAnnotation("file", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#specify-a-dockerfile--f"}) - _ = flags.StringArray("label", []string{}, "Set metadata for an image") - _ = flags.StringArray("cache-from", []string{}, "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)") - _ = flags.StringArray("cache-to", []string{}, "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)") - _ = flags.String("target", "", "Set the target build stage to build.") - flags.SetAnnotation("target", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#specifying-target-build-stage---target"}) - _ = flags.StringSlice("allow", []string{}, "Allow extra privileged entitlement, e.g. network.host, security.insecure") - _ = flags.StringArray("platform", []string{}, "Set target platform for build") - _ = flags.StringArray("secret", []string{}, "Secret file to expose to the build: id=mysecret,src=/local/secret") - _ = flags.StringArray("ssh", []string{}, "SSH agent socket or keys to expose to the build (format: `default|[=|[,]]`)") - _ = flags.StringArrayP("output", "o", []string{}, "Output destination (format: type=local,dest=path)") - // not implemented - _ = flags.String("network", "default", "Set the networking mode for the RUN instructions during build") - _ = flags.StringSlice("add-host", []string{}, "Add a custom host-to-IP mapping (host:ip)") - _ = flags.SetAnnotation("add-host", "docs.external.url", []string{"https://docs.docker.com/engine/reference/commandline/build/#add-entries-to-container-hosts-file---add-host"}) - _ = flags.String("iidfile", "", "Write the image ID to the file") - // hidden flags - _ = flags.BoolP("quiet", "q", false, "Suppress the build output and print image ID on success") - flags.MarkHidden("quiet") - _ = flags.Bool("squash", false, "Squash newly built layers into a single new layer") - flags.MarkHidden("squash") - _ = flags.String("ulimit", "", "Ulimit options") - flags.MarkHidden("ulimit") - _ = flags.StringSlice("security-opt", []string{}, "Security options") - flags.MarkHidden("security-opt") - _ = flags.Bool("compress", false, "Compress the build context using gzip") - - c.AddCommand(s) - tmpdir, err := ioutil.TempDir("", "test-gen-yaml-tree") require.NoError(t, err) - defer os.RemoveAll(tmpdir) - require.NoError(t, GenYamlTree(c, tmpdir)) - fres := filepath.Join(tmpdir, "do_sub.yaml") - require.FileExists(t, fres) - bres, err := ioutil.ReadFile(fres) + c, err := New(Options{ + Root: buildxCmd, + SourceDir: tmpdir, + Plugin: true, + }) require.NoError(t, err) - bexc, err := ioutil.ReadFile("fixtures/do_sub.yaml") - require.NoError(t, err) - assert.Equal(t, string(bres), string(bexc)) + require.NoError(t, c.GenYamlTree(buildxCmd)) + + for _, tt := range []string{"docker_buildx.yaml", "docker_buildx_build.yaml"} { + tt := tt + t.Run(tt, func(t *testing.T) { + fres := filepath.Join(tmpdir, tt) + require.FileExists(t, fres) + bres, err := ioutil.ReadFile(fres) + require.NoError(t, err) + + bexc, err := ioutil.ReadFile(path.Join("fixtures", tt)) + require.NoError(t, err) + assert.Equal(t, string(bexc), string(bres)) + }) + } } diff --git a/example/README.md b/example/README.md index 789df02..731c154 100644 --- a/example/README.md +++ b/example/README.md @@ -1,6 +1,6 @@ # Example -The following example will generate YAML and Markdown docs for +The following example will generate all supported docs for [Docker buildx](https://github.com/docker/buildx) CLI. ```console diff --git a/example/go.mod b/example/go.mod index c2c9fee..d9c2688 100644 --- a/example/go.mod +++ b/example/go.mod @@ -3,14 +3,15 @@ module github.com/docker/cli-docs-tool/example go 1.16 require ( - github.com/docker/buildx v0.6.0 + github.com/docker/buildx v0.6.3 github.com/docker/cli v20.10.7+incompatible github.com/docker/cli-docs-tool v0.0.0 github.com/spf13/cobra v1.2.1 + github.com/spf13/pflag v1.0.5 ) replace ( github.com/docker/cli => github.com/docker/cli v20.10.3-0.20210702143511-f782d1355eff+incompatible - github.com/docker/docker => github.com/docker/docker v20.10.3-0.20210609100121-ef4d47340142+incompatible github.com/docker/cli-docs-tool => ../ + github.com/docker/docker => github.com/docker/docker v20.10.3-0.20210609100121-ef4d47340142+incompatible ) diff --git a/example/go.sum b/example/go.sum index 3ae93d8..de7313f 100644 --- a/example/go.sum +++ b/example/go.sum @@ -69,8 +69,9 @@ github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEY github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16 h1:8/auA4LFIZFTGrqfKhGBSXwM6/4X1fHa/xniyEHu8ac= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.18 h1:cYnKADiM1869gvBpos3YCteeT6sZLB48lB5dmMMs8Tg= +github.com/Microsoft/hcsshim v0.8.18/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -151,8 +152,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/compose-spec/compose-go v0.0.0-20210706130854-69459d4976b5 h1:PpI72CT1bcVPNZyqI1HI8UhQVRVtqLb2tdwi5WphN3c= -github.com/compose-spec/compose-go v0.0.0-20210706130854-69459d4976b5/go.mod h1:5V65rPnTvvQagtoMxTneJ2QicLq6ZRQQ7fOgPN226fo= +github.com/compose-spec/compose-go v0.0.0-20210729195839-de56f4f0cb3c h1:lSR4wokZlq+Q8uJpgZuFMs3VoLaYVV07cJOZHa1zRBg= +github.com/compose-spec/compose-go v0.0.0-20210729195839-de56f4f0cb3c/go.mod h1:5V65rPnTvvQagtoMxTneJ2QicLq6ZRQQ7fOgPN226fo= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= @@ -186,8 +187,10 @@ github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7 github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.2 h1:MG/Bg1pbmMb61j3wHCFWPxESXHieiKr2xG64px/k8zQ= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.2/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.4 h1:uPF0og3ByFzDnaStfiQj3fVGTEtaSNyU+bW7GR/nqGA= +github.com/containerd/containerd v1.5.4/go.mod h1:sx18RgvW6ABJ4iYUw7Q5x7bgFOAB9B6G7+yO0XBc4zw= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -282,8 +285,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e h1:n81KvOMrLZa+VWHwST7dun9f0G98X3zREHS1ztYzZKU= github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e/go.mod h1:xpWTC2KnJMiDLkoawhsPQcXjvwATEBcbq0xevG2YR9M= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/buildx v0.6.0 h1:UuJvtuK8SQX/7Y9i1fJR3Jq6kXBSFcSJtudlALu9iV8= -github.com/docker/buildx v0.6.0/go.mod h1:ue0mOStExTp8vSb1wrvbpL9VoL0e66EK9etSCCIhuFM= +github.com/docker/buildx v0.6.3 h1:/PReewxhHiBdNQmg6E9lUa56wKba0oXRBrKhwA90JXE= +github.com/docker/buildx v0.6.3/go.mod h1:7gkFFXWFWo+vfXMNijFRBO0KwEDITP10TUWty5ZJNz0= github.com/docker/cli v20.10.3-0.20210702143511-f782d1355eff+incompatible h1:CaaxCD/l9Dxogu6lxf7AQautlv3sHULrasPadayp0fM= github.com/docker/cli v20.10.3-0.20210702143511-f782d1355eff+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/compose-on-kubernetes v0.4.19-0.20190128150448-356b2919c496 h1:90ytrX1dbzL7Uf/hHiuWwvywC+gikHv4hkAy4CwRTbs= diff --git a/example/main.go b/example/main.go index deefd8f..0a5b93e 100644 --- a/example/main.go +++ b/example/main.go @@ -17,40 +17,72 @@ package main import ( "log" "os" - "path/filepath" "github.com/docker/buildx/commands" clidocstool "github.com/docker/cli-docs-tool" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) -const sourcePath = "docs/" +const ( + pluginName = "buildx" + defaultSourcePath = "docs/" +) -func main() { +type options struct { + source string + target string +} + +func gen(opts *options) error { log.SetFlags(0) + // create a new instance of Docker CLI dockerCLI, err := command.NewDockerCli() if err != nil { - log.Printf("ERROR: %+v", err) + return err } + // root command cmd := &cobra.Command{ - Use: "docker [OPTIONS] COMMAND [ARG...]", - Short: "The base command for the Docker CLI.", - DisableAutoGenTag: true, + Use: "buildx", + Short: "Build with BuildKit", } - cmd.AddCommand(commands.NewRootCmd("buildx", true, dockerCLI)) - clidocstool.DisableFlagsInUseLine(cmd) + // subcommand for the plugin + cmd.AddCommand(commands.NewRootCmd(pluginName, true, dockerCLI)) - cwd, _ := os.Getwd() - source := filepath.Join(cwd, sourcePath) - - if err = os.MkdirAll(source, 0755); err != nil { - log.Printf("ERROR: %+v", err) + // create a new instance of cli-docs-tool + c, err := clidocstool.New(clidocstool.Options{ + Root: cmd, + SourceDir: opts.source, + TargetDir: opts.target, + Plugin: true, + }) + if err != nil { + return err } - if err = clidocstool.GenTree(cmd, source); err != nil { + c.DisableFlagsInUseLine() + + // generate all supported docs formats + return c.GenAllTree() +} + +func run() error { + opts := &options{} + flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError) + flags.StringVar(&opts.source, "source", defaultSourcePath, "Docs source folder") + flags.StringVar(&opts.target, "target", defaultSourcePath, "Docs target folder") + if err := flags.Parse(os.Args[1:]); err != nil { + return err + } + return gen(opts) +} + +func main() { + if err := run(); err != nil { log.Printf("ERROR: %+v", err) + os.Exit(1) } } diff --git a/fixtures/buildx.md b/fixtures/buildx.md new file mode 100644 index 0000000..76a1bc9 --- /dev/null +++ b/fixtures/buildx.md @@ -0,0 +1,15 @@ +# docker buildx + + +Build with BuildKit + +### Subcommands + +| Name | Description | +| --- | --- | +| [`build`](buildx_build.md) | Start a build | + + + + + diff --git a/fixtures/sub.md b/fixtures/buildx_build.md similarity index 97% rename from fixtures/sub.md rename to fixtures/buildx_build.md index f58a043..ad4d3ba 100644 --- a/fixtures/sub.md +++ b/fixtures/buildx_build.md @@ -1,6 +1,12 @@ -# do sub +# docker buildx build +Start a build + +### Aliases + +`build`, `b` + ### Options | Name | Description | diff --git a/fixtures/docker_buildx.yaml b/fixtures/docker_buildx.yaml new file mode 100644 index 0000000..6294a78 --- /dev/null +++ b/fixtures/docker_buildx.yaml @@ -0,0 +1,15 @@ +command: docker buildx +short: Build with BuildKit +long: Build with BuildKit +pname: docker +plink: docker.yaml +cname: +- docker buildx build +clink: +- docker_buildx_build.yaml +deprecated: false +experimental: false +experimentalcli: false +kubernetes: false +swarm: false + diff --git a/fixtures/do_sub.yaml b/fixtures/docker_buildx_build.yaml similarity index 96% rename from fixtures/do_sub.yaml rename to fixtures/docker_buildx_build.yaml index 9a2fbe4..0ba6e24 100644 --- a/fixtures/do_sub.yaml +++ b/fixtures/docker_buildx_build.yaml @@ -1,7 +1,10 @@ -command: do sub -usage: do sub [OPTIONS] arg1 arg2 [flags] -pname: do -plink: do.yaml +command: docker buildx build +aliases: b +short: Start a build +long: Start a build +usage: docker buildx build [OPTIONS] PATH | URL | - [flags] +pname: docker buildx +plink: docker_buildx.yaml options: - option: add-host value_type: stringSlice