Skip to content

Commit 5c19d77

Browse files
committed
Application Credential support
Adds the end-to-end support for consuming Keystone ApplicationCredentials (AC) in the ironic-operator, enabling IronicAPI,Conductor, Inspector NeutronAGent pods to use AC-based authentication when available. Signed-off-by: Veronika Fisarova <vfisarov@redhat.com>
1 parent b0a37cc commit 5c19d77

17 files changed

+858
-34
lines changed

api/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.14 //allow-merging
9494
replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging
9595

9696
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
97+
98+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251201081347-2823ce61c025

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.14 //allow-merging
142142
replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging
143143

144144
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
145+
146+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251201081347-2823ce61c025

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Deydra71/keystone-operator/api v0.0.0-20251201081347-2823ce61c025 h1:9tVdplONUg5JpgAczs7wV4Swkc0mqAueWVT58cin9ZY=
2+
github.com/Deydra71/keystone-operator/api v0.0.0-20251201081347-2823ce61c025/go.mod h1:b98Jl8eyUw8V07l9YiuQnoMlnWC748oV8IhXH15NCC4=
13
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
24
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
35
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
@@ -120,8 +122,6 @@ github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyU
120122
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo=
121123
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251205192058-5cfbada0ab96 h1:hPgCcrbRHBPfngaEPe6coaCtcauMolI71lfcLdinrKI=
122124
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251205192058-5cfbada0ab96/go.mod h1:ZuglN7IqXfIo75WcJwe0NLHhu82Fs3k/5IXptqnO1H4=
123-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251128160419-8b3a77972a77 h1:XzVPjfzxDJwgW8sNGv9K577Ui2mb6Mp3sDItuDmTv9E=
124-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251128160419-8b3a77972a77/go.mod h1:b98Jl8eyUw8V07l9YiuQnoMlnWC748oV8IhXH15NCC4=
125125
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c h1:wM8qXCB5mQwSosCvtaydzuXitWVVKBHTzH0A2znQ+Jg=
126126
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c/go.mod h1:+Me0raWPPdz8gRi9D4z1khmvUgS9vIKAVC8ckg1yJZU=
127127
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251103072528-9eb684fef4ef h1:Ql4G7sRHpqWFGwXypN7MorDGUWv4jz5n34ayzVt3R9E=

internal/controller/ironic_controller.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,19 @@ func (r *IronicReconciler) reconcileNormal(ctx context.Context, instance *ironic
385385
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
386386
// run check OpenStack secret - end
387387

388+
// Verify Application Credentials if available
389+
ctrlResult, err := keystonev1.VerifyApplicationCredentialsForService(
390+
ctx,
391+
r.Client,
392+
instance.Namespace,
393+
ironic.ServiceName,
394+
&configMapVars,
395+
10*time.Second,
396+
)
397+
if (err != nil || ctrlResult != ctrl.Result{}) {
398+
return ctrlResult, err
399+
}
400+
388401
// Get Keystone endpoints
389402
keystoneEndpoints := ironicv1.KeystoneEndpoints{}
390403
if !instance.Spec.Standalone {
@@ -461,7 +474,7 @@ func (r *IronicReconciler) reconcileNormal(ctx context.Context, instance *ironic
461474
}
462475

463476
// Handle service upgrade
464-
ctrlResult, err := r.reconcileUpgrade(ctx, instance, helper, serviceLabels)
477+
ctrlResult, err = r.reconcileUpgrade(ctx, instance, helper, serviceLabels)
465478
if err != nil {
466479
return ctrlResult, err
467480
} else if (ctrlResult != ctrl.Result{}) {
@@ -890,6 +903,8 @@ func (r *IronicReconciler) generateServiceConfigMaps(
890903
keystoneEndpoints *ironicv1.KeystoneEndpoints,
891904
db *mariadbv1.Database,
892905
) error {
906+
Log := r.GetLogger(ctx)
907+
893908
//
894909
// create Configmap/Secret required for ironic input
895910
// - %-scripts configmap holding scripts to e.g. bootstrap the service
@@ -949,6 +964,18 @@ func (r *IronicReconciler) generateServiceConfigMaps(
949964
templateParameters["KeystonePublicURL"] = keystoneEndpoints.Public
950965
templateParameters["ServiceUser"] = instance.Spec.ServiceUser
951966
templateParameters["ServicePassword"] = servicePassword
967+
968+
// Try to get Application Credential for this service
969+
templateParameters["UseApplicationCredentials"] = false
970+
if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, ironic.ServiceName); err != nil {
971+
Log.Error(err, "Failed to get ApplicationCredential for service", "service", ironic.ServiceName)
972+
return err
973+
} else if acData != nil {
974+
templateParameters["UseApplicationCredentials"] = true
975+
templateParameters["ACID"] = acData.ID
976+
templateParameters["ACSecret"] = acData.Secret
977+
Log.Info("Using ApplicationCredentials auth", "service", ironic.ServiceName)
978+
}
952979
} else {
953980
templateParameters["IronicPublicURL"] = ""
954981
}

internal/controller/ironicapi_controller.go

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,42 @@ func (r *IronicAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
299299
return err
300300
}
301301

302+
// Application Credential secret watching function
303+
acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request {
304+
name := o.GetName()
305+
ns := o.GetNamespace()
306+
result := []reconcile.Request{}
307+
308+
// Only handle Secret objects
309+
if _, isSecret := o.(*corev1.Secret); !isSecret {
310+
return nil
311+
}
312+
313+
// Check if this is an ironic AC secret by name pattern (ac-ironic-secret)
314+
expectedSecretName := keystonev1.GetACSecretName("ironic")
315+
if name == expectedSecretName {
316+
// get all IronicAPI CRs in this namespace
317+
ironicAPIs := &ironicv1.IronicAPIList{}
318+
listOpts := []client.ListOption{
319+
client.InNamespace(ns),
320+
}
321+
if err := r.List(context.Background(), ironicAPIs, listOpts...); err != nil {
322+
return nil
323+
}
324+
325+
// Enqueue reconcile for all ironic API instances
326+
for _, cr := range ironicAPIs.Items {
327+
objKey := client.ObjectKey{
328+
Namespace: ns,
329+
Name: cr.Name,
330+
}
331+
result = append(result, reconcile.Request{NamespacedName: objKey})
332+
}
333+
}
334+
335+
return result
336+
}
337+
302338
return ctrl.NewControllerManagedBy(mgr).
303339
For(&ironicv1.IronicAPI{}).
304340
Owns(&keystonev1.KeystoneService{}).
@@ -315,6 +351,8 @@ func (r *IronicAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
315351
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
316352
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
317353
).
354+
Watches(&corev1.Secret{},
355+
handler.EnqueueRequestsFromMapFunc(acSecretFn)).
318356
Watches(&topologyv1.Topology{},
319357
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
320358
builder.WithPredicates(predicate.GenerationChangedPredicate{}),
@@ -720,6 +758,19 @@ func (r *IronicAPIReconciler) reconcileNormal(ctx context.Context, instance *iro
720758

721759
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
722760

761+
// Verify Application Credentials if available
762+
ctrlResult, err := keystonev1.VerifyApplicationCredentialsForService(
763+
ctx,
764+
r.Client,
765+
instance.Namespace,
766+
ironic.ServiceName,
767+
&configMapVars,
768+
10*time.Second,
769+
)
770+
if (err != nil || ctrlResult != ctrl.Result{}) {
771+
return ctrlResult, err
772+
}
773+
723774
//
724775
// TLS input validation
725776
//
@@ -865,7 +916,7 @@ func (r *IronicAPIReconciler) reconcileNormal(ctx context.Context, instance *iro
865916
}
866917

867918
// Handle service init
868-
ctrlResult, err := r.reconcileInit(ctx, instance, helper, serviceLabels)
919+
ctrlResult, err = r.reconcileInit(ctx, instance, helper, serviceLabels)
869920
if err != nil {
870921
return ctrlResult, err
871922
} else if (ctrlResult != ctrl.Result{}) {
@@ -1010,6 +1061,8 @@ func (r *IronicAPIReconciler) generateServiceConfigMaps(
10101061
instance *ironicv1.IronicAPI,
10111062
envVars *map[string]env.Setter,
10121063
) error {
1064+
Log := r.GetLogger(ctx)
1065+
10131066
//
10141067
// create custom Configmap for ironic-api-specific config input
10151068
// - %-config-data configmap holding custom config for the service's ironic.conf
@@ -1071,6 +1124,18 @@ func (r *IronicAPIReconciler) generateServiceConfigMaps(
10711124
templateParameters["KeystonePublicURL"] = instance.Spec.KeystoneEndpoints.Public
10721125
templateParameters["ServiceUser"] = instance.Spec.ServiceUser
10731126
templateParameters["ServicePassword"] = servicePassword
1127+
1128+
// Try to get Application Credential for this service
1129+
templateParameters["UseApplicationCredentials"] = false
1130+
if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, ironic.ServiceName); err != nil {
1131+
Log.Error(err, "Failed to get ApplicationCredential for service", "service", ironic.ServiceName)
1132+
return err
1133+
} else if acData != nil {
1134+
templateParameters["UseApplicationCredentials"] = true
1135+
templateParameters["ACID"] = acData.ID
1136+
templateParameters["ACSecret"] = acData.Secret
1137+
Log.Info("Using ApplicationCredentials auth", "service", ironic.ServiceName)
1138+
}
10741139
} else {
10751140
templateParameters["IronicPublicURL"] = ""
10761141
}

internal/controller/ironicconductor_controller.go

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
ironicv1 "github.com/openstack-k8s-operators/ironic-operator/api/v1beta1"
4949
ironic "github.com/openstack-k8s-operators/ironic-operator/internal/ironic"
5050
ironicconductor "github.com/openstack-k8s-operators/ironic-operator/internal/ironicconductor"
51+
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
5152
"github.com/openstack-k8s-operators/lib-common/modules/common"
5253
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
5354
"github.com/openstack-k8s-operators/lib-common/modules/common/endpoint"
@@ -256,6 +257,42 @@ func (r *IronicConductorReconciler) SetupWithManager(ctx context.Context, mgr ct
256257
return err
257258
}
258259

260+
// Application Credential secret watching function
261+
acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request {
262+
name := o.GetName()
263+
ns := o.GetNamespace()
264+
result := []reconcile.Request{}
265+
266+
// Only handle Secret objects
267+
if _, isSecret := o.(*corev1.Secret); !isSecret {
268+
return nil
269+
}
270+
271+
// Check if this is an ironic AC secret by name pattern (ac-ironic-secret)
272+
expectedSecretName := keystonev1.GetACSecretName("ironic")
273+
if name == expectedSecretName {
274+
// get all IronicConductor CRs in this namespace
275+
ironicConductors := &ironicv1.IronicConductorList{}
276+
listOpts := []client.ListOption{
277+
client.InNamespace(ns),
278+
}
279+
if err := r.List(context.Background(), ironicConductors, listOpts...); err != nil {
280+
return nil
281+
}
282+
283+
// Enqueue reconcile for all ironic conductor instances
284+
for _, cr := range ironicConductors.Items {
285+
objKey := client.ObjectKey{
286+
Namespace: ns,
287+
Name: cr.Name,
288+
}
289+
result = append(result, reconcile.Request{NamespacedName: objKey})
290+
}
291+
}
292+
293+
return result
294+
}
295+
259296
return ctrl.NewControllerManagedBy(mgr).
260297
For(&ironicv1.IronicConductor{}).
261298
Owns(&appsv1.StatefulSet{}).
@@ -272,6 +309,8 @@ func (r *IronicConductorReconciler) SetupWithManager(ctx context.Context, mgr ct
272309
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
273310
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
274311
).
312+
Watches(&corev1.Secret{},
313+
handler.EnqueueRequestsFromMapFunc(acSecretFn)).
275314
Watches(
276315
&topologyv1.Topology{},
277316
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
@@ -594,6 +633,19 @@ func (r *IronicConductorReconciler) reconcileNormal(ctx context.Context, instanc
594633

595634
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
596635

636+
// Verify Application Credentials if available
637+
ctrlResult, err := keystonev1.VerifyApplicationCredentialsForService(
638+
ctx,
639+
r.Client,
640+
instance.Namespace,
641+
ironic.ServiceName,
642+
&configMapVars,
643+
10*time.Second,
644+
)
645+
if (err != nil || ctrlResult != ctrl.Result{}) {
646+
return ctrlResult, err
647+
}
648+
597649
//
598650
// TLS input validation
599651
//
@@ -760,7 +812,7 @@ func (r *IronicConductorReconciler) reconcileNormal(ctx context.Context, instanc
760812
time.Duration(5)*time.Second,
761813
)
762814

763-
ctrlResult, err := ss.CreateOrPatch(ctx, helper)
815+
ctrlResult, err = ss.CreateOrPatch(ctx, helper)
764816
if err != nil {
765817
instance.Status.Conditions.Set(condition.FalseCondition(
766818
condition.DeploymentReadyCondition,
@@ -885,6 +937,18 @@ func (r *IronicConductorReconciler) generateServiceConfigMaps(
885937
templateParameters["KeystonePublicURL"] = instance.Spec.KeystoneEndpoints.Public
886938
templateParameters["ServiceUser"] = instance.Spec.ServiceUser
887939
templateParameters["ServicePassword"] = servicePassword
940+
941+
// Try to get Application Credential for this service
942+
templateParameters["UseApplicationCredentials"] = false
943+
if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, ironic.ServiceName); err != nil {
944+
Log.Error(err, "Failed to get ApplicationCredential for service", "service", ironic.ServiceName)
945+
return err
946+
} else if acData != nil {
947+
templateParameters["UseApplicationCredentials"] = true
948+
templateParameters["ACID"] = acData.ID
949+
templateParameters["ACSecret"] = acData.Secret
950+
Log.Info("Using ApplicationCredentials auth", "service", ironic.ServiceName)
951+
}
888952
} else {
889953
ironicAPI, err := ironicv1.GetIronicAPI(
890954
ctx, h, instance.Namespace, map[string]string{})

internal/controller/ironicinspector_controller.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,42 @@ func (r *IronicInspectorReconciler) SetupWithManager(
365365
return err
366366
}
367367

368+
// Application Credential secret watching function
369+
acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request {
370+
name := o.GetName()
371+
ns := o.GetNamespace()
372+
result := []reconcile.Request{}
373+
374+
// Only handle Secret objects
375+
if _, isSecret := o.(*corev1.Secret); !isSecret {
376+
return nil
377+
}
378+
379+
// Check if this is an ironic-inspector AC secret by name pattern (ac-ironic-inspector-secret)
380+
expectedSecretName := keystonev1.GetACSecretName("ironic-inspector")
381+
if name == expectedSecretName {
382+
// get all IronicInspector CRs in this namespace
383+
ironicInspectors := &ironicv1.IronicInspectorList{}
384+
listOpts := []client.ListOption{
385+
client.InNamespace(ns),
386+
}
387+
if err := r.List(context.Background(), ironicInspectors, listOpts...); err != nil {
388+
return nil
389+
}
390+
391+
// Enqueue reconcile for all ironic inspector instances
392+
for _, cr := range ironicInspectors.Items {
393+
objKey := client.ObjectKey{
394+
Namespace: ns,
395+
Name: cr.Name,
396+
}
397+
result = append(result, reconcile.Request{NamespacedName: objKey})
398+
}
399+
}
400+
401+
return result
402+
}
403+
368404
return ctrl.NewControllerManagedBy(mgr).
369405
For(&ironicv1.IronicInspector{}).
370406
Owns(&keystonev1.KeystoneService{}).
@@ -386,6 +422,8 @@ func (r *IronicInspectorReconciler) SetupWithManager(
386422
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
387423
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
388424
).
425+
Watches(&corev1.Secret{},
426+
handler.EnqueueRequestsFromMapFunc(acSecretFn)).
389427
Watches(&topologyv1.Topology{},
390428
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
391429
builder.WithPredicates(predicate.GenerationChangedPredicate{})).
@@ -616,6 +654,19 @@ func (r *IronicInspectorReconciler) reconcileConfigMapsAndSecrets(
616654
condition.InputReadyMessage)
617655
// run check secrets - end
618656

657+
// Verify Application Credentials if available
658+
ctrlResult, err := keystonev1.VerifyApplicationCredentialsForService(
659+
ctx,
660+
r.Client,
661+
instance.Namespace,
662+
instance.Spec.ServiceUser,
663+
&configMapVars,
664+
10*time.Second,
665+
)
666+
if (err != nil || ctrlResult != ctrl.Result{}) {
667+
return ctrlResult, "", err
668+
}
669+
619670
//
620671
// TLS input validation
621672
//
@@ -1496,6 +1547,18 @@ func (r *IronicInspectorReconciler) generateServiceSecrets(
14961547
templateParameters["service_catalog"] = servicePassword
14971548
templateParameters["ironic"] = servicePassword
14981549
templateParameters["swift"] = servicePassword
1550+
1551+
// Try to get Application Credential for this service
1552+
templateParameters["UseApplicationCredentials"] = false
1553+
if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, instance.Spec.ServiceUser); err != nil {
1554+
Log.Error(err, "Failed to get ApplicationCredential for service", "service", instance.Spec.ServiceUser)
1555+
return err
1556+
} else if acData != nil {
1557+
templateParameters["UseApplicationCredentials"] = true
1558+
templateParameters["ACID"] = acData.ID
1559+
templateParameters["ACSecret"] = acData.Secret
1560+
Log.Info("Using ApplicationCredentials auth", "service", ironic.ServiceName)
1561+
}
14991562
} else {
15001563
ironicAPI, err := ironicv1.GetIronicAPI(
15011564
ctx, h, instance.Namespace, map[string]string{})

0 commit comments

Comments
 (0)