Skip to content

Commit 72a7594

Browse files
committed
Application Credential support
Signed-off-by: Veronika Fisarova <[email protected]>
1 parent 3064023 commit 72a7594

File tree

6 files changed

+240
-4
lines changed

6 files changed

+240
-4
lines changed

api/go.mod

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

100100
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
101+
102+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251002062345-bf9333e0a92e

controllers/glanceapi_controller.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"strings"
23+
"time"
2324

2425
batchv1 "k8s.io/api/batch/v1"
2526
"k8s.io/apimachinery/pkg/api/resource"
@@ -378,6 +379,42 @@ func (r *GlanceAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
378379
return nil
379380
}
380381

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

751+
// Verify Application Credentials if available
752+
if res, err := r.verifyApplicationCredentials(ctx, helper, instance, &configVars); err != nil || res.RequeueAfter > 0 {
753+
return res, err
754+
}
755+
712756
//
713757
// Check for required memcached used for caching
714758
//
@@ -1164,6 +1208,7 @@ func (r *GlanceAPIReconciler) generateServiceConfig(
11641208
memcached *memcachedv1.Memcached,
11651209
wsgi bool,
11661210
) error {
1211+
Log := r.GetLogger(ctx)
11671212
labels := labels.GetLabels(instance, labels.GetGroupLabel(glance.ServiceName), GetServiceLabels(instance))
11681213

11691214
db, err := mariadbv1.GetDatabaseByNameAndAccount(ctx, h, glance.DatabaseName, instance.Spec.DatabaseAccount, instance.Namespace)
@@ -1261,6 +1306,17 @@ func (r *GlanceAPIReconciler) generateServiceConfig(
12611306
"Wsgi": wsgi,
12621307
}
12631308

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

17011757
return ep, nil
17021758
}
1759+
1760+
// verifyApplicationCredentials checks if ApplicationCredential secret exists and adds it to configVars
1761+
// The AC secret is created by the keystone-operator's AC controller when the AC is ready.
1762+
// If the secret exists and is valid, we use AC auth. Otherwise, we fall back to password auth.
1763+
func (r *GlanceAPIReconciler) verifyApplicationCredentials(
1764+
ctx context.Context,
1765+
_ *helper.Helper,
1766+
instance *glancev1.GlanceAPI,
1767+
configVars *map[string]env.Setter,
1768+
) (ctrl.Result, error) {
1769+
log := r.GetLogger(ctx)
1770+
1771+
// Check if AC secret exists (created by keystone AC controller)
1772+
acSecretName := keystonev1.GetACSecretName(glance.ServiceName)
1773+
secretKey := types.NamespacedName{Namespace: instance.Namespace, Name: acSecretName}
1774+
1775+
hash, res, err := secret.VerifySecret(
1776+
ctx,
1777+
secretKey,
1778+
[]string{"AC_ID", "AC_SECRET"},
1779+
r.Client,
1780+
10*time.Second,
1781+
)
1782+
1783+
// VerifySecret returns res.RequeueAfter > 0 when secret not found (not an error)
1784+
// For AC, this is optional, so we just skip it instead of requeueing
1785+
if res.RequeueAfter > 0 {
1786+
log.Info("ApplicationCredential secret not found, using password auth")
1787+
return ctrl.Result{}, nil
1788+
}
1789+
1790+
if err != nil {
1791+
// Actual error (not NotFound) - log and continue with password auth
1792+
log.Info("ApplicationCredential secret verification failed, continuing with password auth", "error", err.Error())
1793+
return ctrl.Result{}, nil
1794+
}
1795+
1796+
// AC secret exists and is valid - add to configVars for hash tracking
1797+
(*configVars)["secret-"+acSecretName] = env.SetValue(hash)
1798+
log.Info("Using ApplicationCredential authentication")
1799+
1800+
return ctrl.Result{}, nil
1801+
}

go.mod

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

119119
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
120+
121+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251002062345-bf9333e0a92e

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-20251002062345-bf9333e0a92e h1:X/V9w5FetfpKcEUSmeRQuynUZolyLEFMHNo4UliSOcw=
2+
github.com/Deydra71/keystone-operator/api v0.0.0-20251002062345-bf9333e0a92e/go.mod h1:F8KOpXlzGSuPkBIWybFHhbxPwvxrkbpRnnyBzxn65aw=
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=
@@ -89,8 +91,6 @@ github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20250911092040-
8991
github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20250911092040-f829125f6046/go.mod h1:j9yGw80eA38kEvHEkx/BONqIhLnKFmpjAtyAB8S817E=
9092
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20250922155301-057562fb7182 h1:Ea+FZQOW0Eha1jorgSECFeqI9UrKz8TZlGnSM7X8Yf4=
9193
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20250922155301-057562fb7182/go.mod h1:3Im8PFiRKPaOZpOuqYShJRN2O2pfjUuhDTUpW4KMHZw=
92-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20250916093250-82a76386143d h1:lSRMftk/MbN4qd8ihHh9ucdX4sfR/HUudEcy2h/BNhQ=
93-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20250916093250-82a76386143d/go.mod h1:7ZuNZNtwRYklS2H5E5YSjsHOI2sYbAl1AD+N0W/G+8A=
9494
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20250922082314-c83d83092a04 h1:JqJd39rF8rD9KIHmOEFbHP8UyYgttfuouj+kAFNtymU=
9595
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20250922082314-c83d83092a04/go.mod h1:SmKRclrynSSRCXSLOoWlETalJPvt62ObHsfW8iPvtDA=
9696
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20250922082314-c83d83092a04 h1:1t4qZshLvaTzytFb9foCBtTtKT4uXzYtVaYTlgYbt+4=

templates/common/config/00-config.conf

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,14 @@ default_backend=default_backend
4141
[keystone_authtoken]
4242
www_authenticate_uri={{ .KeystonePublicURL }}
4343
auth_url={{ .KeystoneInternalURL }}
44-
auth_type=password
44+
auth_type={{ if .UseApplicationCredentials }}v3applicationcredential{{ else }}password{{ end }}
45+
{{ if .UseApplicationCredentials -}}
46+
application_credential_id = {{ .ACID }}
47+
application_credential_secret = {{ .ACSecret }}
48+
{{ else -}}
4549
username={{ .ServiceUser }}
4650
password = {{ .ServicePassword }}
51+
{{- end }}
4752
{{ if (index . "MemcachedServers") }}
4853
memcached_servers = {{ .MemcachedServers }}
4954
memcache_pool_dead_retry = 10
@@ -55,12 +60,20 @@ memcache_tls_keyfile = {{ .MemcachedAuthKey }}
5560
memcache_tls_cafile = {{ .MemcachedAuthCa }}
5661
memcache_tls_enabled = true
5762
{{end}}
63+
{{ if not .UseApplicationCredentials -}}
5864
project_domain_name=Default
5965
user_domain_name=Default
6066
project_name=service
67+
{{- end }}
6168

6269
[service_user]
70+
{{ if .UseApplicationCredentials -}}
71+
auth_type=v3applicationcredential
72+
application_credential_id = {{ .ACID }}
73+
application_credential_secret = {{ .ACSecret }}
74+
{{ else -}}
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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ package functional
1919
import (
2020
"fmt"
2121
"os"
22+
"time"
2223

2324
appsv1 "k8s.io/api/apps/v1"
2425
"k8s.io/apimachinery/pkg/types"
2526

2627
. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
2728
. "github.com/onsi/gomega" //revive:disable:dot-imports
2829
glancev1 "github.com/openstack-k8s-operators/glance-operator/api/v1beta1"
30+
"github.com/openstack-k8s-operators/glance-operator/pkg/glance"
2931
memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1"
3032
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
33+
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
3134
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
3235

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

0 commit comments

Comments
 (0)