Updated "tctl tokens ..." command.

This commit is contained in:
Russell Jones 2018-08-30 01:29:42 +00:00 committed by Russell Jones
parent 9886411293
commit d98b74d2a6
6 changed files with 204 additions and 132 deletions

View file

@ -0,0 +1,34 @@
/*
Copyright 2018 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package asciitable
import (
"fmt"
)
func ExampleMakeTable() {
// Create a table with three column headers.
t := MakeTable([]string{"Token", "Type", "Expiry Time (UTC)"})
// Add in multiple rows.
t.AddRow([]string{"b53bd9d3e04add33ac53edae1a2b3d4f", "auth", "30 Aug 18 23:31 UTC"})
t.AddRow([]string{"5ecde0ca17824454b21937109df2c2b5", "node", "30 Aug 18 23:31 UTC"})
t.AddRow([]string{"9333929146c08928a36466aea12df963", "trusted_cluster", "30 Aug 18 23:33 UTC"})
// Write the table to stdout.
fmt.Println(t.AsBuffer().String())
}

View file

@ -12,45 +12,33 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This module implements a simple ASCII table formatter for printing
tabular values into a text terminal.
Example usage:
func main() {
// building a table
t := MakeTable([]string{"Name", "Motto", "Age"})
t.AddRow([]string{"Joe Forrester", "Trains are much better than cars", "40"})
t.AddRow([]string{"Jesus", "Read the bible", "2018"})
// using the table:
t.WriteTo(os.Stdout)
}
*/
// Package asciitable implements a simple ASCII table formatter for printing
// tabular values into a text terminal.
package asciitable
import (
"bytes"
"fmt"
"strings"
"text/tabwriter"
)
// column represents a column in the table. Contains the maximum width of the
// column as well as the title.
type column struct {
width int
title string
}
type PrintOptions int
// Table holds tabular values in a rows and columns format.
type Table struct {
columns []column
rows [][]string
}
// MakeTable creates a new instance of the table with a given title
// MakeTable creates a new instance of the table with given column names.
func MakeTable(headers []string) Table {
t := MakeHeadlessTable(len(headers))
for i := range t.columns {
@ -60,8 +48,8 @@ func MakeTable(headers []string) Table {
return t
}
// MakeTable creates a new instance of the table without a title,
// but the number of columns must be set
// MakeTable creates a new instance of the table without any column names.
// The number of columns is required.
func MakeHeadlessTable(columnCount int) Table {
return Table{
columns: make([]column, columnCount),
@ -69,45 +57,7 @@ func MakeHeadlessTable(columnCount int) Table {
}
}
// Body returns the fully formatted table body as a buffer
func (t *Table) Body() *bytes.Buffer {
var (
padding string
buf bytes.Buffer
)
for _, row := range t.rows {
for columnIndex, cell := range row {
padding = strings.Repeat(" ", t.columns[columnIndex].width-len(cell)+1)
fmt.Fprintf(&buf, "%s%s", cell, padding)
}
fmt.Fprintln(&buf, "")
}
return &buf
}
// Header returns the fully formatted header as a buffer
func (t *Table) Header() *bytes.Buffer {
var (
buf bytes.Buffer
padding string
)
for i := range t.columns {
title := t.columns[i].title
padding = strings.Repeat(" ", t.columns[i].width-len(title)+1)
fmt.Fprintf(&buf, "%s%s", title, padding)
}
return &buf
}
// ColumnWidths returns the slice of ints that are the widths of each column
func (t *Table) ColumnWidths() []int {
retval := make([]int, len(t.columns))
for i := range t.columns {
retval[i] = t.columns[i].width
}
return retval
}
// AddRow adds a row of cells to the table.
func (t *Table) AddRow(row []string) {
limit := min(len(row), len(t.columns))
for i := 0; i < limit; i++ {
@ -117,26 +67,40 @@ func (t *Table) AddRow(row []string) {
t.rows = append(t.rows, row[:limit])
}
// WriteTo prints the table to the given writer
// AsBuffer returns a *bytes.Buffer with the printed output of the table.
func (t *Table) AsBuffer() *bytes.Buffer {
var buf bytes.Buffer
var buffer bytes.Buffer
// the hearder:
writer := tabwriter.NewWriter(&buffer, 5, 0, 1, ' ', 0)
template := strings.Repeat("%v\t", len(t.columns))
// Header and separator.
if !t.IsHeadless() {
fmt.Fprintf(&buf, "%s\n", t.Header().String())
// the separator:
for _, w := range t.ColumnWidths() {
fmt.Fprintf(&buf, "%s ", strings.Repeat("-", w))
var colh []interface{}
var cols []interface{}
for _, col := range t.columns {
colh = append(colh, col.title)
cols = append(cols, strings.Repeat("-", col.width))
}
buf.WriteString("\n")
fmt.Fprintf(writer, template+"\n", colh...)
fmt.Fprintf(writer, template+"\n", cols...)
}
// the body:
fmt.Fprintf(&buf, "%s", t.Body().String())
return &buf
// Body.
for _, row := range t.rows {
var rowi []interface{}
for _, cell := range row {
rowi = append(rowi, cell)
}
fmt.Fprintf(writer, template+"\n", rowi...)
}
writer.Flush()
return &buffer
}
// IsHeadless returns 'true' if none of the table title cells contains any text
// IsHeadless returns true if none of the table title cells contains any text.
func (t *Table) IsHeadless() bool {
total := 0
for i := range t.columns {

View file

@ -12,23 +12,23 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
package asciitable
*/
package asciitable
import (
"fmt"
"testing"
"gopkg.in/check.v1"
)
// bootstrap check
func TestAsciiTable(t *testing.T) { check.TestingT(t) }
type TableTestSuite struct {
}
var _ = fmt.Printf
var _ = check.Suite(&TableTestSuite{})
const fullTable = `Name Motto Age
@ -37,8 +37,8 @@ Joe Forrester Trains are much better than cars 40
Jesus Read the bible 2018
`
const headlessTable = `one two
1 2
const headlessTable = `one two
1 2
`
func (s *TableTestSuite) TestFullTable(c *check.C) {
@ -54,6 +54,6 @@ func (s *TableTestSuite) TestHeadlessTable(c *check.C) {
t.AddRow([]string{"one", "two", "three"})
t.AddRow([]string{"1", "2", "3"})
// the table shall have no header and also the 3rd column must be chopped off
// The table shall have no header and also the 3rd column must be chopped off.
c.Assert(t.AsBuffer().String(), check.Equals, headlessTable)
}

View file

@ -97,7 +97,7 @@ func (roles Roles) Include(role Role) bool {
func (roles Roles) StringSlice() []string {
s := make([]string, 0)
for _, r := range roles {
s = append(s, string(r))
s = append(s, r.String())
}
return s
}
@ -140,9 +140,16 @@ func (r *Role) Set(v string) error {
return nil
}
// String returns debug-friendly representation of this role
// String returns debug-friendly representation of this role.
func (r *Role) String() string {
return fmt.Sprintf("%v", string(*r))
switch string(*r) {
case string(RoleSignup):
return "User signup"
case string(RoleTrustedCluster), string(LegacyClusterTokenType):
return "trusted_cluster"
default:
return fmt.Sprintf("%v", string(*r))
}
}
// Check checks if this a a valid role value, returns nil

View file

@ -85,32 +85,14 @@ func (c *NodeCommand) TryRun(cmd string, client auth.ClientI) (match bool, err e
return true, trace.Wrap(err)
}
const trustedClusterTemplate = `kind: trusted_cluster
version: v2
metadata:
name: %v
spec:
enabled: true
token: %v
web_proxy_addr: proxy.example.com:3080
role_map:
- remote: admin
local: [admin]`
const trustedClusterMessage = `The cluster invite token: %v
This token will expire in %d minutes
const trustedClusterMessage = `Trusted cluster token: %v
Use this cluster in trusted cluster resource, for example:
%v
Please note:
- This token will expire in %d minutes.
- Replace address proxy.example.com:3080 with externally accessible teleport proxy address.
- Set proper local and remote role_map property.
Use this token when defining a trusted cluster resource on a remote cluster.
`
const nodeMessage = `The invite token: %v
This token will expire in %d minutes
Run this on the new node to join the cluster:
@ -119,7 +101,7 @@ Run this on the new node to join the cluster:
Please note:
- This invitation token will expire in %d minutes
- %v must be reachable from the new node, see --advertise-ip server flag
- %v must be reachable from the new node
`
// Invite generates a token which can be used to add another SSH node
@ -143,19 +125,20 @@ func (c *NodeCommand) Invite(client auth.ClientI) error {
return trace.Errorf("This cluster does not have any auth servers running.")
}
clusterName, err := client.GetClusterName()
if err != nil {
return trace.Wrap(err)
}
// output format swtich:
if c.format == "text" {
if roles.Include(teleport.RoleTrustedCluster) {
trustedCluster := fmt.Sprintf(trustedClusterTemplate, clusterName.GetClusterName(), token)
fmt.Printf(trustedClusterMessage, token, trustedCluster, int(c.ttl.Minutes()))
if roles.Include(teleport.RoleTrustedCluster) || roles.Include(teleport.LegacyClusterTokenType) {
fmt.Printf(trustedClusterMessage, token, int(c.ttl.Minutes()))
} else {
fmt.Printf(nodeMessage,
token, strings.ToLower(roles.String()), token, authServers[0].GetAddr(), int(c.ttl.Minutes()), authServers[0].GetAddr())
token,
int(c.ttl.Minutes()),
strings.ToLower(roles.String()),
token,
authServers[0].GetAddr(),
int(c.ttl.Minutes()),
authServers[0].GetAddr(),
)
}
} else {
// Always return a list, otherwise we'll break users tooling. See #1846 for

View file

@ -18,24 +18,44 @@ package common
import (
"fmt"
"sort"
"strings"
"time"
"github.com/gravitational/kingpin"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/service"
"github.com/gravitational/trace"
"github.com/gravitational/kingpin"
)
// TokenCommand implements `tctl token` group of commands
type TokenCommand struct {
config *service.Config
// token argument to 'tokens del' command
token string
// CLI clauses (subcommands)
// tokenType is the type of token. For example, "trusted_cluster".
tokenType string
// Value is the value of the token. Can be used to either act on a
// token (for example, delete a token) or used to create a token with a
// specific value.
value string
// ttl is how long the token will live for.
ttl time.Duration
// tokenAdd is used to add a token.
tokenAdd *kingpin.CmdClause
// tokenDel is used to delete a token.
tokenDel *kingpin.CmdClause
// tokenList is used to view all tokens that Teleport knows about.
tokenList *kingpin.CmdClause
tokenDel *kingpin.CmdClause
}
// Initialize allows TokenCommand to plug itself into the CLI parser
@ -43,38 +63,98 @@ func (c *TokenCommand) Initialize(app *kingpin.Application, config *service.Conf
c.config = config
tokens := app.Command("tokens", "List or revoke invitation tokens")
c.tokenList = tokens.Command("ls", "List node and user invitation tokens")
// tctl tokens add ..."
c.tokenAdd = tokens.Command("add", "Create a invitation token")
c.tokenAdd.Flag("type", "Type of token to add").Required().StringVar(&c.tokenType)
c.tokenAdd.Flag("value", "Value of token to add").StringVar(&c.value)
c.tokenAdd.Flag("ttl", fmt.Sprintf("Set expiration time for token, default is %v hour, maximum is %v hours",
int(defaults.SignupTokenTTL/time.Hour), int(defaults.MaxSignupTokenTTL/time.Hour))).
Default(fmt.Sprintf("%v", defaults.SignupTokenTTL)).DurationVar(&c.ttl)
// "tctl tokens rm ..."
c.tokenDel = tokens.Command("rm", "Delete/revoke an invitation token").Alias("del")
c.tokenDel.Arg("token", "Token to delete").StringVar(&c.token)
c.tokenDel.Arg("token", "Token to delete").StringVar(&c.value)
// "tctl tokens ls"
c.tokenList = tokens.Command("ls", "List node and user invitation tokens")
}
// 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) {
switch cmd {
case c.tokenList.FullCommand():
err = c.List(client)
case c.tokenAdd.FullCommand():
err = c.Add(client)
case c.tokenDel.FullCommand():
err = c.Del(client)
case c.tokenList.FullCommand():
err = c.List(client)
default:
return false, nil
}
return true, trace.Wrap(err)
}
// onTokenList is called to execute "tokens del" command
func (c *TokenCommand) Del(client auth.ClientI) error {
if c.token == "" {
return trace.Errorf("Need an argument: token")
}
if err := client.DeleteToken(c.token); err != nil {
// Add is called to execute "tokens add ..." command.
func (c *TokenCommand) Add(client auth.ClientI) error {
// Parse string to see if it's a type of role that Teleport supports.
roles, err := teleport.ParseRoles(c.tokenType)
if err != nil {
return trace.Wrap(err)
}
fmt.Printf("Token %s has been deleted\n", c.token)
// Generate token.
token, err := client.GenerateToken(auth.GenerateTokenRequest{
Roles: roles,
TTL: c.ttl,
Token: c.value,
})
if err != nil {
return trace.Wrap(err)
}
// Get list of auth servers. Used to print friendly signup message.
authServers, err := client.GetAuthServers()
if err != nil {
return trace.Wrap(err)
}
if len(authServers) == 0 {
return trace.Errorf("this cluster has no auth servers")
}
// Print signup message.
switch {
case roles.Include(teleport.RoleTrustedCluster), roles.Include(teleport.LegacyClusterTokenType):
fmt.Printf(trustedClusterMessage,
token,
int(c.ttl.Minutes()))
default:
fmt.Printf(nodeMessage,
token,
int(c.ttl.Minutes()),
strings.ToLower(roles.String()),
token,
authServers[0].GetAddr(),
int(c.ttl.Minutes()),
authServers[0].GetAddr())
}
return nil
}
// onTokenList is called to execute "tokens ls" command
// Del is called to execute "tokens del ..." command.
func (c *TokenCommand) Del(client auth.ClientI) error {
if c.value == "" {
return trace.Errorf("Need an argument: token")
}
if err := client.DeleteToken(c.value); err != nil {
return trace.Wrap(err)
}
fmt.Printf("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 {
tokens, err := client.GetTokens()
if err != nil {
@ -84,8 +164,12 @@ func (c *TokenCommand) List(client auth.ClientI) error {
fmt.Println("No active tokens found.")
return nil
}
// Sort by expire time.
sort.Slice(tokens, func(i, j int) bool { return tokens[i].Expires.Unix() < tokens[j].Expires.Unix() })
tokensView := func() string {
table := asciitable.MakeTable([]string{"Token", "Role", "Expiry Time (UTC)"})
table := asciitable.MakeTable([]string{"Token", "Type", "Expiry Time (UTC)"})
for _, t := range tokens {
expiry := "never"
if t.Expires.Unix() > 0 {