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

Commit 863c945

Browse files
committed
implement more of kubernetes check
1 parent 5eace5e commit 863c945

File tree

10 files changed

+285
-39
lines changed

10 files changed

+285
-39
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ module github.com/cdr/coder-doctor
33
go 1.16
44

55
require (
6+
cdr.dev/slog v1.4.1
67
github.com/Masterminds/semver/v3 v3.1.1
8+
github.com/davecgh/go-spew v1.1.1
79
github.com/spf13/cobra v1.2.1
10+
k8s.io/apimachinery v0.19.13
811
k8s.io/client-go v0.19.13
912
k8s.io/klog/v2 v2.10.0 // indirect
1013
k8s.io/utils v0.0.0-20210305010621-2afb4311ab10 // indirect

go.sum

Lines changed: 52 additions & 6 deletions
Large diffs are not rendered by default.

internal/api/types.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
type Check func(context.Context, CheckOptions) CheckResults
1111

1212
type CheckOptions struct {
13-
CoderVersion semver.Version
13+
CoderVersion *semver.Version
1414
Kubernetes kubernetes.Interface
1515
}
1616

@@ -31,4 +31,15 @@ type CheckResult struct {
3131
Details map[string]interface{}
3232
}
3333

34-
type CheckResults []CheckResult
34+
func ErrorResult(name string, summary string, err error) *CheckResult {
35+
return &CheckResult{
36+
Name: name,
37+
State: StateFailed,
38+
Summary: summary,
39+
Details: map[string]interface{}{
40+
"error": err,
41+
},
42+
}
43+
}
44+
45+
type CheckResults []*CheckResult

internal/checks/checks.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import (
77
"github.com/cdr/coder-doctor/internal/checks/kubernetes"
88
)
99

10-
var kubernetesChecks = []api.Check{
11-
kubernetes.CheckVersion,
12-
}
13-
1410
func RunKubernetes(ctx context.Context, opts api.CheckOptions) api.CheckResults {
15-
return kubernetes.CheckVersion(ctx, opts)
11+
return api.CheckResults{
12+
kubernetes.CheckVersion(ctx, opts),
13+
}
1614
}

internal/checks/kubernetes/version.go

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,93 @@ package kubernetes
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"strings"
68

9+
"github.com/Masterminds/semver/v3"
710
"github.com/cdr/coder-doctor/internal/api"
11+
"k8s.io/apimachinery/pkg/version"
812
)
913

10-
func CheckVersion(ctx context.Context, opts api.CheckOptions) api.CheckResults {
11-
// coderVersion := opts.CoderVersion
14+
type CoderVersionRequirement struct {
15+
CoderVersion *semver.Version
16+
KubernetesVersionMin *semver.Version
17+
KubernetesVersionMax *semver.Version
18+
}
19+
20+
var versionRequirements = []CoderVersionRequirement{
21+
{
22+
CoderVersion: semver.MustParse("1.21"),
23+
KubernetesVersionMin: semver.MustParse("1.19"),
24+
KubernetesVersionMax: semver.MustParse("1.22"),
25+
}, {
26+
CoderVersion: semver.MustParse("1.20"),
27+
KubernetesVersionMin: semver.MustParse("1.19"),
28+
KubernetesVersionMax: semver.MustParse("1.21"),
29+
},
30+
}
31+
32+
func CheckVersion(ctx context.Context, opts api.CheckOptions) *api.CheckResult {
33+
const checkName = "kubernetes-version"
34+
35+
coderVersion := opts.CoderVersion
1236
client := opts.Kubernetes
1337

14-
versionInfo, err := client.Discovery().ServerVersion()
38+
var versionInfo version.Info
39+
40+
// This uses the RESTClient rather than Discovery().ServerVersion()
41+
// because the latter does not accept a context.
42+
body, err := client.Discovery().RESTClient().Get().AbsPath("/version").Do(ctx).Raw()
43+
if err != nil {
44+
return api.ErrorResult(checkName, "failed to get version from server", err)
45+
}
46+
47+
err = json.Unmarshal(body, &versionInfo)
1548
if err != nil {
16-
return api.CheckResults{
17-
api.CheckResult{
18-
Name: "kubernetes-version",
19-
State: api.StateFailed,
20-
Summary: "failed to get Kubernetes version from server",
21-
Details: map[string]interface{}{
22-
"error": err,
23-
},
24-
},
49+
return api.ErrorResult(checkName, "failed to parse server version", err)
50+
}
51+
52+
var v CoderVersionRequirement
53+
for _, v = range versionRequirements {
54+
if !v.CoderVersion.LessThan(coderVersion) {
55+
break
2556
}
2657
}
2758

28-
return api.CheckResults{
29-
api.CheckResult{
30-
Name: "kubernetes-version",
31-
State: api.StateInfo,
32-
Summary: fmt.Sprintf("kubernetes version: %s", versionInfo),
59+
kubernetesVersion, err := semver.NewVersion(strings.TrimLeft(versionInfo.GitVersion, "v"))
60+
if err != nil {
61+
fmt.Printf("error parsing version: %v\n", err)
62+
}
63+
64+
result := &api.CheckResult{
65+
Name: checkName,
66+
Details: map[string]interface{}{
67+
"platform": versionInfo.Platform,
68+
"major": versionInfo.Major,
69+
"minor": versionInfo.Minor,
70+
"git-version": versionInfo.GitVersion,
71+
"git-commit": versionInfo.GitCommit,
72+
"git-tree-state": versionInfo.GitTreeState,
73+
"build-date": versionInfo.BuildDate,
74+
"go-version": versionInfo.GoVersion,
75+
"compiler": versionInfo.Compiler,
3376
},
3477
}
78+
79+
if kubernetesVersion.LessThan(v.KubernetesVersionMin) || kubernetesVersion.GreaterThan(v.KubernetesVersionMax) {
80+
result.State = api.StateFailed
81+
result.Summary = fmt.Sprintf("Coder %s supports Kubernetes %s to %s and was not tested with %s",
82+
v.CoderVersion, v.KubernetesVersionMin, v.KubernetesVersionMax, kubernetesVersion)
83+
} else {
84+
result.State = api.StatePassed
85+
result.Summary = fmt.Sprintf("Coder %s supports Kubernetes %s to %s (server version %s)",
86+
v.CoderVersion, v.KubernetesVersionMin, v.KubernetesVersionMax, kubernetesVersion)
87+
}
88+
89+
// fmt.Printf("server version: %v\n", kubernetesVersion)
90+
// fmt.Printf("min version: %v\n", v.KubernetesVersionMin)
91+
// fmt.Printf("max version: %v\n", v.KubernetesVersionMax)
92+
93+
return result
3594
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package kubernetes_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"cdr.dev/slog/sloggers/slogtest/assert"
11+
"github.com/Masterminds/semver/v3"
12+
"github.com/cdr/coder-doctor/internal/api"
13+
"github.com/cdr/coder-doctor/internal/checks/kubernetes"
14+
"k8s.io/apimachinery/pkg/version"
15+
kclient "k8s.io/client-go/kubernetes"
16+
"k8s.io/client-go/rest"
17+
)
18+
19+
func TestVersions(t *testing.T) {
20+
t.Parallel()
21+
22+
tests := []struct {
23+
Name string
24+
CoderVersion *semver.Version
25+
KubernetesVersion *version.Info
26+
ExpectedResult *api.CheckResult
27+
}{
28+
{
29+
Name: "coder-valid-version-gke",
30+
CoderVersion: semver.MustParse("1.21"),
31+
KubernetesVersion: &version.Info{
32+
Major: "1",
33+
Minor: "20+",
34+
GitVersion: "v1.20.8-gke.900",
35+
GitCommit: "28ab8501be88ea42e897ca8514d7cd0b436253d9",
36+
GitTreeState: "clean",
37+
BuildDate: "2021-06-30T09:23:36Z",
38+
GoVersion: "go1.15.13b5",
39+
Compiler: "gc",
40+
Platform: "linux/amd64",
41+
},
42+
ExpectedResult: &api.CheckResult{
43+
Name: "kubernetes-version",
44+
State: api.StatePassed,
45+
Summary: "",
46+
Details: map[string]interface{}{
47+
"build-date": "2021-06-30T09:23:36Z",
48+
"compiler": "gc",
49+
"git-commit": "28ab8501be88ea42e897ca8514d7cd0b436253d9",
50+
"git-tree-state": "clean",
51+
"git-version": "v1.20.8-gke.900",
52+
"go-version": "go1.15.13b5",
53+
"major": "1",
54+
"minor": "20+",
55+
"platform": "linux/amd64",
56+
},
57+
},
58+
},
59+
{
60+
Name: "coder-old-version-gke",
61+
CoderVersion: semver.MustParse("1.21"),
62+
KubernetesVersion: &version.Info{
63+
Major: "1",
64+
Minor: "18+",
65+
GitVersion: "v1.18.20-gke.900",
66+
GitCommit: "1facb91642e16cb4f5be4e4a632c488aa4700382",
67+
GitTreeState: "clean",
68+
BuildDate: "2021-06-28T09:19:58Z",
69+
GoVersion: "go1.13.15b4",
70+
Compiler: "gc",
71+
Platform: "linux/amd64",
72+
},
73+
},
74+
}
75+
76+
for _, test := range tests {
77+
test := test
78+
t.Run(test.Name, func(t *testing.T) {
79+
t.Parallel()
80+
81+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
82+
w.Header().Set("Content-Type", "application/json")
83+
w.WriteHeader(http.StatusOK)
84+
err := json.NewEncoder(w).Encode(test.KubernetesVersion)
85+
assert.Success(t, "failed to encode response", err)
86+
}))
87+
defer server.Close()
88+
89+
client, err := kclient.NewForConfig(&rest.Config{
90+
Host: server.URL,
91+
})
92+
assert.Success(t, "failed to create client", err)
93+
94+
res := kubernetes.CheckVersion(context.Background(), api.CheckOptions{
95+
CoderVersion: test.CoderVersion,
96+
Kubernetes: client,
97+
})
98+
assert.Equal(t, "check result matches", test.ExpectedResult, res)
99+
})
100+
}
101+
}

internal/cmd/check/check.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import (
88
func NewCommand() *cobra.Command {
99
checkCmd := &cobra.Command{
1010
Use: "check",
11-
Short: "scan the Kubernetes cluster for compatibility",
11+
Short: "check that a resource is compatible with Coder",
1212
Args: cobra.ExactArgs(1),
1313
}
1414

1515
checkCmd.PersistentFlags().Int("verbosity", 0, "log level verbosity")
16+
checkCmd.PersistentFlags().String("coder-version", "1.21", "version of Coder")
1617

1718
checkCmd.AddCommand(
1819
kubernetes.NewCommand(),

internal/cmd/check/kubernetes/kubernetes.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kubernetes
22

33
import (
4+
"github.com/Masterminds/semver/v3"
45
"github.com/cdr/coder-doctor/internal/api"
56
"github.com/cdr/coder-doctor/internal/checks"
67
"github.com/spf13/cobra"
@@ -25,8 +26,6 @@ func NewCommand() *cobra.Command {
2526
}
2627

2728
func run(cmd *cobra.Command, args []string) error {
28-
cmd.Println("scanning kubernetes cluster")
29-
3029
config, err := clientcmd.BuildConfigFromFlags("", "/home/coder/.kube/config")
3130
if err != nil {
3231
panic(err.Error())
@@ -37,12 +36,21 @@ func run(cmd *cobra.Command, args []string) error {
3736
panic(err.Error())
3837
}
3938

39+
coderVersion, err := cmd.InheritedFlags().GetString("coder-version")
40+
if err != nil {
41+
panic(err.Error())
42+
}
43+
44+
cv, err := semver.NewVersion(coderVersion)
45+
if err != nil {
46+
panic(err.Error())
47+
}
48+
4049
results := checks.RunKubernetes(cmd.Context(), api.CheckOptions{
41-
Kubernetes: clientset,
50+
Kubernetes: clientset,
51+
CoderVersion: cv,
4252
})
43-
for _, result := range results {
44-
cmd.Println(result.Summary)
45-
}
53+
PrintResults(cmd.OutOrStdout(), results)
4654

4755
return nil
4856
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package kubernetes
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/cdr/coder-doctor/internal/api"
8+
)
9+
10+
func PrintResults(out io.Writer, results api.CheckResults) {
11+
for _, result := range results {
12+
if result.State == api.StatePassed {
13+
fmt.Fprintf(out, "PASS ")
14+
}
15+
fmt.Fprintln(out, result.Summary)
16+
}
17+
}

internal/cmd/root.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99

1010
func NewDefaultDoctorCommand() *cobra.Command {
1111
rootCmd := &cobra.Command{
12-
Use: "coder-doctor",
13-
Args: cobra.ExactArgs(1),
12+
Use: "coder-doctor",
13+
Short: "coder-doctor checks compatibility with Coder",
14+
Long: `coder-doctor is a tool for analyzing that Coder's dependencies satisfy our requirements.`,
15+
Args: cobra.ExactArgs(1),
1416
}
1517
rootCmd.AddCommand(
1618
version.NewCommand(),

0 commit comments

Comments
 (0)