Skip to content

Commit 4e9ab56

Browse files
committed
Add application credential finalizer management
Heat now tracks which AC secret it is consuming via Status.ApplicationCredentialSecret and manages the openstack.org/heat-ac-consumer finalizer on that secret. This ensures keystone-operator does not prematurely revoke the application credential while Heat is still using it. On rotation (when the spec reference changes), the finalizer is moved from the old secret to the new one. On Heat CR deletion, the finalizer is cleaned up from all referenced secrets. Signed-off-by: Veronika Fisarova <vfisarov@redhat.com>
1 parent 32d27fe commit 4e9ab56

8 files changed

Lines changed: 212 additions & 3 deletions

File tree

api/bases/heat.openstack.org_heats.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,6 +2093,13 @@ spec:
20932093
status:
20942094
description: HeatStatus defines the observed state of Heat
20952095
properties:
2096+
applicationCredentialSecret:
2097+
description: |-
2098+
ApplicationCredentialSecret - the AC secret Heat is currently
2099+
consuming and protecting with the openstack.org/heat-ac-consumer
2100+
finalizer. Tracked so the controller can remove its finalizer from the
2101+
old secret when the openstack-operator rotates the reference.
2102+
type: string
20962103
conditions:
20972104
description: Conditions
20982105
items:

api/v1beta1/heat_types.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const (
4242
HeatDatabaseMigrationAnnotation = "heat.openstack.org/database-migration"
4343
)
4444

45-
4645
// HeatSpec defines the desired state of Heat
4746
type HeatSpec struct {
4847
HeatSpecBase `json:",inline"`
@@ -177,6 +176,12 @@ type HeatStatus struct {
177176
// ReadyCount of Heat Engine instance
178177
HeatEngineReadyCount int32 `json:"heatEngineReadyCount,omitempty"`
179178

179+
// ApplicationCredentialSecret - the AC secret Heat is currently
180+
// consuming and protecting with the openstack.org/heat-ac-consumer
181+
// finalizer. Tracked so the controller can remove its finalizer from the
182+
// old secret when the openstack-operator rotates the reference.
183+
ApplicationCredentialSecret string `json:"applicationCredentialSecret,omitempty"`
184+
180185
// ObservedGeneration - the most recent generation observed for this
181186
// service. If the observed generation is less than the spec generation,
182187
// then the controller has not processed the latest changes injected by

config/crd/bases/heat.openstack.org_heats.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,6 +2093,13 @@ spec:
20932093
status:
20942094
description: HeatStatus defines the observed state of Heat
20952095
properties:
2096+
applicationCredentialSecret:
2097+
description: |-
2098+
ApplicationCredentialSecret - the AC secret Heat is currently
2099+
consuming and protecting with the openstack.org/heat-ac-consumer
2100+
finalizer. Tracked so the controller can remove its finalizer from the
2101+
old secret when the openstack-operator rotates the reference.
2102+
type: string
20962103
conditions:
20972104
description: Conditions
20982105
items:

go.mod

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

143143
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
144+
145+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20260424093804-00a0ccdc9d20

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-20260424093804-00a0ccdc9d20 h1:iyxfh2SDvQrOrsHItYAE3A3+8Ku9UnzWAq9jnLJDLjg=
2+
github.com/Deydra71/keystone-operator/api v0.0.0-20260424093804-00a0ccdc9d20/go.mod h1:SpO4CL7c5/1HG+61fP6kWhL2+3aqR+5SNatdZueKrz8=
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/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
@@ -120,8 +122,6 @@ github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyU
120122
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo=
121123
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260416122644-5476763a36b6 h1:117Gu9HCSu2tAp579WnCJ9QtnslH2qnPB8UFvn8ZpqE=
122124
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260416122644-5476763a36b6/go.mod h1:i7l8cihvFktd/LSuyvL2z6OcwauarQGoVhDMePL4VyI=
123-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260422175310-d957b8482944 h1:C0qDfnVa//1NwYyO6o5EK5RBKohYjldnmbGvj7RTQ2E=
124-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260422175310-d957b8482944/go.mod h1:SpO4CL7c5/1HG+61fP6kWhL2+3aqR+5SNatdZueKrz8=
125125
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk=
126126
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8=
127127
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981 h1:jN3Kvt+RYUTaL9EXeeeIqRXVjqeNF74SuLTDXmi4X2Y=

internal/controller/heat_controller.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,19 @@ func (r *HeatReconciler) reconcileDelete(ctx context.Context, instance *heatv1be
466466
}
467467
}
468468

469+
// Remove consumer finalizer from AC secrets Heat was consuming.
470+
// Check both status and spec to handle the edge case where the reconciler
471+
// crashed after adding the finalizer but before updating the status.
472+
for _, secretName := range []string{
473+
instance.Status.ApplicationCredentialSecret,
474+
instance.Spec.Auth.ApplicationCredentialSecret,
475+
} {
476+
if err := keystonev1.RemoveACSecretConsumerFinalizer(ctx, helper, instance.Namespace,
477+
secretName, heat.ACConsumerFinalizer); err != nil {
478+
return ctrl.Result{}, err
479+
}
480+
}
481+
469482
// Service is deleted so remove the finalizer.
470483
controllerutil.RemoveFinalizer(instance, helper.GetFinalizer())
471484
Log.Info("Reconciled Heat delete successfully")
@@ -813,6 +826,23 @@ func (r *HeatReconciler) reconcileNormal(ctx context.Context, instance *heatv1be
813826

814827
// Create Secrets - end
815828

829+
// Manage consumer finalizer, the AC data was already read and rendered to the service config secret
830+
if instance.Spec.Auth.ApplicationCredentialSecret != "" || instance.Status.ApplicationCredentialSecret != "" {
831+
if err := keystonev1.ManageACSecretFinalizer(ctx, helper, instance.Namespace,
832+
instance.Spec.Auth.ApplicationCredentialSecret,
833+
instance.Status.ApplicationCredentialSecret,
834+
heat.ACConsumerFinalizer); err != nil {
835+
instance.Status.Conditions.Set(condition.FalseCondition(
836+
condition.ServiceConfigReadyCondition,
837+
condition.ErrorReason,
838+
condition.SeverityWarning,
839+
condition.ServiceConfigReadyErrorMessage,
840+
err.Error()))
841+
return ctrl.Result{}, err
842+
}
843+
}
844+
instance.Status.ApplicationCredentialSecret = instance.Spec.Auth.ApplicationCredentialSecret
845+
816846
instance.Status.Conditions.MarkTrue(condition.ServiceConfigReadyCondition, condition.ServiceConfigReadyMessage)
817847

818848
//

internal/heat/const.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ const (
9292

9393
// HeatManage is the base command used for DBPurge
9494
HeatManage = "/usr/bin/heat-manage"
95+
96+
// ACConsumerFinalizer is added to AC secrets that heat is actively consuming
97+
ACConsumerFinalizer = "openstack.org/heat-ac-consumer"
9598
)
9699

97100
// DbsyncPropagation keeps track of the DBSync Service Propagation Type

test/functional/heat_controller_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,4 +1316,159 @@ var _ = Describe("Heat controller", func() {
13161316
}, timeout, interval).Should(Succeed())
13171317
})
13181318
})
1319+
1320+
When("ApplicationCredential consumer finalizer is managed", func() {
1321+
var (
1322+
acSecretName string
1323+
servicePasswordSecret string
1324+
)
1325+
1326+
BeforeEach(func() {
1327+
servicePasswordSecret = "ac-test-osp-secret" //nolint:gosec // G101
1328+
1329+
DeferCleanup(
1330+
k8sClient.Delete, ctx, CreateHeatMessageBusSecret(namespace, HeatMessageBusSecretName))
1331+
DeferCleanup(
1332+
k8sClient.Delete, ctx, CreateHeatSecret(namespace, servicePasswordSecret))
1333+
1334+
acSecretName = "ac-heat-a1b2c-secret" //nolint:gosec // G101
1335+
secret := &corev1.Secret{
1336+
ObjectMeta: metav1.ObjectMeta{
1337+
Namespace: namespace,
1338+
Name: acSecretName,
1339+
},
1340+
Data: map[string][]byte{
1341+
keystonev1.ACIDSecretKey: []byte("a1b2ctest-ac-id"),
1342+
keystonev1.ACSecretSecretKey: []byte("test-ac-secret"),
1343+
},
1344+
}
1345+
DeferCleanup(k8sClient.Delete, ctx, secret)
1346+
Expect(k8sClient.Create(ctx, secret)).To(Succeed())
1347+
1348+
spec := GetDefaultHeatSpec()
1349+
spec["secret"] = servicePasswordSecret
1350+
spec["auth"] = map[string]interface{}{
1351+
"applicationCredentialSecret": acSecretName,
1352+
}
1353+
DeferCleanup(th.DeleteInstance, CreateHeat(heatName, spec))
1354+
DeferCleanup(
1355+
mariadb.DeleteDBService,
1356+
mariadb.CreateDBService(
1357+
namespace,
1358+
GetHeat(heatName).Spec.DatabaseInstance,
1359+
corev1.ServiceSpec{
1360+
Ports: []corev1.ServicePort{{Port: 3306}},
1361+
},
1362+
),
1363+
)
1364+
DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(namespace))
1365+
DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec))
1366+
infra.SimulateMemcachedReady(memcachedName)
1367+
1368+
heatDatabase := types.NamespacedName{
1369+
Namespace: namespace,
1370+
Name: GetHeat(heatName).Spec.DatabaseAccount,
1371+
}
1372+
acc, accSecret := mariadb.CreateMariaDBAccountAndSecret(heatDatabase, mariadbv1.MariaDBAccountSpec{})
1373+
DeferCleanup(k8sClient.Delete, ctx, acc)
1374+
DeferCleanup(k8sClient.Delete, ctx, accSecret)
1375+
1376+
infra.SimulateTransportURLReady(heatTransportURLName)
1377+
mariadb.SimulateMariaDBAccountCompleted(heatDatabase)
1378+
mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: heat.DatabaseCRName})
1379+
th.SimulateJobSuccess(heatDbSyncName)
1380+
})
1381+
1382+
It("should add the consumer finalizer to the AC secret", func() {
1383+
Eventually(func(g Gomega) {
1384+
secret := th.GetSecret(types.NamespacedName{
1385+
Namespace: namespace,
1386+
Name: acSecretName,
1387+
})
1388+
g.Expect(secret.Finalizers).To(
1389+
ContainElement(heat.ACConsumerFinalizer))
1390+
}, timeout, interval).Should(Succeed())
1391+
})
1392+
1393+
It("should track the consumed AC secret in status", func() {
1394+
Eventually(func(g Gomega) {
1395+
h := GetHeat(heatName)
1396+
g.Expect(h.Status.ApplicationCredentialSecret).To(Equal(acSecretName))
1397+
}, timeout, interval).Should(Succeed())
1398+
})
1399+
1400+
It("should move the finalizer from the old to the new secret on rotation", func() {
1401+
Eventually(func(g Gomega) {
1402+
secret := th.GetSecret(types.NamespacedName{
1403+
Namespace: namespace,
1404+
Name: acSecretName,
1405+
})
1406+
g.Expect(secret.Finalizers).To(
1407+
ContainElement(heat.ACConsumerFinalizer))
1408+
}, timeout, interval).Should(Succeed())
1409+
1410+
newACSecretName := "ac-heat-x9y8z-secret" //nolint:gosec // G101
1411+
newSecret := &corev1.Secret{
1412+
ObjectMeta: metav1.ObjectMeta{
1413+
Namespace: namespace,
1414+
Name: newACSecretName,
1415+
},
1416+
Data: map[string][]byte{
1417+
keystonev1.ACIDSecretKey: []byte("x9y8zrotated-ac-id"),
1418+
keystonev1.ACSecretSecretKey: []byte("rotated-ac-secret"),
1419+
},
1420+
}
1421+
DeferCleanup(k8sClient.Delete, ctx, newSecret)
1422+
Expect(k8sClient.Create(ctx, newSecret)).To(Succeed())
1423+
1424+
Eventually(func(g Gomega) {
1425+
h := GetHeat(heatName)
1426+
h.Spec.Auth.ApplicationCredentialSecret = newACSecretName
1427+
g.Expect(k8sClient.Update(ctx, h)).Should(Succeed())
1428+
}, timeout, interval).Should(Succeed())
1429+
1430+
Eventually(func(g Gomega) {
1431+
secret := th.GetSecret(types.NamespacedName{
1432+
Namespace: namespace,
1433+
Name: newACSecretName,
1434+
})
1435+
g.Expect(secret.Finalizers).To(
1436+
ContainElement(heat.ACConsumerFinalizer))
1437+
}, timeout, interval).Should(Succeed())
1438+
1439+
Eventually(func(g Gomega) {
1440+
secret := th.GetSecret(types.NamespacedName{
1441+
Namespace: namespace,
1442+
Name: acSecretName,
1443+
})
1444+
g.Expect(secret.Finalizers).NotTo(
1445+
ContainElement(heat.ACConsumerFinalizer))
1446+
}, timeout, interval).Should(Succeed())
1447+
1448+
Eventually(func(g Gomega) {
1449+
h := GetHeat(heatName)
1450+
g.Expect(h.Status.ApplicationCredentialSecret).To(Equal(newACSecretName))
1451+
}, timeout, interval).Should(Succeed())
1452+
})
1453+
1454+
It("should remove the consumer finalizer from AC secret on CR deletion", func() {
1455+
Eventually(func(g Gomega) {
1456+
secret := th.GetSecret(types.NamespacedName{
1457+
Namespace: namespace,
1458+
Name: acSecretName,
1459+
})
1460+
g.Expect(secret.Finalizers).To(
1461+
ContainElement(heat.ACConsumerFinalizer))
1462+
}, timeout, interval).Should(Succeed())
1463+
1464+
th.DeleteInstance(GetHeat(heatName))
1465+
1466+
secret := th.GetSecret(types.NamespacedName{
1467+
Namespace: namespace,
1468+
Name: acSecretName,
1469+
})
1470+
Expect(secret.Finalizers).NotTo(
1471+
ContainElement(heat.ACConsumerFinalizer))
1472+
})
1473+
})
13191474
})

0 commit comments

Comments
 (0)