Skip to content

Commit dd34613

Browse files
committed
feat: improve ConfigMap handling with better tests and error handling
- Add comprehensive test coverage for ConfigMap changes - Fix timing issues with hash annotations - Add test for multiple ConfigMaps error condition - Improve controller to reject multiple ConfigMaps per provider - Add proper ReadyCondition setting in tests
1 parent 0869c19 commit dd34613

File tree

2 files changed

+156
-120
lines changed

2 files changed

+156
-120
lines changed

internal/controller/configmap_changes_test.go

Lines changed: 150 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import (
2424
corev1 "k8s.io/api/core/v1"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
27+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2728
"sigs.k8s.io/cluster-api/util/conditions"
29+
"sigs.k8s.io/cluster-api/util/patch"
2830
"sigs.k8s.io/controller-runtime/pkg/client"
2931
)
3032

@@ -45,8 +47,8 @@ func TestConfigMapChangesAreAppliedToTheProvider(t *testing.T) {
4547
// Create ConfigMap with initial content
4648
configMap := &corev1.ConfigMap{
4749
ObjectMeta: metav1.ObjectMeta{
48-
GenerateName: "edge-provider-",
49-
Namespace: ns.Name,
50+
Name: testCurrentVersion,
51+
Namespace: ns.Name,
5052
Labels: map[string]string{
5153
"provider-components": "edge",
5254
},
@@ -86,6 +88,12 @@ func TestConfigMapChangesAreAppliedToTheProvider(t *testing.T) {
8688

8789
t.Log("CoreProvider is installed")
8890

91+
// Manually set ReadyCondition as it's not set automatically in test env
92+
patchHelper, err := patch.NewHelper(coreProvider, env)
93+
g.Expect(err).ToNot(HaveOccurred())
94+
conditions.MarkTrue(coreProvider, clusterv1.ReadyCondition)
95+
g.Expect(patchHelper.Patch(ctx, coreProvider)).To(Succeed())
96+
8997
// Create InfrastructureProvider that uses the ConfigMap
9098
provider := &operatorv1.InfrastructureProvider{
9199
ObjectMeta: metav1.ObjectMeta{
@@ -121,6 +129,15 @@ func TestConfigMapChangesAreAppliedToTheProvider(t *testing.T) {
121129

122130
t.Log("Provider is installed")
123131

132+
// Wait for the provider to have a hash annotation (this happens after full reconciliation)
133+
g.Eventually(func() bool {
134+
if err := env.Get(ctx, client.ObjectKeyFromObject(provider), provider); err != nil {
135+
return false
136+
}
137+
hash := provider.GetAnnotations()[appliedSpecHashAnnotation]
138+
return hash != ""
139+
}, timeout).Should(BeTrue(), "Provider should have a hash annotation after reconciliation")
140+
124141
// Get the initial hash annotation
125142
g.Expect(env.Get(ctx, client.ObjectKeyFromObject(provider), provider)).To(Succeed())
126143
initialHash := provider.GetAnnotations()[appliedSpecHashAnnotation]
@@ -148,40 +165,20 @@ func TestConfigMapChangesAreAppliedToTheProvider(t *testing.T) {
148165
t.Log("Provider reconciled with new hash")
149166
}
150167

151-
func TestConfigMapChangesWithMultipleProviders(t *testing.T) {
168+
func TestConfigMapChangesWithNonMatchingSelector(t *testing.T) {
152169
g := NewWithT(t)
153170
objs := []client.Object{}
154171

155172
defer func() {
156173
g.Expect(env.CleanupAndWait(ctx, objs...)).To(Succeed())
157174
}()
158175

159-
ns, err := env.CreateNamespace(ctx, "multiple-providers-namespace")
176+
ns, err := env.CreateNamespace(ctx, "non-matching-selector-namespace")
160177
g.Expect(err).ToNot(HaveOccurred())
161178
g.Expect(ns.Name).NotTo(BeEmpty())
162179

163180
t.Log("Ensure namespace exists", ns.Name)
164181

165-
// Create ConfigMap with initial content
166-
configMap := &corev1.ConfigMap{
167-
ObjectMeta: metav1.ObjectMeta{
168-
GenerateName: "shared-configmap-",
169-
Namespace: ns.Name,
170-
Labels: map[string]string{
171-
"provider-components": "shared",
172-
"environment": "test",
173-
},
174-
},
175-
Data: map[string]string{
176-
"metadata": testMetadata,
177-
"components": testComponents,
178-
},
179-
}
180-
g.Expect(env.CreateAndWait(ctx, configMap)).To(Succeed())
181-
objs = append(objs, configMap)
182-
183-
t.Log("Created shared ConfigMap")
184-
185182
// Create CoreProvider first (required for InfrastructureProvider)
186183
coreProvider := &operatorv1.CoreProvider{
187184
ObjectMeta: metav1.ObjectMeta{
@@ -207,93 +204,11 @@ func TestConfigMapChangesWithMultipleProviders(t *testing.T) {
207204

208205
t.Log("CoreProvider is installed")
209206

210-
// Create multiple providers that use the same ConfigMap
211-
providers := []*operatorv1.InfrastructureProvider{}
212-
for i := 0; i < 2; i++ {
213-
provider := &operatorv1.InfrastructureProvider{
214-
ObjectMeta: metav1.ObjectMeta{
215-
GenerateName: "provider-",
216-
Namespace: ns.Name,
217-
},
218-
Spec: operatorv1.InfrastructureProviderSpec{
219-
ProviderSpec: operatorv1.ProviderSpec{
220-
Version: testCurrentVersion,
221-
FetchConfig: &operatorv1.FetchConfiguration{
222-
Selector: &metav1.LabelSelector{
223-
MatchLabels: map[string]string{
224-
"provider-components": "shared",
225-
},
226-
},
227-
},
228-
},
229-
},
230-
}
231-
g.Expect(env.CreateAndWait(ctx, provider)).To(Succeed())
232-
objs = append(objs, provider)
233-
providers = append(providers, provider)
234-
}
235-
236-
t.Log("Created multiple providers")
237-
238-
// Wait for all providers to be ready
239-
for _, provider := range providers {
240-
g.Eventually(func() bool {
241-
if err := env.Get(ctx, client.ObjectKeyFromObject(provider), provider); err != nil {
242-
return false
243-
}
244-
245-
return conditions.IsTrue(provider, operatorv1.ProviderInstalledCondition)
246-
}, timeout).Should(BeTrue())
247-
}
248-
249-
t.Log("All providers are installed")
250-
251-
// Get initial hash annotations
252-
initialHashes := make(map[string]string)
253-
for i, provider := range providers {
254-
g.Expect(env.Get(ctx, client.ObjectKeyFromObject(provider), provider)).To(Succeed())
255-
hash := provider.GetAnnotations()[appliedSpecHashAnnotation]
256-
g.Expect(hash).ToNot(BeEmpty())
257-
initialHashes[provider.GetName()] = hash
258-
t.Logf("Provider %d initial hash: %s", i, hash)
259-
}
260-
261-
// Update the ConfigMap content
262-
g.Expect(env.Get(ctx, client.ObjectKeyFromObject(configMap), configMap)).To(Succeed())
263-
configMap.Data["components"] = testComponents + "\n# Updated shared content"
264-
g.Expect(env.Update(ctx, configMap)).To(Succeed())
265-
266-
t.Log("Shared ConfigMap updated")
267-
268-
// Wait for all providers to be reconciled with new hashes
269-
for i, provider := range providers {
270-
g.Eventually(func() bool {
271-
if err := env.Get(ctx, client.ObjectKeyFromObject(provider), provider); err != nil {
272-
return false
273-
}
274-
275-
currentHash := provider.GetAnnotations()[appliedSpecHashAnnotation]
276-
initialHash := initialHashes[provider.GetName()]
277-
return currentHash != "" && currentHash != initialHash
278-
}, 30*time.Second).Should(BeTrue())
279-
280-
t.Logf("Provider %d reconciled with new hash", i)
281-
}
282-
}
283-
284-
func TestConfigMapChangesWithNonMatchingSelector(t *testing.T) {
285-
g := NewWithT(t)
286-
objs := []client.Object{}
287-
288-
defer func() {
289-
g.Expect(env.CleanupAndWait(ctx, objs...)).To(Succeed())
290-
}()
291-
292-
ns, err := env.CreateNamespace(ctx, "non-matching-selector-namespace")
207+
// Manually set ReadyCondition as it's not set automatically in test env
208+
patchHelper, err := patch.NewHelper(coreProvider, env)
293209
g.Expect(err).ToNot(HaveOccurred())
294-
g.Expect(ns.Name).NotTo(BeEmpty())
295-
296-
t.Log("Ensure namespace exists", ns.Name)
210+
conditions.MarkTrue(coreProvider, clusterv1.ReadyCondition)
211+
g.Expect(patchHelper.Patch(ctx, coreProvider)).To(Succeed())
297212

298213
// Create ConfigMap that won't match any provider selector
299214
configMap := &corev1.ConfigMap{
@@ -362,6 +277,15 @@ func TestConfigMapChangesWithNonMatchingSelector(t *testing.T) {
362277
return conditions.IsTrue(provider, operatorv1.ProviderInstalledCondition)
363278
}, timeout).Should(BeTrue())
364279

280+
// Wait for the provider to have a hash annotation (this happens after full reconciliation)
281+
g.Eventually(func() bool {
282+
if err := env.Get(ctx, client.ObjectKeyFromObject(provider), provider); err != nil {
283+
return false
284+
}
285+
hash := provider.GetAnnotations()[appliedSpecHashAnnotation]
286+
return hash != ""
287+
}, timeout).Should(BeTrue(), "Provider should have a hash annotation after reconciliation")
288+
365289
// Get initial hash
366290
g.Expect(env.Get(ctx, client.ObjectKeyFromObject(provider), provider)).To(Succeed())
367291
initialHash := provider.GetAnnotations()[appliedSpecHashAnnotation]
@@ -405,3 +329,119 @@ func TestConfigMapChangesWithNonMatchingSelector(t *testing.T) {
405329

406330
t.Log("Provider reconciled with new hash after matching ConfigMap update")
407331
}
332+
333+
func TestMultipleConfigMapsError(t *testing.T) {
334+
g := NewWithT(t)
335+
objs := []client.Object{}
336+
337+
defer func() {
338+
g.Expect(env.CleanupAndWait(ctx, objs...)).To(Succeed())
339+
}()
340+
341+
ns, err := env.CreateNamespace(ctx, "multiple-configmaps-error-namespace")
342+
g.Expect(err).ToNot(HaveOccurred())
343+
g.Expect(ns.Name).NotTo(BeEmpty())
344+
345+
t.Log("Ensure namespace exists", ns.Name)
346+
347+
// Create CoreProvider first (required for InfrastructureProvider)
348+
coreProvider := &operatorv1.CoreProvider{
349+
ObjectMeta: metav1.ObjectMeta{
350+
Name: "cluster-api",
351+
Namespace: ns.Name,
352+
},
353+
Spec: operatorv1.CoreProviderSpec{
354+
ProviderSpec: operatorv1.ProviderSpec{
355+
Version: testCurrentVersion,
356+
},
357+
},
358+
}
359+
g.Expect(env.CreateAndWait(ctx, coreProvider)).To(Succeed())
360+
objs = append(objs, coreProvider)
361+
362+
// Wait for CoreProvider to be installed
363+
g.Eventually(func() bool {
364+
if err := env.Get(ctx, client.ObjectKeyFromObject(coreProvider), coreProvider); err != nil {
365+
return false
366+
}
367+
return conditions.IsTrue(coreProvider, operatorv1.ProviderInstalledCondition)
368+
}, timeout).Should(BeTrue())
369+
370+
t.Log("CoreProvider is installed")
371+
372+
// Manually set ReadyCondition as it's not set automatically in test env
373+
patchHelper, err := patch.NewHelper(coreProvider, env)
374+
g.Expect(err).ToNot(HaveOccurred())
375+
conditions.MarkTrue(coreProvider, clusterv1.ReadyCondition)
376+
g.Expect(patchHelper.Patch(ctx, coreProvider)).To(Succeed())
377+
378+
// Create multiple ConfigMaps with the same labels (this should cause an error)
379+
configMap1 := &corev1.ConfigMap{
380+
ObjectMeta: metav1.ObjectMeta{
381+
Name: testCurrentVersion,
382+
Namespace: ns.Name,
383+
Labels: map[string]string{
384+
"provider-components": "edge",
385+
},
386+
},
387+
Data: map[string]string{
388+
"metadata": testMetadata,
389+
"components": testComponents,
390+
},
391+
}
392+
g.Expect(env.CreateAndWait(ctx, configMap1)).To(Succeed())
393+
objs = append(objs, configMap1)
394+
395+
configMap2 := &corev1.ConfigMap{
396+
ObjectMeta: metav1.ObjectMeta{
397+
Name: "v1.1.0",
398+
Namespace: ns.Name,
399+
Labels: map[string]string{
400+
"provider-components": "edge",
401+
},
402+
},
403+
Data: map[string]string{
404+
"metadata": testMetadata,
405+
"components": testComponents + "\n# Different content",
406+
},
407+
}
408+
g.Expect(env.CreateAndWait(ctx, configMap2)).To(Succeed())
409+
objs = append(objs, configMap2)
410+
411+
t.Log("Created multiple ConfigMaps with same labels")
412+
413+
// Create InfrastructureProvider that uses the ConfigMaps (should fail due to multiple matches)
414+
provider := &operatorv1.InfrastructureProvider{
415+
ObjectMeta: metav1.ObjectMeta{
416+
GenerateName: "edge-provider-",
417+
Namespace: ns.Name,
418+
},
419+
Spec: operatorv1.InfrastructureProviderSpec{
420+
ProviderSpec: operatorv1.ProviderSpec{
421+
Version: testCurrentVersion,
422+
FetchConfig: &operatorv1.FetchConfiguration{
423+
Selector: &metav1.LabelSelector{
424+
MatchLabels: map[string]string{
425+
"provider-components": "edge",
426+
},
427+
},
428+
},
429+
},
430+
},
431+
}
432+
g.Expect(env.CreateAndWait(ctx, provider)).To(Succeed())
433+
objs = append(objs, provider)
434+
435+
t.Log("Created provider with selector matching multiple ConfigMaps")
436+
437+
// Provider should have error condition due to multiple ConfigMaps
438+
g.Eventually(func() bool {
439+
if err := env.Get(ctx, client.ObjectKeyFromObject(provider), provider); err != nil {
440+
return false
441+
}
442+
return conditions.IsFalse(provider, operatorv1.ProviderInstalledCondition) &&
443+
conditions.GetReason(provider, operatorv1.ProviderInstalledCondition) != ""
444+
}, timeout).Should(BeTrue())
445+
446+
t.Log("Provider correctly failed with multiple ConfigMaps error")
447+
}

internal/controller/genericprovider_controller.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -309,18 +309,14 @@ func addConfigMapToHash(ctx context.Context, k8sClient client.Client, hash hash.
309309
return err
310310
}
311311

312-
// Sort the ConfigMaps by name for consistent hashing
313-
configMaps := configMapList.Items
314-
for i := 0; i < len(configMaps)-1; i++ {
315-
for j := i + 1; j < len(configMaps); j++ {
316-
if configMaps[i].Name > configMaps[j].Name {
317-
configMaps[i], configMaps[j] = configMaps[j], configMaps[i]
318-
}
319-
}
312+
// Ensure only one ConfigMap matches the selector
313+
if len(configMapList.Items) > 1 {
314+
return fmt.Errorf("multiple ConfigMaps match the provider selector, only one ConfigMap per provider is allowed")
320315
}
321316

322-
// Add each ConfigMap's data to the hash
323-
for _, cm := range configMaps {
317+
// Add the ConfigMap's data to the hash (if any ConfigMap exists)
318+
if len(configMapList.Items) == 1 {
319+
cm := configMapList.Items[0]
324320
if err := addObjectToHash(hash, cm.Data); err != nil {
325321
return err
326322
}

0 commit comments

Comments
 (0)