mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 14:35:22 +08:00
* Generate a tctl reference page Add a `cli-docs-tctl` Make target and generate the page. To allow the page to render, make the `tctl edit` help text more consistent with that of other commands by using a single line for the resource type/name argument. This change adds 47 `tctl` commands not present in the current reference. Also change the default value of the `tctl recordings download` `output-dir` flag to the system-independent "." value. Otherwise, generating this page prints a system-specific file path. Also make minor modifications to flag, argument, and command descriptions to be consistent with messaging conventions in the docs. * CLI ref generator: add arg/flag default overrides In the CLI reference doc generator, make it possible to override default argument and flag values. This is necessary when these values are dynamic and system dependent, e.g., a user's home directory. Add the `flag_default_overrides` and `arg_default_overrides` fields to the generator config file. Use these fields to create template functions that replace default values in a `*kingpin.FlagModel` or `*kingpin.ArgModel`. * Add flag default overrides for tctl reference Configure flags with dynamic values to have hardcoded values in the reference docs page so there is no need to edit the dynamic argument logic in the tctl source.
544 lines
12 KiB
Go
544 lines
12 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.|
|
|
|
|
`,
|
|
},
|
|
}
|
|
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)
|
|
})
|
|
}
|
|
}
|