1
2
3
4
5 package mutation
6
7 import (
8 "testing"
9
10 "github.com/stretchr/testify/require"
11 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
12 ktestutil "sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
13 )
14
15 var configmap1y = `
16 apiVersion: v1
17 kind: ConfigMap
18 metadata:
19 name: map1-name
20 namespace: map-namespace
21 annotations:
22 config.kubernetes.io/apply-time-mutation: |
23 - sourcePath: $.status.number
24 sourceRef:
25 group: resourcemanager.cnrm.cloud.google.com
26 kind: Project
27 name: example-name
28 namespace: example-namespace
29 targetPath: $.spec.member
30 token: ${project-number}
31 data: {}
32 `
33
34 var m1 = ApplyTimeMutation{
35 {
36 SourceRef: ResourceReference{
37 Group: "resourcemanager.cnrm.cloud.google.com",
38 Kind: "Project",
39 Name: "example-name",
40 Namespace: "example-namespace",
41 },
42 SourcePath: "$.status.number",
43 TargetPath: "$.spec.member",
44 Token: "${project-number}",
45 },
46 }
47
48 var configmap2y = `
49 apiVersion: v1
50 kind: ConfigMap
51 metadata:
52 name: map1-name
53 namespace: map-namespace
54 annotations:
55 config.kubernetes.io/apply-time-mutation: |
56 - sourcePath: .status.field
57 sourceRef:
58 kind: ConfigMap
59 name: example-name
60 targetPath: .spec.field
61 data: {}
62 `
63
64 var m2 = ApplyTimeMutation{
65 {
66 SourceRef: ResourceReference{
67 Group: "",
68 Kind: "ConfigMap",
69 Name: "example-name",
70 },
71 SourcePath: ".status.field",
72 TargetPath: ".spec.field",
73 },
74 }
75
76
77 var u1j = &unstructured.Unstructured{
78 Object: map[string]interface{}{
79 "apiVersion": "v1",
80 "kind": "ConfigMap",
81 "metadata": map[string]interface{}{
82 "name": "unused",
83 "namespace": "unused",
84 "annotations": map[string]interface{}{
85 Annotation: `[` +
86 `{` +
87 `"sourceRef":{` +
88 `"group":"resourcemanager.cnrm.cloud.google.com",` +
89 `"kind":"Project",` +
90 `"name":"example-name",` +
91 `"namespace":"example-namespace"` +
92 `},` +
93 `"sourcePath": "$.status.number",` +
94 `"targetPath": "$.spec.member",` +
95 `"token": "${project-number}"` +
96 `},` +
97 `]`,
98 },
99 },
100 },
101 }
102
103
104 var configmap3y = `
105 apiVersion: v1
106 kind: ConfigMap
107 metadata:
108 name: map1-name
109 namespace: map-namespace
110 annotations:
111 config.kubernetes.io/apply-time-mutation: |
112 - sourcePath: $.status.number
113 sourceRef:
114 group: resourcemanager.cnrm.cloud.google.com
115 kind: Project
116 name: example-name
117 namespace: example-namespace
118 targetPath: $.spec.member
119 token: ${project-number}
120 - sourcePath: .status.field
121 sourceRef:
122 kind: ConfigMap
123 name: example-name
124 targetPath: .spec.field
125 data: {}
126 `
127
128
129 var configmap4y = `
130 apiVersion: v1
131 kind: ConfigMap
132 metadata:
133 name: map1-name
134 namespace: map-namespace
135 annotations:
136 config.kubernetes.io/apply-time-mutation: |
137 [
138 {
139 "sourceRef": {
140 "group": "resourcemanager.cnrm.cloud.google.com",
141 "kind": "Project",
142 "name": "example-name",
143 "namespace": "example-namespace"
144 },
145 "sourcePath": "$.status.number",
146 "targetPath": "$.spec.member",
147 "token": "${project-number}"
148 },
149 {
150 "sourceRef": {
151 "kind": "ConfigMap",
152 "name": "example-name"
153 },
154 "sourcePath": ".status.field",
155 "targetPath": ".spec.field"
156 }
157 ]
158 data: {}
159 `
160
161 var m3 = ApplyTimeMutation{
162 {
163 SourceRef: ResourceReference{
164 Group: "resourcemanager.cnrm.cloud.google.com",
165 Kind: "Project",
166 Name: "example-name",
167 Namespace: "example-namespace",
168 },
169 SourcePath: "$.status.number",
170 TargetPath: "$.spec.member",
171 Token: "${project-number}",
172 },
173 {
174 SourceRef: ResourceReference{
175 Group: "",
176 Kind: "ConfigMap",
177 Name: "example-name",
178 },
179 SourcePath: ".status.field",
180 TargetPath: ".spec.field",
181 },
182 }
183
184 var noAnnotationsYAML = `
185 apiVersion: v1
186 kind: ConfigMap
187 metadata:
188 name: map1-name
189 namespace: map-namespace
190 data: {}
191 `
192
193 var invalidAnnotationsYAML = `
194 apiVersion: v1
195 kind: ConfigMap
196 metadata:
197 name: map1-name
198 namespace: map-namespace
199 annotations:
200 config.kubernetes.io/apply-time-mutation: this string is not a substitution
201 data: {}
202 `
203
204 func TestReadAnnotation(t *testing.T) {
205 configmap1 := ktestutil.YamlToUnstructured(t, configmap1y)
206 configmap2 := ktestutil.YamlToUnstructured(t, configmap2y)
207 configmap3 := ktestutil.YamlToUnstructured(t, configmap3y)
208 configmap4 := ktestutil.YamlToUnstructured(t, configmap4y)
209 noAnnotations := ktestutil.YamlToUnstructured(t, noAnnotationsYAML)
210 invalidAnnotations := ktestutil.YamlToUnstructured(t, invalidAnnotationsYAML)
211
212 testCases := map[string]struct {
213 obj *unstructured.Unstructured
214 expected ApplyTimeMutation
215 isError bool
216 }{
217 "nil object is not found": {
218 obj: nil,
219 expected: ApplyTimeMutation{},
220 },
221 "Object with no annotations returns not found": {
222 obj: noAnnotations,
223 expected: ApplyTimeMutation{},
224 },
225 "Unparseable depends on annotation returns not found": {
226 obj: invalidAnnotations,
227 expected: ApplyTimeMutation{},
228 isError: true,
229 },
230 "Namespace-scoped object apply-time-mutation annotation yaml": {
231 obj: configmap1,
232 expected: m1,
233 },
234 "Namespace-scoped object apply-time-mutation annotation json": {
235 obj: u1j,
236 expected: m1,
237 },
238 "Cluster-scoped object apply-time-mutation annotation yaml": {
239 obj: configmap2,
240 expected: m2,
241 },
242 "Multiple objects specified in annotation yaml": {
243 obj: configmap3,
244 expected: m3,
245 },
246 "Multiple objects specified in annotation json": {
247 obj: configmap4,
248 expected: m3,
249 },
250 }
251
252 for tn, tc := range testCases {
253 t.Run(tn, func(t *testing.T) {
254 actual, err := ReadAnnotation(tc.obj)
255 if tc.isError {
256 if err == nil {
257 t.Fatalf("expected error not received")
258 }
259 } else {
260 if err != nil {
261 t.Fatalf("unexpected error received: %s", err)
262 }
263 if !actual.Equal(tc.expected) {
264 t.Errorf("\nexpected:\t%#v\nreceived:\t%#v", tc.expected, actual)
265 }
266 }
267 })
268 }
269 }
270
271 func TestWriteAnnotation(t *testing.T) {
272 configmap1 := ktestutil.YamlToUnstructured(t, configmap1y)
273 configmap2 := ktestutil.YamlToUnstructured(t, configmap2y)
274 configmap3 := ktestutil.YamlToUnstructured(t, configmap3y)
275
276 testCases := map[string]struct {
277 obj *unstructured.Unstructured
278 mutation ApplyTimeMutation
279 expected *string
280 isError bool
281 }{
282 "nil object": {
283 obj: nil,
284 mutation: ApplyTimeMutation{},
285 expected: nil,
286 isError: true,
287 },
288 "empty mutation": {
289 obj: &unstructured.Unstructured{},
290 mutation: ApplyTimeMutation{},
291 expected: nil,
292 isError: true,
293 },
294 "Namespace-scoped object": {
295 obj: &unstructured.Unstructured{},
296 mutation: m1,
297 expected: getApplyTimeMutation(configmap1),
298 },
299 "Cluster-scoped object": {
300 obj: &unstructured.Unstructured{},
301 mutation: m2,
302 expected: getApplyTimeMutation(configmap2),
303 },
304 "Multiple objects": {
305 obj: &unstructured.Unstructured{},
306 mutation: m3,
307 expected: getApplyTimeMutation(configmap3),
308 },
309 }
310
311 for tn, tc := range testCases {
312 t.Run(tn, func(t *testing.T) {
313 err := WriteAnnotation(tc.obj, tc.mutation)
314 if tc.isError {
315 if err == nil {
316 t.Fatalf("expected error not received")
317 }
318 } else {
319 if err != nil {
320 t.Fatalf("unexpected error received: %s", err)
321 }
322 received := getApplyTimeMutation(tc.obj)
323
324 if received != tc.expected && (received == nil || tc.expected == nil) {
325 t.Errorf("\nexpected:\t%#v\nreceived:\t%#v", tc.expected, received)
326 }
327
328 require.Equal(t, *tc.expected, *received, "unexpected mutation string")
329 }
330 })
331 }
332 }
333
334 func getApplyTimeMutation(obj *unstructured.Unstructured) *string {
335 value, found := obj.GetAnnotations()[Annotation]
336 if !found {
337 return nil
338 }
339 return &value
340 }
341
View as plain text