Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions modules/edpm/unstructured/nodeset.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,83 @@ func AreSecretHashesInSync(
return true, "", nil
}

// IsSecretHashInSync checks whether a single named secret's hash matches what
// is deployed across all OpenStackDataPlaneNodeSets in the given namespace.
// Unlike AreSecretHashesInSync, it ignores all other secrets — useful when a
// controller needs to track deployment of one specific secret without being
// affected by unrelated secret changes.
//
// Returns:
// - inSync=true when the secret's hash matches in every NodeSet that tracks
// it, when no NodeSets exist, when no NodeSet tracks this secret, or when
// the OpenStackDataPlaneNodeSet CRD is not installed.
// - inSync=false with info describing the first mismatch.
func IsSecretHashInSync(
ctx context.Context,
c client.Client,
namespace string,
secretName string,
) (inSync bool, info string, err error) {
Log := log.FromContext(ctx)

nodesetList := &k8s_unstructured.UnstructuredList{}
nodesetList.SetGroupVersionKind(schema.GroupVersionKind{
Group: NodeSetGVK.Group,
Version: NodeSetGVK.Version,
Kind: NodeSetGVK.Kind + "List",
})

if err := c.List(ctx, nodesetList, client.InNamespace(namespace)); err != nil {
if meta.IsNoMatchError(err) {
return true, "", nil
}
return false, "", fmt.Errorf("failed to list OpenStackDataPlaneNodeSets: %w", err)
}

for i := range nodesetList.Items {
item := &nodesetList.Items[i]

secretHashes, found, err := k8s_unstructured.NestedStringMap(item.Object, "status", "secretHashes")
if err != nil {
return false, "", fmt.Errorf("failed to read secretHashes from nodeset %s/%s: %w",
item.GetNamespace(), item.GetName(), err)
}
if !found {
continue
}

deployedHash, tracked := secretHashes[secretName]
if !tracked {
continue
}

currentSecret := &corev1.Secret{}
if err := c.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, currentSecret); err != nil {
if k8s_errors.IsNotFound(err) {
info := fmt.Sprintf("nodeset %s/%s: deployed secret %s no longer exists",
item.GetNamespace(), item.GetName(), secretName)
return false, info, nil
}
return false, "", fmt.Errorf("failed to get secret %s: %w", secretName, err)
}

currentHash, hashErr := oko_secret.Hash(currentSecret)
if hashErr != nil {
return false, "", fmt.Errorf("failed to hash secret %s: %w", secretName, hashErr)
}

if currentHash != deployedHash {
info := fmt.Sprintf("nodeset %s/%s: secret %s has changed since last deployment",
item.GetNamespace(), item.GetName(), secretName)
Log.Info("Secret hash out of sync", "secret", secretName,
"nodeset", item.GetName(), "deployed", deployedHash, "current", currentHash)
return false, info, nil
}
}

return true, "", nil
}

// HaveNodeSets returns true if any OpenStackDataPlaneNodeSets with non-empty
// status.secretHashes exist in the given namespace. Returns false when no
// NodeSets exist, none have secretHashes, or the CRD is not installed.
Expand Down
170 changes: 170 additions & 0 deletions modules/edpm/unstructured/nodeset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,176 @@ func TestAreSecretHashesInSync(t *testing.T) {
}
}

func TestIsSecretHashInSync_CRDNotInstalled(t *testing.T) {
s := runtime.NewScheme()
_ = corev1.AddToScheme(s)
mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{})

c := fake.NewClientBuilder().
WithScheme(s).
WithRESTMapper(mapper).
Build()

inSync, info, err := IsSecretHashInSync(context.Background(), c, "test", "any-secret")
if err != nil {
t.Errorf("IsSecretHashInSync() unexpected error: %v", err)
}
if !inSync {
t.Errorf("IsSecretHashInSync() inSync = false, want true when CRD not installed (info: %s)", info)
}
}

func TestIsSecretHashInSync(t *testing.T) {
hmacSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "instanceha-0-heartbeat-hmac",
Namespace: "test",
},
Data: map[string][]byte{"hmac-key": []byte("current-key")},
}
hmacHash, _ := oko_secret.Hash(hmacSecret)

otherSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "nova-cell1-compute-config",
Namespace: "test",
},
Data: map[string][]byte{"config": []byte("nova-config")},
}

tests := []struct {
name string
secretName string
nodesets []*k8s_unstructured.Unstructured
secrets []*corev1.Secret
wantInSync bool
wantInfoSubstr string
}{
{
name: "no nodesets exist",
secretName: "instanceha-0-heartbeat-hmac",
wantInSync: true,
},
{
name: "secret not tracked by any nodeset",
secretName: "instanceha-0-heartbeat-hmac",
nodesets: []*k8s_unstructured.Unstructured{
makeNodeSet("ns1", "test", map[string]string{
"nova-cell1-compute-config": "some-hash",
}),
},
secrets: []*corev1.Secret{hmacSecret, otherSecret},
wantInSync: true,
},
{
name: "secret in sync",
secretName: "instanceha-0-heartbeat-hmac",
nodesets: []*k8s_unstructured.Unstructured{
makeNodeSet("ns1", "test", map[string]string{
"instanceha-0-heartbeat-hmac": hmacHash,
"nova-cell1-compute-config": "stale-hash",
}),
},
secrets: []*corev1.Secret{hmacSecret, otherSecret},
wantInSync: true,
},
{
name: "secret out of sync",
secretName: "instanceha-0-heartbeat-hmac",
nodesets: []*k8s_unstructured.Unstructured{
makeNodeSet("ns1", "test", map[string]string{
"instanceha-0-heartbeat-hmac": "old-hash",
}),
},
secrets: []*corev1.Secret{hmacSecret},
wantInSync: false,
wantInfoSubstr: "has changed since last deployment",
},
{
name: "secret deleted",
secretName: "instanceha-0-heartbeat-hmac",
nodesets: []*k8s_unstructured.Unstructured{
makeNodeSet("ns1", "test", map[string]string{
"instanceha-0-heartbeat-hmac": "some-hash",
}),
},
secrets: []*corev1.Secret{},
wantInSync: false,
wantInfoSubstr: "no longer exists",
},
{
name: "multiple nodesets - one stale for this secret",
secretName: "instanceha-0-heartbeat-hmac",
nodesets: []*k8s_unstructured.Unstructured{
makeNodeSet("up-to-date", "test", map[string]string{
"instanceha-0-heartbeat-hmac": hmacHash,
}),
makeNodeSet("stale", "test", map[string]string{
"instanceha-0-heartbeat-hmac": "old-hash",
}),
},
secrets: []*corev1.Secret{hmacSecret},
wantInSync: false,
wantInfoSubstr: "has changed since last deployment",
},
{
name: "other secret stale does not affect this secret",
secretName: "instanceha-0-heartbeat-hmac",
nodesets: []*k8s_unstructured.Unstructured{
makeNodeSet("ns1", "test", map[string]string{
"instanceha-0-heartbeat-hmac": hmacHash,
"unrelated-secret": "stale-hash",
}),
},
secrets: []*corev1.Secret{hmacSecret},
wantInSync: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, mapper := newTestSchemeAndMapper()

builder := fake.NewClientBuilder().
WithScheme(s).
WithRESTMapper(mapper)

for _, ns := range tt.nodesets {
builder = builder.WithObjects(ns)
}
for _, sec := range tt.secrets {
builder = builder.WithObjects(sec)
}

c := builder.Build()

inSync, info, err := IsSecretHashInSync(
context.Background(),
c,
"test",
tt.secretName,
)

if err != nil {
t.Errorf("IsSecretHashInSync() unexpected error: %v", err)
return
}

if inSync != tt.wantInSync {
t.Errorf("IsSecretHashInSync() inSync = %v, want %v (info: %s)", inSync, tt.wantInSync, info)
}

if tt.wantInfoSubstr != "" {
if info == "" {
t.Errorf("IsSecretHashInSync() info is empty, want substring %q", tt.wantInfoSubstr)
} else if !strings.Contains(info, tt.wantInfoSubstr) {
t.Errorf("IsSecretHashInSync() info = %q, want substring %q", info, tt.wantInfoSubstr)
}
}
})
}
}

func TestHaveNodeSets_CRDNotInstalled(t *testing.T) {
s := runtime.NewScheme()
_ = corev1.AddToScheme(s)
Expand Down
Loading