Skip to content

Commit d60f34e

Browse files
Oded-Bhnnsgstfssn
andauthored
Split too big comment per cluster (#22)
* Split too big comment per cluster If a "regular" aggregate diff can't fit in a GH comment create one comment per cluster. There's still a fallback to concise diff (just lists changed objects) for extreme cases * Move logging out of executeTemplate to higher up the stack Create a new "testable" generateArgoCdDiffComments function to generate all the comments content Comment all the comments * Add test for 3 "levels" of comments * Apply suggestions from code review Co-authored-by: Hannes Gustafsson <[email protected]>
1 parent bbe49ea commit d60f34e

7 files changed

+216
-30
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ clean:
2525

2626
.PHONY: test
2727
test: $(VENDOR_DIR)
28-
go test -v -timeout 30s ./...
28+
TEMPLATES_PATH=../../../templates/ go test -v -timeout 30s ./...
2929

internal/pkg/githubapi/github.go

+65-26
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ import (
3030

3131
const githubCommentMaxSize = 65536
3232

33+
type DiffCommentData struct {
34+
DiffOfChangedComponents []argocd.DiffResult
35+
HasSyncableComponents bool
36+
BranchName string
37+
Header string
38+
}
39+
3340
type promotionInstanceMetaData struct {
3441
SourcePath string `json:"sourcePath"`
3542
TargetPaths []string `json:"targetPaths"`
@@ -169,40 +176,29 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr
169176
}
170177

171178
if len(diffOfChangedComponents) > 0 {
172-
diffCommentData := struct {
173-
DiffOfChangedComponents []argocd.DiffResult
174-
HasSyncableComponens bool
175-
BranchName string
176-
}{
179+
diffCommentData := DiffCommentData{
177180
DiffOfChangedComponents: diffOfChangedComponents,
178181
BranchName: ghPrClientDetails.Ref,
179182
}
180183

181184
for _, componentPath := range componentPathList {
182185
if isSyncFromBranchAllowedForThisPath(config.Argocd.AllowSyncfromBranchPathRegex, componentPath) {
183-
diffCommentData.HasSyncableComponens = true
186+
diffCommentData.HasSyncableComponents = true
184187
break
185188
}
186189
}
187-
188-
err, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "argoCdDiff", "argoCD-diff-pr-comment.gotmpl", diffCommentData)
190+
comments, err := generateArgoCdDiffComments(diffCommentData, githubCommentMaxSize)
189191
if err != nil {
190192
prHandleError = err
191-
ghPrClientDetails.PrLogger.Errorf("Failed to generate ArgoCD diff comment template: err=%s\n", err)
192-
} else if len(templateOutput) > githubCommentMaxSize {
193-
ghPrClientDetails.PrLogger.Warnf("Diff comment is too large (%d bytes), using concise template", len(templateOutput))
194-
err, templateOutput = executeTemplate(ghPrClientDetails.PrLogger, "argoCdDiffConcise", "argoCD-diff-pr-comment-concise.gotmpl", diffCommentData)
193+
ghPrClientDetails.PrLogger.Errorf("Failed to comment ArgoCD diff: err=%s\n", err)
194+
}
195+
for _, comment := range comments {
196+
err = commentPR(ghPrClientDetails, comment)
195197
if err != nil {
196198
prHandleError = err
197-
ghPrClientDetails.PrLogger.Errorf("Failed to generate ArgoCD diff comment template: err=%s\n", err)
199+
ghPrClientDetails.PrLogger.Errorf("Failed to comment on PR: err=%s\n", err)
198200
}
199201
}
200-
201-
err = commentPR(ghPrClientDetails, templateOutput)
202-
if err != nil {
203-
prHandleError = err
204-
ghPrClientDetails.PrLogger.Errorf("Failed to comment ArgoCD diff: err=%s\n", err)
205-
}
206202
} else {
207203
ghPrClientDetails.PrLogger.Debugf("Diff not find affected ArogCD apps")
208204
}
@@ -230,6 +226,47 @@ func HandlePREvent(eventPayload *github.PullRequestEvent, ghPrClientDetails GhPr
230226
}
231227
}
232228

229+
func generateArgoCdDiffComments(diffCommentData DiffCommentData, githubCommentMaxSize int) (comments []string, err error) {
230+
err, templateOutput := executeTemplate("argoCdDiff", "argoCD-diff-pr-comment.gotmpl", diffCommentData)
231+
if err != nil {
232+
return nil, fmt.Errorf("failed to generate ArgoCD diff comment template: %w", err)
233+
}
234+
235+
// Happy path, the diff comment is small enough to be posted in one comment
236+
if len(templateOutput) < githubCommentMaxSize {
237+
comments = append(comments, templateOutput)
238+
return comments, nil
239+
}
240+
241+
// If the diff comment is too large, we'll split it into multiple comments, one per component
242+
totalComponents := len(diffCommentData.DiffOfChangedComponents)
243+
for i, singleComponentDiff := range diffCommentData.DiffOfChangedComponents {
244+
componentTemplateData := diffCommentData
245+
componentTemplateData.DiffOfChangedComponents = []argocd.DiffResult{singleComponentDiff}
246+
componentTemplateData.Header = fmt.Sprintf("Component %d/%d: %s (Split for comment size)", i+1, totalComponents, singleComponentDiff.ComponentPath)
247+
err, templateOutput := executeTemplate("argoCdDiff", "argoCD-diff-pr-comment.gotmpl", componentTemplateData)
248+
if err != nil {
249+
return nil, fmt.Errorf("failed to generate ArgoCD diff comment template: %w", err)
250+
}
251+
252+
// Even per component comments can be too large, in that case we'll just use the concise template
253+
// Somewhat Happy path, the per-component diff comment is small enough to be posted in one comment
254+
if len(templateOutput) < githubCommentMaxSize {
255+
comments = append(comments, templateOutput)
256+
continue
257+
}
258+
259+
// now we don't have much choice, this is the saddest path, we'll use the concise template
260+
err, templateOutput = executeTemplate("argoCdDiffConcise", "argoCD-diff-pr-comment-concise.gotmpl", componentTemplateData)
261+
if err != nil {
262+
return comments, fmt.Errorf("failed to generate ArgoCD diff comment template: %w", err)
263+
}
264+
comments = append(comments, templateOutput)
265+
}
266+
267+
return comments, nil
268+
}
269+
233270
// ReciveEventFile this one is similar to ReciveWebhook but it's used for CLI triggering, i simulates a webhook event to use the same code path as the webhook handler.
234271
func ReciveEventFile(eventType string, eventFilePath string, mainGhClientCache *lru.Cache[string, GhClientPair], prApproverGhClientCache *lru.Cache[string, GhClientPair]) {
235272
log.Infof("Event type: %s", eventType)
@@ -449,21 +486,23 @@ func handleCommentPrEvent(ghPrClientDetails GhPrClientDetails, ce *github.IssueC
449486
}
450487

451488
func commentPlanInPR(ghPrClientDetails GhPrClientDetails, promotions map[string]PromotionInstance) {
452-
_, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "dryRunMsg", "dry-run-pr-comment.gotmpl", promotions)
489+
err, templateOutput := executeTemplate("dryRunMsg", "dry-run-pr-comment.gotmpl", promotions)
490+
if err != nil {
491+
ghPrClientDetails.PrLogger.Errorf("Failed to generate dry-run comment template: err=%s\n", err)
492+
return
493+
}
453494
_ = commentPR(ghPrClientDetails, templateOutput)
454495
}
455496

456-
func executeTemplate(logger *log.Entry, templateName string, templateFile string, data interface{}) (error, string) {
497+
func executeTemplate(templateName string, templateFile string, data interface{}) (error, string) {
457498
var templateOutput bytes.Buffer
458499
messageTemplate, err := template.New(templateName).ParseFiles(getEnv("TEMPLATES_PATH", "templates/") + templateFile)
459500
if err != nil {
460-
logger.Errorf("Failed to parse template: err=%v", err)
461-
return err, ""
501+
return fmt.Errorf("failed to parse template: %w", err), ""
462502
}
463503
err = messageTemplate.ExecuteTemplate(&templateOutput, templateName, data)
464504
if err != nil {
465-
logger.Errorf("Failed to execute template: err=%v", err)
466-
return err, ""
505+
return fmt.Errorf("failed to execute template: %w", err), ""
467506
}
468507
return nil, templateOutput.String()
469508
}
@@ -594,7 +633,7 @@ func handleMergedPrEvent(ghPrClientDetails GhPrClientDetails, prApproverGithubCl
594633
templateData := map[string]interface{}{
595634
"prNumber": *pull.Number,
596635
}
597-
err, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "autoMerge", "auto-merge-comment.gotmpl", templateData)
636+
err, templateOutput := executeTemplate("autoMerge", "auto-merge-comment.gotmpl", templateData)
598637
if err != nil {
599638
return err
600639
}

internal/pkg/githubapi/github_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package githubapi
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"os"
67
"testing"
78

@@ -161,6 +162,69 @@ func TestIsSyncFromBranchAllowedForThisPath(t *testing.T) {
161162
}
162163
}
163164

165+
func TestGenerateArgoCdDiffComments(t *testing.T) {
166+
t.Parallel()
167+
tests := map[string]struct {
168+
diffCommentDataTestDataFileName string
169+
expectedNumberOfComments int
170+
maxCommentLength int
171+
}{
172+
"All cluster diffs fit in one comment": {
173+
diffCommentDataTestDataFileName: "./testdata/diff_comment_data_test.json",
174+
expectedNumberOfComments: 1,
175+
maxCommentLength: 65535,
176+
},
177+
"Split diffs, one cluster per comment": {
178+
diffCommentDataTestDataFileName: "./testdata/diff_comment_data_test.json",
179+
expectedNumberOfComments: 3,
180+
maxCommentLength: 1000,
181+
},
182+
"Split diffs, but maxCommentLength is very small so need to use the concise template": {
183+
diffCommentDataTestDataFileName: "./testdata/diff_comment_data_test.json",
184+
expectedNumberOfComments: 3,
185+
maxCommentLength: 600,
186+
},
187+
}
188+
189+
for name, tc := range tests {
190+
tc := tc // capture range variable
191+
name := name
192+
t.Run(name, func(t *testing.T) {
193+
t.Parallel()
194+
var diffCommentData DiffCommentData
195+
readJSONFromFile(t, tc.diffCommentDataTestDataFileName, &diffCommentData)
196+
197+
result, err := generateArgoCdDiffComments(diffCommentData, tc.maxCommentLength)
198+
if err != nil {
199+
t.Errorf("Error generating diff comments: %s", err)
200+
}
201+
if len(result) != tc.expectedNumberOfComments {
202+
t.Errorf("%s: Expected number of comments to be %v, got %v", name, tc.expectedNumberOfComments, len(result))
203+
}
204+
for _, comment := range result {
205+
if len(comment) > tc.maxCommentLength {
206+
t.Errorf("%s: Expected comment length to be less than %d, got %d", name, tc.maxCommentLength, len(comment))
207+
}
208+
}
209+
})
210+
}
211+
}
212+
213+
func readJSONFromFile(t *testing.T, filename string, data interface{}) {
214+
t.Helper()
215+
// Read the JSON from the file
216+
jsonData, err := os.ReadFile(filename)
217+
if err != nil {
218+
t.Fatalf("Error loading test data file: %s", err)
219+
}
220+
221+
// Unserialize the JSON into the provided struct
222+
err = json.Unmarshal(jsonData, data)
223+
if err != nil {
224+
t.Fatalf("Error unmarshalling JSON: %s", err)
225+
}
226+
}
227+
164228
func TestPrBody(t *testing.T) {
165229
t.Parallel()
166230
keys := []int{1, 2, 3}

internal/pkg/githubapi/promotion.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func DetectDrift(ghPrClientDetails GhPrClientDetails) error {
7676
}
7777
}
7878
if len(diffOutputMap) != 0 {
79-
err, templateOutput := executeTemplate(ghPrClientDetails.PrLogger, "driftMsg", "drift-pr-comment.gotmpl", diffOutputMap)
79+
err, templateOutput := executeTemplate("driftMsg", "drift-pr-comment.gotmpl", diffOutputMap)
8080
if err != nil {
8181
return err
8282
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
{
3+
"DiffOfChangedComponents": [
4+
{
5+
"ComponentPath": "clusters/playground/aws/eu-central-1/v1/special-delivery/ssllab-test/ssllab-test",
6+
"ArgoCdAppName": "temp-ssllab-test-plg-aws-eu-central1-v1",
7+
"ArgoCdAppURL": "https://argocd-lab.example.com/applications/temp-ssllab-test-plg-aws-eu-central1-v1",
8+
"DiffElements": [
9+
{
10+
"ObjectGroup": "",
11+
"ObjectName": "ssllabs-exporter",
12+
"ObjectKind": "Service",
13+
"ObjectNamespace": "",
14+
"Diff": " (*unstructured.Unstructured)(\n- \tnil,\n+ \t\u0026{\n+ \t\tObject: map[string]any{\n+ \t\t\t\"apiVersion\": string(\"v1\"),\n+ \t\t\t\"kind\": string(\"Service\"),\n+ \t\t\t\"metadata\": map[string]any{\"labels\": map[string]any{...}, \"name\": string(\"ssllabs-exporter\")},\n+ \t\t\t\"spec\": map[string]any{\n+ \t\t\t\t\"ports\": []any{...},\n+ \t\t\t\t\"selector\": map[string]any{...},\n+ \t\t\t\t\"type\": string(\"ClusterIP\"),\n+ \t\t\t},\n+ \t\t},\n+ \t},\n )\n"
15+
},
16+
{
17+
"ObjectGroup": "apps",
18+
"ObjectName": "ssllabs-exporter",
19+
"ObjectKind": "Deployment",
20+
"ObjectNamespace": "",
21+
"Diff": " (*unstructured.Unstructured)(\n- \tnil,\n+ \t\u0026{\n+ \t\tObject: map[string]any{\n+ \t\t\t\"apiVersion\": string(\"apps/v1\"),\n+ \t\t\t\"kind\": string(\"Deployment\"),\n+ \t\t\t\"metadata\": map[string]any{\"labels\": map[string]any{...}, \"name\": string(\"ssllabs-exporter\")},\n+ \t\t\t\"spec\": map[string]any{\n+ \t\t\t\t\"replicas\": int64(2),\n+ \t\t\t\t\"selector\": map[string]any{...},\n+ \t\t\t\t\"template\": map[string]any{...},\n+ \t\t\t},\n+ \t\t},\n+ \t},\n )\n"
22+
}
23+
],
24+
"HasDiff": true,
25+
"DiffError": null,
26+
"AppWasTemporarilyCreated": false
27+
},
28+
{
29+
"ComponentPath": "clusters/playground/aws/eu-central-1/v2/special-delivery/ssllab-test/ssllab-test",
30+
"ArgoCdAppName": "temp-ssllab-test-plg-aws-eu-central1-v2",
31+
"ArgoCdAppURL": "https://argocd-lab.example.com/applications/temp-ssllab-test-plg-aws-eu-central1-v1",
32+
"DiffElements": [
33+
{
34+
"ObjectGroup": "",
35+
"ObjectName": "ssllabs-exporter",
36+
"ObjectKind": "Service",
37+
"ObjectNamespace": "",
38+
"Diff": " (*unstructured.Unstructured)(\n- \tnil,\n+ \t\u0026{\n+ \t\tObject: map[string]any{\n+ \t\t\t\"apiVersion\": string(\"v1\"),\n+ \t\t\t\"kind\": string(\"Service\"),\n+ \t\t\t\"metadata\": map[string]any{\"labels\": map[string]any{...}, \"name\": string(\"ssllabs-exporter\")},\n+ \t\t\t\"spec\": map[string]any{\n+ \t\t\t\t\"ports\": []any{...},\n+ \t\t\t\t\"selector\": map[string]any{...},\n+ \t\t\t\t\"type\": string(\"ClusterIP\"),\n+ \t\t\t},\n+ \t\t},\n+ \t},\n )\n"
39+
},
40+
{
41+
"ObjectGroup": "apps",
42+
"ObjectName": "ssllabs-exporter",
43+
"ObjectKind": "Deployment",
44+
"ObjectNamespace": "",
45+
"Diff": " (*unstructured.Unstructured)(\n- \tnil,\n+ \t\u0026{\n+ \t\tObject: map[string]any{\n+ \t\t\t\"apiVersion\": string(\"apps/v1\"),\n+ \t\t\t\"kind\": string(\"Deployment\"),\n+ \t\t\t\"metadata\": map[string]any{\"labels\": map[string]any{...}, \"name\": string(\"ssllabs-exporter\")},\n+ \t\t\t\"spec\": map[string]any{\n+ \t\t\t\t\"replicas\": int64(2),\n+ \t\t\t\t\"selector\": map[string]any{...},\n+ \t\t\t\t\"template\": map[string]any{...},\n+ \t\t\t},\n+ \t\t},\n+ \t},\n )\n"
46+
}
47+
],
48+
"HasDiff": true,
49+
"DiffError": null,
50+
"AppWasTemporarilyCreated": false
51+
},
52+
{
53+
"ComponentPath": "clusters/playground/aws/eu-central-1/v3/special-delivery/ssllab-test/ssllab-test",
54+
"ArgoCdAppName": "temp-ssllab-test-plg-aws-eu-central1-v3",
55+
"ArgoCdAppURL": "https://argocd-lab.example.com/applications/temp-ssllab-test-plg-aws-eu-central1-v1",
56+
"DiffElements": [
57+
{
58+
"ObjectGroup": "",
59+
"ObjectName": "ssllabs-exporter",
60+
"ObjectKind": "Service",
61+
"ObjectNamespace": "",
62+
"Diff": " (*unstructured.Unstructured)(\n- \tnil,\n+ \t\u0026{\n+ \t\tObject: map[string]any{\n+ \t\t\t\"apiVersion\": string(\"v1\"),\n+ \t\t\t\"kind\": string(\"Service\"),\n+ \t\t\t\"metadata\": map[string]any{\"labels\": map[string]any{...}, \"name\": string(\"ssllabs-exporter\")},\n+ \t\t\t\"spec\": map[string]any{\n+ \t\t\t\t\"ports\": []any{...},\n+ \t\t\t\t\"selector\": map[string]any{...},\n+ \t\t\t\t\"type\": string(\"ClusterIP\"),\n+ \t\t\t},\n+ \t\t},\n+ \t},\n )\n"
63+
},
64+
{
65+
"ObjectGroup": "apps",
66+
"ObjectName": "ssllabs-exporter",
67+
"ObjectKind": "Deployment",
68+
"ObjectNamespace": "",
69+
"Diff": " (*unstructured.Unstructured)(\n- \tnil,\n+ \t\u0026{\n+ \t\tObject: map[string]any{\n+ \t\t\t\"apiVersion\": string(\"apps/v1\"),\n+ \t\t\t\"kind\": string(\"Deployment\"),\n+ \t\t\t\"metadata\": map[string]any{\"labels\": map[string]any{...}, \"name\": string(\"ssllabs-exporter\")},\n+ \t\t\t\"spec\": map[string]any{\n+ \t\t\t\t\"replicas\": int64(2),\n+ \t\t\t\t\"selector\": map[string]any{...},\n+ \t\t\t\t\"template\": map[string]any{...},\n+ \t\t\t},\n+ \t\t},\n+ \t},\n )\n"
70+
}
71+
],
72+
"HasDiff": true,
73+
"DiffError": null,
74+
"AppWasTemporarilyCreated": false
75+
}
76+
],
77+
"HasSyncableComponents": false,
78+
"BranchName": "promotions/284-simulate-error-5c159151017f",
79+
"Header": ""
80+
}

templates/argoCD-diff-pr-comment-concise.gotmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ It will not be present in ArgoCD UI for more than a few seconds and it can not b
3737

3838
{{- end }}
3939

40-
{{- if .HasSyncableComponens }}
40+
{{- if .HasSyncableComponents }}
4141

4242
- [ ] <!-- telefonistka-argocd-branch-sync --> Set ArgoCD apps Target Revision to `{{ .BranchName }}`
4343

templates/argoCD-diff-pr-comment.gotmpl

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{{define "argoCdDiff"}}
2+
{{ if .Header }}
3+
{{ .Header }}
4+
{{- end}}
25
Diff of ArgoCD applications:
36
{{ range $appDiffResult := .DiffOfChangedComponents }}
47

@@ -44,7 +47,7 @@ It will not be present in ArgoCD UI for more than a few seconds and it can not b
4447

4548
{{- end }}
4649

47-
{{- if .HasSyncableComponens }}
50+
{{- if .HasSyncableComponents }}
4851

4952
- [ ] <!-- telefonistka-argocd-branch-sync --> Set ArgoCD apps Target Revision to `{{ .BranchName }}`
5053

0 commit comments

Comments
 (0)