Skip to content

Commit

Permalink
Initial implementation of skupper diagnose
Browse files Browse the repository at this point in the history
This introduces a framework for diagnostics commands, with an initial
implementation of two Kubernetes checks (verifying that the Kubernetes
API is accessible, and that the Kubernetes version is supported).

The framework supports simple declaration of Cobra commands
constructed from individual diagnostics, and dependencies between
diagnostics.

The kind spinner is copied with some adaptations borrowed from the
Submariner project.

Signed-off-by: Stephen Kitt <skitt@redhat.com>
  • Loading branch information
skitt committed Dec 18, 2024
1 parent d9d37ea commit 69dd3ca
Show file tree
Hide file tree
Showing 14 changed files with 1,133 additions and 5 deletions.
8 changes: 3 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
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
Expand All @@ -38,6 +39,8 @@ require (
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
)

Expand Down Expand Up @@ -76,14 +79,12 @@ 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
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/gomega v1.33.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand All @@ -102,12 +103,9 @@ 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
k8s.io/apiextensions-apiserver v0.31.0 // indirect
k8s.io/code-generator v0.31.0 // 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
)
259 changes: 259 additions & 0 deletions internal/cmd/skupper/diagnose/cli/logger.go
Original file line number Diff line number Diff line change
@@ -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/diagnose/env"
"github.com/skupperproject/skupper/internal/cmd/skupper/diagnose/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)
}
Loading

0 comments on commit 69dd3ca

Please sign in to comment.