diff --git a/api/bases/glance.openstack.org_glanceapis.yaml b/api/bases/glance.openstack.org_glanceapis.yaml index fd021ae6..4a73d67c 100644 --- a/api/bases/glance.openstack.org_glanceapis.yaml +++ b/api/bases/glance.openstack.org_glanceapis.yaml @@ -1603,6 +1603,10 @@ spec: type: string description: API endpoint type: object + applicationCredentialSecret: + description: ApplicationCredentialSecret - Secret that GlanceAPI is + actively consuming (AC consumer finalizer present) + type: string conditions: description: Conditions items: diff --git a/api/go.mod b/api/go.mod index a051752f..3d03d1af 100644 --- a/api/go.mod +++ b/api/go.mod @@ -95,3 +95,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.14 //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-20260424093804-00a0ccdc9d20 diff --git a/api/v1beta1/glanceapi_types.go b/api/v1beta1/glanceapi_types.go index 0253caa6..9da2aa36 100644 --- a/api/v1beta1/glanceapi_types.go +++ b/api/v1beta1/glanceapi_types.go @@ -131,6 +131,9 @@ type GlanceAPIStatus struct { // NotificationBusSecret - Secret containing RabbitMQ transportURL for // notification purposes NotificationBusSecret string `json:"notificationBusSecret,omitempty"` + + // ApplicationCredentialSecret - Secret that GlanceAPI is actively consuming (AC consumer finalizer present) + ApplicationCredentialSecret string `json:"applicationCredentialSecret,omitempty"` } // +kubebuilder:object:root=true diff --git a/config/crd/bases/glance.openstack.org_glanceapis.yaml b/config/crd/bases/glance.openstack.org_glanceapis.yaml index fd021ae6..4a73d67c 100644 --- a/config/crd/bases/glance.openstack.org_glanceapis.yaml +++ b/config/crd/bases/glance.openstack.org_glanceapis.yaml @@ -1603,6 +1603,10 @@ spec: type: string description: API endpoint type: object + applicationCredentialSecret: + description: ApplicationCredentialSecret - Secret that GlanceAPI is + actively consuming (AC consumer finalizer present) + type: string conditions: description: Conditions items: diff --git a/go.mod b/go.mod index 247ec3b9..a7485994 100644 --- a/go.mod +++ b/go.mod @@ -144,3 +144,5 @@ replace k8s.io/component-base => k8s.io/component-base v0.31.14 //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-20260424093804-00a0ccdc9d20 diff --git a/go.sum b/go.sum index c701a5e7..4ceaa319 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Deydra71/keystone-operator/api v0.0.0-20260424093804-00a0ccdc9d20 h1:iyxfh2SDvQrOrsHItYAE3A3+8Ku9UnzWAq9jnLJDLjg= +github.com/Deydra71/keystone-operator/api v0.0.0-20260424093804-00a0ccdc9d20/go.mod h1:SpO4CL7c5/1HG+61fP6kWhL2+3aqR+5SNatdZueKrz8= 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/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= @@ -124,8 +126,6 @@ github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20260418053129- github.com/openstack-k8s-operators/horizon-operator/api v0.6.1-0.20260418053129-fb096ad89dce/go.mod h1:ZMH+2206hZgGFjEhC+hhPvU+v6haNaeh5FR1mHylfqw= github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260416122644-5476763a36b6 h1:117Gu9HCSu2tAp579WnCJ9QtnslH2qnPB8UFvn8ZpqE= github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260416122644-5476763a36b6/go.mod h1:i7l8cihvFktd/LSuyvL2z6OcwauarQGoVhDMePL4VyI= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260422175310-d957b8482944 h1:C0qDfnVa//1NwYyO6o5EK5RBKohYjldnmbGvj7RTQ2E= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260422175310-d957b8482944/go.mod h1:SpO4CL7c5/1HG+61fP6kWhL2+3aqR+5SNatdZueKrz8= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk= github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981 h1:jN3Kvt+RYUTaL9EXeeeIqRXVjqeNF74SuLTDXmi4X2Y= diff --git a/internal/controller/glanceapi_controller.go b/internal/controller/glanceapi_controller.go index efeea224..9779c2fd 100644 --- a/internal/controller/glanceapi_controller.go +++ b/internal/controller/glanceapi_controller.go @@ -496,6 +496,19 @@ func (r *GlanceAPIReconciler) reconcileDelete(ctx context.Context, instance *gla return ctrlResult, err } + // Remove consumer finalizer from AC secrets GlanceAPI was consuming. + // Check both status and spec to handle the edge case where the reconciler + // crashed after adding the finalizer but before updating the status. + for _, secretName := range []string{ + instance.Status.ApplicationCredentialSecret, + instance.Spec.Auth.ApplicationCredentialSecret, + } { + if err := keystonev1.RemoveACSecretConsumerFinalizer(ctx, helper, instance.Namespace, + secretName, glance.ACConsumerFinalizerName(instance.APIName())); err != nil { + return ctrl.Result{}, err + } + } + // Remove finalizer on the Topology CR if ctrlResult, err := topologyv1.EnsureDeletedTopologyRef( ctx, @@ -987,6 +1000,23 @@ func (r *GlanceAPIReconciler) reconcileNormal( return ctrl.Result{}, err } + // Manage AC consumer finalizer, the AC data was already read and rendered to the service config + if instance.Spec.Auth.ApplicationCredentialSecret != "" || instance.Status.ApplicationCredentialSecret != "" { + if err := keystonev1.ManageACSecretFinalizer(ctx, helper, instance.Namespace, + instance.Spec.Auth.ApplicationCredentialSecret, + instance.Status.ApplicationCredentialSecret, + glance.ACConsumerFinalizerName(instance.APIName())); err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.ServiceConfigReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.ServiceConfigReadyErrorMessage, + err.Error())) + return ctrl.Result{}, err + } + } + instance.Status.ApplicationCredentialSecret = instance.Spec.Auth.ApplicationCredentialSecret + // At this point the config is generated and the inputHash is computed // we can mark the ServiceConfigReady as True and rollout the new pods instance.Status.Conditions.MarkTrue(condition.ServiceConfigReadyCondition, condition.ServiceConfigReadyMessage) diff --git a/internal/glance/const.go b/internal/glance/const.go index c9a0dd71..6db0bd7f 100644 --- a/internal/glance/const.go +++ b/internal/glance/const.go @@ -106,6 +106,8 @@ const ( GlanceCacheCleaner = "/usr/bin/glance-cache-cleaner" // GlanceCachePruner - GlanceCachePruner = "/usr/bin/glance-cache-pruner" + // ACConsumerFinalizerPrefix is the base prefix for the per-GlanceAPI AC consumer finalizer + ACConsumerFinalizerPrefix = "openstack.org/glanceapi-" // ShortDuration - ShortDuration = time.Duration(5) * time.Second // NormalDuration - diff --git a/internal/glance/funcs.go b/internal/glance/funcs.go index 15076a59..45793e7d 100644 --- a/internal/glance/funcs.go +++ b/internal/glance/funcs.go @@ -1,11 +1,20 @@ package glance import ( + "fmt" + corev1 "k8s.io/api/core/v1" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" ) +// ACConsumerFinalizerName returns a per-GlanceAPI finalizer name so that +// multiple GlanceAPI instances (e.g. internal, external) sharing the same +// AC secret each get their own distinct finalizer on the secret. +func ACConsumerFinalizerName(apiName string) string { + return fmt.Sprintf("%s%s-ac-consumer", ACConsumerFinalizerPrefix, apiName) +} + // GetOwningGlanceName - Given a GlanceAPI (both internal and external) // object, return the parent Glance object that created it (if any) func GetOwningGlanceName(instance client.Object) string { diff --git a/test/functional/glanceapi_controller_test.go b/test/functional/glanceapi_controller_test.go index c38e2de4..e260ffd5 100644 --- a/test/functional/glanceapi_controller_test.go +++ b/test/functional/glanceapi_controller_test.go @@ -1669,4 +1669,138 @@ var _ = Describe("Glanceapi controller", func() { }, timeout, interval).Should(Succeed()) }) }) + + When("ApplicationCredential consumer finalizer is managed", func() { + var 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)) + + acSecretName = "ac-glance-a1b2c-secret" //nolint:gosec // G101 + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }, + Data: map[string][]byte{ + keystonev1.ACIDSecretKey: []byte("a1b2ctest-ac-id"), + keystonev1.ACSecretSecretKey: []byte("test-ac-secret"), + }, + } + DeferCleanup(k8sClient.Delete, ctx, secret) + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + spec := GetGlanceAPISpecWithAC(GlanceAPITypeInternal, acSecretName) + DeferCleanup(th.DeleteInstance, CreateGlanceAPI(glanceTest.GlanceInternal, spec)) + th.SimulateStatefulSetReplicaReady(glanceTest.GlanceInternalStatefulSet) + keystone.SimulateKeystoneEndpointReady(glanceTest.GlanceInternal) + }) + + It("should add the consumer finalizer to the AC secret", func() { + Eventually(func(g Gomega) { + secret := th.GetSecret(types.NamespacedName{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }) + g.Expect(secret.Finalizers).To( + ContainElement(glance.ACConsumerFinalizerName("default"))) + }, timeout, interval).Should(Succeed()) + }) + + It("should track the consumed AC secret in status", func() { + Eventually(func(g Gomega) { + api := GetGlanceAPI(glanceTest.GlanceInternal) + g.Expect(api.Status.ApplicationCredentialSecret).To(Equal(acSecretName)) + }, timeout, interval).Should(Succeed()) + }) + + It("should move the finalizer from the old to the new secret on rotation", func() { + Eventually(func(g Gomega) { + secret := th.GetSecret(types.NamespacedName{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }) + g.Expect(secret.Finalizers).To( + ContainElement(glance.ACConsumerFinalizerName("default"))) + }, timeout, interval).Should(Succeed()) + + newACSecretName := "ac-glance-x9y8z-secret" //nolint:gosec // G101 + newSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: glanceTest.Instance.Namespace, + Name: newACSecretName, + }, + Data: map[string][]byte{ + keystonev1.ACIDSecretKey: []byte("x9y8zrotated-ac-id"), + keystonev1.ACSecretSecretKey: []byte("rotated-ac-secret"), + }, + } + DeferCleanup(k8sClient.Delete, ctx, newSecret) + Expect(k8sClient.Create(ctx, newSecret)).To(Succeed()) + + Eventually(func(g Gomega) { + api := GetGlanceAPI(glanceTest.GlanceInternal) + api.Spec.Auth.ApplicationCredentialSecret = newACSecretName + g.Expect(k8sClient.Update(ctx, api)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + secret := th.GetSecret(types.NamespacedName{ + Namespace: glanceTest.Instance.Namespace, + Name: newACSecretName, + }) + g.Expect(secret.Finalizers).To( + ContainElement(glance.ACConsumerFinalizerName("default"))) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + secret := th.GetSecret(types.NamespacedName{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }) + g.Expect(secret.Finalizers).NotTo( + ContainElement(glance.ACConsumerFinalizerName("default"))) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + api := GetGlanceAPI(glanceTest.GlanceInternal) + g.Expect(api.Status.ApplicationCredentialSecret).To(Equal(newACSecretName)) + }, timeout, interval).Should(Succeed()) + }) + + It("should remove the consumer finalizer from AC secret on CR deletion", func() { + Eventually(func(g Gomega) { + secret := th.GetSecret(types.NamespacedName{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }) + g.Expect(secret.Finalizers).To( + ContainElement(glance.ACConsumerFinalizerName("default"))) + }, timeout, interval).Should(Succeed()) + + th.DeleteInstance(GetGlanceAPI(glanceTest.GlanceInternal)) + + secret := th.GetSecret(types.NamespacedName{ + Namespace: glanceTest.Instance.Namespace, + Name: acSecretName, + }) + Expect(secret.Finalizers).NotTo( + ContainElement(glance.ACConsumerFinalizerName("default"))) + }) + }) })