Generate a tctl reference page (#62729)

* 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.
This commit is contained in:
Paul Gottschling
2026-01-30 15:46:28 +00:00
committed by GitHub
parent 9d2f6b88ce
commit e58e510ef2
12 changed files with 2459 additions and 2243 deletions
+8
View File
@@ -2036,6 +2036,14 @@ cli-docs-teleport:
$(BUILDDIR)/teleportdocs help 2>docs/pages/reference/cli/teleport.mdx && \
rm $(BUILDDIR)/teleportdocs
.PHONY: cli-docs-tctl
cli-docs-tctl:
# Executing go build instead of go run since we don't want to redirect
# irrelevant output along with the docs page content.
go build -o $(BUILDDIR)/tctldocs -tags docs ./tool/tctl && \
$(BUILDDIR)/tctldocs help 2>docs/pages/reference/cli/tctl.mdx && \
rm $(BUILDDIR)/tctldocs
# audit-event-reference generates audit event reference docs using the Web UI
# source.
.PHONY: audit-event-reference
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -30,7 +30,7 @@ Flags:
|Flag|Default|Description|
|---|---|---|
{{- .Flags|FlagsToRows|FormatThreeColMarkdownTable }}
{{- (ReplaceFlagDefaults (printf "%v %v" $appName .FullCommand) .Flags)|FlagsToRows|FormatThreeColMarkdownTable }}
{{end -}}
{{ if .Args -}}
@@ -38,7 +38,7 @@ Arguments:
|Argument|Default|Description|
|---|---|---|
{{- .Args|ArgsToRows|FormatThreeColMarkdownTable }}
{{- (ReplaceArgDefaults (printf "%v %v" $appName .FullCommand) .Args)|ArgsToRows|FormatThreeColMarkdownTable }}
{{end -}}
{{end -}}
@@ -71,7 +71,7 @@ Global flags:
|Flag|Default|Description|
|---|---|---|
{{- .Context.Flags|FlagsToRows|FormatThreeColMarkdownTable}}
{{- (ReplaceFlagDefaults .App.Name .Context.Flags)|FlagsToRows|FormatThreeColMarkdownTable}}
{{end -}}
{{if AnyEnvVarsForCmd .Context.Args .Context.Flags -}}
@@ -87,7 +87,7 @@ Arguments:
|Argument|Default|Description|
|---|---|---|
{{- .Context.Args|ArgsToRows|FormatThreeColMarkdownTable}}
{{- (ReplaceArgDefaults .App.Name .Context.Args)|ArgsToRows|FormatThreeColMarkdownTable}}
{{end -}}
{{if .App.Commands -}}
+18
View File
@@ -0,0 +1,18 @@
introduction: |-
`tctl` is a CLI tool that allows a cluster administrator to manage all
resources in a cluster, including nodes, users, tokens, certificates, and
devices.
`tctl` can also be used to modify the dynamic configuration of the cluster,
such as creating new user roles or connecting to trusted clusters.
For a conceptual overview of `tctl`, see [Getting Started with
`tctl`](../../zero-trust-access/infrastructure-as-code/using-tctl.mdx).
flag_default_overrides:
- full_command: tctl recordings download
flag: output-dir
value: "<current working directory>"
- full_command: tctl plugins install github
flag: start-date
value: "<today>"
+87 -2
View File
@@ -306,6 +306,9 @@ func updateAppUsageTemplate(r io.Reader, config generatorConfig, app *kingpin.Ap
}
}
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
@@ -318,6 +321,8 @@ func updateAppUsageTemplate(r io.Reader, config generatorConfig, app *kingpin.Ap
"FlagsToRows": flagsToRows,
"FormatThreeColMarkdownTable": formatThreeColMarkdownTable,
"FormatUsageArg": formatUsageArg,
"ReplaceFlagDefaults": replaceFlagDefaults,
"ReplaceArgDefaults": replaceArgDefaults,
"SortCommandsByName": sortCommandsByName,
})
app.UsageTemplate(buf.String())
@@ -331,8 +336,22 @@ type envVarDefault struct {
}
type generatorConfig struct {
Introduction string `yaml:"introduction"`
EnvVars map[string]envVarDefault `yaml:"env_vars"`
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
@@ -362,6 +381,72 @@ func loadConfig(appName string) (generatorConfig, error) {
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
+82
View File
@@ -23,6 +23,7 @@ import (
"os"
"strings"
"testing"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/stretchr/testify/require"
@@ -430,6 +431,87 @@ Arguments:
|---|---|---|
|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.|
`,
},
}
+10 -10
View File
@@ -80,18 +80,18 @@ type AccessRequestCommand struct {
// Initialize allows AccessRequestCommand to plug itself into the CLI parser
func (c *AccessRequestCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, config *servicecfg.Config) {
c.config = config
requests := app.Command("requests", "Manage access requests.").Alias("request")
requests := app.Command("requests", "Manage Access Requests.").Alias("request")
c.requestList = requests.Command("ls", "Show active access requests.")
c.requestList = requests.Command("ls", "Show active Access Requests.")
c.requestList.Flag("format", "Output format, 'text' or 'json'").Hidden().Default(teleport.Text).StringVar(&c.format)
c.requestList.Flag("sort-index", "Request sort index, 'created' or 'state'").Default("created").StringVar(&c.sortIndex)
c.requestList.Flag("sort-order", "Request sort order, 'ascending' or 'descending'").Default("descending").StringVar(&c.sortOrder)
c.requestGet = requests.Command("get", "Show access request by ID.")
c.requestGet = requests.Command("get", "Show Access Request by ID.")
c.requestGet.Arg("request-id", "ID of target request(s)").Required().StringVar(&c.reqIDs)
c.requestGet.Flag("format", "Output format, 'text' or 'json'").Hidden().Default(teleport.Text).StringVar(&c.format)
c.requestApprove = requests.Command("approve", "Approve pending access request.")
c.requestApprove = requests.Command("approve", "Approve pending Access Request.")
c.requestApprove.Arg("request-id", "ID of target request(s)").Required().StringVar(&c.reqIDs)
c.requestApprove.Flag("delegator", "Optional delegating identity").StringVar(&c.delegator)
c.requestApprove.Flag("reason", "Optional reason message").StringVar(&c.reason)
@@ -99,27 +99,27 @@ func (c *AccessRequestCommand) Initialize(app *kingpin.Application, _ *tctlcfg.G
c.requestApprove.Flag("roles", "Override requested roles <role>[,...]").StringVar(&c.roles)
c.requestApprove.Flag("assume-start-time", "Sets time roles can be assumed by requestor (RFC3339 e.g 2023-12-12T23:20:50.52Z)").StringVar(&c.assumeStartTimeRaw)
c.requestDeny = requests.Command("deny", "Deny pending access request.")
c.requestDeny = requests.Command("deny", "Deny pending Access Request.")
c.requestDeny.Arg("request-id", "ID of target request(s)").Required().StringVar(&c.reqIDs)
c.requestDeny.Flag("delegator", "Optional delegating identity").StringVar(&c.delegator)
c.requestDeny.Flag("reason", "Optional reason message").StringVar(&c.reason)
c.requestDeny.Flag("annotations", "Resolution annotations <key>=<val>[,...]").StringVar(&c.annotations)
c.requestCreate = requests.Command("create", "Create pending access request.")
c.requestCreate = requests.Command("create", "Create pending Access Request.")
c.requestCreate.Arg("username", "Name of target user").Required().StringVar(&c.user)
c.requestCreate.Flag("roles", "Roles to be requested").StringVar(&c.roles)
c.requestCreate.Flag("resource", "Resource ID to be requested").StringsVar(&c.requestedResourceIDs)
c.requestCreate.Flag("reason", "Optional reason message").StringVar(&c.reason)
c.requestCreate.Flag("dry-run", "Don't actually generate the access request").BoolVar(&c.dryRun)
c.requestCreate.Flag("dry-run", "Don't actually generate the Access Request").BoolVar(&c.dryRun)
c.requestDelete = requests.Command("rm", "Delete an access request.")
c.requestDelete = requests.Command("rm", "Delete an Access Request.")
c.requestDelete.Arg("request-id", "ID of target request(s)").Required().StringVar(&c.reqIDs)
c.requestDelete.Flag("force", "Force the deletion of an active access request").Short('f').BoolVar(&c.force)
c.requestDelete.Flag("force", "Force the deletion of an active Access Request").Short('f').BoolVar(&c.force)
c.requestCaps = requests.Command("capabilities", "Check a user's access capabilities.").Alias("caps").Hidden()
c.requestCaps.Arg("username", "Name of target user").Required().StringVar(&c.user)
c.requestCaps.Flag("format", "Output format, 'text' or 'json'").Hidden().Default(teleport.Text).StringVar(&c.format)
c.requestReview = requests.Command("review", "Review an access request.")
c.requestReview = requests.Command("review", "Review an Access Request.")
c.requestReview.Arg("request-id", "ID of target request").Required().StringVar(&c.reqIDs)
c.requestReview.Flag("author", "Username of reviewer").Required().StringVar(&c.user)
c.requestReview.Flag("approve", "Review proposes approval").BoolVar(&c.approve)
+15 -15
View File
@@ -67,30 +67,30 @@ const (
// Initialize allows ACLCommand to plug itself into the CLI parser
func (c *ACLCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIFlags, _ *servicecfg.Config) {
acl := app.Command("acl", "Manage access lists.").Alias("access-lists")
acl := app.Command("acl", "Manage Access Lists.").Alias("access-lists")
c.ls = acl.Command("ls", "List cluster access lists.")
c.ls = acl.Command("ls", "List cluster Access Lists.")
c.ls.Flag("format", "Output format, 'yaml', 'json', or 'text'").Default(teleport.YAML).EnumVar(&c.format, teleport.YAML, teleport.JSON, teleport.Text)
c.get = acl.Command("get", "Get detailed information for an access list.")
c.get.Arg("access-list-name", "The access list name.").Required().StringVar(&c.accessListName)
c.get = acl.Command("get", "Get detailed information for an Access List.")
c.get.Arg("access-list-name", "The Access List name.").Required().StringVar(&c.accessListName)
c.get.Flag("format", "Output format, 'yaml', 'json', or 'text'").Default(teleport.YAML).EnumVar(&c.format, teleport.YAML, teleport.JSON, teleport.Text)
users := acl.Command("users", "Manage user membership to access lists.")
users := acl.Command("users", "Manage user membership to Access Lists.")
c.usersAdd = users.Command("add", "Add a user to an access list.")
c.usersAdd = users.Command("add", "Add a user to an Access List.")
c.usersAdd.Flag("kind", "Access list member kind, 'user' or 'list'").Default(memberKindUser).EnumVar(&c.memberKind, memberKindUser, memberKindList)
c.usersAdd.Arg("access-list-name", "The access list name.").Required().StringVar(&c.accessListName)
c.usersAdd.Arg("user", "The user to add to the access list.").Required().StringVar(&c.userName)
c.usersAdd.Arg("expires", "When the user's access expires (must be in RFC3339). Defaults to the expiration time of the access list.").StringVar(&c.expires)
c.usersAdd.Arg("reason", "The reason the user has been added to the access list. Defaults to empty.").StringVar(&c.reason)
c.usersAdd.Arg("access-list-name", "The Access List name.").Required().StringVar(&c.accessListName)
c.usersAdd.Arg("user", "The user to add to the Access List.").Required().StringVar(&c.userName)
c.usersAdd.Arg("expires", "When the user's access expires (must be in RFC3339). Defaults to the expiration time of the Access List.").StringVar(&c.expires)
c.usersAdd.Arg("reason", "The reason the user has been added to the Access List. Defaults to empty.").StringVar(&c.reason)
c.usersRemove = users.Command("rm", "Remove a user from an access list.")
c.usersRemove.Arg("access-list-name", "The access list name.").Required().StringVar(&c.accessListName)
c.usersRemove.Arg("user", "The user to remove from the access list.").Required().StringVar(&c.userName)
c.usersRemove = users.Command("rm", "Remove a user from an Access List.")
c.usersRemove.Arg("access-list-name", "The Access List name.").Required().StringVar(&c.accessListName)
c.usersRemove.Arg("user", "The user to remove from the Access List.").Required().StringVar(&c.userName)
c.usersList = users.Command("ls", "List users that are members of an access list.")
c.usersList.Arg("access-list-name", "The access list name.").Required().StringVar(&c.accessListName)
c.usersList = users.Command("ls", "List users that are members of an Access List.")
c.usersList.Arg("access-list-name", "The Access List name.").Required().StringVar(&c.accessListName)
c.usersList.Flag("format", "Output format 'json', or 'text'").Default(teleport.Text).EnumVar(&c.format, teleport.JSON, teleport.Text)
}
+1 -6
View File
@@ -65,12 +65,7 @@ func (e *EditCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIF
e.app = app
e.config = config
e.cmd = app.Command("edit", "Edit a Teleport resource.")
e.cmd.Arg("resource type/resource name", `Resource to update
<resource type> Type of a resource [for example: rc]
<resource name> Resource name to update
Example:
$ tctl edit rc/remote`).SetValue(&e.ref)
e.cmd.Arg("resource type/resource name", `Resource to update, e.g., "user/myuser"`).SetValue(&e.ref)
e.cmd.Flag("confirm", "Confirm an unsafe or temporary resource update").Hidden().BoolVar(&e.confirm)
}
+1 -1
View File
@@ -53,7 +53,7 @@ func (c *LockCommand) Initialize(app *kingpin.Application, _ *tctlcfg.GlobalCLIF
c.mainCmd.Flag("login", "Name of a local UNIX user to disable.").StringVar(&c.spec.Target.Login)
c.mainCmd.Flag("mfa-device", "UUID of a user MFA device to disable.").StringVar(&c.spec.Target.MFADevice)
c.mainCmd.Flag("windows-desktop", "Name of a Windows desktop to disable.").StringVar(&c.spec.Target.WindowsDesktop)
c.mainCmd.Flag("access-request", "UUID of an access request to disable.").StringVar(&c.spec.Target.AccessRequest)
c.mainCmd.Flag("access-request", "UUID of an Access Request to disable.").StringVar(&c.spec.Target.AccessRequest)
c.mainCmd.Flag("device", "UUID of a trusted device to disable.").StringVar(&c.spec.Target.Device)
c.mainCmd.Flag("message", "Message to display to locked-out users.").StringVar(&c.spec.Message)
c.mainCmd.Flag("expires", "Time point (RFC3339) when the lock expires.").StringVar(&c.expires)
+1 -1
View File
@@ -204,7 +204,7 @@ func (a *awsICInstallArgs) parseUserFilters() ([]*types.AWSICUserSyncFilter, err
func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) {
p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS IAM Identity Center integration.")
cmd := p.install.awsIC.cmd
cmd.Flag("access-list-default-owner", "Teleport user to set as default owner for the imported access lists. Multiple flags allowed.").
cmd.Flag("access-list-default-owner", "Teleport user to set as default owner for the imported Access Lists. Multiple flags allowed.").
Required().
StringsVar(&p.install.awsIC.defaultOwners)
cmd.Flag("scim-url", "AWS Identity Center SCIM provisioning endpoint").
+1 -1
View File
@@ -114,7 +114,7 @@ func (p *PluginsCommand) initInstallEntra(parent *kingpin.CmdClause) {
Flag("use-system-credentials", "Uses system credentials instead of OIDC.").
BoolVar(&p.install.entraID.useSystemCredentials)
cmd.Flag("default-owner", "List of Teleport users that are default owners for the imported access lists. Multiple flags allowed.").
cmd.Flag("default-owner", "List of Teleport users that are default owners for the imported Access Lists. Multiple flags allowed.").
Required().
StringsVar(&p.install.entraID.defaultOwners)