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

Commit b33297a

Browse files
authored
feat: check helm version (#5)
* log current kubernetes context * check local helm version
1 parent 8bd8d17 commit b33297a

File tree

10 files changed

+528
-6
lines changed

10 files changed

+528
-6
lines changed

internal/api/skip.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package api
2+
3+
// SkippedResult returns a CheckResult indicating the check was skipped.
4+
func SkippedResult(name string, summary string) *CheckResult {
5+
return &CheckResult{
6+
Name: name,
7+
State: StateSkipped,
8+
Summary: summary,
9+
Details: map[string]interface{}{},
10+
}
11+
}

internal/api/skip_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 TestSkippedResult(t *testing.T) {
11+
t.Parallel()
12+
13+
checkName := "don't wanna"
14+
checkSummary := "just because"
15+
res := api.SkippedResult(checkName, checkSummary)
16+
assert.Equal(t, "name matches", checkName, res.Name)
17+
assert.Equal(t, "state matches", api.StateSkipped, res.State)
18+
assert.Equal(t, "summary matches", checkSummary, res.Summary)
19+
}

internal/api/types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,17 @@ type CheckResult struct {
119119
Summary string `json:"summary"`
120120
Details map[string]interface{} `json:"details,omitempty"`
121121
}
122+
123+
// CheckTarget indicates the subject of a Checker
124+
type CheckTarget string
125+
126+
const (
127+
// CheckTargetUndefined indicates that a Checker does not run against any specific target.
128+
CheckTargetUndefined CheckTarget = ""
129+
130+
// CheckTargetLocal indicates that a Checker runs against the local machine.
131+
CheckTargetLocal CheckTarget = "local"
132+
133+
// CheckTargetKubernetes indicates that a Checker runs against a Kubernetes cluster.
134+
CheckTargetKubernetes = "kubernetes"
135+
)

internal/api/version.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package api
2+
3+
import (
4+
"github.com/Masterminds/semver/v3"
5+
"golang.org/x/xerrors"
6+
)
7+
8+
func MustConstraint(s string) *semver.Constraints {
9+
c, err := semver.NewConstraint(s)
10+
if err != nil {
11+
panic(xerrors.Errorf("parse constraint: %w", err))
12+
}
13+
14+
return c
15+
}

internal/checks/kube/kubernetes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"io"
66

77
"github.com/Masterminds/semver/v3"
8-
"k8s.io/client-go/kubernetes"
98
"golang.org/x/xerrors"
9+
"k8s.io/client-go/kubernetes"
1010

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

internal/checks/local/helm.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package local
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/Masterminds/semver/v3"
10+
11+
"cdr.dev/slog"
12+
"github.com/cdr/coder-doctor/internal/api"
13+
)
14+
15+
const LocalHelmVersionCheck = "local-helm-version"
16+
17+
type VersionRequirement struct {
18+
Coder *semver.Version
19+
HelmConstraint *semver.Constraints
20+
}
21+
22+
var versionRequirements = []VersionRequirement{
23+
{
24+
Coder: semver.MustParse("1.21.0"),
25+
HelmConstraint: api.MustConstraint(">= 3.6.0"),
26+
},
27+
{
28+
Coder: semver.MustParse("1.20.0"),
29+
HelmConstraint: api.MustConstraint(">= 3.6.0"),
30+
},
31+
}
32+
33+
func (l *Checker) CheckLocalHelmVersion(ctx context.Context) *api.CheckResult {
34+
if l.target != api.CheckTargetKubernetes {
35+
return api.SkippedResult(LocalHelmVersionCheck, "not applicable for target "+string(l.target))
36+
}
37+
38+
helmBin, err := l.lookPathF("helm")
39+
if err != nil {
40+
return api.ErrorResult(LocalHelmVersionCheck, "could not find helm binary in $PATH", err)
41+
}
42+
43+
helmVersionRaw, err := l.execF(ctx, helmBin, "version", "--short")
44+
if err != nil {
45+
return api.ErrorResult(LocalHelmVersionCheck, "failed to determine helm version", err)
46+
}
47+
48+
helmVersion, err := semver.NewVersion(string(bytes.TrimSpace(helmVersionRaw)))
49+
if err != nil {
50+
return api.ErrorResult(LocalHelmVersionCheck, "failed to parse helm version", err)
51+
}
52+
53+
selectedVersion := findNearestHelmVersion(l.coderVersion)
54+
if selectedVersion == nil {
55+
return api.ErrorResult(LocalHelmVersionCheck, fmt.Sprintf("checking coder version %s not supported", l.coderVersion.String()), nil)
56+
}
57+
l.log.Debug(ctx, "selected coder version", slog.F("requested", l.coderVersion), slog.F("selected", selectedVersion.Coder))
58+
59+
result := &api.CheckResult{
60+
Name: LocalHelmVersionCheck,
61+
Details: map[string]interface{}{
62+
"helm-bin": helmBin,
63+
"helm-version": helmVersion.String(),
64+
"helm-version-constraints": selectedVersion.HelmConstraint.String(),
65+
},
66+
}
67+
68+
if ok, cerrs := selectedVersion.HelmConstraint.Validate(helmVersion); !ok {
69+
result.State = api.StateFailed
70+
var b strings.Builder
71+
_, err := fmt.Fprintf(&b, "Coder %s requires Helm version %s (installed: %s)\n", selectedVersion.Coder, selectedVersion.HelmConstraint, helmVersion)
72+
if err != nil {
73+
return api.ErrorResult(LocalHelmVersionCheck, "failed to write error result", err)
74+
}
75+
for _, cerr := range cerrs {
76+
if _, err := fmt.Fprintf(&b, "constraint failed: %s\n", cerr); err != nil {
77+
return api.ErrorResult(LocalHelmVersionCheck, "failed to write constraint error", err)
78+
}
79+
}
80+
result.Summary = b.String()
81+
} else {
82+
result.State = api.StatePassed
83+
result.Summary = fmt.Sprintf("Coder %s supports Helm %s", selectedVersion.Coder, selectedVersion.HelmConstraint)
84+
}
85+
86+
return result
87+
}
88+
89+
func findNearestHelmVersion(target *semver.Version) *VersionRequirement {
90+
var selected *VersionRequirement
91+
92+
for _, v := range versionRequirements {
93+
v := v
94+
if !v.Coder.GreaterThan(target) {
95+
selected = &v
96+
break
97+
}
98+
}
99+
100+
return selected
101+
}

internal/checks/local/helm_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package local
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/Masterminds/semver/v3"
9+
10+
"cdr.dev/slog/sloggers/slogtest/assert"
11+
"github.com/cdr/coder-doctor/internal/api"
12+
)
13+
14+
func Test_CheckLocalHelmVersion(t *testing.T) {
15+
t.Parallel()
16+
17+
type params struct {
18+
W *api.CaptureWriter
19+
EX *fakeExecer
20+
LP *fakeLookPather
21+
Opts []Option
22+
Ctx context.Context
23+
}
24+
25+
run := func(t *testing.T, name string, fn func(t *testing.T, p *params)) {
26+
t.Run(name, func(t *testing.T) {
27+
ctx := context.Background()
28+
cw := &api.CaptureWriter{}
29+
ex := newFakeExecer(t)
30+
lp := newFakeLookPather(t)
31+
opts := []Option{
32+
WithWriter(cw),
33+
WithExecF(ex.ExecContext),
34+
WithLookPathF(lp.LookPath),
35+
WithTarget(api.CheckTargetKubernetes), // default
36+
}
37+
p := &params{
38+
W: cw,
39+
EX: ex,
40+
LP: lp,
41+
Opts: opts,
42+
Ctx: ctx,
43+
}
44+
fn(t, p)
45+
})
46+
}
47+
48+
run(t, "helm: when not running against kubernetes", func(t *testing.T, p *params) {
49+
p.Opts = append(p.Opts, WithTarget(api.CheckTargetUndefined))
50+
lc := NewChecker(p.Opts...)
51+
err := lc.Run(p.Ctx)
52+
assert.Success(t, "run local checker", err)
53+
assert.False(t, "results should not be empty", p.W.Empty())
54+
for _, res := range p.W.Get() {
55+
if res.Name == LocalHelmVersionCheck {
56+
assert.Equal(t, "should skip helm check if not running against kubernetes", api.StateSkipped, res.State)
57+
}
58+
}
59+
})
60+
61+
run(t, "helm: with version 3.6", func(t *testing.T, p *params) {
62+
p.LP.Handle("helm", "/usr/local/bin/helm", nil)
63+
p.EX.Handle("/usr/local/bin/helm version --short", []byte("v3.6.0+g7f2df64"), nil)
64+
lc := NewChecker(p.Opts...)
65+
err := lc.Run(p.Ctx)
66+
assert.Success(t, "run local checker", err)
67+
assert.False(t, "results should not be empty", p.W.Empty())
68+
for _, res := range p.W.Get() {
69+
if res.Name == LocalHelmVersionCheck {
70+
assert.Equal(t, "should pass", api.StatePassed, res.State)
71+
}
72+
}
73+
})
74+
75+
run(t, "helm: with version 2", func(t *testing.T, p *params) {
76+
p.LP.Handle("helm", "/usr/local/bin/helm", nil)
77+
p.EX.Handle("/usr/local/bin/helm version --short", []byte("v2.0.0"), nil)
78+
lc := NewChecker(p.Opts...)
79+
err := lc.Run(p.Ctx)
80+
assert.Success(t, "run local checker", err)
81+
assert.False(t, "results should not be empty", p.W.Empty())
82+
for _, res := range p.W.Get() {
83+
if res.Name == LocalHelmVersionCheck {
84+
assert.Equal(t, "should fail", api.StateFailed, res.State)
85+
}
86+
}
87+
})
88+
89+
run(t, "helm: not in path", func(t *testing.T, p *params) {
90+
p.LP.Handle("helm", "", os.ErrNotExist)
91+
lc := NewChecker(p.Opts...)
92+
err := lc.Run(p.Ctx)
93+
assert.Success(t, "run local checker", err)
94+
assert.False(t, "results should not be empty", p.W.Empty())
95+
for _, res := range p.W.Get() {
96+
if res.Name == LocalHelmVersionCheck {
97+
assert.Equal(t, "should fail", api.StateFailed, res.State)
98+
}
99+
}
100+
})
101+
102+
run(t, "helm: cannot be executed", func(t *testing.T, p *params) {
103+
p.LP.Handle("helm", "/usr/local/bin/helm", nil)
104+
p.EX.Handle("/usr/local/bin/helm version --short", []byte(""), os.ErrPermission)
105+
lc := NewChecker(p.Opts...)
106+
err := lc.Run(p.Ctx)
107+
assert.Success(t, "run local checker", err)
108+
assert.False(t, "results should not be empty", p.W.Empty())
109+
for _, res := range p.W.Get() {
110+
if res.Name == LocalHelmVersionCheck {
111+
assert.Equal(t, "should fail", api.StateFailed, res.State)
112+
}
113+
}
114+
})
115+
116+
run(t, "helm: returns garbage version", func(t *testing.T, p *params) {
117+
p.LP.Handle("helm", "/usr/local/bin/helm", nil)
118+
p.EX.Handle("/usr/local/bin/helm version --short", []byte(""), nil)
119+
lc := NewChecker(p.Opts...)
120+
err := lc.Run(p.Ctx)
121+
assert.Success(t, "run local checker", err)
122+
assert.False(t, "results should not be empty", p.W.Empty())
123+
for _, res := range p.W.Get() {
124+
if res.Name == LocalHelmVersionCheck {
125+
assert.Equal(t, "should fail", api.StateFailed, res.State)
126+
}
127+
}
128+
})
129+
130+
run(t, "helm: coder version is unsupported", func(t *testing.T, p *params) {
131+
p.Opts = append(p.Opts, WithCoderVersion(semver.MustParse("v1.19")))
132+
p.LP.Handle("helm", "/usr/local/bin/helm", nil)
133+
p.EX.Handle("/usr/local/bin/helm version --short", []byte("v3.6.0+g7f2df64"), nil)
134+
lc := NewChecker(p.Opts...)
135+
err := lc.Run(p.Ctx)
136+
assert.Success(t, "run local checker", err)
137+
assert.False(t, "results should not be empty", p.W.Empty())
138+
for _, res := range p.W.Get() {
139+
if res.Name == LocalHelmVersionCheck {
140+
assert.Equal(t, "should fail", api.StateFailed, res.State)
141+
}
142+
}
143+
})
144+
}

0 commit comments

Comments
 (0)