From 1d3edb610a2dfed02ec4779a2036170b2e3cd21f Mon Sep 17 00:00:00 2001 From: Nader Ziada Date: Mon, 27 Jul 2026 15:03:55 -0400 Subject: [PATCH 1/5] feat(machine): resolve boot disk image at reconcile time When a Machine's boot disk has no image specified, resolve it dynamically by reading the coreos-bootimages ConfigMap from the MCO namespace and parsing the stream metadata for the correct architecture-specific RHCOS image. Falls back to hardcoded defaults when the ConfigMap is unavailable. Architecture is determined via the GCP MachineTypes API, with prefix-based detection as a secondary fallback. Signed-off-by: Nader Ziada --- go.mod | 1 + go.sum | 2 + pkg/cloud/gcp/actuators/machine/boot_image.go | 104 +++++++++ .../gcp/actuators/machine/boot_image_test.go | 202 ++++++++++++++++++ pkg/cloud/gcp/actuators/machine/reconciler.go | 21 +- .../gcp/actuators/machine/reconciler_test.go | 79 ++++++- .../coreos/stream-metadata-go/LICENSE | 201 +++++++++++++++++ .../stream/artifact_utils.go | 98 +++++++++ .../stream-metadata-go/stream/rhcos/rhcos.go | 92 ++++++++ .../stream-metadata-go/stream/stream.go | 116 ++++++++++ .../stream-metadata-go/stream/stream_utils.go | 94 ++++++++ vendor/modules.txt | 4 + 12 files changed, 1009 insertions(+), 5 deletions(-) create mode 100644 pkg/cloud/gcp/actuators/machine/boot_image.go create mode 100644 pkg/cloud/gcp/actuators/machine/boot_image_test.go create mode 100644 vendor/github.com/coreos/stream-metadata-go/LICENSE create mode 100644 vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go create mode 100644 vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go create mode 100644 vendor/github.com/coreos/stream-metadata-go/stream/stream.go create mode 100644 vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go diff --git a/go.mod b/go.mod index 1699f7994..aa3474d99 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/blang/semver v3.5.1+incompatible + github.com/coreos/stream-metadata-go v0.4.11 github.com/go-logr/logr v1.4.3 github.com/googleapis/gax-go/v2 v2.15.0 github.com/onsi/ginkgo/v2 v2.28.1 diff --git a/go.sum b/go.sum index 22fc60662..b8aceb352 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= +github.com/coreos/stream-metadata-go v0.4.11 h1:sQQOpI+v/eTyYO76r7jJeCmqZcm1oQYKdwirFJ7f0M0= +github.com/coreos/stream-metadata-go v0.4.11/go.mod h1:dTE8UEFgyUcrbdUg7vGT3uIP7S8a1IwUlmWLKlOp8G8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= diff --git a/pkg/cloud/gcp/actuators/machine/boot_image.go b/pkg/cloud/gcp/actuators/machine/boot_image.go new file mode 100644 index 000000000..e2cb6f9e6 --- /dev/null +++ b/pkg/cloud/gcp/actuators/machine/boot_image.go @@ -0,0 +1,104 @@ +package machine + +import ( + "encoding/json" + "fmt" + + "github.com/coreos/stream-metadata-go/stream" + "github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/util" + corev1 "k8s.io/api/core/v1" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (r *Reconciler) resolveBootImage() (string, error) { + arch := r.resolveArchitecture() + + image, err := r.resolveImageFromConfigMap(arch) + if err != nil { + klog.V(3).Infof("Failed to resolve boot image from coreos-bootimages ConfigMap: %v, using fallback", err) + return fallbackImage(arch), nil + } + if image == "" { + klog.V(3).Infof("No GCP image found in coreos-bootimages for arch %s, using fallback", arch) + return fallbackImage(arch), nil + } + + klog.V(3).Infof("Resolved boot image from coreos-bootimages ConfigMap: %s (arch: %s)", image, arch) + return image, nil +} + +func (r *Reconciler) resolveArchitecture() util.NormalizedArch { + mt, err := r.computeService.MachineTypesGet(r.projectID, r.providerSpec.Zone, r.providerSpec.MachineType) + if err != nil || mt == nil { + klog.V(3).Infof("Failed to get machine type %s from GCP API, falling back to prefix-based detection", r.providerSpec.MachineType) + return util.CPUArchitecture(r.providerSpec.MachineType) + } + + switch mt.Architecture { + case "ARM64": + return util.ArchitectureArm64 + case "X86_64": + return util.ArchitectureAmd64 + case "", "ARCHITECTURE_UNSPECIFIED": + klog.V(3).Infof("GCP API returned no architecture for machine type %s, falling back to prefix-based detection", r.providerSpec.MachineType) + return util.CPUArchitecture(r.providerSpec.MachineType) + default: + klog.Warningf("Unknown GCP architecture %q for machine type %s, falling back to prefix-based detection", mt.Architecture, r.providerSpec.MachineType) + return util.CPUArchitecture(r.providerSpec.MachineType) + } +} + +func (r *Reconciler) resolveImageFromConfigMap(arch util.NormalizedArch) (string, error) { + var cm corev1.ConfigMap + if err := r.coreClient.Get(r.Context, client.ObjectKey{ + Namespace: coreOSBootImagesNamespace, + Name: coreOSBootImagesName, + }, &cm); err != nil { + return "", fmt.Errorf("failed to get coreos-bootimages ConfigMap: %w", err) + } + + streamData, ok := cm.Data["stream"] + if !ok { + return "", fmt.Errorf("coreos-bootimages ConfigMap missing 'stream' key") + } + + var st stream.Stream + if err := json.Unmarshal([]byte(streamData), &st); err != nil { + return "", fmt.Errorf("failed to parse stream metadata: %w", err) + } + + streamArch := archToStreamArch(arch) + archData, ok := st.Architectures[streamArch] + if !ok { + return "", fmt.Errorf("no architecture %q in stream metadata", streamArch) + } + + if archData.Images.Gcp == nil { + return "", fmt.Errorf("no GCP image entry for architecture %q in stream metadata", streamArch) + } + + return gcpImageReference(archData.Images.Gcp.Project, archData.Images.Gcp.Name), nil +} + +func archToStreamArch(arch util.NormalizedArch) string { + switch arch { + case util.ArchitectureArm64: + return "aarch64" + case util.ArchitectureAmd64: + return "x86_64" + default: + return "x86_64" + } +} + +func fallbackImage(arch util.NormalizedArch) string { + if arch == util.ArchitectureArm64 { + return defaultGCPBootImageARM + } + return defaultGCPBootImageX86 +} + +func gcpImageReference(project, name string) string { + return fmt.Sprintf("projects/%s/global/images/%s", project, name) +} diff --git a/pkg/cloud/gcp/actuators/machine/boot_image_test.go b/pkg/cloud/gcp/actuators/machine/boot_image_test.go new file mode 100644 index 000000000..f4daff13e --- /dev/null +++ b/pkg/cloud/gcp/actuators/machine/boot_image_test.go @@ -0,0 +1,202 @@ +package machine + +import ( + "context" + "fmt" + "testing" + + machinev1 "github.com/openshift/api/machine/v1beta1" + computeservice "github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/services/compute" + "github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/util" + compute "google.golang.org/api/compute/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + controllerfake "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const testStreamJSON = `{ + "stream": "stable", + "metadata": {"last-modified": "2024-01-01T00:00:00Z"}, + "architectures": { + "x86_64": { + "artifacts": {}, + "images": { + "gcp": { + "release": "418.stable", + "project": "rhcos-cloud", + "name": "rhcos-418-stable-x86-64" + } + } + }, + "aarch64": { + "artifacts": {}, + "images": { + "gcp": { + "release": "418.stable", + "project": "rhcos-cloud", + "name": "rhcos-418-stable-aarch64" + } + } + } + } +}` + +func testBootImagesConfigMap() *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreOSBootImagesName, + Namespace: coreOSBootImagesNamespace, + }, + Data: map[string]string{ + "stream": testStreamJSON, + }, + } +} + +func TestResolveBootImage(t *testing.T) { + cases := []struct { + name string + machineType string + mockMachineType *compute.MachineType + mockMachineTypeErr error + configMap *corev1.ConfigMap + expectedImage string + }{ + { + name: "x86_64 machine type resolves from ConfigMap", + machineType: "n2-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "X86_64", + }, + configMap: testBootImagesConfigMap(), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + }, + { + name: "ARM64 machine type resolves from ConfigMap", + machineType: "t2a-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "ARM64", + }, + configMap: testBootImagesConfigMap(), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-aarch64"), + }, + { + name: "missing ConfigMap falls back to x86 default", + machineType: "n2-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "X86_64", + }, + configMap: nil, + expectedImage: defaultGCPBootImageX86, + }, + { + name: "missing ConfigMap falls back to ARM default", + machineType: "t2a-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "ARM64", + }, + configMap: nil, + expectedImage: defaultGCPBootImageARM, + }, + { + name: "MachineType API failure uses prefix-based arch and fallback image", + machineType: "n2-standard-4", + mockMachineTypeErr: fmt.Errorf("API unavailable"), + configMap: nil, + expectedImage: defaultGCPBootImageX86, + }, + { + name: "MachineType API failure with ARM prefix uses ARM fallback", + machineType: "t2a-standard-4", + mockMachineTypeErr: fmt.Errorf("API unavailable"), + configMap: nil, + expectedImage: defaultGCPBootImageARM, + }, + { + name: "empty Architecture field falls back to prefix-based detection", + machineType: "t2a-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "", + }, + configMap: testBootImagesConfigMap(), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-aarch64"), + }, + { + name: "ARCHITECTURE_UNSPECIFIED falls back to prefix-based detection", + machineType: "n2-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "ARCHITECTURE_UNSPECIFIED", + }, + configMap: testBootImagesConfigMap(), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + mockComputeService := &computeservice.GCPComputeServiceMock{ + MockMachineTypesGet: func(project, zone, mt string) (*compute.MachineType, error) { + if tc.mockMachineTypeErr != nil { + return nil, tc.mockMachineTypeErr + } + return tc.mockMachineType, nil + }, + } + + clientBuilder := controllerfake.NewClientBuilder().WithScheme(scheme.Scheme) + if tc.configMap != nil { + clientBuilder.WithObjects(tc.configMap) + } + fakeClient := clientBuilder.Build() + + r := &Reconciler{ + machineScope: &machineScope{ + Context: context.Background(), + coreClient: fakeClient, + computeService: mockComputeService, + projectID: "test-project", + providerSpec: &machinev1.GCPMachineProviderSpec{ + MachineType: tc.machineType, + Zone: "us-central1-a", + }, + }, + } + + image, err := r.resolveBootImage() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if image != tc.expectedImage { + t.Errorf("expected image %q, got %q", tc.expectedImage, image) + } + }) + } +} + +func TestArchToStreamArch(t *testing.T) { + cases := []struct { + input util.NormalizedArch + expected string + }{ + {util.ArchitectureArm64, "aarch64"}, + {util.ArchitectureAmd64, "x86_64"}, + {util.NormalizedArch("unknown"), "x86_64"}, + } + for _, tc := range cases { + t.Run(string(tc.input), func(t *testing.T) { + got := archToStreamArch(tc.input) + if got != tc.expected { + t.Errorf("archToStreamArch(%q) = %q, want %q", tc.input, got, tc.expected) + } + }) + } +} + +func TestGcpImageReference(t *testing.T) { + got := gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64") + expected := "projects/rhcos-cloud/global/images/rhcos-418-stable-x86-64" + if got != expected { + t.Errorf("gcpImageReference() = %q, want %q", got, expected) + } +} diff --git a/pkg/cloud/gcp/actuators/machine/reconciler.go b/pkg/cloud/gcp/actuators/machine/reconciler.go index 601c8e224..dd87228cc 100644 --- a/pkg/cloud/gcp/actuators/machine/reconciler.go +++ b/pkg/cloud/gcp/actuators/machine/reconciler.go @@ -35,6 +35,11 @@ const ( windowsScriptMetadataKey = "sysprep-specialize-script-ps1" openshiftMachineRoleLabel = "machine.openshift.io/cluster-api-machine-role" masterMachineRole = "master" + + defaultGCPBootImageX86 = "projects/rhcos-cloud/global/images/rhcos-414-92-202311241643-0-gcp-x86-64" + defaultGCPBootImageARM = "projects/rhcos-cloud/global/images/rhcos-414-92-202311241643-0-gcp-aarch64" + coreOSBootImagesNamespace = "openshift-machine-config-operator" + coreOSBootImagesName = "coreos-bootimages" ) // Reconciler are list of services required by machine actuator, easy to create a fake @@ -238,6 +243,20 @@ func (r *Reconciler) create() error { instance.Scheduling.AutomaticRestart = automaticRestart } + // Resolve empty boot disk images before the UEFI check, which needs a + // concrete image to query GCP's ImageGet API. + for _, disk := range r.providerSpec.Disks { + if disk.Boot && disk.Image == "" { + resolved, err := r.resolveBootImage() + if err != nil { + return fmt.Errorf("failed to resolve boot disk image: %w", err) + } + disk.Image = resolved + klog.Infof("Resolved boot disk image for machine %s: %s", r.machine.Name, resolved) + break + } + } + // This is mostly to smooth off a rough edge, and hopefully should not be a // case that is hit often. If an existing machineset has a non UEFI // compatible disk, the check in the machineset controller should explicitly @@ -321,7 +340,7 @@ func (r *Reconciler) create() error { srcImage = googleapi.ResolveRelative(r.computeService.BasePath(), fmt.Sprintf("projects/%s/global/images/%s", r.projectID, disk.Image)) } } else if disk.Boot { - return machinecontroller.InvalidMachineConfiguration("boot disk must specify an image") + return fmt.Errorf("failed to resolve boot disk image for machine %s", r.machine.Name) } labels, err := util.GetLabelsList(r.coreClient, r.machine.Labels[machinev1.MachineClusterIDLabel], disk.Labels) diff --git a/pkg/cloud/gcp/actuators/machine/reconciler_test.go b/pkg/cloud/gcp/actuators/machine/reconciler_test.go index 7c3041811..725103118 100644 --- a/pkg/cloud/gcp/actuators/machine/reconciler_test.go +++ b/pkg/cloud/gcp/actuators/machine/reconciler_test.go @@ -35,6 +35,7 @@ func TestCreate(t *testing.T) { mockGPUCompatibleMachineTypesList func(project string, zone string, ctx context.Context) (map[string]computeservice.GpuInfo, []string) mockInstancesInsert func(project string, zone string, instance *compute.Instance) (*compute.Operation, error) mockRegionGet func(project string, region string) (*compute.Region, error) + mockMachineTypesGet func(project string, zone string, machineType string) (*compute.MachineType, error) validateInstance func(t *testing.T, instance *compute.Instance) expectedError error }{ @@ -387,16 +388,40 @@ func TestCreate(t *testing.T) { }, }, { - name: "Boot disk without image fails validation", + name: "Boot disk without image resolves fallback image", providerSpec: &machinev1.GCPMachineProviderSpec{ + Zone: "us-central1-a", + MachineType: "n2-standard-4", + ProjectID: "testProject", Disks: []*machinev1.GCPDisk{ { - Boot: true, - Image: "", + Boot: true, + Image: "", + SizeGB: 128, + Type: "pd-ssd", + AutoDelete: true, }, }, + NetworkInterfaces: []*machinev1.GCPNetworkInterface{ + {Network: "default", Subnetwork: "default"}, + }, + Region: "us-central1", + ServiceAccounts: []machinev1.GCPServiceAccount{{Email: "test@test.iam.gserviceaccount.com", Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}}}, + }, + expectedCondition: &metav1.Condition{ + Type: string(machinev1.MachineCreated), + Status: metav1.ConditionTrue, + Reason: machineCreationSucceedReason, + Message: machineCreationSucceedMessage, + }, + validateInstance: func(t *testing.T, instance *compute.Instance) { + if len(instance.Disks) != 1 { + t.Fatalf("expected 1 disk, got %d", len(instance.Disks)) + } + if instance.Disks[0].InitializeParams.SourceImage == "" { + t.Error("expected boot disk SourceImage to be resolved, got empty") + } }, - expectedError: machinecontroller.InvalidMachineConfiguration("boot disk must specify an image"), }, { name: "Secondary disk without image creates blank disk", @@ -873,6 +898,48 @@ func TestCreate(t *testing.T) { }, expectedError: errors.New("failed validating machine provider spec: preemptible cannot be used together with 'Spot' provisioning model"), }, + { + name: "Boot disk with empty image resolves from machine type architecture", + providerSpec: &machinev1.GCPMachineProviderSpec{ + Zone: "us-central1-a", + MachineType: "n2-standard-4", + ProjectID: "testProject", + Disks: []*machinev1.GCPDisk{ + { + Boot: true, + Image: "", + SizeGB: 128, + Type: "pd-ssd", + AutoDelete: true, + }, + }, + NetworkInterfaces: []*machinev1.GCPNetworkInterface{ + {Network: "default", Subnetwork: "default"}, + }, + Region: "us-central1", + ServiceAccounts: []machinev1.GCPServiceAccount{{Email: "test@test.iam.gserviceaccount.com", Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}}}, + }, + mockMachineTypesGet: func(project, zone, machineType string) (*compute.MachineType, error) { + return &compute.MachineType{Architecture: "X86_64"}, nil + }, + expectedCondition: &metav1.Condition{ + Type: string(machinev1.MachineCreated), + Status: metav1.ConditionTrue, + Reason: machineCreationSucceedReason, + Message: machineCreationSucceedMessage, + }, + validateInstance: func(t *testing.T, instance *compute.Instance) { + if len(instance.Disks) != 1 { + t.Fatalf("expected 1 disk, got %d", len(instance.Disks)) + } + if instance.Disks[0].InitializeParams.SourceImage == "" { + t.Error("expected boot disk SourceImage to be resolved, got empty") + } + if !strings.Contains(instance.Disks[0].InitializeParams.SourceImage, "rhcos") { + t.Errorf("expected RHCOS image, got %s", instance.Disks[0].InitializeParams.SourceImage) + } + }, + }, } mockTagService := tagservice.NewMockTagService() @@ -983,6 +1050,10 @@ func TestCreate(t *testing.T) { mockComputeService.MockRegionGet = tc.mockRegionGet } + if tc.mockMachineTypesGet != nil { + mockComputeService.MockMachineTypesGet = tc.mockMachineTypesGet + } + err = reconciler.create() if tc.expectedCondition != nil { diff --git a/vendor/github.com/coreos/stream-metadata-go/LICENSE b/vendor/github.com/coreos/stream-metadata-go/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go b/vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go new file mode 100644 index 000000000..312a94b7b --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go @@ -0,0 +1,98 @@ +package stream + +import ( + "crypto/sha256" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" +) + +// Fetch an artifact, validating its checksum. If applicable, +// the artifact will not be decompressed. Does not +// validate GPG signature. +func (a *Artifact) Fetch(w io.Writer) error { + resp, err := http.Get(a.Location) + if err != nil { + return err + } + defer func() { + err = errors.Join(resp.Body.Close()) + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%s returned status: %s", a.Location, resp.Status) + } + hasher := sha256.New() + reader := io.TeeReader(resp.Body, hasher) + + _, err = io.Copy(w, reader) + if err != nil { + return err + } + + // Validate sha256 checksum + foundChecksum := fmt.Sprintf("%x", hasher.Sum(nil)) + if a.Sha256 != foundChecksum { + return fmt.Errorf("checksum mismatch for %s; expected=%s found=%s", a.Location, a.Sha256, foundChecksum) + } + + return nil +} + +// Name returns the "basename" of the artifact, i.e. the contents +// after the last `/`. This can be useful when downloading to a file. +func (a *Artifact) Name() (string, error) { + loc, err := url.Parse(a.Location) + if err != nil { + return "", fmt.Errorf("failed to parse artifact url: %w", err) + } + // Note this one uses `path` since even on Windows URLs have forward slashes. + return path.Base(loc.Path), nil +} + +// Download fetches the specified artifact and saves it to the target +// directory. The full file path will be returned as a string. +// If the target file path exists, it will be overwritten. +// If the download fails, the temporary file will be deleted. +func (a *Artifact) Download(destdir string) (string, error) { + name, err := a.Name() + if err != nil { + return "", err + } + destfile := filepath.Join(destdir, name) + w, err := os.CreateTemp(destdir, ".coreos-artifact-") + if err != nil { + return "", err + } + finalized := false + defer func() { + if !finalized { + // Ignore an error to unlink + _ = os.Remove(w.Name()) + } + }() + + if err := a.Fetch(w); err != nil { + return "", err + } + if err := w.Sync(); err != nil { + return "", err + } + if err := w.Chmod(0644); err != nil { + return "", err + } + if err := w.Close(); err != nil { + return "", err + } + if err := os.Rename(w.Name(), destfile); err != nil { + return "", err + } + finalized = true + + return destfile, nil +} diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go b/vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go new file mode 100644 index 000000000..62616b3b2 --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go @@ -0,0 +1,92 @@ +package rhcos + +import "fmt" + +// Extensions is data specific to Red Hat Enterprise Linux CoreOS +type Extensions struct { + AwsWinLi *AwsWinLi `json:"aws-winli,omitempty"` + AzureDisk *AzureDisk `json:"azure-disk,omitempty"` + Marketplace *Marketplace `json:"marketplace,omitempty"` +} + +// AzureDisk represents an Azure disk image that can be imported +// into an image gallery or otherwise replicated, and then used +// as a boot source for virtual machines. +type AzureDisk struct { + // Release is the source release version + Release string `json:"release"` + // URL to an image already stored in Azure infrastructure + // that can be copied into an image gallery. Avoid creating VMs directly + // from this URL as that may lead to performance limitations. + URL string `json:"url,omitempty"` +} + +// AwsWinLi represents prebuilt AWS Windows License Included Images. +type AwsWinLi = ReplicatedImage + +// ReplicatedImage represents an image in all regions of an AWS-like cloud +// This struct was copied from the release package to avoid an import cycle, +// and is used to describe all AWS WinLI Images in all regions. +type ReplicatedImage struct { + Regions map[string]SingleImage `json:"regions,omitempty"` +} + +// SingleImage represents a globally-accessible image or an image in a +// single region of an AWS-like cloud +// This struct was copied from the release package to avoid an import cycle, +// and is used to describe individual AWS WinLI Images. +type SingleImage struct { + Release string `json:"release"` + Image string `json:"image"` +} + +// Marketplace contains marketplace images for all clouds. +type Marketplace struct { + Azure *AzureMarketplace `json:"azure,omitempty"` +} + +// AzureMarketplaceImages contains both the HyperV- Gen1 & Gen2 +// images for a purchase plan. +type AzureMarketplaceImages struct { + Gen1 *AzureMarketplaceImage `json:"hyperVGen1,omitempty"` + Gen2 *AzureMarketplaceImage `json:"hyperVGen2,omitempty"` +} + +// AzureMarketplace lists images, both paid and +// unpaid, available in the Azure marketplace. +type AzureMarketplace struct { + // NoPurchasePlan is the standard, unpaid RHCOS image. + NoPurchasePlan *AzureMarketplaceImages `json:"no-purchase-plan,omitempty"` + + // OCP is the paid marketplace image for OpenShift Container Platform. + OCP *AzureMarketplaceImages `json:"ocp,omitempty"` + + // OPP is the paid marketplace image for OpenShift Platform Plus. + OPP *AzureMarketplaceImages `json:"opp,omitempty"` + + // OKE is the paid marketplace image for OpenShift Kubernetes Engine. + OKE *AzureMarketplaceImages `json:"oke,omitempty"` + + // OCPEMEA is the paid marketplace image for OpenShift Container Platform in EMEA regions. + OCPEMEA *AzureMarketplaceImages `json:"ocp-emea,omitempty"` + + // OPPEMEA is the paid marketplace image for OpenShift Platform Plus in EMEA regions. + OPPEMEA *AzureMarketplaceImages `json:"opp-emea,omitempty"` + + // OKEEMEA is the paid marketplace image for OpenShift Kubernetes Engine in EMEA regions. + OKEEMEA *AzureMarketplaceImages `json:"oke-emea,omitempty"` +} + +// AzureMarketplaceImage defines the attributes for an Azure +// marketplace image. +type AzureMarketplaceImage struct { + Publisher string `json:"publisher"` + Offer string `json:"offer"` + SKU string `json:"sku"` + Version string `json:"version"` +} + +// URN returns the image URN for the marketplace image. +func (i *AzureMarketplaceImage) URN() string { + return fmt.Sprintf("%s:%s:%s:%s", i.Publisher, i.Offer, i.SKU, i.Version) +} diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/stream.go b/vendor/github.com/coreos/stream-metadata-go/stream/stream.go new file mode 100644 index 000000000..1ed9b1fd7 --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/stream.go @@ -0,0 +1,116 @@ +// Package stream models a CoreOS "stream", which is +// a description of the recommended set of binary images for CoreOS. Use +// this API to find cloud images, bare metal disk images, etc. +package stream + +import ( + "github.com/coreos/stream-metadata-go/stream/rhcos" +) + +// Stream contains artifacts available in a stream +type Stream struct { + Stream string `json:"stream"` + Metadata Metadata `json:"metadata"` + Architectures map[string]Arch `json:"architectures"` +} + +// Metadata for a release or stream +type Metadata struct { + LastModified string `json:"last-modified"` + Generator string `json:"generator,omitempty"` +} + +// Arch contains release details for a particular hardware architecture +type Arch struct { + Artifacts map[string]PlatformArtifacts `json:"artifacts"` + Images Images `json:"images,omitempty"` + // RHELCoreOSExtensions is data specific to Red Hat Enterprise Linux CoreOS + RHELCoreOSExtensions *rhcos.Extensions `json:"rhel-coreos-extensions,omitempty"` +} + +// PlatformArtifacts contains images for a platform +type PlatformArtifacts struct { + Release string `json:"release"` + Formats map[string]ImageFormat `json:"formats"` +} + +// ImageFormat contains all artifacts for a single OS image +type ImageFormat struct { + Disk *Artifact `json:"disk,omitempty"` + Kernel *Artifact `json:"kernel,omitempty"` + Initramfs *Artifact `json:"initramfs,omitempty"` + Rootfs *Artifact `json:"rootfs,omitempty"` +} + +// Artifact represents one image file, plus its metadata +type Artifact struct { + Location string `json:"location"` + Signature string `json:"signature,omitempty"` + Sha256 string `json:"sha256"` + UncompressedSha256 string `json:"uncompressed-sha256,omitempty"` +} + +// Images contains images available in cloud providers +type Images struct { + Aliyun *ReplicatedImage `json:"aliyun,omitempty"` + Aws *AwsImage `json:"aws,omitempty"` + Gcp *GcpImage `json:"gcp,omitempty"` + Ibmcloud *ReplicatedObject `json:"ibmcloud,omitempty"` + KubeVirt *ContainerImage `json:"kubevirt,omitempty"` + PowerVS *ReplicatedObject `json:"powervs,omitempty"` +} + +// ReplicatedImage represents an image in all regions of an AWS-like cloud +type ReplicatedImage struct { + Regions map[string]SingleImage `json:"regions,omitempty"` +} + +// SingleImage represents a globally-accessible image or an image in a +// single region of an AWS-like cloud +type SingleImage struct { + Release string `json:"release"` + Image string `json:"image"` +} + +// ContainerImage represents a tagged container image +type ContainerImage struct { + Release string `json:"release"` + // Preferred way to reference the image, which might be by tag or digest + Image string `json:"image"` + DigestRef string `json:"digest-ref"` +} + +// AwsImage is a typedef for backwards compatibility. +type AwsImage = ReplicatedImage + +// AwsRegionImage is a typedef for backwards compatibility. +type AwsRegionImage = SingleImage + +// RegionImage is a typedef for backwards compatibility. +type RegionImage = SingleImage + +// GcpImage represents a GCP cloud image +type GcpImage struct { + Release string `json:"release"` + Project string `json:"project"` + Family string `json:"family,omitempty"` + Name string `json:"name"` +} + +// ReplicatedObject represents an object in all regions of an IBMCloud-like +// cloud +type ReplicatedObject struct { + Regions map[string]SingleObject `json:"regions,omitempty"` +} + +// SingleObject represents a globally-accessible cloud storage object, or +// an object in a single region of an IBMCloud-like cloud +type SingleObject struct { + Release string `json:"release"` + Object string `json:"object"` + Bucket string `json:"bucket"` + Url string `json:"url"` +} + +// RegionObject is a typedef for backwards compatibility. +type RegionObject = SingleObject diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go b/vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go new file mode 100644 index 000000000..b1ca95564 --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go @@ -0,0 +1,94 @@ +package stream + +import "fmt" + +// FormatPrefix describes a stream+architecture combination, intended for prepending to error messages +func (st *Stream) FormatPrefix(archname string) string { + return fmt.Sprintf("%s/%s", st.Stream, archname) +} + +// GetArchitecture loads the architecture-specific builds from a stream, +// with a useful descriptive error message if the architecture is not found. +func (st *Stream) GetArchitecture(archname string) (*Arch, error) { + archdata, ok := st.Architectures[archname] + if !ok { + return nil, fmt.Errorf("stream:%s does not have architecture '%s'", st.Stream, archname) + } + return &archdata, nil +} + +// GetAliyunRegionImage returns the release data (Image ID and release ID) for a particular +// architecture and region. +func (st *Stream) GetAliyunRegionImage(archname, region string) (*SingleImage, error) { + starch, err := st.GetArchitecture(archname) + if err != nil { + return nil, err + } + aliyunimages := starch.Images.Aliyun + if aliyunimages == nil { + return nil, fmt.Errorf("%s: No Aliyun images", st.FormatPrefix(archname)) + } + var regionVal SingleImage + var ok bool + if regionVal, ok = aliyunimages.Regions[region]; !ok { + return nil, fmt.Errorf("%s: No Aliyun images in region %s", st.FormatPrefix(archname), region) + } + + return ®ionVal, nil +} + +// GetAliyunImage returns the Aliyun image for a particular architecture and region. +func (st *Stream) GetAliyunImage(archname, region string) (string, error) { + regionVal, err := st.GetAliyunRegionImage(archname, region) + if err != nil { + return "", err + } + return regionVal.Image, nil +} + +// GetAwsRegionImage returns the release data (AMI and release ID) for a particular +// architecture and region. +func (st *Stream) GetAwsRegionImage(archname, region string) (*SingleImage, error) { + starch, err := st.GetArchitecture(archname) + if err != nil { + return nil, err + } + awsimages := starch.Images.Aws + if awsimages == nil { + return nil, fmt.Errorf("%s: No AWS images", st.FormatPrefix(archname)) + } + var regionVal SingleImage + var ok bool + if regionVal, ok = awsimages.Regions[region]; !ok { + return nil, fmt.Errorf("%s: No AWS images in region %s", st.FormatPrefix(archname), region) + } + + return ®ionVal, nil +} + +// GetAMI returns the AWS machine image for a particular architecture and region. +func (st *Stream) GetAMI(archname, region string) (string, error) { + regionVal, err := st.GetAwsRegionImage(archname, region) + if err != nil { + return "", err + } + return regionVal.Image, nil +} + +// QueryDisk finds the singleton disk artifact for a given format and architecture. +func (st *Stream) QueryDisk(architectureName, artifactName, formatName string) (*Artifact, error) { + arch, err := st.GetArchitecture(architectureName) + if err != nil { + return nil, err + } + artifacts := arch.Artifacts[artifactName] + if artifacts.Release == "" { + return nil, fmt.Errorf("%s: artifact '%s' not found", st.FormatPrefix(architectureName), artifactName) + } + format := artifacts.Formats[formatName] + if format.Disk == nil { + return nil, fmt.Errorf("%s: artifact '%s' format '%s' disk not found", st.FormatPrefix(architectureName), artifactName, formatName) + } + + return format.Disk, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index fcc95ae25..6a0dd7897 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -50,6 +50,10 @@ github.com/chai2010/gettext-go github.com/chai2010/gettext-go/mo github.com/chai2010/gettext-go/plural github.com/chai2010/gettext-go/po +# github.com/coreos/stream-metadata-go v0.4.11 +## explicit; go 1.18 +github.com/coreos/stream-metadata-go/stream +github.com/coreos/stream-metadata-go/stream/rhcos # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew From 7d5374385e89e0ad28837b4d2d8475fbbd9e519f Mon Sep 17 00:00:00 2001 From: Nader Ziada Date: Fri, 31 Jul 2026 15:18:46 -0400 Subject: [PATCH 2/5] use streams key from coreos-bootimages ConfigMap The coreos-bootimages ConfigMap now contains a streams key with per-stream boot image data (rhel-9, rhel-10) for OCP 5.0. Read the OSImageStream cluster singleton to determine the active stream and resolve the correct boot image from the streams key. Falls back to the deprecated stream key for backwards compatibility during upgrades. Signed-off-by: Nader Ziada --- pkg/cloud/gcp/actuators/machine/boot_image.go | 64 +++++++- .../gcp/actuators/machine/boot_image_test.go | 142 +++++++++++++++++- pkg/cloud/gcp/actuators/machine/reconciler.go | 3 + 3 files changed, 199 insertions(+), 10 deletions(-) diff --git a/pkg/cloud/gcp/actuators/machine/boot_image.go b/pkg/cloud/gcp/actuators/machine/boot_image.go index e2cb6f9e6..c68eabd20 100644 --- a/pkg/cloud/gcp/actuators/machine/boot_image.go +++ b/pkg/cloud/gcp/actuators/machine/boot_image.go @@ -7,10 +7,19 @@ import ( "github.com/coreos/stream-metadata-go/stream" "github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/util" corev1 "k8s.io/api/core/v1" + apimachineryerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" ) +var osImageStreamGVK = schema.GroupVersionKind{ + Group: "machineconfiguration.openshift.io", + Version: "v1", + Kind: "OSImageStream", +} + func (r *Reconciler) resolveBootImage() (string, error) { arch := r.resolveArchitecture() @@ -58,9 +67,9 @@ func (r *Reconciler) resolveImageFromConfigMap(arch util.NormalizedArch) (string return "", fmt.Errorf("failed to get coreos-bootimages ConfigMap: %w", err) } - streamData, ok := cm.Data["stream"] - if !ok { - return "", fmt.Errorf("coreos-bootimages ConfigMap missing 'stream' key") + streamData, err := r.resolveStreamData(cm.Data) + if err != nil { + return "", err } var st stream.Stream @@ -81,6 +90,55 @@ func (r *Reconciler) resolveImageFromConfigMap(arch util.NormalizedArch) (string return gcpImageReference(archData.Images.Gcp.Project, archData.Images.Gcp.Name), nil } +func (r *Reconciler) resolveStreamData(cmData map[string]string) (string, error) { + streamsRaw, hasStreams := cmData["streams"] + if hasStreams { + streamName := r.resolveActiveStreamName() + var streams map[string]json.RawMessage + if err := json.Unmarshal([]byte(streamsRaw), &streams); err != nil { + return "", fmt.Errorf("failed to parse streams data from ConfigMap: %w", err) + } + + data, ok := streams[streamName] + if !ok { + return "", fmt.Errorf("stream %q not found in coreos-bootimages ConfigMap streams key", streamName) + } + return string(data), nil + } + + streamData, hasStream := cmData["stream"] + if hasStream { + klog.V(3).Info("coreos-bootimages ConfigMap missing 'streams' key, falling back to deprecated 'stream' key") + return streamData, nil + } + + return "", fmt.Errorf("coreos-bootimages ConfigMap missing both 'streams' and 'stream' keys") +} + +func (r *Reconciler) resolveActiveStreamName() string { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(osImageStreamGVK) + + err := r.coreClient.Get(r.Context, client.ObjectKey{Name: osImageStreamName}, obj) + if err != nil { + if apimachineryerrors.IsNotFound(err) { + klog.V(3).Infof("OSImageStream CR not found, defaulting to stream %q", defaultOSStreamName) + } else { + klog.Warningf("Failed to get OSImageStream CR: %v, defaulting to stream %q", err, defaultOSStreamName) + } + return defaultOSStreamName + } + + defaultStream, found, err := unstructured.NestedString(obj.Object, "spec", "defaultStream") + if err != nil || !found || defaultStream == "" { + klog.V(3).Infof("OSImageStream CR has no spec.defaultStream set, defaulting to stream %q", defaultOSStreamName) + return defaultOSStreamName + } + + klog.V(3).Infof("Resolved active OS stream from OSImageStream CR: %s", defaultStream) + return defaultStream +} + func archToStreamArch(arch util.NormalizedArch) string { switch arch { case util.ArchitectureArm64: diff --git a/pkg/cloud/gcp/actuators/machine/boot_image_test.go b/pkg/cloud/gcp/actuators/machine/boot_image_test.go index f4daff13e..ea0c884ac 100644 --- a/pkg/cloud/gcp/actuators/machine/boot_image_test.go +++ b/pkg/cloud/gcp/actuators/machine/boot_image_test.go @@ -2,6 +2,7 @@ package machine import ( "context" + "encoding/json" "fmt" "testing" @@ -10,12 +11,16 @@ import ( "github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/util" compute "google.golang.org/api/compute/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/scheme" controllerfake "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -const testStreamJSON = `{ +const testStreamRHEL9JSON = `{ "stream": "stable", "metadata": {"last-modified": "2024-01-01T00:00:00Z"}, "architectures": { @@ -42,6 +47,42 @@ const testStreamJSON = `{ } }` +const testStreamRHEL10JSON = `{ + "stream": "stable", + "metadata": {"last-modified": "2025-06-01T00:00:00Z"}, + "architectures": { + "x86_64": { + "artifacts": {}, + "images": { + "gcp": { + "release": "420.stable", + "project": "rhcos-cloud", + "name": "rhcos-420-stable-x86-64" + } + } + }, + "aarch64": { + "artifacts": {}, + "images": { + "gcp": { + "release": "420.stable", + "project": "rhcos-cloud", + "name": "rhcos-420-stable-aarch64" + } + } + } + } +}` + +func testStreamsJSON() string { + streams := map[string]json.RawMessage{ + "rhel-9": json.RawMessage(testStreamRHEL9JSON), + "rhel-10": json.RawMessage(testStreamRHEL10JSON), + } + data, _ := json.Marshal(streams) + return string(data) +} + func testBootImagesConfigMap() *corev1.ConfigMap { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -49,11 +90,34 @@ func testBootImagesConfigMap() *corev1.ConfigMap { Namespace: coreOSBootImagesNamespace, }, Data: map[string]string{ - "stream": testStreamJSON, + "streams": testStreamsJSON(), }, } } +func testBootImagesConfigMapLegacy() *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreOSBootImagesName, + Namespace: coreOSBootImagesNamespace, + }, + Data: map[string]string{ + "stream": testStreamRHEL9JSON, + }, + } +} + +func testOSImageStream(t *testing.T, defaultStream string) *unstructured.Unstructured { + t.Helper() + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(osImageStreamGVK) + obj.SetName(osImageStreamName) + if err := unstructured.SetNestedField(obj.Object, defaultStream, "spec", "defaultStream"); err != nil { + t.Fatalf("failed to set defaultStream on OSImageStream fixture: %v", err) + } + return obj +} + func TestResolveBootImage(t *testing.T) { cases := []struct { name string @@ -61,25 +125,58 @@ func TestResolveBootImage(t *testing.T) { mockMachineType *compute.MachineType mockMachineTypeErr error configMap *corev1.ConfigMap + osImageStream *unstructured.Unstructured expectedImage string }{ { - name: "x86_64 machine type resolves from ConfigMap", + name: "x86_64 resolves rhel-10 image when OSImageStream defaults to rhel-10", machineType: "n2-standard-4", mockMachineType: &compute.MachineType{ Architecture: "X86_64", }, configMap: testBootImagesConfigMap(), - expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + osImageStream: testOSImageStream(t, "rhel-10"), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-420-stable-x86-64"), }, { - name: "ARM64 machine type resolves from ConfigMap", + name: "ARM64 resolves rhel-10 image when OSImageStream defaults to rhel-10", machineType: "t2a-standard-4", mockMachineType: &compute.MachineType{ Architecture: "ARM64", }, configMap: testBootImagesConfigMap(), - expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-aarch64"), + osImageStream: testOSImageStream(t, "rhel-10"), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-420-stable-aarch64"), + }, + { + name: "x86_64 resolves rhel-9 image when OSImageStream defaults to rhel-9", + machineType: "n2-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "X86_64", + }, + configMap: testBootImagesConfigMap(), + osImageStream: testOSImageStream(t, "rhel-9"), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + }, + { + name: "defaults to rhel-9 when OSImageStream CR not found", + machineType: "n2-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "X86_64", + }, + configMap: testBootImagesConfigMap(), + osImageStream: nil, + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + }, + { + name: "falls back to deprecated stream key when streams key missing", + machineType: "n2-standard-4", + mockMachineType: &compute.MachineType{ + Architecture: "X86_64", + }, + configMap: testBootImagesConfigMapLegacy(), + osImageStream: nil, + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), }, { name: "missing ConfigMap falls back to x86 default", @@ -88,6 +185,7 @@ func TestResolveBootImage(t *testing.T) { Architecture: "X86_64", }, configMap: nil, + osImageStream: nil, expectedImage: defaultGCPBootImageX86, }, { @@ -97,6 +195,7 @@ func TestResolveBootImage(t *testing.T) { Architecture: "ARM64", }, configMap: nil, + osImageStream: nil, expectedImage: defaultGCPBootImageARM, }, { @@ -104,6 +203,7 @@ func TestResolveBootImage(t *testing.T) { machineType: "n2-standard-4", mockMachineTypeErr: fmt.Errorf("API unavailable"), configMap: nil, + osImageStream: nil, expectedImage: defaultGCPBootImageX86, }, { @@ -111,6 +211,7 @@ func TestResolveBootImage(t *testing.T) { machineType: "t2a-standard-4", mockMachineTypeErr: fmt.Errorf("API unavailable"), configMap: nil, + osImageStream: nil, expectedImage: defaultGCPBootImageARM, }, { @@ -120,6 +221,7 @@ func TestResolveBootImage(t *testing.T) { Architecture: "", }, configMap: testBootImagesConfigMap(), + osImageStream: nil, expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-aarch64"), }, { @@ -129,10 +231,17 @@ func TestResolveBootImage(t *testing.T) { Architecture: "ARCHITECTURE_UNSPECIFIED", }, configMap: testBootImagesConfigMap(), + osImageStream: nil, expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), }, } + osImageStreamGVR := schema.GroupVersionResource{ + Group: osImageStreamGVK.Group, + Version: osImageStreamGVK.Version, + Resource: "osimagestreams", + } + for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { mockComputeService := &computeservice.GCPComputeServiceMock{ @@ -144,10 +253,23 @@ func TestResolveBootImage(t *testing.T) { }, } - clientBuilder := controllerfake.NewClientBuilder().WithScheme(scheme.Scheme) + s := runtime.NewScheme() + if err := scheme.AddToScheme(s); err != nil { + t.Fatalf("failed to add scheme: %v", err) + } + s.AddKnownTypeWithName( + osImageStreamGVK, + &unstructured.Unstructured{}, + ) + + clientBuilder := controllerfake.NewClientBuilder().WithScheme(s) if tc.configMap != nil { clientBuilder.WithObjects(tc.configMap) } + if tc.osImageStream != nil { + clientBuilder.WithRESTMapper(newFakeRESTMapper(osImageStreamGVK, osImageStreamGVR)) + clientBuilder.WithObjects(tc.osImageStream) + } fakeClient := clientBuilder.Build() r := &Reconciler{ @@ -200,3 +322,9 @@ func TestGcpImageReference(t *testing.T) { t.Errorf("gcpImageReference() = %q, want %q", got, expected) } } + +func newFakeRESTMapper(gvk schema.GroupVersionKind, gvr schema.GroupVersionResource) meta.RESTMapper { + m := meta.NewDefaultRESTMapper([]schema.GroupVersion{gvk.GroupVersion()}) + m.Add(gvk, meta.RESTScopeRoot) + return m +} diff --git a/pkg/cloud/gcp/actuators/machine/reconciler.go b/pkg/cloud/gcp/actuators/machine/reconciler.go index dd87228cc..9875e4ce4 100644 --- a/pkg/cloud/gcp/actuators/machine/reconciler.go +++ b/pkg/cloud/gcp/actuators/machine/reconciler.go @@ -40,6 +40,9 @@ const ( defaultGCPBootImageARM = "projects/rhcos-cloud/global/images/rhcos-414-92-202311241643-0-gcp-aarch64" coreOSBootImagesNamespace = "openshift-machine-config-operator" coreOSBootImagesName = "coreos-bootimages" + + defaultOSStreamName = "rhel-9" + osImageStreamName = "cluster" ) // Reconciler are list of services required by machine actuator, easy to create a fake From efc3d2fdc73b9bcc036cacdb1d7b742e6f15952a Mon Sep 17 00:00:00 2001 From: Nader Ziada Date: Fri, 7 Aug 2026 09:12:11 -0400 Subject: [PATCH 3/5] address review findings for boot image resolver - Add openshift-machine-config-operator to cache DefaultNamespaces so the cache-backed client can read the coreos-bootimages ConfigMap cross-namespace (P0 fix) - Upgrade fallback log lines from V(3) to Warningf for operator visibility when image resolution fails - Update fallback images from RHCOS 4.14 to 4.18 (418.94.202602022246-0) - Add a4x prefix to ARM64 machine type detection Signed-off-by: Nader Ziada --- cmd/manager/main.go | 1 + pkg/cloud/gcp/actuators/machine/actuator.go | 7 +++++++ pkg/cloud/gcp/actuators/machine/actuator_test.go | 5 ++++- pkg/cloud/gcp/actuators/machine/boot_image.go | 8 ++++---- pkg/cloud/gcp/actuators/machine/boot_image_test.go | 1 + pkg/cloud/gcp/actuators/machine/machine_scope.go | 3 +++ pkg/cloud/gcp/actuators/machine/reconciler.go | 6 ++++-- pkg/cloud/gcp/actuators/machine/reconciler_test.go | 1 + pkg/cloud/gcp/actuators/util/gcp_machine_architecture.go | 1 + .../gcp/actuators/util/gcp_machine_architecture_test.go | 7 +++++++ 10 files changed, 33 insertions(+), 7 deletions(-) diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 19f4733ce..d95ed2cb8 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -163,6 +163,7 @@ func main() { // Initialize machine actuator. machineActuator := machine.NewActuator(machine.ActuatorParams{ CoreClient: mgr.GetClient(), + APIReader: mgr.GetAPIReader(), EventRecorder: mgr.GetEventRecorderFor("gcpcontroller"), ComputeClientBuilder: computeservice.NewComputeService, TagsClientBuilder: tagservice.NewTagService, diff --git a/pkg/cloud/gcp/actuators/machine/actuator.go b/pkg/cloud/gcp/actuators/machine/actuator.go index 4110d8dae..18b5d2f4c 100644 --- a/pkg/cloud/gcp/actuators/machine/actuator.go +++ b/pkg/cloud/gcp/actuators/machine/actuator.go @@ -30,6 +30,7 @@ const ( // Actuator is responsible for performing machine reconciliation. type Actuator struct { coreClient controllerclient.Client + apiReader controllerclient.Reader eventRecorder record.EventRecorder computeClientBuilder computeservice.BuilderFuncType tagsClientBuilder tagservice.BuilderFuncType @@ -39,6 +40,7 @@ type Actuator struct { // ActuatorParams holds parameter information for Actuator. type ActuatorParams struct { CoreClient controllerclient.Client + APIReader controllerclient.Reader EventRecorder record.EventRecorder ComputeClientBuilder computeservice.BuilderFuncType TagsClientBuilder tagservice.BuilderFuncType @@ -49,6 +51,7 @@ type ActuatorParams struct { func NewActuator(params ActuatorParams) *Actuator { return &Actuator{ coreClient: params.CoreClient, + apiReader: params.APIReader, eventRecorder: params.EventRecorder, computeClientBuilder: params.ComputeClientBuilder, tagsClientBuilder: params.TagsClientBuilder, @@ -72,6 +75,7 @@ func (a *Actuator) Create(ctx context.Context, machine *machinev1.Machine) error scope, err := newMachineScope(machineScopeParams{ Context: ctx, coreClient: a.coreClient, + apiReader: a.apiReader, machine: machine, computeClientBuilder: a.computeClientBuilder, tagsClientBuilder: a.tagsClientBuilder, @@ -96,6 +100,7 @@ func (a *Actuator) Exists(ctx context.Context, machine *machinev1.Machine) (bool scope, err := newMachineScope(machineScopeParams{ Context: ctx, coreClient: a.coreClient, + apiReader: a.apiReader, machine: machine, computeClientBuilder: a.computeClientBuilder, tagsClientBuilder: a.tagsClientBuilder, @@ -137,6 +142,7 @@ func (a *Actuator) Update(ctx context.Context, machine *machinev1.Machine) error scope, err := newMachineScope(machineScopeParams{ Context: ctx, coreClient: a.coreClient, + apiReader: a.apiReader, machine: machine, computeClientBuilder: a.computeClientBuilder, tagsClientBuilder: a.tagsClientBuilder, @@ -174,6 +180,7 @@ func (a *Actuator) Delete(ctx context.Context, machine *machinev1.Machine) error scope, err := newMachineScope(machineScopeParams{ Context: ctx, coreClient: a.coreClient, + apiReader: a.apiReader, machine: machine, computeClientBuilder: a.computeClientBuilder, tagsClientBuilder: a.tagsClientBuilder, diff --git a/pkg/cloud/gcp/actuators/machine/actuator_test.go b/pkg/cloud/gcp/actuators/machine/actuator_test.go index 5f02d6a4a..60cba0236 100644 --- a/pkg/cloud/gcp/actuators/machine/actuator_test.go +++ b/pkg/cloud/gcp/actuators/machine/actuator_test.go @@ -317,6 +317,7 @@ func TestActuatorEvents(t *testing.T) { gs.Expect(err).ToNot(HaveOccurred()) params := ActuatorParams{ CoreClient: k8sClient, + APIReader: k8sClient, EventRecorder: eventRecorder, ComputeClientBuilder: computeservice.MockBuilderFuncType, TagsClientBuilder: tagservice.NewMockTagServiceBuilder, @@ -420,8 +421,10 @@ func TestActuatorExists(t *testing.T) { if err != nil { t.Fatalf("failed to configure feature gates: %s", err.Error()) } + fakeClient := controllerfake.NewFakeClient(userDataSecret, credentialsSecret) params := ActuatorParams{ - CoreClient: controllerfake.NewFakeClient(userDataSecret, credentialsSecret), + CoreClient: fakeClient, + APIReader: fakeClient, ComputeClientBuilder: computeservice.MockBuilderFuncType, TagsClientBuilder: tagservice.NewMockTagServiceBuilder, FeatureGates: gate, diff --git a/pkg/cloud/gcp/actuators/machine/boot_image.go b/pkg/cloud/gcp/actuators/machine/boot_image.go index c68eabd20..19886585d 100644 --- a/pkg/cloud/gcp/actuators/machine/boot_image.go +++ b/pkg/cloud/gcp/actuators/machine/boot_image.go @@ -25,11 +25,11 @@ func (r *Reconciler) resolveBootImage() (string, error) { image, err := r.resolveImageFromConfigMap(arch) if err != nil { - klog.V(3).Infof("Failed to resolve boot image from coreos-bootimages ConfigMap: %v, using fallback", err) + klog.Warningf("Failed to resolve boot image from coreos-bootimages ConfigMap: %v, using fallback", err) return fallbackImage(arch), nil } if image == "" { - klog.V(3).Infof("No GCP image found in coreos-bootimages for arch %s, using fallback", arch) + klog.Warningf("No GCP image found in coreos-bootimages for arch %s, using fallback", arch) return fallbackImage(arch), nil } @@ -60,7 +60,7 @@ func (r *Reconciler) resolveArchitecture() util.NormalizedArch { func (r *Reconciler) resolveImageFromConfigMap(arch util.NormalizedArch) (string, error) { var cm corev1.ConfigMap - if err := r.coreClient.Get(r.Context, client.ObjectKey{ + if err := r.apiReader.Get(r.Context, client.ObjectKey{ Namespace: coreOSBootImagesNamespace, Name: coreOSBootImagesName, }, &cm); err != nil { @@ -119,7 +119,7 @@ func (r *Reconciler) resolveActiveStreamName() string { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(osImageStreamGVK) - err := r.coreClient.Get(r.Context, client.ObjectKey{Name: osImageStreamName}, obj) + err := r.apiReader.Get(r.Context, client.ObjectKey{Name: osImageStreamName}, obj) if err != nil { if apimachineryerrors.IsNotFound(err) { klog.V(3).Infof("OSImageStream CR not found, defaulting to stream %q", defaultOSStreamName) diff --git a/pkg/cloud/gcp/actuators/machine/boot_image_test.go b/pkg/cloud/gcp/actuators/machine/boot_image_test.go index ea0c884ac..d71de1f04 100644 --- a/pkg/cloud/gcp/actuators/machine/boot_image_test.go +++ b/pkg/cloud/gcp/actuators/machine/boot_image_test.go @@ -276,6 +276,7 @@ func TestResolveBootImage(t *testing.T) { machineScope: &machineScope{ Context: context.Background(), coreClient: fakeClient, + apiReader: fakeClient, computeService: mockComputeService, projectID: "test-project", providerSpec: &machinev1.GCPMachineProviderSpec{ diff --git a/pkg/cloud/gcp/actuators/machine/machine_scope.go b/pkg/cloud/gcp/actuators/machine/machine_scope.go index 4c88951f1..248fe7c49 100644 --- a/pkg/cloud/gcp/actuators/machine/machine_scope.go +++ b/pkg/cloud/gcp/actuators/machine/machine_scope.go @@ -22,6 +22,7 @@ type machineScopeParams struct { context.Context coreClient controllerclient.Client + apiReader controllerclient.Reader machine *machinev1.Machine computeClientBuilder computeservice.BuilderFuncType tagsClientBuilder tagservice.BuilderFuncType @@ -33,6 +34,7 @@ type machineScope struct { context.Context coreClient controllerclient.Client + apiReader controllerclient.Reader projectID string providerID string computeService computeservice.GCPComputeService @@ -98,6 +100,7 @@ func newMachineScope(params machineScopeParams) (*machineScope, error) { return &machineScope{ Context: params.Context, coreClient: params.coreClient, + apiReader: params.apiReader, projectID: projectID, // https://github.com/kubernetes/kubernetes/blob/8765fa2e48974e005ad16e65cb5c3acf5acff17b/staging/src/k8s.io/legacy-cloud-providers/gce/gce_util.go#L204 providerID: fmt.Sprintf("gce://%s/%s/%s", projectID, providerSpec.Zone, params.machine.Name), diff --git a/pkg/cloud/gcp/actuators/machine/reconciler.go b/pkg/cloud/gcp/actuators/machine/reconciler.go index 9875e4ce4..b0050f40b 100644 --- a/pkg/cloud/gcp/actuators/machine/reconciler.go +++ b/pkg/cloud/gcp/actuators/machine/reconciler.go @@ -36,8 +36,10 @@ const ( openshiftMachineRoleLabel = "machine.openshift.io/cluster-api-machine-role" masterMachineRole = "master" - defaultGCPBootImageX86 = "projects/rhcos-cloud/global/images/rhcos-414-92-202311241643-0-gcp-x86-64" - defaultGCPBootImageARM = "projects/rhcos-cloud/global/images/rhcos-414-92-202311241643-0-gcp-aarch64" + // Last-resort fallback images used when the coreos-bootimages ConfigMap is unavailable. + // Sourced from openshift/installer release-4.18 data/data/coreos/rhcos.json. + defaultGCPBootImageX86 = "projects/rhcos-cloud/global/images/rhcos-418-94-202602022246-0-gcp-x86-64" + defaultGCPBootImageARM = "projects/rhcos-cloud/global/images/rhcos-418-94-202602022246-0-gcp-aarch64" coreOSBootImagesNamespace = "openshift-machine-config-operator" coreOSBootImagesName = "coreos-bootimages" diff --git a/pkg/cloud/gcp/actuators/machine/reconciler_test.go b/pkg/cloud/gcp/actuators/machine/reconciler_test.go index 725103118..98f2c2260 100644 --- a/pkg/cloud/gcp/actuators/machine/reconciler_test.go +++ b/pkg/cloud/gcp/actuators/machine/reconciler_test.go @@ -1028,6 +1028,7 @@ func TestCreate(t *testing.T) { }, }, coreClient: fakeClient, + apiReader: fakeClient, providerSpec: providerSpec, providerStatus: &machinev1.GCPMachineProviderStatus{}, computeService: mockComputeService, diff --git a/pkg/cloud/gcp/actuators/util/gcp_machine_architecture.go b/pkg/cloud/gcp/actuators/util/gcp_machine_architecture.go index bd5ac7026..feb640429 100644 --- a/pkg/cloud/gcp/actuators/util/gcp_machine_architecture.go +++ b/pkg/cloud/gcp/actuators/util/gcp_machine_architecture.go @@ -26,6 +26,7 @@ const ( // machineTypePrefixArchitectureMap contains a map of (machineTypePrefix, architecture) tuples var machineTypePrefixArchitectureMap = map[string]NormalizedArch{ + "a4x": ArchitectureArm64, "c4a": ArchitectureArm64, "n4a": ArchitectureArm64, "t2a": ArchitectureArm64, diff --git a/pkg/cloud/gcp/actuators/util/gcp_machine_architecture_test.go b/pkg/cloud/gcp/actuators/util/gcp_machine_architecture_test.go index a8a788b56..1fbc515ff 100644 --- a/pkg/cloud/gcp/actuators/util/gcp_machine_architecture_test.go +++ b/pkg/cloud/gcp/actuators/util/gcp_machine_architecture_test.go @@ -11,6 +11,13 @@ func TestCPUArchitecture(t *testing.T) { args args want NormalizedArch }{ + { + name: "should return arm64 for a4x-* machine types", + args: args{ + machineType: "a4x-megagpu-1g", + }, + want: ArchitectureArm64, + }, { name: "should return arm64 for t2a-* machine types", args: args{ From ede75e9218074142af6d1b0ca5be0fcd824a9a36 Mon Sep 17 00:00:00 2001 From: Nader Ziada Date: Mon, 10 Aug 2026 12:01:54 -0400 Subject: [PATCH 4/5] update fallback boot images from 4.18 to 5.0 attempting to resolve the create a machine from a minimal providerSpec failure in CI Signed-off-by: Nader Ziada --- pkg/cloud/gcp/actuators/machine/reconciler.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cloud/gcp/actuators/machine/reconciler.go b/pkg/cloud/gcp/actuators/machine/reconciler.go index b0050f40b..62916a438 100644 --- a/pkg/cloud/gcp/actuators/machine/reconciler.go +++ b/pkg/cloud/gcp/actuators/machine/reconciler.go @@ -37,9 +37,9 @@ const ( masterMachineRole = "master" // Last-resort fallback images used when the coreos-bootimages ConfigMap is unavailable. - // Sourced from openshift/installer release-4.18 data/data/coreos/rhcos.json. - defaultGCPBootImageX86 = "projects/rhcos-cloud/global/images/rhcos-418-94-202602022246-0-gcp-x86-64" - defaultGCPBootImageARM = "projects/rhcos-cloud/global/images/rhcos-418-94-202602022246-0-gcp-aarch64" + // Sourced from openshift/installer release-5.0 data/data/coreos/rhcos.json. + defaultGCPBootImageX86 = "projects/rhcos-cloud/global/images/rhcos-10-2-20260423-0-gcp-x86-64" + defaultGCPBootImageARM = "projects/rhcos-cloud/global/images/rhcos-10-2-20260423-0-gcp-aarch64" coreOSBootImagesNamespace = "openshift-machine-config-operator" coreOSBootImagesName = "coreos-bootimages" From 4cff597b5eed22dc1bbee505fd6b16a0cc2792d1 Mon Sep 17 00:00:00 2001 From: Nader Ziada Date: Tue, 11 Aug 2026 09:06:12 -0400 Subject: [PATCH 5/5] use rhel-10 as default OS stream for boot image resolution Signed-off-by: Nader Ziada --- pkg/cloud/gcp/actuators/machine/boot_image_test.go | 10 +++++----- pkg/cloud/gcp/actuators/machine/reconciler.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cloud/gcp/actuators/machine/boot_image_test.go b/pkg/cloud/gcp/actuators/machine/boot_image_test.go index d71de1f04..e107fa48c 100644 --- a/pkg/cloud/gcp/actuators/machine/boot_image_test.go +++ b/pkg/cloud/gcp/actuators/machine/boot_image_test.go @@ -159,14 +159,14 @@ func TestResolveBootImage(t *testing.T) { expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), }, { - name: "defaults to rhel-9 when OSImageStream CR not found", + name: "defaults to rhel-10 when OSImageStream CR not found", machineType: "n2-standard-4", mockMachineType: &compute.MachineType{ Architecture: "X86_64", }, configMap: testBootImagesConfigMap(), osImageStream: nil, - expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-420-stable-x86-64"), }, { name: "falls back to deprecated stream key when streams key missing", @@ -176,7 +176,7 @@ func TestResolveBootImage(t *testing.T) { }, configMap: testBootImagesConfigMapLegacy(), osImageStream: nil, - expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), // legacy key ignores stream name }, { name: "missing ConfigMap falls back to x86 default", @@ -222,7 +222,7 @@ func TestResolveBootImage(t *testing.T) { }, configMap: testBootImagesConfigMap(), osImageStream: nil, - expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-aarch64"), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-420-stable-aarch64"), }, { name: "ARCHITECTURE_UNSPECIFIED falls back to prefix-based detection", @@ -232,7 +232,7 @@ func TestResolveBootImage(t *testing.T) { }, configMap: testBootImagesConfigMap(), osImageStream: nil, - expectedImage: gcpImageReference("rhcos-cloud", "rhcos-418-stable-x86-64"), + expectedImage: gcpImageReference("rhcos-cloud", "rhcos-420-stable-x86-64"), }, } diff --git a/pkg/cloud/gcp/actuators/machine/reconciler.go b/pkg/cloud/gcp/actuators/machine/reconciler.go index 62916a438..e8f25c525 100644 --- a/pkg/cloud/gcp/actuators/machine/reconciler.go +++ b/pkg/cloud/gcp/actuators/machine/reconciler.go @@ -43,7 +43,7 @@ const ( coreOSBootImagesNamespace = "openshift-machine-config-operator" coreOSBootImagesName = "coreos-bootimages" - defaultOSStreamName = "rhel-9" + defaultOSStreamName = "rhel-10" osImageStreamName = "cluster" )