Skip to content

Commit 1c2821a

Browse files
committed
feat: Add listen-helmfile-sync
1 parent 823e466 commit 1c2821a

19 files changed

+252
-53
lines changed

README.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Diff without `collapse-helm-diff`:
2222
After hiding `image: docker.foo.fr...` diffs:
2323

2424
```
25+
# Don't forget to set -o pipefail (in bash e.g.) to pervent helm diff errors from being masked.
2526
- helm diff | ./cicdbox collapse-helm-diff '^\s+-?image: docker\.foo\.fr'
2627
```
2728

@@ -60,8 +61,8 @@ metadata:
6061
name: sh.helm.release.v1.SELECTED-RELEASE.v1
6162
...
6263
annotations:
63-
cicdbox/releaser: $GITLAB_USER_LOGIN
64-
cicdbox/gitlab-ci-pipeline-url: $CI_PIPELINE_URL
64+
cicdbox/releaser: username
65+
cicdbox/gitlab-ci-pipeline-url: https://gitlab.foo.bar/foo/bar/foo/-/pipelines/173171
6566
...
6667
```
6768

@@ -70,3 +71,19 @@ This can help understanding how a release ended up there, how to fix it, whom to
7071
`./cicdbox annotate-helmfile-releases --help` for more details.
7172

7273
Supposes `helmfile` and `kubectl` are available.
74+
75+
### listen-helm-debug
76+
77+
Get Helm relevant `--debug` logs and hide the rest in a collapsed section, these logs can tell you for which resources Helm is waiting and provide users a fast feedback. No more `Help! my deploy job is stuck`
78+
79+
Example:
80+
81+
```
82+
# Don't forget to set -o pipefail (in bash e.g.) to pervent helm diff errors from being masked.
83+
# Don't forget to set the --debug flag
84+
- helmfile --debug sync | ./cicdbox listen-helm-debug
85+
```
86+
87+
The output would look like:
88+
89+
![listen_helm_output](./images/listen_helm_output.png)

annotate_helmfile_releases_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func TestAnnotateHelmfileReleases(t *testing.T) {
3737
TEST SUITE: None
3838
3939
`, []namespacedSecret{{namespace: "foo", secretName: "sh.helm.release.v1.bar.v5"}}},
40+
{"no release", ``, []namespacedSecret(nil)},
4041
}
4142
for _, tc := range tt {
4243
t.Run(tc.description, func(t *testing.T) {

collapse_helm_diff.go

+10-24
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package main
33
import (
44
"bufio"
55
"flag"
6-
"fmt"
76
"io"
87
"log"
98
"os"
@@ -13,18 +12,12 @@ import (
1312
)
1413

1514
const (
16-
COLOR_RESET = "[0m"
17-
RESOURCE_DIFF = "[0;33m"
15+
COLOR_RESET = "\x1b[0m"
16+
RESOURCE_DIFF = "\x1b[0;33m"
1817
ELLIPSIS = "..."
19-
20-
COLLAPSED_SECTION_END = `\e[0Ksection_end:1664723888:section\r\e[0K`
2118
)
2219

23-
var DIFF_COLOR_PREFIXES = []string{"-", "+"}
24-
25-
func collapsedSectionStart(header string) string {
26-
return fmt.Sprintf(`\e[0Ksection_start:1664723888:section[collapsed=true]\r\e[0K%s`, header)
27-
}
20+
var DIFF_COLOR_PREFIXES = []string{"\x1b[0;31m-", "\x1b[0;32m+"}
2821

2922
func matchesPattern(str string, patterns []*regexp.Regexp) bool {
3023
for _, pattern := range patterns {
@@ -67,20 +60,16 @@ func echo(w io.Writer, str string) {
6760
}
6861
}
6962

70-
func collapse(w io.Writer, diffs []string, patterns []*regexp.Regexp) {
71-
if len(diffs) == 0 {
63+
func collapse(w io.Writer, lines []string, patterns []*regexp.Regexp) {
64+
if len(lines) == 0 {
7265
return
7366
}
74-
header, body := diffs[0], diffs[1:]
67+
header, body := lines[0], lines[1:]
7568
if matchesPattern(uncolor(header), patterns) {
7669
header = ELLIPSIS
77-
body = diffs
70+
body = lines
7871
}
79-
echo(w, collapsedSectionStart(header))
80-
for _, line := range body {
81-
echo(w, line)
82-
}
83-
echo(w, COLLAPSED_SECTION_END)
72+
echoCollapsed(w, header, body)
8473
}
8574

8675
func processResourceDiff(w io.Writer, diffs []string, patterns []*regexp.Regexp) {
@@ -125,10 +114,7 @@ func collapseHelmDiff(input *bufio.Scanner, output io.Writer, args []string) {
125114
}
126115
patterns := make([]*regexp.Regexp, len(regexes))
127116
for i, regex := range regexes {
128-
pattern, err := regexp.Compile(regex)
129-
if err != nil {
130-
log.Fatal(err)
131-
}
117+
pattern := regexp.MustCompile(regex)
132118
patterns[i] = pattern
133119
}
134120

@@ -144,7 +130,7 @@ func collapseHelmDiff(input *bufio.Scanner, output io.Writer, args []string) {
144130
processResourceDiff(output, resourceDiff, patterns)
145131

146132
if err := input.Err(); err != nil {
147-
log.Println(err)
133+
log.Fatal(err)
148134
}
149135
}
150136

collapse_helm_diff_test.go

+28-26
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,13 @@ import (
99
"testing"
1010
)
1111

12-
func check(e error) {
13-
if e != nil {
14-
panic(e)
15-
}
16-
}
17-
18-
func absolutePath(name string) string {
19-
return fmt.Sprintf("files/collapse_helm_diff/%s", name)
12+
func absoluteTestPath(name string) string {
13+
return fmt.Sprintf("files/%s", name)
2014
}
2115

2216
func openFile(name string) *os.File {
2317
file, err := os.Open(name)
24-
check(err)
18+
panicOnError(err)
2519
return file
2620
}
2721

@@ -31,6 +25,20 @@ func echoLines(input *bufio.Scanner, output io.Writer) {
3125
}
3226
}
3327

28+
func loadTestFiles(inputFilePath, expectFilePath string) (*bufio.Scanner, strings.Builder) {
29+
// input (we d not close it explicitly)
30+
inputFile := openFile(inputFilePath)
31+
input := bufio.NewScanner(inputFile)
32+
33+
// expected output
34+
expectFile := openFile(expectFilePath)
35+
defer expectFile.Close()
36+
var expect strings.Builder
37+
echoLines(bufio.NewScanner(expectFile), &expect)
38+
39+
return input, expect
40+
}
41+
3442
func TestEcho(t *testing.T) {
3543
input := "foo-bar"
3644
expect := fmt.Sprintf("%s\n", input)
@@ -41,36 +49,30 @@ func TestEcho(t *testing.T) {
4149
}
4250
}
4351

52+
func assertEqual(t *testing.T, expect, output strings.Builder) {
53+
if expect.String() != output.String() {
54+
t.Errorf("got %s but expected %s", output.String(), expect.String())
55+
}
56+
}
57+
4458
func TestCollapseHelmDiff(t *testing.T) {
4559
tt := []struct {
4660
description string
4761
diff string
4862
args []string
4963
expect string
5064
}{
51-
{"", absolutePath("diff_1.in"), []string{`bar`}, absolutePath("diff_1.outt")},
52-
{"", absolutePath("diff_2.in"), []string{`^\s+-?image: docker\.foo\.fr`}, absolutePath("diff_2.outt")},
53-
{"", absolutePath("diff_3.in"), []string{`^\s+-?image: docker\.foo\.fr`}, absolutePath("diff_3.outt")},
65+
{"no diff", absoluteTestPath("diff_1.in"), []string{`bar`}, absoluteTestPath("diff_1.outt")},
66+
{"diff to callapse", absoluteTestPath("diff_2.in"), []string{`^\s+-?image: docker\.foo\.fr`}, absoluteTestPath("diff_2.outt")},
67+
{"diff to keep", absoluteTestPath("diff_3.in"), []string{`^\s+-?image: docker\.foo\.fr`}, absoluteTestPath("diff_3.outt")},
5468
}
5569
for _, tc := range tt {
5670
t.Run(tc.description, func(t *testing.T) {
57-
// input
58-
diffInFile := openFile(tc.diff)
59-
defer diffInFile.Close()
60-
input := bufio.NewScanner(diffInFile)
61-
62-
// expected output
63-
rawExpectInFile := openFile(tc.expect)
64-
defer rawExpectInFile.Close()
65-
var expect strings.Builder
66-
echoLines(bufio.NewScanner(rawExpectInFile), &expect)
71+
input, expect := loadTestFiles(tc.diff, tc.expect)
6772

68-
// run
6973
var output strings.Builder
7074
collapseHelmDiff(input, &output, tc.args)
71-
if expect.String() != output.String() {
72-
t.Errorf("got %s but expected %s", output.String(), expect.String())
73-
}
75+
assertEqual(t, expect, output)
7476
})
7577
}
7678
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

files/helm_debug_1.in

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
...
2+
foo:
3+
nodeAffinity:
4+
requiredDuringSchedulingIgnoredDuringExecution:
5+
nodeSelectorTerms:
6+
- matchExpressions:
7+
- key: Preemptible
8+
operator: In
9+
values:
10+
- "true"
11+
podAnnotations:
12+
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
13+
tolerations:
14+
- effect: NoSchedule
15+
key: Preemptible
16+
value: "true"
17+
Successfully generated the value file at values/per-environment/review-values.yaml.gotmpl. produced:
18+
worker:
19+
replicaCount: 0
20+
Upgrading release=release, chart=chart
21+
exec: helm upgrade --install --debug ...
22+
helm:vaGzI> history.go:56: [debug] getting history for release release
23+
helm:vaGzI> upgrade.go:142: [debug] preparing upgrade for release
24+
helm:vaGzI> upgrade.go:499: [debug] resetting values to the chart's original version
25+
helm:vaGzI> coalesce.go:175: warning: skipped value for etcd-patroni.tolerations: Not a table.
26+
27+
helm:vaGzI> upgrade.go:150: [debug] performing update for release
28+
helm:vaGzI> upgrade.go:322: [debug] creating upgraded release for release
29+
helm:vaGzI> client.go:310: [debug] Starting delete for "release-ephemeral" ServiceAccount
30+
helm:vaGzI> client.go:128: [debug] creating 1 resource(s)
31+
helm:vaGzI> client.go:310: [debug] Starting delete for "release-ephemeral" Secret
32+
...

files/helm_debug_1.outt

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
\x1b[0;33mcicdbox: Only Helm debug logs will be displayed...\x1b[0m
2+
getting history for release release
3+
preparing upgrade for release
4+
resetting values to the chart's original version
5+
performing update for release
6+
creating upgraded release for release
7+
Starting delete for "release-ephemeral" ServiceAccount
8+
creating 1 resource(s)
9+
Starting delete for "release-ephemeral" Secret
10+
\e[0Ksection_start:1664723888:section[collapsed=true]\r\e[0K\x1b[0;33mExpand to see the entire output.\x1b[0m
11+
...
12+
foo:
13+
nodeAffinity:
14+
requiredDuringSchedulingIgnoredDuringExecution:
15+
nodeSelectorTerms:
16+
- matchExpressions:
17+
- key: Preemptible
18+
operator: In
19+
values:
20+
- "true"
21+
podAnnotations:
22+
cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
23+
tolerations:
24+
- effect: NoSchedule
25+
key: Preemptible
26+
value: "true"
27+
Successfully generated the value file at values/per-environment/review-values.yaml.gotmpl. produced:
28+
worker:
29+
replicaCount: 0
30+
Upgrading release=release, chart=chart
31+
exec: helm upgrade --install --debug ...
32+
helm:vaGzI> history.go:56: [debug] getting history for release release
33+
helm:vaGzI> upgrade.go:142: [debug] preparing upgrade for release
34+
helm:vaGzI> upgrade.go:499: [debug] resetting values to the chart's original version
35+
helm:vaGzI> coalesce.go:175: warning: skipped value for etcd-patroni.tolerations: Not a table.
36+
37+
helm:vaGzI> upgrade.go:150: [debug] performing update for release
38+
helm:vaGzI> upgrade.go:322: [debug] creating upgraded release for release
39+
helm:vaGzI> client.go:310: [debug] Starting delete for "release-ephemeral" ServiceAccount
40+
helm:vaGzI> client.go:128: [debug] creating 1 resource(s)
41+
helm:vaGzI> client.go:310: [debug] Starting delete for "release-ephemeral" Secret
42+
...
43+
\e[0Ksection_end:1664723888:section\r\e[0K

files/helm_debug_2.in

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
foo
2+
bar

files/helm_debug_2.outt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
\x1b[0;33mcicdbox: Only Helm debug logs will be displayed...\x1b[0m
2+
\e[0Ksection_start:1664723888:section[collapsed=true]\r\e[0K\x1b[0;33mExpand to see the entire output.\x1b[0m
3+
foo
4+
bar
5+
\e[0Ksection_end:1664723888:section\r\e[0K

images/listen_helm_output.png

163 KB
Loading

listen_helm_debug.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"flag"
6+
"io"
7+
"log"
8+
"os"
9+
"regexp"
10+
)
11+
12+
var debugLineRegex = regexp.MustCompile(`.+\[debug\]\s*(?P<debug_log>.*)$`)
13+
14+
// TODO: also warnings?
15+
func printIfDebugLine(output io.Writer, line []byte) {
16+
result := debugLineRegex.FindSubmatch(line)
17+
if len(result) > 0 {
18+
debugLog := result[len(result)-1]
19+
_, err := output.Write(append(debugLog, []byte("\n")...))
20+
panicOnError(err)
21+
}
22+
}
23+
24+
func listenHelmDebug(input *bufio.Scanner, output io.Writer, args []string) {
25+
flag := flag.NewFlagSet(LISTEN_HELM_DEBUG_CMD_NAME, flag.ExitOnError)
26+
err := flag.Parse(args)
27+
if err != nil {
28+
log.Fatal(err)
29+
}
30+
31+
_, err = output.Write([]byte("\x1b[0;33mcicdbox: Only Helm debug logs will be displayed...\x1b[0m\n"))
32+
panicOnError(err)
33+
34+
var outputToCollapse []string
35+
for input.Scan() {
36+
rawLine := input.Bytes()
37+
// Keep for the end
38+
outputToCollapse = append(outputToCollapse, string(rawLine))
39+
printIfDebugLine(output, rawLine)
40+
}
41+
echoCollapsed(output, "\x1b[0;33mExpand to see the entire output.\x1b[0m", outputToCollapse)
42+
43+
if err := input.Err(); err != nil {
44+
log.Fatal(err)
45+
}
46+
}
47+
48+
func ListenHelmDebug(args []string) {
49+
// read stdin
50+
input := bufio.NewScanner(os.Stdin)
51+
// write to stdout
52+
output := os.Stdout
53+
listenHelmDebug(input, output, args)
54+
}

listen_helm_debug_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestListenHelmDebug(t *testing.T) {
9+
tt := []struct {
10+
description string
11+
diff string
12+
expect string
13+
}{
14+
{"debug logs", absoluteTestPath("helm_debug_1.in"), absoluteTestPath("helm_debug_1.outt")},
15+
{"no debug logs", absoluteTestPath("helm_debug_2.in"), absoluteTestPath("helm_debug_2.outt")},
16+
}
17+
for _, tc := range tt {
18+
t.Run(tc.description, func(t *testing.T) {
19+
input, expect := loadTestFiles(tc.diff, tc.expect)
20+
21+
var output strings.Builder
22+
listenHelmDebug(input, &output, []string{})
23+
assertEqual(t, expect, output)
24+
})
25+
}
26+
}

0 commit comments

Comments
 (0)