Skip to content

Commit 6ac3284

Browse files
committed
Prevent sync phase errors from blocking subsequent phases
A regression in 1dcc49e ("Sync pending migration part 2", PR #792) causes stale ReleasePayload CRDs (with all-Unknown conditions) to be misclassified as Pending by GetTagPhase, which now prefers CRD-derived phases over annotations. Old tags like 4.7.0-fc.0 re-enter syncPending, hit a mirror hash mismatch error, and abort the entire sync loop for the 4-stable stream. This prevents syncReady from ever running, so releases like 4.22.5 never get their verification jobs launched — tests stay pending indefinitely. This change makes sync phase errors non-fatal: - syncPending, syncReady, and syncAccepted errors are collected instead of causing an immediate return. All three phases always run. - Per-tag errors in the stable-mode syncPending loop are isolated via a new syncPendingStableTag helper, so one broken tag cannot block others. - An aggregate error is returned at the end so the item is still requeued. - Conflict errors remain an immediate return nil (resource was modified, resync is imminent). Adds a regression test reproducing the exact scenario: a stale CRD returning Pending for an Accepted tag must not cause an unrelated Ready tag to be misclassified.
1 parent b3be4f5 commit 6ac3284

3 files changed

Lines changed: 341 additions & 104 deletions

File tree

cmd/release-controller/sync.go

Lines changed: 105 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,15 @@ func (c *Controller) sync(key queueKey) error {
100100
pendingTags = []*imagev1.TagReference{releaseTag}
101101
}
102102

103+
var syncErrors []error
104+
103105
// ensure any pending tags have the necessary jobs/mirrors created
104106
if err := c.syncPending(release, pendingTags, inputImageHash); err != nil {
105107
if errors.IsConflict(err) {
106108
return nil
107109
}
108110
c.eventRecorder.Eventf(release.Source, corev1.EventTypeWarning, "UnableToProcessRelease", "%v", err)
109-
return err
111+
syncErrors = append(syncErrors, err)
110112
}
111113

112114
// ensure verification steps are run on the ready tags
@@ -115,7 +117,7 @@ func (c *Controller) sync(key queueKey) error {
115117
return nil
116118
}
117119
c.eventRecorder.Eventf(release.Source, corev1.EventTypeWarning, "UnableToVerifyRelease", "%v", err)
118-
return err
120+
syncErrors = append(syncErrors, err)
119121
}
120122

121123
// ensure publish steps are run on the accepted tags
@@ -124,7 +126,7 @@ func (c *Controller) sync(key queueKey) error {
124126
return nil
125127
}
126128
c.eventRecorder.Eventf(release.Source, corev1.EventTypeWarning, "UnableToVerifyRelease", "%v", err)
127-
return err
129+
syncErrors = append(syncErrors, err)
128130
}
129131

130132
// if we're waiting for an interval to elapse, go ahead and queue to be woken
@@ -133,7 +135,7 @@ func (c *Controller) sync(key queueKey) error {
133135
}
134136

135137
c.gcQueue.AddAfter("", 15*time.Second)
136-
return nil
138+
return utilerrors.NewAggregate(syncErrors)
137139
}
138140

139141
func calculateSyncActions(release *releasecontroller.Release, now time.Time) (adoptTags, pendingTags, removeTags []*imagev1.TagReference, hasNewImages bool, inputImageHash string, queueAfter time.Duration) {
@@ -290,109 +292,14 @@ func (c *Controller) syncAdopted(release *releasecontroller.Release, adoptTags [
290292
func (c *Controller) syncPending(release *releasecontroller.Release, pendingTags []*imagev1.TagReference, inputImageHash string) (err error) {
291293
switch release.Config.As {
292294
case releasecontroller.ReleaseConfigModeStable:
295+
var errs []error
293296
for _, tag := range pendingTags {
294-
// wait for import, then determine whether the requested version (tag name) matches the source version (label on image)
295-
id := releasecontroller.FindImageIDForTag(release.Source, tag.Name)
296-
if len(id) == 0 {
297-
klog.V(2).Infof("Waiting for release %s to be imported before we can retrieve metadata", tag.Name)
298-
continue
299-
}
300-
klog.V(2).Infof("Processing pending release %s", tag.Name)
301-
rewriteValue := tag.Annotations[releasecontroller.ReleaseAnnotationRewrite]
302-
if len(rewriteValue) == 0 {
303-
klog.V(2).Infof("Rewriting pending release %s", tag.Name)
304-
isi, err := c.imageClient.ImageStreamImages(release.Source.Namespace).Get(context.TODO(), fmt.Sprintf("%s@%s", release.Source.Name, id), metav1.GetOptions{})
305-
if err != nil {
306-
return err
307-
}
308-
// Handle manifest list based releases...
309-
for _, m := range isi.Image.DockerImageManifests {
310-
if m.Architecture == "amd64" {
311-
isi, err = c.imageClient.ImageStreamImages(release.Source.Namespace).Get(context.TODO(), fmt.Sprintf("%s@%s", release.Source.Name, m.Digest), metav1.GetOptions{})
312-
if err != nil {
313-
return err
314-
}
315-
break
316-
}
317-
}
318-
metadata := &docker10.DockerImage{}
319-
if len(isi.Image.DockerImageMetadata.Raw) == 0 {
320-
return fmt.Errorf("could not fetch Docker image metadata for release %s", tag.Name)
321-
}
322-
if err := json.Unmarshal(isi.Image.DockerImageMetadata.Raw, metadata); err != nil {
323-
return fmt.Errorf("malformed Docker image metadata on ImageStreamTag: %v", err)
324-
}
325-
var name string
326-
if metadata.Config != nil {
327-
name = metadata.Config.Labels["io.openshift.release"]
328-
}
329-
rewriteValue = fmt.Sprintf("%t", name != tag.Name)
330-
if err := c.setReleaseAnnotation(release, tag.Annotations[releasecontroller.ReleaseAnnotationPhase], map[string]string{releasecontroller.ReleaseAnnotationRewrite: rewriteValue}, tag.Name); err != nil {
331-
return err
332-
}
333-
continue
334-
}
335-
rewrite := rewriteValue == "true"
336-
337-
hash := fmt.Sprintf("%s-%d", tag.Name, *tag.Generation)
338-
// mirror any internal images
339-
// rewrite payload for internal images with metadata
340-
mirror, err := c.ensureReleaseMirror(release, tag.Name, hash)
341-
if err != nil {
342-
return err
343-
}
344-
if len(tag.Annotations[releasecontroller.ReleaseAnnotationImageHash]) == 0 {
345-
if err := c.setReleaseAnnotation(release, tag.Annotations[releasecontroller.ReleaseAnnotationPhase], map[string]string{releasecontroller.ReleaseAnnotationImageHash: mirror.Annotations[releasecontroller.ReleaseAnnotationImageHash]}, tag.Name); err != nil {
346-
return err
347-
}
348-
continue
349-
}
350-
if mirror.Annotations[releasecontroller.ReleaseAnnotationImageHash] != tag.Annotations[releasecontroller.ReleaseAnnotationImageHash] {
351-
// delete the mirror and exit
352-
return fmt.Errorf("unimplemented, should regenerate contents of tag")
353-
}
354-
// Create the corresponding ReleasePayload object...
355-
_, err = c.ensureReleasePayload(release, tag)
356-
if err != nil {
357-
return err
358-
}
359-
// get metadata about the release
360-
// get upgrade graph edges
361-
// check to see any required edges are missing? wait for latest edge? wait for pending edges?
362-
// how do we calculate required edge set?
363-
var job *batchv1.Job
364-
if rewrite {
365-
job, err = c.ensureRewriteJob(release, tag.Name, mirror, `{}`)
366-
} else {
367-
job, err = c.ensureImportJob(release, tag.Name, mirror)
368-
}
369-
if err != nil || job == nil {
370-
return err
371-
}
372-
payload, err := c.releasePayloadLister.ReleasePayloads(release.Target.Namespace).Get(tag.Name)
373-
if err != nil {
374-
if !errors.IsNotFound(err) {
375-
return err
376-
}
377-
return c.ensureRewriteJobImageRetrieved(release, job, mirror)
378-
}
379-
phase := releasecontroller.GetReleasePhase(payload)
380-
switch phase {
381-
case releasecontroller.ReleasePhaseReady:
382-
if err := c.markReleaseReady(release, nil, tag.Name); err != nil {
383-
return err
384-
}
385-
c.precacheChangelog(release, tag)
386-
case releasecontroller.ReleasePhaseFailed:
387-
log, _, _ := ensureJobTerminationMessageRetrieved(c.podClient, job, "status.phase=Failed", "build", false)
388-
if err := c.transitionReleasePhaseFailure(release, []string{releasecontroller.ReleasePhasePending}, releasecontroller.ReleasePhaseFailed, withLog(reasonAndMessage("CreateReleaseFailed", "Could not create the release image"), log), tag.Name); err != nil {
389-
return err
390-
}
391-
default:
392-
return c.ensureRewriteJobImageRetrieved(release, job, mirror)
297+
if err := c.syncPendingStableTag(release, tag); err != nil {
298+
klog.Errorf("Error processing pending stable tag %s: %v", tag.Name, err)
299+
errs = append(errs, err)
393300
}
394301
}
395-
return nil
302+
return utilerrors.NewAggregate(errs)
396303
}
397304

398305
if len(pendingTags) > 1 {
@@ -452,6 +359,100 @@ func (c *Controller) syncPending(release *releasecontroller.Release, pendingTags
452359
return nil
453360
}
454361

362+
func (c *Controller) syncPendingStableTag(release *releasecontroller.Release, tag *imagev1.TagReference) error {
363+
id := releasecontroller.FindImageIDForTag(release.Source, tag.Name)
364+
if len(id) == 0 {
365+
klog.V(2).Infof("Waiting for release %s to be imported before we can retrieve metadata", tag.Name)
366+
return nil
367+
}
368+
klog.V(2).Infof("Processing pending release %s", tag.Name)
369+
rewriteValue := tag.Annotations[releasecontroller.ReleaseAnnotationRewrite]
370+
if len(rewriteValue) == 0 {
371+
klog.V(2).Infof("Rewriting pending release %s", tag.Name)
372+
isi, err := c.imageClient.ImageStreamImages(release.Source.Namespace).Get(context.TODO(), fmt.Sprintf("%s@%s", release.Source.Name, id), metav1.GetOptions{})
373+
if err != nil {
374+
return err
375+
}
376+
for _, m := range isi.Image.DockerImageManifests {
377+
if m.Architecture == "amd64" {
378+
isi, err = c.imageClient.ImageStreamImages(release.Source.Namespace).Get(context.TODO(), fmt.Sprintf("%s@%s", release.Source.Name, m.Digest), metav1.GetOptions{})
379+
if err != nil {
380+
return err
381+
}
382+
break
383+
}
384+
}
385+
metadata := &docker10.DockerImage{}
386+
if len(isi.Image.DockerImageMetadata.Raw) == 0 {
387+
return fmt.Errorf("could not fetch Docker image metadata for release %s", tag.Name)
388+
}
389+
if err := json.Unmarshal(isi.Image.DockerImageMetadata.Raw, metadata); err != nil {
390+
return fmt.Errorf("malformed Docker image metadata on ImageStreamTag: %v", err)
391+
}
392+
var name string
393+
if metadata.Config != nil {
394+
name = metadata.Config.Labels["io.openshift.release"]
395+
}
396+
rewriteValue = fmt.Sprintf("%t", name != tag.Name)
397+
if err := c.setReleaseAnnotation(release, tag.Annotations[releasecontroller.ReleaseAnnotationPhase], map[string]string{releasecontroller.ReleaseAnnotationRewrite: rewriteValue}, tag.Name); err != nil {
398+
return err
399+
}
400+
return nil
401+
}
402+
rewrite := rewriteValue == "true"
403+
404+
hash := fmt.Sprintf("%s-%d", tag.Name, *tag.Generation)
405+
mirror, err := c.ensureReleaseMirror(release, tag.Name, hash)
406+
if err != nil {
407+
return err
408+
}
409+
if len(tag.Annotations[releasecontroller.ReleaseAnnotationImageHash]) == 0 {
410+
if err := c.setReleaseAnnotation(release, tag.Annotations[releasecontroller.ReleaseAnnotationPhase], map[string]string{releasecontroller.ReleaseAnnotationImageHash: mirror.Annotations[releasecontroller.ReleaseAnnotationImageHash]}, tag.Name); err != nil {
411+
return err
412+
}
413+
return nil
414+
}
415+
if mirror.Annotations[releasecontroller.ReleaseAnnotationImageHash] != tag.Annotations[releasecontroller.ReleaseAnnotationImageHash] {
416+
return fmt.Errorf("unimplemented, should regenerate contents of tag %s", tag.Name)
417+
}
418+
_, err = c.ensureReleasePayload(release, tag)
419+
if err != nil {
420+
return err
421+
}
422+
var job *batchv1.Job
423+
if rewrite {
424+
job, err = c.ensureRewriteJob(release, tag.Name, mirror, `{}`)
425+
} else {
426+
job, err = c.ensureImportJob(release, tag.Name, mirror)
427+
}
428+
if err != nil || job == nil {
429+
return err
430+
}
431+
payload, err := c.releasePayloadLister.ReleasePayloads(release.Target.Namespace).Get(tag.Name)
432+
if err != nil {
433+
if !errors.IsNotFound(err) {
434+
return err
435+
}
436+
return c.ensureRewriteJobImageRetrieved(release, job, mirror)
437+
}
438+
phase := releasecontroller.GetReleasePhase(payload)
439+
switch phase {
440+
case releasecontroller.ReleasePhaseReady:
441+
if err := c.markReleaseReady(release, nil, tag.Name); err != nil {
442+
return err
443+
}
444+
c.precacheChangelog(release, tag)
445+
case releasecontroller.ReleasePhaseFailed:
446+
log, _, _ := ensureJobTerminationMessageRetrieved(c.podClient, job, "status.phase=Failed", "build", false)
447+
if err := c.transitionReleasePhaseFailure(release, []string{releasecontroller.ReleasePhasePending}, releasecontroller.ReleasePhaseFailed, withLog(reasonAndMessage("CreateReleaseFailed", "Could not create the release image"), log), tag.Name); err != nil {
448+
return err
449+
}
450+
default:
451+
return c.ensureRewriteJobImageRetrieved(release, job, mirror)
452+
}
453+
return nil
454+
}
455+
455456
func (c *Controller) syncReady(release *releasecontroller.Release) error {
456457
readyTags := releasecontroller.SortedRawReleaseTags(release, releasecontroller.ReleasePhaseReady)
457458

cmd/release-controller/sync_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package main
22

33
import (
44
"testing"
5+
"time"
56

7+
imagev1 "github.com/openshift/api/image/v1"
68
"github.com/openshift/release-controller/pkg/apis/release/v1alpha1"
9+
releasecontroller "github.com/openshift/release-controller/pkg/release-controller"
710
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
811
)
912

@@ -92,3 +95,83 @@ func TestGetRejectionDetails(t *testing.T) {
9295
})
9396
}
9497
}
98+
99+
func TestCalculateSyncActions_StalePayloadPhaseDoesNotBlockOtherTags(t *testing.T) {
100+
// Regression test: a tag whose ReleasePayload CRD has all-Unknown conditions
101+
// (returning Pending from GetReleasePhase) but whose annotation says Accepted
102+
// should not cause unrelated Ready tags to be misclassified as Pending.
103+
// With PayloadPhases populated, GetTagPhase prefers the CRD value, so the
104+
// stale tag appears as Pending. The Ready tag should still be counted as
105+
// unready (not pending) so it proceeds through syncReady.
106+
gen := int64(1)
107+
stableRelease := &releasecontroller.Release{
108+
Config: &releasecontroller.ReleaseConfig{
109+
Name: "4-stable",
110+
As: releasecontroller.ReleaseConfigModeStable,
111+
},
112+
Source: &imagev1.ImageStream{
113+
ObjectMeta: metav1.ObjectMeta{
114+
Name: "release",
115+
Namespace: "ocp",
116+
},
117+
},
118+
Target: &imagev1.ImageStream{
119+
ObjectMeta: metav1.ObjectMeta{
120+
Name: "release",
121+
Namespace: "ocp",
122+
},
123+
Spec: imagev1.ImageStreamSpec{
124+
Tags: []imagev1.TagReference{
125+
{
126+
Name: "4.7.0-fc.0",
127+
Generation: &gen,
128+
Annotations: map[string]string{
129+
releasecontroller.ReleaseAnnotationSource: "ocp/release",
130+
releasecontroller.ReleaseAnnotationName: "4-stable",
131+
releasecontroller.ReleaseAnnotationPhase: releasecontroller.ReleasePhaseAccepted,
132+
},
133+
},
134+
{
135+
Name: "4.22.5",
136+
Generation: &gen,
137+
Annotations: map[string]string{
138+
releasecontroller.ReleaseAnnotationSource: "ocp/release",
139+
releasecontroller.ReleaseAnnotationName: "4-stable",
140+
releasecontroller.ReleaseAnnotationPhase: releasecontroller.ReleasePhaseReady,
141+
},
142+
},
143+
},
144+
},
145+
},
146+
PayloadPhases: map[string]string{
147+
"4.7.0-fc.0": releasecontroller.ReleasePhasePending,
148+
"4.22.5": releasecontroller.ReleasePhaseReady,
149+
},
150+
}
151+
152+
adoptTags, pendingTags, _, _, _, _ := calculateSyncActions(stableRelease, time.Now())
153+
154+
// 4.7.0-fc.0 should be in pendingTags (CRD says Pending)
155+
foundStale := false
156+
for _, tag := range pendingTags {
157+
if tag.Name == "4.7.0-fc.0" {
158+
foundStale = true
159+
}
160+
}
161+
if !foundStale {
162+
t.Errorf("expected 4.7.0-fc.0 in pendingTags, got %v", releasecontroller.TagNames(pendingTags))
163+
}
164+
165+
// 4.22.5 should NOT be in pendingTags or adoptTags — it should fall through
166+
// to the default case (unreadyTagCount++) and be handled by syncReady
167+
for _, tag := range pendingTags {
168+
if tag.Name == "4.22.5" {
169+
t.Errorf("4.22.5 should not be in pendingTags, but it was found")
170+
}
171+
}
172+
for _, tag := range adoptTags {
173+
if tag.Name == "4.22.5" {
174+
t.Errorf("4.22.5 should not be in adoptTags, but it was found")
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)