Skip to content

Commit 13d1c14

Browse files
lmicciniclaude
andcommitted
Skip SecretHashes update on ansibleLimit-scoped dataplane deployments
For servicesOverride deployments, merge SecretHashes into the existing map without resetting it, since all nodes were touched. For ansibleLimit-scoped deployments, skip the update entirely as not all nodes received the secrets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64e838e commit 13d1c14

2 files changed

Lines changed: 125 additions & 5 deletions

File tree

internal/controller/dataplane/openstackdataplanenodeset_controller.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -592,11 +592,17 @@ func checkDeployment(ctx context.Context, helper *helper.Helper,
592592
for k, v := range deployment.Status.ConfigMapHashes {
593593
instance.Status.ConfigMapHashes[k] = v
594594
}
595-
if len(deployment.Spec.ServicesOverride) == 0 {
596-
instance.Status.SecretHashes = make(map[string]string, len(deployment.Status.SecretHashes))
597-
}
598-
for k, v := range deployment.Status.SecretHashes {
599-
instance.Status.SecretHashes[k] = v
595+
// Skip SecretHashes update for ansibleLimit-scoped deployments
596+
// since not all nodes received the secrets.
597+
isNodeScoped := deployment.Spec.AnsibleLimit != "" && deployment.Spec.AnsibleLimit != "*"
598+
if !isNodeScoped {
599+
if len(deployment.Spec.ServicesOverride) == 0 {
600+
// Full deployment: reset and replace the entire map.
601+
instance.Status.SecretHashes = make(map[string]string, len(deployment.Status.SecretHashes))
602+
}
603+
for k, v := range deployment.Status.SecretHashes {
604+
instance.Status.SecretHashes[k] = v
605+
}
600606
}
601607
for k, v := range deployment.Status.ContainerImages {
602608
instance.Status.ContainerImages[k] = v

test/functional/dataplane/openstackdataplanenodeset_controller_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"encoding/json"
2020
"fmt"
2121
"os"
22+
"time"
2223

2324
. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
2425
. "github.com/onsi/gomega" //revive:disable:dot-imports
@@ -1613,6 +1614,119 @@ var _ = Describe("Dataplane NodeSet Test", func() {
16131614
})
16141615
})
16151616

1617+
When("A scoped deployment does not update SecretHashes on NodeSet", func() {
1618+
var dataSourceSecretName types.NamespacedName
1619+
var hashTestServiceName types.NamespacedName
1620+
1621+
BeforeEach(func() {
1622+
dataSourceSecretName = types.NamespacedName{
1623+
Name: "test-datasource-secret",
1624+
Namespace: namespace,
1625+
}
1626+
hashTestServiceName = types.NamespacedName{
1627+
Name: "hash-test-service",
1628+
Namespace: namespace,
1629+
}
1630+
1631+
nodeSetSpec := DefaultDataPlaneNodeSetSpec("edpm-compute")
1632+
nodeSetSpec["preProvisioned"] = true
1633+
nodeSetSpec["services"] = []string{"hash-test-service"}
1634+
1635+
th.CreateSecret(dataSourceSecretName, map[string][]byte{
1636+
"transport_url": []byte("rabbit://nova:old-password@rabbitmq:5672/"),
1637+
})
1638+
1639+
DeferCleanup(th.DeleteInstance, CreateDataPlaneServiceFromSpec(hashTestServiceName, map[string]interface{}{
1640+
"playbook": "test",
1641+
"dataSources": []map[string]interface{}{
1642+
{
1643+
"secretRef": map[string]interface{}{
1644+
"name": dataSourceSecretName.Name,
1645+
},
1646+
},
1647+
},
1648+
}))
1649+
1650+
DeferCleanup(th.DeleteInstance, CreateNetConfig(dataplaneNetConfigName, DefaultNetConfigSpec()))
1651+
DeferCleanup(th.DeleteInstance, CreateDNSMasq(dnsMasqName, DefaultDNSMasqSpec()))
1652+
DeferCleanup(th.DeleteInstance, CreateDataplaneNodeSet(dataplaneNodeSetName, nodeSetSpec))
1653+
DeferCleanup(th.DeleteInstance, CreateDataplaneDeployment(dataplaneDeploymentName, DefaultDataPlaneDeploymentSpec()))
1654+
CreateSSHSecret(dataplaneSSHSecretName)
1655+
CreateCABundleSecret(caBundleSecretName)
1656+
SimulateDNSMasqComplete(dnsMasqName)
1657+
SimulateIPSetComplete(dataplaneNodeName)
1658+
SimulateDNSDataComplete(dataplaneNodeSetName)
1659+
})
1660+
1661+
It("Should preserve SecretHashes when secret changes and scoped deployment completes", func() {
1662+
// Complete the full deployment
1663+
Eventually(func(g Gomega) {
1664+
ansibleeeName := types.NamespacedName{
1665+
Name: "hash-test-service-" + dataplaneDeploymentName.Name + "-" + dataplaneNodeSetName.Name,
1666+
Namespace: namespace,
1667+
}
1668+
ansibleEE := GetAnsibleee(ansibleeeName)
1669+
ansibleEE.Status.Succeeded = 1
1670+
g.Expect(th.K8sClient.Status().Update(th.Ctx, ansibleEE)).To(Succeed())
1671+
}, th.Timeout, th.Interval).Should(Succeed())
1672+
1673+
// Wait for SecretHashes to be populated on the nodeset
1674+
Eventually(func(g Gomega) {
1675+
instance := GetDataplaneNodeSet(dataplaneNodeSetName)
1676+
g.Expect(instance.Status.SecretHashes).ShouldNot(BeEmpty())
1677+
g.Expect(instance.Status.SecretHashes).Should(HaveKey(dataSourceSecretName.Name))
1678+
}, th.Timeout, th.Interval).Should(Succeed())
1679+
1680+
// Capture the SecretHashes after full deployment
1681+
instance := GetDataplaneNodeSet(dataplaneNodeSetName)
1682+
savedSecretHashes := make(map[string]string)
1683+
for k, v := range instance.Status.SecretHashes {
1684+
savedSecretHashes[k] = v
1685+
}
1686+
1687+
// Modify the secret to simulate credential rotation
1688+
Eventually(func(g Gomega) {
1689+
secret := &corev1.Secret{}
1690+
g.Expect(th.K8sClient.Get(th.Ctx, dataSourceSecretName, secret)).To(Succeed())
1691+
secret.Data["transport_url"] = []byte("rabbit://nova:new-rotated-password@rabbitmq:5672/")
1692+
g.Expect(th.K8sClient.Update(th.Ctx, secret)).To(Succeed())
1693+
}, th.Timeout, th.Interval).Should(Succeed())
1694+
1695+
// Create a scoped deployment with ServicesOverride
1696+
scopedDeploymentName := types.NamespacedName{
1697+
Name: "scoped-deployment",
1698+
Namespace: namespace,
1699+
}
1700+
scopedSpec := DefaultDataPlaneDeploymentSpec()
1701+
scopedSpec["servicesOverride"] = []string{"hash-test-service"}
1702+
DeferCleanup(th.DeleteInstance, CreateDataplaneDeployment(scopedDeploymentName, scopedSpec))
1703+
1704+
// Complete the scoped deployment
1705+
Eventually(func(g Gomega) {
1706+
ansibleeeName := types.NamespacedName{
1707+
Name: "hash-test-service-" + scopedDeploymentName.Name + "-" + dataplaneNodeSetName.Name,
1708+
Namespace: namespace,
1709+
}
1710+
ansibleEE := GetAnsibleee(ansibleeeName)
1711+
ansibleEE.Status.Succeeded = 1
1712+
g.Expect(th.K8sClient.Status().Update(th.Ctx, ansibleEE)).To(Succeed())
1713+
}, th.Timeout, th.Interval).Should(Succeed())
1714+
1715+
// Wait for scoped deployment to be processed
1716+
Eventually(func(g Gomega) {
1717+
instance := GetDataplaneNodeSet(dataplaneNodeSetName)
1718+
g.Expect(instance.Status.DeploymentStatuses).Should(HaveKey(scopedDeploymentName.Name))
1719+
}, th.Timeout, th.Interval).Should(Succeed())
1720+
1721+
// SecretHashes should still have the old hashes from the full deployment,
1722+
// not the new hashes from the scoped deployment
1723+
Consistently(func(g Gomega) {
1724+
instance := GetDataplaneNodeSet(dataplaneNodeSetName)
1725+
g.Expect(instance.Status.SecretHashes).Should(Equal(savedSecretHashes))
1726+
}, 5*time.Second, th.Interval).Should(Succeed())
1727+
})
1728+
})
1729+
16161730
When("Running deployments exist with completed deployment", func() {
16171731
BeforeEach(func() {
16181732
nodeSetSpec := DefaultDataPlaneNodeSetSpec("edpm-compute")

0 commit comments

Comments
 (0)