mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 05:55:42 +08:00
470 lines
14 KiB
Go
470 lines
14 KiB
Go
// Teleport
|
|
// Copyright (C) 2025 Gravitational, Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//go:build docs
|
|
|
|
package utils
|
|
|
|
import (
|
|
"bytes"
|
|
"cmp"
|
|
_ "embed"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/alecthomas/kingpin/v2"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var nonLetters = regexp.MustCompile(`\W`)
|
|
|
|
// lowerWordChars returns only the word characters (\w) from in, in lowercase,
|
|
// so we can sort short-format flags alongside long-format flags in tables.
|
|
func lowerWordChars(in string) string {
|
|
return strings.ToLower(nonLetters.ReplaceAllString(in, ""))
|
|
}
|
|
|
|
// formatThreeColMarkdownTable formats the provided row data into a three-column
|
|
// Markdown table, minus the header, sorted lexicographically by the values of
|
|
// the first column.
|
|
func formatThreeColMarkdownTable(rows [][3]string) string {
|
|
newRows := slices.Clone(rows)
|
|
slices.SortFunc(newRows, func(a, b [3]string) int {
|
|
return strings.Compare(
|
|
lowerWordChars(a[0]),
|
|
lowerWordChars(b[0]),
|
|
)
|
|
})
|
|
|
|
var buf bytes.Buffer
|
|
for _, r := range newRows {
|
|
fmt.Fprintf(&buf, "\n|%v|%v|%v|", r[0], r[1], r[2])
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// flagsToRows outputs data for a table that lists flags, their default
|
|
// values, and help texts.
|
|
func flagsToRows(f []*kingpin.FlagModel) [][3]string {
|
|
rows := [][3]string{}
|
|
|
|
for _, flag := range f {
|
|
// Skip hidden flags and flags whose only purpose is to expose
|
|
// YAML-based default env variables.
|
|
if flag.Hidden || flag.Name == flag.Envar {
|
|
continue
|
|
}
|
|
flagString := ""
|
|
flagName := flag.Name
|
|
if flag.IsBoolFlag() {
|
|
flagName = "[no-]" + flagName
|
|
}
|
|
if flag.Short != 0 {
|
|
flagString += fmt.Sprintf("`-%c`, `--%s`", flag.Short, flagName)
|
|
} else {
|
|
flagString += fmt.Sprintf("`--%s`", flagName)
|
|
}
|
|
|
|
rows = append(rows, [3]string{
|
|
flagString,
|
|
formatDefaultFlagValue(flag),
|
|
formatHelp(flag.Help),
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// anyVisibleFlags returns whether any flags in f are visible, i.e., should be
|
|
// included in a table of flags.
|
|
func anyVisibleFlags(f []*kingpin.FlagModel) bool {
|
|
return slices.ContainsFunc(f, func(m *kingpin.FlagModel) bool {
|
|
return !m.Hidden
|
|
})
|
|
}
|
|
|
|
// anyEnvVarsForCmd indicates whether at least one of the arguments and flags
|
|
// provided exposes an environment variable for configuration.
|
|
func anyEnvVarsForCmd(args []*kingpin.ArgModel, flags []*kingpin.FlagModel) bool {
|
|
return slices.ContainsFunc(args, func(arg *kingpin.ArgModel) bool {
|
|
return arg.Envar != "" && !arg.Hidden
|
|
}) || slices.ContainsFunc(flags, func(flag *kingpin.FlagModel) bool {
|
|
return flag.Envar != "" && !flag.Hidden
|
|
})
|
|
}
|
|
|
|
// argsToRows outputs data for a table that lists arguments, their default
|
|
// values, and help texts.
|
|
func argsToRows(a []*kingpin.ArgModel) [][3]string {
|
|
rows := [][3]string{}
|
|
for _, arg := range a {
|
|
if arg.Hidden {
|
|
continue
|
|
}
|
|
|
|
// Some commands declare empty argument names and help texts as
|
|
// a hack to allow arbitrary values. Indicate this in the table
|
|
// as a special case.
|
|
argName := cmp.Or(arg.Name, "args")
|
|
|
|
help := "Arbitrary arguments"
|
|
if arg.Help != "" {
|
|
help = formatHelp(arg.Help)
|
|
}
|
|
|
|
rows = append(rows, [3]string{
|
|
argName,
|
|
formatDefaultArgValue(arg),
|
|
help,
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// envVarsToRows prints table data for a list of environment variables, their
|
|
// default values, and help texts.
|
|
func envVarsToRows(args []*kingpin.ArgModel, flags []*kingpin.FlagModel) [][3]string {
|
|
rows := [][3]string{}
|
|
for _, arg := range args {
|
|
if arg.Hidden || arg.Envar == "" {
|
|
continue
|
|
}
|
|
|
|
rows = append(rows, [3]string{
|
|
fmt.Sprintf("`%v`", arg.Envar),
|
|
formatDefaultArgValue(arg),
|
|
arg.Help,
|
|
})
|
|
}
|
|
for _, flg := range flags {
|
|
if flg.Hidden || flg.Envar == "" {
|
|
continue
|
|
}
|
|
|
|
rows = append(rows, [3]string{
|
|
fmt.Sprintf("`%v`", flg.Envar),
|
|
formatDefaultFlagValue(flg),
|
|
flg.Help,
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// sortcommandsByName sorts the commands in cmds by their full command names,
|
|
// including all subcommands.
|
|
func sortCommandsByName(cmds []*kingpin.CmdModel) []*kingpin.CmdModel {
|
|
slices.SortStableFunc(cmds, func(a, b *kingpin.CmdModel) int {
|
|
switch {
|
|
case a.FullCommand < b.FullCommand:
|
|
return -1
|
|
case a.FullCommand > b.FullCommand:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
})
|
|
return cmds
|
|
}
|
|
|
|
// formatDefaultFlagValue returns the default value of flag to display in a
|
|
// table of flags. Assumes that a Boolean flag is false unless it is true by
|
|
// default.
|
|
func formatDefaultFlagValue(flag *kingpin.FlagModel) string {
|
|
switch {
|
|
case len(flag.Default) == 0 && flag.IsBoolFlag():
|
|
return "`false`"
|
|
case len(flag.Default) > 0:
|
|
ret := make([]string, len(flag.Default))
|
|
for i, v := range flag.Default {
|
|
ret[i] = fmt.Sprintf("`%v`", v)
|
|
}
|
|
return strings.Join(ret, ",")
|
|
default:
|
|
return "none"
|
|
}
|
|
}
|
|
|
|
// formatDefaultArgValue returns the default value of arg to display in a table
|
|
// of flags. It also indicates whether the value is optional or required.
|
|
func formatDefaultArgValue(arg *kingpin.ArgModel) string {
|
|
var ret string
|
|
if len(arg.Default) > 0 {
|
|
def := make([]string, len(arg.Default))
|
|
for i, v := range arg.Default {
|
|
def[i] = fmt.Sprintf("`%v`", v)
|
|
}
|
|
ret = strings.Join(def, ",")
|
|
} else {
|
|
ret = "none"
|
|
}
|
|
if arg.Required {
|
|
ret += " (required)"
|
|
} else {
|
|
ret += " (optional)"
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// repeatableFlag is an interface for flags that can be repeated. Unexported
|
|
// type in github.com/alecthomas/kingpin/v2.
|
|
type repeatableFlag interface {
|
|
IsCumulative() bool
|
|
}
|
|
|
|
// formatUsageArg prints a command argument to include in a usage snippet.
|
|
func formatUsageArg(arg *kingpin.ArgModel) string {
|
|
var ret string
|
|
switch {
|
|
case arg.PlaceHolder != "":
|
|
ret = arg.PlaceHolder
|
|
// Some special cases have empty arg names
|
|
case arg.Name == "":
|
|
ret = "args"
|
|
default:
|
|
ret = "<" + arg.Name + ">"
|
|
}
|
|
if v, ok := arg.Value.(repeatableFlag); ok && v.IsCumulative() {
|
|
ret += "..."
|
|
}
|
|
if !arg.Required {
|
|
ret = "[" + ret + "]"
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// formatHelp prints help text to include in a Markdown table cell. It escapes
|
|
// curly, angle, and square braces to avoid breaking the MDX parser, and it
|
|
// escapes pipes to avoid breaking the cell.
|
|
func formatHelp(help string) string {
|
|
return strings.NewReplacer(
|
|
"{", `\{`,
|
|
"}", `\}`,
|
|
"|", `\|`,
|
|
"[", `\[`,
|
|
"]", `\]`,
|
|
"<", `\<`,
|
|
">", `\>`,
|
|
).Replace(help)
|
|
}
|
|
|
|
// docsUsageTemplatePath points to a help text template for CLI reference
|
|
// documentation. Intended to be used as the argument to
|
|
// *kingpin.Application.UsageTemplate.
|
|
var docsUsageTemplatePath = filepath.Join("lib", "utils", "docs-usage.md.tmpl")
|
|
|
|
// updateAppUsageTemplatePath updates the app usage template to print a reference
|
|
// guide for the CLI application. It reads the template from r and uses the
|
|
// config to add an introductory paragraph and entries for environment variables
|
|
// that are not available to kingpin.
|
|
func updateAppUsageTemplate(r io.Reader, config generatorConfig, app *kingpin.Application) {
|
|
var buf bytes.Buffer
|
|
if _, err := buf.ReadFrom(r); err != nil {
|
|
panic(fmt.Sprintf("unable to read from the docs usage template: %v", err))
|
|
}
|
|
|
|
existingEnvVars := make(map[string]struct{})
|
|
for _, flag := range app.Model().Flags {
|
|
if flag.Envar != "" {
|
|
existingEnvVars[flag.Envar] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for envVarName, envVar := range config.EnvVars {
|
|
// Check if the flag already exists in the app model to avoid
|
|
// duplicate flag errors.
|
|
if _, flagExists := existingEnvVars[envVarName]; flagExists {
|
|
continue
|
|
}
|
|
|
|
// If the flag does not exist, create it with the default value
|
|
// and description from the YAML file.
|
|
flag := app.Flag(envVarName, envVar.Description).
|
|
Envar(envVarName).
|
|
Default(envVar.Default)
|
|
if envVar.Type == "bool" {
|
|
flag.Bool()
|
|
} else {
|
|
flag.String()
|
|
}
|
|
}
|
|
|
|
replaceFlagDefaults := makeDefaultFlagValueOverrider(config.FlagDefaultOverrides)
|
|
replaceArgDefaults := makeDefaultArgValueOverrider(config.ArgDefaultOverrides)
|
|
|
|
// We override the default app description with a custom description
|
|
// that is better suited to the docs.
|
|
app.Help = config.Introduction
|
|
|
|
app.UsageFuncs(map[string]any{
|
|
"AnyEnvVarsForCmd": anyEnvVarsForCmd,
|
|
"AnyVisibleFlags": anyVisibleFlags,
|
|
"ArgsToRows": argsToRows,
|
|
"EnvVarsToRows": envVarsToRows,
|
|
"FlagsToRows": flagsToRows,
|
|
"FormatThreeColMarkdownTable": formatThreeColMarkdownTable,
|
|
"FormatUsageArg": formatUsageArg,
|
|
"ReplaceFlagDefaults": replaceFlagDefaults,
|
|
"ReplaceArgDefaults": replaceArgDefaults,
|
|
"SortCommandsByName": sortCommandsByName,
|
|
})
|
|
app.UsageTemplate(buf.String())
|
|
}
|
|
|
|
// envVarDefault represents the structure of environment variable defaults in YAML files.
|
|
type envVarDefault struct {
|
|
Description string `yaml:"description"`
|
|
Default string `yaml:"default"`
|
|
Type string `yaml:"type"`
|
|
}
|
|
|
|
type generatorConfig struct {
|
|
Introduction string `yaml:"introduction"`
|
|
EnvVars map[string]envVarDefault `yaml:"env_vars"`
|
|
FlagDefaultOverrides []flagDefaultOverride `yaml:"flag_default_overrides"`
|
|
ArgDefaultOverrides []argDefaultOverride `yaml:"arg_default_overrides"`
|
|
}
|
|
|
|
type flagDefaultOverride struct {
|
|
FullCommand string `yaml:"full_command"`
|
|
Flag string `yaml:"flag"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
|
|
type argDefaultOverride struct {
|
|
FullCommand string `yaml:"full_command"`
|
|
Arg string `yaml:"arg"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
|
|
// loadConfig loads possible default environment variables defined in a YAML file
|
|
// that matches the application name.
|
|
func loadConfig(appName string) (generatorConfig, error) {
|
|
pathname := filepath.Join("lib", "utils", "docsconfigs", appName+".yaml")
|
|
f, err := os.Open(pathname)
|
|
if err != nil {
|
|
return generatorConfig{}, fmt.Errorf("unable to open CLI doc generation config file at %v: %w", pathname, err)
|
|
}
|
|
|
|
var conf generatorConfig
|
|
if err = yaml.NewDecoder(f).Decode(&conf); err != nil {
|
|
return generatorConfig{}, fmt.Errorf("unable to parse the CLI doc generation config file at %v: %w", pathname, err)
|
|
}
|
|
|
|
if conf.Introduction == "" {
|
|
return generatorConfig{}, fmt.Errorf(`CLI doc generation config file at %v must have an 'introduction' field`, pathname)
|
|
}
|
|
|
|
for envVar, def := range conf.EnvVars {
|
|
if def.Description == "" || def.Default == "" || def.Type == "" {
|
|
return generatorConfig{}, fmt.Errorf("invalid YAML structure in %s: entry %q is missing one of required fields 'description', 'default' or 'type'", pathname, envVar)
|
|
}
|
|
}
|
|
|
|
return conf, nil
|
|
}
|
|
|
|
// makeDefaultFlagValueOverrider returns a template function that overrides the
|
|
// configured default values in kingpin flag models.
|
|
func makeDefaultFlagValueOverrider(overrides []flagDefaultOverride) func(fullCommand string, f []*kingpin.FlagModel) []*kingpin.FlagModel {
|
|
// maps full commands to flags to new default values
|
|
cmdToOverride := make(map[string]map[string]string)
|
|
for _, o := range overrides {
|
|
if _, ok := cmdToOverride[o.FullCommand]; !ok {
|
|
cmdToOverride[o.FullCommand] = make(map[string]string)
|
|
}
|
|
cmdToOverride[o.FullCommand][o.Flag] = o.Value
|
|
}
|
|
|
|
return func(fullCommand string, allFlags []*kingpin.FlagModel) []*kingpin.FlagModel {
|
|
if allFlags == nil || len(allFlags) == 0 {
|
|
return allFlags
|
|
}
|
|
|
|
flagsToDefaults, ok := cmdToOverride[fullCommand]
|
|
if !ok {
|
|
return allFlags
|
|
}
|
|
|
|
for _, fl := range allFlags {
|
|
d, ok := flagsToDefaults[fl.Name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
fl.Default = []string{d}
|
|
}
|
|
return allFlags
|
|
}
|
|
}
|
|
|
|
// makeDefaultArgValueOverrider returns a template function that overrides the
|
|
// configured default values in kingpin arg models.
|
|
func makeDefaultArgValueOverrider(overrides []argDefaultOverride) func(fullCommand string, f []*kingpin.ArgModel) []*kingpin.ArgModel {
|
|
// maps full commands to args to new default values
|
|
cmdToOverride := make(map[string]map[string]string)
|
|
for _, o := range overrides {
|
|
if _, ok := cmdToOverride[o.FullCommand]; !ok {
|
|
cmdToOverride[o.FullCommand] = make(map[string]string)
|
|
}
|
|
cmdToOverride[o.FullCommand][o.Arg] = o.Value
|
|
}
|
|
|
|
return func(fullCommand string, allArgs []*kingpin.ArgModel) []*kingpin.ArgModel {
|
|
if allArgs == nil || len(allArgs) == 0 {
|
|
return allArgs
|
|
}
|
|
|
|
argsToDefaults, ok := cmdToOverride[fullCommand]
|
|
if !ok {
|
|
return allArgs
|
|
}
|
|
|
|
for _, fl := range allArgs {
|
|
d, ok := argsToDefaults[fl.Name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
fl.Default = []string{d}
|
|
}
|
|
return allArgs
|
|
}
|
|
}
|
|
|
|
// UpdateAppUsageTemplate updates the app usage template to print a reference
|
|
// guide for the CLI application. Exits on errors since we need to keep the
|
|
// signature of UpdateAppUsageTemplate consistent with the one included without
|
|
// build tags, i.e., with no return value. Writes error messages to stdout to
|
|
// separate them from the help text, which kingpin writes to stderr.
|
|
func UpdateAppUsageTemplate(app *kingpin.Application) {
|
|
config, err := loadConfig(app.Name)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stdout, "Unable to load the docs generator configuration for %v: %v", app.Name, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
f, err := os.Open(docsUsageTemplatePath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stdout, "Unable to open the docs usage template at %v: %v", docsUsageTemplatePath, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
updateAppUsageTemplate(f, config, app)
|
|
}
|