istio
720 строк · 17.8 Кб
1//go:build !agent
2// +build !agent
3
4// Copyright Istio Authors
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18/*
19NOTICE: The zsh constants are derived from the kubectl completion code,
20(k8s.io/kubernetes/pkg/kubectl/cmd/completion/completion.go), with the
21following copyright/license:
22
23Copyright 2016 The Kubernetes Authors.
24Licensed under the Apache License, Version 2.0 (the "License");
25you may not use this file except in compliance with the License.
26You may obtain a copy of the License at
27http://www.apache.org/licenses/LICENSE-2.0
28Unless required by applicable law or agreed to in writing, software
29distributed under the License is distributed on an "AS IS" BASIS,
30WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31See the License for the specific language governing permissions and
32limitations under the License.
33*/
34
35package collateral
36
37import (
38"bytes"
39"fmt"
40"html"
41"os"
42"sort"
43"strconv"
44"strings"
45
46"github.com/spf13/cobra"
47"github.com/spf13/cobra/doc"
48"github.com/spf13/pflag"
49"sigs.k8s.io/yaml"
50
51"istio.io/istio/pkg/collateral/metrics"
52"istio.io/istio/pkg/env"
53)
54
55// Control determines the behavior of the EmitCollateral function
56type Control struct {
57// OutputDir specifies the directory to output the collateral files
58OutputDir string
59
60// EmitManPages controls whether to produce man pages.
61EmitManPages bool
62
63// EmitYAML controls whether to produce YAML files.
64EmitYAML bool
65
66// EmitBashCompletion controls whether to produce bash completion files.
67EmitBashCompletion bool
68
69// EmitZshCompletion controls whether to produce zsh completion files.
70EmitZshCompletion bool
71
72// EmitMarkdown controls whether to produce markdown documentation files.
73EmitMarkdown bool
74
75// EmitHTMLFragmentWithFrontMatter controls whether to produce HTML fragments with Jekyll/Hugo front matter.
76EmitHTMLFragmentWithFrontMatter bool
77
78// ManPageInfo provides extra information necessary when emitting man pages.
79ManPageInfo doc.GenManHeader
80
81// Predicates to use to filter environment variables and metrics. If not set, all items will be selected.
82Predicates Predicates
83}
84
85// EmitCollateral produces a set of collateral files for a CLI command. You can
86// select to emit markdown to describe a command's function, man pages, YAML
87// descriptions, and bash completion files.
88func EmitCollateral(root *cobra.Command, c *Control) error {
89if c.EmitManPages {
90if err := doc.GenManTree(root, &c.ManPageInfo, c.OutputDir); err != nil {
91return fmt.Errorf("unable to output manpage tree: %v", err)
92}
93}
94
95if c.EmitMarkdown {
96if err := doc.GenMarkdownTree(root, c.OutputDir); err != nil {
97return fmt.Errorf("unable to output markdown tree: %v", err)
98}
99}
100
101if c.EmitHTMLFragmentWithFrontMatter {
102if err := genHTMLFragment(root, c.OutputDir+"/"+root.Name()+".html", c.Predicates); err != nil {
103return fmt.Errorf("unable to output HTML fragment file: %v", err)
104}
105}
106
107if c.EmitYAML {
108if err := doc.GenYamlTree(root, c.OutputDir); err != nil {
109return fmt.Errorf("unable to output YAML tree: %v", err)
110}
111}
112
113if c.EmitBashCompletion {
114if err := root.GenBashCompletionFile(c.OutputDir + "/" + root.Name() + ".bash"); err != nil {
115return fmt.Errorf("unable to output bash completion file: %v", err)
116}
117}
118
119if c.EmitZshCompletion {
120
121// Constants used in zsh completion file
122zshInitialization := `#compdef ` + root.Name() + `
123
124__istio_bash_source() {
125alias shopt=':'
126alias _expand=_bash_expand
127alias _complete=_bash_comp
128emulate -L sh
129setopt kshglob noshglob braceexpand
130source "$@"
131}
132__istio_type() {
133# -t is not supported by zsh
134if [ "$1" == "-t" ]; then
135shift
136# fake Bash 4 to disable "complete -o nospace". Instead
137# "compopt +-o nospace" is used in the code to toggle trailing
138# spaces. We don't support that, but leave trailing spaces on
139# all the time
140if [ "$1" = "__istio_compopt" ]; then
141echo builtin
142return 0
143fi
144fi
145type "$@"
146}
147__istio_compgen() {
148local completions w
149completions=( $(compgen "$@") ) || return $?
150# filter by given word as prefix
151while [[ "$1" = -* && "$1" != -- ]]; do
152shift
153shift
154done
155if [[ "$1" == -- ]]; then
156shift
157fi
158for w in "${completions[@]}"; do
159if [[ "${w}" = "$1"* ]]; then
160echo "${w}"
161fi
162done
163}
164__istio_compopt() {
165true # don't do anything. Not supported by bashcompinit in zsh
166}
167__istio_ltrim_colon_completions()
168{
169if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
170# Remove colon-word prefix from COMPREPLY items
171local colon_word=${1%${1##*:}}
172local i=${#COMPREPLY[*]}
173while [[ $((--i)) -ge 0 ]]; do
174COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
175done
176fi
177}
178__istio_get_comp_words_by_ref() {
179cur="${COMP_WORDS[COMP_CWORD]}"
180prev="${COMP_WORDS[${COMP_CWORD}-1]}"
181words=("${COMP_WORDS[@]}")
182cword=("${COMP_CWORD[@]}")
183}
184__istio_filedir() {
185local RET OLD_IFS w qw
186__istio_debug "_filedir $@ cur=$cur"
187if [[ "$1" = \~* ]]; then
188# somehow does not work. Maybe, zsh does not call this at all
189eval echo "$1"
190return 0
191fi
192OLD_IFS="$IFS"
193IFS=$'\n'
194if [ "$1" = "-d" ]; then
195shift
196RET=( $(compgen -d) )
197else
198RET=( $(compgen -f) )
199fi
200IFS="$OLD_IFS"
201IFS="," __istio_debug "RET=${RET[@]} len=${#RET[@]}"
202for w in ${RET[@]}; do
203if [[ ! "${w}" = "${cur}"* ]]; then
204continue
205fi
206if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
207qw="$(__istio_quote "${w}")"
208if [ -d "${w}" ]; then
209COMPREPLY+=("${qw}/")
210else
211COMPREPLY+=("${qw}")
212fi
213fi
214done
215}
216__istio_quote() {
217if [[ $1 == \'* || $1 == \"* ]]; then
218# Leave out first character
219printf %q "${1:1}"
220else
221printf %q "$1"
222fi
223}
224autoload -U +X bashcompinit && bashcompinit
225# use word boundary patterns for BSD or GNU sed
226LWORD='[[:<:]]'
227RWORD='[[:>:]]'
228if sed --help 2>&1 | grep -q GNU; then
229LWORD='\<'
230RWORD='\>'
231fi
232__istio_convert_bash_to_zsh() {
233sed \
234-e 's/declare -F/whence -w/' \
235-e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
236-e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
237-e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
238-e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
239-e "s/${LWORD}_filedir${RWORD}/__istio_filedir/g" \
240-e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__istio_get_comp_words_by_ref/g" \
241-e "s/${LWORD}__ltrim_colon_completions${RWORD}/__istio_ltrim_colon_completions/g" \
242-e "s/${LWORD}compgen${RWORD}/__istio_compgen/g" \
243-e "s/${LWORD}compopt${RWORD}/__istio_compopt/g" \
244-e "s/${LWORD}declare${RWORD}/builtin declare/g" \
245-e "s/\\\$(type${RWORD}/\$(__istio_type/g" \
246<<'BASH_COMPLETION_EOF'
247`
248
249zshTail := `
250BASH_COMPLETION_EOF
251}
252
253__istio_bash_source <(__istio_convert_bash_to_zsh)
254_complete istio 2>/dev/null
255`
256
257// Create the output file.
258outFile, err := os.Create(c.OutputDir + "/_" + root.Name())
259if err != nil {
260return fmt.Errorf("unable to create zsh completion file: %v", err)
261}
262defer func() { _ = outFile.Close() }()
263
264// Concatenate the head, initialization, generated bash, and tail to the file
265if _, err = outFile.WriteString(zshInitialization); err != nil {
266return fmt.Errorf("unable to output zsh initialization: %v", err)
267}
268if err = root.GenBashCompletion(outFile); err != nil {
269return fmt.Errorf("unable to output zsh completion file: %v", err)
270}
271if _, err = outFile.WriteString(zshTail); err != nil {
272return fmt.Errorf("unable to output zsh tail: %v", err)
273}
274}
275
276return nil
277}
278
279type generator struct {
280buffer *bytes.Buffer
281}
282
283func (g *generator) emit(str ...string) {
284for _, s := range str {
285g.buffer.WriteString(s)
286}
287g.buffer.WriteByte('\n')
288}
289
290func findCommands(commands map[string]*cobra.Command, cmd *cobra.Command) {
291cmd.InitDefaultHelpCmd()
292cmd.InitDefaultHelpFlag()
293
294commands[cmd.CommandPath()] = cmd
295for _, c := range cmd.Commands() {
296findCommands(commands, c)
297}
298}
299
300const help = "help"
301
302func genHTMLFragment(cmd *cobra.Command, path string, p Predicates) error {
303commands := make(map[string]*cobra.Command)
304findCommands(commands, cmd)
305
306names := make([]string, len(commands))
307i := 0
308for n := range commands {
309names[i] = n
310i++
311}
312sort.Strings(names)
313
314g := &generator{
315buffer: &bytes.Buffer{},
316}
317
318count := 0
319for _, n := range names {
320if commands[n].Name() == help {
321continue
322}
323
324count++
325}
326
327g.genFrontMatter(cmd, count)
328for _, n := range names {
329if commands[n].Name() == help {
330continue
331}
332
333g.genCommand(commands[n])
334_ = g.genConfigFile(commands[n].Flags())
335}
336
337g.genVars(cmd, p.SelectEnv)
338g.genMetrics(p.SelectMetric)
339
340f, err := os.Create(path)
341if err != nil {
342return err
343}
344_, err = g.buffer.WriteTo(f)
345_ = f.Close()
346
347return err
348}
349
350func (g *generator) genConfigFile(flags *pflag.FlagSet) error {
351// find all flag names and aliases which contain dots, as these allow
352// structured config files, which aren't intuitive
353deepkeys := make(map[string]string)
354flags.VisitAll(func(f *pflag.Flag) {
355if strings.Contains(f.Name, ".") {
356deepkeys[f.Name] = "--" + f.Name
357}
358})
359
360if len(deepkeys) < 1 {
361return nil
362}
363
364deepConfig := buildNestedMap(deepkeys)
365strb, err := yaml.Marshal(deepConfig)
366if err != nil {
367return err
368}
369str := string(strb)
370
371g.emit("<p/>Accepts deep config files, like:")
372g.emit("<pre class=\"language-yaml\"><code>", str)
373g.emit("</code></pre>")
374return nil
375}
376
377func dereferenceMap(m map[string]string) (result map[string]string) {
378// return a map keyed by alias, whose value is the alias' ultimate target
379// consider the map as the list of edges in a set of trees
380// return a map where each node has one key pointing to the root node of its tree
381reversemap := make(map[string][]string)
382result = make(map[string]string)
383for key, value := range m {
384if deepvalue, ok := result[value]; ok {
385value = deepvalue
386}
387if deepkeys, ok := reversemap[key]; ok {
388// we have found a new candidate root node for this tree
389// look through our results so far and update to new candidate root
390for _, deepkey := range deepkeys {
391result[deepkey] = value
392}
393delete(reversemap, key)
394reversemap[value] = append(reversemap[value], deepkeys...)
395}
396result[key] = value
397reversemap[value] = append(reversemap[value], key)
398}
399return
400}
401
402func buildNestedMap(flatMap map[string]string) (result map[string]any) {
403result = make(map[string]any)
404for complexkey, value := range flatMap {
405buildMapRecursive(strings.Split(complexkey, "."), result, value)
406}
407return
408}
409
410func buildMapRecursive(remainingPath []string, currentPointer map[string]any, value string) {
411if len(remainingPath) == 1 {
412currentPointer[remainingPath[0]] = value
413return
414}
415var nextPointer any
416var existingPath bool
417if nextPointer, existingPath = currentPointer[remainingPath[0]]; !existingPath {
418nextPointer = make(map[string]any)
419currentPointer[remainingPath[0]] = nextPointer
420}
421buildMapRecursive(remainingPath[1:], nextPointer.(map[string]any), value)
422}
423
424func (g *generator) genFrontMatter(root *cobra.Command, numEntries int) {
425g.emit("---")
426g.emit("title: ", root.Name())
427g.emit("description: ", root.Short)
428g.emit("generator: pkg-collateral-docs")
429g.emit("number_of_entries: ", strconv.Itoa(numEntries))
430g.emit("max_toc_level: 2")
431g.emit("remove_toc_prefix: '" + root.Name() + " '")
432g.emit("---")
433}
434
435func (g *generator) genCommand(cmd *cobra.Command) {
436if cmd.Hidden || cmd.Deprecated != "" {
437return
438}
439
440if cmd.HasParent() {
441g.emit("<h2 id=\"", normalizeID(cmd.CommandPath()), "\">", cmd.CommandPath(), "</h2>")
442}
443
444if cmd.Long != "" {
445g.emitText(cmd.Long)
446} else if cmd.Short != "" {
447g.emitText(cmd.Short)
448}
449
450if cmd.Runnable() {
451g.emit("<pre class=\"language-bash\"><code>", html.EscapeString(cmd.UseLine()))
452g.emit("</code></pre>")
453
454if len(cmd.Aliases) > 0 {
455// first word in cmd.Use represents the command that is being aliased
456word := cmd.Use
457index := strings.Index(word, " ")
458if index > 0 {
459word = word[0:index]
460}
461
462g.emit("<div class=\"aliases\">")
463line := cmd.UseLine()
464for i, alias := range cmd.Aliases {
465r := strings.Replace(line, word, alias, 1)
466if i == 0 {
467g.emit("<pre class=\"language-bash\"><code>", html.EscapeString(r))
468} else {
469g.emit(html.EscapeString(r))
470}
471}
472g.emit("</code></pre></div>")
473}
474}
475
476flags := cmd.NonInheritedFlags()
477flags.SetOutput(g.buffer)
478
479parentFlags := cmd.InheritedFlags()
480parentFlags.SetOutput(g.buffer)
481
482if flags.HasFlags() || parentFlags.HasFlags() {
483f := make(map[string]*pflag.Flag)
484addFlags(f, flags)
485addFlags(f, parentFlags)
486
487if len(f) > 0 {
488names := make([]string, len(f))
489i := 0
490for n := range f {
491names[i] = n
492i++
493}
494sort.Strings(names)
495
496genShorthand := false
497for _, v := range f {
498if v.Shorthand != "" && v.ShorthandDeprecated == "" {
499genShorthand = true
500break
501}
502}
503
504g.emit("<table class=\"command-flags\">")
505g.emit("<thead>")
506g.emit("<tr>")
507g.emit("<th>Flags</th>")
508if genShorthand {
509g.emit("<th>Shorthand</th>")
510}
511g.emit("<th>Description</th>")
512g.emit("</tr>")
513g.emit("</thead>")
514g.emit("<tbody>")
515
516for _, n := range names {
517g.genFlag(f[n], genShorthand)
518}
519
520g.emit("</tbody>")
521g.emit("</table>")
522}
523}
524
525if len(cmd.Example) > 0 {
526g.emit("<h3 id=\"", normalizeID(cmd.CommandPath()), " Examples\">", "Examples", "</h3>")
527g.emit("<pre class=\"language-bash\"><code>", html.EscapeString(cmd.Example))
528g.emit("</code></pre>")
529}
530}
531
532func addFlags(f map[string]*pflag.Flag, s *pflag.FlagSet) {
533s.VisitAll(func(flag *pflag.Flag) {
534if flag.Deprecated != "" || flag.Hidden {
535return
536}
537
538if flag.Name == help {
539return
540}
541
542f[flag.Name] = flag
543})
544}
545
546func (g *generator) genFlag(flag *pflag.Flag, genShorthand bool) {
547varname, usage := unquoteUsage(flag)
548if varname != "" {
549varname = " <" + varname + ">"
550}
551
552def := ""
553if flag.Value.Type() == "string" {
554def = fmt.Sprintf(" (default `%s`)", flag.DefValue)
555} else if flag.Value.Type() != "bool" {
556def = fmt.Sprintf(" (default `%s`)", flag.DefValue)
557}
558
559g.emit("<tr>")
560g.emit("<td><code>", "--", flag.Name, html.EscapeString(varname), "</code></td>")
561
562if genShorthand {
563if flag.Shorthand != "" && flag.ShorthandDeprecated == "" {
564g.emit("<td><code>", "-", flag.Shorthand, "</code></td>")
565} else {
566g.emit("<td></td>")
567}
568}
569
570g.emit("<td>", html.EscapeString(usage), " ", def, "</td>")
571g.emit("</tr>")
572}
573
574func (g *generator) emitText(text string) {
575paras := strings.Split(text, "\n\n")
576for _, p := range paras {
577g.emit("<p>", html.EscapeString(p), "</p>")
578}
579}
580
581// unquoteUsage extracts a back-quoted name from the usage
582// string for a flag and returns it and the un-quoted usage.
583// Given "a `name` to show" it returns ("name", "a name to show").
584// If there are no back quotes, the name is an educated guess of the
585// type of the flag's value, or the empty string if the flag is boolean.
586func unquoteUsage(flag *pflag.Flag) (name string, usage string) {
587// Look for a back-quoted name, but avoid the strings package.
588usage = flag.Usage
589for i := 0; i < len(usage); i++ {
590if usage[i] == '`' {
591for j := i + 1; j < len(usage); j++ {
592if usage[j] == '`' {
593name = usage[i+1 : j]
594usage = usage[:i] + name + usage[j+1:]
595return name, usage
596}
597}
598break // Only one back quote; use type name.
599}
600}
601
602name = flag.Value.Type()
603switch name {
604case "bool":
605name = ""
606case "float64":
607name = "float"
608case "int64":
609name = "int"
610case "uint64":
611name = "uint"
612}
613
614return
615}
616
617func normalizeID(id string) string {
618id = strings.Replace(id, " ", "-", -1)
619return strings.Replace(id, ".", "-", -1)
620}
621
622func (g *generator) genVars(root *cobra.Command, selectFn SelectEnvFn) {
623if selectFn == nil {
624selectFn = DefaultSelectEnvFn
625}
626
627envVars := env.VarDescriptions()
628
629count := 0
630for _, v := range envVars {
631if v.Hidden {
632continue
633}
634if !selectFn(v) {
635continue
636}
637count++
638}
639
640if count == 0 {
641return
642}
643
644g.emit("<h2 id=\"envvars\">Environment variables</h2>")
645
646g.emit("These environment variables affect the behavior of the <code>", root.Name(), "</code> command. "+
647"Please use with caution as these environment variables are experimental and can change anytime.")
648
649g.emit("<table class=\"envvars\">")
650g.emit("<thead>")
651g.emit("<tr>")
652g.emit("<th>Variable Name</th>")
653g.emit("<th>Type</th>")
654g.emit("<th>Default Value</th>")
655g.emit("<th>Description</th>")
656g.emit("</tr>")
657g.emit("</thead>")
658g.emit("<tbody>")
659
660for _, v := range envVars {
661if v.Hidden {
662continue
663}
664if !selectFn(v) {
665continue
666}
667
668if v.Deprecated {
669g.emit("<tr class='deprecated'>")
670} else {
671g.emit("<tr>")
672}
673g.emit("<td><code>", html.EscapeString(v.Name), "</code></td>")
674
675switch v.Type {
676case env.STRING:
677g.emit("<td>String</td>")
678case env.BOOL:
679g.emit("<td>Boolean</td>")
680case env.INT:
681g.emit("<td>Integer</td>")
682case env.FLOAT:
683g.emit("<td>Floating-Point</td>")
684case env.DURATION:
685g.emit("<td>Time Duration</td>")
686case env.OTHER:
687g.emit(fmt.Sprintf("<td>%s</td>", v.GoType))
688}
689
690g.emit("<td><code>", html.EscapeString(v.DefaultValue), "</code></td>")
691g.emit("<td>", html.EscapeString(v.Description), "</td>")
692g.emit("</tr>")
693}
694
695g.emit("</tbody>")
696g.emit("</table>")
697}
698
699func (g *generator) genMetrics(selectFn SelectMetricFn) {
700if selectFn == nil {
701selectFn = DefaultSelectMetricFn
702}
703
704g.emit(`<h2 id="metrics">Exported metrics</h2>
705<table class="metrics">
706<thead>
707<tr><th>Metric Name</th><th>Type</th><th>Description</th></tr>
708</thead>
709<tbody>`)
710
711for _, metric := range metrics.ExportedMetrics() {
712if !selectFn(metric) {
713continue
714}
715g.emit("<tr><td><code>", metric.Name, "</code></td><td><code>", metric.Type, "</code></td><td>", metric.Description, "</td></tr>")
716}
717
718g.emit(`</tbody>
719</table>`)
720}
721