1
16
17 package patches
18
19 import (
20 "bytes"
21 "io"
22 "os"
23 "path/filepath"
24 "reflect"
25 "testing"
26
27 utiltesting "k8s.io/client-go/util/testing"
28
29 "github.com/pkg/errors"
30
31 v1 "k8s.io/api/core/v1"
32 "k8s.io/apimachinery/pkg/types"
33 )
34
35 var testKnownTargets = []string{
36 "etcd",
37 "kube-apiserver",
38 "kube-controller-manager",
39 "kube-scheduler",
40 "kubeletconfiguration",
41 }
42
43 const testDirPattern = "patch-files"
44
45 func TestParseFilename(t *testing.T) {
46 tests := []struct {
47 name string
48 fileName string
49 expectedTargetName string
50 expectedPatchType types.PatchType
51 expectedWarning bool
52 expectedError bool
53 }{
54 {
55 name: "valid: known target and patch type",
56 fileName: "etcd+merge.json",
57 expectedTargetName: "etcd",
58 expectedPatchType: types.MergePatchType,
59 },
60 {
61 name: "valid: known target and default patch type",
62 fileName: "etcd0.yaml",
63 expectedTargetName: "etcd",
64 expectedPatchType: types.StrategicMergePatchType,
65 },
66 {
67 name: "valid: known target and custom patch type",
68 fileName: "etcd0+merge.yaml",
69 expectedTargetName: "etcd",
70 expectedPatchType: types.MergePatchType,
71 },
72 {
73 name: "invalid: unknown target",
74 fileName: "foo.yaml",
75 expectedWarning: true,
76 },
77 {
78 name: "invalid: unknown extension",
79 fileName: "etcd.foo",
80 expectedWarning: true,
81 },
82 {
83 name: "invalid: missing extension",
84 fileName: "etcd",
85 expectedWarning: true,
86 },
87 {
88 name: "invalid: unknown patch type",
89 fileName: "etcd+foo.json",
90 expectedError: true,
91 },
92 {
93 name: "invalid: missing patch type",
94 fileName: "etcd+.json",
95 expectedError: true,
96 },
97 }
98
99 for _, tc := range tests {
100 t.Run(tc.name, func(t *testing.T) {
101 targetName, patchType, warn, err := parseFilename(tc.fileName, testKnownTargets)
102 if (err != nil) != tc.expectedError {
103 t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
104 }
105 if (warn != nil) != tc.expectedWarning {
106 t.Errorf("expected warning: %v, got: %v, warning: %v", tc.expectedWarning, warn != nil, warn)
107 }
108 if targetName != tc.expectedTargetName {
109 t.Errorf("expected target name: %v, got: %v", tc.expectedTargetName, targetName)
110 }
111 if patchType != tc.expectedPatchType {
112 t.Errorf("expected patch type: %v, got: %v", tc.expectedPatchType, patchType)
113 }
114 })
115 }
116 }
117
118 func TestCreatePatchSet(t *testing.T) {
119 tests := []struct {
120 name string
121 targetName string
122 patchType types.PatchType
123 expectedPatchSet *patchSet
124 data string
125 }{
126 {
127
128 name: "valid: YAML patches are separated and converted to JSON",
129 targetName: "etcd",
130 patchType: types.StrategicMergePatchType,
131 data: "foo: bar\n---\nfoo: baz\n",
132 expectedPatchSet: &patchSet{
133 targetName: "etcd",
134 patchType: types.StrategicMergePatchType,
135 patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
136 },
137 },
138 {
139 name: "valid: JSON patches are separated",
140 targetName: "etcd",
141 patchType: types.StrategicMergePatchType,
142 data: `{"foo":"bar"}` + "\n---\n" + `{"foo":"baz"}`,
143 expectedPatchSet: &patchSet{
144 targetName: "etcd",
145 patchType: types.StrategicMergePatchType,
146 patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
147 },
148 },
149 {
150 name: "valid: empty patches are ignored",
151 targetName: "etcd",
152 patchType: types.StrategicMergePatchType,
153 data: `{"foo":"bar"}` + "\n---\n ---\n" + `{"foo":"baz"}`,
154 expectedPatchSet: &patchSet{
155 targetName: "etcd",
156 patchType: types.StrategicMergePatchType,
157 patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
158 },
159 },
160 }
161
162 for _, tc := range tests {
163 t.Run(tc.name, func(t *testing.T) {
164 ps, _ := createPatchSet(tc.targetName, tc.patchType, tc.data)
165 if !reflect.DeepEqual(ps, tc.expectedPatchSet) {
166 t.Fatalf("expected patch set:\n%+v\ngot:\n%+v\n", tc.expectedPatchSet, ps)
167 }
168 })
169 }
170 }
171
172 func TestGetPatchSetsForPathMustBeDirectory(t *testing.T) {
173 tempFile, err := os.CreateTemp("", "test-file")
174 if err != nil {
175 t.Errorf("error creating temporary file: %v", err)
176 }
177 defer utiltesting.CloseAndRemove(t, tempFile)
178
179 _, _, _, err = getPatchSetsFromPath(tempFile.Name(), testKnownTargets, io.Discard)
180 var pathErr *os.PathError
181 if !errors.As(err, &pathErr) {
182 t.Fatalf("expected os.PathError for non-directory path %q, but got %v", tempFile.Name(), err)
183 }
184 }
185
186 func TestGetPatchSetsForPath(t *testing.T) {
187 const patchData = `{"foo":"bar"}`
188
189 tests := []struct {
190 name string
191 filesToWrite []string
192 expectedPatchSets []*patchSet
193 expectedPatchFiles []string
194 expectedIgnoredFiles []string
195 expectedError bool
196 patchData string
197 }{
198 {
199 name: "valid: patch files are sorted and non-patch files are ignored",
200 filesToWrite: []string{"kube-scheduler+merge.json", "kube-apiserver+json.yaml", "etcd.yaml", "foo", "bar.json"},
201 patchData: patchData,
202 expectedPatchSets: []*patchSet{
203 {
204 targetName: "etcd",
205 patchType: types.StrategicMergePatchType,
206 patches: []string{patchData},
207 },
208 {
209 targetName: "kube-apiserver",
210 patchType: types.JSONPatchType,
211 patches: []string{patchData},
212 },
213 {
214 targetName: "kube-scheduler",
215 patchType: types.MergePatchType,
216 patches: []string{patchData},
217 },
218 },
219 expectedPatchFiles: []string{"etcd.yaml", "kube-apiserver+json.yaml", "kube-scheduler+merge.json"},
220 expectedIgnoredFiles: []string{"bar.json", "foo"},
221 },
222 {
223 name: "valid: empty files are ignored",
224 patchData: "",
225 filesToWrite: []string{"kube-scheduler.json"},
226 expectedPatchFiles: []string{},
227 expectedIgnoredFiles: []string{"kube-scheduler.json"},
228 expectedPatchSets: []*patchSet{},
229 },
230 {
231 name: "invalid: bad patch type in filename returns and error",
232 filesToWrite: []string{"kube-scheduler+foo.json"},
233 expectedError: true,
234 },
235 }
236
237 for _, tc := range tests {
238 t.Run(tc.name, func(t *testing.T) {
239 tempDir, err := os.MkdirTemp("", testDirPattern)
240 if err != nil {
241 t.Fatal(err)
242 }
243 defer os.RemoveAll(tempDir)
244
245 for _, file := range tc.filesToWrite {
246 filePath := filepath.Join(tempDir, file)
247 err := os.WriteFile(filePath, []byte(tc.patchData), 0644)
248 if err != nil {
249 t.Fatalf("could not write temporary file %q", filePath)
250 }
251 }
252
253 patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(tempDir, testKnownTargets, io.Discard)
254 if (err != nil) != tc.expectedError {
255 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
256 }
257
258 if !reflect.DeepEqual(tc.expectedPatchFiles, patchFiles) {
259 t.Fatalf("expected patch files:\n%+v\ngot:\n%+v", tc.expectedPatchFiles, patchFiles)
260 }
261 if !reflect.DeepEqual(tc.expectedIgnoredFiles, ignoredFiles) {
262 t.Fatalf("expected ignored files:\n%+v\ngot:\n%+v", tc.expectedIgnoredFiles, ignoredFiles)
263 }
264 if !reflect.DeepEqual(tc.expectedPatchSets, patchSets) {
265 t.Fatalf("expected patch sets:\n%+v\ngot:\n%+v", tc.expectedPatchSets, patchSets)
266 }
267 })
268 }
269 }
270
271 func TestGetPatchManagerForPath(t *testing.T) {
272 type file struct {
273 name string
274 data string
275 }
276
277 tests := []struct {
278 name string
279 files []*file
280 patchTarget *PatchTarget
281 expectedData []byte
282 expectedError bool
283 }{
284 {
285 name: "valid: patch a kube-apiserver target using merge patch; json patch is applied first",
286 patchTarget: &PatchTarget{
287 Name: "kube-apiserver",
288 StrategicMergePatchObject: v1.Pod{},
289 Data: []byte("foo: bar\nbaz: qux\n"),
290 },
291 expectedData: []byte(`{"baz":"qux","foo":"patched"}`),
292 files: []*file{
293 {
294 name: "kube-apiserver+merge.yaml",
295 data: "foo: patched",
296 },
297 {
298 name: "kube-apiserver+json.json",
299 data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
300 },
301 },
302 },
303 {
304 name: "valid: kube-apiserver target is patched with json patch",
305 patchTarget: &PatchTarget{
306 Name: "kube-apiserver",
307 StrategicMergePatchObject: v1.Pod{},
308 Data: []byte("foo: bar\n"),
309 },
310 expectedData: []byte(`{"foo":"zzz"}`),
311 files: []*file{
312 {
313 name: "kube-apiserver+json.json",
314 data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
315 },
316 },
317 },
318 {
319 name: "valid: kubeletconfiguration target is patched with json patch",
320 patchTarget: &PatchTarget{
321 Name: "kubeletconfiguration",
322 StrategicMergePatchObject: nil,
323 Data: []byte("foo: bar\n"),
324 },
325 expectedData: []byte(`{"foo":"zzz"}`),
326 files: []*file{
327 {
328 name: "kubeletconfiguration+json.json",
329 data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
330 },
331 },
332 },
333 {
334 name: "valid: kube-apiserver target is patched with strategic merge patch",
335 patchTarget: &PatchTarget{
336 Name: "kube-apiserver",
337 StrategicMergePatchObject: v1.Pod{},
338 Data: []byte("foo: bar\n"),
339 },
340 expectedData: []byte(`{"foo":"zzz"}`),
341 files: []*file{
342 {
343 name: "kube-apiserver+strategic.json",
344 data: `{"foo":"zzz"}`,
345 },
346 },
347 },
348 {
349 name: "valid: etcd target is not changed because there are no patches for it",
350 patchTarget: &PatchTarget{
351 Name: "etcd",
352 StrategicMergePatchObject: v1.Pod{},
353 Data: []byte("foo: bar\n"),
354 },
355 expectedData: []byte("foo: bar\n"),
356 files: []*file{
357 {
358 name: "kube-apiserver+merge.yaml",
359 data: "foo: patched",
360 },
361 },
362 },
363 {
364 name: "invalid: cannot patch etcd target due to malformed json patch",
365 patchTarget: &PatchTarget{
366 Name: "etcd",
367 StrategicMergePatchObject: v1.Pod{},
368 Data: []byte("foo: bar\n"),
369 },
370 files: []*file{
371 {
372 name: "etcd+json.json",
373 data: `{"foo":"zzz"}`,
374 },
375 },
376 expectedError: true,
377 },
378 }
379
380 for _, tc := range tests {
381 t.Run(tc.name, func(t *testing.T) {
382 tempDir, err := os.MkdirTemp("", testDirPattern)
383 if err != nil {
384 t.Fatal(err)
385 }
386 defer os.RemoveAll(tempDir)
387
388 for _, file := range tc.files {
389 filePath := filepath.Join(tempDir, file.name)
390 err := os.WriteFile(filePath, []byte(file.data), 0644)
391 if err != nil {
392 t.Fatalf("could not write temporary file %q", filePath)
393 }
394 }
395
396 pm, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
397 if err != nil {
398 t.Fatal(err)
399 }
400
401 err = pm.ApplyPatchesToTarget(tc.patchTarget)
402 if (err != nil) != tc.expectedError {
403 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
404 }
405 if err != nil {
406 return
407 }
408
409 if !bytes.Equal(tc.patchTarget.Data, tc.expectedData) {
410 t.Fatalf("expected result:\n%s\ngot:\n%s", tc.expectedData, tc.patchTarget.Data)
411 }
412 })
413 }
414 }
415
416 func TestGetPatchManagerForPathCache(t *testing.T) {
417 tempDir, err := os.MkdirTemp("", testDirPattern)
418 if err != nil {
419 t.Fatal(err)
420 }
421 defer os.RemoveAll(tempDir)
422
423 pmOld, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
424 if err != nil {
425 t.Fatal(err)
426 }
427 pmNew, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
428 if err != nil {
429 t.Fatal(err)
430 }
431 if pmOld != pmNew {
432 t.Logf("path %q was not cached, expected pointer: %p, got: %p", tempDir, pmOld, pmNew)
433 }
434 }
435
View as plain text