Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v9] Backport tctl token --format flag #12588

Merged
merged 2 commits into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/pages/setup/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ $ tctl tokens add --type=TYPE [<flags>]
| `--type` | none | `proxy`, `auth`, `trusted_cluster`, `node`, `db`, `kube`, `app`, `windowsdesktop` | Type of token to add |
| `--value` | none | **string** token value | Value of token to add |
| `--ttl` | 1h | relative duration like 5s, 2m, or 3h | Set expiration time for token |
| `--format` | none | `text`, `json`, `yaml` | Output format |

#### Global flags

Expand Down Expand Up @@ -1051,6 +1052,17 @@ List node and user invitation tokens:
$ tctl tokens ls [<flags>]
```

#### Flags

| Name | Default Value(s) | Allowed Value(s) | Description |
| - | - | - | - |
| `--format` | none | `text`, `json`, `yaml` | Output format |

#### Global flags

These flags are available for all commands `--debug, --config` . Run
`tctl help <subcommand>` or see the [Global Flags section](#tctl-global-flags).

#### Example

```code
Expand Down
2 changes: 1 addition & 1 deletion e
Submodule e updated from f6d4d4 to 47fc05
45 changes: 45 additions & 0 deletions tool/tctl/common/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,56 @@ func runResourceCommand(t *testing.T, fc *config.FileConfig, args []string, opts
return &stdoutBuff, nil
}

func runTokensCommand(t *testing.T, fc *config.FileConfig, args []string, opts ...optionsFunc) (*bytes.Buffer, error) {
var options options
for _, v := range opts {
v(&options)
}

var stdoutBuff bytes.Buffer
command := &TokensCommand{
stdout: &stdoutBuff,
}
cfg := service.MakeDefaultConfig()

app := utils.InitCLIParser("tctl", GlobalHelpString)
command.Initialize(app, cfg)

args = append([]string{"tokens"}, args...)
selectedCmd, err := app.Parse(args)
require.NoError(t, err)

var ccf GlobalCLIFlags
ccf.ConfigString = mustGetBase64EncFileConfig(t, fc)
ccf.Insecure = options.Insecure

clientConfig, err := applyConfig(&ccf, cfg)
require.NoError(t, err)

if options.CertPool != nil {
clientConfig.TLS.RootCAs = options.CertPool
}

client, err := authclient.Connect(context.Background(), clientConfig)
require.NoError(t, err)

_, err = command.TryRun(selectedCmd, client)
if err != nil {
return nil, err
}
return &stdoutBuff, nil
}

func mustDecodeJSON(t *testing.T, r io.Reader, i interface{}) {
err := json.NewDecoder(r).Decode(i)
require.NoError(t, err)
}

func mustDecodeYAML(t *testing.T, r io.Reader, i interface{}) {
err := yaml.NewDecoder(r).Decode(i)
require.NoError(t, err)
}

func mustGetFreeLocalListenerAddr(t *testing.T) string {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
Expand Down
98 changes: 76 additions & 22 deletions tool/tctl/common/token_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"
"time"

"github.com/ghodss/yaml"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
Expand All @@ -39,8 +41,8 @@ import (
"github.com/gravitational/trace"
)

// TokenCommand implements `tctl token` group of commands
type TokenCommand struct {
// TokensCommand implements `tctl tokens` group of commands
type TokensCommand struct {
config *service.Config

// format is the output format, e.g. text or json
Expand Down Expand Up @@ -81,14 +83,19 @@ type TokenCommand struct {

// tokenList is used to view all tokens that Teleport knows about.
tokenList *kingpin.CmdClause

// stdout allows to switch the standard output source. Used in tests.
stdout io.Writer
}

// Initialize allows TokenCommand to plug itself into the CLI parser
func (c *TokenCommand) Initialize(app *kingpin.Application, config *service.Config) {
func (c *TokensCommand) Initialize(app *kingpin.Application, config *service.Config) {
c.config = config

tokens := app.Command("tokens", "List or revoke invitation tokens")

formats := []string{teleport.Text, teleport.JSON, teleport.YAML}

// tctl tokens add ..."
c.tokenAdd = tokens.Command("add", "Create a invitation token")
c.tokenAdd.Flag("type", "Type(s) of token to add, e.g. --type=node,app,db").Required().StringVar(&c.tokenType)
Expand All @@ -103,18 +110,23 @@ func (c *TokenCommand) Initialize(app *kingpin.Application, config *service.Conf
c.tokenAdd.Flag("db-name", "Name of the database to add").StringVar(&c.dbName)
c.tokenAdd.Flag("db-protocol", fmt.Sprintf("Database protocol to use. Supported are: %v", defaults.DatabaseProtocols)).StringVar(&c.dbProtocol)
c.tokenAdd.Flag("db-uri", "Address the database is reachable at").StringVar(&c.dbURI)
c.tokenAdd.Flag("format", "Output format, 'text', 'json', or 'yaml'").EnumVar(&c.format, formats...)

// "tctl tokens rm ..."
c.tokenDel = tokens.Command("rm", "Delete/revoke an invitation token").Alias("del")
c.tokenDel.Arg("token", "Token to delete").StringVar(&c.value)

// "tctl tokens ls"
c.tokenList = tokens.Command("ls", "List node and user invitation tokens")
c.tokenList.Flag("format", "Output format, 'text' or 'json'").Hidden().Default(teleport.Text).StringVar(&c.format)
c.tokenList.Flag("format", "Output format, 'text', 'json' or 'yaml'").EnumVar(&c.format, formats...)

if c.stdout == nil {
c.stdout = os.Stdout
}
}

// TryRun takes the CLI command as an argument (like "nodes ls") and executes it.
func (c *TokenCommand) TryRun(cmd string, client auth.ClientI) (match bool, err error) {
func (c *TokensCommand) TryRun(cmd string, client auth.ClientI) (match bool, err error) {
switch cmd {
case c.tokenAdd.FullCommand():
err = c.Add(client)
Expand All @@ -129,7 +141,7 @@ func (c *TokenCommand) TryRun(cmd string, client auth.ClientI) (match bool, err
}

// Add is called to execute "tokens add ..." command.
func (c *TokenCommand) Add(client auth.ClientI) error {
func (c *TokensCommand) Add(client auth.ClientI) error {
// Parse string to see if it's a type of role that Teleport supports.
roles, err := types.ParseTeleportRoles(c.tokenType)
if err != nil {
Expand All @@ -155,6 +167,36 @@ func (c *TokenCommand) Add(client auth.ClientI) error {
return trace.Wrap(err)
}

// Print token information formatted with JSON, YAML, or just print the raw token.
switch c.format {
case teleport.JSON, teleport.YAML:
expires := time.Now().Add(c.ttl)
tokenInfo := map[string]interface{}{
"token": token,
"roles": roles,
"expires": expires,
}

var (
data []byte
err error
)
if c.format == teleport.JSON {
data, err = json.MarshalIndent(tokenInfo, "", " ")
} else {
data, err = yaml.Marshal(tokenInfo)
}
if err != nil {
return trace.Wrap(err)
}
fmt.Fprint(c.stdout, string(data))

return nil
case teleport.Text:
fmt.Fprintln(c.stdout, token)
return nil
}

// Calculate the CA pins for this cluster. The CA pins are used by the
// client to verify the identity of the Auth Server.
localCAResponse, err := client.GetClusterCACert()
Expand Down Expand Up @@ -187,7 +229,7 @@ func (c *TokenCommand) Add(client auth.ClientI) error {
}
appPublicAddr := fmt.Sprintf("%v.%v", c.appName, proxies[0].GetPublicAddr())

return appMessageTemplate.Execute(os.Stdout,
return appMessageTemplate.Execute(c.stdout,
map[string]interface{}{
"token": token,
"minutes": c.ttl.Minutes(),
Expand All @@ -205,7 +247,7 @@ func (c *TokenCommand) Add(client auth.ClientI) error {
if len(proxies) == 0 {
return trace.NotFound("cluster has no proxies")
}
return dbMessageTemplate.Execute(os.Stdout,
return dbMessageTemplate.Execute(c.stdout,
map[string]interface{}{
"token": token,
"minutes": c.ttl.Minutes(),
Expand All @@ -216,7 +258,7 @@ func (c *TokenCommand) Add(client auth.ClientI) error {
"db_uri": c.dbURI,
})
case roles.Include(types.RoleTrustedCluster):
fmt.Printf(trustedClusterMessage,
fmt.Fprintf(c.stdout, trustedClusterMessage,
token,
int(c.ttl.Minutes()))
default:
Expand All @@ -237,7 +279,8 @@ func (c *TokenCommand) Add(client auth.ClientI) error {
authServer = proxies[0].GetPublicAddr()
}
}
return nodeMessageTemplate.Execute(os.Stdout, map[string]interface{}{

return nodeMessageTemplate.Execute(c.stdout, map[string]interface{}{
"token": token,
"roles": strings.ToLower(roles.String()),
"minutes": int(c.ttl.Minutes()),
Expand All @@ -250,34 +293,51 @@ func (c *TokenCommand) Add(client auth.ClientI) error {
}

// Del is called to execute "tokens del ..." command.
func (c *TokenCommand) Del(client auth.ClientI) error {
func (c *TokensCommand) Del(client auth.ClientI) error {
ctx := context.TODO()
if c.value == "" {
return trace.Errorf("Need an argument: token")
}
if err := client.DeleteToken(ctx, c.value); err != nil {
return trace.Wrap(err)
}
fmt.Printf("Token %s has been deleted\n", c.value)
fmt.Fprintf(c.stdout, "Token %s has been deleted\n", c.value)
return nil
}

// List is called to execute "tokens ls" command.
func (c *TokenCommand) List(client auth.ClientI) error {
func (c *TokensCommand) List(client auth.ClientI) error {
ctx := context.TODO()
tokens, err := client.GetTokens(ctx)
if err != nil {
return trace.Wrap(err)
}
if len(tokens) == 0 {
fmt.Println("No active tokens found.")
fmt.Fprintln(c.stdout, "No active tokens found.")
return nil
}

// Sort by expire time.
sort.Slice(tokens, func(i, j int) bool { return tokens[i].Expiry().Unix() < tokens[j].Expiry().Unix() })

if c.format == teleport.Text {
switch c.format {
case teleport.JSON:
data, err := json.MarshalIndent(tokens, "", " ")
if err != nil {
return trace.Wrap(err, "failed to marshal tokens")
}
fmt.Fprint(c.stdout, string(data))
case teleport.YAML:
data, err := yaml.Marshal(tokens)
if err != nil {
return trace.Wrap(err, "failed to marshal tokens")
}
fmt.Fprint(c.stdout, string(data))
case teleport.Text:
for _, token := range tokens {
fmt.Fprintln(c.stdout, token.GetName())
}
default:
tokensView := func() string {
table := asciitable.MakeTable([]string{"Token", "Type", "Labels", "Expiry Time (UTC)"})
now := time.Now()
Expand All @@ -292,13 +352,7 @@ func (c *TokenCommand) List(client auth.ClientI) error {
}
return table.AsBuffer().String()
}
fmt.Print(tokensView())
} else {
data, err := json.MarshalIndent(tokens, "", " ")
if err != nil {
return trace.Wrap(err, "failed to marshal tokens")
}
fmt.Print(string(data))
fmt.Fprint(c.stdout, tokensView())
}
return nil
}
Loading