Skip to content

Commit da9e337

Browse files
authored
Prevent showing duplicate entry under .status.activeRevisions (#2444)
Updating `ClusterExtension` with duplicate entry under `.status.activeRevisions` fails. Thus, we repopulate it from the installed and rolling out revisions.
1 parent 049f813 commit da9e337

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

internal/operator-controller/controllers/boxcutter_reconcile_steps.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func ApplyBundleWithBoxcutter(a Applier) ReconcileStepFunc {
115115
return nil, err
116116
}
117117

118+
ext.Status.ActiveRevisions = []ocv1.RevisionStatus{}
118119
// Mirror Available/Progressing conditions from the installed revision
119120
if i := state.revisionStates.Installed; i != nil {
120121
for _, cndType := range []string{ocv1.ClusterExtensionRevisionTypeAvailable, ocv1.ClusterExtensionRevisionTypeProgressing} {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"io/fs"
22+
"testing"
23+
"testing/fstest"
24+
25+
"github.com/stretchr/testify/require"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
28+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
29+
)
30+
31+
type mockApplier struct {
32+
err error
33+
}
34+
35+
func (m *mockApplier) Apply(_ context.Context, _ fs.FS, _ *ocv1.ClusterExtension, _ map[string]string, _ map[string]string) (bool, string, error) {
36+
return true, "", m.err
37+
}
38+
39+
func TestApplyBundleWithBoxcutter(t *testing.T) {
40+
type args struct {
41+
activeRevisions []ocv1.RevisionStatus
42+
revisionStates *RevisionStates
43+
}
44+
type want struct {
45+
activeRevisions []ocv1.RevisionStatus
46+
}
47+
48+
for _, tc := range []struct {
49+
name string
50+
args args
51+
want want
52+
}{
53+
{
54+
name: "two active revisions during update",
55+
args: args{
56+
activeRevisions: []ocv1.RevisionStatus{
57+
{Name: "ce-1"},
58+
},
59+
revisionStates: &RevisionStates{
60+
Installed: &RevisionMetadata{
61+
RevisionName: "ce-1",
62+
BundleMetadata: ocv1.BundleMetadata{
63+
Name: "test-bundle",
64+
Version: "1.0.0",
65+
},
66+
},
67+
RollingOut: []*RevisionMetadata{
68+
{RevisionName: "ce-2"},
69+
},
70+
},
71+
},
72+
want: want{
73+
activeRevisions: []ocv1.RevisionStatus{
74+
{Name: "ce-1"},
75+
{Name: "ce-2"},
76+
},
77+
},
78+
},
79+
{
80+
name: "replaces existing with new active revisions",
81+
args: args{
82+
activeRevisions: []ocv1.RevisionStatus{
83+
{Name: "ce-1"},
84+
},
85+
revisionStates: &RevisionStates{
86+
Installed: &RevisionMetadata{
87+
RevisionName: "ce-2",
88+
BundleMetadata: ocv1.BundleMetadata{
89+
Name: "test-bundle",
90+
Version: "1.0.1",
91+
},
92+
},
93+
},
94+
},
95+
want: want{
96+
activeRevisions: []ocv1.RevisionStatus{
97+
{Name: "ce-2"},
98+
},
99+
},
100+
},
101+
{
102+
name: "ongoing installation",
103+
args: args{
104+
activeRevisions: []ocv1.RevisionStatus{
105+
{Name: "ce-1"},
106+
},
107+
revisionStates: &RevisionStates{
108+
RollingOut: []*RevisionMetadata{
109+
{RevisionName: "ce-1"},
110+
},
111+
},
112+
},
113+
want: want{
114+
activeRevisions: []ocv1.RevisionStatus{
115+
{Name: "ce-1"},
116+
},
117+
},
118+
},
119+
} {
120+
t.Run(tc.name, func(t *testing.T) {
121+
ctx := context.Background()
122+
applier := &mockApplier{}
123+
124+
ext := &ocv1.ClusterExtension{
125+
ObjectMeta: metav1.ObjectMeta{
126+
Name: "test-ext",
127+
Generation: 1,
128+
},
129+
Status: ocv1.ClusterExtensionStatus{
130+
ActiveRevisions: tc.args.activeRevisions,
131+
},
132+
}
133+
134+
state := &reconcileState{
135+
revisionStates: tc.args.revisionStates,
136+
resolvedRevisionMetadata: &RevisionMetadata{
137+
BundleMetadata: ocv1.BundleMetadata{
138+
Name: "test-bundle",
139+
Version: "1.0.0",
140+
},
141+
},
142+
imageFS: fstest.MapFS{},
143+
}
144+
145+
stepFunc := ApplyBundleWithBoxcutter(applier)
146+
result, err := stepFunc(ctx, state, ext)
147+
require.NoError(t, err)
148+
require.Nil(t, result)
149+
150+
require.Len(t, ext.Status.ActiveRevisions, len(tc.want.activeRevisions))
151+
for i, expected := range tc.want.activeRevisions {
152+
require.Equal(t, expected.Name, ext.Status.ActiveRevisions[i].Name,
153+
"ActiveRevisions[%d].Name mismatch", i)
154+
}
155+
})
156+
}
157+
}

0 commit comments

Comments
 (0)