diff --git a/go.mod b/go.mod index 176863345..6e2828e31 100644 --- a/go.mod +++ b/go.mod @@ -9,14 +9,18 @@ require ( github.com/briandowns/spinner v1.23.0 github.com/cenkalti/backoff/v4 v4.3.0 github.com/fsnotify/fsnotify v1.7.0 + github.com/go-openapi/errors v0.20.3 github.com/go-openapi/runtime v0.24.1 github.com/go-openapi/strfmt v0.21.3 + github.com/go-openapi/swag v0.22.8 + github.com/go-openapi/validate v0.22.0 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/heimdalr/dag v1.5.0 github.com/interconnectedcloud/go-amqp v0.12.6-0.20200506124159-f51e540008b5 + github.com/mattn/go-isatty v0.0.20 github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 github.com/oapi-codegen/runtime v1.1.1 github.com/openshift/api v0.0.0-20210428205234-a8389931bee7 @@ -29,12 +33,12 @@ require ( golang.org/x/sys v0.28.0 golang.org/x/text v0.21.0 golang.org/x/time v0.5.0 - gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.1 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/client-go v0.31.0 k8s.io/code-generator v0.31.0 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/yaml v1.4.0 ) @@ -58,13 +62,10 @@ require ( github.com/getkin/kin-openapi v0.124.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/analysis v0.21.2 // indirect - github.com/go-openapi/errors v0.20.3 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/loads v0.21.1 // indirect github.com/go-openapi/spec v0.20.4 // indirect - github.com/go-openapi/swag v0.22.8 // indirect - github.com/go-openapi/validate v0.22.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect @@ -76,7 +77,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -101,10 +101,10 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/internal/cmd/skupper/debug/check/check.go b/internal/cmd/skupper/debug/check/check.go new file mode 100644 index 000000000..c356fe9db --- /dev/null +++ b/internal/cmd/skupper/debug/check/check.go @@ -0,0 +1,111 @@ +package check + +import ( + "github.com/skupperproject/skupper/api/types" + "github.com/skupperproject/skupper/internal/cmd/skupper/common/utils" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/cli" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/command" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/kube" + "github.com/skupperproject/skupper/internal/config" + "github.com/spf13/cobra" + "k8s.io/utils/ptr" +) + +type cmdCheck struct { + cmd *cobra.Command + platformCommands map[types.Platform][]*command.Check +} + +func NewCmdCheck() *cobra.Command { + checkCmd := cmdCheck{ + platformCommands: map[types.Platform][]*command.Check{}, + } + + checkCmd.cmd = &cobra.Command{ + Use: "check", + Short: "Run diagnostics", + Long: `Runs diagnostics to identify potential issues in the environment hosting Skupper`, + Example: `skupper debug check -p kubernetes`, + Run: func(cmd *cobra.Command, args []string) { + utils.HandleError(utils.GenericError, checkCmd.Run(cmd, args)) + }, + } + + checkCmd.registerCommand(types.PlatformKubernetes, ptr.To(kube.NewCmdCheckK8sAccess())) + checkCmd.registerCommand(types.PlatformKubernetes, ptr.To(kube.NewCmdCheckK8sVersion())) + for _, cmds := range checkCmd.platformCommands { + for i := range cmds { + subCommand := *cmds[i] + cmd := &cobra.Command{ + Use: subCommand.Name(), + Short: "check that " + subCommand.CheckDescription(), + Run: func(cmd *cobra.Command, args []string) { + status := cli.NewReporter() + defer status.End() + runCommandWithDeps(status, subCommand, map[string]bool{}, cmd) + }, + } + // TODO Adjust "skupper" to args[0] + cmd.Example = "skupper debug check " + subCommand.Name() + checkCmd.cmd.AddCommand(cmd) + } + } + + return checkCmd.cmd +} + +func (c *cmdCheck) registerCommand(platform types.Platform, cmd *command.Check) { + c.platformCommands[platform] = append(c.platformCommands[platform], cmd) +} + +func (c cmdCheck) Run(cmd *cobra.Command, args []string) error { + platform := config.GetPlatform() + + // Run all available sub-commands, in dependency order (falling back to declaration order) + // In the map of processed dependencies, true indicates that the command previously ran successfully, + // false that it failed previously + processedDependencies := make(map[string]bool) + + status := cli.NewReporter() + defer status.End() + + for i := range c.platformCommands[platform] { + subCommand := *c.platformCommands[platform][i] + if _, seen := processedDependencies[subCommand.Name()]; seen { + // The command has already been run as a dependency, skip it + continue + } + _ = runCommandWithDeps(status, subCommand, processedDependencies, cmd) + } + + // For UX consistency, errors are handled internally + return nil +} + +func runCommandWithDeps(status cli.Reporter, dc command.Check, processed map[string]bool, cmd *cobra.Command) error { + dependencies := dc.Dependencies() + for i := range dependencies { + dependency := *dependencies[i] + if succeeded, seen := processed[dependency.Name()]; seen { + if succeeded { + // The command previously succeeded, skip it but continue + continue + } + // The command previously failed, stop (assuming that the previous run reported the error) + return nil + } + if err := runCommandWithDeps(status, dependency, processed, cmd); err != nil { + return err + } + } + + status.Start("Checking that " + dc.CheckDescription()) + if err := dc.Run(status, cmd); err != nil { + processed[dc.Name()] = false + return err + } + + processed[dc.Name()] = true + status.Success("") + return nil +} diff --git a/internal/cmd/skupper/debug/check/cli/logger.go b/internal/cmd/skupper/debug/check/cli/logger.go new file mode 100644 index 000000000..2ccd8b261 --- /dev/null +++ b/internal/cmd/skupper/debug/check/cli/logger.go @@ -0,0 +1,259 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 cli + +import ( + "bytes" + "fmt" + "io" + "runtime" + "strings" + "sync" + "sync/atomic" + + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/env" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/log" +) + +// Logger is the kind cli's log.Logger implementation. +type Logger struct { + writer io.Writer + bufferPool *bufferPool + writerMu sync.Mutex + verbosity log.Level + // kind special additions + isSmartWriter bool +} + +var _ log.Logger = &Logger{} + +// NewLogger returns a new Logger with the given verbosity. +func NewLogger(writer io.Writer, verbosity log.Level) *Logger { + l := &Logger{ + verbosity: verbosity, + bufferPool: newBufferPool(), + } + l.SetWriter(writer) + + return l +} + +// SetWriter sets the output writer. +func (l *Logger) SetWriter(w io.Writer) { + l.writerMu.Lock() + defer l.writerMu.Unlock() + l.writer = w + _, isSpinner := w.(*Spinner) + l.isSmartWriter = isSpinner || env.IsSmartTerminal(w) +} + +// ColorEnabled returns true if the caller is OK to write colored output. +func (l *Logger) ColorEnabled() bool { + l.writerMu.Lock() + defer l.writerMu.Unlock() + + return l.isSmartWriter +} + +func (l *Logger) getVerbosity() log.Level { + return log.Level(atomic.LoadInt32((*int32)(&l.verbosity))) +} + +// SetVerbosity sets the loggers verbosity. +func (l *Logger) SetVerbosity(verbosity log.Level) { + atomic.StoreInt32((*int32)(&l.verbosity), int32(verbosity)) +} + +// synchronized write to the inner writer. +func (l *Logger) write(p []byte) (n int, err error) { + l.writerMu.Lock() + defer l.writerMu.Unlock() + + return l.writer.Write(p) //nolint:wrapcheck // No need to wrap here +} + +// writeBuffer writes buf with write, ensuring there is a trailing newline. +func (l *Logger) writeBuffer(buf *bytes.Buffer) { + // ensure trailing newline + if buf.Len() == 0 || buf.Bytes()[buf.Len()-1] != '\n' { + buf.WriteByte('\n') + } + + // TODO: should we handle this somehow?? + // Who logs for the logger? 🤔 + _, _ = l.write(buf.Bytes()) +} + +// print writes a simple string to the log writer. +func (l *Logger) print(message string) { + buf := bytes.NewBufferString(message) + l.writeBuffer(buf) +} + +// printf is roughly fmt.Fprintf against the log writer. +func (l *Logger) printf(format string, args ...interface{}) { + buf := l.bufferPool.Get() + fmt.Fprintf(buf, format, args...) + l.writeBuffer(buf) + l.bufferPool.Put(buf) +} + +// addDebugHeader inserts the debug line header to buf. +func addDebugHeader(buf *bytes.Buffer) { + _, file, line, ok := runtime.Caller(3) + // lifted from klog + if !ok { + file = "???" + line = 1 + } else if slash := strings.LastIndex(file, "/"); slash >= 0 { + path := file + file = path[slash+1:] + + if dirsep := strings.LastIndex(path[:slash], "/"); dirsep >= 0 { + file = path[dirsep+1:] + } + } + + buf.Grow(len(file) + 11) // we know at least this many bytes are needed. + buf.WriteString("DEBUG: ") + buf.WriteString(file) + buf.WriteByte(':') + fmt.Fprintf(buf, "%d", line) + buf.WriteByte(']') + buf.WriteByte(' ') +} + +// debug is like print but with a debug log header. +func (l *Logger) debug(message string) { + buf := l.bufferPool.Get() + addDebugHeader(buf) + buf.WriteString(message) + l.writeBuffer(buf) + l.bufferPool.Put(buf) +} + +// debugf is like printf but with a debug log header. +func (l *Logger) debugf(format string, args ...interface{}) { + buf := l.bufferPool.Get() + addDebugHeader(buf) + fmt.Fprintf(buf, format, args...) + l.writeBuffer(buf) + l.bufferPool.Put(buf) +} + +// Warn is part of the log.Logger interface. +func (l *Logger) Warn(message string) { + l.print(message) +} + +// Warnf is part of the log.Logger interface. +func (l *Logger) Warnf(format string, args ...interface{}) { + l.printf(format, args...) +} + +// Error is part of the log.Logger interface. +func (l *Logger) Error(message string) { + l.print(message) +} + +// Errorf is part of the log.Logger interface. +func (l *Logger) Errorf(format string, args ...interface{}) { + l.printf(format, args...) +} + +// V is part of the log.Logger interface. +func (l *Logger) V(level log.Level) log.InfoLogger { + return infoLogger{ + logger: l, + level: level, + enabled: level <= l.getVerbosity(), + } +} + +// infoLogger implements log.InfoLogger for Logger. +type infoLogger struct { + logger *Logger + level log.Level + enabled bool +} + +// Enabled is part of the log.InfoLogger interface. +func (i infoLogger) Enabled() bool { + return i.enabled +} + +// Info is part of the log.InfoLogger interface. +func (i infoLogger) Info(message string) { + if !i.enabled { + return + } + // for > 0, we are writing debug messages, include extra info + if i.level > 0 { + i.logger.debug(message) + } else { + i.logger.print(message) + } +} + +// Infof is part of the log.InfoLogger interface. +func (i infoLogger) Infof(format string, args ...interface{}) { + if !i.enabled { + return + } + // for > 0, we are writing debug messages, include extra info + if i.level > 0 { + i.logger.debugf(format, args...) + } else { + i.logger.printf(format, args...) + } +} + +// bufferPool is a type safe sync.Pool of *byte.Buffer, guaranteed to be Reset. +type bufferPool struct { + sync.Pool +} + +// newBufferPool returns a new bufferPool. +func newBufferPool() *bufferPool { + return &bufferPool{ + sync.Pool{ + New: func() interface{} { + // The Pool's New function should generally only return pointer + // types, since a pointer can be put into the return interface + // value without an allocation: + return new(bytes.Buffer) + }, + }, + } +} + +// Get obtains a buffer from the pool. +func (b *bufferPool) Get() *bytes.Buffer { + return b.Pool.Get().(*bytes.Buffer) +} + +// Put returns a buffer to the pool, resetting it first. +func (b *bufferPool) Put(x *bytes.Buffer) { + // only store small buffers to avoid pointless allocation + // avoid keeping arbitrarily large buffers + if x.Len() > 256 { + return + } + + x.Reset() + b.Pool.Put(x) +} diff --git a/internal/cmd/skupper/debug/check/cli/spinner.go b/internal/cmd/skupper/debug/check/cli/spinner.go new file mode 100644 index 000000000..9dd2919a5 --- /dev/null +++ b/internal/cmd/skupper/debug/check/cli/spinner.go @@ -0,0 +1,171 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 cli + +import ( + "fmt" + "io" + "runtime" + "sync" + "time" +) + +// custom CLI loading spinner for kind. +var spinnerFrames = []string{ + "⠈⠁", + "⠈⠑", + "⠈⠱", + "⠈⡱", + "⢀⡱", + "⢄⡱", + "⢄⡱", + "⢆⡱", + "⢎⡱", + "⢎⡰", + "⢎⡠", + "⢎⡀", + "⢎⠁", + "⠎⠁", + "⠊⠁", +} + +// Spinner is a simple and efficient CLI loading spinner used by kind +// It is simplistic and assumes that the line length will not change. +type Spinner struct { + stop chan struct{} // signals writer goroutine to stop from Stop() + stopped chan struct{} // signals Stop() that the writer goroutine stopped + mu *sync.Mutex // protects the mutable bits + // below are protected by mu + running bool + writer io.Writer + ticker *time.Ticker // signals that it is time to write a frame + prefix string + suffix string + // format string used to write a frame, depends on the host OS / terminal + frameFormat string +} + +// spinner implements writer. +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 { + frameFormat := "\x1b[?7l\r%s%s%s\x1b[?7h" + // toggling wrapping seems to behave poorly on windows + // in general only the simplest escape codes behave well at the moment, + // and only in newer shells + if runtime.GOOS == "windows" { + frameFormat = "\r%s%s%s" + } + + return &Spinner{ + stop: make(chan struct{}, 1), + stopped: make(chan struct{}), + mu: &sync.Mutex{}, + writer: w, + frameFormat: frameFormat, + } +} + +// SetPrefix sets the prefix to print before the spinner. +func (s *Spinner) SetPrefix(prefix string) { + s.mu.Lock() + defer s.mu.Unlock() + s.prefix = prefix +} + +// SetSuffix sets the suffix to print after the spinner. +func (s *Spinner) SetSuffix(suffix string) { + s.mu.Lock() + defer s.mu.Unlock() + s.suffix = suffix +} + +// Start starts the spinner running. +func (s *Spinner) Start() { + s.mu.Lock() + defer s.mu.Unlock() + // don't start if we've already started. + if s.running { + return + } + // flag that we've started. + s.running = true + // start / create a frame ticker. + s.ticker = time.NewTicker(time.Millisecond * 100) + // spin in the background. + go func() { + // write frames forever (until signaled to stop). + for { + for _, frame := range spinnerFrames { + select { + // prefer stopping, select this signal first. + case <-s.stop: + func() { + s.mu.Lock() + defer s.mu.Unlock() + s.ticker.Stop() // free up the ticker + s.running = false // mark as stopped (it's fine to start now) + s.stopped <- struct{}{} // tell Stop() that we're done + }() + + return // ... and stop + // otherwise continue and write one frame. + case <-s.ticker.C: + func() { + s.mu.Lock() + defer s.mu.Unlock() + fmt.Fprintf(s.writer, s.frameFormat, s.prefix, frame, s.suffix) + }() + } + } + } + }() +} + +// Stop signals the spinner to stop. +func (s *Spinner) Stop() { + s.mu.Lock() + if !s.running { + s.mu.Unlock() + return + } + // try to stop, do nothing if channel is full (IE already busy stopping). + s.stop <- struct{}{} + s.mu.Unlock() + // wait for stop to be finished. + <-s.stopped +} + +// Write implements io.Writer, interrupting the spinner and writing to +// the inner writer. +func (s *Spinner) Write(p []byte) (n int, err error) { + // lock first, so nothing else can start writing until we are done. + s.mu.Lock() + defer s.mu.Unlock() + // it the spinner is not running, just write directly. + if !s.running { + return s.writer.Write(p) //nolint:wrapcheck // No need to wrap here + } + // otherwise: we will rewrite the line first. + if _, err := s.writer.Write([]byte("\r")); err != nil { + return 0, err //nolint:wrapcheck // No need to wrap here + } + + return s.writer.Write(p) //nolint:wrapcheck // No need to wrap here +} diff --git a/internal/cmd/skupper/debug/check/cli/status.go b/internal/cmd/skupper/debug/check/cli/status.go new file mode 100644 index 000000000..42e2b69ee --- /dev/null +++ b/internal/cmd/skupper/debug/check/cli/status.go @@ -0,0 +1,256 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 cli + +import ( + "fmt" + "io" + "os" + "unicode" + + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/env" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/log" +) + +type Reporter interface { + // Start reports that an operation or sequence of operations is starting; + // any operation in progress is ended. + Start(message string, args ...interface{}) + + // Success reports that the last operation succeeded with the specified message. + Success(message string, args ...interface{}) + + // Failure reports that the last operation failed with the specified message. + Failure(message string, args ...interface{}) + + // End the current operation that was previously initiated via Start. + End() + + // Warning reports a warning message for the last operation. + Warning(message string, args ...interface{}) + + // Error wraps err with the supplied message, reports it as a failure, ends the current operation, and returns the error. + // If err is nil, does nothing and returns nil. + Error(err error, message string, args ...interface{}) error +} + +type resultType int + +const ( + success resultType = iota + failure + warning +) + +type ( + successType string + warningType string + failureType string +) + +// status is used to track ongoing status in a CLI, with a nice loading spinner +// when attached to a terminal. +type status struct { + spinner *Spinner + status string + logger log.Logger + // for controlling coloring etc. + successFormat string + failureFormat string + warningFormat string + // message queue + messageQueue []interface{} +} + +func NewReporter() Reporter { + var writer io.Writer = os.Stderr + if env.IsSmartTerminal(writer) { + writer = NewSpinner(writer) + } + + s := &status{ + logger: NewLogger(writer, 0), + successFormat: " ✓ %s\n", + failureFormat: " ✗ %s\n", + warningFormat: " ⚠ %s\n", + messageQueue: []interface{}{}, + } + + // if we're using the CLI logger, check for if it has a spinner setup + // and wire the status to that. + if l, ok := s.logger.(*Logger); ok { + if w, ok := l.writer.(*Spinner); ok { + s.spinner = w + // use colored success / failure / warning messages. + s.successFormat = " \x1b[32m✓\x1b[0m %s\n" + s.failureFormat = " \x1b[31m✗\x1b[0m %s\n" + s.warningFormat = " \x1b[33m⚠\x1b[0m %s\n" + } + } + + return s +} + +func (s *status) hasFailureMessages() bool { + for _, message := range s.messageQueue { + if _, ok := message.(failureType); ok { + return true + } + } + + return false +} + +func (s *status) hasWarningMessages() bool { + for _, message := range s.messageQueue { + if _, ok := message.(warningType); ok { + return true + } + } + + return false +} + +func (s *status) resultFromMessages() resultType { + if s.hasFailureMessages() { + return failure + } + + if s.hasWarningMessages() { + return warning + } + + return success +} + +// Start starts a new phase of the status, if attached to a terminal +// there will be a loading spinner with this status. +func (s *status) Start(message string, args ...interface{}) { + s.End() + s.status = fmt.Sprintf(message, args...) + + if s.spinner != nil { + s.spinner.SetSuffix(fmt.Sprintf(" %s ", s.status)) + s.spinner.Start() + } else { + s.logger.V(0).Infof(" • %s ...\n", s.status) + } +} + +// Failure queues up a message, which will be displayed once +// the status ends (using the failure format). +func (s *status) Failure(message string, a ...interface{}) { + if message == "" { + return + } + + if s.status != "" { + s.messageQueue = append(s.messageQueue, failureType(fmt.Sprintf(message, a...))) + } else { + s.logger.V(0).Infof(s.failureFormat, fmt.Sprintf(message, a...)) + } +} + +// Success queues up a message, which will be displayed once +// the status ends (using the warning format). +// An empty message will result in the start message being confirmed. +func (s *status) Success(message string, a ...interface{}) { + if message == "" { + return + } + + if s.status != "" { + s.messageQueue = append(s.messageQueue, successType(fmt.Sprintf(message, a...))) + } else { + s.logger.V(0).Infof(s.successFormat, fmt.Sprintf(message, a...)) + } +} + +// Warning queues up a message, which will be displayed once +// the status ends (using the warning format). +func (s *status) Warning(message string, a ...interface{}) { + if message == "" { + return + } + + if s.status != "" { + s.messageQueue = append(s.messageQueue, warningType(fmt.Sprintf(message, a...))) + } else { + s.logger.V(0).Infof(s.warningFormat, fmt.Sprintf(message, a...)) + } +} + +func (s *status) Error(err error, message string, args ...interface{}) error { + if err == nil { + return nil + } + + if message != "" { + err = fmt.Errorf("%s: %w", fmt.Sprintf(message, args...), err) + } + + capitalizeFirst := func(str string) string { + for i, v := range str { + return string(unicode.ToUpper(v)) + str[i+1:] + } + + return "" + } + + s.Failure(capitalizeFirst(err.Error())) + s.End() + + return err +} + +// End completes the current status, ending any previous spinning and +// marking the status as success or failure. +func (s *status) End() { + if s.status == "" { + return + } + + if s.spinner != nil { + s.spinner.Stop() + fmt.Fprint(s.spinner.writer, "\r") + } + + result := s.resultFromMessages() + + switch result { + case success: + s.logger.V(0).Infof(s.successFormat, s.status) + case failure: + s.logger.V(0).Infof(s.failureFormat, s.status) + case warning: + s.logger.V(0).Infof(s.warningFormat, s.status) + } + + for _, message := range s.messageQueue { + switch m := message.(type) { + case successType: + s.logger.V(0).Infof(s.successFormat, m) + case failureType: + s.logger.V(0).Infof(s.failureFormat, m) + case warningType: + s.logger.V(0).Infof(s.warningFormat, m) + } + } + + s.status = "" + s.messageQueue = []interface{}{} +} diff --git a/internal/cmd/skupper/debug/check/command/diagnose.go b/internal/cmd/skupper/debug/check/command/diagnose.go new file mode 100644 index 000000000..d2aca96cb --- /dev/null +++ b/internal/cmd/skupper/debug/check/command/diagnose.go @@ -0,0 +1,41 @@ +package command + +import ( + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/cli" + "github.com/spf13/cobra" +) + +type Check interface { + Name() string + CheckDescription() string + Example() string + Run(cli.Reporter, *cobra.Command) error + Dependencies() []*Check +} + +type BaseCheck struct { + name string + checkDescription string + example string + dependencies []*Check +} + +func NewBaseCheckCommand(name, checkDescription string, dependencies ...*Check) BaseCheck { + return BaseCheck{name: name, checkDescription: checkDescription, dependencies: dependencies} +} + +func (bd *BaseCheck) Name() string { + return bd.name +} + +func (bd *BaseCheck) CheckDescription() string { + return bd.checkDescription +} + +func (bd *BaseCheck) Example() string { + return bd.example +} + +func (bd *BaseCheck) Dependencies() []*Check { + return bd.dependencies +} diff --git a/internal/cmd/skupper/debug/check/env/term.go b/internal/cmd/skupper/debug/check/env/term.go new file mode 100644 index 000000000..67c794423 --- /dev/null +++ b/internal/cmd/skupper/debug/check/env/term.go @@ -0,0 +1,54 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 env + +import ( + "io" + "os" + "runtime" + + isatty "github.com/mattn/go-isatty" +) + +// IsTerminal returns true if the writer w is a terminal. +func IsTerminal(w io.Writer) bool { + if v, ok := w.(*os.File); ok { + return isatty.IsTerminal(v.Fd()) + } + + return false +} + +// IsSmartTerminal returns true if the writer w is a terminal AND +// we think that the terminal is smart enough to use VT escape codes etc. +func IsSmartTerminal(w io.Writer) bool { + if !IsTerminal(w) { + return false + } + + // explicitly dumb terminals are not smart. + if os.Getenv("TERM") == "dumb" { + return false + } + // On Windows WT_SESSION is set by the modern terminal component. + // Older terminals have poor support for UTF-8, VT escape codes, etc. + if runtime.GOOS == "windows" && os.Getenv("WT_SESSION") == "" { + return false + } + + return true +} diff --git a/internal/cmd/skupper/debug/check/kube/doc.go b/internal/cmd/skupper/debug/check/kube/doc.go new file mode 100644 index 000000000..646c219eb --- /dev/null +++ b/internal/cmd/skupper/debug/check/kube/doc.go @@ -0,0 +1,15 @@ +/* +Package kube provides Kubernetes-specific diagnostics. + +Each diagnostic is implemented in a function which is given a KubeClient and a status reporter. +The status reporter should be used to report warnings only; +outright errors and success are indicated respectively by returning an error and returning nil, +and are handled by the caller. +Each diagnostic is described by an instance of KubeCheck, constructed using newKubeCheckCommand(). +The name should be suitable as a Cobra command name, and the check description should work in a sentence of the form +"Checks that …". The check description is used to build command-line help and to provide status messages. +Diagnostics can have dependencies, which are other diagnostics. +These should be strong dependencies: when a given diagnostic is invoked, its dependencies will be invoked first, +and if any of them return an error, the entire chain will be aborted. +*/ +package kube diff --git a/internal/cmd/skupper/debug/check/kube/kube_access.go b/internal/cmd/skupper/debug/check/kube/kube_access.go new file mode 100644 index 000000000..4c35d77e6 --- /dev/null +++ b/internal/cmd/skupper/debug/check/kube/kube_access.go @@ -0,0 +1,27 @@ +package kube + +import ( + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/cli" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/command" + "github.com/skupperproject/skupper/internal/kube/client" +) + +var checkK8sAccess = newKubeCheckCommand( + "kube-access", + "the Kubernetes API server is accessible", + kubeAccessRun, +) + +func NewCmdCheckK8sAccess() command.Check { + return checkK8sAccess +} + +func kubeAccessRun(status cli.Reporter, kubeClient *client.KubeClient) error { + // We use this as a proxy for access to the Kubernetes API + _, err := kubeClient.Kube.Discovery().ServerVersion() + if err != nil { + return status.Error(err, "The Kubernetes API server is not accessible") + } + + return nil +} diff --git a/internal/cmd/skupper/debug/check/kube/kube_support.go b/internal/cmd/skupper/debug/check/kube/kube_support.go new file mode 100644 index 000000000..d721ac45c --- /dev/null +++ b/internal/cmd/skupper/debug/check/kube/kube_support.go @@ -0,0 +1,34 @@ +package kube + +import ( + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/cli" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/command" + "github.com/skupperproject/skupper/internal/kube/client" + "github.com/spf13/cobra" +) + +type KubeCheck struct { + command.BaseCheck + diagnostic func(cli.Reporter, *client.KubeClient) error +} + +func newKubeCheckCommand( + name, + shortDescription string, + cmd func(cli.Reporter, *client.KubeClient) error, + dependencies ...*command.Check, +) command.Check { + return &KubeCheck{ + BaseCheck: command.NewBaseCheckCommand(name, shortDescription, dependencies...), + diagnostic: cmd, + } +} + +func (kd *KubeCheck) Run(status cli.Reporter, cmd *cobra.Command) error { + newClient, err := client.NewClient(cmd.Flag("namespace").Value.String(), cmd.Flag("context").Value.String(), cmd.Flag("kubeconfig").Value.String()) + if err != nil { + return status.Error(err, "failed to obtain a Kubernetes client") + } + + return kd.diagnostic(status, newClient) +} diff --git a/internal/cmd/skupper/debug/check/kube/kube_version.go b/internal/cmd/skupper/debug/check/kube/kube_version.go new file mode 100644 index 000000000..d4ee02915 --- /dev/null +++ b/internal/cmd/skupper/debug/check/kube/kube_version.go @@ -0,0 +1,66 @@ +package kube + +import ( + "fmt" + "strconv" + "strings" + + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/cli" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check/command" + "github.com/skupperproject/skupper/internal/kube/client" + "k8s.io/apimachinery/pkg/version" +) + +const ( + minKubeMajor = 1 + minKubeMinor = 24 +) + +var checkK8sVersion = newKubeCheckCommand( + "kube-version", + "the Kubernetes version is supported", + kubeVersionRun, + &checkK8sAccess, +) + +func NewCmdCheckK8sVersion() command.Check { + return checkK8sVersion +} + +func kubeVersionRun(status cli.Reporter, kubeClient *client.KubeClient) error { + version, err := kubeClient.Kube.Discovery().ServerVersion() + if err != nil { + return status.Error(err, "Failed to retrieve the Kubernetes API server version") + } + + return status.Error(checkVersion(version), "the Kubernetes version is not supported") +} + +func checkVersion(ver *version.Info) error { + major, err := strconv.Atoi(ver.Major) + if err != nil { + return fmt.Errorf("error parsing API server major version %v: %w", ver.Major, err) + } + + if major > minKubeMajor { + return nil + } + + var minor int + if strings.HasSuffix(ver.Minor, "+") { + minor, err = strconv.Atoi(ver.Minor[0 : len(ver.Minor)-1]) + } else { + minor, err = strconv.Atoi(ver.Minor) + } + + if err != nil { + return fmt.Errorf("error parsing API server minor version %v: %w", ver.Minor, err) + } + + if major < minKubeMajor || minor < minKubeMinor { + return fmt.Errorf("installed Kubernetes version %s.%s is too old; Skupper requires at least %d.%d", + ver.Major, ver.Minor, minKubeMajor, minKubeMinor) + } + + return nil +} diff --git a/internal/cmd/skupper/debug/check/kube/kube_version_test.go b/internal/cmd/skupper/debug/check/kube/kube_version_test.go new file mode 100644 index 000000000..6cdeffe10 --- /dev/null +++ b/internal/cmd/skupper/debug/check/kube/kube_version_test.go @@ -0,0 +1,28 @@ +package kube + +import ( + "testing" + + "k8s.io/apimachinery/pkg/version" +) + +func TestCheckVersion(t *testing.T) { + tests := []struct { + major string + minor string + errorExpected bool + }{ + {"0", "25", true}, + {"1", "23", true}, + {"1", "24", false}, + {"1", "25", false}, + {"2", "22", false}, + } + + for _, test := range tests { + err := checkVersion(&version.Info{Major: test.major, Minor: test.minor}) + if (test.errorExpected && err == nil) || (!test.errorExpected && err != nil) { + t.Fail() + } + } +} diff --git a/internal/cmd/skupper/debug/check/log/types.go b/internal/cmd/skupper/debug/check/log/types.go new file mode 100644 index 000000000..4f42ea261 --- /dev/null +++ b/internal/cmd/skupper/debug/check/log/types.go @@ -0,0 +1,66 @@ +/* +Copyright 2019 The Kubernetes Authors. + +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 log + +// Level is a verbosity logging level for Info logs. +// See also https://github.com/kubernetes/klog. +type Level int32 + +// Logger defines the logging interface kind uses. +// It is roughly a subset of github.com/kubernetes/klog. +type Logger interface { + // Warn should be used to write user facing warnings. + Warn(message string) + // Warnf should be used to write Printf style user facing warnings. + Warnf(format string, args ...interface{}) + // Error may be used to write an error message when it occurs. + // Prefer returning an error instead in most cases. + Error(message string) + // Errorf may be used to write a Printf style error message when it occurs. + // Prefer returning an error instead in most cases. + Errorf(format string, args ...interface{}) + // V() returns an InfoLogger for a given verbosity Level. + // + // Normal verbosity levels: + // V(0): normal user facing messages go to V(0) + // V(1): debug messages start when V(N > 0), these should be high level + // V(2): more detailed log messages + // V(3+): trace level logging, in increasing "noisiness" ... allowing + // arbitrarily detailed logging at extremely low cost unless the + // logger has actually been configured to display these (E.G. via the -v + // command line flag) + // + // It is expected that the returned InfoLogger will be extremely cheap + // to interact with for a Level greater than the enabled level. + V(Level) InfoLogger +} + +// InfoLogger defines the info logging interface kind uses. +// It is roughly a subset of Verbose from github.com/kubernetes/klog. +type InfoLogger interface { + // Info is used to write a user facing status message. + // + // See: Logger.V + Info(message string) + // Infof is used to write a Printf style user facing status message. + Infof(format string, args ...interface{}) + // Enabled should return true if this verbosity level is enabled. + // on the Logger + // + // See: Logger.V + Enabled() bool +} diff --git a/internal/cmd/skupper/debug/debug.go b/internal/cmd/skupper/debug/debug.go index 51342d222..272c7b2fd 100644 --- a/internal/cmd/skupper/debug/debug.go +++ b/internal/cmd/skupper/debug/debug.go @@ -2,6 +2,7 @@ package debug import ( "github.com/skupperproject/skupper/internal/cmd/skupper/common" + "github.com/skupperproject/skupper/internal/cmd/skupper/debug/check" "github.com/skupperproject/skupper/internal/cmd/skupper/debug/kube" "github.com/skupperproject/skupper/internal/cmd/skupper/debug/nonkube" "github.com/skupperproject/skupper/internal/config" @@ -28,6 +29,8 @@ func CmdDebugFactory(configuredPlatform common.Platform) *cobra.Command { cmd := common.ConfigureCobraCommand(configuredPlatform, cmdDebugDesc, kubeCommand, nonKubeCommand) + cmd.AddCommand(check.NewCmdCheck()) + cmdFlags := common.CommandDebugFlags{} //Add flags if necessary