Skip to content
This repository was archived by the owner on Nov 14, 2024. It is now read-only.

Commit 4a854f0

Browse files
authored
feat: initial implementation of coder-doctor (#3)
1 parent f4ddbf6 commit 4a854f0

27 files changed

+2142
-0
lines changed

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/cdr/coder-doctor
2+
3+
go 1.16
4+
5+
require (
6+
cdr.dev/slog v1.4.1
7+
github.com/Masterminds/semver/v3 v3.1.1
8+
github.com/spf13/cobra v1.2.1
9+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
10+
k8s.io/apimachinery v0.19.14
11+
k8s.io/client-go v0.19.14
12+
k8s.io/klog/v2 v2.10.0 // indirect
13+
k8s.io/utils v0.0.0-20210305010621-2afb4311ab10 // indirect
14+
)

go.sum

Lines changed: 741 additions & 0 deletions
Large diffs are not rendered by default.

internal/api/error.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package api
2+
3+
// ErrorResult returns a CheckResult when an error occurs.
4+
func ErrorResult(name string, summary string, err error) *CheckResult {
5+
return &CheckResult{
6+
Name: name,
7+
State: StateFailed,
8+
Summary: summary,
9+
Details: map[string]interface{}{
10+
"error": err,
11+
},
12+
}
13+
}

internal/api/error_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package api_test
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"cdr.dev/slog/sloggers/slogtest/assert"
8+
"github.com/cdr/coder-doctor/internal/api"
9+
)
10+
11+
func TestErrorResult(t *testing.T) {
12+
t.Parallel()
13+
14+
err := errors.New("failed to connect to database")
15+
res := api.ErrorResult("check-name", "check failed", err)
16+
17+
assert.Equal(t, "name matches", "check-name", res.Name)
18+
assert.Equal(t, "state matches", api.StateFailed, res.State)
19+
assert.Equal(t, "summary matches", "check failed", res.Summary)
20+
assert.Equal(t, "error matches", err, res.Details["error"])
21+
}

internal/api/types.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"golang.org/x/xerrors"
8+
)
9+
10+
type Checker interface {
11+
// Validate returns an error if, and only if, the Checker was not
12+
// configured correctly.
13+
//
14+
// This method is responsible for verifying that the Checker has
15+
// all required parameters and the required parameters are valid,
16+
// and that optional parameters are valid, if set.
17+
Validate() error
18+
19+
// Run runs the checks and returns the results.
20+
//
21+
// This method will run through the checks and return results.
22+
Run(context.Context) error
23+
}
24+
25+
var _ = fmt.Stringer(StatePassed)
26+
27+
type CheckState int
28+
29+
const (
30+
// StatePassed indicates that the check passed successfully.
31+
StatePassed CheckState = iota
32+
33+
// StateWarning indicates a condition where Coder will gracefully degrade,
34+
// but the user will not have an optimal experience.
35+
StateWarning
36+
37+
// StateFailed indicates a condition where Coder will not be able to install
38+
// successfully.
39+
StateFailed
40+
41+
// StateInfo indicates a result for informational or diagnostic purposes
42+
// only, with no bearing on the ability to install Coder.
43+
StateInfo
44+
45+
// StateSkipped indicates an indeterminate result due to a skipped check.
46+
StateSkipped
47+
)
48+
49+
func (s CheckState) MustEmoji() string {
50+
emoji, err := s.Emoji()
51+
if err != nil {
52+
panic(err.Error())
53+
}
54+
return emoji
55+
}
56+
57+
func (s CheckState) Emoji() (string, error) {
58+
switch s {
59+
case StatePassed:
60+
return "👍", nil
61+
case StateWarning:
62+
return "⚠️", nil
63+
case StateFailed:
64+
return "👎", nil
65+
case StateInfo:
66+
return "🔔", nil
67+
case StateSkipped:
68+
return "⏩", nil
69+
}
70+
71+
return "", xerrors.Errorf("unknown state: %d", s)
72+
}
73+
74+
func (s CheckState) MustText() string {
75+
text, err := s.Text()
76+
if err != nil {
77+
panic(err.Error())
78+
}
79+
return text
80+
}
81+
82+
func (s CheckState) Text() (string, error) {
83+
switch s {
84+
case StatePassed:
85+
return "PASS", nil
86+
case StateWarning:
87+
return "WARN", nil
88+
case StateFailed:
89+
return "FAIL", nil
90+
case StateInfo:
91+
return "INFO", nil
92+
case StateSkipped:
93+
return "SKIP", nil
94+
}
95+
96+
return "", xerrors.Errorf("unknown state: %d", s)
97+
}
98+
99+
func (s CheckState) String() string {
100+
switch s {
101+
case StatePassed:
102+
return "StatePassed"
103+
case StateWarning:
104+
return "StateWarning"
105+
case StateFailed:
106+
return "StateFailed"
107+
case StateInfo:
108+
return "StateInfo"
109+
case StateSkipped:
110+
return "StateSkipped"
111+
}
112+
113+
panic(fmt.Sprintf("unknown state: %d", s))
114+
}
115+
116+
type CheckResult struct {
117+
Name string `json:"name"`
118+
State CheckState `json:"state"`
119+
Summary string `json:"summary"`
120+
Details map[string]interface{} `json:"details,omitempty"`
121+
}

internal/api/types_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package api_test
2+
3+
import (
4+
"testing"
5+
6+
"cdr.dev/slog/sloggers/slogtest/assert"
7+
"github.com/cdr/coder-doctor/internal/api"
8+
)
9+
10+
func TestKnownStates(t *testing.T) {
11+
t.Parallel()
12+
13+
states := []api.CheckState{
14+
api.StatePassed,
15+
api.StateWarning,
16+
api.StateFailed,
17+
api.StateInfo,
18+
api.StateSkipped,
19+
}
20+
21+
for _, state := range states {
22+
state := state
23+
24+
t.Run(state.String(), func(t *testing.T) {
25+
t.Parallel()
26+
27+
emoji, err := state.Emoji()
28+
assert.Success(t, "state.Emoji() error non-nil", err)
29+
assert.True(t, "state.Emoji() is non-empty", len(emoji) > 0)
30+
_ = state.MustEmoji()
31+
32+
text, err := state.Text()
33+
assert.Success(t, "state.Text() error non-nil", err)
34+
assert.True(t, "state.Text() is non-empty", len(text) > 0)
35+
_ = state.MustText()
36+
37+
str := state.String()
38+
assert.True(t, "state.String() is non-empty", len(str) > 0)
39+
})
40+
}
41+
}

internal/api/writer.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package api
2+
3+
var _ = ResultWriter(&DiscardWriter{})
4+
5+
// ResultWriter writes the given result to a configured output.
6+
type ResultWriter interface {
7+
WriteResult(*CheckResult) error
8+
}
9+
10+
// DiscardWriter is a writer that discards all results.
11+
type DiscardWriter struct {
12+
}
13+
14+
func (*DiscardWriter) WriteResult(_ *CheckResult) error {
15+
return nil
16+
}
17+
18+
var _ = ResultWriter(&CaptureWriter{})
19+
20+
// CaptureWriter is a writer that stores all results in an
21+
// internal buffer.
22+
type CaptureWriter struct {
23+
results []*CheckResult
24+
}
25+
26+
func (w *CaptureWriter) WriteResult(result *CheckResult) error {
27+
w.results = append(w.results, result)
28+
return nil
29+
}
30+
31+
func (w *CaptureWriter) Clear() {
32+
w.results = nil
33+
}
34+
35+
func (w *CaptureWriter) Get() []*CheckResult {
36+
return w.results
37+
}
38+
39+
func (w *CaptureWriter) Empty() bool {
40+
return w.Len() == 0
41+
}
42+
43+
func (w *CaptureWriter) Len() int {
44+
return len(w.results)
45+
}

internal/api/writer_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package api_test
2+
3+
import (
4+
"testing"
5+
6+
"cdr.dev/slog/sloggers/slogtest/assert"
7+
"github.com/cdr/coder-doctor/internal/api"
8+
)
9+
10+
func TestDiscardWriter(t *testing.T) {
11+
t.Parallel()
12+
13+
w := &api.DiscardWriter{}
14+
err := w.WriteResult(nil)
15+
16+
assert.Success(t, "discard with nil result", err)
17+
18+
err = w.WriteResult(&api.CheckResult{
19+
Name: "test-check",
20+
State: api.StatePassed,
21+
Summary: "check successful",
22+
})
23+
assert.Success(t, "discard with success result", err)
24+
}
25+
26+
func TestCaptureWriter(t *testing.T) {
27+
t.Parallel()
28+
29+
w := &api.CaptureWriter{}
30+
31+
assert.True(t, "initially empty", w.Empty())
32+
33+
result := &api.CheckResult{
34+
Name: "test",
35+
State: api.StatePassed,
36+
Summary: "test result",
37+
}
38+
err := w.WriteResult(result)
39+
assert.Success(t, "captured result", err)
40+
assert.True(t, "result length 1", w.Len() == 1)
41+
assert.Equal(t, "get back result", result, w.Get()[0])
42+
43+
w.Clear()
44+
assert.True(t, "empty buffer", w.Empty())
45+
}

internal/checks/kube/kubernetes.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package kube
2+
3+
import (
4+
"context"
5+
"io"
6+
7+
"github.com/Masterminds/semver/v3"
8+
"k8s.io/client-go/kubernetes"
9+
"golang.org/x/xerrors"
10+
11+
"cdr.dev/slog"
12+
"cdr.dev/slog/sloggers/sloghuman"
13+
14+
"github.com/cdr/coder-doctor/internal/api"
15+
)
16+
17+
var _ = api.Checker(&KubernetesChecker{})
18+
19+
type KubernetesChecker struct {
20+
client kubernetes.Interface
21+
writer api.ResultWriter
22+
coderVersion *semver.Version
23+
log slog.Logger
24+
}
25+
26+
type Option func(k *KubernetesChecker)
27+
28+
func NewKubernetesChecker(client kubernetes.Interface, opts ...Option) *KubernetesChecker {
29+
checker := &KubernetesChecker{
30+
client: client,
31+
log: slog.Make(sloghuman.Sink(io.Discard)),
32+
writer: &api.DiscardWriter{},
33+
// Select the newest version by default
34+
coderVersion: semver.MustParse("100.0.0"),
35+
}
36+
37+
for _, opt := range opts {
38+
opt(checker)
39+
}
40+
41+
return checker
42+
}
43+
44+
func WithWriter(writer api.ResultWriter) Option {
45+
return func(k *KubernetesChecker) {
46+
k.writer = writer
47+
}
48+
}
49+
50+
func WithCoderVersion(version *semver.Version) Option {
51+
return func(k *KubernetesChecker) {
52+
k.coderVersion = version
53+
}
54+
}
55+
56+
func WithLogger(log slog.Logger) Option {
57+
return func(k *KubernetesChecker) {
58+
k.log = log
59+
}
60+
}
61+
62+
func (*KubernetesChecker) Validate() error {
63+
return nil
64+
}
65+
66+
func (k *KubernetesChecker) Run(ctx context.Context) error {
67+
err := k.writer.WriteResult(k.CheckVersion(ctx))
68+
if err != nil {
69+
return xerrors.Errorf("check version: %w", err)
70+
}
71+
return nil
72+
}

0 commit comments

Comments
 (0)