Skip to content

Commit a1d7e4e

Browse files
authored
fix: Annotate PLR when when started status is reported (openshift-pipelines#2208)
* fix: Annotate PLR when started status is reported The use of the state label (which is mutable) for deciding when to report to the SCM that the PLR was started is flaky. It was seen that the reconciler get events about PLRs with unexpected value for the state label. For example, after the status is reported to the SCM, and the label value is patched to "started", after serval reconcile iterations the label had the "queued" value again. This can happen because of unsafe patching done by controllers (not just the PAC controllers) which reconciles PLRs. Introduce a new annotation for indicating the the status was reported to the SCM. By adding an annotation which is set once, we remove the risk that its value will get overwritten by other controllers (since maps are merged when patched, values are not getting removed unless explicitly defined in the patch - https://datatracker.ietf.org/doc/html/rfc7386#section-2) In addition, at the start of each reconcile loop, ensure that we operate on the latest version of the PLR and not using a stale value from the cache. Assisted-By: Cursor Signed-off-by: Gal Ben Haim <[email protected]>
1 parent 633f025 commit a1d7e4e

File tree

6 files changed

+291
-63
lines changed

6 files changed

+291
-63
lines changed

pkg/apis/pipelinesascode/keys/keys.go

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,43 +23,44 @@ import (
2323
)
2424

2525
const (
26-
ControllerInfo = pipelinesascode.GroupName + "/controller-info"
27-
Task = pipelinesascode.GroupName + "/task"
28-
Pipeline = pipelinesascode.GroupName + "/pipeline"
29-
URLOrg = pipelinesascode.GroupName + "/url-org"
30-
URLRepository = pipelinesascode.GroupName + "/url-repository"
31-
SHA = pipelinesascode.GroupName + "/sha"
32-
Sender = pipelinesascode.GroupName + "/sender"
33-
EventType = pipelinesascode.GroupName + "/event-type"
34-
Branch = pipelinesascode.GroupName + "/branch"
35-
SourceBranch = pipelinesascode.GroupName + "/source-branch"
36-
Repository = pipelinesascode.GroupName + "/repository"
37-
GitProvider = pipelinesascode.GroupName + "/git-provider"
38-
State = pipelinesascode.GroupName + "/state"
39-
ShaTitle = pipelinesascode.GroupName + "/sha-title"
40-
ShaURL = pipelinesascode.GroupName + "/sha-url"
41-
RepoURL = pipelinesascode.GroupName + "/repo-url"
42-
SourceRepoURL = pipelinesascode.GroupName + "/source-repo-url"
43-
PullRequest = pipelinesascode.GroupName + "/pull-request"
44-
InstallationID = pipelinesascode.GroupName + "/installation-id"
45-
GHEURL = pipelinesascode.GroupName + "/ghe-url"
46-
SourceProjectID = pipelinesascode.GroupName + "/source-project-id"
47-
TargetProjectID = pipelinesascode.GroupName + "/target-project-id"
48-
OriginalPRName = pipelinesascode.GroupName + "/original-prname"
49-
GitAuthSecret = pipelinesascode.GroupName + "/git-auth-secret"
50-
CheckRunID = pipelinesascode.GroupName + "/check-run-id"
51-
OnEvent = pipelinesascode.GroupName + "/on-event"
52-
OnComment = pipelinesascode.GroupName + "/on-comment"
53-
OnTargetBranch = pipelinesascode.GroupName + "/on-target-branch"
54-
OnPathChange = pipelinesascode.GroupName + "/on-path-change"
55-
OnLabel = pipelinesascode.GroupName + "/on-label"
56-
OnPathChangeIgnore = pipelinesascode.GroupName + "/on-path-change-ignore"
57-
OnCelExpression = pipelinesascode.GroupName + "/on-cel-expression"
58-
TargetNamespace = pipelinesascode.GroupName + "/target-namespace"
59-
MaxKeepRuns = pipelinesascode.GroupName + "/max-keep-runs"
60-
CancelInProgress = pipelinesascode.GroupName + "/cancel-in-progress"
61-
LogURL = pipelinesascode.GroupName + "/log-url"
62-
ExecutionOrder = pipelinesascode.GroupName + "/execution-order"
26+
ControllerInfo = pipelinesascode.GroupName + "/controller-info"
27+
Task = pipelinesascode.GroupName + "/task"
28+
Pipeline = pipelinesascode.GroupName + "/pipeline"
29+
URLOrg = pipelinesascode.GroupName + "/url-org"
30+
URLRepository = pipelinesascode.GroupName + "/url-repository"
31+
SHA = pipelinesascode.GroupName + "/sha"
32+
Sender = pipelinesascode.GroupName + "/sender"
33+
EventType = pipelinesascode.GroupName + "/event-type"
34+
Branch = pipelinesascode.GroupName + "/branch"
35+
SourceBranch = pipelinesascode.GroupName + "/source-branch"
36+
Repository = pipelinesascode.GroupName + "/repository"
37+
GitProvider = pipelinesascode.GroupName + "/git-provider"
38+
State = pipelinesascode.GroupName + "/state"
39+
ShaTitle = pipelinesascode.GroupName + "/sha-title"
40+
ShaURL = pipelinesascode.GroupName + "/sha-url"
41+
RepoURL = pipelinesascode.GroupName + "/repo-url"
42+
SourceRepoURL = pipelinesascode.GroupName + "/source-repo-url"
43+
PullRequest = pipelinesascode.GroupName + "/pull-request"
44+
InstallationID = pipelinesascode.GroupName + "/installation-id"
45+
GHEURL = pipelinesascode.GroupName + "/ghe-url"
46+
SourceProjectID = pipelinesascode.GroupName + "/source-project-id"
47+
TargetProjectID = pipelinesascode.GroupName + "/target-project-id"
48+
OriginalPRName = pipelinesascode.GroupName + "/original-prname"
49+
GitAuthSecret = pipelinesascode.GroupName + "/git-auth-secret"
50+
CheckRunID = pipelinesascode.GroupName + "/check-run-id"
51+
OnEvent = pipelinesascode.GroupName + "/on-event"
52+
OnComment = pipelinesascode.GroupName + "/on-comment"
53+
OnTargetBranch = pipelinesascode.GroupName + "/on-target-branch"
54+
OnPathChange = pipelinesascode.GroupName + "/on-path-change"
55+
OnLabel = pipelinesascode.GroupName + "/on-label"
56+
OnPathChangeIgnore = pipelinesascode.GroupName + "/on-path-change-ignore"
57+
OnCelExpression = pipelinesascode.GroupName + "/on-cel-expression"
58+
TargetNamespace = pipelinesascode.GroupName + "/target-namespace"
59+
MaxKeepRuns = pipelinesascode.GroupName + "/max-keep-runs"
60+
CancelInProgress = pipelinesascode.GroupName + "/cancel-in-progress"
61+
LogURL = pipelinesascode.GroupName + "/log-url"
62+
ExecutionOrder = pipelinesascode.GroupName + "/execution-order"
63+
SCMReportingPLRStarted = pipelinesascode.GroupName + "/scm-reporting-plr-started"
6364
// PublicGithubAPIURL default is "https://api.github.com" but it can be overridden by X-GitHub-Enterprise-Host header.
6465
PublicGithubAPIURL = "https://api.github.com"
6566
GithubApplicationID = "github-application-id"

pkg/kubeinteraction/labels.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ func AddLabelsAndAnnotations(event *info.Event, pipelineRun *tektonv1.PipelineRu
5555
keys.SourceBranch: event.HeadBranch,
5656
keys.Repository: repo.GetName(),
5757
keys.GitProvider: providerConfig.Name,
58-
keys.State: StateStarted,
5958
keys.ControllerInfo: fmt.Sprintf(`{"name":"%s","configmap":"%s","secret":"%s", "gRepo": "%s"}`,
6059
paramsinfo.Controller.Name, paramsinfo.Controller.Configmap, paramsinfo.Controller.Secret, paramsinfo.Controller.GlobalRepository),
6160
}

pkg/pipelineascode/pipelineascode.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,10 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi
176176
p.logger.Errorf("Error adding labels/annotations to PipelineRun '%s' in namespace '%s': %v", match.PipelineRun.GetName(), match.Repo.GetNamespace(), err)
177177
}
178178

179-
// if concurrency is defined then start the pipelineRun in pending state and
180-
// state as queued
179+
// if concurrency is defined then start the pipelineRun in pending state
181180
if match.Repo.Spec.ConcurrencyLimit != nil && *match.Repo.Spec.ConcurrencyLimit != 0 {
182181
// pending status
183182
match.PipelineRun.Spec.Status = tektonv1.PipelineRunSpecStatusPending
184-
// pac state as queued
185-
match.PipelineRun.Labels[keys.State] = kubeinteraction.StateQueued
186-
match.PipelineRun.Annotations[keys.State] = kubeinteraction.StateQueued
187183
}
188184

189185
// Create the actual pipelineRun
@@ -240,23 +236,31 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi
240236
OriginalPipelineRunName: pr.GetAnnotations()[keys.OriginalPRName],
241237
}
242238

239+
// Patch the pipelineRun with the appropriate annotations and labels.
240+
// Set the state so the watcher will continue with reconciling the pipelineRun
241+
// The watcher reconciles only pipelineRuns that has the state annotation.
243242
patchAnnotations := map[string]string{}
244243
patchLabels := map[string]string{}
245244
whatPatching := ""
246245
// if pipelineRun is in pending state then report status as queued
246+
// The pipelineRun can be pending because of PAC's concurrency limit or because of an external mutatingwebhook
247247
if pr.Spec.Status == tektonv1.PipelineRunSpecStatusPending {
248248
status.Status = queuedStatus
249249
if status.Text, err = mt.MakeTemplate(p.vcx.GetTemplate(provider.QueueingPipelineType)); err != nil {
250250
return nil, fmt.Errorf("cannot create message template: %w", err)
251251
}
252-
// If the PipelineRun is in the "queued" state, add the appropriate label and annotation.
253-
// These are later used by the watcher to determine whether the PipelineRun status
254-
// should be reported back to the Git provider. We do add the `state` annotations and label when
255-
// concurrency is enabled but this would happen when PipelineRun's status has been changed by
256-
// the other controller and PaC is not aware of that change.
257252
whatPatching = "annotations.state and labels.state"
258253
patchAnnotations[keys.State] = kubeinteraction.StateQueued
259254
patchLabels[keys.State] = kubeinteraction.StateQueued
255+
} else {
256+
// Mark that the start will be reported to the Git provider
257+
patchAnnotations[keys.SCMReportingPLRStarted] = "true"
258+
patchAnnotations[keys.State] = kubeinteraction.StateStarted
259+
patchLabels[keys.State] = kubeinteraction.StateStarted
260+
whatPatching = fmt.Sprintf(
261+
"annotation.%s and annotations.state and labels.state",
262+
keys.SCMReportingPLRStarted,
263+
)
260264
}
261265

262266
if err := p.vcx.CreateStatus(ctx, p.event, status); err != nil {
@@ -278,6 +282,20 @@ func (p *PacRun) startPR(ctx context.Context, match matcher.Match) (*tektonv1.Pi
278282
// unneeded SIGSEGV's
279283
return pr, fmt.Errorf("cannot patch pipelinerun %s: %w", pr.GetGenerateName(), err)
280284
}
285+
currentReason := ""
286+
if len(pr.Status.GetConditions()) > 0 {
287+
currentReason = pr.Status.GetConditions()[0].GetReason()
288+
}
289+
290+
p.logger.Infof("PipelineRun %s/%s patched successfully - Spec.Status: %s, State annotation: '%s', SCMReportingPLRStarted annotation: '%s', Status reason: '%s', Git provider status: '%s', Patched: %s",
291+
pr.GetNamespace(),
292+
pr.GetName(),
293+
pr.Spec.Status,
294+
pr.GetAnnotations()[keys.State],
295+
pr.GetAnnotations()[keys.SCMReportingPLRStarted],
296+
currentReason,
297+
status.Status,
298+
whatPatching)
281299
}
282300

283301
return pr, nil

pkg/pipelineascode/pipelineascode_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,10 +718,19 @@ func TestRun(t *testing.T) {
718718
secretName, ok := pr.GetAnnotations()[keys.GitAuthSecret]
719719
assert.Assert(t, ok, "Cannot find secret %s on annotations", secretName)
720720
}
721-
if tt.concurrencyLimit > 0 || pr.Spec.Status == pipelinev1.PipelineRunSpecStatusPending {
721+
if pr.Spec.Status == pipelinev1.PipelineRunSpecStatusPending {
722722
state, ok := pr.GetAnnotations()[keys.State]
723723
assert.Assert(t, ok, "State hasn't been set on PR", state)
724724
assert.Equal(t, state, kubeinteraction.StateQueued)
725+
726+
// When PipelineRun is queued, SCMReportingPLRStarted should not be set
727+
_, scmStartedExists := pr.GetAnnotations()[keys.SCMReportingPLRStarted]
728+
assert.Assert(t, !scmStartedExists, "SCMReportingPLRStarted should not be set for queued PipelineRuns")
729+
} else {
730+
// When PipelineRun is not queued, SCMReportingPLRStarted should be set to "true"
731+
scmStarted, scmStartedExists := pr.GetAnnotations()[keys.SCMReportingPLRStarted]
732+
assert.Assert(t, scmStartedExists, "SCMReportingPLRStarted should be set for non-queued PipelineRuns")
733+
assert.Equal(t, scmStarted, "true", "SCMReportingPLRStarted should be 'true'")
725734
}
726735
}
727736
}

pkg/reconciler/reconciler.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ var (
5454
func (r *Reconciler) ReconcileKind(ctx context.Context, pr *tektonv1.PipelineRun) pkgreconciler.Event {
5555
ctx = info.StoreNS(ctx, system.Namespace())
5656
logger := logging.FromContext(ctx).With("namespace", pr.GetNamespace())
57+
58+
logger.Debugf("reconciling pipelineRun %s/%s", pr.GetNamespace(), pr.GetName())
59+
60+
// make sure we have the latest pipelinerun to reconcile, since there is something updating at the same time
61+
lpr, err := r.run.Clients.Tekton.TektonV1().PipelineRuns(pr.GetNamespace()).Get(ctx, pr.GetName(), metav1.GetOptions{})
62+
if err != nil {
63+
return fmt.Errorf("cannot get pipelineRun: %w", err)
64+
}
65+
66+
if lpr.GetResourceVersion() != pr.GetResourceVersion() {
67+
logger.Debugf("Skipping reconciliation, pipelineRun was updated (cached version %s vs fresh version %s)", pr.GetResourceVersion(), lpr.GetResourceVersion())
68+
return nil
69+
}
70+
5771
// if pipelineRun is in completed or failed state then return
5872
state, exist := pr.GetAnnotations()[keys.State]
5973
if exist && (state == kubeinteraction.StateCompleted || state == kubeinteraction.StateFailed) {
@@ -69,14 +83,20 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, pr *tektonv1.PipelineRun
6983
// another controller outside PaC). To maintain consistency between the PipelineRun
7084
// status and the Git provider status, we update both the PipelineRun resource and
7185
// the corresponding status on the Git provider here.
72-
if reason == string(tektonv1.PipelineRunReasonRunning) && state == kubeinteraction.StateQueued {
86+
scmReportingPLRStarted, exist := pr.GetAnnotations()[keys.SCMReportingPLRStarted]
87+
startReported := exist && scmReportingPLRStarted == "true"
88+
logger.Debugf("pipelineRun %s/%s scmReportingPLRStarted=%v, exist=%v", pr.GetNamespace(), pr.GetName(), startReported, exist)
89+
90+
if reason == string(tektonv1.PipelineRunReasonRunning) && !startReported {
91+
logger.Infof("pipelineRun %s/%s is running but not yet reported to provider, updating status", pr.GetNamespace(), pr.GetName())
7392
repoName := pr.GetAnnotations()[keys.Repository]
7493
repo, err := r.repoLister.Repositories(pr.Namespace).Get(repoName)
7594
if err != nil {
7695
return fmt.Errorf("failed to get repository CR: %w", err)
7796
}
7897
return r.updatePipelineRunToInProgress(ctx, logger, repo, pr)
7998
}
99+
logger.Debugf("pipelineRun %s/%s condition not met: reason='%s', startReported=%v", pr.GetNamespace(), pr.GetName(), reason, startReported)
80100

81101
// if its a GitHub App pipelineRun PR then process only if check run id is added otherwise wait
82102
if _, ok := pr.Annotations[keys.InstallationID]; ok {
@@ -95,16 +115,6 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, pr *tektonv1.PipelineRun
95115
return nil
96116
}
97117

98-
// make sure we have the latest pipelinerun to reconcile, since there is something updating at the same time
99-
lpr, err := r.run.Clients.Tekton.TektonV1().PipelineRuns(pr.GetNamespace()).Get(ctx, pr.GetName(), metav1.GetOptions{})
100-
if err != nil {
101-
return fmt.Errorf("cannot get pipelineRun: %w", err)
102-
}
103-
104-
if lpr.GetResourceVersion() != pr.GetResourceVersion() {
105-
return nil
106-
}
107-
108118
// If we have a controllerInfo annotation, then we need to get the
109119
// configmap configuration for it
110120
//
@@ -343,16 +353,24 @@ func (r *Reconciler) updatePipelineRunToInProgress(ctx context.Context, logger *
343353
}
344354

345355
func (r *Reconciler) updatePipelineRunState(ctx context.Context, logger *zap.SugaredLogger, pr *tektonv1.PipelineRun, state string) (*tektonv1.PipelineRun, error) {
356+
currentState := pr.GetAnnotations()[keys.State]
357+
logger.Infof("updating pipelineRun %v/%v state from %s to %s", pr.GetNamespace(), pr.GetName(), currentState, state)
358+
annotations := map[string]string{
359+
keys.State: state,
360+
}
361+
if state == kubeinteraction.StateStarted {
362+
annotations[keys.SCMReportingPLRStarted] = "true"
363+
}
364+
346365
mergePatch := map[string]any{
347366
"metadata": map[string]any{
348367
"labels": map[string]string{
349368
keys.State: state,
350369
},
351-
"annotations": map[string]string{
352-
keys.State: state,
353-
},
370+
"annotations": annotations,
354371
},
355372
}
373+
356374
// if state is started then remove pipelineRun pending status
357375
if state == kubeinteraction.StateStarted {
358376
mergePatch["spec"] = map[string]any{

0 commit comments

Comments
 (0)