Skip to content

Commit

Permalink
Add JSON and YAML to several tsh commands (#11681)
Browse files Browse the repository at this point in the history
This change adds support for JSON and YAML output to several tsh commands via the --format flag.
  • Loading branch information
atburke authored Apr 19, 2022
1 parent 89473b0 commit 03956d7
Show file tree
Hide file tree
Showing 9 changed files with 1,227 additions and 97 deletions.
11 changes: 11 additions & 0 deletions lib/utils/jsontools.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ func FastMarshal(v interface{}) ([]byte, error) {
return data, nil
}

// FastMarshal uses the json-iterator library for fast JSON marshalling
// with indentation. Note, this function unmarshals floats with 6 digits precision.
func FastMarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
data, err := SafeConfig.MarshalIndent(v, prefix, indent)
if err != nil {
return nil, trace.Wrap(err)
}

return data, nil
}

const yamlDocDelimiter = "---"

// WriteYAML detects whether value is a list
Expand Down
59 changes: 52 additions & 7 deletions tool/tsh/access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@ limitations under the License.
package main

import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"time"

"github.com/ghodss/yaml"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"

"github.com/gravitational/trace"
)
Expand Down Expand Up @@ -100,23 +101,36 @@ func onRequestList(cf *CLIConf) error {
}
reqs = filtered
}
switch cf.Format {
case teleport.Text:

format := strings.ToLower(cf.Format)
switch format {
case teleport.Text, "":
if err := showRequestTable(reqs); err != nil {
return trace.Wrap(err)
}
case teleport.JSON:
ser, err := json.MarshalIndent(reqs, "", " ")
case teleport.JSON, teleport.YAML:
out, err := serializeAccessRequests(reqs, format)
if err != nil {
return trace.Wrap(err)
}
fmt.Printf("%s\n", ser)
fmt.Println(out)
default:
return trace.BadParameter("unsupported format %q", cf.Format)
}
return nil
}

func serializeAccessRequests(reqs []types.AccessRequest, format string) (string, error) {
var out []byte
var err error
if format == teleport.JSON {
out, err = utils.FastMarshalIndent(reqs, "", " ")
} else {
out, err = yaml.Marshal(reqs)
}
return string(out), trace.Wrap(err)
}

func onRequestShow(cf *CLIConf) error {
tc, err := makeClient(cf, false)
if err != nil {
Expand All @@ -136,6 +150,37 @@ func onRequestShow(cf *CLIConf) error {
return trace.Wrap(err)
}

format := strings.ToLower(cf.Format)
switch format {
case teleport.Text, "":
err = printRequest(req)
if err != nil {
return trace.Wrap(err)
}
case teleport.JSON, teleport.YAML:
out, err := serializeAccessRequest(req, format)
if err != nil {
return trace.Wrap(err)
}
fmt.Println(out)
default:
return trace.BadParameter("unsupported format %q", cf.Format)
}
return nil
}

func serializeAccessRequest(req types.AccessRequest, format string) (string, error) {
var out []byte
var err error
if format == teleport.JSON {
out, err = utils.FastMarshalIndent(req, "", " ")
} else {
out, err = yaml.Marshal(req)
}
return string(out), trace.Wrap(err)
}

func printRequest(req types.AccessRequest) error {
reason := "[none]"
if r := req.GetRequestReason(); r != "" {
reason = fmt.Sprintf("%q", r)
Expand All @@ -154,7 +199,7 @@ func onRequestShow(cf *CLIConf) error {
table.AddRow([]string{"Reviewers:", reviewers + " (suggested)"})
table.AddRow([]string{"Status:", req.GetState().String()})

_, err = table.AsBuffer().WriteTo(os.Stdout)
_, err := table.AsBuffer().WriteTo(os.Stdout)
if err != nil {
return trace.Wrap(err)
}
Expand Down
92 changes: 72 additions & 20 deletions tool/tsh/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import (
"strings"
"text/template"

"github.com/ghodss/yaml"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"

"github.com/gravitational/trace"
)
Expand Down Expand Up @@ -92,9 +95,13 @@ func onAppLogin(cf *CLIConf) error {
"awsCmd": "s3 ls",
})
}
curlCmd, err := formatAppConfig(tc, profile, app.GetName(), app.GetPublicAddr(), appFormatCURL, rootCluster)
if err != nil {
return trace.Wrap(err)
}
return appLoginTpl.Execute(os.Stdout, map[string]string{
"appName": app.GetName(),
"curlCmd": formatAppConfig(tc, profile, app.GetName(), app.GetPublicAddr(), appFormatCURL, rootCluster),
"curlCmd": curlCmd,
})
}

Expand Down Expand Up @@ -196,39 +203,80 @@ func onAppConfig(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
fmt.Print(formatAppConfig(tc, profile, app.Name, app.PublicAddr, cf.Format, ""))
conf, err := formatAppConfig(tc, profile, app.Name, app.PublicAddr, cf.Format, "")
if err != nil {
return trace.Wrap(err)
}
fmt.Print(conf)
return nil
}

func formatAppConfig(tc *client.TeleportClient, profile *client.ProfileStatus, appName, appPublicAddr, format, cluster string) string {
func formatAppConfig(tc *client.TeleportClient, profile *client.ProfileStatus, appName, appPublicAddr, format, cluster string) (string, error) {
var uri string
if port := tc.WebProxyPort(); port == teleport.StandardHTTPSPort {
uri = fmt.Sprintf("https://%v", appPublicAddr)
} else {
uri = fmt.Sprintf("https://%v:%v", appPublicAddr, port)
}
curlCmd := fmt.Sprintf(`curl \
--cacert %v \
--cert %v \
--key %v \
%v`,
profile.CACertPathForCluster(cluster),
profile.AppCertPath(appName),
profile.KeyPath(),
uri)
format = strings.ToLower(format)
switch format {
case appFormatURI:
return fmt.Sprintf("https://%v:%v", appPublicAddr, tc.WebProxyPort())
return uri, nil
case appFormatCA:
return profile.CACertPathForCluster(cluster)
return profile.CACertPathForCluster(cluster), nil
case appFormatCert:
return profile.AppCertPath(appName)
return profile.AppCertPath(appName), nil
case appFormatKey:
return profile.KeyPath()
return profile.KeyPath(), nil
case appFormatCURL:
return fmt.Sprintf(`curl \
--cacert %v \
--cert %v \
--key %v \
https://%v:%v`,
profile.CACertPathForCluster(cluster),
profile.AppCertPath(appName),
profile.KeyPath(),
appPublicAddr,
tc.WebProxyPort())
return curlCmd, nil
case appFormatJSON, appFormatYAML:
appConfig := &appConfigInfo{
appName, uri, profile.CACertPathForCluster(cluster),
profile.AppCertPath(appName), profile.KeyPath(), curlCmd,
}
out, err := serializeAppConfig(appConfig, format)
if err != nil {
return "", trace.Wrap(err)
}
return fmt.Sprintf("%s\n", out), nil
}
return fmt.Sprintf(`Name: %v
URI: https://%v:%v
URI: %v
CA: %v
Cert: %v
Key: %v
`, appName, appPublicAddr, tc.WebProxyPort(), profile.CACertPathForCluster(cluster),
profile.AppCertPath(appName), profile.KeyPath())
`, appName, uri, profile.CACertPathForCluster(cluster),
profile.AppCertPath(appName), profile.KeyPath()), nil
}

type appConfigInfo struct {
Name string `json:"name"`
URI string `json:"uri"`
CA string `json:"ca"`
Cert string `json:"cert"`
Key string `json:"key"`
Curl string `json:"curl"`
}

func serializeAppConfig(configInfo *appConfigInfo, format string) (string, error) {
var out []byte
var err error
if format == appFormatJSON {
out, err = utils.FastMarshalIndent(configInfo, "", " ")
} else {
out, err = yaml.Marshal(configInfo)
}
return string(out), trace.Wrap(err)
}

// pickActiveApp returns the app the current profile is logged into.
Expand Down Expand Up @@ -271,4 +319,8 @@ const (
appFormatKey = "key"
// appFormatCURL prints app curl command.
appFormatCURL = "curl"
// appFormatJSON prints app URI, CA cert path, cert path, key path, and curl command in JSON format.
appFormatJSON = "json"
// appFormatYAML prints app URI, CA cert path, cert path, key path, and curl command in YAML format.
appFormatYAML = "yaml"
)
75 changes: 70 additions & 5 deletions tool/tsh/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"sort"
"strings"

"github.com/ghodss/yaml"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
Expand Down Expand Up @@ -62,8 +63,7 @@ func onListDatabases(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
showDatabases(cf.SiteName, databases, activeDatabases, cf.Verbose)
return nil
return trace.Wrap(showDatabases(cf.SiteName, databases, activeDatabases, cf.Format, cf.Verbose))
}

// onDatabaseLogin implements "tsh db login" command.
Expand Down Expand Up @@ -215,12 +215,37 @@ func onDatabaseEnv(cf *CLIConf) error {
if err != nil {
return trace.Wrap(err)
}
for k, v := range env {
fmt.Printf("export %v=%v\n", k, v)

format := strings.ToLower(cf.Format)
switch format {
case dbFormatText, "":
for k, v := range env {
fmt.Printf("export %v=%v\n", k, v)
}
case dbFormatJSON, dbFormatYAML:
out, err := serializeDatabaseEnvironment(env, format)
if err != nil {
return trace.Wrap(err)
}
fmt.Println(out)
default:
return trace.BadParameter("unsupported format %q", cf.Format)
}

return nil
}

func serializeDatabaseEnvironment(env map[string]string, format string) (string, error) {
var out []byte
var err error
if format == dbFormatJSON {
out, err = utils.FastMarshalIndent(env, "", " ")
} else {
out, err = yaml.Marshal(env)
}
return string(out), trace.Wrap(err)
}

// onDatabaseConfig implements "tsh db config" command.
func onDatabaseConfig(cf *CLIConf) error {
tc, err := makeClient(cf, false)
Expand Down Expand Up @@ -253,13 +278,27 @@ func onDatabaseConfig(cf *CLIConf) error {
default:
return trace.BadParameter("unknown database protocol: %q", database)
}
switch cf.Format {

format := strings.ToLower(cf.Format)
switch format {
case dbFormatCommand:
cmd, err := newCmdBuilder(tc, profile, database, rootCluster).getConnectCommand()
if err != nil {
return trace.Wrap(err)
}
fmt.Println(cmd.Path, strings.Join(cmd.Args[1:], " "))
case dbFormatJSON, dbFormatYAML:
configInfo := &dbConfigInfo{
database.ServiceName, host, port, database.Username,
database.Database, profile.CACertPathForCluster(rootCluster),
profile.DatabaseCertPathForCluster(tc.SiteName, database.ServiceName),
profile.KeyPath(),
}
out, err := serializeDatabaseConfig(configInfo, format)
if err != nil {
return trace.Wrap(err)
}
fmt.Println(out)
default:
fmt.Printf(`Name: %v
Host: %v
Expand All @@ -277,6 +316,28 @@ Key: %v
return nil
}

type dbConfigInfo struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user,omitempty"`
Database string `json:"database,omitempty"`
CA string `json:"ca"`
Cert string `json:"cert"`
Key string `json:"key"`
}

func serializeDatabaseConfig(configInfo *dbConfigInfo, format string) (string, error) {
var out []byte
var err error
if format == dbFormatJSON {
out, err = utils.FastMarshalIndent(configInfo, "", " ")
} else {
out, err = yaml.Marshal(configInfo)
}
return string(out), trace.Wrap(err)
}

// maybeStartLocalProxy starts local TLS ALPN proxy if needed depending on the
// connection scenario and returns a list of options to use in the connect
// command.
Expand Down Expand Up @@ -642,4 +703,8 @@ const (
dbFormatText = "text"
// dbFormatCommand prints database connection command.
dbFormatCommand = "cmd"
// dbFormatJSON prints database info as JSON.
dbFormatJSON = "json"
// dbFormatYAML prints database info as YAML.
dbFormatYAML = "yaml"
)
Loading

0 comments on commit 03956d7

Please sign in to comment.