Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
7 changes: 7 additions & 0 deletions pkg/cloud/gcp/actuators/machine/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion pkg/cloud/gcp/actuators/machine/actuator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
162 changes: 162 additions & 0 deletions pkg/cloud/gcp/actuators/machine/boot_image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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"
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()

image, err := r.resolveImageFromConfigMap(arch)
if err != nil {
klog.Warningf("Failed to resolve boot image from coreos-bootimages ConfigMap: %v, using fallback", err)
return fallbackImage(arch), nil
}
if image == "" {
klog.Warningf("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.apiReader.Get(r.Context, client.ObjectKey{
Namespace: coreOSBootImagesNamespace,
Name: coreOSBootImagesName,
}, &cm); err != nil {
return "", fmt.Errorf("failed to get coreos-bootimages ConfigMap: %w", err)
}

streamData, err := r.resolveStreamData(cm.Data)
if err != nil {
return "", err
}

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
Comment thread
nader-ziada marked this conversation as resolved.
}

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.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)
} else {
klog.Warningf("Failed to get OSImageStream CR: %v, defaulting to stream %q", err, defaultOSStreamName)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
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:
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)
}
Loading