Skip to content

Commit 64b4e37

Browse files
committed
Application Credential support
Signed-off-by: Veronika Fisarova <[email protected]>
1 parent 5f00daf commit 64b4e37

File tree

6 files changed

+199
-8
lines changed

6 files changed

+199
-8
lines changed

api/go.mod

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

9898
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
99+
100+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251014080018-27792b7a40a5

controllers/glanceapi_controller.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,42 @@ func (r *GlanceAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
378378
return nil
379379
}
380380

381+
// Application Credential secret watching function
382+
acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request {
383+
name := o.GetName()
384+
ns := o.GetNamespace()
385+
result := []reconcile.Request{}
386+
387+
// Only handle Secret objects
388+
if _, isSecret := o.(*corev1.Secret); !isSecret {
389+
return nil
390+
}
391+
392+
// Check if this is a glance AC secret by name pattern (ac-glance-secret)
393+
expectedSecretName := keystonev1.GetACSecretName("glance")
394+
if name == expectedSecretName {
395+
// get all GlanceAPI CRs in this namespace
396+
glanceAPIs := &glancev1.GlanceAPIList{}
397+
listOpts := []client.ListOption{
398+
client.InNamespace(ns),
399+
}
400+
if err := r.List(context.Background(), glanceAPIs, listOpts...); err != nil {
401+
return nil
402+
}
403+
404+
// Enqueue reconcile for all glance API instances
405+
for _, cr := range glanceAPIs.Items {
406+
objKey := client.ObjectKey{
407+
Namespace: ns,
408+
Name: cr.Name,
409+
}
410+
result = append(result, reconcile.Request{NamespacedName: objKey})
411+
}
412+
}
413+
414+
return result
415+
}
416+
381417
return ctrl.NewControllerManagedBy(mgr).
382418
For(&glancev1.GlanceAPI{}).
383419
Owns(&keystonev1.KeystoneEndpoint{}).
@@ -393,6 +429,8 @@ func (r *GlanceAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
393429
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
394430
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
395431
).
432+
Watches(&corev1.Secret{},
433+
handler.EnqueueRequestsFromMapFunc(acSecretFn)).
396434
Watches(&memcachedv1.Memcached{},
397435
handler.EnqueueRequestsFromMapFunc(memcachedFn)).
398436
Watches(&topologyv1.Topology{},
@@ -709,6 +747,11 @@ func (r *GlanceAPIReconciler) reconcileNormal(
709747
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)
710748
// run check OpenStack secret - end
711749

750+
// Verify Application Credentials if available
751+
if res, err := keystonev1.VerifyApplicationCredentialsForService(ctx, r.Client, instance.Namespace, instance.APIName(), &configVars, glance.NormalDuration); err != nil || res.RequeueAfter > 0 {
752+
return res, err
753+
}
754+
712755
//
713756
// Check for required memcached used for caching
714757
//
@@ -1164,6 +1207,7 @@ func (r *GlanceAPIReconciler) generateServiceConfig(
11641207
memcached *memcachedv1.Memcached,
11651208
wsgi bool,
11661209
) error {
1210+
Log := r.GetLogger(ctx)
11671211
labels := labels.GetLabels(instance, labels.GetGroupLabel(glance.ServiceName), GetServiceLabels(instance))
11681212

11691213
db, err := mariadbv1.GetDatabaseByNameAndAccount(ctx, h, glance.DatabaseName, instance.Spec.DatabaseAccount, instance.Namespace)
@@ -1261,6 +1305,17 @@ func (r *GlanceAPIReconciler) generateServiceConfig(
12611305
"Wsgi": wsgi,
12621306
}
12631307

1308+
templateParameters["UseApplicationCredentials"] = false
1309+
// Try to get Application Credential for this service (via keystone api helper)
1310+
if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, glance.ServiceName); err != nil {
1311+
Log.Error(err, "Failed to get ApplicationCredential for service", "service", glance.ServiceName)
1312+
} else if acData != nil {
1313+
templateParameters["UseApplicationCredentials"] = true
1314+
templateParameters["ACID"] = acData.ID
1315+
templateParameters["ACSecret"] = acData.Secret
1316+
Log.Info("Using ApplicationCredentials auth", "service", glance.ServiceName)
1317+
}
1318+
12641319
// (OSPRH-18291)Only set EndpointID parameter when the Endpoint has been
12651320
// created and the associated ID is set in the keystoneapi CR. Because we
12661321
// have the Keystone CR, we get the Region parameter mirrored in its

go.mod

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

120120
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
121+
122+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251014080018-27792b7a40a5

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-20251014080018-27792b7a40a5 h1:qpFuqb7xf9rgua2qwOIJYhB9+ePHB0FfQ/mIufzw7Nc=
2+
github.com/Deydra71/keystone-operator/api v0.0.0-20251014080018-27792b7a40a5/go.mod h1:braI3juap0JIy6XOvu0AHqVGkfn2/dbw5BBRv84oSAw=
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -104,8 +106,6 @@ github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20251002161904-
104106
github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20251002161904-e6aac32a6d8a/go.mod h1:DBUCGwCEp3dEncPLPWHUX9P9EQMVMTDjXxHpdkXxsQQ=
105107
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251002120642-c2d58c6fc03e h1:5q47hHT53v0PnNj2pwHHQ1+ZWC3kQLu1jtulTUrJ2cE=
106108
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251002120642-c2d58c6fc03e/go.mod h1:LfqzznghLpo+b9jVgyvqUoOZMcc3Ff0gXSmLLtFsj9w=
107-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251002084815-17073bbaef03 h1:6yHHmTZ/AM+RIR3EBuXgrIXTsKE2bGIOfX4v5FrNcho=
108-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251002084815-17073bbaef03/go.mod h1:fuXKxuK4eCE9zV/Uk4d/i1Cny77wXWHpVTL9/UKwxbo=
109109
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20250929092825-4c2402451077 h1:missBxDwEfOdkHVKd6zyCyaQjSObw9Ge1O4A7WU5EuM=
110110
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20250929092825-4c2402451077/go.mod h1:CjsYQ/dUr4eUmBEvM3UFUxvYvl2bAhGfGflaD+N4fWA=
111111
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20250929092825-4c2402451077 h1:z576+XURUzBr7S48aJyHryDgnMFtvbOshXgXCbrGt04=

templates/common/config/00-config.conf

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,18 @@ default_backend=default_backend
4141
[keystone_authtoken]
4242
www_authenticate_uri={{ .KeystonePublicURL }}
4343
auth_url={{ .KeystoneInternalURL }}
44-
auth_type=password
45-
username={{ .ServiceUser }}
44+
{{ if .UseApplicationCredentials -}}
45+
auth_type = v3applicationcredential
46+
application_credential_id = {{ .ACID }}
47+
application_credential_secret = {{ .ACSecret }}
48+
{{ else -}}
49+
auth_type = password
50+
username = {{ .ServiceUser }}
4651
password = {{ .ServicePassword }}
52+
project_domain_name = Default
53+
user_domain_name = Default
54+
project_name = service
55+
{{- end }}
4756
{{ if (index . "MemcachedServers") }}
4857
memcached_servers = {{ .MemcachedServers }}
4958
memcache_pool_dead_retry = 10
@@ -55,12 +64,16 @@ memcache_tls_keyfile = {{ .MemcachedAuthKey }}
5564
memcache_tls_cafile = {{ .MemcachedAuthCa }}
5665
memcache_tls_enabled = true
5766
{{end}}
58-
project_domain_name=Default
59-
user_domain_name=Default
60-
project_name=service
6167

6268
[service_user]
69+
{{ if .UseApplicationCredentials -}}
70+
auth_type = v3applicationcredential
71+
application_credential_id = {{ .ACID }}
72+
application_credential_secret = {{ .ACSecret }}
73+
{{ else -}}
74+
auth_type = password
6375
password = {{ .ServicePassword }}
76+
{{- end }}
6477

6578
[oslo_messaging_notifications]
6679
{{ if (index . "TransportURL") -}}
@@ -94,11 +107,16 @@ filesystem_store_datadir = /var/lib/glance/os_glance_tasks_store/
94107

95108
[oslo_limit]
96109
auth_url={{ .KeystoneInternalURL }}
97-
auth_type = password
110+
auth_type = {{ if .UseApplicationCredentials }}v3applicationcredential{{ else }}password{{ end }}
111+
{{ if .UseApplicationCredentials -}}
112+
application_credential_id = {{ .ACID }}
113+
application_credential_secret = {{ .ACSecret }}
114+
{{ else -}}
98115
username={{ .ServiceUser }}
99116
password = {{ .ServicePassword }}
100117
system_scope = all
101118
user_domain_id = default
119+
{{- end }}
102120
{{ if (index . "EndpointID") -}}
103121
endpoint_id = {{ .EndpointID }}
104122
{{ end -}}

test/functional/glanceapi_controller_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ import (
2626
. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
2727
. "github.com/onsi/gomega" //revive:disable:dot-imports
2828
glancev1 "github.com/openstack-k8s-operators/glance-operator/api/v1beta1"
29+
"github.com/openstack-k8s-operators/glance-operator/pkg/glance"
2930
memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1"
3031
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
32+
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
3133
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
3234

3335
//revive:disable-next-line:dot-imports
@@ -1286,4 +1288,116 @@ var _ = Describe("Glanceapi controller", func() {
12861288
}, timeout, interval).Should(Succeed())
12871289
})
12881290
})
1291+
1292+
When("An ApplicationCredential is created for Glance", func() {
1293+
var (
1294+
acName string
1295+
acSecretName string
1296+
servicePasswordSecret string
1297+
passwordSelector string
1298+
)
1299+
BeforeEach(func() {
1300+
servicePasswordSecret = "ac-test-osp-secret" //nolint:gosec // G101
1301+
passwordSelector = "GlancePassword"
1302+
1303+
DeferCleanup(k8sClient.Delete, ctx, CreateGlanceSecret(glanceTest.Instance.Namespace, servicePasswordSecret))
1304+
DeferCleanup(k8sClient.Delete, ctx, CreateGlanceMessageBusSecret(glanceTest.Instance.Namespace, glanceTest.RabbitmqSecretName))
1305+
DeferCleanup(th.DeleteInstance, CreateDefaultGlance(glanceTest.Instance))
1306+
DeferCleanup(
1307+
mariadb.DeleteDBService,
1308+
mariadb.CreateDBService(
1309+
glanceTest.Instance.Namespace,
1310+
glanceTest.GlanceDatabaseName.Name,
1311+
corev1.ServiceSpec{
1312+
Ports: []corev1.ServicePort{{Port: 3306}}}))
1313+
mariadb.CreateMariaDBDatabase(glanceTest.GlanceDatabaseName.Namespace, glanceTest.GlanceDatabaseName.Name, mariadbv1.MariaDBDatabaseSpec{})
1314+
DeferCleanup(k8sClient.Delete, ctx, mariadb.GetMariaDBDatabase(glanceTest.GlanceDatabaseName))
1315+
1316+
DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(glanceTest.Instance.Namespace))
1317+
DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(glanceTest.Instance.Namespace, MemcachedInstance, memcachedv1.MemcachedSpec{}))
1318+
infra.SimulateMemcachedReady(glanceTest.GlanceMemcached)
1319+
1320+
// Create AC secret with test credentials
1321+
acName = fmt.Sprintf("ac-%s", glance.ServiceName)
1322+
acSecretName = acName + "-secret"
1323+
acSecret := &corev1.Secret{
1324+
ObjectMeta: metav1.ObjectMeta{
1325+
Namespace: glanceTest.Instance.Namespace,
1326+
Name: acSecretName,
1327+
},
1328+
Data: map[string][]byte{
1329+
"AC_ID": []byte("test-ac-id"),
1330+
"AC_SECRET": []byte("test-ac-secret"),
1331+
},
1332+
}
1333+
DeferCleanup(k8sClient.Delete, ctx, acSecret)
1334+
Expect(k8sClient.Create(ctx, acSecret)).To(Succeed())
1335+
1336+
// Create AC CR
1337+
ac := &keystonev1.KeystoneApplicationCredential{
1338+
ObjectMeta: metav1.ObjectMeta{
1339+
Namespace: glanceTest.Instance.Namespace,
1340+
Name: acName,
1341+
},
1342+
Spec: keystonev1.KeystoneApplicationCredentialSpec{
1343+
UserName: glance.ServiceName,
1344+
Secret: servicePasswordSecret,
1345+
PasswordSelector: passwordSelector,
1346+
Roles: []string{"admin", "member"},
1347+
AccessRules: []keystonev1.ACRule{{Service: "identity", Method: "POST", Path: "/auth/tokens"}},
1348+
ExpirationDays: 30,
1349+
GracePeriodDays: 5,
1350+
},
1351+
}
1352+
DeferCleanup(k8sClient.Delete, ctx, ac)
1353+
Expect(k8sClient.Create(ctx, ac)).To(Succeed())
1354+
1355+
// Simulate AC controller updating the status
1356+
fetched := &keystonev1.KeystoneApplicationCredential{}
1357+
key := types.NamespacedName{Namespace: ac.Namespace, Name: ac.Name}
1358+
Expect(k8sClient.Get(ctx, key, fetched)).To(Succeed())
1359+
1360+
fetched.Status.SecretName = acSecretName
1361+
now := metav1.Now()
1362+
readyCond := condition.Condition{
1363+
Type: condition.ReadyCondition,
1364+
Status: corev1.ConditionTrue,
1365+
Reason: condition.ReadyReason,
1366+
Message: condition.ReadyMessage,
1367+
LastTransitionTime: now,
1368+
}
1369+
fetched.Status.Conditions = condition.Conditions{readyCond}
1370+
Expect(k8sClient.Status().Update(ctx, fetched)).To(Succeed())
1371+
1372+
// Create GlanceAPI using the service password secret
1373+
spec := CreateGlanceAPISpec(GlanceAPITypeInternal)
1374+
spec["secret"] = servicePasswordSecret
1375+
DeferCleanup(th.DeleteInstance, CreateGlanceAPI(glanceTest.GlanceInternal, spec))
1376+
1377+
mariadb.SimulateMariaDBAccountCompleted(glanceTest.GlanceDatabaseAccount)
1378+
mariadb.SimulateMariaDBDatabaseCompleted(glanceTest.GlanceDatabaseName)
1379+
th.SimulateStatefulSetReplicaReady(glanceTest.GlanceInternalStatefulSet)
1380+
1381+
keystone.SimulateKeystoneEndpointReady(glanceTest.GlanceInternal)
1382+
})
1383+
1384+
It("should render ApplicationCredential auth in 00-config.conf", func() {
1385+
keystone.SimulateKeystoneEndpointReady(glanceTest.GlanceInternal)
1386+
1387+
// Wait for the config to be generated and updated with AC auth
1388+
Eventually(func(g Gomega) {
1389+
cfgSecret := th.GetSecret(glanceTest.GlanceInternalConfigMapData)
1390+
g.Expect(cfgSecret).NotTo(BeNil())
1391+
1392+
conf := string(cfgSecret.Data["00-config.conf"])
1393+
1394+
g.Expect(conf).To(ContainSubstring(
1395+
"application_credential_id = test-ac-id"),
1396+
)
1397+
g.Expect(conf).To(ContainSubstring(
1398+
"application_credential_secret = test-ac-secret"),
1399+
)
1400+
}, timeout, interval).Should(Succeed())
1401+
})
1402+
})
12891403
})

0 commit comments

Comments
 (0)