Browse code

docs: added support for CLI yaml file generation

Signed-off-by: French Ben <frenchben@docker.com>
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 5443f0152f77eb983e6475474b02c2e11d975f1c)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>

French Ben authored on 2017/01/28 09:47:41
Showing 8 changed files
... ...
@@ -141,6 +141,9 @@ run: build ## run the docker daemon in a container
141 141
 shell: build ## start a shell inside the build env
142 142
 	$(DOCKER_RUN_DOCKER) bash
143 143
 
144
+yaml-docs-gen: build ## generate documentation YAML files consumed by docs repo
145
+	$(DOCKER_RUN_DOCKER) sh -c 'hack/make.sh yaml-docs-generator && ( cd bundles/latest/yaml-docs-generator; mkdir docs; ./yaml-docs-generator --target $$(pwd)/docs )'
146
+
144 147
 test: build ## run the unit, integration and docker-py tests
145 148
 	$(DOCKER_RUN_DOCKER) hack/make.sh dynbinary cross test-unit test-integration-cli test-docker-py
146 149
 
... ...
@@ -26,7 +26,7 @@ Options:
26 26
                        The tarball may be compressed with gzip, bzip, or xz
27 27
   -q, --quiet          Suppress the load output but still outputs the imported images
28 28
 ```
29
-## Descriptino
29
+## Description
30 30
 
31 31
 `docker load` loads a tarred repository from a file or the standard input stream.
32 32
 It restores both images and tags.
... ...
@@ -27,7 +27,7 @@ Options:
27 27
       --help   Print usage
28 28
 ```
29 29
 
30
-## Descriptino
30
+## Description
31 31
 
32 32
 Lists the stacks.
33 33
 
34 34
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+FROM scratch
1
+COPY docs /docs
2
+# CMD cannot be nil so we set it to empty string
3
+CMD  [""]
0 4
new file mode 100644
... ...
@@ -0,0 +1,86 @@
0
+package main
1
+
2
+import (
3
+	"fmt"
4
+	"io/ioutil"
5
+	"log"
6
+	"os"
7
+	"path/filepath"
8
+	"strings"
9
+
10
+	"github.com/docker/docker/cli/command"
11
+	"github.com/docker/docker/cli/command/commands"
12
+	"github.com/docker/docker/pkg/term"
13
+	"github.com/spf13/cobra"
14
+	"github.com/spf13/pflag"
15
+)
16
+
17
+const descriptionSourcePath = "docs/reference/commandline/"
18
+
19
+func generateCliYaml(opts *options) error {
20
+	stdin, stdout, stderr := term.StdStreams()
21
+	dockerCli := command.NewDockerCli(stdin, stdout, stderr)
22
+	cmd := &cobra.Command{Use: "docker"}
23
+	commands.AddCommands(cmd, dockerCli)
24
+	source := filepath.Join(opts.source, descriptionSourcePath)
25
+	if err := loadLongDescription(cmd, source); err != nil {
26
+		return err
27
+	}
28
+
29
+	cmd.DisableAutoGenTag = true
30
+	return GenYamlTree(cmd, opts.target)
31
+}
32
+
33
+func loadLongDescription(cmd *cobra.Command, path ...string) error {
34
+	for _, cmd := range cmd.Commands() {
35
+		if cmd.Name() == "" {
36
+			continue
37
+		}
38
+		fullpath := filepath.Join(path[0], strings.Join(append(path[1:], cmd.Name()), "_")+".md")
39
+
40
+		if cmd.HasSubCommands() {
41
+			loadLongDescription(cmd, path[0], cmd.Name())
42
+		}
43
+
44
+		if _, err := os.Stat(fullpath); err != nil {
45
+			log.Printf("WARN: %s does not exist, skipping\n", fullpath)
46
+			continue
47
+		}
48
+
49
+		content, err := ioutil.ReadFile(fullpath)
50
+		if err != nil {
51
+			return err
52
+		}
53
+		description, examples := parseMDContent(string(content))
54
+		cmd.Long = description
55
+		cmd.Example = examples
56
+	}
57
+	return nil
58
+}
59
+
60
+type options struct {
61
+	source string
62
+	target string
63
+}
64
+
65
+func parseArgs() (*options, error) {
66
+	opts := &options{}
67
+	cwd, _ := os.Getwd()
68
+	flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
69
+	flags.StringVar(&opts.source, "root", cwd, "Path to project root")
70
+	flags.StringVar(&opts.target, "target", "/tmp", "Target path for generated yaml files")
71
+	err := flags.Parse(os.Args[1:])
72
+	return opts, err
73
+}
74
+
75
+func main() {
76
+	opts, err := parseArgs()
77
+	if err != nil {
78
+		fmt.Fprintln(os.Stderr, err.Error())
79
+	}
80
+	fmt.Printf("Project root: %s\n", opts.source)
81
+	fmt.Printf("Generating yaml files into %s\n", opts.target)
82
+	if err := generateCliYaml(opts); err != nil {
83
+		fmt.Fprintf(os.Stderr, "Failed to generate yaml files: %s\n", err.Error())
84
+	}
85
+}
0 86
new file mode 100644
... ...
@@ -0,0 +1,212 @@
0
+package main
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"os"
6
+	"path/filepath"
7
+	"sort"
8
+	"strings"
9
+
10
+	"github.com/spf13/cobra"
11
+	"github.com/spf13/pflag"
12
+	"gopkg.in/yaml.v2"
13
+)
14
+
15
+type cmdOption struct {
16
+	Option       string
17
+	Shorthand    string `yaml:",omitempty"`
18
+	DefaultValue string `yaml:"default_value,omitempty"`
19
+	Description  string `yaml:",omitempty"`
20
+}
21
+
22
+type cmdDoc struct {
23
+	Name             string      `yaml:"command"`
24
+	SeeAlso          []string    `yaml:"parent,omitempty"`
25
+	Version          string      `yaml:"engine_version,omitempty"`
26
+	Aliases          string      `yaml:",omitempty"`
27
+	Short            string      `yaml:",omitempty"`
28
+	Long             string      `yaml:",omitempty"`
29
+	Usage            string      `yaml:",omitempty"`
30
+	Pname            string      `yaml:",omitempty"`
31
+	Plink            string      `yaml:",omitempty"`
32
+	Cname            []string    `yaml:",omitempty"`
33
+	Clink            []string    `yaml:",omitempty"`
34
+	Options          []cmdOption `yaml:",omitempty"`
35
+	InheritedOptions []cmdOption `yaml:"inherited_options,omitempty"`
36
+	Example          string      `yaml:"examples,omitempty"`
37
+}
38
+
39
+// GenYamlTree creates yaml structured ref files
40
+func GenYamlTree(cmd *cobra.Command, dir string) error {
41
+	identity := func(s string) string { return s }
42
+	emptyStr := func(s string) string { return "" }
43
+	return GenYamlTreeCustom(cmd, dir, emptyStr, identity)
44
+}
45
+
46
+// GenYamlTreeCustom creates yaml structured ref files
47
+func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error {
48
+	for _, c := range cmd.Commands() {
49
+		if !c.IsAvailableCommand() || c.IsHelpCommand() {
50
+			continue
51
+		}
52
+		if err := GenYamlTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
53
+			return err
54
+		}
55
+	}
56
+
57
+	basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".yaml"
58
+	filename := filepath.Join(dir, basename)
59
+	f, err := os.Create(filename)
60
+	if err != nil {
61
+		return err
62
+	}
63
+	defer f.Close()
64
+
65
+	if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
66
+		return err
67
+	}
68
+	if err := GenYamlCustom(cmd, f, linkHandler); err != nil {
69
+		return err
70
+	}
71
+	return nil
72
+}
73
+
74
+// GenYamlCustom creates custom yaml output
75
+func GenYamlCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
76
+	cliDoc := cmdDoc{}
77
+	cliDoc.Name = cmd.CommandPath()
78
+
79
+	// Check experimental: ok := cmd.Tags["experimental"]
80
+
81
+	cliDoc.Aliases = strings.Join(cmd.Aliases, ", ")
82
+	cliDoc.Short = cmd.Short
83
+	cliDoc.Long = cmd.Long
84
+	if len(cliDoc.Long) == 0 {
85
+		cliDoc.Long = cliDoc.Short
86
+	}
87
+
88
+	if cmd.Runnable() {
89
+		cliDoc.Usage = cmd.UseLine()
90
+	}
91
+
92
+	if len(cmd.Example) > 0 {
93
+		cliDoc.Example = cmd.Example
94
+	}
95
+
96
+	flags := cmd.NonInheritedFlags()
97
+	if flags.HasFlags() {
98
+		cliDoc.Options = genFlagResult(flags)
99
+	}
100
+	flags = cmd.InheritedFlags()
101
+	if flags.HasFlags() {
102
+		cliDoc.InheritedOptions = genFlagResult(flags)
103
+	}
104
+
105
+	if hasSeeAlso(cmd) {
106
+		if cmd.HasParent() {
107
+			parent := cmd.Parent()
108
+			cliDoc.Pname = parent.CommandPath()
109
+			link := cliDoc.Pname + ".yaml"
110
+			cliDoc.Plink = strings.Replace(link, " ", "_", -1)
111
+			cmd.VisitParents(func(c *cobra.Command) {
112
+				if c.DisableAutoGenTag {
113
+					cmd.DisableAutoGenTag = c.DisableAutoGenTag
114
+				}
115
+			})
116
+		}
117
+
118
+		children := cmd.Commands()
119
+		sort.Sort(byName(children))
120
+
121
+		for _, child := range children {
122
+			if !child.IsAvailableCommand() || child.IsHelpCommand() {
123
+				continue
124
+			}
125
+			currentChild := cliDoc.Name + " " + child.Name()
126
+			cliDoc.Cname = append(cliDoc.Cname, cliDoc.Name+" "+child.Name())
127
+			link := currentChild + ".yaml"
128
+			cliDoc.Clink = append(cliDoc.Clink, strings.Replace(link, " ", "_", -1))
129
+		}
130
+	}
131
+
132
+	final, err := yaml.Marshal(&cliDoc)
133
+	if err != nil {
134
+		fmt.Println(err)
135
+		os.Exit(1)
136
+	}
137
+	if _, err := fmt.Fprintln(w, string(final)); err != nil {
138
+		return err
139
+	}
140
+	return nil
141
+}
142
+
143
+func genFlagResult(flags *pflag.FlagSet) []cmdOption {
144
+	var result []cmdOption
145
+
146
+	flags.VisitAll(func(flag *pflag.Flag) {
147
+		// Todo, when we mark a shorthand is deprecated, but specify an empty message.
148
+		// The flag.ShorthandDeprecated is empty as the shorthand is deprecated.
149
+		// Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok.
150
+		if !(len(flag.ShorthandDeprecated) > 0) && len(flag.Shorthand) > 0 {
151
+			opt := cmdOption{
152
+				Option:       flag.Name,
153
+				Shorthand:    flag.Shorthand,
154
+				DefaultValue: flag.DefValue,
155
+				Description:  forceMultiLine(flag.Usage),
156
+			}
157
+			result = append(result, opt)
158
+		} else {
159
+			opt := cmdOption{
160
+				Option:       flag.Name,
161
+				DefaultValue: forceMultiLine(flag.DefValue),
162
+				Description:  forceMultiLine(flag.Usage),
163
+			}
164
+			result = append(result, opt)
165
+		}
166
+	})
167
+
168
+	return result
169
+}
170
+
171
+// Temporary workaround for yaml lib generating incorrect yaml with long strings
172
+// that do not contain \n.
173
+func forceMultiLine(s string) string {
174
+	if len(s) > 60 && !strings.Contains(s, "\n") {
175
+		s = s + "\n"
176
+	}
177
+	return s
178
+}
179
+
180
+// Small duplication for cobra utils
181
+func hasSeeAlso(cmd *cobra.Command) bool {
182
+	if cmd.HasParent() {
183
+		return true
184
+	}
185
+	for _, c := range cmd.Commands() {
186
+		if !c.IsAvailableCommand() || c.IsHelpCommand() {
187
+			continue
188
+		}
189
+		return true
190
+	}
191
+	return false
192
+}
193
+
194
+func parseMDContent(mdString string) (description string, examples string) {
195
+	parsedContent := strings.Split(mdString, "\n## ")
196
+	for _, s := range parsedContent {
197
+		if strings.Index(s, "Description") == 0 {
198
+			description = strings.Trim(s, "Description\n")
199
+		}
200
+		if strings.Index(s, "Examples") == 0 {
201
+			examples = strings.Trim(s, "Examples\n")
202
+		}
203
+	}
204
+	return
205
+}
206
+
207
+type byName []*cobra.Command
208
+
209
+func (s byName) Len() int           { return len(s) }
210
+func (s byName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
211
+func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
0 212
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+#!/usr/bin/env bash
1
+set -e
2
+
3
+[ -z "$KEEPDEST" ] && \
4
+	rm -rf "$DEST"
5
+
6
+(
7
+	source "${MAKEDIR}/.binary-setup"
8
+	export BINARY_SHORT_NAME="yaml-docs-generator"
9
+	export GO_PACKAGE='github.com/docker/docker/docs/yaml'
10
+	source "${MAKEDIR}/.binary"
11
+)
0 12
new file mode 100755
... ...
@@ -0,0 +1,19 @@
0
+#!/bin/bash
1
+
2
+if [ -n "${BUILD_DOCS}" ]; then
3
+	set -e
4
+	DOCS_IMAGE=${DOCS_IMAGE:-${IMAGE_NAME}-docs}
5
+	docker run \
6
+		--entrypoint '' \
7
+		--privileged \
8
+		-e DOCKER_GITCOMMIT=$(git rev-parse --short HEAD) \
9
+		-v $(pwd)/docs/yaml/docs:/docs \
10
+		"${IMAGE_NAME}" \
11
+		sh -c 'hack/make.sh yaml-docs-generator && bundles/latest/yaml-docs-generator/yaml-docs-generator --target /docs'
12
+
13
+	(
14
+		cd docs/yaml
15
+		docker build -t ${DOCS_IMAGE} .
16
+		docker push ${DOCS_IMAGE}
17
+	)
18
+fi