Skip to content

Commit da7b7ae

Browse files
authored
Introduce doctor command (#32)
Signed-off-by: Jan Lohage <lohage@23technologies.cloud> Signed-off-by: Jens Schneider <jens.schneider.ac@posteo.de>
1 parent 0767f8c commit da7b7ae

16 files changed

Lines changed: 465 additions & 17 deletions

File tree

cmd/doctor.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3+
*/
4+
package cmd
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"github.com/23technologies/23kectl/pkg/check"
10+
"github.com/fluxcd/helm-controller/api/v2beta1"
11+
"github.com/fluxcd/kustomize-controller/api/v1beta2"
12+
"github.com/spf13/cobra"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
)
15+
16+
// installCmd represents the install command
17+
var doctorCmd = &cobra.Command{
18+
Use: "doctor",
19+
Short: "Check the status of a current 23ke installation",
20+
Long: `This command will print status messages for flux resources.
21+
22+
If e.g. a HelmRelease failed, the error message message including a hint
23+
will be printed.
24+
`,
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
27+
doctor()
28+
return nil
29+
},
30+
}
31+
32+
func init() {
33+
rootCmd.AddCommand(doctorCmd)
34+
doctorCmd.PersistentFlags().String("kubeconfig", "", "The KUBECONFIG of your base cluster")
35+
}
36+
37+
func doctor() {
38+
var checks []check.Check
39+
40+
hrList := &v2beta1.HelmReleaseList{}
41+
_ = check.KubeClient.List(context.TODO(), hrList, &client.ListOptions{Namespace: "flux-system"})
42+
43+
for _, hr := range hrList.Items {
44+
checks = append(checks, &check.HelmReleaseCheck{Name: hr.Name, Namespace: hr.Namespace})
45+
}
46+
47+
ksList := &v1beta2.KustomizationList{}
48+
_ = check.KubeClient.List(context.TODO(), ksList, &client.ListOptions{Namespace: "flux-system"})
49+
50+
for _, ks := range ksList.Items {
51+
checks = append(checks, &check.KustomizationCheck{Name: ks.Name, Namespace: ks.Namespace})
52+
}
53+
54+
fmt.Print("\033[H\033[2J")
55+
56+
for _, c := range checks {
57+
result := c.Run()
58+
59+
emoji := "⌛"
60+
61+
if result.IsError {
62+
emoji = "❌"
63+
} else if result.IsOkay {
64+
emoji = "✔️"
65+
}
66+
67+
fmt.Printf("%s %s status: %s\n", emoji, c.GetName(), result.Status)
68+
}
69+
70+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require (
2525
github.com/google/go-github/v36 v36.0.0
2626
github.com/itchyny/json2yaml v0.1.4
2727
github.com/minio/minio-go/v7 v7.0.45
28+
github.com/mitchellh/go-wordwrap v1.0.1
2829
github.com/mitchellh/mapstructure v1.5.0
2930
github.com/onsi/ginkgo/v2 v2.7.0
3031
github.com/onsi/gomega v1.26.0
@@ -114,7 +115,6 @@ require (
114115
github.com/minio/md5-simd v1.1.2 // indirect
115116
github.com/minio/sha256-simd v1.0.0 // indirect
116117
github.com/mitchellh/copystructure v1.0.0 // indirect
117-
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
118118
github.com/mitchellh/reflectwalk v1.0.0 // indirect
119119
github.com/moby/spdystream v0.2.0 // indirect
120120
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect

pkg/check/check.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package check
2+
3+
type Runnable interface {
4+
Run() *Result
5+
}
6+
7+
type WithHint interface {
8+
Hint() string
9+
}
10+
11+
type WithFix interface {
12+
Fix()
13+
}
14+
15+
type WithOnError interface {
16+
OnError()
17+
}
18+
19+
type WithName interface {
20+
GetName() string
21+
}
22+
23+
type Check interface {
24+
Runnable
25+
WithName
26+
}

pkg/check/hc-check.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package check
2+
3+
import (
4+
"context"
5+
v1 "github.com/fluxcd/source-controller/api/v1beta2"
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
)
8+
9+
type HelmChartsCheck struct {
10+
Name string
11+
Namespace string
12+
}
13+
14+
func (d *HelmChartsCheck) GetName() string {
15+
return d.Name
16+
}
17+
18+
func (d *HelmChartsCheck) Run() *Result {
19+
result := &Result{}
20+
21+
hc := &v1.HelmChart{}
22+
23+
err := KubeClient.Get(context.Background(), client.ObjectKey{
24+
Namespace: d.Namespace,
25+
Name: d.Name,
26+
}, hc)
27+
28+
if err != nil {
29+
result.IsError = true
30+
return result
31+
}
32+
33+
result.Status = getMessage(hc.Status.Conditions, "Ready")
34+
35+
if result.Status == "Applied revision" {
36+
result.IsError = false
37+
result.IsOkay = true
38+
} else if result.Status == "SOME DEFINITIVE ERROR" {
39+
result.IsError = true
40+
result.IsOkay = false
41+
}
42+
43+
return result
44+
}

pkg/check/hr-check.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package check
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
9+
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
10+
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
11+
"github.com/mitchellh/go-wordwrap"
12+
corev1 "k8s.io/api/core/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
)
15+
16+
type HelmReleaseCheck struct {
17+
Name string
18+
Namespace string
19+
}
20+
21+
func (d *HelmReleaseCheck) GetName() string {
22+
return d.Name
23+
}
24+
25+
func (d *HelmReleaseCheck) Run() *Result {
26+
result := &Result{}
27+
28+
hr := &helmv2.HelmRelease{}
29+
30+
err := KubeClient.Get(context.Background(), client.ObjectKey{
31+
Namespace: d.Namespace,
32+
Name: d.Name,
33+
}, hr)
34+
35+
if err != nil {
36+
result.IsError = true
37+
return result
38+
}
39+
40+
// define a slice of handler including a regexp and a function
41+
// if we find a match we process the event by an appropriate function
42+
// this is assumed to stay branchless in the future which enables easy extensibility
43+
// the order of processing is important as we prioritize the status messages
44+
type handler struct {
45+
regex *regexp.Regexp
46+
fn func(res *Result, matches []string)
47+
}
48+
49+
handlers := []handler{
50+
{
51+
regex: regexp.MustCompile("Helm test failed: pod (?P<podName>.*) failed"),
52+
fn: handeHelmTestError,
53+
},
54+
{
55+
regex: regexp.MustCompile("(install retries exhausted|upgrade retries exhausted|Helm install failed|Helm upgrade failed).*"),
56+
fn: func(res *Result, matches []string) {
57+
res.Status = prettify(matches[0])
58+
res.IsError = true
59+
res.IsOkay = false
60+
},
61+
},
62+
{
63+
regex: regexp.MustCompile("^HelmChart '(?P<namespace>.*)/(?P<name>.*)' is not ready$"),
64+
fn: handleHelmChartError,
65+
},
66+
{
67+
regex: regexp.MustCompile("Release reconciliation succeeded"),
68+
fn: func(res *Result, matches []string) {
69+
res.Status = prettify(matches[0])
70+
res.IsError = false
71+
res.IsOkay = true
72+
},
73+
},
74+
}
75+
76+
// iterate over status conditions in the helm releases
77+
// here all useful information about potential errors should be found
78+
for _, curHandler := range handlers {
79+
for _, condition := range hr.GetConditions() {
80+
matches := curHandler.regex.FindStringSubmatch(condition.Message)
81+
if matches != nil {
82+
curHandler.fn(result, matches)
83+
return result
84+
}
85+
}
86+
}
87+
88+
return result
89+
}
90+
91+
// handeHelmTestError ...
92+
func handeHelmTestError(res *Result, matches []string) {
93+
94+
// It seems controller-runtime does not allow to access the logs.
95+
// Use kubectl directly for the moment.
96+
test := KubeClientGo.CoreV1().Pods("garden").GetLogs(matches[1], &corev1.PodLogOptions{})
97+
logs, err := test.Do(context.Background()).Raw()
98+
log := string(logs)
99+
if err != nil {
100+
log = fmt.Sprintf("couldn't get pod logs: %s", err)
101+
}
102+
103+
// Do some easy formatting for the moment.
104+
// We should definitely look for some package doing the job in the end.
105+
const replacement = "\n > "
106+
var replacer = strings.NewReplacer(
107+
"\r\n", replacement,
108+
"\r", replacement,
109+
"\n", replacement,
110+
"\v", replacement,
111+
"\f", replacement,
112+
"\u0085", replacement,
113+
"\u2028", replacement,
114+
"\u2029", replacement,
115+
)
116+
117+
newline := "\n > "
118+
res.Status = matches[0] + newline + replacer.Replace(wordwrap.WrapString(strings.TrimSpace(log), 100))
119+
120+
res.IsError = true
121+
res.IsOkay = false
122+
}
123+
124+
func handleHelmChartError(res *Result, matches []string) {
125+
namespace := matches[1]
126+
name := matches[2]
127+
128+
hc := &sourcev1.HelmChart{}
129+
130+
err := KubeClient.Get(context.Background(), client.ObjectKey{
131+
Namespace: namespace,
132+
Name: name,
133+
}, hc)
134+
135+
status := matches[0]
136+
137+
if err != nil {
138+
status = status + ": " + err.Error()
139+
} else {
140+
hcReadyMessage := getMessage(hc.Status.Conditions, "Ready")
141+
status = status + ": " + hcReadyMessage
142+
}
143+
144+
res.Status = prettify(status)
145+
res.IsError = true
146+
res.IsOkay = false
147+
}
148+
149+
func prettify(message string) string {
150+
newline := "\n > "
151+
return strings.Replace(message, ": ", newline, -1)
152+
}

pkg/check/ks-check.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package check
2+
3+
import (
4+
"context"
5+
"github.com/fluxcd/kustomize-controller/api/v1beta2"
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
"strings"
8+
)
9+
10+
type KustomizationCheck struct {
11+
Name string
12+
Namespace string
13+
}
14+
15+
func (d *KustomizationCheck) GetName() string {
16+
return d.Name
17+
}
18+
19+
func (d *KustomizationCheck) Run() *Result {
20+
result := &Result{}
21+
22+
ks := &v1beta2.Kustomization{}
23+
24+
err := KubeClient.Get(context.Background(), client.ObjectKey{
25+
Namespace: d.Namespace,
26+
Name: d.Name,
27+
}, ks)
28+
29+
if err != nil {
30+
result.IsError = true
31+
return result
32+
}
33+
34+
result.Status = getMessage(ks.Status.Conditions, "Ready")
35+
36+
if strings.Contains(result.Status, "Applied revision") {
37+
result.IsError = false
38+
result.IsOkay = true
39+
} else if result.Status == "SOME DEFINITIVE ERROR" {
40+
result.IsError = true
41+
result.IsOkay = false
42+
}
43+
44+
return result
45+
}

pkg/check/state.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package check
2+
3+
type Result struct {
4+
IsError bool
5+
IsOkay bool
6+
Status string
7+
}

0 commit comments

Comments
 (0)