Skip to content

Commit 5443f01

Browse files
French BenTibor Vass
French Ben
authored and
Tibor Vass
committedMar 14, 2017
docs: added support for CLI yaml file generation
Signed-off-by: French Ben <[email protected]> Signed-off-by: Tibor Vass <[email protected]>
1 parent f7cfacb commit 5443f01

File tree

8 files changed

+338
-2
lines changed

8 files changed

+338
-2
lines changed
 

‎Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ run: build ## run the docker daemon in a container
141141
shell: build ## start a shell inside the build env
142142
$(DOCKER_RUN_DOCKER) bash
143143

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+
144147
test: build ## run the unit, integration and docker-py tests
145148
$(DOCKER_RUN_DOCKER) hack/make.sh dynbinary cross test-unit test-integration-cli test-docker-py
146149

‎docs/reference/commandline/load.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Options:
2626
The tarball may be compressed with gzip, bzip, or xz
2727
-q, --quiet Suppress the load output but still outputs the imported images
2828
```
29-
## Descriptino
29+
## Description
3030

3131
`docker load` loads a tarred repository from a file or the standard input stream.
3232
It restores both images and tags.

‎docs/reference/commandline/stack_ls.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Options:
2727
--help Print usage
2828
```
2929

30-
## Descriptino
30+
## Description
3131

3232
Lists the stacks.
3333

‎docs/yaml/Dockerfile

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM scratch
2+
COPY docs /docs
3+
# CMD cannot be nil so we set it to empty string
4+
CMD [""]

‎docs/yaml/generate.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/docker/docker/cli/command"
12+
"github.com/docker/docker/cli/command/commands"
13+
"github.com/docker/docker/pkg/term"
14+
"github.com/spf13/cobra"
15+
"github.com/spf13/pflag"
16+
)
17+
18+
const descriptionSourcePath = "docs/reference/commandline/"
19+
20+
func generateCliYaml(opts *options) error {
21+
stdin, stdout, stderr := term.StdStreams()
22+
dockerCli := command.NewDockerCli(stdin, stdout, stderr)
23+
cmd := &cobra.Command{Use: "docker"}
24+
commands.AddCommands(cmd, dockerCli)
25+
source := filepath.Join(opts.source, descriptionSourcePath)
26+
if err := loadLongDescription(cmd, source); err != nil {
27+
return err
28+
}
29+
30+
cmd.DisableAutoGenTag = true
31+
return GenYamlTree(cmd, opts.target)
32+
}
33+
34+
func loadLongDescription(cmd *cobra.Command, path ...string) error {
35+
for _, cmd := range cmd.Commands() {
36+
if cmd.Name() == "" {
37+
continue
38+
}
39+
fullpath := filepath.Join(path[0], strings.Join(append(path[1:], cmd.Name()), "_")+".md")
40+
41+
if cmd.HasSubCommands() {
42+
loadLongDescription(cmd, path[0], cmd.Name())
43+
}
44+
45+
if _, err := os.Stat(fullpath); err != nil {
46+
log.Printf("WARN: %s does not exist, skipping\n", fullpath)
47+
continue
48+
}
49+
50+
content, err := ioutil.ReadFile(fullpath)
51+
if err != nil {
52+
return err
53+
}
54+
description, examples := parseMDContent(string(content))
55+
cmd.Long = description
56+
cmd.Example = examples
57+
}
58+
return nil
59+
}
60+
61+
type options struct {
62+
source string
63+
target string
64+
}
65+
66+
func parseArgs() (*options, error) {
67+
opts := &options{}
68+
cwd, _ := os.Getwd()
69+
flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
70+
flags.StringVar(&opts.source, "root", cwd, "Path to project root")
71+
flags.StringVar(&opts.target, "target", "/tmp", "Target path for generated yaml files")
72+
err := flags.Parse(os.Args[1:])
73+
return opts, err
74+
}
75+
76+
func main() {
77+
opts, err := parseArgs()
78+
if err != nil {
79+
fmt.Fprintln(os.Stderr, err.Error())
80+
}
81+
fmt.Printf("Project root: %s\n", opts.source)
82+
fmt.Printf("Generating yaml files into %s\n", opts.target)
83+
if err := generateCliYaml(opts); err != nil {
84+
fmt.Fprintf(os.Stderr, "Failed to generate yaml files: %s\n", err.Error())
85+
}
86+
}

‎docs/yaml/yaml.go

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
"strings"
10+
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/pflag"
13+
"gopkg.in/yaml.v2"
14+
)
15+
16+
type cmdOption struct {
17+
Option string
18+
Shorthand string `yaml:",omitempty"`
19+
DefaultValue string `yaml:"default_value,omitempty"`
20+
Description string `yaml:",omitempty"`
21+
}
22+
23+
type cmdDoc struct {
24+
Name string `yaml:"command"`
25+
SeeAlso []string `yaml:"parent,omitempty"`
26+
Version string `yaml:"engine_version,omitempty"`
27+
Aliases string `yaml:",omitempty"`
28+
Short string `yaml:",omitempty"`
29+
Long string `yaml:",omitempty"`
30+
Usage string `yaml:",omitempty"`
31+
Pname string `yaml:",omitempty"`
32+
Plink string `yaml:",omitempty"`
33+
Cname []string `yaml:",omitempty"`
34+
Clink []string `yaml:",omitempty"`
35+
Options []cmdOption `yaml:",omitempty"`
36+
InheritedOptions []cmdOption `yaml:"inherited_options,omitempty"`
37+
Example string `yaml:"examples,omitempty"`
38+
}
39+
40+
// GenYamlTree creates yaml structured ref files
41+
func GenYamlTree(cmd *cobra.Command, dir string) error {
42+
identity := func(s string) string { return s }
43+
emptyStr := func(s string) string { return "" }
44+
return GenYamlTreeCustom(cmd, dir, emptyStr, identity)
45+
}
46+
47+
// GenYamlTreeCustom creates yaml structured ref files
48+
func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error {
49+
for _, c := range cmd.Commands() {
50+
if !c.IsAvailableCommand() || c.IsHelpCommand() {
51+
continue
52+
}
53+
if err := GenYamlTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
54+
return err
55+
}
56+
}
57+
58+
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".yaml"
59+
filename := filepath.Join(dir, basename)
60+
f, err := os.Create(filename)
61+
if err != nil {
62+
return err
63+
}
64+
defer f.Close()
65+
66+
if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
67+
return err
68+
}
69+
if err := GenYamlCustom(cmd, f, linkHandler); err != nil {
70+
return err
71+
}
72+
return nil
73+
}
74+
75+
// GenYamlCustom creates custom yaml output
76+
func GenYamlCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
77+
cliDoc := cmdDoc{}
78+
cliDoc.Name = cmd.CommandPath()
79+
80+
// Check experimental: ok := cmd.Tags["experimental"]
81+
82+
cliDoc.Aliases = strings.Join(cmd.Aliases, ", ")
83+
cliDoc.Short = cmd.Short
84+
cliDoc.Long = cmd.Long
85+
if len(cliDoc.Long) == 0 {
86+
cliDoc.Long = cliDoc.Short
87+
}
88+
89+
if cmd.Runnable() {
90+
cliDoc.Usage = cmd.UseLine()
91+
}
92+
93+
if len(cmd.Example) > 0 {
94+
cliDoc.Example = cmd.Example
95+
}
96+
97+
flags := cmd.NonInheritedFlags()
98+
if flags.HasFlags() {
99+
cliDoc.Options = genFlagResult(flags)
100+
}
101+
flags = cmd.InheritedFlags()
102+
if flags.HasFlags() {
103+
cliDoc.InheritedOptions = genFlagResult(flags)
104+
}
105+
106+
if hasSeeAlso(cmd) {
107+
if cmd.HasParent() {
108+
parent := cmd.Parent()
109+
cliDoc.Pname = parent.CommandPath()
110+
link := cliDoc.Pname + ".yaml"
111+
cliDoc.Plink = strings.Replace(link, " ", "_", -1)
112+
cmd.VisitParents(func(c *cobra.Command) {
113+
if c.DisableAutoGenTag {
114+
cmd.DisableAutoGenTag = c.DisableAutoGenTag
115+
}
116+
})
117+
}
118+
119+
children := cmd.Commands()
120+
sort.Sort(byName(children))
121+
122+
for _, child := range children {
123+
if !child.IsAvailableCommand() || child.IsHelpCommand() {
124+
continue
125+
}
126+
currentChild := cliDoc.Name + " " + child.Name()
127+
cliDoc.Cname = append(cliDoc.Cname, cliDoc.Name+" "+child.Name())
128+
link := currentChild + ".yaml"
129+
cliDoc.Clink = append(cliDoc.Clink, strings.Replace(link, " ", "_", -1))
130+
}
131+
}
132+
133+
final, err := yaml.Marshal(&cliDoc)
134+
if err != nil {
135+
fmt.Println(err)
136+
os.Exit(1)
137+
}
138+
if _, err := fmt.Fprintln(w, string(final)); err != nil {
139+
return err
140+
}
141+
return nil
142+
}
143+
144+
func genFlagResult(flags *pflag.FlagSet) []cmdOption {
145+
var result []cmdOption
146+
147+
flags.VisitAll(func(flag *pflag.Flag) {
148+
// Todo, when we mark a shorthand is deprecated, but specify an empty message.
149+
// The flag.ShorthandDeprecated is empty as the shorthand is deprecated.
150+
// Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok.
151+
if !(len(flag.ShorthandDeprecated) > 0) && len(flag.Shorthand) > 0 {
152+
opt := cmdOption{
153+
Option: flag.Name,
154+
Shorthand: flag.Shorthand,
155+
DefaultValue: flag.DefValue,
156+
Description: forceMultiLine(flag.Usage),
157+
}
158+
result = append(result, opt)
159+
} else {
160+
opt := cmdOption{
161+
Option: flag.Name,
162+
DefaultValue: forceMultiLine(flag.DefValue),
163+
Description: forceMultiLine(flag.Usage),
164+
}
165+
result = append(result, opt)
166+
}
167+
})
168+
169+
return result
170+
}
171+
172+
// Temporary workaround for yaml lib generating incorrect yaml with long strings
173+
// that do not contain \n.
174+
func forceMultiLine(s string) string {
175+
if len(s) > 60 && !strings.Contains(s, "\n") {
176+
s = s + "\n"
177+
}
178+
return s
179+
}
180+
181+
// Small duplication for cobra utils
182+
func hasSeeAlso(cmd *cobra.Command) bool {
183+
if cmd.HasParent() {
184+
return true
185+
}
186+
for _, c := range cmd.Commands() {
187+
if !c.IsAvailableCommand() || c.IsHelpCommand() {
188+
continue
189+
}
190+
return true
191+
}
192+
return false
193+
}
194+
195+
func parseMDContent(mdString string) (description string, examples string) {
196+
parsedContent := strings.Split(mdString, "\n## ")
197+
for _, s := range parsedContent {
198+
if strings.Index(s, "Description") == 0 {
199+
description = strings.Trim(s, "Description\n")
200+
}
201+
if strings.Index(s, "Examples") == 0 {
202+
examples = strings.Trim(s, "Examples\n")
203+
}
204+
}
205+
return
206+
}
207+
208+
type byName []*cobra.Command
209+
210+
func (s byName) Len() int { return len(s) }
211+
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
212+
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }

‎hack/make/yaml-docs-generator

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
[ -z "$KEEPDEST" ] && \
5+
rm -rf "$DEST"
6+
7+
(
8+
source "${MAKEDIR}/.binary-setup"
9+
export BINARY_SHORT_NAME="yaml-docs-generator"
10+
export GO_PACKAGE='github.com/docker/docker/docs/yaml'
11+
source "${MAKEDIR}/.binary"
12+
)

‎hooks/post_build

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
3+
if [ -n "${BUILD_DOCS}" ]; then
4+
set -e
5+
DOCS_IMAGE=${DOCS_IMAGE:-${IMAGE_NAME}-docs}
6+
docker run \
7+
--entrypoint '' \
8+
--privileged \
9+
-e DOCKER_GITCOMMIT=$(git rev-parse --short HEAD) \
10+
-v $(pwd)/docs/yaml/docs:/docs \
11+
"${IMAGE_NAME}" \
12+
sh -c 'hack/make.sh yaml-docs-generator && bundles/latest/yaml-docs-generator/yaml-docs-generator --target /docs'
13+
14+
(
15+
cd docs/yaml
16+
docker build -t ${DOCS_IMAGE} .
17+
docker push ${DOCS_IMAGE}
18+
)
19+
fi

0 commit comments

Comments
 (0)