kubeadm token list: implement structured output

Used cli-runtime API to print bootstrap tokens in 5 formats:

 - TEXT (identical to the current output)
 - YAML
 - JSON
 - JSONPATH
 - Go template
This commit is contained in:
Ed Bartosh
2019-06-04 18:19:57 +03:00
parent 3765f21012
commit 766e2a45f8
2 changed files with 101 additions and 36 deletions

View File

@@ -19,6 +19,8 @@ go_library(
"//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library",
"//cmd/kubeadm/app/apis/output/scheme:go_default_library",
"//cmd/kubeadm/app/apis/output/v1alpha1:go_default_library",
"//cmd/kubeadm/app/cmd/alpha:go_default_library",
"//cmd/kubeadm/app/cmd/options:go_default_library",
"//cmd/kubeadm/app/cmd/phases:go_default_library",
@@ -41,12 +43,15 @@ go_library(
"//cmd/kubeadm/app/util/apiclient:go_default_library",
"//cmd/kubeadm/app/util/config:go_default_library",
"//cmd/kubeadm/app/util/kubeconfig:go_default_library",
"//cmd/kubeadm/app/util/output:go_default_library",
"//cmd/kubeadm/app/util/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/duration:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/version:go_default_library",
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
"//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library",

View File

@@ -31,7 +31,9 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/cli-runtime/pkg/genericclioptions"
clientset "k8s.io/client-go/kubernetes"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
bootstraputil "k8s.io/cluster-bootstrap/token/util"
@@ -39,6 +41,8 @@ import (
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation"
outputapischeme "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/scheme"
outputapiv1alpha1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha1"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
@@ -46,6 +50,7 @@ import (
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
"k8s.io/kubernetes/cmd/kubeadm/app/util/output"
)
// NewCmdToken returns cobra.Command for token management
@@ -142,6 +147,8 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command {
tokenCmd.AddCommand(createCmd)
tokenCmd.AddCommand(NewCmdTokenGenerate(out))
outputFlags := output.NewOutputFlags(&tokenTextPrintFlags{}).WithTypeSetter(outputapischeme.Scheme).WithDefaultOutput(output.TextOutput)
listCmd := &cobra.Command{
Use: "list",
Short: "List bootstrap tokens on the server",
@@ -155,9 +162,17 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command {
return err
}
return RunListTokens(out, errW, client)
printer, err := outputFlags.ToPrinter()
if err != nil {
return err
}
return RunListTokens(out, errW, client, printer)
},
}
outputFlags.AddFlags(listCmd)
tokenCmd.AddCommand(listCmd)
deleteCmd := &cobra.Command{
@@ -268,8 +283,71 @@ func RunGenerateToken(out io.Writer) error {
return nil
}
func formatBootstrapToken(obj *outputapiv1alpha1.BootstrapToken) string {
ttl := "<forever>"
expires := "<never>"
if obj.Expires != nil {
ttl = duration.ShortHumanDuration(obj.Expires.Sub(time.Now()))
expires = obj.Expires.Format(time.RFC3339)
}
ttl = fmt.Sprintf("%-9s", ttl)
usages := strings.Join(obj.Usages, ",")
if len(usages) == 0 {
usages = "<none>"
}
usages = fmt.Sprintf("%-22s", usages)
description := obj.Description
if len(description) == 0 {
description = "<none>"
}
description = fmt.Sprintf("%-56s", description)
groups := strings.Join(obj.Groups, ",")
if len(groups) == 0 {
groups = "<none>"
}
return fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\n", obj.Token, ttl, expires, usages, description, groups)
}
// tokenTextPrinter prints bootstrap token in a text form
type tokenTextPrinter struct {
output.TextPrinter
columns []string
headerIsPrinted bool
}
// PrintObj is an implementation of ResourcePrinter.PrintObj for plain text output
func (ttp *tokenTextPrinter) PrintObj(obj runtime.Object, writer io.Writer) error {
tabw := tabwriter.NewWriter(writer, 10, 4, 3, ' ', 0)
// Print header
if !ttp.headerIsPrinted {
fmt.Fprintln(tabw, strings.Join(ttp.columns, "\t"))
ttp.headerIsPrinted = true
}
// Print token
fmt.Fprint(tabw, formatBootstrapToken(obj.(*outputapiv1alpha1.BootstrapToken)))
return tabw.Flush()
}
// tokenTextPrintFlags provides flags necessary for printing bootstrap token in a text form.
type tokenTextPrintFlags struct{}
// ToPrinter returns kubeadm printer for the text output format
func (tpf *tokenTextPrintFlags) ToPrinter(outputFormat string) (output.Printer, error) {
if outputFormat == output.TextOutput {
return &tokenTextPrinter{columns: []string{"TOKEN", "TTL", "EXPIRES", "USAGES", "DESCRIPTION", "EXTRA GROUPS"}}, nil
}
return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: []string{output.TextOutput}}
}
// RunListTokens lists details on all existing bootstrap tokens on the server.
func RunListTokens(out io.Writer, errW io.Writer, client clientset.Interface) error {
func RunListTokens(out io.Writer, errW io.Writer, client clientset.Interface, printer output.Printer) error {
// First, build our selector for bootstrap tokens only
klog.V(1).Infoln("[token] preparing selector for bootstrap token")
tokenSelector := fields.SelectorFromSet(
@@ -284,16 +362,13 @@ func RunListTokens(out io.Writer, errW io.Writer, client clientset.Interface) er
FieldSelector: tokenSelector.String(),
}
klog.V(1).Infoln("[token] retrieving list of bootstrap tokens")
klog.V(1).Info("[token] retrieving list of bootstrap tokens")
secrets, err := client.CoreV1().Secrets(metav1.NamespaceSystem).List(listOptions)
if err != nil {
return errors.Wrap(err, "failed to list bootstrap tokens")
}
w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0)
fmt.Fprintln(w, "TOKEN\tTTL\tEXPIRES\tUSAGES\tDESCRIPTION\tEXTRA GROUPS")
for _, secret := range secrets.Items {
// Get the BootstrapToken struct representation from the Secret object
token, err := kubeadmapi.BootstrapTokenFromSecret(&secret)
if err != nil {
@@ -301,11 +376,22 @@ func RunListTokens(out io.Writer, errW io.Writer, client clientset.Interface) er
continue
}
// Get the human-friendly string representation for the token
humanFriendlyTokenOutput := humanReadableBootstrapToken(token)
fmt.Fprintln(w, humanFriendlyTokenOutput)
// Convert token into versioned output structure
outputToken := outputapiv1alpha1.BootstrapToken{
BootstrapToken: kubeadmapiv1beta2.BootstrapToken{
Token: &kubeadmapiv1beta2.BootstrapTokenString{ID: token.Token.ID, Secret: token.Token.Secret},
Description: token.Description,
TTL: token.TTL,
Expires: token.Expires,
Usages: token.Usages,
Groups: token.Groups,
},
}
if err := printer.PrintObj(&outputToken, out); err != nil {
return errors.Wrapf(err, "unable to print token %s", token.Token)
}
}
w.Flush()
return nil
}
@@ -335,32 +421,6 @@ func RunDeleteTokens(out io.Writer, client clientset.Interface, tokenIDsOrTokens
return nil
}
func humanReadableBootstrapToken(token *kubeadmapi.BootstrapToken) string {
description := token.Description
if len(description) == 0 {
description = "<none>"
}
ttl := "<forever>"
expires := "<never>"
if token.Expires != nil {
ttl = duration.ShortHumanDuration(token.Expires.Sub(time.Now()))
expires = token.Expires.Format(time.RFC3339)
}
usagesString := strings.Join(token.Usages, ",")
if len(usagesString) == 0 {
usagesString = "<none>"
}
groupsString := strings.Join(token.Groups, ",")
if len(groupsString) == 0 {
groupsString = "<none>"
}
return fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s", token.Token.String(), ttl, expires, usagesString, description, groupsString)
}
func getClientset(file string, dryRun bool) (clientset.Interface, error) {
if dryRun {
dryRunGetter, err := apiclient.NewClientBackedDryRunGetterFromKubeconfig(file)