diff --git a/api/go.mod b/api/go.mod index b5061222..45cc96c8 100644 --- a/api/go.mod +++ b/api/go.mod @@ -97,3 +97,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.13 //allow-merging replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging + +replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63 diff --git a/controllers/glanceapi_controller.go b/controllers/glanceapi_controller.go index 5d38095c..80a97dfb 100644 --- a/controllers/glanceapi_controller.go +++ b/controllers/glanceapi_controller.go @@ -378,6 +378,42 @@ func (r *GlanceAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man return nil } + // Application Credential secret watching function + acSecretFn := func(_ context.Context, o client.Object) []reconcile.Request { + name := o.GetName() + ns := o.GetNamespace() + result := []reconcile.Request{} + + // Only handle Secret objects + if _, isSecret := o.(*corev1.Secret); !isSecret { + return nil + } + + // Check if this is a glance AC secret by name pattern (ac-glance-secret) + expectedSecretName := keystonev1.GetACSecretName("glance") + if name == expectedSecretName { + // get all GlanceAPI CRs in this namespace + glanceAPIs := &glancev1.GlanceAPIList{} + listOpts := []client.ListOption{ + client.InNamespace(ns), + } + if err := r.List(context.Background(), glanceAPIs, listOpts...); err != nil { + return nil + } + + // Enqueue reconcile for all glance API instances + for _, cr := range glanceAPIs.Items { + objKey := client.ObjectKey{ + Namespace: ns, + Name: cr.Name, + } + result = append(result, reconcile.Request{NamespacedName: objKey}) + } + } + + return result + } + return ctrl.NewControllerManagedBy(mgr). For(&glancev1.GlanceAPI{}). Owns(&keystonev1.KeystoneEndpoint{}). @@ -393,6 +429,8 @@ func (r *GlanceAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(acSecretFn)). Watches(&memcachedv1.Memcached{}, handler.EnqueueRequestsFromMapFunc(memcachedFn)). Watches(&topologyv1.Topology{}, @@ -709,6 +747,11 @@ func (r *GlanceAPIReconciler) reconcileNormal( instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage) // run check OpenStack secret - end + // Verify Application Credentials if available + if res, err := keystonev1.VerifyApplicationCredentialsForService(ctx, r.Client, instance.Namespace, instance.APIName(), &configVars, glance.NormalDuration); err != nil || res.RequeueAfter > 0 { + return res, err + } + // // Check for required memcached used for caching // @@ -1164,6 +1207,7 @@ func (r *GlanceAPIReconciler) generateServiceConfig( memcached *memcachedv1.Memcached, wsgi bool, ) error { + Log := r.GetLogger(ctx) labels := labels.GetLabels(instance, labels.GetGroupLabel(glance.ServiceName), GetServiceLabels(instance)) db, err := mariadbv1.GetDatabaseByNameAndAccount(ctx, h, glance.DatabaseName, instance.Spec.DatabaseAccount, instance.Namespace) @@ -1261,6 +1305,17 @@ func (r *GlanceAPIReconciler) generateServiceConfig( "Wsgi": wsgi, } + templateParameters["UseApplicationCredentials"] = false + // Try to get Application Credential for this service (via keystone api helper) + if acData, err := keystonev1.GetApplicationCredentialFromSecret(ctx, r.Client, instance.Namespace, glance.ServiceName); err != nil { + Log.Error(err, "Failed to get ApplicationCredential for service", "service", glance.ServiceName) + } else if acData != nil { + templateParameters["UseApplicationCredentials"] = true + templateParameters["ACID"] = acData.ID + templateParameters["ACSecret"] = acData.Secret + Log.Info("Using ApplicationCredentials auth", "service", glance.ServiceName) + } + // (OSPRH-18291)Only set EndpointID parameter when the Endpoint has been // created and the associated ID is set in the keystoneapi CR. Because we // have the Keystone CR, we get the Region parameter mirrored in its diff --git a/go.mod b/go.mod index 00ef460c..8fbb8e88 100644 --- a/go.mod +++ b/go.mod @@ -117,3 +117,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.13 //allow-merging replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging + +replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63 diff --git a/go.sum b/go.sum index 90894a2b..bce8e62e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63 h1:ug2YPMQJ/+0ifOjFyaPx1YtX0zsVnL02pB2ngacYviw= +github.com/Deydra71/keystone-operator/api v0.0.0-20251103091514-244e15fe5d63/go.mod h1:FMFoO4MjEQ85JpdLtDHxYSZxvJ9KzHua+HdKhpl0KRI= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/templates/common/config/00-config.conf b/templates/common/config/00-config.conf index 4e66eb27..51625090 100644 --- a/templates/common/config/00-config.conf +++ b/templates/common/config/00-config.conf @@ -41,9 +41,18 @@ default_backend=default_backend [keystone_authtoken] www_authenticate_uri={{ .KeystonePublicURL }} auth_url={{ .KeystoneInternalURL }} -auth_type=password -username={{ .ServiceUser }} +{{ if .UseApplicationCredentials -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else -}} +auth_type = password +username = {{ .ServiceUser }} password = {{ .ServicePassword }} +{{- end }} +project_domain_name = Default +user_domain_name = Default +project_name = service {{ if (index . "MemcachedServers") }} memcached_servers = {{ .MemcachedServers }} memcache_pool_dead_retry = 10 @@ -55,12 +64,16 @@ memcache_tls_keyfile = {{ .MemcachedAuthKey }} memcache_tls_cafile = {{ .MemcachedAuthCa }} memcache_tls_enabled = true {{end}} -project_domain_name=Default -user_domain_name=Default -project_name=service [service_user] +{{ if .UseApplicationCredentials -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else -}} +auth_type = password password = {{ .ServicePassword }} +{{- end }} [oslo_messaging_notifications] {{ if (index . "TransportURL") -}} @@ -94,11 +107,16 @@ filesystem_store_datadir = /var/lib/glance/os_glance_tasks_store/ [oslo_limit] auth_url={{ .KeystoneInternalURL }} -auth_type = password +auth_type = {{ if .UseApplicationCredentials }}v3applicationcredential{{ else }}password{{ end }} +{{ if .UseApplicationCredentials -}} +application_credential_id = {{ .ACID }} +application_credential_secret = {{ .ACSecret }} +{{ else -}} username={{ .ServiceUser }} password = {{ .ServicePassword }} system_scope = all user_domain_id = default +{{- end }} {{ if (index . "EndpointID") -}} endpoint_id = {{ .EndpointID }} {{ end -}} diff --git a/test/functional/glanceapi_controller_test.go b/test/functional/glanceapi_controller_test.go index 1fe39a72..7f5734ee 100644 --- a/test/functional/glanceapi_controller_test.go +++ b/test/functional/glanceapi_controller_test.go @@ -26,8 +26,10 @@ import ( . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports . "github.com/onsi/gomega" //revive:disable:dot-imports glancev1 "github.com/openstack-k8s-operators/glance-operator/api/v1beta1" + "github.com/openstack-k8s-operators/glance-operator/pkg/glance" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" + keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" //revive:disable-next-line:dot-imports @@ -1286,4 +1288,116 @@ var _ = Describe("Glanceapi controller", func() { }, timeout, interval).Should(Succeed()) }) }) + + When("An ApplicationCredential is created for Glance", func() { + var ( + acName string + acSecretName string + servicePasswordSecret string + passwordSelector string + ) + BeforeEach(func() { + servicePasswordSecret = "ac-test-osp-secret" //nolint:gosec // G101 + passwordSelector = "GlancePassword" + + DeferCleanup(k8sClient.Delete, ctx, CreateGlanceSecret(glanceTest.Instance.Namespace, servicePasswordSecret)) + DeferCleanup(k8sClient.Delete, ctx, CreateGlanceMessageBusSecret(glanceTest.Instance.Namespace, glanceTest.RabbitmqSecretName)) + DeferCleanup(th.DeleteInstance, CreateDefaultGlance(glanceTest.Instance)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + glanceTest.Instance.Namespace, + glanceTest.GlanceDatabaseName.Name, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}})) + mariadb.CreateMariaDBDatabase(glanceTest.GlanceDatabaseName.Namespace, glanceTest.GlanceDatabaseName.Name, mariadbv1.MariaDBDatabaseSpec{}) + DeferCleanup(k8sClient.Delete, ctx, mariadb.GetMariaDBDatabase(glanceTest.GlanceDatabaseName)) + + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(glanceTest.Instance.Namespace)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(glanceTest.Instance.Namespace, MemcachedInstance, memcachedv1.MemcachedSpec{})) + infra.SimulateMemcachedReady(glanceTest.GlanceMemcached) + + // Create AC secret with test credentials + acName = fmt.Sprintf("ac-%s", glance.ServiceName) + acSecretName = acName + "-secret" + acSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }, + Data: map[string][]byte{ + "AC_ID": []byte("test-ac-id"), + "AC_SECRET": []byte("test-ac-secret"), + }, + } + DeferCleanup(k8sClient.Delete, ctx, acSecret) + Expect(k8sClient.Create(ctx, acSecret)).To(Succeed()) + + // Create AC CR + ac := &keystonev1.KeystoneApplicationCredential{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: glanceTest.Instance.Namespace, + Name: acName, + }, + Spec: keystonev1.KeystoneApplicationCredentialSpec{ + UserName: glance.ServiceName, + Secret: servicePasswordSecret, + PasswordSelector: passwordSelector, + Roles: []string{"admin", "member"}, + AccessRules: []keystonev1.ACRule{{Service: "identity", Method: "POST", Path: "/auth/tokens"}}, + ExpirationDays: 30, + GracePeriodDays: 5, + }, + } + DeferCleanup(k8sClient.Delete, ctx, ac) + Expect(k8sClient.Create(ctx, ac)).To(Succeed()) + + // Simulate AC controller updating the status + fetched := &keystonev1.KeystoneApplicationCredential{} + key := types.NamespacedName{Namespace: ac.Namespace, Name: ac.Name} + Expect(k8sClient.Get(ctx, key, fetched)).To(Succeed()) + + fetched.Status.SecretName = acSecretName + now := metav1.Now() + readyCond := condition.Condition{ + Type: condition.ReadyCondition, + Status: corev1.ConditionTrue, + Reason: condition.ReadyReason, + Message: condition.ReadyMessage, + LastTransitionTime: now, + } + fetched.Status.Conditions = condition.Conditions{readyCond} + Expect(k8sClient.Status().Update(ctx, fetched)).To(Succeed()) + + // Create GlanceAPI using the service password secret + spec := CreateGlanceAPISpec(GlanceAPITypeInternal) + spec["secret"] = servicePasswordSecret + DeferCleanup(th.DeleteInstance, CreateGlanceAPI(glanceTest.GlanceInternal, spec)) + + mariadb.SimulateMariaDBAccountCompleted(glanceTest.GlanceDatabaseAccount) + mariadb.SimulateMariaDBDatabaseCompleted(glanceTest.GlanceDatabaseName) + th.SimulateStatefulSetReplicaReady(glanceTest.GlanceInternalStatefulSet) + + keystone.SimulateKeystoneEndpointReady(glanceTest.GlanceInternal) + }) + + It("should render ApplicationCredential auth in 00-config.conf", func() { + keystone.SimulateKeystoneEndpointReady(glanceTest.GlanceInternal) + + // Wait for the config to be generated and updated with AC auth + Eventually(func(g Gomega) { + cfgSecret := th.GetSecret(glanceTest.GlanceInternalConfigMapData) + g.Expect(cfgSecret).NotTo(BeNil()) + + conf := string(cfgSecret.Data["00-config.conf"]) + + g.Expect(conf).To(ContainSubstring( + "application_credential_id = test-ac-id"), + ) + g.Expect(conf).To(ContainSubstring( + "application_credential_secret = test-ac-secret"), + ) + }, timeout, interval).Should(Succeed()) + }) + }) })