Skip to content

Commit 275774a

Browse files
Merge pull request #5442 from pablintino/osimagestream-fetching
MCO-1956: Osimagestream fetching
2 parents 71ee88e + 0e7dd9d commit 275774a

File tree

86 files changed

+6913
-67
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+6913
-67
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ require (
3737
github.com/onsi/gomega v1.36.2
3838
github.com/opencontainers/go-digest v1.0.0
3939
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250916161632-d81c09058835
40-
github.com/openshift/api v0.0.0-20251119073004-138912d4ee99
41-
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235
40+
github.com/openshift/api v0.0.0-20251122153900-88cca31a44c9
41+
github.com/openshift/client-go v0.0.0-20251123231646-4685125c2287
4242
github.com/openshift/library-go v0.0.0-20251015151611-6fc7a74b67c5
4343
github.com/openshift/runtime-utils v0.0.0-20230921210328-7bdb5b9c177b
4444
github.com/prometheus/client_golang v1.22.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,10 +609,10 @@ github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplU
609609
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
610610
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250916161632-d81c09058835 h1:rkqIIfdYYkasXbF2XKVgh/3f1mhjSQK9By8WtVMgYo8=
611611
github.com/openshift-eng/openshift-tests-extension v0.0.0-20250916161632-d81c09058835/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M=
612-
github.com/openshift/api v0.0.0-20251119073004-138912d4ee99 h1:VGkPn3iO7ZapVYtUd7Lj1tE2ZwRfOOUVFzoA/sWlWDc=
613-
github.com/openshift/api v0.0.0-20251119073004-138912d4ee99/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
614-
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235 h1:9JBeIXmnHlpXTQPi7LPmu1jdxznBhAE7bb1K+3D8gxY=
615-
github.com/openshift/client-go v0.0.0-20251015124057-db0dee36e235/go.mod h1:L49W6pfrZkfOE5iC1PqEkuLkXG4W0BX4w8b+L2Bv7fM=
612+
github.com/openshift/api v0.0.0-20251122153900-88cca31a44c9 h1:RKbCmhOI6XOKMjoXLjANJ1ic7wd4dVV7nSfrn3csEuQ=
613+
github.com/openshift/api v0.0.0-20251122153900-88cca31a44c9/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY=
614+
github.com/openshift/client-go v0.0.0-20251123231646-4685125c2287 h1:Spullg4rMMWUjYiBMvYMhyeZ+j36mYOrkSO7ad43xrA=
615+
github.com/openshift/client-go v0.0.0-20251123231646-4685125c2287/go.mod h1:liCuDDdOsPSZIDP0QuTveFhF7ldXuvnPhBd/OTsJdJc=
616616
github.com/openshift/kubernetes v1.30.1-0.20251028145634-9e794b89909a h1:uaeiYAYOVlXChnGxvsziVTkzaSlBV7h8Y2U2Bc81UKM=
617617
github.com/openshift/kubernetes v1.30.1-0.20251028145634-9e794b89909a/go.mod h1:w3+IfrXNp5RosdDXg3LB55yijJqR/FwouvVntYHQf0o=
618618
github.com/openshift/kubernetes/staging/src/k8s.io/api v0.0.0-20251028145634-9e794b89909a h1:hZUZg/qpvT23oUoCkFWe/Q4VNu5zOeqmDOl3f/F6uRk=
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package osimagestream
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
configlisters "github.com/openshift/client-go/config/listers/config/v1"
8+
)
9+
10+
// clusterVersionSingletonName is the well-known name of the cluster-scoped ClusterVersion singleton resource.
11+
const clusterVersionSingletonName = "version"
12+
13+
// GetReleasePayloadImage retrieves the release payload image from the ClusterVersion resource.
14+
func GetReleasePayloadImage(lister configlisters.ClusterVersionLister) (string, error) {
15+
clusterVersion, err := lister.Get(clusterVersionSingletonName)
16+
if err != nil {
17+
return "", fmt.Errorf("failed to get ClusterVersion: %w", err)
18+
}
19+
if clusterVersion == nil || clusterVersion.Status.Desired.Image == "" {
20+
return "", errors.New("ClusterVersion desired image is not yet available")
21+
}
22+
// Got it, store the variable and exit
23+
return clusterVersion.Status.Desired.Image, nil
24+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Assisted-by: Claude
2+
package osimagestream
3+
4+
import (
5+
"testing"
6+
7+
configv1 "github.com/openshift/api/config/v1"
8+
fakeconfigclientset "github.com/openshift/client-go/config/clientset/versioned/fake"
9+
configinformers "github.com/openshift/client-go/config/informers/externalversions"
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 TestGetClusterVersionOperatorImage(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
clusterVersion *configv1.ClusterVersion
19+
expectedImage string
20+
errorContains string
21+
}{
22+
{
23+
name: "success - returns image from ClusterVersion",
24+
clusterVersion: &configv1.ClusterVersion{
25+
ObjectMeta: metav1.ObjectMeta{
26+
Name: "version",
27+
},
28+
Status: configv1.ClusterVersionStatus{
29+
Desired: configv1.Release{
30+
Image: "quay.io/openshift-release-dev/ocp-release@sha256:abc123",
31+
},
32+
},
33+
},
34+
expectedImage: "quay.io/openshift-release-dev/ocp-release@sha256:abc123",
35+
},
36+
{
37+
name: "error - ClusterVersion does not exist",
38+
errorContains: "not found",
39+
},
40+
{
41+
name: "error - ClusterVersion has empty desired image",
42+
clusterVersion: &configv1.ClusterVersion{
43+
ObjectMeta: metav1.ObjectMeta{
44+
Name: "version",
45+
},
46+
Status: configv1.ClusterVersionStatus{
47+
Desired: configv1.Release{
48+
Image: "",
49+
},
50+
},
51+
},
52+
errorContains: "ClusterVersion desired image is not yet available",
53+
},
54+
}
55+
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
var fakeClient *fakeconfigclientset.Clientset
59+
if tt.clusterVersion != nil {
60+
fakeClient = fakeconfigclientset.NewSimpleClientset(tt.clusterVersion)
61+
} else {
62+
fakeClient = fakeconfigclientset.NewSimpleClientset()
63+
}
64+
65+
informerFactory := configinformers.NewSharedInformerFactory(fakeClient, 0)
66+
clusterVersionInformer := informerFactory.Config().V1().ClusterVersions()
67+
68+
if tt.clusterVersion != nil {
69+
err := clusterVersionInformer.Informer().GetIndexer().Add(tt.clusterVersion)
70+
require.NoError(t, err)
71+
}
72+
73+
image, err := GetReleasePayloadImage(clusterVersionInformer.Lister())
74+
75+
if tt.errorContains != "" {
76+
require.Error(t, err)
77+
assert.Contains(t, err.Error(), tt.errorContains)
78+
assert.Empty(t, image)
79+
return
80+
}
81+
82+
require.NoError(t, err)
83+
assert.Equal(t, tt.expectedImage, image)
84+
})
85+
}
86+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package osimagestream
2+
3+
import (
4+
"maps"
5+
"slices"
6+
7+
"github.com/openshift/api/machineconfiguration/v1alpha1"
8+
"k8s.io/klog/v2"
9+
)
10+
11+
const (
12+
// coreOSLabelStreamClass is the container image label that identifies the OS stream name.
13+
coreOSLabelStreamClass = "io.openshift.os.streamclass"
14+
// coreOSLabelDiscriminator is the container image label that distinguishes OS images from Extensions images.
15+
coreOSLabelDiscriminator = "ostree.linux"
16+
)
17+
18+
// ImageType indicates whether a container image is an OS image or an extensions image.
19+
type ImageType int
20+
21+
const (
22+
// ImageTypeOS represents a base operating system image.
23+
ImageTypeOS = iota
24+
// ImageTypeExtensions represents an OS extensions image.
25+
ImageTypeExtensions
26+
)
27+
28+
// ImageDataExtractor extracts metadata from container image labels to determine
29+
// the image type and associated OS stream.
30+
type ImageDataExtractor interface {
31+
// GetImageData analyzes container image labels and returns structured metadata,
32+
// or nil if the image is not an OS or extensions image.
33+
GetImageData(image string, labels map[string]string) *ImageData
34+
}
35+
36+
// ImageData contains metadata extracted from a container image.
37+
type ImageData struct {
38+
Image string // Container image reference
39+
Type ImageType // Image type (OS or extensions)
40+
Stream string // OS stream name
41+
}
42+
43+
// ImageStreamExtractorImpl implements ImageDataExtractor using container image labels
44+
// to identify and classify OS and extensions images.
45+
type ImageStreamExtractorImpl struct {
46+
imagesInspector ImagesInspector
47+
streamLabels []string
48+
osImageDiscriminator string
49+
}
50+
51+
// NewImageStreamExtractor creates a new ImageDataExtractor configured to recognize
52+
// CoreOS images using standard image labels.
53+
func NewImageStreamExtractor() ImageDataExtractor {
54+
// The type is thought to allow future extra label addition for
55+
// i.e. Allow a customer to bring their own images with their own labels (defining a selector)
56+
return &ImageStreamExtractorImpl{
57+
streamLabels: []string{coreOSLabelStreamClass},
58+
osImageDiscriminator: coreOSLabelDiscriminator,
59+
}
60+
}
61+
62+
// GetImageData analyzes container image labels to extract OS stream metadata.
63+
// Returns nil if the image does not have the required stream label.
64+
// Distinguishes between OS and extensions images based on the presence of the ostree discriminator label.
65+
func (e *ImageStreamExtractorImpl) GetImageData(image string, labels map[string]string) *ImageData {
66+
imageData := &ImageData{
67+
Image: image,
68+
Stream: findLabelValue(labels, e.streamLabels...),
69+
}
70+
if imageData.Stream == "" {
71+
// Not an OS/extensions image
72+
return nil
73+
}
74+
75+
if findLabelValue(labels, e.osImageDiscriminator) != "" {
76+
imageData.Type = ImageTypeOS
77+
} else {
78+
imageData.Type = ImageTypeExtensions
79+
}
80+
81+
return imageData
82+
}
83+
84+
// findLabelValue searches for the first matching key in the map and returns its value.
85+
// Returns an empty string if none of the keys are found.
86+
func findLabelValue(m map[string]string, keys ...string) string {
87+
for _, k := range keys {
88+
if v, ok := m[k]; ok {
89+
return v
90+
}
91+
}
92+
return ""
93+
}
94+
95+
// GroupOSContainerImageMetadataToStream groups OS and extensions images by stream name.
96+
// Combines pairs of OS and extensions images that share the same stream name into OSImageStreamSet objects.
97+
// If multiple images are found for the same stream and type, the last one wins.
98+
func GroupOSContainerImageMetadataToStream(imagesMetadata []*ImageData) []*v1alpha1.OSImageStreamSet {
99+
streamMaps := make(map[string]*v1alpha1.OSImageStreamSet)
100+
for _, imageMetadata := range imagesMetadata {
101+
streamURLSet, exists := streamMaps[imageMetadata.Stream]
102+
if !exists {
103+
streamMaps[imageMetadata.Stream] = NewOSImageStreamURLSetFromImageMetadata(imageMetadata)
104+
continue
105+
}
106+
107+
// The stream already exists. Maybe it has not both urls yet
108+
imageName := v1alpha1.ImageDigestFormat(imageMetadata.Image)
109+
if imageMetadata.Type == ImageTypeOS {
110+
if streamURLSet.OSImage != "" && streamURLSet.OSImage != imageName {
111+
// Looks like we have a conflict. Log it and override
112+
klog.V(4).Infof("multiple OS images for the same %s stream. Previous one was %s. Overriding with %s", streamURLSet.Name, streamURLSet.OSImage, imageName)
113+
}
114+
streamURLSet.OSImage = imageName
115+
} else {
116+
if streamURLSet.OSExtensionsImage != "" && streamURLSet.OSExtensionsImage != imageName {
117+
// Looks like we have a conflict. Log it and override
118+
klog.V(4).Infof("multiple OS Extensions images for the same %s stream. Previous one was %s. Overriding with %s", streamURLSet.Name, streamURLSet.OSImage, imageName)
119+
}
120+
streamURLSet.OSExtensionsImage = imageName
121+
}
122+
}
123+
return slices.Collect(maps.Values(streamMaps))
124+
}
125+
126+
// NewOSImageStreamURLSetFromImageMetadata creates an OSImageStreamSet from image metadata.
127+
// Populates either the OSImage or OSExtensionsImage field based on the image type.
128+
func NewOSImageStreamURLSetFromImageMetadata(imageMetadata *ImageData) *v1alpha1.OSImageStreamSet {
129+
urlSet := &v1alpha1.OSImageStreamSet{
130+
Name: imageMetadata.Stream,
131+
}
132+
imageName := v1alpha1.ImageDigestFormat(imageMetadata.Image)
133+
if imageMetadata.Type == ImageTypeOS {
134+
urlSet.OSImage = imageName
135+
} else {
136+
urlSet.OSExtensionsImage = imageName
137+
}
138+
return urlSet
139+
}

0 commit comments

Comments
 (0)