diff --git a/docs/pages/reference/cli/tsh.mdx b/docs/pages/reference/cli/tsh.mdx index 21f4e2e0739..688709ad3e7 100644 --- a/docs/pages/reference/cli/tsh.mdx +++ b/docs/pages/reference/cli/tsh.mdx @@ -9,7 +9,11 @@ tags: {/*vale messaging = NO*/} This guide provides a comprehensive list of commands, arguments, and flags for -tsh: Teleport Command Line Client. +tsh. + +tsh is a CLI client for accessing Teleport-protected resources. It allows users +to interact with current and past sessions on the cluster, copy files to and +from nodes, and list information about the cluster. ```code $ tsh [] [ ...] diff --git a/lib/utils/docenvdefaults/tsh.yaml b/lib/utils/docenvdefaults/tsh.yaml deleted file mode 100644 index b9f14831d75..00000000000 --- a/lib/utils/docenvdefaults/tsh.yaml +++ /dev/null @@ -1,69 +0,0 @@ -TELEPORT_AUTH: - description: "Any defined authentication connector, including `passwordless` and `local` (i.e., no authentication connector)" - default: "none" - type: "string" - -TELEPORT_CLUSTER: - description: "Name of a Teleport root or leaf cluster" - default: "none" - type: "string" - -TELEPORT_LOGIN: - description: "Login name to be used by default on the remote host" - default: "none" - type: "string" - -TELEPORT_LOGIN_BIND_ADDR: - description: "Address in the form of host:port to bind to for login command webhook" - default: "none" - type: "string" - -TELEPORT_PROXY: - description: "Address of the Teleport proxy server" - default: "none" - type: "string" - -TELEPORT_RELAY: - description: "Address of the Teleport relay server to use, \"none\" to disable the use of a relay, or \"default\" to use the default address specified by the control plane at login time. Defaults to port 443." - default: "none" - type: "string" - -TELEPORT_HEADLESS: - description: "Use headless authentication" - default: false - type: "bool" - -TELEPORT_HOME: - description: "Home location for tsh configuration and data" - default: "none" - type: "string" - -TELEPORT_USER: - description: "A Teleport user name" - default: "none" - type: "string" - -TELEPORT_ADD_KEYS_TO_AGENT: - description: "Specifies if the user certificate should be stored on the running SSH agent" - default: "auto" - type: "string" - -TELEPORT_USE_LOCAL_SSH_AGENT: - description: "Disable or enable local SSH agent integration" - default: "none" - type: "string" - -TELEPORT_GLOBAL_TSH_CONFIG: - description: "Override location of global `tsh` config file from default `/etc/tsh.yaml`" - default: "none" - type: "string" - -TELEPORT_MFA_MODE: - description: "Preferred mode for MFA and Passwordless assertions" - default: "auto" - type: "string" - -TELEPORT_IDENTITY_FILE: - description: "File path to identity file" - default: "none" - type: "string" diff --git a/lib/utils/docs-usage.md.tmpl b/lib/utils/docs-usage.md.tmpl index 4b2f0ff868b..24617f6d8b5 100644 --- a/lib/utils/docs-usage.md.tmpl +++ b/lib/utils/docs-usage.md.tmpl @@ -62,10 +62,9 @@ tags: {/*vale messaging = NO*/} This guide provides a comprehensive list of commands, arguments, and flags for -{{.App.Name}}: {{ if .App.Help -}} -{{- .App.Help|Wrap 0 }} -{{ end -}} +{{.App.Name}}. +{{ .App.Help|Wrap 0 }} {{template "FormatUsage" .App -}} {{if .Context.Flags|AnyVisibleFlags -}} Global flags: diff --git a/lib/utils/docsconfigs/tsh.yaml b/lib/utils/docsconfigs/tsh.yaml new file mode 100644 index 00000000000..ae1ce7411e7 --- /dev/null +++ b/lib/utils/docsconfigs/tsh.yaml @@ -0,0 +1,75 @@ +introduction: |- + tsh is a CLI client for accessing Teleport-protected resources. It allows + users to interact with current and past sessions on the cluster, copy files to + and from nodes, and list information about the cluster. + +env_vars: + TELEPORT_AUTH: + description: "Any defined authentication connector, including `passwordless` and `local` (i.e., no authentication connector)" + default: "none" + type: "string" + + TELEPORT_CLUSTER: + description: "Name of a Teleport root or leaf cluster" + default: "none" + type: "string" + + TELEPORT_LOGIN: + description: "Login name to be used by default on the remote host" + default: "none" + type: "string" + + TELEPORT_LOGIN_BIND_ADDR: + description: "Address in the form of host:port to bind to for login command webhook" + default: "none" + type: "string" + + TELEPORT_PROXY: + description: "Address of the Teleport proxy server" + default: "none" + type: "string" + + TELEPORT_RELAY: + description: "Address of the Teleport relay server to use, \"none\" to disable the use of a relay, or \"default\" to use the default address specified by the control plane at login time. Defaults to port 443." + default: "none" + type: "string" + + TELEPORT_HEADLESS: + description: "Use headless authentication" + default: false + type: "bool" + + TELEPORT_HOME: + description: "Home location for tsh configuration and data" + default: "none" + type: "string" + + TELEPORT_USER: + description: "A Teleport user name" + default: "none" + type: "string" + + TELEPORT_ADD_KEYS_TO_AGENT: + description: "Specifies if the user certificate should be stored on the running SSH agent" + default: "auto" + type: "string" + + TELEPORT_USE_LOCAL_SSH_AGENT: + description: "Disable or enable local SSH agent integration" + default: "none" + type: "string" + + TELEPORT_GLOBAL_TSH_CONFIG: + description: "Override location of global `tsh` config file from default `/etc/tsh.yaml`" + default: "none" + type: "string" + + TELEPORT_MFA_MODE: + description: "Preferred mode for MFA and Passwordless assertions" + default: "auto" + type: "string" + + TELEPORT_IDENTITY_FILE: + description: "File path to identity file" + default: "none" + type: "string" diff --git a/lib/utils/usage_docs.go b/lib/utils/usage_docs.go index e1457a60bde..491d6e87262 100644 --- a/lib/utils/usage_docs.go +++ b/lib/utils/usage_docs.go @@ -22,10 +22,8 @@ import ( "bytes" "cmp" _ "embed" - "errors" "fmt" "io" - "io/fs" "os" "path/filepath" "regexp" @@ -273,13 +271,45 @@ func formatHelp(help string) string { 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. -func updateAppUsageTemplate(r io.Reader, app *kingpin.Application) { +// 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() + } + } + + // 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, @@ -300,78 +330,55 @@ type envVarDefault struct { Type string `yaml:"type"` } -// loadDefaultEnvVars loads possible default environment variables defined in a YAML file +type generatorConfig struct { + Introduction string `yaml:"introduction"` + EnvVars map[string]envVarDefault `yaml:"env_vars"` +} + +// loadConfig loads possible default environment variables defined in a YAML file // that matches the application name. -func loadDefaultEnvVars(appName string) (map[string]envVarDefault, error) { - pathname := filepath.Join("lib", "utils", "docenvdefaults", appName+".yaml") - data, err := os.ReadFile(pathname) - envDefaults := make(map[string]envVarDefault) - if errors.Is(err, fs.ErrNotExist) { - fmt.Printf("No doc generation config file at %v. Skipping manual environment variable additions.", pathname) - return envDefaults, nil - } - +func loadConfig(appName string) (generatorConfig, error) { + pathname := filepath.Join("lib", "utils", "docsconfigs", appName+".yaml") + f, err := os.Open(pathname) if err != nil { - return nil, fmt.Errorf("unable to read CLI doc generation config file at %v: %w", pathname, err) + return generatorConfig{}, fmt.Errorf("unable to open CLI doc generation config file at %v: %w", pathname, err) } - if len(data) == 0 { - return nil, fmt.Errorf("read zero bytes from 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 err := yaml.Unmarshal(data, &envDefaults); err != nil { - return nil, fmt.Errorf("unable to parse YAML from %s: %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 envDefaults { + for envVar, def := range conf.EnvVars { if def.Description == "" || def.Default == "" || def.Type == "" { - return nil, fmt.Errorf("invalid YAML structure in %s: entry %q is missing one of required fields 'description', 'default' or 'type'", pathname, envVar) + return generatorConfig{}, fmt.Errorf("invalid YAML structure in %s: entry %q is missing one of required fields 'description', 'default' or 'type'", pathname, envVar) } } - return envDefaults, nil + return conf, nil } // UpdateAppUsageTemplate updates the app usage template to print a reference -// guide for the CLI application. Panics on errors since we need to keep the +// 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. +// 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, _ []string) { - defaultEnvVars, err := loadDefaultEnvVars(app.Name) + config, err := loadConfig(app.Name) if err != nil { - panic(err) - } - - existingEnvVars := make(map[string]struct{}) - for _, flag := range app.Model().Flags { - if flag.Envar != "" { - existingEnvVars[flag.Envar] = struct{}{} - } - } - - for envVarName, envVar := range defaultEnvVars { - // 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() - } + 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 { - panic(fmt.Sprintf("unable to open the docs usage template at %v: %v", docsUsageTemplatePath, err)) + fmt.Fprintf(os.Stdout, "Unable to open the docs usage template at %v: %v", docsUsageTemplatePath, err) + os.Exit(1) } - updateAppUsageTemplate(f, app) + updateAppUsageTemplate(f, config, app) } diff --git a/lib/utils/usage_docs_test.go b/lib/utils/usage_docs_test.go index b9a77696045..67bbd3f827f 100644 --- a/lib/utils/usage_docs_test.go +++ b/lib/utils/usage_docs_test.go @@ -33,6 +33,7 @@ func TestUpdateAppUsageTemplate(t *testing.T) { name string makeApp func() *kingpin.Application expectSubstring string // The @ character is replaced with a backtick + config generatorConfig }{ { name: "subcommand flags and global flags", @@ -46,13 +47,23 @@ func TestUpdateAppUsageTemplate(t *testing.T) { 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. +myapp. + +This is the main CLI tool. @@@code $ myapp [] [ ...] @@ -117,8 +128,13 @@ Arguments: 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. +myapp. + +This is the main CLI tool. @@@code $ myapp [] [ ...] @@ -129,8 +145,8 @@ Global flags: |Flag|Default|Description| |---|---|---| |@--config@|@config.yaml@|The location of the config file| -|@--verbosity@|@3@|Verbosity level.| |@--[no-]dry-run@|@false@|Whether to use dry-run mode| +|@--verbosity@|@3@|Verbosity level.| `, }, @@ -158,8 +174,8 @@ Flags: |Flag|Default|Description| |---|---|---| -|@--verbosity@|@3@|Verbosity level.| |@--[no-]dry-run@|@false@|Whether to use dry-run mode| +|@--verbosity@|@3@|Verbosity level.| `, }, @@ -187,8 +203,8 @@ Arguments: |Argument|Default|Description| |---|---|---| -|verbosity|@3@ (optional)|Verbosity level.| |dry-run|@false@ (optional)|Whether to use dry-run mode| +|verbosity|@3@ (optional)|Verbosity level.| `, }, @@ -399,8 +415,8 @@ Environment variables: |Variable|Default|Description| |---|---|---| -|@CREATE_TYPE@|none (optional)|The type of the resource| |@CREATE_NAME@|@myresource@|The name of the resource| +|@CREATE_TYPE@|none (optional)|The type of the resource| Flags: @@ -428,7 +444,7 @@ Arguments: docsUsageTemplatePath := "docs-usage.md.tmpl" f, err := os.Open(docsUsageTemplatePath) require.NoError(t, err) - updateAppUsageTemplate(f, app) + updateAppUsageTemplate(f, tt.config, app) // kingpin only adds a help command if there is at least // one subcommand. Make sure that all test cases