Skip to content

Commit b6d036f

Browse files
lmicciniclaude
andcommitted
Add transport URL secret rotation with consumer finalizer
When infra-operator rotates a RabbitMQ transport URL (creating a new secret and user), consumer operators must hold a consumer finalizer on the old secret until all their pods have rolled out with the new credentials. Without this, infra-operator cleans up the old RabbitMQ user while pods are still connected with old credentials, causing message bus outages. Design: 1. Add consumer finalizer to the current transport URL secret early in reconcile. Set instance.Status.TransportURLSecret for first-time setup only (when empty or unchanged); during rotation the status is updated solely by FinalizeSecretRotation at end of reconcile. 2. Pass transportURL.Status.SecretName directly to sub-CR creation functions and config generation as a parameter — never read from instance.Status.TransportURLSecret for sub-CR specs. 3. Use statefulset.IsReady() / deployment.IsReady() from lib-common in all sub-CR controllers for accurate rollout status. 4. Use object.ManageRotationGracePeriod() to enforce a 60-second grace period before evaluating the rotation guard. This gives sub-CRs time to detect config changes, update their workloads, and roll pods — without relying on informer cache freshness. 5. Guard: CredentialRotationGuardReady(true, conditions) — evaluates AllSubConditionIsTrue after the grace period expires. Only when all sub-CR conditions are True does FinalizeSecretRotation remove the consumer finalizer from the old secret. The same pattern applies to notification transport URL secrets and application credential secrets where applicable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dd47f94 commit b6d036f

8 files changed

Lines changed: 218 additions & 86 deletions

File tree

api/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ require (
7474
sigs.k8s.io/yaml v1.6.0 // indirect
7575
)
7676

77+
replace github.com/openstack-k8s-operators/lib-common/modules/common => github.com/lmiccini/lib-common/modules/common v0.0.0-20260623081928-d67d41c03594
78+
79+
replace github.com/openstack-k8s-operators/infra-operator/apis => github.com/lmiccini/infra-operator/apis v0.0.0-20260623100659-aca54b995462
80+
7781
// mschuppert: map to latest commit from release-4.18 tag
7882
// must consistent within modules and service operators
7983
replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e //allow-merging

api/go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
6868
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
6969
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
7070
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
71+
github.com/lmiccini/infra-operator/apis v0.0.0-20260623100659-aca54b995462 h1:CiQFCVrdzGgeEQqt3C39e6s9Vpi1784HIm+LNYm9py4=
72+
github.com/lmiccini/infra-operator/apis v0.0.0-20260623100659-aca54b995462/go.mod h1:fcTuxQ/hzNBPxCf99vbsBt7dgZ3W12gUthaCXSvkPr8=
73+
github.com/lmiccini/lib-common/modules/common v0.0.0-20260623081928-d67d41c03594 h1:rMCoyJyTQaekhmKJYsIgK/u6w2Gat0dd/9xgBiUlyxM=
74+
github.com/lmiccini/lib-common/modules/common v0.0.0-20260623081928-d67d41c03594/go.mod h1:oeIagnkOxEsxluKFcFMW80Lf1rXdV7FT2W+peB6kSE0=
7175
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
7276
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
7377
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -85,10 +89,6 @@ github.com/onsi/gomega v1.42.0 h1:CJby8u36xb7v34W78F8WKvqTQP7PCMIPB78IVDB73l4=
8589
github.com/onsi/gomega v1.42.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
8690
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U=
8791
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo=
88-
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260618172644-5a4764bdaa36 h1:nGpBRRuWJbxiH9Vv5ir0TUWmL3XFChvqvXX8We5Lvnc=
89-
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260618172644-5a4764bdaa36/go.mod h1:fcTuxQ/hzNBPxCf99vbsBt7dgZ3W12gUthaCXSvkPr8=
90-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260618132757-fe8e60d1d8a6 h1:aIc5ECO3dubv265jjUZ66oi56kf5iUt8Y1DWmCPrzOc=
91-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260618132757-fe8e60d1d8a6/go.mod h1:oeIagnkOxEsxluKFcFMW80Lf1rXdV7FT2W+peB6kSE0=
9292
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260618132757-fe8e60d1d8a6 h1:OVFoNXzinsI0rq8gbegu8TnlDPkO409iyVoWhU4nEdQ=
9393
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260618132757-fe8e60d1d8a6/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI=
9494
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260618132757-fe8e60d1d8a6 h1:3cyU3HUhCoV7vscqea6ZUbkwxNSAJd1Rwk0P15vsUZw=

api/v1beta1/keystoneapplicationcredential.go

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,8 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20-
"context"
2120
"fmt"
2221
"strings"
23-
24-
corev1 "k8s.io/api/core/v1"
25-
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
26-
"k8s.io/apimachinery/pkg/types"
27-
"sigs.k8s.io/controller-runtime/pkg/client"
28-
29-
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
30-
"github.com/openstack-k8s-operators/lib-common/modules/common/object"
3122
)
3223

3324
// ApplicationCredentialData contains AC ID/Secret extracted from a Secret
@@ -67,68 +58,3 @@ func (ac *KeystoneApplicationCredential) IsEDPMService() bool {
6758
return ac.GetAnnotations()[EDPMServiceAnnotation] != "false"
6859
}
6960

70-
// ManageACSecretFinalizer ensures consumerFinalizer is present on the AC secret
71-
// identified by newSecretName and absent from the one identified by
72-
// oldSecretName. It is a no-op when both names are equal.
73-
func ManageACSecretFinalizer(
74-
ctx context.Context,
75-
h *helper.Helper,
76-
namespace string,
77-
newSecretName string,
78-
oldSecretName string,
79-
consumerFinalizer string,
80-
) error {
81-
if newSecretName == oldSecretName {
82-
return nil
83-
}
84-
85-
var newObj, oldObj client.Object
86-
87-
if newSecretName != "" {
88-
secret := &corev1.Secret{}
89-
key := types.NamespacedName{Name: newSecretName, Namespace: namespace}
90-
if err := h.GetClient().Get(ctx, key, secret); err != nil {
91-
return fmt.Errorf("failed to get new AC secret %s: %w", newSecretName, err)
92-
}
93-
newObj = secret
94-
}
95-
96-
if oldSecretName != "" {
97-
secret := &corev1.Secret{}
98-
key := types.NamespacedName{Name: oldSecretName, Namespace: namespace}
99-
if err := h.GetClient().Get(ctx, key, secret); err != nil {
100-
if !k8s_errors.IsNotFound(err) {
101-
return fmt.Errorf("failed to get old AC secret %s: %w", oldSecretName, err)
102-
}
103-
} else {
104-
oldObj = secret
105-
}
106-
}
107-
108-
return object.ManageConsumerFinalizer(ctx, h, newObj, oldObj, consumerFinalizer)
109-
}
110-
111-
// RemoveACSecretConsumerFinalizer removes consumerFinalizer from the AC secret
112-
// identified by secretName. It is a no-op when secretName is empty or the
113-
// secret no longer exists.
114-
func RemoveACSecretConsumerFinalizer(
115-
ctx context.Context,
116-
h *helper.Helper,
117-
namespace string,
118-
secretName string,
119-
consumerFinalizer string,
120-
) error {
121-
if secretName == "" {
122-
return nil
123-
}
124-
125-
secret := &corev1.Secret{}
126-
key := types.NamespacedName{Name: secretName, Namespace: namespace}
127-
if err := h.GetClient().Get(ctx, key, secret); err != nil {
128-
if k8s_errors.IsNotFound(err) {
129-
return nil
130-
}
131-
return err
132-
}
133-
return object.RemoveConsumerFinalizer(ctx, h, secret, consumerFinalizer)
134-
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ require (
118118

119119
replace github.com/openstack-k8s-operators/keystone-operator/api => ./api
120120

121+
replace github.com/openstack-k8s-operators/lib-common/modules/common => github.com/lmiccini/lib-common/modules/common v0.0.0-20260623081928-d67d41c03594
122+
123+
replace github.com/openstack-k8s-operators/infra-operator/apis => github.com/lmiccini/infra-operator/apis v0.0.0-20260623100659-aca54b995462
124+
121125
// mschuppert: map to latest commit from release-4.18 tag
122126
// must consistent within modules and service operators
123127
replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e //allow-merging

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
100100
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
101101
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
102102
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
103+
github.com/lmiccini/infra-operator/apis v0.0.0-20260623100659-aca54b995462 h1:CiQFCVrdzGgeEQqt3C39e6s9Vpi1784HIm+LNYm9py4=
104+
github.com/lmiccini/infra-operator/apis v0.0.0-20260623100659-aca54b995462/go.mod h1:fcTuxQ/hzNBPxCf99vbsBt7dgZ3W12gUthaCXSvkPr8=
105+
github.com/lmiccini/lib-common/modules/common v0.0.0-20260623081928-d67d41c03594 h1:rMCoyJyTQaekhmKJYsIgK/u6w2Gat0dd/9xgBiUlyxM=
106+
github.com/lmiccini/lib-common/modules/common v0.0.0-20260623081928-d67d41c03594/go.mod h1:oeIagnkOxEsxluKFcFMW80Lf1rXdV7FT2W+peB6kSE0=
103107
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
104108
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
105109
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
@@ -120,10 +124,6 @@ github.com/onsi/gomega v1.42.0 h1:CJby8u36xb7v34W78F8WKvqTQP7PCMIPB78IVDB73l4=
120124
github.com/onsi/gomega v1.42.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
121125
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U=
122126
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo=
123-
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260618172644-5a4764bdaa36 h1:nGpBRRuWJbxiH9Vv5ir0TUWmL3XFChvqvXX8We5Lvnc=
124-
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260618172644-5a4764bdaa36/go.mod h1:fcTuxQ/hzNBPxCf99vbsBt7dgZ3W12gUthaCXSvkPr8=
125-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260618132757-fe8e60d1d8a6 h1:aIc5ECO3dubv265jjUZ66oi56kf5iUt8Y1DWmCPrzOc=
126-
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260618132757-fe8e60d1d8a6/go.mod h1:oeIagnkOxEsxluKFcFMW80Lf1rXdV7FT2W+peB6kSE0=
127127
github.com/openstack-k8s-operators/lib-common/modules/edpm v0.0.0-20260618132757-fe8e60d1d8a6 h1:Iq7reRH6OtQ3MCqA/Tiz6e8fqfYi+h1o5cyZv0RNv0E=
128128
github.com/openstack-k8s-operators/lib-common/modules/edpm v0.0.0-20260618132757-fe8e60d1d8a6/go.mod h1:xsKeDFU3/xEObaVDqd6XEYV3MzvFswbWMlnr2Z3q3ZI=
129129
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260618132757-fe8e60d1d8a6 h1:OVFoNXzinsI0rq8gbegu8TnlDPkO409iyVoWhU4nEdQ=

internal/controller/keystoneapi_controller.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
job "github.com/openstack-k8s-operators/lib-common/modules/common/job"
4343
labels "github.com/openstack-k8s-operators/lib-common/modules/common/labels"
4444
nad "github.com/openstack-k8s-operators/lib-common/modules/common/networkattachment"
45+
"github.com/openstack-k8s-operators/lib-common/modules/common/object"
4546
common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac"
4647
oko_secret "github.com/openstack-k8s-operators/lib-common/modules/common/secret"
4748
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
@@ -522,6 +523,11 @@ func (r *KeystoneAPIReconciler) reconcileDelete(ctx context.Context, instance *k
522523
}
523524
}
524525

526+
if err := object.RemoveSecretConsumerFinalizer(ctx, helper, instance.Namespace,
527+
instance.Status.TransportURLSecret, keystone.TransportConsumerFinalizer); err != nil {
528+
return ctrl.Result{}, err
529+
}
530+
525531
// Service is deleted so remove the finalizer.
526532
controllerutil.RemoveFinalizer(instance, helper.GetFinalizer())
527533
Log.Info("Reconciled Service delete successfully")
@@ -1069,9 +1075,9 @@ func (r *KeystoneAPIReconciler) reconcileNormal(
10691075
Log.Info(fmt.Sprintf("TransportURL %s successfully reconciled - operation: %s", transportURL.Name, string(op)))
10701076
}
10711077

1072-
instance.Status.TransportURLSecret = transportURL.Status.SecretName
1078+
currentTransportSecret := transportURL.Status.SecretName
10731079

1074-
if instance.Status.TransportURLSecret == "" {
1080+
if currentTransportSecret == "" {
10751081
// Since the TransportURL secret is automatically created by the Infra operator,
10761082
// we treat this as an info (because the user is not responsible for manually creating it).
10771083
Log.Info(fmt.Sprintf("Waiting for TransportURL %s secret to be created", transportURL.Name))
@@ -1084,6 +1090,19 @@ func (r *KeystoneAPIReconciler) reconcileNormal(
10841090
}
10851091
Log.Info(fmt.Sprintf("TransportURL secret name %s", transportURL.Status.SecretName))
10861092
instance.Status.Conditions.MarkTrue(condition.RabbitMqTransportURLReadyCondition, condition.RabbitMqTransportURLReadyMessage)
1093+
1094+
// Set status early for first-time setup so PatchInstance persists it
1095+
// even on early returns. During rotation (old != current), the status
1096+
// is only updated by FinalizeSecretRotation at end of reconcile.
1097+
if instance.Status.TransportURLSecret == "" ||
1098+
instance.Status.TransportURLSecret == currentTransportSecret {
1099+
instance.Status.TransportURLSecret = currentTransportSecret
1100+
}
1101+
1102+
if err := object.ManageSecretConsumerFinalizer(ctx, helper, instance.Namespace,
1103+
currentTransportSecret, keystone.TransportConsumerFinalizer); err != nil {
1104+
return ctrl.Result{}, err
1105+
}
10871106
// run check rabbitmq - end
10881107

10891108
//
@@ -1152,7 +1171,7 @@ func (r *KeystoneAPIReconciler) reconcileNormal(
11521171
// - %-config configmap holding minimal keystone config required to get the service up, user can add additional files to be added to the service
11531172
// - parameters which has passwords gets added from the OpenStack secret via the init container
11541173
//
1155-
err = r.generateServiceConfigMaps(ctx, instance, helper, &configMapVars, memcached, db)
1174+
err = r.generateServiceConfigMaps(ctx, instance, helper, &configMapVars, memcached, db, currentTransportSecret)
11561175
if err != nil {
11571176
instance.Status.Conditions.Set(condition.FalseCondition(
11581177
condition.ServiceConfigReadyCondition,
@@ -1468,6 +1487,30 @@ func (r *KeystoneAPIReconciler) reconcileNormal(
14681487
return ctrl.Result{}, err
14691488
}
14701489

1490+
rotationPending := instance.Status.TransportURLSecret != "" &&
1491+
instance.Status.TransportURLSecret != currentTransportSecret
1492+
result, graceActive, err := object.ManageRotationGracePeriod(
1493+
ctx, r.Client, instance, rotationPending, 60*time.Second)
1494+
if err != nil {
1495+
return ctrl.Result{}, err
1496+
}
1497+
if graceActive {
1498+
return result, nil
1499+
}
1500+
1501+
guardReady := condition.CredentialRotationGuardReady(true, &instance.Status.Conditions)
1502+
transportSecretName, err := object.FinalizeSecretRotation(
1503+
ctx, helper, instance.Namespace,
1504+
instance.Status.TransportURLSecret,
1505+
currentTransportSecret,
1506+
keystone.TransportConsumerFinalizer,
1507+
guardReady,
1508+
)
1509+
if err != nil {
1510+
return ctrl.Result{}, err
1511+
}
1512+
instance.Status.TransportURLSecret = transportSecretName
1513+
14711514
Log.Info("Reconciled Service successfully")
14721515
return ctrl.Result{}, nil
14731516
}
@@ -1520,6 +1563,7 @@ func (r *KeystoneAPIReconciler) generateServiceConfigMaps(
15201563
envVars *map[string]env.Setter,
15211564
mc *memcachedv1.Memcached,
15221565
db *mariadbv1.Database,
1566+
transportURLSecretName string,
15231567
) error {
15241568
//
15251569
// create Configmap/Secret required for keystone input
@@ -1545,7 +1589,7 @@ func (r *KeystoneAPIReconciler) generateServiceConfigMaps(
15451589
}
15461590
maps.Copy(customData, instance.Spec.DefaultConfigOverwrite)
15471591

1548-
transportURLSecret, _, err := oko_secret.GetSecret(ctx, h, instance.Status.TransportURLSecret, instance.Namespace)
1592+
transportURLSecret, _, err := oko_secret.GetSecret(ctx, h, transportURLSecretName, instance.Namespace)
15491593
if err != nil {
15501594
return err
15511595
}

internal/keystone/const.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const (
5353
FederationMultiRealmSecret = "keystone-multirealm-federation-secret"
5454
// FederationDefaultMountPath - if user doesn't specify otherwise, this location is used
5555
FederationDefaultMountPath = "/var/lib/config-data/default/multirealm-federation"
56+
// TransportConsumerFinalizer is the finalizer added to transport URL secrets
57+
TransportConsumerFinalizer = "openstack.org/keystone-transport-consumer"
5658
)
5759

5860
// KeystonePropagation is the definition of the Keystone propagation service

0 commit comments

Comments
 (0)