Skip to content

Commit ce00f67

Browse files
authored
Merge pull request #700 from lmiccini/add-IsSecretHashInSync
Add IsSecretHashInSync for scoped single-secret hash checking
2 parents fbef9a0 + 102d676 commit ce00f67

2 files changed

Lines changed: 247 additions & 0 deletions

File tree

modules/edpm/unstructured/nodeset.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,83 @@ func AreSecretHashesInSync(
134134
return true, "", nil
135135
}
136136

137+
// IsSecretHashInSync checks whether a single named secret's hash matches what
138+
// is deployed across all OpenStackDataPlaneNodeSets in the given namespace.
139+
// Unlike AreSecretHashesInSync, it ignores all other secrets — useful when a
140+
// controller needs to track deployment of one specific secret without being
141+
// affected by unrelated secret changes.
142+
//
143+
// Returns:
144+
// - inSync=true when the secret's hash matches in every NodeSet that tracks
145+
// it, when no NodeSets exist, when no NodeSet tracks this secret, or when
146+
// the OpenStackDataPlaneNodeSet CRD is not installed.
147+
// - inSync=false with info describing the first mismatch.
148+
func IsSecretHashInSync(
149+
ctx context.Context,
150+
c client.Client,
151+
namespace string,
152+
secretName string,
153+
) (inSync bool, info string, err error) {
154+
Log := log.FromContext(ctx)
155+
156+
nodesetList := &k8s_unstructured.UnstructuredList{}
157+
nodesetList.SetGroupVersionKind(schema.GroupVersionKind{
158+
Group: NodeSetGVK.Group,
159+
Version: NodeSetGVK.Version,
160+
Kind: NodeSetGVK.Kind + "List",
161+
})
162+
163+
if err := c.List(ctx, nodesetList, client.InNamespace(namespace)); err != nil {
164+
if meta.IsNoMatchError(err) {
165+
return true, "", nil
166+
}
167+
return false, "", fmt.Errorf("failed to list OpenStackDataPlaneNodeSets: %w", err)
168+
}
169+
170+
for i := range nodesetList.Items {
171+
item := &nodesetList.Items[i]
172+
173+
secretHashes, found, err := k8s_unstructured.NestedStringMap(item.Object, "status", "secretHashes")
174+
if err != nil {
175+
return false, "", fmt.Errorf("failed to read secretHashes from nodeset %s/%s: %w",
176+
item.GetNamespace(), item.GetName(), err)
177+
}
178+
if !found {
179+
continue
180+
}
181+
182+
deployedHash, tracked := secretHashes[secretName]
183+
if !tracked {
184+
continue
185+
}
186+
187+
currentSecret := &corev1.Secret{}
188+
if err := c.Get(ctx, types.NamespacedName{Name: secretName, Namespace: namespace}, currentSecret); err != nil {
189+
if k8s_errors.IsNotFound(err) {
190+
info := fmt.Sprintf("nodeset %s/%s: deployed secret %s no longer exists",
191+
item.GetNamespace(), item.GetName(), secretName)
192+
return false, info, nil
193+
}
194+
return false, "", fmt.Errorf("failed to get secret %s: %w", secretName, err)
195+
}
196+
197+
currentHash, hashErr := oko_secret.Hash(currentSecret)
198+
if hashErr != nil {
199+
return false, "", fmt.Errorf("failed to hash secret %s: %w", secretName, hashErr)
200+
}
201+
202+
if currentHash != deployedHash {
203+
info := fmt.Sprintf("nodeset %s/%s: secret %s has changed since last deployment",
204+
item.GetNamespace(), item.GetName(), secretName)
205+
Log.Info("Secret hash out of sync", "secret", secretName,
206+
"nodeset", item.GetName(), "deployed", deployedHash, "current", currentHash)
207+
return false, info, nil
208+
}
209+
}
210+
211+
return true, "", nil
212+
}
213+
137214
// HaveNodeSets returns true if any OpenStackDataPlaneNodeSets with non-empty
138215
// status.secretHashes exist in the given namespace. Returns false when no
139216
// NodeSets exist, none have secretHashes, or the CRD is not installed.

modules/edpm/unstructured/nodeset_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,176 @@ func TestAreSecretHashesInSync(t *testing.T) {
219219
}
220220
}
221221

222+
func TestIsSecretHashInSync_CRDNotInstalled(t *testing.T) {
223+
s := runtime.NewScheme()
224+
_ = corev1.AddToScheme(s)
225+
mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{})
226+
227+
c := fake.NewClientBuilder().
228+
WithScheme(s).
229+
WithRESTMapper(mapper).
230+
Build()
231+
232+
inSync, info, err := IsSecretHashInSync(context.Background(), c, "test", "any-secret")
233+
if err != nil {
234+
t.Errorf("IsSecretHashInSync() unexpected error: %v", err)
235+
}
236+
if !inSync {
237+
t.Errorf("IsSecretHashInSync() inSync = false, want true when CRD not installed (info: %s)", info)
238+
}
239+
}
240+
241+
func TestIsSecretHashInSync(t *testing.T) {
242+
hmacSecret := &corev1.Secret{
243+
ObjectMeta: metav1.ObjectMeta{
244+
Name: "instanceha-0-heartbeat-hmac",
245+
Namespace: "test",
246+
},
247+
Data: map[string][]byte{"hmac-key": []byte("current-key")},
248+
}
249+
hmacHash, _ := oko_secret.Hash(hmacSecret)
250+
251+
otherSecret := &corev1.Secret{
252+
ObjectMeta: metav1.ObjectMeta{
253+
Name: "nova-cell1-compute-config",
254+
Namespace: "test",
255+
},
256+
Data: map[string][]byte{"config": []byte("nova-config")},
257+
}
258+
259+
tests := []struct {
260+
name string
261+
secretName string
262+
nodesets []*k8s_unstructured.Unstructured
263+
secrets []*corev1.Secret
264+
wantInSync bool
265+
wantInfoSubstr string
266+
}{
267+
{
268+
name: "no nodesets exist",
269+
secretName: "instanceha-0-heartbeat-hmac",
270+
wantInSync: true,
271+
},
272+
{
273+
name: "secret not tracked by any nodeset",
274+
secretName: "instanceha-0-heartbeat-hmac",
275+
nodesets: []*k8s_unstructured.Unstructured{
276+
makeNodeSet("ns1", "test", map[string]string{
277+
"nova-cell1-compute-config": "some-hash",
278+
}),
279+
},
280+
secrets: []*corev1.Secret{hmacSecret, otherSecret},
281+
wantInSync: true,
282+
},
283+
{
284+
name: "secret in sync",
285+
secretName: "instanceha-0-heartbeat-hmac",
286+
nodesets: []*k8s_unstructured.Unstructured{
287+
makeNodeSet("ns1", "test", map[string]string{
288+
"instanceha-0-heartbeat-hmac": hmacHash,
289+
"nova-cell1-compute-config": "stale-hash",
290+
}),
291+
},
292+
secrets: []*corev1.Secret{hmacSecret, otherSecret},
293+
wantInSync: true,
294+
},
295+
{
296+
name: "secret out of sync",
297+
secretName: "instanceha-0-heartbeat-hmac",
298+
nodesets: []*k8s_unstructured.Unstructured{
299+
makeNodeSet("ns1", "test", map[string]string{
300+
"instanceha-0-heartbeat-hmac": "old-hash",
301+
}),
302+
},
303+
secrets: []*corev1.Secret{hmacSecret},
304+
wantInSync: false,
305+
wantInfoSubstr: "has changed since last deployment",
306+
},
307+
{
308+
name: "secret deleted",
309+
secretName: "instanceha-0-heartbeat-hmac",
310+
nodesets: []*k8s_unstructured.Unstructured{
311+
makeNodeSet("ns1", "test", map[string]string{
312+
"instanceha-0-heartbeat-hmac": "some-hash",
313+
}),
314+
},
315+
secrets: []*corev1.Secret{},
316+
wantInSync: false,
317+
wantInfoSubstr: "no longer exists",
318+
},
319+
{
320+
name: "multiple nodesets - one stale for this secret",
321+
secretName: "instanceha-0-heartbeat-hmac",
322+
nodesets: []*k8s_unstructured.Unstructured{
323+
makeNodeSet("up-to-date", "test", map[string]string{
324+
"instanceha-0-heartbeat-hmac": hmacHash,
325+
}),
326+
makeNodeSet("stale", "test", map[string]string{
327+
"instanceha-0-heartbeat-hmac": "old-hash",
328+
}),
329+
},
330+
secrets: []*corev1.Secret{hmacSecret},
331+
wantInSync: false,
332+
wantInfoSubstr: "has changed since last deployment",
333+
},
334+
{
335+
name: "other secret stale does not affect this secret",
336+
secretName: "instanceha-0-heartbeat-hmac",
337+
nodesets: []*k8s_unstructured.Unstructured{
338+
makeNodeSet("ns1", "test", map[string]string{
339+
"instanceha-0-heartbeat-hmac": hmacHash,
340+
"unrelated-secret": "stale-hash",
341+
}),
342+
},
343+
secrets: []*corev1.Secret{hmacSecret},
344+
wantInSync: true,
345+
},
346+
}
347+
348+
for _, tt := range tests {
349+
t.Run(tt.name, func(t *testing.T) {
350+
s, mapper := newTestSchemeAndMapper()
351+
352+
builder := fake.NewClientBuilder().
353+
WithScheme(s).
354+
WithRESTMapper(mapper)
355+
356+
for _, ns := range tt.nodesets {
357+
builder = builder.WithObjects(ns)
358+
}
359+
for _, sec := range tt.secrets {
360+
builder = builder.WithObjects(sec)
361+
}
362+
363+
c := builder.Build()
364+
365+
inSync, info, err := IsSecretHashInSync(
366+
context.Background(),
367+
c,
368+
"test",
369+
tt.secretName,
370+
)
371+
372+
if err != nil {
373+
t.Errorf("IsSecretHashInSync() unexpected error: %v", err)
374+
return
375+
}
376+
377+
if inSync != tt.wantInSync {
378+
t.Errorf("IsSecretHashInSync() inSync = %v, want %v (info: %s)", inSync, tt.wantInSync, info)
379+
}
380+
381+
if tt.wantInfoSubstr != "" {
382+
if info == "" {
383+
t.Errorf("IsSecretHashInSync() info is empty, want substring %q", tt.wantInfoSubstr)
384+
} else if !strings.Contains(info, tt.wantInfoSubstr) {
385+
t.Errorf("IsSecretHashInSync() info = %q, want substring %q", info, tt.wantInfoSubstr)
386+
}
387+
}
388+
})
389+
}
390+
}
391+
222392
func TestHaveNodeSets_CRDNotInstalled(t *testing.T) {
223393
s := runtime.NewScheme()
224394
_ = corev1.AddToScheme(s)

0 commit comments

Comments
 (0)