Skip to content

Commit f059a34

Browse files
committed
Add application credential finalizer management
Signed-off-by: Veronika Fisarova <vfisarov@redhat.com>
1 parent 4d37539 commit f059a34

8 files changed

Lines changed: 310 additions & 2 deletions

File tree

api/bases/placement.openstack.org_placementapis.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,13 @@ spec:
412412
status:
413413
description: PlacementAPIStatus defines the observed state of PlacementAPI
414414
properties:
415+
applicationCredentialSecret:
416+
description: |-
417+
ApplicationCredentialSecret - the AC secret placement is currently
418+
consuming and protecting with the openstack.org/placementapi-ac-consumer
419+
finalizer. Tracked so the controller can remove its finalizer from the
420+
old secret when the openstack-operator rotates the reference.
421+
type: string
415422
conditions:
416423
description: Conditions
417424
items:

api/placement/v1beta1/api_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ type PlacementAPIStatus struct {
182182

183183
// LastAppliedTopology - the last applied Topology
184184
LastAppliedTopology *topologyv1.TopoRef `json:"lastAppliedTopology,omitempty"`
185+
186+
// ApplicationCredentialSecret - the AC secret placement is currently
187+
// consuming and protecting with the openstack.org/placementapi-ac-consumer
188+
// finalizer. Tracked so the controller can remove its finalizer from the
189+
// old secret when the openstack-operator rotates the reference.
190+
ApplicationCredentialSecret string `json:"applicationCredentialSecret,omitempty"`
185191
}
186192

187193
// PlacementAPI is the Schema for the placementapis API

config/crd/bases/placement.openstack.org_placementapis.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,13 @@ spec:
412412
status:
413413
description: PlacementAPIStatus defines the observed state of PlacementAPI
414414
properties:
415+
applicationCredentialSecret:
416+
description: |-
417+
ApplicationCredentialSecret - the AC secret placement is currently
418+
consuming and protecting with the openstack.org/placementapi-ac-consumer
419+
finalizer. Tracked so the controller can remove its finalizer from the
420+
old secret when the openstack-operator rotates the reference.
421+
type: string
415422
conditions:
416423
description: Conditions
417424
items:

go.mod

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

145145
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e //allow-merging
146+
147+
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.20260420052838-77f94aef5af2 h1:h7pTz90cHqX6nTYjYDphuitIfD4UpM9yGnI3AbLdHrY=
124-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260420052838-77f94aef5af2/go.mod h1:SpO4CL7c5/1HG+61fP6kWhL2+3aqR+5SNatdZueKrz8=
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=
127127
github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20260417092244-81c71b39e981 h1:jN3Kvt+RYUTaL9EXeeeIqRXVjqeNF74SuLTDXmi4X2Y=

internal/controller/placement/api_controller.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,22 @@ func (r *PlacementAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request
456456
return ctrl.Result{}, nil
457457
}
458458

459+
if instance.Spec.Auth.ApplicationCredentialSecret != "" || instance.Status.ApplicationCredentialSecret != "" {
460+
if err := keystonev1.ManageACSecretFinalizer(ctx, h, instance.Namespace,
461+
instance.Spec.Auth.ApplicationCredentialSecret,
462+
instance.Status.ApplicationCredentialSecret,
463+
placement.ACConsumerFinalizer); err != nil {
464+
instance.Status.Conditions.Set(condition.FalseCondition(
465+
condition.ServiceConfigReadyCondition,
466+
condition.ErrorReason,
467+
condition.SeverityWarning,
468+
condition.ServiceConfigReadyErrorMessage,
469+
err.Error()))
470+
return ctrl.Result{}, err
471+
}
472+
}
473+
instance.Status.ApplicationCredentialSecret = instance.Spec.Auth.ApplicationCredentialSecret
474+
459475
instance.Status.Conditions.MarkTrue(condition.ServiceConfigReadyCondition, condition.ServiceConfigReadyMessage)
460476

461477
serviceAnnotations, result, err := r.ensureNetworkAttachments(ctx, h, instance)
@@ -1095,6 +1111,17 @@ func (r *PlacementAPIReconciler) reconcileDelete(ctx context.Context, instance *
10951111
}
10961112
}
10971113

1114+
// Remove consumer finalizer from AC secrets placement was consuming.
1115+
for _, secretName := range []string{
1116+
instance.Status.ApplicationCredentialSecret,
1117+
instance.Spec.Auth.ApplicationCredentialSecret,
1118+
} {
1119+
if err := keystonev1.RemoveACSecretConsumerFinalizer(ctx, helper, instance.Namespace,
1120+
secretName, placement.ACConsumerFinalizer); err != nil {
1121+
return ctrl.Result{}, err
1122+
}
1123+
}
1124+
10981125
// We did all the cleanup on the objects we created so we can remove the
10991126
// finalizer from ourselves to allow the deletion
11001127
controllerutil.RemoveFinalizer(instance, helper.GetFinalizer())

internal/placement/const.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ const (
3636
// PlacementUserID is the linux user ID used by Kolla for the placement
3737
// user in the service containers
3838
PlacementUserID int64 = 42482
39+
40+
// ACConsumerFinalizer is added to AC secrets that placement is actively consuming
41+
ACConsumerFinalizer = "openstack.org/placementapi-ac-consumer"
3942
)

test/functional/placement/api_controller_test.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1"
3333
"github.com/openstack-k8s-operators/nova-operator/internal/placement"
3434
corev1 "k8s.io/api/core/v1"
35+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3536
"k8s.io/apimachinery/pkg/types"
3637
)
3738

@@ -1526,4 +1527,259 @@ var _ = Describe("PlacementAPI reconfiguration", func() {
15261527
})
15271528
})
15281529

1530+
When("ApplicationCredential consumer finalizer is managed", func() {
1531+
var acSecretName string
1532+
1533+
BeforeEach(func() {
1534+
acSecretName = "ac-placement-a1b2c-secret" //nolint:gosec // G101
1535+
secret := &corev1.Secret{
1536+
ObjectMeta: metav1.ObjectMeta{
1537+
Namespace: names.Namespace,
1538+
Name: acSecretName,
1539+
},
1540+
Data: map[string][]byte{
1541+
keystonev1.ACIDSecretKey: []byte("a1b2ctest-ac-id"),
1542+
keystonev1.ACSecretSecretKey: []byte("test-ac-secret"),
1543+
},
1544+
}
1545+
DeferCleanup(k8sClient.Delete, ctx, secret)
1546+
Expect(k8sClient.Create(ctx, secret)).To(Succeed())
1547+
1548+
spec := GetDefaultPlacementAPISpec()
1549+
spec["auth"] = map[string]any{
1550+
"applicationCredentialSecret": acSecretName,
1551+
}
1552+
1553+
DeferCleanup(th.DeleteInstance, CreatePlacementAPI(names.PlacementAPIName, spec))
1554+
DeferCleanup(
1555+
k8sClient.Delete, ctx, CreatePlacementAPISecret(names.Namespace, SecretName))
1556+
keystoneAPIName := keystone.CreateKeystoneAPI(names.Namespace)
1557+
DeferCleanup(keystone.DeleteKeystoneAPI, keystoneAPIName)
1558+
DeferCleanup(
1559+
mariadb.DeleteDBService,
1560+
mariadb.CreateDBService(
1561+
names.Namespace,
1562+
GetDefaultPlacementAPISpec()["databaseInstance"].(string),
1563+
corev1.ServiceSpec{
1564+
Ports: []corev1.ServicePort{{Port: 3306}},
1565+
},
1566+
),
1567+
)
1568+
mariadb.SimulateMariaDBDatabaseCompleted(names.MariaDBDatabaseName)
1569+
mariadb.SimulateMariaDBAccountCompleted(names.MariaDBAccount)
1570+
})
1571+
1572+
It("should add the consumer finalizer to the AC secret", func() {
1573+
Eventually(func(g Gomega) {
1574+
secret := th.GetSecret(types.NamespacedName{
1575+
Namespace: names.Namespace,
1576+
Name: acSecretName,
1577+
})
1578+
g.Expect(secret.Finalizers).To(
1579+
ContainElement(placement.ACConsumerFinalizer))
1580+
}, timeout, interval).Should(Succeed())
1581+
})
1582+
1583+
It("should track the consumed AC secret in status", func() {
1584+
Eventually(func(g Gomega) {
1585+
p := GetPlacementAPI(names.PlacementAPIName)
1586+
g.Expect(p.Status.ApplicationCredentialSecret).To(Equal(acSecretName))
1587+
}, timeout, interval).Should(Succeed())
1588+
})
1589+
1590+
It("should move the finalizer from the old to the new secret on rotation", func() {
1591+
Eventually(func(g Gomega) {
1592+
secret := th.GetSecret(types.NamespacedName{
1593+
Namespace: names.Namespace,
1594+
Name: acSecretName,
1595+
})
1596+
g.Expect(secret.Finalizers).To(
1597+
ContainElement(placement.ACConsumerFinalizer))
1598+
}, timeout, interval).Should(Succeed())
1599+
1600+
newACSecretName := "ac-placement-x9y8z-secret" //nolint:gosec // G101
1601+
newSecret := &corev1.Secret{
1602+
ObjectMeta: metav1.ObjectMeta{
1603+
Namespace: names.Namespace,
1604+
Name: newACSecretName,
1605+
},
1606+
Data: map[string][]byte{
1607+
keystonev1.ACIDSecretKey: []byte("x9y8zrotated-ac-id"),
1608+
keystonev1.ACSecretSecretKey: []byte("rotated-ac-secret"),
1609+
},
1610+
}
1611+
DeferCleanup(k8sClient.Delete, ctx, newSecret)
1612+
Expect(k8sClient.Create(ctx, newSecret)).To(Succeed())
1613+
1614+
Eventually(func(g Gomega) {
1615+
p := GetPlacementAPI(names.PlacementAPIName)
1616+
p.Spec.Auth.ApplicationCredentialSecret = newACSecretName
1617+
g.Expect(k8sClient.Update(ctx, p)).Should(Succeed())
1618+
}, timeout, interval).Should(Succeed())
1619+
1620+
Eventually(func(g Gomega) {
1621+
secret := th.GetSecret(types.NamespacedName{
1622+
Namespace: names.Namespace,
1623+
Name: newACSecretName,
1624+
})
1625+
g.Expect(secret.Finalizers).To(
1626+
ContainElement(placement.ACConsumerFinalizer))
1627+
}, timeout, interval).Should(Succeed())
1628+
1629+
Eventually(func(g Gomega) {
1630+
secret := th.GetSecret(types.NamespacedName{
1631+
Namespace: names.Namespace,
1632+
Name: acSecretName,
1633+
})
1634+
g.Expect(secret.Finalizers).NotTo(
1635+
ContainElement(placement.ACConsumerFinalizer))
1636+
}, timeout, interval).Should(Succeed())
1637+
1638+
Eventually(func(g Gomega) {
1639+
p := GetPlacementAPI(names.PlacementAPIName)
1640+
g.Expect(p.Status.ApplicationCredentialSecret).To(Equal(newACSecretName))
1641+
}, timeout, interval).Should(Succeed())
1642+
})
1643+
1644+
It("should remove the consumer finalizer from AC secret on CR deletion", func() {
1645+
Eventually(func(g Gomega) {
1646+
secret := th.GetSecret(types.NamespacedName{
1647+
Namespace: names.Namespace,
1648+
Name: acSecretName,
1649+
})
1650+
g.Expect(secret.Finalizers).To(
1651+
ContainElement(placement.ACConsumerFinalizer))
1652+
}, timeout, interval).Should(Succeed())
1653+
1654+
th.DeleteInstance(GetPlacementAPI(names.PlacementAPIName))
1655+
1656+
secret := th.GetSecret(types.NamespacedName{
1657+
Namespace: names.Namespace,
1658+
Name: acSecretName,
1659+
})
1660+
Expect(secret.Finalizers).NotTo(
1661+
ContainElement(placement.ACConsumerFinalizer))
1662+
})
1663+
1664+
It("should remove the consumer finalizer when AC auth is cleared from spec", func() {
1665+
Eventually(func(g Gomega) {
1666+
secret := th.GetSecret(types.NamespacedName{
1667+
Namespace: names.Namespace,
1668+
Name: acSecretName,
1669+
})
1670+
g.Expect(secret.Finalizers).To(
1671+
ContainElement(placement.ACConsumerFinalizer))
1672+
}, timeout, interval).Should(Succeed())
1673+
1674+
Eventually(func(g Gomega) {
1675+
p := GetPlacementAPI(names.PlacementAPIName)
1676+
p.Spec.Auth.ApplicationCredentialSecret = ""
1677+
g.Expect(k8sClient.Update(ctx, p)).Should(Succeed())
1678+
}, timeout, interval).Should(Succeed())
1679+
1680+
Eventually(func(g Gomega) {
1681+
secret := th.GetSecret(types.NamespacedName{
1682+
Namespace: names.Namespace,
1683+
Name: acSecretName,
1684+
})
1685+
g.Expect(secret.Finalizers).NotTo(
1686+
ContainElement(placement.ACConsumerFinalizer))
1687+
}, timeout, interval).Should(Succeed())
1688+
1689+
Eventually(func(g Gomega) {
1690+
p := GetPlacementAPI(names.PlacementAPIName)
1691+
g.Expect(p.Status.ApplicationCredentialSecret).To(BeEmpty())
1692+
}, timeout, interval).Should(Succeed())
1693+
})
1694+
})
1695+
1696+
When("ApplicationCredential secret is not found", func() {
1697+
BeforeEach(func() {
1698+
DeferCleanup(
1699+
k8sClient.Delete, ctx, CreatePlacementAPISecret(names.Namespace, SecretName))
1700+
keystoneAPIName := keystone.CreateKeystoneAPI(names.Namespace)
1701+
DeferCleanup(keystone.DeleteKeystoneAPI, keystoneAPIName)
1702+
1703+
spec := GetDefaultPlacementAPISpec()
1704+
spec["auth"] = map[string]any{
1705+
"applicationCredentialSecret": "nonexistent-ac-secret",
1706+
}
1707+
1708+
DeferCleanup(th.DeleteInstance, CreatePlacementAPI(names.PlacementAPIName, spec))
1709+
1710+
DeferCleanup(
1711+
mariadb.DeleteDBService,
1712+
mariadb.CreateDBService(
1713+
names.Namespace,
1714+
GetDefaultPlacementAPISpec()["databaseInstance"].(string),
1715+
corev1.ServiceSpec{
1716+
Ports: []corev1.ServicePort{{Port: 3306}},
1717+
},
1718+
),
1719+
)
1720+
mariadb.SimulateMariaDBDatabaseCompleted(names.MariaDBDatabaseName)
1721+
mariadb.SimulateMariaDBAccountCompleted(names.MariaDBAccount)
1722+
})
1723+
1724+
It("should set ServiceConfigReady to False", func() {
1725+
th.ExpectCondition(
1726+
names.PlacementAPIName,
1727+
ConditionGetterFunc(PlacementConditionGetter),
1728+
condition.ServiceConfigReadyCondition,
1729+
corev1.ConditionFalse,
1730+
)
1731+
})
1732+
})
1733+
1734+
When("ApplicationCredential secret is missing required keys", func() {
1735+
BeforeEach(func() {
1736+
DeferCleanup(
1737+
k8sClient.Delete, ctx, CreatePlacementAPISecret(names.Namespace, SecretName))
1738+
keystoneAPIName := keystone.CreateKeystoneAPI(names.Namespace)
1739+
DeferCleanup(keystone.DeleteKeystoneAPI, keystoneAPIName)
1740+
1741+
badACSecretName := "ac-placement-bad-secret" //nolint:gosec // G101
1742+
badSecret := &corev1.Secret{
1743+
ObjectMeta: metav1.ObjectMeta{
1744+
Namespace: names.Namespace,
1745+
Name: badACSecretName,
1746+
},
1747+
Data: map[string][]byte{
1748+
"WRONG_KEY": []byte("some-value"),
1749+
},
1750+
}
1751+
DeferCleanup(k8sClient.Delete, ctx, badSecret)
1752+
Expect(k8sClient.Create(ctx, badSecret)).To(Succeed())
1753+
1754+
spec := GetDefaultPlacementAPISpec()
1755+
spec["auth"] = map[string]any{
1756+
"applicationCredentialSecret": badACSecretName,
1757+
}
1758+
1759+
DeferCleanup(th.DeleteInstance, CreatePlacementAPI(names.PlacementAPIName, spec))
1760+
1761+
DeferCleanup(
1762+
mariadb.DeleteDBService,
1763+
mariadb.CreateDBService(
1764+
names.Namespace,
1765+
GetDefaultPlacementAPISpec()["databaseInstance"].(string),
1766+
corev1.ServiceSpec{
1767+
Ports: []corev1.ServicePort{{Port: 3306}},
1768+
},
1769+
),
1770+
)
1771+
mariadb.SimulateMariaDBDatabaseCompleted(names.MariaDBDatabaseName)
1772+
mariadb.SimulateMariaDBAccountCompleted(names.MariaDBAccount)
1773+
})
1774+
1775+
It("should set ServiceConfigReady to False", func() {
1776+
th.ExpectCondition(
1777+
names.PlacementAPIName,
1778+
ConditionGetterFunc(PlacementConditionGetter),
1779+
condition.ServiceConfigReadyCondition,
1780+
corev1.ConditionFalse,
1781+
)
1782+
})
1783+
})
1784+
15291785
})

0 commit comments

Comments
 (0)