Skip to content

Commit c2e23bb

Browse files
committed
MCO-1961: Add the OSImageStream Controller
This change adds the OSImageStream controller and the additional logic required to introduce OSImageStreams into the MCO.
1 parent efc86f6 commit c2e23bb

File tree

9 files changed

+1078
-5
lines changed

9 files changed

+1078
-5
lines changed

pkg/controller/common/constants.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const (
6060
// APIServerInstanceName is a singleton name for APIServer configuration
6161
APIServerInstanceName = "cluster"
6262

63+
// ClusterInstanceNameOSImageStream is the name of the singleton cluster-scoped OSImageStream instance.
64+
ClusterInstanceNameOSImageStream = "cluster"
65+
6366
// APIServerInstanceName is a singleton name for APIServer configuration
6467
APIServerBootstrapFileLocation = "/etc/mcs/bootstrap/api-server/api-server.yaml"
6568

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package osimagestream
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
v1 "github.com/openshift/api/machineconfiguration/v1"
8+
"github.com/openshift/api/machineconfiguration/v1alpha1"
9+
"github.com/openshift/machine-config-operator/pkg/controller/common"
10+
"github.com/openshift/machine-config-operator/pkg/helpers"
11+
)
12+
13+
// GetStreamSetsNames extracts the names from a slice of OSImageStreamSets.
14+
func GetStreamSetsNames(streamSet []v1alpha1.OSImageStreamSet) []string {
15+
streams := make([]string, 0)
16+
for _, stream := range streamSet {
17+
streams = append(streams, stream.Name)
18+
}
19+
return streams
20+
}
21+
22+
// GetOSImageStreamSetByName retrieves an OSImageStreamSet by name from an OSImageStream.
23+
// If name is empty, the default stream is returned. Returns an error if the stream is not found.
24+
func GetOSImageStreamSetByName(osImageStream *v1alpha1.OSImageStream, name string) (*v1alpha1.OSImageStreamSet, error) {
25+
if osImageStream == nil {
26+
return nil, fmt.Errorf("requested OSImageStreamSet %s does not exist. OSImageStream cannot be nil", name)
27+
}
28+
if name == "" {
29+
name = osImageStream.Status.DefaultStream
30+
}
31+
32+
for _, stream := range osImageStream.Status.AvailableStreams {
33+
if stream.Name == name {
34+
return &stream, nil
35+
}
36+
}
37+
38+
return nil, fmt.Errorf("requested OSImageStream %s does not exist. Existing: %s", name, strings.Join(GetStreamSetsNames(osImageStream.Status.AvailableStreams), ","))
39+
}
40+
41+
// TryGetOSImageStreamSetByName retrieves an OSImageStreamSet by name, returning nil if not found.
42+
func TryGetOSImageStreamSetByName(osImageStream *v1alpha1.OSImageStream, name string) *v1alpha1.OSImageStreamSet {
43+
stream, _ := GetOSImageStreamSetByName(osImageStream, name)
44+
return stream
45+
}
46+
47+
// TryGetOSImageStreamFromPoolListByPoolName retrieves an OSImageStreamSet for a given pool name,
48+
// returning nil if the pool or stream is not found. For custom pools (non-master, non-arbiter),
49+
// falls back to the worker pool if the custom pool is not found.
50+
func TryGetOSImageStreamFromPoolListByPoolName(osImageStream *v1alpha1.OSImageStream, pools []*v1.MachineConfigPool, poolName string) *v1alpha1.OSImageStreamSet {
51+
targetPool := helpers.GetPoolByName(pools, poolName)
52+
if targetPool == nil && (poolName != common.MachineConfigPoolMaster && poolName != common.MachineConfigPoolArbiter) {
53+
targetPool = helpers.GetPoolByName(pools, common.MachineConfigPoolWorker)
54+
}
55+
if targetPool == nil {
56+
return nil
57+
}
58+
59+
return TryGetOSImageStreamSetByName(osImageStream, targetPool.Spec.OSImageStream.Name)
60+
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
// Assisted-by: Claude
2+
package osimagestream
3+
4+
import (
5+
"testing"
6+
7+
v1 "github.com/openshift/api/machineconfiguration/v1"
8+
"github.com/openshift/api/machineconfiguration/v1alpha1"
9+
"github.com/openshift/machine-config-operator/pkg/controller/common"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
)
14+
15+
func TestGetStreamSetsNames(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
input []v1alpha1.OSImageStreamSet
19+
expected []string
20+
}{
21+
{
22+
name: "empty slice",
23+
input: []v1alpha1.OSImageStreamSet{},
24+
expected: []string{},
25+
},
26+
{
27+
name: "single stream",
28+
input: []v1alpha1.OSImageStreamSet{
29+
{Name: "rhel-9"},
30+
},
31+
expected: []string{"rhel-9"},
32+
},
33+
{
34+
name: "multiple streams",
35+
input: []v1alpha1.OSImageStreamSet{
36+
{Name: "rhel-9"},
37+
{Name: "rhel-10"},
38+
{Name: "custom-stream"},
39+
},
40+
expected: []string{"rhel-9", "rhel-10", "custom-stream"},
41+
},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
result := GetStreamSetsNames(tt.input)
47+
assert.Equal(t, tt.expected, result)
48+
})
49+
}
50+
}
51+
52+
func TestGetOSImageStreamSetByName(t *testing.T) {
53+
osImageStream := &v1alpha1.OSImageStream{
54+
Status: v1alpha1.OSImageStreamStatus{
55+
DefaultStream: "rhel-9",
56+
AvailableStreams: []v1alpha1.OSImageStreamSet{
57+
{Name: "rhel-9", OSImage: "image1", OSExtensionsImage: "ext1"},
58+
{Name: "rhel-10", OSImage: "image2", OSExtensionsImage: "ext2"},
59+
},
60+
},
61+
}
62+
63+
tests := []struct {
64+
name string
65+
osImageStream *v1alpha1.OSImageStream
66+
streamName string
67+
expected *v1alpha1.OSImageStreamSet
68+
errorContains string
69+
}{
70+
{
71+
name: "find existing stream",
72+
osImageStream: osImageStream,
73+
streamName: "rhel-9",
74+
expected: &v1alpha1.OSImageStreamSet{Name: "rhel-9", OSImage: "image1", OSExtensionsImage: "ext1"},
75+
},
76+
{
77+
name: "find another existing stream",
78+
osImageStream: osImageStream,
79+
streamName: "rhel-10",
80+
expected: &v1alpha1.OSImageStreamSet{Name: "rhel-10", OSImage: "image2", OSExtensionsImage: "ext2"},
81+
},
82+
{
83+
name: "empty name returns default stream",
84+
osImageStream: osImageStream,
85+
streamName: "",
86+
expected: &v1alpha1.OSImageStreamSet{Name: "rhel-9", OSImage: "image1", OSExtensionsImage: "ext1"},
87+
},
88+
{
89+
name: "non-existent stream",
90+
osImageStream: osImageStream,
91+
streamName: "non-existent",
92+
errorContains: "does not exist",
93+
},
94+
{
95+
name: "nil osImageStream",
96+
osImageStream: nil,
97+
streamName: "rhel-9",
98+
errorContains: "cannot be nil",
99+
},
100+
}
101+
102+
for _, tt := range tests {
103+
t.Run(tt.name, func(t *testing.T) {
104+
result, err := GetOSImageStreamSetByName(tt.osImageStream, tt.streamName)
105+
if tt.errorContains != "" {
106+
require.Error(t, err)
107+
assert.Contains(t, err.Error(), tt.errorContains)
108+
assert.Nil(t, result)
109+
} else {
110+
require.NoError(t, err)
111+
assert.Equal(t, tt.expected, result)
112+
}
113+
})
114+
}
115+
}
116+
117+
func TestTryGetOSImageStreamSetByName(t *testing.T) {
118+
osImageStream := &v1alpha1.OSImageStream{
119+
Status: v1alpha1.OSImageStreamStatus{
120+
DefaultStream: "rhel-9",
121+
AvailableStreams: []v1alpha1.OSImageStreamSet{
122+
{Name: "rhel-9", OSImage: "image1", OSExtensionsImage: "ext1"},
123+
{Name: "rhel-10", OSImage: "image2", OSExtensionsImage: "ext2"},
124+
},
125+
},
126+
}
127+
128+
tests := []struct {
129+
name string
130+
osImageStream *v1alpha1.OSImageStream
131+
streamName string
132+
expected *v1alpha1.OSImageStreamSet
133+
}{
134+
{
135+
name: "find existing stream",
136+
osImageStream: osImageStream,
137+
streamName: "rhel-9",
138+
expected: &v1alpha1.OSImageStreamSet{Name: "rhel-9", OSImage: "image1", OSExtensionsImage: "ext1"},
139+
},
140+
{
141+
name: "non-existent stream returns nil",
142+
osImageStream: osImageStream,
143+
streamName: "non-existent",
144+
expected: nil,
145+
},
146+
{
147+
name: "nil osImageStream returns nil",
148+
osImageStream: nil,
149+
streamName: "rhel-9",
150+
expected: nil,
151+
},
152+
}
153+
154+
for _, tt := range tests {
155+
t.Run(tt.name, func(t *testing.T) {
156+
result := TryGetOSImageStreamSetByName(tt.osImageStream, tt.streamName)
157+
assert.Equal(t, tt.expected, result)
158+
})
159+
}
160+
}
161+
162+
func TestTryGetOSImageStreamFromPoolListByPoolName(t *testing.T) {
163+
osImageStream := &v1alpha1.OSImageStream{
164+
Status: v1alpha1.OSImageStreamStatus{
165+
DefaultStream: "stream-master",
166+
AvailableStreams: []v1alpha1.OSImageStreamSet{
167+
{Name: "stream-master", OSImage: "image1", OSExtensionsImage: "ext1"},
168+
{Name: "stream-worker", OSImage: "image2", OSExtensionsImage: "ext2"},
169+
{Name: "stream-arbiter", OSImage: "image3", OSExtensionsImage: "ext3"},
170+
{Name: "stream-custom", OSImage: "image4", OSExtensionsImage: "ext4"},
171+
},
172+
},
173+
}
174+
175+
masterPool := &v1.MachineConfigPool{
176+
ObjectMeta: metav1.ObjectMeta{Name: common.MachineConfigPoolMaster},
177+
Spec: v1.MachineConfigPoolSpec{
178+
OSImageStream: v1.OSImageStreamReference{Name: "stream-master"},
179+
},
180+
}
181+
182+
workerPool := &v1.MachineConfigPool{
183+
ObjectMeta: metav1.ObjectMeta{Name: common.MachineConfigPoolWorker},
184+
Spec: v1.MachineConfigPoolSpec{
185+
OSImageStream: v1.OSImageStreamReference{Name: "stream-worker"},
186+
},
187+
}
188+
189+
arbiterPool := &v1.MachineConfigPool{
190+
ObjectMeta: metav1.ObjectMeta{Name: common.MachineConfigPoolArbiter},
191+
Spec: v1.MachineConfigPoolSpec{
192+
OSImageStream: v1.OSImageStreamReference{Name: "stream-arbiter"},
193+
},
194+
}
195+
196+
customPool := &v1.MachineConfigPool{
197+
ObjectMeta: metav1.ObjectMeta{Name: "custom"},
198+
Spec: v1.MachineConfigPoolSpec{
199+
OSImageStream: v1.OSImageStreamReference{Name: "stream-custom"},
200+
},
201+
}
202+
203+
tests := []struct {
204+
name string
205+
osImageStream *v1alpha1.OSImageStream
206+
pools []*v1.MachineConfigPool
207+
poolName string
208+
expected *v1alpha1.OSImageStreamSet
209+
}{
210+
{
211+
name: "find stream for master pool",
212+
osImageStream: osImageStream,
213+
pools: []*v1.MachineConfigPool{masterPool, workerPool},
214+
poolName: common.MachineConfigPoolMaster,
215+
expected: &v1alpha1.OSImageStreamSet{Name: "stream-master", OSImage: "image1", OSExtensionsImage: "ext1"},
216+
},
217+
{
218+
name: "find stream for worker pool",
219+
osImageStream: osImageStream,
220+
pools: []*v1.MachineConfigPool{masterPool, workerPool},
221+
poolName: common.MachineConfigPoolWorker,
222+
expected: &v1alpha1.OSImageStreamSet{Name: "stream-worker", OSImage: "image2", OSExtensionsImage: "ext2"},
223+
},
224+
{
225+
name: "find stream for arbiter pool",
226+
osImageStream: osImageStream,
227+
pools: []*v1.MachineConfigPool{masterPool, workerPool, arbiterPool},
228+
poolName: common.MachineConfigPoolArbiter,
229+
expected: &v1alpha1.OSImageStreamSet{Name: "stream-arbiter", OSImage: "image3", OSExtensionsImage: "ext3"},
230+
},
231+
{
232+
name: "find stream for custom pool",
233+
osImageStream: osImageStream,
234+
pools: []*v1.MachineConfigPool{masterPool, workerPool, customPool},
235+
poolName: "custom",
236+
expected: &v1alpha1.OSImageStreamSet{Name: "stream-custom", OSImage: "image4", OSExtensionsImage: "ext4"},
237+
},
238+
{
239+
name: "custom pool not found - fallback to worker",
240+
osImageStream: osImageStream,
241+
pools: []*v1.MachineConfigPool{masterPool, workerPool},
242+
poolName: "non-existent-custom",
243+
expected: &v1alpha1.OSImageStreamSet{Name: "stream-worker", OSImage: "image2", OSExtensionsImage: "ext2"},
244+
},
245+
{
246+
name: "master pool not found - no fallback",
247+
osImageStream: osImageStream,
248+
pools: []*v1.MachineConfigPool{workerPool},
249+
poolName: common.MachineConfigPoolMaster,
250+
expected: nil,
251+
},
252+
{
253+
name: "arbiter pool not found - no fallback",
254+
osImageStream: osImageStream,
255+
pools: []*v1.MachineConfigPool{masterPool, workerPool},
256+
poolName: common.MachineConfigPoolArbiter,
257+
expected: nil,
258+
},
259+
{
260+
name: "worker pool not found - no fallback",
261+
osImageStream: osImageStream,
262+
pools: []*v1.MachineConfigPool{masterPool},
263+
poolName: common.MachineConfigPoolWorker,
264+
expected: nil,
265+
},
266+
{
267+
name: "pool found but stream not in osImageStream",
268+
osImageStream: osImageStream,
269+
pools: []*v1.MachineConfigPool{
270+
{
271+
ObjectMeta: metav1.ObjectMeta{Name: "custom"},
272+
Spec: v1.MachineConfigPoolSpec{
273+
OSImageStream: v1.OSImageStreamReference{Name: "non-existent-stream"},
274+
},
275+
},
276+
workerPool,
277+
},
278+
poolName: "custom",
279+
expected: nil,
280+
},
281+
{
282+
name: "nil osImageStream",
283+
osImageStream: nil,
284+
pools: []*v1.MachineConfigPool{masterPool, workerPool},
285+
poolName: common.MachineConfigPoolMaster,
286+
expected: nil,
287+
},
288+
{
289+
name: "empty pools list",
290+
osImageStream: osImageStream,
291+
pools: []*v1.MachineConfigPool{},
292+
poolName: common.MachineConfigPoolMaster,
293+
expected: nil,
294+
},
295+
}
296+
297+
for _, tt := range tests {
298+
t.Run(tt.name, func(t *testing.T) {
299+
result := TryGetOSImageStreamFromPoolListByPoolName(tt.osImageStream, tt.pools, tt.poolName)
300+
assert.Equal(t, tt.expected, result)
301+
})
302+
}
303+
}

0 commit comments

Comments
 (0)