@@ -13,6 +13,12 @@ import (
1313 "github.com/gophercloud/gophercloud/v2"
1414 "github.com/metal3-io/baremetal-operator/pkg/provisioner"
1515 "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients"
16+ "github.com/metal3-io/baremetal-operator/pkg/secretutils"
17+ ironicv1alpha1 "github.com/metal3-io/ironic-standalone-operator/api/v1alpha1"
18+ "k8s.io/apimachinery/pkg/api/meta"
19+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+ "k8s.io/apimachinery/pkg/types"
21+ "sigs.k8s.io/controller-runtime/pkg/client"
1622)
1723
1824type ironicProvisionerFactory struct {
@@ -22,6 +28,14 @@ type ironicProvisionerFactory struct {
2228 // Keep pointers to ironic client configured with the global
2329 // auth settings to reuse the connection between reconcilers.
2430 clientIronic * gophercloud.ServiceClient
31+
32+ // Kubernetes client for reading Ironic CR
33+ k8sClient client.Client
34+ apiReader client.Reader
35+
36+ // Ironic CR configuration
37+ ironicName string
38+ ironicNamespace string
2539}
2640
2741func NewProvisionerFactory (logger logr.Logger , havePreprovImgBuilder bool ) (provisioner.Factory , error ) {
@@ -33,13 +47,42 @@ func NewProvisionerFactory(logger logr.Logger, havePreprovImgBuilder bool) (prov
3347 return factory , err
3448}
3549
50+ func NewProvisionerFactoryWithClient (logger logr.Logger , havePreprovImgBuilder bool , k8sClient client.Client , apiReader client.Reader , ironicName , ironicNamespace string ) (provisioner.Factory , error ) {
51+ factory := ironicProvisionerFactory {
52+ log : logger .WithName ("ironic" ),
53+ k8sClient : k8sClient ,
54+ apiReader : apiReader ,
55+ ironicName : ironicName ,
56+ ironicNamespace : ironicNamespace ,
57+ }
58+
59+ err := factory .init (havePreprovImgBuilder )
60+ return factory , err
61+ }
62+
3663func (f * ironicProvisionerFactory ) init (havePreprovImgBuilder bool ) error {
37- ironicAuth , err := clients .LoadAuth ()
64+ var err error
65+ f .config , err = loadConfigFromEnv (havePreprovImgBuilder )
3866 if err != nil {
3967 return err
4068 }
4169
42- f .config , err = loadConfigFromEnv (havePreprovImgBuilder )
70+ if f .ironicName != "" && f .ironicNamespace != "" {
71+ f .log .Info ("will use Ironic resource configuration" ,
72+ "ironicName" , f .ironicName ,
73+ "ironicNamespace" , f .ironicNamespace ,
74+ "deployKernelURL" , f .config .deployKernelURL ,
75+ "deployRamdiskURL" , f .config .deployRamdiskURL ,
76+ "deployISOURL" , f .config .deployISOURL ,
77+ "liveISOForcePersistentBootDevice" , f .config .liveISOForcePersistentBootDevice ,
78+ )
79+ // NOTE(dtantsur): the Ironic object will be loaded from the client cache on each reconciliation, so exiting here.
80+ return nil
81+ }
82+
83+ f .log .V (1 ).Info ("will use environment variables configuration" )
84+ // For environment variable mode, validate configuration early and create a static client
85+ ironicAuth , err := clients .LoadAuth ()
4386 if err != nil {
4487 return err
4588 }
@@ -51,7 +94,7 @@ func (f *ironicProvisionerFactory) init(havePreprovImgBuilder bool) error {
5194
5295 tlsConf := loadTLSConfigFromEnv ()
5396
54- f .log .Info ("ironic settings" ,
97+ f .log .Info ("ironic settings from environment variables " ,
5598 "endpoint" , ironicEndpoint ,
5699 "ironicAuthType" , ironicAuth .Type ,
57100 "deployKernelURL" , f .config .deployKernelURL ,
@@ -65,8 +108,7 @@ func (f *ironicProvisionerFactory) init(havePreprovImgBuilder bool) error {
65108 "SkipClientSANVerify" , tlsConf .SkipClientSANVerify ,
66109 )
67110
68- f .clientIronic , err = clients .IronicClient (
69- ironicEndpoint , ironicAuth , tlsConf )
111+ f .clientIronic , err = clients .IronicClient (ironicEndpoint , ironicAuth , tlsConf )
70112 if err != nil {
71113 return err
72114 }
@@ -77,6 +119,31 @@ func (f *ironicProvisionerFactory) init(havePreprovImgBuilder bool) error {
77119func (f ironicProvisionerFactory ) ironicProvisioner (ctx context.Context , hostData provisioner.HostData , publisher provisioner.EventPublisher ) (* ironicProvisioner , error ) {
78120 provisionerLogger := f .log .WithValues ("host" , ironicNodeName (hostData .ObjectMeta ))
79121
122+ var ironicClient * gophercloud.ServiceClient
123+
124+ // Check if we should use Ironic CR configuration (fetch fresh config on each provisioner creation)
125+ if f .ironicName != "" && f .ironicNamespace != "" && f .k8sClient != nil {
126+ ironicEndpoint , ironicAuth , tlsConf , err := f .loadConfigFromIronicCR (ctx )
127+ if err != nil {
128+ return nil , fmt .Errorf ("failed to load configuration from Ironic resource %s/%s: %w" , f .ironicNamespace , f .ironicName , err )
129+ }
130+
131+ provisionerLogger .Info ("ironic settings from Ironic resource" ,
132+ "ironicName" , f .ironicName ,
133+ "ironicNamespace" , f .ironicNamespace ,
134+ "endpoint" , ironicEndpoint ,
135+ "CACertFile" , tlsConf .TrustedCAFile ,
136+ )
137+
138+ ironicClient , err = clients .IronicClient (ironicEndpoint , ironicAuth , tlsConf )
139+ if err != nil {
140+ return nil , fmt .Errorf ("failed to create a client from Ironic resource %s/%s: %w" , f .ironicNamespace , f .ironicName , err )
141+ }
142+ } else {
143+ // Use the pre-configured client from environment variables
144+ ironicClient = f .clientIronic
145+ }
146+
80147 p := & ironicProvisioner {
81148 config : f .config ,
82149 objectMeta : hostData .ObjectMeta ,
@@ -85,7 +152,7 @@ func (f ironicProvisionerFactory) ironicProvisioner(ctx context.Context, hostDat
85152 bmcAddress : hostData .BMCAddress ,
86153 disableCertVerification : hostData .DisableCertificateVerification ,
87154 bootMACAddress : hostData .BootMACAddress ,
88- client : f . clientIronic ,
155+ client : ironicClient ,
89156 log : provisionerLogger ,
90157 debugLog : provisionerLogger .V (1 ),
91158 publisher : publisher ,
@@ -192,3 +259,120 @@ func loadTLSConfigFromEnv() clients.TLSConfig {
192259 SkipClientSANVerify : skipClientSANVerify ,
193260 }
194261}
262+
263+ func ironicUnreadyReason (ironic * ironicv1alpha1.Ironic ) string {
264+ cond := meta .FindStatusCondition (ironic .Status .Conditions , string (ironicv1alpha1 .IronicStatusReady ))
265+ if cond == nil || cond .ObservedGeneration != ironic .Generation {
266+ return "reconciliation hasn't started yet"
267+ }
268+ if cond .Status != metav1 .ConditionTrue {
269+ return cond .Message
270+ }
271+ return ""
272+ }
273+
274+ func (f * ironicProvisionerFactory ) loadConfigFromIronicCR (ctx context.Context ) (endpoint string , auth clients.AuthConfig , tlsConfig clients.TLSConfig , err error ) {
275+ sm := secretutils .NewSecretManager (ctx , f .log , f .k8sClient , f .apiReader )
276+
277+ // Get the Ironic CR
278+ ironicCR := & ironicv1alpha1.Ironic {}
279+ key := types.NamespacedName {
280+ Name : f .ironicName ,
281+ Namespace : f .ironicNamespace ,
282+ }
283+
284+ err = f .k8sClient .Get (ctx , key , ironicCR )
285+ if err != nil {
286+ return "" , clients.AuthConfig {}, clients.TLSConfig {}, fmt .Errorf ("failed to get Ironic resource %s/%s: %w" , f .ironicNamespace , f .ironicName , err )
287+ }
288+
289+ // Check if the Ironic CR is ready
290+ if unreadyReason := ironicUnreadyReason (ironicCR ); unreadyReason != "" {
291+ return "" , clients.AuthConfig {}, clients.TLSConfig {}, fmt .Errorf ("ironic resource %s/%s is not ready: %s" , f .ironicNamespace , f .ironicName , unreadyReason )
292+ }
293+
294+ endpoint = fmt .Sprintf ("http://%s.%s.svc" , f .ironicName , f .ironicNamespace )
295+
296+ // Handle TLS
297+ if ironicCR .Spec .TLS .CertificateName != "" {
298+ endpoint = fmt .Sprintf ("https://%s.%s.svc" , f .ironicName , f .ironicNamespace )
299+ tlsConfig , err = f .loadTLSConfigFromIronicCR (& sm , ironicCR )
300+ if err != nil {
301+ return "" , clients.AuthConfig {}, clients.TLSConfig {}, err
302+ }
303+ }
304+
305+ // Handle authentication
306+ auth , err = f .loadAuthConfigFromIronicCR (& sm , ironicCR )
307+ if err != nil {
308+ return "" , clients.AuthConfig {}, clients.TLSConfig {}, err
309+ }
310+
311+ return endpoint , auth , tlsConfig , nil
312+ }
313+
314+ func (f * ironicProvisionerFactory ) loadAuthConfigFromIronicCR (sm * secretutils.SecretManager , ironicCR * ironicv1alpha1.Ironic ) (clients.AuthConfig , error ) {
315+ key := types.NamespacedName {
316+ Name : ironicCR .Spec .APICredentialsName ,
317+ Namespace : ironicCR .Namespace ,
318+ }
319+
320+ secret , err := sm .ObtainSecret (key )
321+ if err != nil {
322+ return clients.AuthConfig {}, fmt .Errorf ("failed to get auth secret %s/%s: %w" , key .Namespace , key .Name , err )
323+ }
324+
325+ username , usernameExists := secret .Data ["username" ]
326+ password , passwordExists := secret .Data ["password" ]
327+
328+ if ! usernameExists || ! passwordExists {
329+ return clients.AuthConfig {}, fmt .Errorf ("auth secret %s/%s must contain 'username' and 'password' keys" , key .Namespace , key .Name )
330+ }
331+
332+ return clients.AuthConfig {
333+ Type : clients .HTTPBasicAuth ,
334+ Username : string (username ),
335+ Password : string (password ),
336+ }, nil
337+ }
338+
339+ func (f * ironicProvisionerFactory ) loadTLSConfigFromIronicCR (sm * secretutils.SecretManager , ironicCR * ironicv1alpha1.Ironic ) (clients.TLSConfig , error ) {
340+ // Allow client TLS configuration from the environment
341+ tlsConfig := loadTLSConfigFromEnv ()
342+
343+ key := types.NamespacedName {
344+ Name : ironicCR .Spec .TLS .CertificateName ,
345+ Namespace : ironicCR .Namespace ,
346+ }
347+
348+ secret , err := sm .ObtainSecret (key )
349+ if err != nil {
350+ return clients.TLSConfig {}, fmt .Errorf ("failed to get TLS secret %s/%s: %w" , ironicCR .Namespace , ironicCR .Spec .TLS .CertificateName , err )
351+ }
352+
353+ caCert := secret .Data ["tls.crt" ]
354+ if caCert != nil {
355+ caFile , err := writeTempFile ("ironic-ca-" , caCert )
356+ if err != nil {
357+ return clients.TLSConfig {}, fmt .Errorf ("failed to write CA certificate: %w" , err )
358+ }
359+ tlsConfig .TrustedCAFile = caFile
360+ }
361+
362+ return tlsConfig , nil
363+ }
364+
365+ func writeTempFile (prefix string , data []byte ) (string , error ) {
366+ tmpFile , err := os .CreateTemp ("" , prefix )
367+ if err != nil {
368+ return "" , err
369+ }
370+ defer tmpFile .Close ()
371+
372+ _ , err = tmpFile .Write (data )
373+ if err != nil {
374+ return "" , err
375+ }
376+
377+ return tmpFile .Name (), nil
378+ }
0 commit comments