Skip to content
This repository was archived by the owner on Nov 14, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/cdr/coder-doctor

go 1.16

require (
cdr.dev/slog v1.4.1
github.com/Masterminds/semver/v3 v3.1.1
github.com/spf13/cobra v1.2.1
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
k8s.io/apimachinery v0.19.14
k8s.io/client-go v0.19.14
k8s.io/klog/v2 v2.10.0 // indirect
k8s.io/utils v0.0.0-20210305010621-2afb4311ab10 // indirect
)
741 changes: 741 additions & 0 deletions go.sum

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions internal/api/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package api

// ErrorResult returns a CheckResult when an error occurs.
func ErrorResult(name string, summary string, err error) *CheckResult {
return &CheckResult{
Name: name,
State: StateFailed,
Summary: summary,
Details: map[string]interface{}{
"error": err,
},
}
}
21 changes: 21 additions & 0 deletions internal/api/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package api_test

import (
"errors"
"testing"

"cdr.dev/slog/sloggers/slogtest/assert"
"github.com/cdr/coder-doctor/internal/api"
)

func TestErrorResult(t *testing.T) {
t.Parallel()

err := errors.New("failed to connect to database")
res := api.ErrorResult("check-name", "check failed", err)

assert.Equal(t, "name matches", "check-name", res.Name)
assert.Equal(t, "state matches", api.StateFailed, res.State)
assert.Equal(t, "summary matches", "check failed", res.Summary)
assert.Equal(t, "error matches", err, res.Details["error"])
}
121 changes: 121 additions & 0 deletions internal/api/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package api

import (
"context"
"fmt"

"golang.org/x/xerrors"
)

type Checker interface {
// Validate returns an error if, and only if, the Checker was not
// configured correctly.
//
// This method is responsible for verifying that the Checker has
// all required parameters and the required parameters are valid,
// and that optional parameters are valid, if set.
Validate() error

// Run runs the checks and returns the results.
//
// This method will run through the checks and return results.
Run(context.Context) error
}

var _ = fmt.Stringer(StatePassed)

type CheckState int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we make these integers rather than strings?

That way we could eliminate the string function, and set the values to the const names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There wasn't a particular reason, no! I thought that integers were just the common way to do enums like this in Go.

A nice benefit of this is that the filtering can be done with bitwise operations (to determine whether to log each level), but of course we could do that with a set of bools or something too: https://github.com/cdr/coder-doctor/blob/31cb1c565fc42be9b40d9c68e634625287086b0b/internal/filterwriter/filter.go#L42-L60

I suppose another benefit is that comparisons should be faster with integers, because then it's just a numeric equality instead of string comparison

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neato. Appreciate the context, and I'd agree with you here!


const (
// StatePassed indicates that the check passed successfully.
StatePassed CheckState = iota

// StateWarning indicates a condition where Coder will gracefully degrade,
// but the user will not have an optimal experience.
StateWarning

// StateFailed indicates a condition where Coder will not be able to install
// successfully.
StateFailed

// StateInfo indicates a result for informational or diagnostic purposes
// only, with no bearing on the ability to install Coder.
StateInfo

// StateSkipped indicates an indeterminate result due to a skipped check.
StateSkipped
)

func (s CheckState) MustEmoji() string {
emoji, err := s.Emoji()
if err != nil {
panic(err.Error())
}
return emoji
}

func (s CheckState) Emoji() (string, error) {
switch s {
case StatePassed:
return "👍", nil
case StateWarning:
return "⚠️", nil
case StateFailed:
return "👎", nil
case StateInfo:
return "🔔", nil
case StateSkipped:
return "⏩", nil
}

return "", xerrors.Errorf("unknown state: %d", s)
}

func (s CheckState) MustText() string {
text, err := s.Text()
if err != nil {
panic(err.Error())
}
return text
}

func (s CheckState) Text() (string, error) {
switch s {
case StatePassed:
return "PASS", nil
case StateWarning:
return "WARN", nil
case StateFailed:
return "FAIL", nil
case StateInfo:
return "INFO", nil
case StateSkipped:
return "SKIP", nil
}

return "", xerrors.Errorf("unknown state: %d", s)
}

func (s CheckState) String() string {
switch s {
case StatePassed:
return "StatePassed"
case StateWarning:
return "StateWarning"
case StateFailed:
return "StateFailed"
case StateInfo:
return "StateInfo"
case StateSkipped:
return "StateSkipped"
}

panic(fmt.Sprintf("unknown state: %d", s))
}

type CheckResult struct {
Name string `json:"name"`
State CheckState `json:"state"`
Summary string `json:"summary"`
Details map[string]interface{} `json:"details,omitempty"`
}
41 changes: 41 additions & 0 deletions internal/api/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package api_test

import (
"testing"

"cdr.dev/slog/sloggers/slogtest/assert"
"github.com/cdr/coder-doctor/internal/api"
)

func TestKnownStates(t *testing.T) {
t.Parallel()

states := []api.CheckState{
api.StatePassed,
api.StateWarning,
api.StateFailed,
api.StateInfo,
api.StateSkipped,
}

for _, state := range states {
state := state

t.Run(state.String(), func(t *testing.T) {
t.Parallel()

emoji, err := state.Emoji()
assert.Success(t, "state.Emoji() error non-nil", err)
assert.True(t, "state.Emoji() is non-empty", len(emoji) > 0)
_ = state.MustEmoji()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ this


text, err := state.Text()
assert.Success(t, "state.Text() error non-nil", err)
assert.True(t, "state.Text() is non-empty", len(text) > 0)
_ = state.MustText()

str := state.String()
assert.True(t, "state.String() is non-empty", len(str) > 0)
})
}
}
45 changes: 45 additions & 0 deletions internal/api/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api

var _ = ResultWriter(&DiscardWriter{})

// ResultWriter writes the given result to a configured output.
type ResultWriter interface {
WriteResult(*CheckResult) error
}

// DiscardWriter is a writer that discards all results.
type DiscardWriter struct {
}

func (*DiscardWriter) WriteResult(_ *CheckResult) error {
return nil
}

var _ = ResultWriter(&CaptureWriter{})

// CaptureWriter is a writer that stores all results in an
// internal buffer.
type CaptureWriter struct {
results []*CheckResult
}

func (w *CaptureWriter) WriteResult(result *CheckResult) error {
w.results = append(w.results, result)
return nil
}

func (w *CaptureWriter) Clear() {
w.results = nil
}

func (w *CaptureWriter) Get() []*CheckResult {
return w.results
}

func (w *CaptureWriter) Empty() bool {
return w.Len() == 0
}

func (w *CaptureWriter) Len() int {
return len(w.results)
}
45 changes: 45 additions & 0 deletions internal/api/writer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api_test

import (
"testing"

"cdr.dev/slog/sloggers/slogtest/assert"
"github.com/cdr/coder-doctor/internal/api"
)

func TestDiscardWriter(t *testing.T) {
t.Parallel()

w := &api.DiscardWriter{}
err := w.WriteResult(nil)

assert.Success(t, "discard with nil result", err)

err = w.WriteResult(&api.CheckResult{
Name: "test-check",
State: api.StatePassed,
Summary: "check successful",
})
assert.Success(t, "discard with success result", err)
}

func TestCaptureWriter(t *testing.T) {
t.Parallel()

w := &api.CaptureWriter{}

assert.True(t, "initially empty", w.Empty())

result := &api.CheckResult{
Name: "test",
State: api.StatePassed,
Summary: "test result",
}
err := w.WriteResult(result)
assert.Success(t, "captured result", err)
assert.True(t, "result length 1", w.Len() == 1)
assert.Equal(t, "get back result", result, w.Get()[0])

w.Clear()
assert.True(t, "empty buffer", w.Empty())
}
72 changes: 72 additions & 0 deletions internal/checks/kube/kubernetes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package kube

import (
"context"
"io"

"github.com/Masterminds/semver/v3"
"k8s.io/client-go/kubernetes"
"golang.org/x/xerrors"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"

"github.com/cdr/coder-doctor/internal/api"
)

var _ = api.Checker(&KubernetesChecker{})

type KubernetesChecker struct {
client kubernetes.Interface
writer api.ResultWriter
coderVersion *semver.Version
log slog.Logger
}

type Option func(k *KubernetesChecker)

func NewKubernetesChecker(client kubernetes.Interface, opts ...Option) *KubernetesChecker {
checker := &KubernetesChecker{
client: client,
log: slog.Make(sloghuman.Sink(io.Discard)),
writer: &api.DiscardWriter{},
// Select the newest version by default
coderVersion: semver.MustParse("100.0.0"),
}

for _, opt := range opts {
opt(checker)
}

return checker
}

func WithWriter(writer api.ResultWriter) Option {
return func(k *KubernetesChecker) {
k.writer = writer
}
}

func WithCoderVersion(version *semver.Version) Option {
return func(k *KubernetesChecker) {
k.coderVersion = version
}
}

func WithLogger(log slog.Logger) Option {
return func(k *KubernetesChecker) {
k.log = log
}
}

func (*KubernetesChecker) Validate() error {
return nil
}

func (k *KubernetesChecker) Run(ctx context.Context) error {
err := k.writer.WriteResult(k.CheckVersion(ctx))
if err != nil {
return xerrors.Errorf("check version: %w", err)
}
return nil
}
Loading