This repository was archived by the owner on Nov 14, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
feat: initial implementation of coder-doctor #3
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
8a7c768
feat: initial implementation of coder-doctor
jawnsy 7152293
rename doctor -> coder-doctor
jawnsy 215ee39
implement more of kubernetes check
jawnsy fabea0d
fix tests
jawnsy 04e12e3
refactor interfaces, add logging
jawnsy 12836d2
add flags to control kubernetes settings
jawnsy 84882af
wip
jawnsy 5a26ffc
more tests
jawnsy f2f9988
add other resultwriter implementations
jawnsy 31cb1c5
update to 0.19.14
jawnsy 71f97fc
cian's code review
jawnsy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
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 | ||
jawnsy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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"` | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
}) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!