Skip to content

Commit b0c2f62

Browse files
committed
tests: Add initial ObjectHandler tests
1 parent f515dc9 commit b0c2f62

File tree

2 files changed

+454
-9
lines changed

2 files changed

+454
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
Copyright 2022.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"fmt"
21+
v1 "k8s.io/api/core/v1"
22+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"math/rand/v2"
25+
client2 "sigs.k8s.io/controller-runtime/pkg/client"
26+
"time"
27+
28+
templatesv1alpha1 "github.com/kluctl/template-controller/api/v1alpha1"
29+
. "github.com/onsi/ginkgo/v2"
30+
. "github.com/onsi/gomega"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
)
33+
34+
func buildTestConfigMap(name string, namespace string, data map[string]string) *unstructured.Unstructured {
35+
m := map[string]any{}
36+
for k, v := range data {
37+
m[k] = v
38+
}
39+
return &unstructured.Unstructured{
40+
Object: map[string]any{
41+
"apiVersion": "v1",
42+
"kind": "ConfigMap",
43+
"metadata": map[string]any{
44+
"name": name,
45+
"namespace": namespace,
46+
},
47+
"data": m,
48+
},
49+
}
50+
}
51+
52+
func buildTestSecret(name string, namespace string, data map[string]string) *unstructured.Unstructured {
53+
m := map[string]any{}
54+
for k, v := range data {
55+
m[k] = v
56+
}
57+
return &unstructured.Unstructured{
58+
Object: map[string]any{
59+
"apiVersion": "v1",
60+
"kind": "Secret",
61+
"metadata": map[string]any{
62+
"name": name,
63+
"namespace": namespace,
64+
},
65+
"stringData": m,
66+
},
67+
}
68+
}
69+
70+
func buildObjectTemplate(name string, namespace string, matrixEntries []*templatesv1alpha1.MatrixEntry, templates []templatesv1alpha1.Template) *templatesv1alpha1.ObjectTemplate {
71+
t := &templatesv1alpha1.ObjectTemplate{
72+
TypeMeta: metav1.TypeMeta{
73+
APIVersion: templatesv1alpha1.GroupVersion.String(),
74+
Kind: "ObjectTemplate",
75+
},
76+
ObjectMeta: metav1.ObjectMeta{
77+
Name: name,
78+
Namespace: namespace,
79+
},
80+
Spec: templatesv1alpha1.ObjectTemplateSpec{
81+
Interval: metav1.Duration{Duration: time.Second},
82+
Matrix: matrixEntries,
83+
Templates: templates,
84+
},
85+
}
86+
return t
87+
}
88+
89+
func buildMatrixListEntry(name string) *templatesv1alpha1.MatrixEntry {
90+
return &templatesv1alpha1.MatrixEntry{
91+
Name: name,
92+
List: []runtime.RawExtension{
93+
{
94+
Raw: []byte(`{"k1": 1, "k2": 2}`),
95+
},
96+
},
97+
}
98+
}
99+
100+
func buildMatrixObjectEntry(name string, objName string, objNamespace string, objKind string, jsonPath string, expandLists bool) *templatesv1alpha1.MatrixEntry {
101+
var jsonPathPtr *string
102+
if jsonPath != "" {
103+
jsonPathPtr = &jsonPath
104+
}
105+
return &templatesv1alpha1.MatrixEntry{
106+
Name: name,
107+
Object: &templatesv1alpha1.MatrixEntryObject{
108+
Ref: templatesv1alpha1.ObjectRef{
109+
APIVersion: "v1",
110+
Kind: objKind,
111+
Name: objName,
112+
Namespace: objNamespace,
113+
},
114+
JsonPath: jsonPathPtr,
115+
ExpandLists: expandLists,
116+
},
117+
}
118+
}
119+
120+
func updateObjectTemplate(key client2.ObjectKey, fn func(t *templatesv1alpha1.ObjectTemplate)) {
121+
t := getObjectTemplate(key)
122+
fn(t)
123+
err := k8sClient.Update(ctx, t, client2.FieldOwner("tests"))
124+
Expect(err).To(Succeed())
125+
}
126+
127+
func triggerReconcile(key client2.ObjectKey) {
128+
updateObjectTemplate(key, func(t *templatesv1alpha1.ObjectTemplate) {
129+
t.Spec.Interval.Duration += time.Millisecond
130+
})
131+
}
132+
133+
func waitUntiReconciled(key client2.ObjectKey, timeout time.Duration) {
134+
Eventually(func() bool {
135+
t := getObjectTemplate(key)
136+
if t == nil {
137+
return false
138+
}
139+
c := getReadyCondition(t.GetConditions())
140+
if c == nil {
141+
return false
142+
}
143+
return c.ObservedGeneration == t.Generation
144+
}, timeout, time.Millisecond*250).Should(BeTrue())
145+
}
146+
147+
func getObjectTemplate(key client2.ObjectKey) *templatesv1alpha1.ObjectTemplate {
148+
var t templatesv1alpha1.ObjectTemplate
149+
err := k8sClient.Get(ctx, key, &t)
150+
if err != nil {
151+
return nil
152+
}
153+
return &t
154+
}
155+
156+
func assertAppliedConfigMaps(key client2.ObjectKey, keys ...client2.ObjectKey) {
157+
t := getObjectTemplate(key)
158+
Expect(t).ToNot(BeNil())
159+
160+
var found []client2.ObjectKey
161+
for _, as := range t.Status.AppliedResources {
162+
if as.Success {
163+
found = append(found, client2.ObjectKey{Name: as.Ref.Name, Namespace: as.Ref.Namespace})
164+
}
165+
}
166+
167+
Expect(found).To(ConsistOf(keys))
168+
}
169+
170+
func assertFailedConfigMaps(key client2.ObjectKey, keys ...client2.ObjectKey) {
171+
t := getObjectTemplate(key)
172+
Expect(t).ToNot(BeNil())
173+
174+
var found []client2.ObjectKey
175+
for _, as := range t.Status.AppliedResources {
176+
if !as.Success {
177+
found = append(found, client2.ObjectKey{Name: as.Ref.Name, Namespace: as.Ref.Namespace})
178+
}
179+
}
180+
181+
Expect(found).To(ConsistOf(keys))
182+
}
183+
184+
func assertFailedConfigMap(key client2.ObjectKey, cmKey client2.ObjectKey, errStr string) {
185+
t := getObjectTemplate(key)
186+
Expect(t).ToNot(BeNil())
187+
for _, as := range t.Status.AppliedResources {
188+
if !as.Success && as.Ref.Name == cmKey.Name && as.Ref.Namespace == cmKey.Namespace {
189+
Expect(as.Error).To(ContainSubstring(errStr))
190+
return
191+
}
192+
}
193+
Expect(false).To(BeTrue())
194+
}
195+
196+
var _ = Describe("ObjectTemplate controller", func() {
197+
const (
198+
timeout = time.Second * 1000
199+
duration = time.Second * 10
200+
interval = time.Millisecond * 250
201+
)
202+
203+
Context("Template without permissions to write object", func() {
204+
ns := fmt.Sprintf("test-%d", rand.Int64())
205+
206+
key := client2.ObjectKey{Name: "t1", Namespace: ns}
207+
cmKey := client2.ObjectKey{Name: "cm1", Namespace: ns}
208+
209+
t := buildObjectTemplate(key.Name, key.Namespace,
210+
[]*templatesv1alpha1.MatrixEntry{buildMatrixListEntry("m1")},
211+
[]templatesv1alpha1.Template{
212+
{Object: buildTestConfigMap(cmKey.Name, cmKey.Namespace, map[string]string{
213+
"k1": `{{ matrix.m1.k1 + matrix.m1.k2 }}`,
214+
})},
215+
})
216+
217+
It("Should fail initially", func() {
218+
createNamespace(ns)
219+
createServiceAccount("default", ns)
220+
221+
Expect(k8sClient.Create(ctx, t)).Should(Succeed())
222+
waitUntiReconciled(key, timeout)
223+
224+
Consistently(func() error {
225+
return k8sClient.Get(ctx, cmKey, &v1.ConfigMap{})
226+
}, "2s").Should(MatchError("configmaps \"cm1\" not found"))
227+
228+
assertFailedConfigMaps(key, cmKey)
229+
})
230+
It("Should succeed when RBAC is added", func() {
231+
createRoleWithBinding("default", ns, []string{"configmaps"})
232+
233+
triggerReconcile(key)
234+
waitUntiReconciled(key, timeout)
235+
236+
assertAppliedConfigMaps(key, cmKey)
237+
238+
var cm v1.ConfigMap
239+
err := k8sClient.Get(ctx, cmKey, &cm)
240+
Expect(err).To(Succeed())
241+
242+
Expect(cm.Data).To(Equal(map[string]string{
243+
"k1": "3",
244+
}))
245+
})
246+
It("Should fail with non-existing SA", func() {
247+
updateObjectTemplate(key, func(t *templatesv1alpha1.ObjectTemplate) {
248+
t.Spec.ServiceAccountName = "non-existent"
249+
})
250+
waitUntiReconciled(key, timeout)
251+
252+
assertFailedConfigMap(key, cmKey, "configmaps \"cm1\" is forbidden")
253+
})
254+
It("Should succeed after the SA is being created", func() {
255+
createServiceAccount("non-existent", ns)
256+
createRoleWithBinding("non-existent", ns, []string{"configmaps"})
257+
triggerReconcile(key)
258+
waitUntiReconciled(key, timeout)
259+
assertAppliedConfigMaps(key, cmKey)
260+
})
261+
})
262+
Context("Template without permissions to read matrix object", func() {
263+
ns := fmt.Sprintf("test-%d", rand.Int64())
264+
265+
key := client2.ObjectKey{Name: "t1", Namespace: ns}
266+
cmKey := client2.ObjectKey{Name: "cm1", Namespace: ns}
267+
268+
It("Should fail initially", func() {
269+
createNamespace(ns)
270+
createServiceAccount("default", ns)
271+
createRoleWithBinding("default", ns, []string{"configmaps"})
272+
273+
t := buildObjectTemplate(key.Name, key.Namespace,
274+
[]*templatesv1alpha1.MatrixEntry{
275+
buildMatrixObjectEntry("m1", "m1", ns, "Secret", "", false),
276+
},
277+
[]templatesv1alpha1.Template{
278+
{Object: buildTestConfigMap(cmKey.Name, cmKey.Namespace, map[string]string{
279+
"k1": `{{ matrix.m1.k1 + matrix.m1.k2 }}`,
280+
})},
281+
})
282+
283+
err := k8sClient.Create(ctx, buildTestSecret("m1", ns, map[string]string{
284+
"k1": "1",
285+
"k2": "2",
286+
}))
287+
Expect(err).To(Succeed())
288+
289+
Expect(k8sClient.Create(ctx, t)).Should(Succeed())
290+
waitUntiReconciled(key, timeout)
291+
292+
t2 := getObjectTemplate(key)
293+
c := getReadyCondition(t2.GetConditions())
294+
Expect(c.Status).To(Equal(metav1.ConditionFalse))
295+
Expect(c.Message).To(ContainSubstring("secrets \"m1\" is forbidden"))
296+
})
297+
It("Should succeed when RBAC is created", func() {
298+
createRoleWithBinding("default", ns, []string{"secrets"})
299+
triggerReconcile(key)
300+
waitUntiReconciled(key, timeout)
301+
assertAppliedConfigMaps(key, cmKey)
302+
})
303+
It("Should fail with non-existing SA", func() {
304+
updateObjectTemplate(key, func(t *templatesv1alpha1.ObjectTemplate) {
305+
t.Spec.ServiceAccountName = "non-existent"
306+
})
307+
waitUntiReconciled(key, timeout)
308+
309+
t2 := getObjectTemplate(key)
310+
c := getReadyCondition(t2.GetConditions())
311+
Expect(c.Status).To(Equal(metav1.ConditionFalse))
312+
Expect(c.Message).To(ContainSubstring("secrets \"m1\" is forbidden"))
313+
})
314+
It("Should succeed after the SA is being created", func() {
315+
createServiceAccount("non-existent", ns)
316+
createRoleWithBinding("non-existent", ns, []string{"configmaps"})
317+
triggerReconcile(key)
318+
waitUntiReconciled(key, timeout)
319+
assertAppliedConfigMaps(key, cmKey)
320+
})
321+
})
322+
})

0 commit comments

Comments
 (0)