From 4a87f6fed047159e193ad681de5449deae823f4a Mon Sep 17 00:00:00 2001 From: Grzegorz Grasza Date: Thu, 29 Jan 2026 16:29:55 +0100 Subject: [PATCH] Application Credential support Co-authored-by: Veronika Fisarova Co-authored-by: Cursor --- .../glance.openstack.org_glanceapis.yaml | 8 + api/bases/glance.openstack.org_glances.yaml | 8 + api/v1beta1/common_types.go | 17 +- api/v1beta1/glance_webhook.go | 3 +- api/v1beta1/glanceapi_webhook.go | 7 +- api/v1beta1/zz_generated.deepcopy.go | 16 ++ .../glance.openstack.org_glanceapis.yaml | 8 + .../bases/glance.openstack.org_glances.yaml | 8 + internal/controller/glance_common.go | 4 + internal/controller/glanceapi_controller.go | 34 ++++ templates/common/config/00-config.conf | 29 +++- test/functional/base_test.go | 51 ++++++ test/functional/glance_test_data.go | 4 + test/functional/glanceapi_controller_test.go | 157 ++++++++++++++++++ 14 files changed, 342 insertions(+), 12 deletions(-) diff --git a/api/bases/glance.openstack.org_glanceapis.yaml b/api/bases/glance.openstack.org_glanceapis.yaml index 01068a351..43ac78dcd 100644 --- a/api/bases/glance.openstack.org_glanceapis.yaml +++ b/api/bases/glance.openstack.org_glanceapis.yaml @@ -65,6 +65,14 @@ spec: - single - edge type: string + auth: + description: Auth - Parameters related to authentication + properties: + applicationCredentialSecret: + description: ApplicationCredentialSecret - Secret containing Application + Credential ID and Secret + type: string + type: object containerImage: description: ContainerImage - GlanceAPI Container Image URL type: string diff --git a/api/bases/glance.openstack.org_glances.yaml b/api/bases/glance.openstack.org_glances.yaml index 80729cafe..34c483d4d 100644 --- a/api/bases/glance.openstack.org_glances.yaml +++ b/api/bases/glance.openstack.org_glances.yaml @@ -1229,6 +1229,14 @@ spec: APITimeout minimum: 1 type: integer + auth: + description: Auth - Parameters related to authentication + properties: + applicationCredentialSecret: + description: ApplicationCredentialSecret - Secret containing + Application Credential ID and Secret + type: string + type: object customServiceConfig: description: |- CustomServiceConfig - customize the service config using this parameter to change service defaults, diff --git a/api/v1beta1/common_types.go b/api/v1beta1/common_types.go index 7b41df7ef..2b8965bda 100644 --- a/api/v1beta1/common_types.go +++ b/api/v1beta1/common_types.go @@ -19,12 +19,12 @@ package v1beta1 import ( "strings" + topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/service" "github.com/openstack-k8s-operators/lib-common/modules/common/tls" "github.com/openstack-k8s-operators/lib-common/modules/common/util" - topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" - "k8s.io/apimachinery/pkg/util/validation/field" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation/field" ) const ( @@ -100,6 +100,11 @@ type GlanceAPITemplate struct { // TLS - Parameters related to the TLS TLS tls.API `json:"tls,omitempty"` + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Auth - Parameters related to authentication + Auth AuthSpec `json:"auth,omitempty"` + // ImageCache - It represents the struct to expose the ImageCache related // parameters (size of the PVC and cronJob schedule) // +kubebuilder:validation:Optional @@ -147,6 +152,14 @@ type APIOverrideSpec struct { Service map[service.Endpoint]service.RoutedOverrideSpec `json:"service,omitempty"` } +// AuthSpec defines authentication parameters +type AuthSpec struct { + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // ApplicationCredentialSecret - Secret containing Application Credential ID and Secret + ApplicationCredentialSecret string `json:"applicationCredentialSecret,omitempty"` +} + // SetupDefaults - initializes any CRD field defaults based on environment variables (the defaulting mechanism itself is implemented via webhooks) func SetupDefaults() { // Acquire environmental defaults and initialize Glance defaults with them diff --git a/api/v1beta1/glance_webhook.go b/api/v1beta1/glance_webhook.go index 8ba51c4d7..e0fcb2f6e 100644 --- a/api/v1beta1/glance_webhook.go +++ b/api/v1beta1/glance_webhook.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/google/go-cmp/cmp" + topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/service" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -29,7 +30,6 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook" ) @@ -157,7 +157,6 @@ func (r *GlanceSpecCore) Default() { } } - // Check if File is used as a backend for Glance func isFileBackend(customServiceConfig string, topLevel bool) bool { availableBackends := GetEnabledBackends(customServiceConfig) diff --git a/api/v1beta1/glanceapi_webhook.go b/api/v1beta1/glanceapi_webhook.go index 9f6f5cd7b..033ec1ec3 100644 --- a/api/v1beta1/glanceapi_webhook.go +++ b/api/v1beta1/glanceapi_webhook.go @@ -17,13 +17,14 @@ limitations under the License. package v1beta1 import ( + "fmt" + + "github.com/google/go-cmp/cmp" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" - "fmt" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - "github.com/google/go-cmp/cmp" - apierrors "k8s.io/apimachinery/pkg/api/errors" ) // GlanceAPIDefaults - diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index f7a0c84c5..8551a004d 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -50,6 +50,21 @@ func (in *APIOverrideSpec) DeepCopy() *APIOverrideSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthSpec) DeepCopyInto(out *AuthSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthSpec. +func (in *AuthSpec) DeepCopy() *AuthSpec { + if in == nil { + return nil + } + out := new(AuthSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DBPurge) DeepCopyInto(out *DBPurge) { *out = *in @@ -285,6 +300,7 @@ func (in *GlanceAPITemplate) DeepCopyInto(out *GlanceAPITemplate) { in.Override.DeepCopyInto(&out.Override) out.Storage = in.Storage in.TLS.DeepCopyInto(&out.TLS) + out.Auth = in.Auth out.ImageCache = in.ImageCache } diff --git a/config/crd/bases/glance.openstack.org_glanceapis.yaml b/config/crd/bases/glance.openstack.org_glanceapis.yaml index 01068a351..43ac78dcd 100644 --- a/config/crd/bases/glance.openstack.org_glanceapis.yaml +++ b/config/crd/bases/glance.openstack.org_glanceapis.yaml @@ -65,6 +65,14 @@ spec: - single - edge type: string + auth: + description: Auth - Parameters related to authentication + properties: + applicationCredentialSecret: + description: ApplicationCredentialSecret - Secret containing Application + Credential ID and Secret + type: string + type: object containerImage: description: ContainerImage - GlanceAPI Container Image URL type: string diff --git a/config/crd/bases/glance.openstack.org_glances.yaml b/config/crd/bases/glance.openstack.org_glances.yaml index 80729cafe..34c483d4d 100644 --- a/config/crd/bases/glance.openstack.org_glances.yaml +++ b/config/crd/bases/glance.openstack.org_glances.yaml @@ -1229,6 +1229,14 @@ spec: APITimeout minimum: 1 type: integer + auth: + description: Auth - Parameters related to authentication + properties: + applicationCredentialSecret: + description: ApplicationCredentialSecret - Secret containing + Application Credential ID and Secret + type: string + type: object customServiceConfig: description: |- CustomServiceConfig - customize the service config using this parameter to change service defaults, diff --git a/internal/controller/glance_common.go b/internal/controller/glance_common.go index c3cc57141..ed614c400 100644 --- a/internal/controller/glance_common.go +++ b/internal/controller/glance_common.go @@ -48,6 +48,8 @@ import ( // Common static errors for glance controllers var ( ErrNetworkAttachmentConfig = errors.New("not all pods have interfaces with ips as configured in NetworkAttachments") + ErrACSecretNotFound = errors.New("ApplicationCredential secret not found") + ErrACSecretMissingKeys = errors.New("ApplicationCredential secret missing required keys") ) // fields to index to reconcile when change @@ -58,6 +60,7 @@ const ( tlsAPIPublicField = ".spec.tls.api.public.secretName" topologyField = ".spec.topologyRef.Name" notificationBusSecretField = ".spec.notificationBusSecret" + authAppCredSecretField = ".spec.auth.applicationCredentialSecret" // #nosec G101 ) var ( @@ -71,6 +74,7 @@ var ( tlsAPIPublicField, topologyField, notificationBusSecretField, + authAppCredSecretField, } ) diff --git a/internal/controller/glanceapi_controller.go b/internal/controller/glanceapi_controller.go index 92e3565f8..ea59a5a83 100644 --- a/internal/controller/glanceapi_controller.go +++ b/internal/controller/glanceapi_controller.go @@ -286,6 +286,18 @@ func (r *GlanceAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man return err } + // index authAppCredSecretField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &glancev1.GlanceAPI{}, authAppCredSecretField, func(rawObj client.Object) []string { + // Extract the application credential secret name from the spec, if one is provided + cr := rawObj.(*glancev1.GlanceAPI) + if cr.Spec.Auth.ApplicationCredentialSecret == "" { + return nil + } + return []string{cr.Spec.Auth.ApplicationCredentialSecret} + }); err != nil { + return err + } + // Watch for changes to any CustomServiceConfigSecrets. Global secrets svcSecretFn := func(_ context.Context, o client.Object) []reconcile.Request { var namespace = o.GetNamespace() @@ -1285,6 +1297,28 @@ func (r *GlanceAPIReconciler) generateServiceConfig( "Wsgi": wsgi, } + // Try to get Application Credential from the secret specified in the CR + if instance.Spec.Auth.ApplicationCredentialSecret != "" { + acSecretObj, _, err := secret.GetSecret(ctx, h, instance.Spec.Auth.ApplicationCredentialSecret, instance.Namespace) + if err != nil { + if k8s_errors.IsNotFound(err) { + Log.Info("ApplicationCredential secret not found, waiting", "secret", instance.Spec.Auth.ApplicationCredentialSecret) + return fmt.Errorf("%w: %s", ErrACSecretNotFound, instance.Spec.Auth.ApplicationCredentialSecret) + } + Log.Error(err, "Failed to get ApplicationCredential secret", "secret", instance.Spec.Auth.ApplicationCredentialSecret) + return err + } + acID, okID := acSecretObj.Data[keystonev1.ACIDSecretKey] + acSecretData, okSecret := acSecretObj.Data[keystonev1.ACSecretSecretKey] + if okID && len(acID) > 0 && okSecret && len(acSecretData) > 0 { + templateParameters["ApplicationCredentialID"] = string(acID) + templateParameters["ApplicationCredentialSecret"] = string(acSecretData) + Log.Info("Using ApplicationCredentials auth", "secret", instance.Spec.Auth.ApplicationCredentialSecret) + } else { + return fmt.Errorf("%w: %s", ErrACSecretMissingKeys, instance.Spec.Auth.ApplicationCredentialSecret) + } + } + // (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/templates/common/config/00-config.conf b/templates/common/config/00-config.conf index 417dcbc3a..b96f96339 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 (index . "ApplicationCredentialID") -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ApplicationCredentialID }} +application_credential_secret = {{ .ApplicationCredentialSecret }} +{{- else -}} +auth_type = password +username = {{ .ServiceUser }} password = {{ .ServicePassword }} +project_domain_name = Default +user_domain_name = Default +project_name = service +{{- end }} {{ if (index . "MemcachedServers") }} memcached_servers = {{ .MemcachedServers }} memcache_pool_dead_retry = 10 @@ -55,15 +64,19 @@ memcache_tls_keyfile = {{ .MemcachedAuthKey }} memcache_tls_cafile = {{ .MemcachedAuthCa }} memcache_tls_enabled = true {{end}} -project_domain_name=Default -user_domain_name=Default -project_name=service {{ if (index . "Region") -}} region_name = {{ .Region }} {{ end -}} [service_user] +{{ if (index . "ApplicationCredentialID") -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ApplicationCredentialID }} +application_credential_secret = {{ .ApplicationCredentialSecret }} +{{ else -}} +auth_type = password password = {{ .ServicePassword }} +{{- end }} [oslo_messaging_notifications] {{ if (index . "TransportURL") -}} @@ -97,11 +110,17 @@ filesystem_store_datadir = /var/lib/glance/os_glance_tasks_store/ [oslo_limit] auth_url={{ .KeystoneInternalURL }} +{{ if (index . "ApplicationCredentialID") -}} +auth_type = v3applicationcredential +application_credential_id = {{ .ApplicationCredentialID }} +application_credential_secret = {{ .ApplicationCredentialSecret }} +{{ else -}} auth_type = password username={{ .ServiceUser }} password = {{ .ServicePassword }} system_scope = all user_domain_id = default +{{- end }} {{ if (index . "EndpointID") -}} endpoint_id = {{ .EndpointID }} {{ end -}} diff --git a/test/functional/base_test.go b/test/functional/base_test.go index 873ccc9f9..9acbadf8a 100644 --- a/test/functional/base_test.go +++ b/test/functional/base_test.go @@ -26,6 +26,8 @@ import ( . "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/internal/glance" + keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -446,3 +448,52 @@ func CreateGlanceMessageBusSecret(namespace string, name string) *corev1.Secret logger.Info("Secret created", "name", name) return s } + +// GetGlanceAPISpecWithAC returns a GlanceAPI spec with Application Credential configured +func GetGlanceAPISpecWithAC(apiType APIType, acSecretName string) map[string]interface{} { + spec := CreateGlanceAPISpec(apiType) + spec["secret"] = ACTestServicePasswordSecret + spec["auth"] = map[string]interface{}{ + "applicationCredentialSecret": acSecretName, + } + return spec +} + +// GetDefaultGlanceAC returns a default KeystoneApplicationCredential spec for testing +func GetDefaultGlanceAC(namespace string, acName string) *keystonev1.KeystoneApplicationCredential { + return &keystonev1.KeystoneApplicationCredential{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: acName, + }, + Spec: keystonev1.KeystoneApplicationCredentialSpec{ + UserName: glance.ServiceName, + Secret: ACTestServicePasswordSecret, + PasswordSelector: ACTestPasswordSelector, + Roles: []string{"admin", "member"}, + AccessRules: []keystonev1.ACRule{{Service: "identity", Method: "POST", Path: "/auth/tokens"}}, + ExpirationDays: 30, + GracePeriodDays: 5, + }, + } +} + +// CreateACSecret creates an Application Credential secret for testing +func CreateACSecret(namespace string, secretName string) *corev1.Secret { + return th.CreateSecret( + types.NamespacedName{Namespace: namespace, Name: secretName}, + map[string][]byte{ + keystonev1.ACIDSecretKey: []byte("test-ac-id"), + keystonev1.ACSecretSecretKey: []byte("test-ac-secret"), + }, + ) +} + +// GetKeystoneAC fetches a KeystoneApplicationCredential by name +func GetKeystoneAC(name types.NamespacedName) *keystonev1.KeystoneApplicationCredential { + instance := &keystonev1.KeystoneApplicationCredential{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, name, instance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + return instance +} diff --git a/test/functional/glance_test_data.go b/test/functional/glance_test_data.go index 2bf501bf3..3b1beb8a1 100644 --- a/test/functional/glance_test_data.go +++ b/test/functional/glance_test_data.go @@ -59,6 +59,10 @@ const ( GlanceCephExtraMountsPath = "/etc/ceph" // GlanceCephExtraMountsSecretName - GlanceCephExtraMountsSecretName = "ceph" + // ACTestServicePasswordSecret - secret name for AC test service password + ACTestServicePasswordSecret = "ac-test-osp-secret" // #nosec G101 + // ACTestPasswordSelector - password selector for AC test + ACTestPasswordSelector = "GlancePassword" ) // GlanceTestData is the data structure used to provide input data to envTest diff --git a/test/functional/glanceapi_controller_test.go b/test/functional/glanceapi_controller_test.go index 856f97330..c73df9112 100644 --- a/test/functional/glanceapi_controller_test.go +++ b/test/functional/glanceapi_controller_test.go @@ -26,6 +26,7 @@ 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/internal/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" @@ -1516,4 +1517,160 @@ var _ = Describe("Glanceapi controller", func() { }, timeout, interval).Should(Succeed()) }) }) + + When("An ApplicationCredential is created for Glance", func() { + var ( + acName string + acSecretName string + ) + BeforeEach(func() { + DeferCleanup(k8sClient.Delete, ctx, CreateGlanceSecret(glanceTest.Instance.Namespace, ACTestServicePasswordSecret)) + DeferCleanup(k8sClient.Delete, ctx, CreateGlanceMessageBusSecret(glanceTest.Instance.Namespace, glanceTest.RabbitmqSecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(glanceTest.Instance.Namespace, MemcachedInstance, memcachedv1.MemcachedSpec{})) + infra.SimulateMemcachedReady(glanceTest.GlanceMemcached) + + 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)) + mariadb.SimulateMariaDBAccountCompleted(glanceTest.GlanceDatabaseAccount) + mariadb.SimulateMariaDBDatabaseCompleted(glanceTest.GlanceDatabaseName) + + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(glanceTest.Instance.Namespace)) + + acName = fmt.Sprintf("ac-%s", glance.ServiceName) + acSecretName = acName + "-secret" + DeferCleanup(k8sClient.Delete, ctx, CreateACSecret(glanceTest.Instance.Namespace, acSecretName)) + + // Create AC CR using helper + ac := GetDefaultGlanceAC(glanceTest.Instance.Namespace, acName) + 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 standalone GlanceAPI CR with AC secret using helper + spec := GetGlanceAPISpecWithAC(GlanceAPITypeInternal, acSecretName) + DeferCleanup(th.DeleteInstance, CreateGlanceAPI(glanceTest.GlanceInternal, spec)) + + th.SimulateStatefulSetReplicaReady(glanceTest.GlanceInternalStatefulSet) + keystone.SimulateKeystoneEndpointReady(glanceTest.GlanceInternal) + }) + + It("should render ApplicationCredential auth in 00-config.conf", func() { + // 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"]) + + // AC auth is configured + g.Expect(conf).To(ContainSubstring("auth_type = v3applicationcredential")) + g.Expect(conf).To(ContainSubstring("application_credential_id = test-ac-id")) + g.Expect(conf).To(ContainSubstring("application_credential_secret = test-ac-secret")) + + // Password auth fields should not be present + g.Expect(conf).NotTo(ContainSubstring("auth_type = password")) + g.Expect(conf).NotTo(ContainSubstring("username = glance")) + g.Expect(conf).NotTo(ContainSubstring("project_name = service")) + }, timeout, interval).Should(Succeed()) + }) + + It("should update config when AC secret is updated", func() { + keystone.SimulateKeystoneEndpointReady(glanceTest.GlanceInternal) + + // Wait for initial AC config + 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")) + }, timeout, interval).Should(Succeed()) + + // Update the AC secret with new values + secret := th.GetSecret(types.NamespacedName{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }) + secret.Data[keystonev1.ACIDSecretKey] = []byte("updated-ac-id") + secret.Data[keystonev1.ACSecretSecretKey] = []byte("updated-ac-secret") + Expect(k8sClient.Update(ctx, &secret)).Should(Succeed()) + + // Wait for GlanceAPI ServiceConfig update + Eventually(func(_ Gomega) { + th.ExpectCondition( + glanceTest.GlanceInternal, + ConditionGetterFunc(GlanceAPIConditionGetter), + condition.InputReadyCondition, + corev1.ConditionTrue, + ) + }, timeout, interval).Should(Succeed()) + + // Verify config is updated with new values + 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 = updated-ac-id")) + g.Expect(conf).To(ContainSubstring("application_credential_secret = updated-ac-secret")) + }, timeout, interval).Should(Succeed()) + }) + + It("should fallback to password auth when AC is removed", func() { + // Wait for initial AC config + 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("auth_type = v3applicationcredential")) + }, timeout, interval).Should(Succeed()) + + // Remove AC secret reference from GlanceAPI CR + Eventually(func(g Gomega) { + glanceAPIInstance := GetGlanceAPI(glanceTest.GlanceInternal) + glanceAPIInstance.Spec.Auth.ApplicationCredentialSecret = "" + g.Expect(k8sClient.Update(ctx, glanceAPIInstance)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + + // Wait for GlanceAPI ServiceConfig update + Eventually(func(_ Gomega) { + th.ExpectCondition( + glanceTest.GlanceInternal, + ConditionGetterFunc(GlanceAPIConditionGetter), + condition.InputReadyCondition, + corev1.ConditionTrue, + ) + }, timeout, interval).Should(Succeed()) + + // Verify config falls back to password 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("auth_type = password")) + g.Expect(conf).To(ContainSubstring("username = glance")) + }, timeout, interval).Should(Succeed()) + }) + }) })