Skip to content

Commit 9f84d7e

Browse files
committed
Add tests to cover ApplyOnce reentry and idempotency
1 parent 2891a9c commit 9f84d7e

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

exp/addons/internal/controllers/clusterresourceset_controller_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package controllers
1818

1919
import (
20+
"crypto/sha1"
2021
"fmt"
2122
"reflect"
2223
"testing"
@@ -34,6 +35,7 @@ import (
3435
addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1"
3536
"sigs.k8s.io/cluster-api/internal/test/envtest"
3637
"sigs.k8s.io/cluster-api/util"
38+
"sigs.k8s.io/cluster-api/util/conditions"
3739
)
3840

3941
const (
@@ -724,6 +726,195 @@ metadata:
724726
t.Log("Checking resource ConfigMap 2 has been updated")
725727
g.Eventually(configMapHasBeenUpdated(env, resourceConfigMap2Key, resourceConfigMap2), timeout).Should(Succeed())
726728
})
729+
730+
t.Run("Should reconcile a ClusterResourceSet with ApplyOnce strategy even when one of the resources already exist", func(t *testing.T) {
731+
g := NewWithT(t)
732+
ns := setup(t, g)
733+
defer teardown(t, g, ns)
734+
735+
t.Log("Updating the cluster with labels")
736+
testCluster.SetLabels(labels)
737+
g.Expect(env.Update(ctx, testCluster)).To(Succeed())
738+
739+
t.Log("Creating resource CM before creating CRS")
740+
// This CM is defined in the data of "configmapName", which is included in the
741+
// CRS we will create in this test
742+
resourceConfigMap1 := configMap(
743+
resourceConfigMap1Name,
744+
resourceConfigMapsNamespace,
745+
map[string]string{
746+
"created": "before CRS",
747+
},
748+
)
749+
g.Expect(env.Create(ctx, resourceConfigMap1)).To(Succeed())
750+
751+
t.Log("Creating a ClusterResourceSet instance that has same labels as selector")
752+
clusterResourceSet := &addonsv1.ClusterResourceSet{
753+
ObjectMeta: metav1.ObjectMeta{
754+
Name: clusterResourceSetName,
755+
Namespace: ns.Name,
756+
},
757+
Spec: addonsv1.ClusterResourceSetSpec{
758+
Strategy: string(addonsv1.ClusterResourceSetStrategyApplyOnce),
759+
ClusterSelector: metav1.LabelSelector{
760+
MatchLabels: labels,
761+
},
762+
Resources: []addonsv1.ResourceRef{{Name: configmapName, Kind: "ConfigMap"}, {Name: secretName, Kind: "Secret"}},
763+
},
764+
}
765+
766+
g.Expect(env.Create(ctx, clusterResourceSet)).To(Succeed())
767+
768+
t.Log("Checking resource ConfigMap 1 hasn't been updated")
769+
resourceConfigMap1Key := client.ObjectKey{
770+
Namespace: resourceConfigMapsNamespace,
771+
Name: resourceConfigMap1Name,
772+
}
773+
g.Eventually(configMapHasBeenUpdated(env, resourceConfigMap1Key, resourceConfigMap1), timeout).Should(Succeed())
774+
775+
t.Log("Verifying resource ConfigMap 2 has been created")
776+
resourceConfigMap2Key := client.ObjectKey{
777+
Namespace: resourceConfigMapsNamespace,
778+
Name: resourceConfigMap2Name,
779+
}
780+
g.Eventually(func() error {
781+
cm := &corev1.ConfigMap{}
782+
return env.Get(ctx, resourceConfigMap2Key, cm)
783+
}, timeout).Should(Succeed())
784+
})
785+
786+
t.Run("Should reconcile a ClusterResourceSet with ApplyOnce strategy even when there is an error, after the error has been corrected", func(t *testing.T) {
787+
// To trigger an error in the middle of the reconciliation, we'll define a an object in a namespace that doesn't yet exist.
788+
// We'll expect the CRS to reconcile all other objects except that one and bubble up the error.
789+
// Once that happens, we'll go ahead a create the namespace. Then we'll expect the CRS to, eventually, create that remaining object.
790+
791+
g := NewWithT(t)
792+
ns := setup(t, g)
793+
defer teardown(t, g, ns)
794+
795+
t.Log("Updating the cluster with labels")
796+
testCluster.SetLabels(labels)
797+
g.Expect(env.Update(ctx, testCluster)).To(Succeed())
798+
799+
t.Log("Updating the test config map with the missing namespace resource")
800+
missingNamespace := randomNamespaceForTest(t)
801+
802+
resourceConfigMap1 := configMap(
803+
resourceConfigMap1Name,
804+
resourceConfigMapsNamespace,
805+
map[string]string{
806+
"my_new_config": "some_value",
807+
},
808+
)
809+
810+
resourceConfigMap1Content, err := yaml.Marshal(resourceConfigMap1)
811+
g.Expect(err).NotTo(HaveOccurred())
812+
813+
resourceConfigMapWithMissingNamespace := configMap(
814+
"cm-missing-namespace",
815+
missingNamespace,
816+
map[string]string{
817+
"my_new_config": "this is all new",
818+
},
819+
)
820+
821+
resourceConfigMapMissingNamespaceContent, err := yaml.Marshal(resourceConfigMapWithMissingNamespace)
822+
g.Expect(err).NotTo(HaveOccurred())
823+
824+
testConfigmap := configMap(
825+
configmapName,
826+
ns.Name,
827+
map[string]string{
828+
"cm": string(resourceConfigMap1Content),
829+
"problematic_cm": string(resourceConfigMapMissingNamespaceContent),
830+
},
831+
)
832+
833+
g.Expect(env.Update(ctx, testConfigmap)).To(Succeed())
834+
835+
t.Log("Creating a ClusterResourceSet instance that has same labels as selector")
836+
clusterResourceSet := &addonsv1.ClusterResourceSet{
837+
ObjectMeta: metav1.ObjectMeta{
838+
Name: clusterResourceSetName,
839+
Namespace: ns.Name,
840+
},
841+
Spec: addonsv1.ClusterResourceSetSpec{
842+
Strategy: string(addonsv1.ClusterResourceSetStrategyApplyOnce),
843+
ClusterSelector: metav1.LabelSelector{
844+
MatchLabels: labels,
845+
},
846+
Resources: []addonsv1.ResourceRef{{Name: testConfigmap.Name, Kind: "ConfigMap"}, {Name: secretName, Kind: "Secret"}},
847+
},
848+
}
849+
850+
g.Expect(env.Create(ctx, clusterResourceSet)).To(Succeed())
851+
852+
t.Log("Verifying resource ConfigMap 1 has been created")
853+
resourceConfigMap1Key := client.ObjectKeyFromObject(resourceConfigMap1)
854+
g.Eventually(configMapHasBeenUpdated(env, resourceConfigMap1Key, resourceConfigMap1), timeout).Should(Succeed())
855+
856+
t.Log("Verifying resource ConfigMap 2 has been created")
857+
resourceConfigMap2Key := client.ObjectKey{
858+
Namespace: resourceConfigMapsNamespace,
859+
Name: resourceConfigMap2Name,
860+
}
861+
g.Eventually(func() error {
862+
cm := &corev1.ConfigMap{}
863+
return env.Get(ctx, resourceConfigMap2Key, cm)
864+
}, timeout).Should(Succeed())
865+
866+
t.Log("Verifying CRS Binding failed marked the resource as not applied")
867+
g.Eventually(func(g Gomega) {
868+
clusterResourceSetBindingKey := client.ObjectKey{
869+
Namespace: testCluster.Namespace,
870+
Name: testCluster.Name,
871+
}
872+
binding := &addonsv1.ClusterResourceSetBinding{}
873+
g.Expect(env.Get(ctx, clusterResourceSetBindingKey, binding)).To(Succeed())
874+
875+
g.Expect(binding.Spec.Bindings).To(HaveLen(1))
876+
g.Expect(binding.Spec.Bindings[0].Resources).To(HaveLen(2))
877+
878+
for _, r := range binding.Spec.Bindings[0].Resources {
879+
switch r.ResourceRef.Name {
880+
case testConfigmap.Name:
881+
g.Expect(r.Applied).To(BeFalse(), "test-configmap should be not applied bc of missing namespace")
882+
case secretName:
883+
g.Expect(r.Applied).To(BeTrue(), "test-secret should be applied")
884+
}
885+
}
886+
}, timeout).Should(Succeed())
887+
888+
t.Log("Verifying CRS has a false ResourcesApplied condition")
889+
g.Eventually(func(g Gomega) {
890+
clusterResourceSetKey := client.ObjectKeyFromObject(clusterResourceSet)
891+
crs := &addonsv1.ClusterResourceSet{}
892+
g.Expect(env.Get(ctx, clusterResourceSetKey, crs)).To(Succeed())
893+
894+
appliedCondition := conditions.Get(crs, addonsv1.ResourcesAppliedCondition)
895+
g.Expect(appliedCondition).NotTo(BeNil())
896+
g.Expect(appliedCondition.Status).To(Equal(corev1.ConditionFalse))
897+
g.Expect(appliedCondition.Reason).To(Equal(addonsv1.ApplyFailedReason))
898+
g.Expect(appliedCondition.Message).To(ContainSubstring("creating object /v1, Kind=ConfigMap %s/cm-missing-namespace", missingNamespace))
899+
}, timeout).Should(Succeed())
900+
901+
t.Log("Creating missing namespace")
902+
missingNs := &corev1.Namespace{
903+
ObjectMeta: metav1.ObjectMeta{
904+
Name: missingNamespace,
905+
},
906+
}
907+
g.Expect(env.Create(ctx, missingNs)).To(Succeed())
908+
909+
t.Log("Verifying CRS Binding has all resources applied")
910+
g.Eventually(clusterResourceSetBindingReady(env, testCluster), timeout).Should(BeTrue())
911+
912+
t.Log("Verifying resource ConfigMap with previsouly missing namespace has been created")
913+
g.Eventually(configMapHasBeenUpdated(env, client.ObjectKeyFromObject(resourceConfigMapWithMissingNamespace), resourceConfigMapWithMissingNamespace), timeout).Should(Succeed())
914+
915+
g.Expect(env.Delete(ctx, resourceConfigMapWithMissingNamespace)).To(Succeed())
916+
g.Expect(env.Delete(ctx, missingNs)).To(Succeed())
917+
})
727918
}
728919

729920
func clusterResourceSetBindingReady(env *envtest.Environment, cluster *clusterv1.Cluster) func() bool {
@@ -786,3 +977,10 @@ func configMap(name, namespace string, data map[string]string) *corev1.ConfigMap
786977
Data: data,
787978
}
788979
}
980+
981+
func randomNamespaceForTest(t testing.TB) string {
982+
h := sha1.New()
983+
h.Write([]byte(t.Name()))
984+
testNameHash := fmt.Sprintf("%x", h.Sum(nil))
985+
return "ns-" + testNameHash[:7] + "-" + util.RandomString(6)
986+
}

exp/addons/internal/controllers/clusterresourceset_scope.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ func (r *reconcileApplyOnceScope) applyObj(ctx context.Context, c client.Client,
167167
if err := createUnstructured(ctx, c, obj); err != nil && !apierrors.IsAlreadyExists(err) {
168168
return err
169169
}
170+
170171
return nil
171172
}
172173

@@ -180,5 +181,6 @@ func apply(ctx context.Context, c client.Client, applyObj applyObj, objs []unstr
180181
errList = append(errList, err)
181182
}
182183
}
184+
183185
return kerrors.NewAggregate(errList)
184186
}

0 commit comments

Comments
 (0)