Skip to content

Commit 47cbbe1

Browse files
DjinnSGuillaume Leccese
authored andcommitted
feat: commit statuses and merge comment
Adds two related improvements to the pull request lifecycle: - Post a comment on the pull/merge request once Burrito applies the merged changes, reporting success or failure per affected layer (internal/controllers/terraformpullrequest/comment/apply.go). The TerraformPullRequest resource is kept alive after merge (annotated with the merge date instead of being deleted) so its controller can track layer applies and post that comment once they complete. - Report plan and apply status via the GitHub Commit Statuses API and GitLab External Commit Statuses (internal/controllers/terraformpullrequest/status, APIProvider.SetStatus in the github/gitlab/mock providers). The status is set to pending when a phase starts and updated to success/failure based on the actual outcome: - plan: pending when temp layers are created, success/failure once the comment is posted, based on whether every layer produced a plan. - apply: pending while waiting for the merged layers to apply, success/failure once they all have, based on LastApplyDate vs a failed TerraformRun created after the merge. Status updates are best-effort and never fail the reconciliation loop.
1 parent 1378bf1 commit 47cbbe1

24 files changed

Lines changed: 906 additions & 18 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ credentials/
5151

5252
test-repo/
5353
TIltfile
54+
.DS_Store

deploy/charts/burrito/values-dev.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ config:
33
runner:
44
image:
55
repository: burrito
6-
tag: "1783089222"
6+
tag: "1783690876"
77
pullPolicy: Never
88
datastore:
99
storage:
@@ -16,7 +16,7 @@ global:
1616
deployment:
1717
image:
1818
repository: burrito
19-
tag: "1783089222"
19+
tag: "1783690876"
2020
pullPolicy: Never
2121
tenants:
2222
- namespace:

internal/annotations/annotations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const (
1818
LastPlanRun string = "runner.terraform.padok.cloud/plan-run"
1919
Lock string = "runner.terraform.padok.cloud/lock"
2020

21+
MergedAt string = "webhook.terraform.padok.cloud/merged-at"
22+
MergeCommit string = "webhook.terraform.padok.cloud/merge-commit"
2123
LastBranchCommit string = "webhook.terraform.padok.cloud/branch-commit"
2224
LastBranchCommitDate string = "webhook.terraform.padok.cloud/branch-commit-date"
2325
LastRelevantCommit string = "webhook.terraform.padok.cloud/relevant-commit"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package comment
2+
3+
import (
4+
"bytes"
5+
"text/template"
6+
7+
_ "embed"
8+
)
9+
10+
var (
11+
//go:embed templates/apply.md
12+
applyTemplateRaw string
13+
applyTemplate = template.Must(template.New("apply-report").Parse(applyTemplateRaw))
14+
)
15+
16+
type ApplyReportedLayer struct {
17+
Path string
18+
Succeeded bool
19+
}
20+
21+
type ApplyComment struct {
22+
layers []ApplyReportedLayer
23+
}
24+
25+
func NewApplyComment(layers []ApplyReportedLayer) *ApplyComment {
26+
return &ApplyComment{layers: layers}
27+
}
28+
29+
func (c *ApplyComment) Generate(commit string) (string, error) {
30+
data := struct {
31+
Commit string
32+
Layers []ApplyReportedLayer
33+
}{
34+
Commit: commit,
35+
Layers: c.layers,
36+
}
37+
buf := bytes.NewBufferString("")
38+
err := applyTemplate.Execute(buf, data)
39+
if err != nil {
40+
return "", err
41+
}
42+
return buf.String(), nil
43+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## :burrito: Burrito Apply Report
2+
3+
{{ len .Layers }} layer(s) affected by this pull request have been applied on commit `{{ .Commit }}`.
4+
5+
{{ range .Layers }}
6+
### Layer {{ .Path }}
7+
8+
{{ if .Succeeded }}:white_check_mark: Apply succeeded.{{ else }}:x: Apply failed. Check the Burrito UI or runner logs for details.{{ end }}
9+
10+
{{ end }}

internal/controllers/terraformpullrequest/conditions.go

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

33
import (
4+
"context"
45
"time"
56

67
configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
@@ -107,6 +108,68 @@ func (r *Reconciler) AreLayersStillPlanning(pr *configv1alpha1.TerraformPullRequ
107108
return condition, false
108109
}
109110

111+
func (r *Reconciler) IsMerged(pr *configv1alpha1.TerraformPullRequest) (metav1.Condition, bool) {
112+
condition := metav1.Condition{
113+
Type: "IsMerged",
114+
ObservedGeneration: pr.GetObjectMeta().GetGeneration(),
115+
Status: metav1.ConditionUnknown,
116+
LastTransitionTime: metav1.NewTime(time.Now()),
117+
}
118+
_, ok := pr.Annotations[annotations.MergedAt]
119+
if ok {
120+
condition.Reason = "PullRequestMerged"
121+
condition.Message = "Pull request has been merged."
122+
condition.Status = metav1.ConditionTrue
123+
return condition, true
124+
}
125+
condition.Reason = "PullRequestNotMerged"
126+
condition.Message = "Pull request has not been merged yet."
127+
condition.Status = metav1.ConditionFalse
128+
return condition, false
129+
}
130+
131+
func (r *Reconciler) AreLayersApplied(ctx context.Context, pr *configv1alpha1.TerraformPullRequest, mainLayers []configv1alpha1.TerraformLayer) (metav1.Condition, bool, []LayerApplyResult) {
132+
condition := metav1.Condition{
133+
Type: "AreLayersApplied",
134+
ObservedGeneration: pr.GetObjectMeta().GetGeneration(),
135+
Status: metav1.ConditionUnknown,
136+
LastTransitionTime: metav1.NewTime(time.Now()),
137+
}
138+
139+
mergedAtStr := pr.Annotations[annotations.MergedAt]
140+
mergedAt, err := time.Parse(time.RFC3339, mergedAtStr)
141+
if err != nil {
142+
condition.Reason = "InvalidMergedAt"
143+
condition.Message = "Could not parse merged-at annotation, this is likely a bug."
144+
condition.Status = metav1.ConditionFalse
145+
return condition, false, nil
146+
}
147+
148+
if len(mainLayers) == 0 {
149+
condition.Reason = "NoAffectedLayers"
150+
condition.Message = "No layers are affected by this pull request."
151+
condition.Status = metav1.ConditionTrue
152+
return condition, true, nil
153+
}
154+
155+
results := make([]LayerApplyResult, 0, len(mainLayers))
156+
for _, layer := range mainLayers {
157+
res := r.getLayerApplyResult(ctx, layer, mergedAt)
158+
results = append(results, res)
159+
if !res.Applied {
160+
condition.Reason = "LayerNotApplied"
161+
condition.Message = "At least one layer has not applied since merge."
162+
condition.Status = metav1.ConditionFalse
163+
return condition, false, results
164+
}
165+
}
166+
167+
condition.Reason = "AllLayersApplied"
168+
condition.Message = "All affected layers have applied since merge."
169+
condition.Status = metav1.ConditionTrue
170+
return condition, true, results
171+
}
172+
110173
func (r *Reconciler) IsCommentUpToDate(pr *configv1alpha1.TerraformPullRequest) (metav1.Condition, bool) {
111174
condition := metav1.Condition{
112175
Type: "IsCommentUpToDate",

internal/controllers/terraformpullrequest/layer.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/hex"
77
"fmt"
88
"slices"
9+
"time"
910

1011
corev1 "k8s.io/api/core/v1"
1112
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -166,7 +167,6 @@ func (r *Reconciler) deleteTempLayersByLabel(ctx context.Context, pr *configv1al
166167
ctx, &configv1alpha1.TerraformLayer{}, client.InNamespace(pr.Namespace), client.MatchingLabels{managedByLabel: labelValue},
167168
)
168169
}
169-
170170
func tempLayerGenerateName(layerName string, pr *configv1alpha1.TerraformPullRequest) string {
171171
hash := shortHash(fmt.Sprintf("%s/%s/%s", pr.Namespace, pr.Name, pr.Spec.ID))
172172
prefix := fmt.Sprintf("%s-pr-%s-", layerName, hash)
@@ -186,3 +186,84 @@ func shortHash(value string) string {
186186
sum := sha256.Sum256([]byte(value))
187187
return hex.EncodeToString(sum[:])[:16]
188188
}
189+
190+
// getMainBranchLayers returns the main-branch layers that correspond to the
191+
// temp layers linked to this PR (same path + repository, on the base branch).
192+
func (r *Reconciler) getMainBranchLayers(ctx context.Context, pr *configv1alpha1.TerraformPullRequest) ([]configv1alpha1.TerraformLayer, error) {
193+
tempLayers, err := GetLinkedLayers(r.Client, pr)
194+
if err != nil {
195+
return nil, err
196+
}
197+
198+
allLayers := &configv1alpha1.TerraformLayerList{}
199+
if err := r.Client.List(ctx, allLayers); err != nil {
200+
return nil, err
201+
}
202+
203+
result := []configv1alpha1.TerraformLayer{}
204+
for _, layer := range allLayers.Items {
205+
if layer.Spec.Branch != pr.Spec.Base {
206+
continue
207+
}
208+
if layer.Spec.Repository.Name != pr.Spec.Repository.Name ||
209+
layer.Spec.Repository.Namespace != pr.Spec.Repository.Namespace {
210+
continue
211+
}
212+
for _, temp := range tempLayers {
213+
if layer.Spec.Path == temp.Spec.Path {
214+
result = append(result, layer)
215+
break
216+
}
217+
}
218+
}
219+
return result, nil
220+
}
221+
222+
type LayerApplyResult struct {
223+
Layer configv1alpha1.TerraformLayer
224+
Applied bool
225+
Succeeded bool
226+
}
227+
228+
// getLayerApplyResult determines whether a layer has applied after mergedAt.
229+
// It checks:
230+
// - success: LastApplyDate annotation is set and after mergedAt
231+
// - failure: a TerraformRun with action=apply exists, was created after mergedAt,
232+
// and is in Failed state
233+
func (r *Reconciler) getLayerApplyResult(ctx context.Context, layer configv1alpha1.TerraformLayer, mergedAt time.Time) LayerApplyResult {
234+
result := LayerApplyResult{Layer: layer}
235+
236+
// Check success via annotation (only updated when apply succeeds)
237+
lastApplyDateStr := layer.Annotations[annotations.LastApplyDate]
238+
if lastApplyDateStr != "" {
239+
lastApplyDate, err := time.Parse(time.UnixDate, lastApplyDateStr)
240+
if err == nil && lastApplyDate.After(mergedAt) {
241+
result.Applied = true
242+
result.Succeeded = true
243+
return result
244+
}
245+
}
246+
247+
// Check failure by looking for a Failed TerraformRun with action=apply after mergedAt
248+
runs := &configv1alpha1.TerraformRunList{}
249+
requirement, err := labels.NewRequirement(managedByLabel, selection.Equals, []string{layer.Name})
250+
if err != nil {
251+
return result
252+
}
253+
selector := labels.NewSelector().Add(*requirement)
254+
if err := r.Client.List(ctx, runs, client.MatchingLabelsSelector{Selector: selector}, client.InNamespace(layer.Namespace)); err != nil {
255+
return result
256+
}
257+
for _, run := range runs.Items {
258+
if run.Spec.Action != "apply" {
259+
continue
260+
}
261+
if run.CreationTimestamp.After(mergedAt) && run.Status.State == "Failed" {
262+
result.Applied = true
263+
result.Succeeded = false
264+
return result
265+
}
266+
}
267+
268+
return result
269+
}

0 commit comments

Comments
 (0)