Merge pull request #1088 from BenTheElder/cli-fun

CLI fun
This commit is contained in:
Kubernetes Prow Robot
2019-11-10 20:11:42 -08:00
committed by GitHub
9 changed files with 87 additions and 21 deletions

View File

@@ -77,7 +77,7 @@ kind create cluster --image kindest/node:latest
Multi-node clusters and other advanced features may be configured with a config
file, for more usage see [the docs][user guide] or run `kind [command] --help`
## Community, discussion, contribution, and support
## Community
Please reach out for bugs, feature requests, and other issues!
The maintainers of this project are reachable via:
@@ -90,7 +90,10 @@ Current maintainers are [@BenTheElder] and [@munnerz] - feel free to
reach out if you have any questions!
Pull Requests are very welcome!
See the [issue tracker] if you're unsure where to start, or feel free to reach out to discuss.
If you're planning a new feature, please file an issue to discuss first.
Check the [issue tracker] for `help wanted` issues if you're unsure where to
start, or feel free to reach out to discuss. 🙂
See also: our own [contributor guide] and the Kubernetes [community page].

1
go.mod
View File

@@ -5,6 +5,7 @@ go 1.13
require (
github.com/alessio/shellescape v0.0.0-20190409004728-b115ca0f9053
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/mattn/go-colorable v0.1.4
github.com/mattn/go-isatty v0.0.10
github.com/pkg/errors v0.8.1
github.com/spf13/cobra v0.0.5

4
go.sum
View File

@@ -52,6 +52,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -98,6 +101,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII=

View File

@@ -107,3 +107,20 @@ func CreateWithStopBeforeSettingUpKubernetes(stopBeforeSettingUpKubernetes bool)
return nil
})
}
// CreateWithDisplayUsage enables displaying usage if displayUsage is true
func CreateWithDisplayUsage(displayUsage bool) CreateOption {
return createOptionAdapter(func(o *internalcreate.ClusterOptions) error {
o.DisplayUsage = displayUsage
return nil
})
}
// CreateWithDisplaySalutation enables display a salutation t the end of create
// cluster if displaySalutation is true
func CreateWithDisplaySalutation(displaySalutation bool) CreateOption {
return createOptionAdapter(func(o *internalcreate.ClusterOptions) error {
o.DisplaySalutation = displaySalutation
return nil
})
}

View File

@@ -90,6 +90,8 @@ func runE(logger log.Logger, streams cmd.IOStreams, flags *flagpole) error {
cluster.CreateWithRetain(flags.Retain),
cluster.CreateWithWaitForReady(flags.Wait),
cluster.CreateWithKubeconfigPath(flags.Kubeconfig),
cluster.CreateWithDisplayUsage(true),
cluster.CreateWithDisplaySalutation(true),
); err != nil {
if errs := errors.Errors(err); errs != nil {
for _, problem := range errs {

View File

@@ -18,6 +18,7 @@ package create
import (
"fmt"
"math/rand"
"regexp"
"time"
@@ -64,6 +65,9 @@ type ClusterOptions struct {
KubeconfigPath string
// see https://github.com/kubernetes-sigs/kind/issues/324
StopBeforeSettingUpKubernetes bool // if false kind should setup kubernetes after creating nodes
// Options to control output
DisplayUsage bool
DisplaySalutation bool
}
// Cluster creates a cluster
@@ -137,31 +141,49 @@ func Cluster(logger log.Logger, ctx *context.Context, opts *ClusterOptions) erro
}
}
// skip the rest if we're not setting up kubernetes
if opts.StopBeforeSettingUpKubernetes {
return nil
}
return exportKubeconfig(logger, ctx, opts.KubeconfigPath)
}
// exportKubeconfig exports the cluster's kubeconfig and prints usage
func exportKubeconfig(logger log.Logger, ctx *context.Context, kubeconfigPath string) error {
// actually export KUBECONFIG
if err := kubeconfig.Export(ctx, kubeconfigPath); err != nil {
if err := kubeconfig.Export(ctx, opts.KubeconfigPath); err != nil {
return err
}
// optionally display usage
if opts.DisplayUsage {
logUsage(logger, ctx, opts.KubeconfigPath)
}
// optionally give the user a friendly salutation
if opts.DisplaySalutation {
logger.V(0).Info("")
logSalutation(logger)
}
return nil
}
func logUsage(logger log.Logger, ctx *context.Context, explicitKubeconfigPath string) {
// construct a sample command for interacting with the cluster
kctx := kubeconfig.ContextForCluster(ctx.Name())
sampleCommand := fmt.Sprintf("kubectl cluster-info --context %s", kctx)
if kubeconfigPath != "" {
if explicitKubeconfigPath != "" {
// explicit path, include this
sampleCommand += " --kubeconfig " + shellescape.Quote(kubeconfigPath)
sampleCommand += " --kubeconfig " + shellescape.Quote(explicitKubeconfigPath)
}
logger.V(0).Infof(`Set kubectl context to "%s"`, kctx)
logger.V(0).Infof("You can now use your cluster with:\n\n" + sampleCommand)
return nil
}
func logSalutation(logger log.Logger) {
salutations := []string{
"Have a nice day! 👋",
"Thanks for using kind! 😊",
"Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/",
"Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂",
}
r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
s := salutations[r.Intn(len(salutations))]
logger.V(0).Info(s)
}
func fixupOptions(opts *ClusterOptions) error {

View File

@@ -19,8 +19,11 @@ package cli
import (
"fmt"
"io"
"os"
"sync"
"time"
colorable "github.com/mattn/go-colorable"
)
// custom CLI loading spinner for kind
@@ -60,7 +63,11 @@ type Spinner struct {
var _ io.Writer = &Spinner{}
// NewSpinner initializes and returns a new Spinner that will write to w
// NOTE: w should be os.Stderr or similar, and it should be a Terminal
func NewSpinner(w io.Writer) *Spinner {
if v, ok := w.(*os.File); ok {
w = colorable.NewColorable(v)
}
return &Spinner{
stop: make(chan struct{}, 1),
stopped: make(chan struct{}),
@@ -116,7 +123,7 @@ func (s *Spinner) Start() {
func() {
s.mu.Lock()
defer s.mu.Unlock()
fmt.Fprintf(s.writer, "\r%s%s%s", s.prefix, frame, s.suffix)
fmt.Fprintf(s.writer, "\x1b[?7l\x1b[2K\r%s%s%s\x1b[?7h", s.prefix, frame, s.suffix)
}()
}
}
@@ -149,7 +156,7 @@ func (s *Spinner) Write(p []byte) (n int, err error) {
return s.writer.Write(p)
}
// otherwise: we will rewrite the line first
if _, err := s.writer.Write([]byte("\r")); err != nil {
if _, err := s.writer.Write([]byte("\x1b[2K\r")); err != nil {
return 0, err
}
return s.writer.Write(p)

View File

@@ -28,6 +28,9 @@ type Status struct {
spinner *Spinner
status string
logger log.Logger
// for controlling coloring etc
successFormat string
failureFormat string
}
// StatusForLogger returns a new status object for the logger l,
@@ -35,13 +38,18 @@ type Status struct {
// will be used for the status
func StatusForLogger(l log.Logger) *Status {
s := &Status{
logger: l,
logger: l,
successFormat: " ✓ %s\n",
failureFormat: " ✗ %s\n",
}
// if we're using the CLI logger, check for if it has a spinner setup
// and wire the status to that
if v, ok := l.(*Logger); ok {
if v2, ok := v.writer.(*Spinner); ok {
s.spinner = v2
// use colored success / failure messages
s.successFormat = " \x1b[32m✓\x1b[0m %s\n"
s.failureFormat = " \x1b[31m✗\x1b[0m %s\n"
}
}
return s
@@ -72,11 +80,10 @@ func (s *Status) End(success bool) {
s.spinner.Stop()
fmt.Fprint(s.spinner.writer, "\r")
}
if success {
s.logger.V(0).Infof(" ✓ %s\n", s.status)
s.logger.V(0).Infof(s.successFormat, s.status)
} else {
s.logger.V(0).Infof(" ✗ %s\n", s.status)
s.logger.V(0).Infof(s.failureFormat, s.status)
}
s.status = ""

View File

@@ -54,7 +54,7 @@ kind create cluster --image kindest/node:latest
Multi-node clusters and other advanced features may be configured with a config
file, for more usage see [the user guide][user guide] or run `kind [command] --help`
## Community, discussion, contribution, and support
## Community
Please reach out for bugs, feature requests, and other issues!
The maintainers of this project are reachable via:
@@ -67,7 +67,10 @@ Current maintainers are [@BenTheElder] and [@munnerz] - feel free to
reach out if you have any questions!
Pull Requests are very welcome!
See the [issue tracker] if you're unsure where to start, or feel free to reach out to discuss.
If you're planning a new feature, please file an issue to discuss first.
Check the [issue tracker] for `help wanted` issues if you're unsure where to
start, or feel free to reach out to discuss. 🙂
See also: our own [contributor guide] and the Kubernetes [community page].