11/*
2- Copyright 2023 The Kubernetes Authors.
2+ Copyright 2025 The Kubernetes Authors.
33
44Licensed under the Apache License, Version 2.0 (the "License");
55you may not use this file except in compliance with the License.
@@ -17,21 +17,34 @@ package controllers
1717
1818import (
1919 "context"
20+ "errors"
2021
22+ corev1 "k8s.io/api/core/v1"
2123 apierrors "k8s.io/apimachinery/pkg/api/errors"
24+ "k8s.io/apimachinery/pkg/api/resource"
25+ kerrors "k8s.io/apimachinery/pkg/util/errors"
2226 "k8s.io/client-go/tools/record"
27+ "sigs.k8s.io/cluster-api/util"
28+ "sigs.k8s.io/cluster-api/util/annotations"
29+ "sigs.k8s.io/cluster-api/util/patch"
30+ "sigs.k8s.io/cluster-api/util/predicates"
2331 ctrl "sigs.k8s.io/controller-runtime"
2432 "sigs.k8s.io/controller-runtime/pkg/client"
2533 "sigs.k8s.io/controller-runtime/pkg/controller"
2634
27- "sigs.k8s.io/cluster-api/util/predicates"
28-
2935 infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
36+ "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/compute"
3037 "sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
38+ controllers "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/controllers"
3139)
3240
41+ const imagePropertyForOS = "os_type"
42+
43+ // Set here so we can easily mock it in tests.
44+ var newComputeService = compute .NewService
45+
3346// OpenStackMachineTemplateReconciler reconciles a OpenStackMachineTemplate object.
34- // it only updates the .status field to allow auto-scaling
47+ // it only updates the .status field to allow auto-scaling.
3548type OpenStackMachineTemplateReconciler struct {
3649 Client client.Client
3750 Recorder record.EventRecorder
@@ -40,14 +53,14 @@ type OpenStackMachineTemplateReconciler struct {
4053 CaCertificates []byte // PEM encoded ca certificates.
4154}
4255
43- // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachinetemplatess ,verbs=get;list;watch;create;
44- // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachinetemplatess /status,verbs=get;update;patch
56+ // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachinetemplates ,verbs=get;list;watch
57+ // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=openstackmachinetemplates /status,verbs=get;update;patch
4558
4659func (r * OpenStackMachineTemplateReconciler ) Reconcile (ctx context.Context , req ctrl.Request ) (result ctrl.Result , reterr error ) {
4760 log := ctrl .LoggerFrom (ctx )
4861
4962 // Fetch the OpenStackMachine instance.
50- openStackMachineTemplate := & infrav1.OpenStackMachine {}
63+ openStackMachineTemplate := & infrav1.OpenStackMachineTemplate {}
5164 err := r .Client .Get (ctx , req .NamespacedName , openStackMachineTemplate )
5265 if err != nil {
5366 if apierrors .IsNotFound (err ) {
@@ -59,9 +72,141 @@ func (r *OpenStackMachineTemplateReconciler) Reconcile(ctx context.Context, req
5972 log = log .WithValues ("openStackMachineTemplate" , openStackMachineTemplate .Name )
6073 log .V (4 ).Info ("Reconciling openStackMachineTemplate" )
6174
75+ // If OSMT is set for deletion, do nothing
76+ if ! openStackMachineTemplate .DeletionTimestamp .IsZero () {
77+ log .Info ("OpenStackMachineTemplate marked for deletion, skipping reconciliation" )
78+ return ctrl.Result {}, nil
79+ }
80+
81+ // Fetch the Cluster.
82+ cluster , err := util .GetClusterFromMetadata (ctx , r .Client , openStackMachineTemplate .ObjectMeta )
83+ if err != nil {
84+ log .Info ("openStackMachineTemplate is missing cluster label or cluster does not exist" )
85+ return ctrl.Result {}, nil
86+ }
87+
88+ log = log .WithValues ("cluster" , cluster .Name )
89+
90+ if annotations .IsPaused (cluster , openStackMachineTemplate ) {
91+ log .Info ("OpenStackMachineTemplate or linked Cluster is marked as paused. Won't reconcile" )
92+ return ctrl.Result {}, nil
93+ }
94+
95+ infraCluster , err := controllers .GetInfraCluster (ctx , r .Client , cluster , openStackMachineTemplate .Namespace )
96+ if err != nil {
97+ return ctrl.Result {}, errors .New ("error getting infra provider cluster" )
98+ }
99+ if infraCluster == nil {
100+ log .Info ("OpenStackCluster not ready" , "name" , cluster .Spec .InfrastructureRef .Name )
101+ return ctrl.Result {}, nil
102+ }
103+
104+ log = log .WithValues ("openStackCluster" , infraCluster .Name )
105+
106+ clientScope , err := r .ScopeFactory .NewClientScopeFromObject (ctx , r .Client , r .CaCertificates , log , openStackMachineTemplate , infraCluster )
107+ if err != nil {
108+ return ctrl.Result {}, err
109+ }
110+ scope := scope .NewWithLogger (clientScope , log )
111+
112+ // Initialize the patch helper
113+ patchHelper , err := patch .NewHelper (openStackMachineTemplate , r .Client )
114+ if err != nil {
115+ return ctrl.Result {}, err
116+ }
117+
118+ // Always patch the openStackMachine when exiting this function so we can persist any OpenStackMachine changes.
119+ defer func () {
120+ if err := patchHelper .Patch (ctx , openStackMachineTemplate ); err != nil {
121+ log .Error (err , "Failed to patch OpenStackMachineTemplate after reconciliation" )
122+ result = ctrl.Result {}
123+ reterr = kerrors .NewAggregate ([]error {reterr , err })
124+ }
125+ }()
126+
127+ // Handle non-deleted OpenStackMachineTemplates
128+ if err := r .reconcileNormal (ctx , scope , openStackMachineTemplate ); err != nil {
129+ return ctrl.Result {}, err
130+ }
131+ log .V (4 ).Info ("Successfully reconciled OpenStackMachineTemplate" )
62132 return ctrl.Result {}, nil
63133}
64134
135+ func (r * OpenStackMachineTemplateReconciler ) reconcileNormal (ctx context.Context , scope * scope.WithLogger , openStackMachineTemplate * infrav1.OpenStackMachineTemplate ) (reterr error ) {
136+ log := scope .Logger ()
137+
138+ computeService , err := newComputeService (scope )
139+ if err != nil {
140+ return err
141+ }
142+
143+ flavorID , err := computeService .GetFlavorID (openStackMachineTemplate .Spec .Template .Spec .FlavorID , openStackMachineTemplate .Spec .Template .Spec .Flavor )
144+ if err != nil {
145+ return err
146+ }
147+
148+ flavor , err := computeService .GetFlavor (flavorID )
149+ if err != nil {
150+ return err
151+ }
152+
153+ log .V (4 ).Info ("Retrieved flavor details" , "flavorID" , flavorID )
154+
155+ if openStackMachineTemplate .Status .Capacity == nil {
156+ log .V (4 ).Info ("Initializing status capacity map" )
157+ openStackMachineTemplate .Status .Capacity = corev1.ResourceList {}
158+ }
159+
160+ if flavor .VCPUs > 0 {
161+ openStackMachineTemplate .Status .Capacity [corev1 .ResourceCPU ] = * resource .NewQuantity (int64 (flavor .VCPUs ), resource .DecimalSI )
162+ }
163+
164+ if flavor .RAM > 0 {
165+ // flavor.RAM is in MiB -> convert to bytes
166+ ramBytes := int64 (flavor .RAM ) * 1024 * 1024
167+ openStackMachineTemplate .Status .Capacity [corev1 .ResourceMemory ] = * resource .NewQuantity (ramBytes , resource .BinarySI )
168+ }
169+
170+ if flavor .Ephemeral > 0 {
171+ // flavor.Ephemeral is in GiB -> convert to bytes
172+ ephemeralBytes := int64 (flavor .Ephemeral ) * 1024 * 1024 * 1024
173+ openStackMachineTemplate .Status .Capacity [corev1 .ResourceEphemeralStorage ] = * resource .NewQuantity (ephemeralBytes , resource .BinarySI )
174+ }
175+
176+ // storage depends on whether user boots-from-volume or not
177+ if openStackMachineTemplate .Spec .Template .Spec .RootVolume != nil && openStackMachineTemplate .Spec .Template .Spec .RootVolume .SizeGiB > 0 {
178+ // RootVolume.SizeGib is in GiB -> convert to bytes
179+ storageBytes := int64 (openStackMachineTemplate .Spec .Template .Spec .RootVolume .SizeGiB ) * 1024 * 1024 * 1024
180+ openStackMachineTemplate .Status .Capacity [corev1 .ResourceStorage ] = * resource .NewQuantity (storageBytes , resource .BinarySI )
181+ } else if flavor .Disk > 0 {
182+ // flavor.Disk is in GiB -> convert to bytes
183+ storageBytes := int64 (flavor .Disk ) * 1024 * 1024 * 1024
184+ openStackMachineTemplate .Status .Capacity [corev1 .ResourceStorage ] = * resource .NewQuantity (storageBytes , resource .BinarySI )
185+ }
186+
187+ imageID , err := computeService .GetImageID (ctx , r .Client , openStackMachineTemplate .Namespace , openStackMachineTemplate .Spec .Template .Spec .Image )
188+ if err != nil {
189+ return err
190+ }
191+
192+ image , err := computeService .GetImageDetails (* imageID )
193+ if err != nil {
194+ return err
195+ }
196+
197+ log .V (4 ).Info ("Retrieved image details" , "imageID" , imageID )
198+
199+ if image .Properties != nil {
200+ if v , ok := image .Properties [imagePropertyForOS ]; ok {
201+ if osType , ok := v .(string ); ok {
202+ openStackMachineTemplate .Status .NodeInfo .OperatingSystem = osType
203+ }
204+ }
205+ }
206+
207+ return nil
208+ }
209+
65210func (r * OpenStackMachineTemplateReconciler ) SetupWithManager (ctx context.Context , mgr ctrl.Manager , options controller.Options ) error {
66211 log := ctrl .LoggerFrom (ctx )
67212
0 commit comments