1
16
17 package diff
18
19 import (
20 "bytes"
21 "fmt"
22 "os"
23 "path"
24 "path/filepath"
25 "strings"
26 "testing"
27
28 "github.com/google/go-cmp/cmp"
29
30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/cli-runtime/pkg/genericiooptions"
33 "k8s.io/utils/exec"
34 )
35
36 type FakeObject struct {
37 name string
38 merged map[string]interface{}
39 live map[string]interface{}
40 }
41
42 var _ Object = &FakeObject{}
43
44 func (f *FakeObject) Name() string {
45 return f.name
46 }
47
48 func (f *FakeObject) Merged() (runtime.Object, error) {
49
50 if f.merged == nil {
51 return nil, nil
52 }
53 return &unstructured.Unstructured{Object: f.merged}, nil
54 }
55
56 func (f *FakeObject) Live() runtime.Object {
57
58 if f.live == nil {
59 return nil
60 }
61 return &unstructured.Unstructured{Object: f.live}
62 }
63
64 func TestDiffProgram(t *testing.T) {
65 externalDiffCommands := [3]string{"diff", "diff -ruN", "diff --report-identical-files"}
66
67 t.Setenv("LANG", "C")
68
69 for i, c := range externalDiffCommands {
70 t.Setenv("KUBECTL_EXTERNAL_DIFF", c)
71 streams, _, stdout, _ := genericiooptions.NewTestIOStreams()
72 diff := DiffProgram{
73 IOStreams: streams,
74 Exec: exec.New(),
75 }
76 err := diff.Run("/dev/zero", "/dev/zero")
77 if err != nil {
78 t.Fatal(err)
79 }
80
81
82 if i == 2 {
83 output_msg := "Files /dev/zero and /dev/zero are identical\n"
84 if output := stdout.String(); output != output_msg {
85 t.Fatalf(`stdout = %q, expected = %s"`, output, output_msg)
86 }
87 }
88 }
89 }
90
91 func TestPrinter(t *testing.T) {
92 printer := Printer{}
93
94 obj := &unstructured.Unstructured{Object: map[string]interface{}{
95 "string": "string",
96 "list": []int{1, 2, 3},
97 "int": 12,
98 }}
99 buf := bytes.Buffer{}
100 printer.Print(obj, &buf)
101 want := `int: 12
102 list:
103 - 1
104 - 2
105 - 3
106 string: string
107 `
108 if buf.String() != want {
109 t.Errorf("Print() = %q, want %q", buf.String(), want)
110 }
111 }
112
113 func TestDiffVersion(t *testing.T) {
114 diff, err := NewDiffVersion("MERGED")
115 if err != nil {
116 t.Fatal(err)
117 }
118 defer diff.Dir.Delete()
119
120 obj := FakeObject{
121 name: "bla",
122 live: map[string]interface{}{"live": true},
123 merged: map[string]interface{}{"merged": true},
124 }
125 rObj, err := obj.Merged()
126 if err != nil {
127 t.Fatal(err)
128 }
129 err = diff.Print(obj.Name(), rObj, Printer{})
130 if err != nil {
131 t.Fatal(err)
132 }
133 fcontent, err := os.ReadFile(path.Join(diff.Dir.Name, obj.Name()))
134 if err != nil {
135 t.Fatal(err)
136 }
137 econtent := "merged: true\n"
138 if string(fcontent) != econtent {
139 t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
140 }
141 }
142
143 func TestDirectory(t *testing.T) {
144 dir, err := CreateDirectory("prefix")
145 defer dir.Delete()
146 if err != nil {
147 t.Fatal(err)
148 }
149 _, err = os.Stat(dir.Name)
150 if err != nil {
151 t.Fatal(err)
152 }
153 if !strings.HasPrefix(filepath.Base(dir.Name), "prefix") {
154 t.Fatalf(`Directory doesn't start with "prefix": %q`, dir.Name)
155 }
156 entries, err := os.ReadDir(dir.Name)
157 if err != nil {
158 t.Fatal(err)
159 }
160 if len(entries) != 0 {
161 t.Fatalf("Directory should be empty, has %d elements", len(entries))
162 }
163 _, err = dir.NewFile("ONE")
164 if err != nil {
165 t.Fatal(err)
166 }
167 _, err = dir.NewFile("TWO")
168 if err != nil {
169 t.Fatal(err)
170 }
171 entries, err = os.ReadDir(dir.Name)
172 if err != nil {
173 t.Fatal(err)
174 }
175 if len(entries) != 2 {
176 t.Fatalf("ReadDir should have two elements, has %d elements", len(entries))
177 }
178 err = dir.Delete()
179 if err != nil {
180 t.Fatal(err)
181 }
182 _, err = os.Stat(dir.Name)
183 if err == nil {
184 t.Fatal("Directory should be gone, still present.")
185 }
186 }
187
188 func TestDiffer(t *testing.T) {
189 diff, err := NewDiffer("LIVE", "MERGED")
190 if err != nil {
191 t.Fatal(err)
192 }
193 defer diff.TearDown()
194
195 obj := FakeObject{
196 name: "bla",
197 live: map[string]interface{}{"live": true},
198 merged: map[string]interface{}{"merged": true},
199 }
200 err = diff.Diff(&obj, Printer{}, true)
201 if err != nil {
202 t.Fatal(err)
203 }
204 fcontent, err := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
205 if err != nil {
206 t.Fatal(err)
207 }
208 econtent := "live: true\n"
209 if string(fcontent) != econtent {
210 t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
211 }
212
213 fcontent, err = os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
214 if err != nil {
215 t.Fatal(err)
216 }
217 econtent = "merged: true\n"
218 if string(fcontent) != econtent {
219 t.Fatalf("File has %q, expected %q", string(fcontent), econtent)
220 }
221 }
222
223 func TestShowManagedFields(t *testing.T) {
224 diff, err := NewDiffer("LIVE", "MERGED")
225 if err != nil {
226 t.Fatal(err)
227 }
228 defer diff.TearDown()
229
230 testCases := []struct {
231 name string
232 showManagedFields bool
233 expectedFromContent string
234 expectedToContent string
235 }{
236 {
237 name: "without managed fields",
238 showManagedFields: false,
239 expectedFromContent: `live: true
240 metadata:
241 name: foo
242 `,
243 expectedToContent: `merged: true
244 metadata:
245 name: foo
246 `,
247 },
248 {
249 name: "with managed fields",
250 showManagedFields: true,
251 expectedFromContent: `live: true
252 metadata:
253 managedFields: mf-data
254 name: foo
255 `,
256 expectedToContent: `merged: true
257 metadata:
258 managedFields: mf-data
259 name: foo
260 `,
261 },
262 }
263
264 for i, tc := range testCases {
265 t.Run(tc.name, func(t *testing.T) {
266 obj := FakeObject{
267 name: fmt.Sprintf("TestCase%d", i),
268 live: map[string]interface{}{
269 "live": true,
270 "metadata": map[string]interface{}{
271 "managedFields": "mf-data",
272 "name": "foo",
273 },
274 },
275 merged: map[string]interface{}{
276 "merged": true,
277 "metadata": map[string]interface{}{
278 "managedFields": "mf-data",
279 "name": "foo",
280 },
281 },
282 }
283
284 err = diff.Diff(&obj, Printer{}, tc.showManagedFields)
285 if err != nil {
286 t.Fatal(err)
287 }
288
289 actualFromContent, _ := os.ReadFile(path.Join(diff.From.Dir.Name, obj.Name()))
290 if string(actualFromContent) != tc.expectedFromContent {
291 t.Fatalf("File has %q, expected %q", string(actualFromContent), tc.expectedFromContent)
292 }
293
294 actualToContent, _ := os.ReadFile(path.Join(diff.To.Dir.Name, obj.Name()))
295 if string(actualToContent) != tc.expectedToContent {
296 t.Fatalf("File has %q, expected %q", string(actualToContent), tc.expectedToContent)
297 }
298 })
299 }
300 }
301
302 func TestMasker(t *testing.T) {
303 type diff struct {
304 from runtime.Object
305 to runtime.Object
306 }
307 cases := []struct {
308 name string
309 input diff
310 want diff
311 }{
312 {
313 name: "no_changes",
314 input: diff{
315 from: &unstructured.Unstructured{
316 Object: map[string]interface{}{
317 "data": map[string]interface{}{
318 "username": "abc",
319 "password": "123",
320 },
321 },
322 },
323 to: &unstructured.Unstructured{
324 Object: map[string]interface{}{
325 "data": map[string]interface{}{
326 "username": "abc",
327 "password": "123",
328 },
329 },
330 },
331 },
332 want: diff{
333 from: &unstructured.Unstructured{
334 Object: map[string]interface{}{
335 "data": map[string]interface{}{
336 "username": "***",
337 "password": "***",
338 },
339 },
340 },
341 to: &unstructured.Unstructured{
342 Object: map[string]interface{}{
343 "data": map[string]interface{}{
344 "username": "***",
345 "password": "***",
346 },
347 },
348 },
349 },
350 },
351 {
352 name: "object_created",
353 input: diff{
354 from: nil,
355 to: &unstructured.Unstructured{
356 Object: map[string]interface{}{
357 "data": map[string]interface{}{
358 "username": "abc",
359 "password": "123",
360 },
361 },
362 },
363 },
364 want: diff{
365 from: nil,
366 to: &unstructured.Unstructured{
367 Object: map[string]interface{}{
368 "data": map[string]interface{}{
369 "username": "***",
370 "password": "***",
371 },
372 },
373 },
374 },
375 },
376 {
377 name: "object_removed",
378 input: diff{
379 from: &unstructured.Unstructured{
380 Object: map[string]interface{}{
381 "data": map[string]interface{}{
382 "username": "abc",
383 "password": "123",
384 },
385 },
386 },
387 to: nil,
388 },
389 want: diff{
390 from: &unstructured.Unstructured{
391 Object: map[string]interface{}{
392 "data": map[string]interface{}{
393 "username": "***",
394 "password": "***",
395 },
396 },
397 },
398 to: nil,
399 },
400 },
401 {
402 name: "data_key_added",
403 input: diff{
404 from: &unstructured.Unstructured{
405 Object: map[string]interface{}{
406 "data": map[string]interface{}{
407 "username": "abc",
408 },
409 },
410 },
411 to: &unstructured.Unstructured{
412 Object: map[string]interface{}{
413 "data": map[string]interface{}{
414 "username": "abc",
415 "password": "123",
416 },
417 },
418 },
419 },
420 want: diff{
421 from: &unstructured.Unstructured{
422 Object: map[string]interface{}{
423 "data": map[string]interface{}{
424 "username": "***",
425 },
426 },
427 },
428 to: &unstructured.Unstructured{
429 Object: map[string]interface{}{
430 "data": map[string]interface{}{
431 "username": "***",
432 "password": "***",
433 },
434 },
435 },
436 },
437 },
438 {
439 name: "data_key_changed",
440 input: diff{
441 from: &unstructured.Unstructured{
442 Object: map[string]interface{}{
443 "data": map[string]interface{}{
444 "username": "abc",
445 "password": "123",
446 },
447 },
448 },
449 to: &unstructured.Unstructured{
450 Object: map[string]interface{}{
451 "data": map[string]interface{}{
452 "username": "abc",
453 "password": "456",
454 },
455 },
456 },
457 },
458 want: diff{
459 from: &unstructured.Unstructured{
460 Object: map[string]interface{}{
461 "data": map[string]interface{}{
462 "username": "***",
463 "password": "*** (before)",
464 },
465 },
466 },
467 to: &unstructured.Unstructured{
468 Object: map[string]interface{}{
469 "data": map[string]interface{}{
470 "username": "***",
471 "password": "*** (after)",
472 },
473 },
474 },
475 },
476 },
477 {
478 name: "data_key_removed",
479 input: diff{
480 from: &unstructured.Unstructured{
481 Object: map[string]interface{}{
482 "data": map[string]interface{}{
483 "username": "abc",
484 "password": "123",
485 },
486 },
487 },
488 to: &unstructured.Unstructured{
489 Object: map[string]interface{}{
490 "data": map[string]interface{}{
491 "username": "abc",
492
493 },
494 },
495 },
496 },
497 want: diff{
498 from: &unstructured.Unstructured{
499 Object: map[string]interface{}{
500 "data": map[string]interface{}{
501 "username": "***",
502 "password": "***",
503 },
504 },
505 },
506 to: &unstructured.Unstructured{
507 Object: map[string]interface{}{
508 "data": map[string]interface{}{
509 "username": "***",
510
511 },
512 },
513 },
514 },
515 },
516 {
517 name: "empty_secret_from",
518 input: diff{
519 from: &unstructured.Unstructured{
520 Object: map[string]interface{}{},
521 },
522 to: &unstructured.Unstructured{
523 Object: map[string]interface{}{
524 "data": map[string]interface{}{
525 "username": "abc",
526 "password": "123",
527 },
528 },
529 },
530 },
531 want: diff{
532 from: &unstructured.Unstructured{
533 Object: map[string]interface{}{},
534 },
535 to: &unstructured.Unstructured{
536 Object: map[string]interface{}{
537 "data": map[string]interface{}{
538 "username": "***",
539 "password": "***",
540 },
541 },
542 },
543 },
544 },
545 {
546 name: "empty_secret_to",
547 input: diff{
548 from: &unstructured.Unstructured{
549 Object: map[string]interface{}{
550 "data": map[string]interface{}{
551 "username": "abc",
552 "password": "123",
553 },
554 },
555 },
556 to: &unstructured.Unstructured{
557 Object: map[string]interface{}{},
558 },
559 },
560 want: diff{
561 from: &unstructured.Unstructured{
562 Object: map[string]interface{}{
563 "data": map[string]interface{}{
564 "username": "***",
565 "password": "***",
566 },
567 },
568 },
569 to: &unstructured.Unstructured{
570 Object: map[string]interface{}{},
571 },
572 },
573 },
574 {
575 name: "invalid_data_key",
576 input: diff{
577 from: &unstructured.Unstructured{
578 Object: map[string]interface{}{
579 "some_other_key": map[string]interface{}{
580 "username": "abc",
581 "password": "123",
582 },
583 },
584 },
585 to: &unstructured.Unstructured{
586 Object: map[string]interface{}{
587 "some_other_key": map[string]interface{}{
588 "username": "abc",
589 "password": "123",
590 },
591 },
592 },
593 },
594 want: diff{
595 from: &unstructured.Unstructured{
596 Object: map[string]interface{}{
597 "some_other_key": map[string]interface{}{
598 "username": "abc",
599 "password": "123",
600 },
601 },
602 },
603 to: &unstructured.Unstructured{
604 Object: map[string]interface{}{
605 "some_other_key": map[string]interface{}{
606 "username": "abc",
607 "password": "123",
608 },
609 },
610 },
611 },
612 },
613 }
614 for _, tc := range cases {
615 tc := tc
616 t.Run(tc.name, func(t *testing.T) {
617 t.Parallel()
618 m, err := NewMasker(tc.input.from, tc.input.to)
619 if err != nil {
620 t.Fatal(err)
621 }
622 from, to := m.From(), m.To()
623 if from != nil && tc.want.from != nil {
624 if diff := cmp.Diff(from, tc.want.from); diff != "" {
625 t.Errorf("from: (-want +got):\n%s", diff)
626 }
627 }
628 if to != nil && tc.want.to != nil {
629 if diff := cmp.Diff(to, tc.want.to); diff != "" {
630 t.Errorf("to: (-want +got):\n%s", diff)
631 }
632 }
633 })
634 }
635 }
636
View as plain text