mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 14:35:22 +08:00
This commit enhances the CLI reference documentation by automatically displaying valid enum values in the "Default" column of flag tables, eliminating the need to manually list these values in flag descriptions. It also adds a CI step to verify that CLI reference docs stay in sync with source code changes. The core implementation uses reflection to extract enum values from kingpin flags and formats them as "(valid: `value1`, `value2`, ...)". All CLI source files have been updated to remove duplicate enum value lists from flag help text, as these are now automatically rendered. A new 'cli-docs-up-to-date' Makefile target and corresponding GitHub Actions workflow job ensure that any changes to CLI flags are reflected in the generated documentation. Example output: Before: |`--format`|`yaml`|Output format, 'yaml', 'json', or 'text'| After: |`--format`|`yaml` (valid: `yaml`, `json`, `text`)|Output format.| Signed-off-by: Tiago Silva <tiago.silva@goteleport.com>
593 lines
13 KiB
Go
593 lines
13 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"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kingpin/v2"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestUpdateAppUsageTemplate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
makeApp func() *kingpin.Application
|
|
expectSubstring string // The @ character is replaced with a backtick
|
|
config generatorConfig
|
|
}{
|
|
{
|
|
name: "subcommand flags and global flags",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
app.Command("hello", "Hello.")
|
|
create := app.Command("create", "Create.")
|
|
create.Flag("name", "The name of the resource").Default("myresource").String()
|
|
createRocket := create.Command("rocket", "Rocket.")
|
|
createRocket.Flag("launch", "Whether to launch the Rocket").Bool()
|
|
return app
|
|
},
|
|
config: generatorConfig{
|
|
Introduction: "This is the main CLI tool.",
|
|
},
|
|
expectSubstring: `---
|
|
title: myapp Reference
|
|
description: Provides a comprehensive list of commands, arguments, and flags for myapp.
|
|
sidebar_label: myapp
|
|
tags:
|
|
- reference
|
|
- platform-wide
|
|
---
|
|
{/*vale messaging = NO*/}
|
|
|
|
This guide provides a comprehensive list of commands, arguments, and flags for
|
|
myapp.
|
|
|
|
This is the main CLI tool.
|
|
|
|
@@@code
|
|
$ myapp [<flags>] <command> [<args> ...]
|
|
@@@
|
|
|
|
Global flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--config@|@config.yaml@|The location of the config file|
|
|
|
|
## myapp create rocket
|
|
|
|
Rocket.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp create rocket [<flags>]
|
|
@@@
|
|
|
|
Flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--[no-]launch@|@false@|Whether to launch the Rocket|
|
|
|
|
## myapp hello
|
|
|
|
Hello.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp hello
|
|
@@@
|
|
|
|
## myapp help
|
|
|
|
Show help.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp help [<command>...]
|
|
@@@
|
|
|
|
Arguments:
|
|
|
|
|Argument|Default|Description|
|
|
|---|---|---|
|
|
|command|none (optional)|Show help on command.|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "multiple main command flags",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
app.Flag("verbosity", "Verbosity level.").Default("3").Int()
|
|
app.Flag("dry-run", "Whether to use dry-run mode").Default("false").Bool()
|
|
return app
|
|
},
|
|
config: generatorConfig{
|
|
Introduction: "This is the main CLI tool.",
|
|
},
|
|
expectSubstring: `This guide provides a comprehensive list of commands, arguments, and flags for
|
|
myapp.
|
|
|
|
This is the main CLI tool.
|
|
|
|
@@@code
|
|
$ myapp [<flags>] <command> [<args> ...]
|
|
@@@
|
|
|
|
Global flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--config@|@config.yaml@|The location of the config file|
|
|
|@--[no-]dry-run@|@false@|Whether to use dry-run mode|
|
|
|@--verbosity@|@3@|Verbosity level.|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "multiple subcommand flags",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
create := app.Command("create", "Create a resource.")
|
|
create.Flag("verbosity", "Verbosity level.").Default("3").Int()
|
|
create.Flag("dry-run", "Whether to use dry-run mode").Default("false").Bool()
|
|
return app
|
|
},
|
|
expectSubstring: `## myapp create
|
|
|
|
Create a resource.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp create [<flags>]
|
|
@@@
|
|
|
|
Flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--[no-]dry-run@|@false@|Whether to use dry-run mode|
|
|
|@--verbosity@|@3@|Verbosity level.|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "multiple sub-command args",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
create := app.Command("create", "Create.")
|
|
create.Arg("verbosity", "Verbosity level.").Default("3").Int()
|
|
create.Arg("dry-run", "Whether to use dry-run mode").Default("false").Bool()
|
|
return app
|
|
},
|
|
expectSubstring: `## myapp create
|
|
|
|
Create.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp create [<verbosity>] [<dry-run>]
|
|
@@@
|
|
|
|
Arguments:
|
|
|
|
|Argument|Default|Description|
|
|
|---|---|---|
|
|
|dry-run|@false@ (optional)|Whether to use dry-run mode|
|
|
|verbosity|@3@ (optional)|Verbosity level.|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "sub-command order",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
app.Command("create", "Create a resource.")
|
|
app.Command("validate", "Validate the config.")
|
|
app.Command("connect", "Connect to a server.")
|
|
return app
|
|
},
|
|
expectSubstring: `## myapp connect
|
|
|
|
Connect to a server.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp connect
|
|
@@@
|
|
|
|
## myapp create
|
|
|
|
Create a resource.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp create
|
|
@@@
|
|
|
|
## myapp help
|
|
|
|
Show help.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp help [<command>...]
|
|
@@@
|
|
|
|
Arguments:
|
|
|
|
|Argument|Default|Description|
|
|
|---|---|---|
|
|
|command|none (optional)|Show help on command.|
|
|
|
|
## myapp validate
|
|
|
|
Validate the config.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp validate
|
|
@@@
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "level-3 command order",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
mfa := app.Command("mfa", "Manage MFA resources.")
|
|
mfa.Command("add", "Add an MFA device.")
|
|
app.Command("create", "Create a resource")
|
|
return app
|
|
},
|
|
expectSubstring: `## myapp create
|
|
|
|
Create a resource
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp create
|
|
@@@
|
|
|
|
## myapp help
|
|
|
|
Show help.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp help [<command>...]
|
|
@@@
|
|
|
|
Arguments:
|
|
|
|
|Argument|Default|Description|
|
|
|---|---|---|
|
|
|command|none (optional)|Show help on command.|
|
|
|
|
## myapp mfa add
|
|
|
|
Add an MFA device.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp mfa add
|
|
@@@
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "empty arg",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
app.Command("kubectl", "Proxy kubectl commands.")
|
|
kubectl := app.Command("kubectl", "Proxy kubectl commands.").Interspersed(false)
|
|
// This hack is required in order to accept any args for tsh kubectl.
|
|
kubectl.Arg("", "").StringsVar(new([]string))
|
|
|
|
return app
|
|
},
|
|
expectSubstring: `## myapp kubectl
|
|
|
|
Proxy kubectl commands.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp kubectl [args...]
|
|
@@@
|
|
|
|
Arguments:
|
|
|
|
|Argument|Default|Description|
|
|
|---|---|---|
|
|
|args|none (optional)|Arbitrary arguments|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "hidden flag",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
app.Command("kubectl", "Proxy kubectl commands.")
|
|
kubectl := app.Command("kubectl", "Proxy kubectl commands.").Interspersed(false)
|
|
kubectl.Flag("diag", "Run diagnostics").Hidden().Bool()
|
|
app.Command("log", "Print logs")
|
|
|
|
return app
|
|
},
|
|
expectSubstring: `## myapp kubectl
|
|
|
|
Proxy kubectl commands.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp kubectl
|
|
@@@
|
|
|
|
## myapp log
|
|
`,
|
|
},
|
|
{
|
|
name: "main command env vars",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("verbosity", "Verbosity level.").Default("3").Envar("MYAPP_VERBOSITY").Int()
|
|
return app
|
|
},
|
|
expectSubstring: `Global flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--verbosity@|@3@|Verbosity level.|
|
|
|
|
Global environment variables:
|
|
|
|
|Variable|Default|Description|
|
|
|---|---|---|
|
|
|@MYAPP_VERBOSITY@|@3@|Verbosity level.|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "subcommand env vars",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("config", "The location of the config file").Default("config.yaml").String()
|
|
create := app.Command("create", "Create a resource.")
|
|
create.Flag("name", "The name of the resource").Envar("CREATE_NAME").Default("myresource").String()
|
|
create.Arg("type", "The type of the resource").Envar("CREATE_TYPE").String()
|
|
|
|
return app
|
|
},
|
|
expectSubstring: `## myapp create
|
|
|
|
Create a resource.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp create [<flags>] [<type>]
|
|
@@@
|
|
|
|
Environment variables:
|
|
|
|
|Variable|Default|Description|
|
|
|---|---|---|
|
|
|@CREATE_NAME@|@myresource@|The name of the resource|
|
|
|@CREATE_TYPE@|none (optional)|The type of the resource|
|
|
|
|
Flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--name@|@myresource@|The name of the resource|
|
|
|
|
Arguments:
|
|
|
|
|Argument|Default|Description|
|
|
|---|---|---|
|
|
|type|none (optional)|The type of the resource|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "overridden dynamic flag value",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
app.Flag("start_time", "When to start the app").Default(time.Now().String()).String()
|
|
return app
|
|
},
|
|
config: generatorConfig{
|
|
Introduction: "This is the main CLI tool.",
|
|
FlagDefaultOverrides: []flagDefaultOverride{
|
|
{
|
|
FullCommand: "myapp",
|
|
Flag: "start_time",
|
|
Value: "now",
|
|
},
|
|
},
|
|
},
|
|
expectSubstring: `---
|
|
title: myapp Reference
|
|
description: Provides a comprehensive list of commands, arguments, and flags for myapp.
|
|
sidebar_label: myapp
|
|
tags:
|
|
- reference
|
|
- platform-wide
|
|
---
|
|
{/*vale messaging = NO*/}
|
|
|
|
This guide provides a comprehensive list of commands, arguments, and flags for
|
|
myapp.
|
|
|
|
This is the main CLI tool.
|
|
|
|
@@@code
|
|
$ myapp [<flags>] <command> [<args> ...]
|
|
@@@
|
|
|
|
Global flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--start_time@|@now@|When to start the app|
|
|
`,
|
|
},
|
|
{
|
|
name: "overridden subcommand argument default",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
create := app.Command("create", "Create.")
|
|
create.Arg("verbosity", "Verbosity level.").Default("3").Int()
|
|
create.Arg("start_time", "When to start the app").Default(time.Now().String()).String()
|
|
return app
|
|
},
|
|
config: generatorConfig{
|
|
Introduction: "This is the main CLI tool.",
|
|
ArgDefaultOverrides: []argDefaultOverride{
|
|
{
|
|
FullCommand: "myapp create",
|
|
Arg: "start_time",
|
|
Value: "now",
|
|
},
|
|
},
|
|
},
|
|
expectSubstring: `## myapp create
|
|
|
|
Create.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp create [<verbosity>] [<start_time>]
|
|
@@@
|
|
|
|
Arguments:
|
|
|
|
|Argument|Default|Description|
|
|
|---|---|---|
|
|
|start_time|@now@ (optional)|When to start the app|
|
|
|verbosity|@3@ (optional)|Verbosity level.|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "enum flag with default value",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
var format string
|
|
app.Flag("format", "Output format").Default("text").EnumVar(&format, "text", "json", "yaml")
|
|
return app
|
|
},
|
|
config: generatorConfig{
|
|
Introduction: "This is the main CLI tool.",
|
|
},
|
|
expectSubstring: `Global flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--format@|@text@ (valid: @text@, @json@, @yaml@)|Output format|
|
|
|
|
`,
|
|
},
|
|
{
|
|
name: "enum flag in subcommand",
|
|
makeApp: func() *kingpin.Application {
|
|
app := InitCLIParser("myapp", "This is the main CLI tool.")
|
|
list := app.Command("list", "List resources.")
|
|
var mode string
|
|
list.Flag("mode", "List mode").Default("all").EnumVar(&mode, "all", "active", "inactive")
|
|
return app
|
|
},
|
|
config: generatorConfig{
|
|
Introduction: "This is the main CLI tool.",
|
|
},
|
|
expectSubstring: `## myapp list
|
|
|
|
List resources.
|
|
|
|
Usage:
|
|
|
|
@@@code
|
|
$ myapp list [<flags>]
|
|
@@@
|
|
|
|
Flags:
|
|
|
|
|Flag|Default|Description|
|
|
|---|---|---|
|
|
|@--mode@|@all@ (valid: @all@, @active@, @inactive@)|List mode|
|
|
|
|
`,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
app := tt.makeApp()
|
|
var buffer bytes.Buffer
|
|
app.UsageWriter(&buffer)
|
|
args := []string{"help"}
|
|
app.Terminate(func(int) {})
|
|
|
|
docsUsageTemplatePath := "docs-usage.md.tmpl"
|
|
f, err := os.Open(docsUsageTemplatePath)
|
|
require.NoError(t, err)
|
|
updateAppUsageTemplate(f, tt.config, app)
|
|
|
|
// kingpin only adds a help command if there is at least
|
|
// one subcommand. Make sure that all test cases
|
|
// introduce a help command.
|
|
app.HelpCommand = app.Command("help", "Print help for the application.")
|
|
// HelpCommand is triggered on PreAction during Parse.
|
|
// See kingpin.Application.init for more details.
|
|
_, err = app.Parse(args)
|
|
require.NoError(t, err)
|
|
expected := strings.ReplaceAll(tt.expectSubstring, "@", "`")
|
|
require.Contains(t, buffer.String(), expected)
|
|
})
|
|
}
|
|
}
|