Skip to content

Commit 32c942f

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 88f3a3e commit 32c942f

8 files changed

Lines changed: 217 additions & 10 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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ require (
1111
github.com/onsi/gomega v1.41.0
1212
github.com/openstack-k8s-operators/heat-operator/api v0.3.1-0.20240214134649-6643d1b09d49
1313
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260518151731-513cdc50e41c
14-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260518173352-69a47a6187d5
14+
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260520090027-4d7b7a01c0bf
1515
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260518125357-72bdd580c587
16-
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260518125357-72bdd580c587
16+
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260506154724-30a976ba8ef0
1717
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260518125357-72bdd580c587
1818
github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260518125357-72bdd580c587
19-
github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260518062040-f70bb2ebbdc2
19+
github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260421135251-4fb605db7d18
2020
k8s.io/api v0.31.14
2121
k8s.io/apimachinery v0.31.14
2222
k8s.io/client-go v0.31.14

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,18 @@ github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyU
120120
github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo=
121121
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260518151731-513cdc50e41c h1:RIe9ZvPyK07eoDR+IWC0eTojEsVYgDP9AdOYT1r6KpE=
122122
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260518151731-513cdc50e41c/go.mod h1:RFFB4Zs9IJv1jXs/yMjo+VswSW+rsrFZsoP0QrB1EbI=
123-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260518173352-69a47a6187d5 h1:eKSWFldHZyv3Q6Q8xO6IfvlKUxcQ1GstOPCa8HnlWEc=
124-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260518173352-69a47a6187d5/go.mod h1:voVyXEWocD4O+I+bIXLZovkzL51RE17deynYYgKbs0w=
123+
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260520090027-4d7b7a01c0bf h1:FoKK0zNo48i4ZMFxScupCK/YAmy6Ps4IILz3CK4BCTI=
124+
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260520090027-4d7b7a01c0bf/go.mod h1:VNX1Mda2u5+yGxycIyVrgABucitMDR9ct3Lj6ROS92I=
125125
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260518125357-72bdd580c587 h1:p03uEXoSreyu7LpFmb9YyYM8tEx2D2+7qqhLXNWHTq0=
126126
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260518125357-72bdd580c587/go.mod h1:JC04T5G4E/he5ukonV1oCqa0QzFkLv761VbLruVghJM=
127-
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260518125357-72bdd580c587 h1:sg+VXXWM8amYFpynStptXbE3fNFwzYef4vVniMMzmAE=
128-
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260518125357-72bdd580c587/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI=
127+
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260506154724-30a976ba8ef0 h1:kMie+G0aHlGwDHjimjj8AUxTl2R7LGfai/8pev2T+TY=
128+
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260506154724-30a976ba8ef0/go.mod h1:7yqbVpg0k0vW+kZks+TMU/cd1ovoejyHfVPWcyGYLHI=
129129
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260518125357-72bdd580c587 h1:GeTQP0NzJOL3Gec6t0ZcngTuftFA6r9uxzInfccOhxA=
130130
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20260518125357-72bdd580c587/go.mod h1:4UPRrHSuvNj1Ep10VWb7GK+jELc31n94qhBwkTwxrA4=
131131
github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260518125357-72bdd580c587 h1:jpouKcgs2Kc5z2JHIpvsXMxEonfXLgzX3KswuBoeKQ0=
132132
github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260518125357-72bdd580c587/go.mod h1:nLS2oK4pBo756JNN1cPgr44S0X9V11QScgVla89Ojok=
133-
github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260518062040-f70bb2ebbdc2 h1:9hNruYsDk3H6xNSyC6+F6F9YGeemyCzAWvwJoRO3KdU=
134-
github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260518062040-f70bb2ebbdc2/go.mod h1:/9N3XCC/QR2CowKaVmz1R5BP9NVJxL6RAb6aplOYGe8=
133+
github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260421135251-4fb605db7d18 h1:fMMY6pp7+PEkcsdDoodTfJ6ct2RyGI8VB+toVJxxB5w=
134+
github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260421135251-4fb605db7d18/go.mod h1:g/xgMnzNHxdTkqnEgAKwVOv75uPN4nuApbkGqSvASvs=
135135
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
136136
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
137137
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

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)