mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 00:33:50 +00:00
Updated "tctl tokens ..." command.
This commit is contained in:
parent
9886411293
commit
d98b74d2a6
34
lib/asciitable/example_test.go
Normal file
34
lib/asciitable/example_test.go
Normal 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())
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
13
roles.go
13
roles.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue