1
2
3
4
19
20 package util
21
22 import (
23 "encoding/base64"
24 "fmt"
25 "os"
26 "path/filepath"
27 "reflect"
28 "strings"
29 "testing"
30
31 "k8s.io/apimachinery/pkg/util/sets"
32 utiltesting "k8s.io/client-go/util/testing"
33 )
34
35 func TestNewAtomicWriter(t *testing.T) {
36 targetDir, err := utiltesting.MkTmpdir("atomic-write")
37 if err != nil {
38 t.Fatalf("unexpected error creating tmp dir: %v", err)
39 }
40 defer os.RemoveAll(targetDir)
41
42 _, err = NewAtomicWriter(targetDir, "-test-")
43 if err != nil {
44 t.Fatalf("unexpected error creating writer for existing target dir: %v", err)
45 }
46
47 nonExistentDir, err := utiltesting.MkTmpdir("atomic-write")
48 if err != nil {
49 t.Fatalf("unexpected error creating tmp dir: %v", err)
50 }
51 err = os.Remove(nonExistentDir)
52 if err != nil {
53 t.Fatalf("unexpected error ensuring dir %v does not exist: %v", nonExistentDir, err)
54 }
55
56 _, err = NewAtomicWriter(nonExistentDir, "-test-")
57 if err == nil {
58 t.Fatalf("unexpected success creating writer for nonexistent target dir: %v", err)
59 }
60 }
61
62 func TestValidatePath(t *testing.T) {
63 maxPath := strings.Repeat("a", maxPathLength+1)
64 maxFile := strings.Repeat("a", maxFileNameLength+1)
65
66 cases := []struct {
67 name string
68 path string
69 valid bool
70 }{
71 {
72 name: "valid 1",
73 path: "i/am/well/behaved.txt",
74 valid: true,
75 },
76 {
77 name: "valid 2",
78 path: "keepyourheaddownandfollowtherules.txt",
79 valid: true,
80 },
81 {
82 name: "max path length",
83 path: maxPath,
84 valid: false,
85 },
86 {
87 name: "max file length",
88 path: maxFile,
89 valid: false,
90 },
91 {
92 name: "absolute failure",
93 path: "/dev/null",
94 valid: false,
95 },
96 {
97 name: "reserved path",
98 path: "..sneaky.txt",
99 valid: false,
100 },
101 {
102 name: "contains doubledot 1",
103 path: "hello/there/../../../../../../etc/passwd",
104 valid: false,
105 },
106 {
107 name: "contains doubledot 2",
108 path: "hello/../etc/somethingbad",
109 valid: false,
110 },
111 {
112 name: "empty",
113 path: "",
114 valid: false,
115 },
116 }
117
118 for _, tc := range cases {
119 err := validatePath(tc.path)
120 if tc.valid && err != nil {
121 t.Errorf("%v: unexpected failure: %v", tc.name, err)
122 continue
123 }
124
125 if !tc.valid && err == nil {
126 t.Errorf("%v: unexpected success", tc.name)
127 }
128 }
129 }
130
131 func TestPathsToRemove(t *testing.T) {
132 cases := []struct {
133 name string
134 payload1 map[string]FileProjection
135 payload2 map[string]FileProjection
136 expected sets.String
137 }{
138 {
139 name: "simple",
140 payload1: map[string]FileProjection{
141 "foo.txt": {Mode: 0644, Data: []byte("foo")},
142 "bar.txt": {Mode: 0644, Data: []byte("bar")},
143 },
144 payload2: map[string]FileProjection{
145 "foo.txt": {Mode: 0644, Data: []byte("foo")},
146 },
147 expected: sets.NewString("bar.txt"),
148 },
149 {
150 name: "simple 2",
151 payload1: map[string]FileProjection{
152 "foo.txt": {Mode: 0644, Data: []byte("foo")},
153 "zip/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
154 },
155 payload2: map[string]FileProjection{
156 "foo.txt": {Mode: 0644, Data: []byte("foo")},
157 },
158 expected: sets.NewString("zip/bar.txt", "zip"),
159 },
160 {
161 name: "subdirs 1",
162 payload1: map[string]FileProjection{
163 "foo.txt": {Mode: 0644, Data: []byte("foo")},
164 "zip/zap/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
165 },
166 payload2: map[string]FileProjection{
167 "foo.txt": {Mode: 0644, Data: []byte("foo")},
168 },
169 expected: sets.NewString("zip/zap/bar.txt", "zip", "zip/zap"),
170 },
171 {
172 name: "subdirs 2",
173 payload1: map[string]FileProjection{
174 "foo.txt": {Mode: 0644, Data: []byte("foo")},
175 "zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
176 },
177 payload2: map[string]FileProjection{
178 "foo.txt": {Mode: 0644, Data: []byte("foo")},
179 },
180 expected: sets.NewString("zip/1/2/3/4/bar.txt", "zip", "zip/1", "zip/1/2", "zip/1/2/3", "zip/1/2/3/4"),
181 },
182 {
183 name: "subdirs 3",
184 payload1: map[string]FileProjection{
185 "foo.txt": {Mode: 0644, Data: []byte("foo")},
186 "zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
187 "zap/a/b/c/bar.txt": {Mode: 0644, Data: []byte("zap/bar")},
188 },
189 payload2: map[string]FileProjection{
190 "foo.txt": {Mode: 0644, Data: []byte("foo")},
191 },
192 expected: sets.NewString("zip/1/2/3/4/bar.txt", "zip", "zip/1", "zip/1/2", "zip/1/2/3", "zip/1/2/3/4", "zap", "zap/a", "zap/a/b", "zap/a/b/c", "zap/a/b/c/bar.txt"),
193 },
194 {
195 name: "subdirs 4",
196 payload1: map[string]FileProjection{
197 "foo.txt": {Mode: 0644, Data: []byte("foo")},
198 "zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
199 "zap/1/2/c/bar.txt": {Mode: 0644, Data: []byte("zap/bar")},
200 "zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")},
201 },
202 payload2: map[string]FileProjection{
203 "foo.txt": {Mode: 0644, Data: []byte("foo")},
204 "zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")},
205 },
206 expected: sets.NewString("zap/1/2/3/4/bar.txt", "zap/1/2/3", "zap/1/2/3/4", "zap/1/2/3/4/bar.txt", "zap/1/2/c", "zap/1/2/c/bar.txt"),
207 },
208 {
209 name: "subdirs 5",
210 payload1: map[string]FileProjection{
211 "foo.txt": {Mode: 0644, Data: []byte("foo")},
212 "zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
213 "zap/1/2/c/bar.txt": {Mode: 0644, Data: []byte("zap/bar")},
214 },
215 payload2: map[string]FileProjection{
216 "foo.txt": {Mode: 0644, Data: []byte("foo")},
217 "zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")},
218 },
219 expected: sets.NewString("zap/1/2/3/4/bar.txt", "zap/1/2/3", "zap/1/2/3/4", "zap/1/2/3/4/bar.txt", "zap/1/2/c", "zap/1/2/c/bar.txt"),
220 },
221 }
222
223 for _, tc := range cases {
224 targetDir, err := utiltesting.MkTmpdir("atomic-write")
225 if err != nil {
226 t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
227 continue
228 }
229 defer os.RemoveAll(targetDir)
230
231 writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
232 err = writer.Write(tc.payload1, nil)
233 if err != nil {
234 t.Errorf("%v: unexpected error writing: %v", tc.name, err)
235 continue
236 }
237
238 dataDirPath := filepath.Join(targetDir, dataDirName)
239 oldTsDir, err := os.Readlink(dataDirPath)
240 if err != nil && os.IsNotExist(err) {
241 t.Errorf("Data symlink does not exist: %v", dataDirPath)
242 continue
243 } else if err != nil {
244 t.Errorf("Unable to read symlink %v: %v", dataDirPath, err)
245 continue
246 }
247
248 actual, err := writer.pathsToRemove(tc.payload2, filepath.Join(targetDir, oldTsDir))
249 if err != nil {
250 t.Errorf("%v: unexpected error determining paths to remove: %v", tc.name, err)
251 continue
252 }
253
254 if e, a := tc.expected, actual; !e.Equal(a) {
255 t.Errorf("%v: unexpected paths to remove:\nexpected: %v\n got: %v", tc.name, e, a)
256 }
257 }
258 }
259
260 func TestWriteOnce(t *testing.T) {
261
262 encodedMysteryBinary := `f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAAB
263 AAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAfQAAAAAAAAB9AAAAAAAAAAAA
264 IAAAAAAAsDyZDwU=`
265
266 mysteryBinaryBytes := make([]byte, base64.StdEncoding.DecodedLen(len(encodedMysteryBinary)))
267 numBytes, err := base64.StdEncoding.Decode(mysteryBinaryBytes, []byte(encodedMysteryBinary))
268 if err != nil {
269 t.Fatalf("Unexpected error decoding binary payload: %v", err)
270 }
271
272 if numBytes != 125 {
273 t.Fatalf("Unexpected decoded binary size: expected 125, got %v", numBytes)
274 }
275
276 cases := []struct {
277 name string
278 payload map[string]FileProjection
279 success bool
280 }{
281 {
282 name: "invalid payload 1",
283 payload: map[string]FileProjection{
284 "foo": {Mode: 0644, Data: []byte("foo")},
285 "..bar": {Mode: 0644, Data: []byte("bar")},
286 "binary.bin": {Mode: 0644, Data: mysteryBinaryBytes},
287 },
288 success: false,
289 },
290 {
291 name: "invalid payload 2",
292 payload: map[string]FileProjection{
293 "foo/../bar": {Mode: 0644, Data: []byte("foo")},
294 },
295 success: false,
296 },
297 {
298 name: "basic 1",
299 payload: map[string]FileProjection{
300 "foo": {Mode: 0644, Data: []byte("foo")},
301 "bar": {Mode: 0644, Data: []byte("bar")},
302 },
303 success: true,
304 },
305 {
306 name: "basic 2",
307 payload: map[string]FileProjection{
308 "binary.bin": {Mode: 0644, Data: mysteryBinaryBytes},
309 ".binary.bin": {Mode: 0644, Data: mysteryBinaryBytes},
310 },
311 success: true,
312 },
313 {
314 name: "basic mode 1",
315 payload: map[string]FileProjection{
316 "foo": {Mode: 0777, Data: []byte("foo")},
317 "bar": {Mode: 0400, Data: []byte("bar")},
318 },
319 success: true,
320 },
321 {
322 name: "dotfiles",
323 payload: map[string]FileProjection{
324 "foo": {Mode: 0644, Data: []byte("foo")},
325 "bar": {Mode: 0644, Data: []byte("bar")},
326 ".dotfile": {Mode: 0644, Data: []byte("dotfile")},
327 ".dotfile.file": {Mode: 0644, Data: []byte("dotfile.file")},
328 },
329 success: true,
330 },
331 {
332 name: "dotfiles mode",
333 payload: map[string]FileProjection{
334 "foo": {Mode: 0407, Data: []byte("foo")},
335 "bar": {Mode: 0440, Data: []byte("bar")},
336 ".dotfile": {Mode: 0777, Data: []byte("dotfile")},
337 ".dotfile.file": {Mode: 0666, Data: []byte("dotfile.file")},
338 },
339 success: true,
340 },
341 {
342 name: "subdirectories 1",
343 payload: map[string]FileProjection{
344 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
345 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
346 },
347 success: true,
348 },
349 {
350 name: "subdirectories mode 1",
351 payload: map[string]FileProjection{
352 "foo/bar.txt": {Mode: 0400, Data: []byte("foo/bar")},
353 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
354 },
355 success: true,
356 },
357 {
358 name: "subdirectories 2",
359 payload: map[string]FileProjection{
360 "foo//bar.txt": {Mode: 0644, Data: []byte("foo//bar")},
361 "bar///bar/zab.txt": {Mode: 0644, Data: []byte("bar/../bar/zab.txt")},
362 },
363 success: true,
364 },
365 {
366 name: "subdirectories 3",
367 payload: map[string]FileProjection{
368 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
369 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
370 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
371 "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
372 },
373 success: true,
374 },
375 {
376 name: "kitchen sink",
377 payload: map[string]FileProjection{
378 "foo.log": {Mode: 0644, Data: []byte("foo")},
379 "bar.zap": {Mode: 0644, Data: []byte("bar")},
380 ".dotfile": {Mode: 0644, Data: []byte("dotfile")},
381 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
382 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
383 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
384 "bar/zib/zab.txt": {Mode: 0400, Data: []byte("bar/zib/zab.txt")},
385 "1/2/3/4/5/6/7/8/9/10/.dotfile.lib": {Mode: 0777, Data: []byte("1-2-3-dotfile")},
386 },
387 success: true,
388 },
389 }
390
391 for _, tc := range cases {
392 targetDir, err := utiltesting.MkTmpdir("atomic-write")
393 if err != nil {
394 t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
395 continue
396 }
397 defer os.RemoveAll(targetDir)
398
399 writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
400 err = writer.Write(tc.payload, nil)
401 if err != nil && tc.success {
402 t.Errorf("%v: unexpected error writing payload: %v", tc.name, err)
403 continue
404 } else if err == nil && !tc.success {
405 t.Errorf("%v: unexpected success", tc.name)
406 continue
407 } else if err != nil {
408 continue
409 }
410
411 checkVolumeContents(targetDir, tc.name, tc.payload, t)
412 }
413 }
414
415 func TestUpdate(t *testing.T) {
416 cases := []struct {
417 name string
418 first map[string]FileProjection
419 next map[string]FileProjection
420 shouldWrite bool
421 }{
422 {
423 name: "update",
424 first: map[string]FileProjection{
425 "foo": {Mode: 0644, Data: []byte("foo")},
426 "bar": {Mode: 0644, Data: []byte("bar")},
427 },
428 next: map[string]FileProjection{
429 "foo": {Mode: 0644, Data: []byte("foo2")},
430 "bar": {Mode: 0640, Data: []byte("bar2")},
431 },
432 shouldWrite: true,
433 },
434 {
435 name: "no update",
436 first: map[string]FileProjection{
437 "foo": {Mode: 0644, Data: []byte("foo")},
438 "bar": {Mode: 0644, Data: []byte("bar")},
439 },
440 next: map[string]FileProjection{
441 "foo": {Mode: 0644, Data: []byte("foo")},
442 "bar": {Mode: 0644, Data: []byte("bar")},
443 },
444 shouldWrite: false,
445 },
446 {
447 name: "no update 2",
448 first: map[string]FileProjection{
449 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
450 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
451 },
452 next: map[string]FileProjection{
453 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
454 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
455 },
456 shouldWrite: false,
457 },
458 {
459 name: "add 1",
460 first: map[string]FileProjection{
461 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
462 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
463 },
464 next: map[string]FileProjection{
465 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
466 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
467 "blu/zip.txt": {Mode: 0644, Data: []byte("zip")},
468 },
469 shouldWrite: true,
470 },
471 {
472 name: "add 2",
473 first: map[string]FileProjection{
474 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
475 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
476 },
477 next: map[string]FileProjection{
478 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
479 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
480 "blu/two/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")},
481 },
482 shouldWrite: true,
483 },
484 {
485 name: "add 3",
486 first: map[string]FileProjection{
487 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
488 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
489 },
490 next: map[string]FileProjection{
491 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
492 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
493 "bar/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")},
494 },
495 shouldWrite: true,
496 },
497 {
498 name: "delete 1",
499 first: map[string]FileProjection{
500 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
501 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
502 },
503 next: map[string]FileProjection{
504 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
505 },
506 shouldWrite: true,
507 },
508 {
509 name: "delete 2",
510 first: map[string]FileProjection{
511 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
512 "bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")},
513 },
514 next: map[string]FileProjection{
515 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
516 },
517 shouldWrite: true,
518 },
519 {
520 name: "delete 3",
521 first: map[string]FileProjection{
522 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
523 "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
524 "bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")},
525 },
526 next: map[string]FileProjection{
527 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
528 "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
529 },
530 shouldWrite: true,
531 },
532 {
533 name: "delete 4",
534 first: map[string]FileProjection{
535 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
536 "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
537 "bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")},
538 },
539 next: map[string]FileProjection{
540 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
541 "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
542 },
543 shouldWrite: true,
544 },
545 {
546 name: "delete all",
547 first: map[string]FileProjection{
548 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
549 "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
550 "bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")},
551 },
552 next: map[string]FileProjection{},
553 shouldWrite: true,
554 },
555 {
556 name: "add and delete 1",
557 first: map[string]FileProjection{
558 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
559 },
560 next: map[string]FileProjection{
561 "bar/baz.txt": {Mode: 0644, Data: []byte("baz")},
562 },
563 shouldWrite: true,
564 },
565 }
566
567 for _, tc := range cases {
568 targetDir, err := utiltesting.MkTmpdir("atomic-write")
569 if err != nil {
570 t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
571 continue
572 }
573 defer os.RemoveAll(targetDir)
574
575 writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
576
577 err = writer.Write(tc.first, nil)
578 if err != nil {
579 t.Errorf("%v: unexpected error writing: %v", tc.name, err)
580 continue
581 }
582
583 checkVolumeContents(targetDir, tc.name, tc.first, t)
584 if !tc.shouldWrite {
585 continue
586 }
587
588 err = writer.Write(tc.next, nil)
589 if err != nil {
590 if tc.shouldWrite {
591 t.Errorf("%v: unexpected error writing: %v", tc.name, err)
592 continue
593 }
594 } else if !tc.shouldWrite {
595 t.Errorf("%v: unexpected success", tc.name)
596 continue
597 }
598
599 checkVolumeContents(targetDir, tc.name, tc.next, t)
600 }
601 }
602
603 func TestMultipleUpdates(t *testing.T) {
604 cases := []struct {
605 name string
606 payloads []map[string]FileProjection
607 }{
608 {
609 name: "update 1",
610 payloads: []map[string]FileProjection{
611 {
612 "foo": {Mode: 0644, Data: []byte("foo")},
613 "bar": {Mode: 0644, Data: []byte("bar")},
614 },
615 {
616 "foo": {Mode: 0400, Data: []byte("foo2")},
617 "bar": {Mode: 0400, Data: []byte("bar2")},
618 },
619 {
620 "foo": {Mode: 0600, Data: []byte("foo3")},
621 "bar": {Mode: 0600, Data: []byte("bar3")},
622 },
623 },
624 },
625 {
626 name: "update 2",
627 payloads: []map[string]FileProjection{
628 {
629 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
630 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
631 },
632 {
633 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
634 "bar/zab.txt": {Mode: 0400, Data: []byte("bar/zab.txt2")},
635 },
636 },
637 },
638 {
639 name: "clear sentinel",
640 payloads: []map[string]FileProjection{
641 {
642 "foo": {Mode: 0644, Data: []byte("foo")},
643 "bar": {Mode: 0644, Data: []byte("bar")},
644 },
645 {
646 "foo": {Mode: 0644, Data: []byte("foo2")},
647 "bar": {Mode: 0644, Data: []byte("bar2")},
648 },
649 {
650 "foo": {Mode: 0644, Data: []byte("foo3")},
651 "bar": {Mode: 0644, Data: []byte("bar3")},
652 },
653 {
654 "foo": {Mode: 0644, Data: []byte("foo4")},
655 "bar": {Mode: 0644, Data: []byte("bar4")},
656 },
657 },
658 },
659 {
660 name: "subdirectories 2",
661 payloads: []map[string]FileProjection{
662 {
663 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
664 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
665 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
666 "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
667 },
668 {
669 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
670 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")},
671 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
672 "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
673 },
674 },
675 },
676 {
677 name: "add 1",
678 payloads: []map[string]FileProjection{
679 {
680 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
681 "bar//zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
682 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
683 "bar/zib////zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
684 },
685 {
686 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
687 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")},
688 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
689 "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
690 "add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")},
691 },
692 },
693 },
694 {
695 name: "add 2",
696 payloads: []map[string]FileProjection{
697 {
698 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
699 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")},
700 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
701 "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
702 "add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")},
703 },
704 {
705 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
706 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")},
707 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
708 "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
709 "add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")},
710 "add/new/keys2.txt": {Mode: 0644, Data: []byte("addNewKeys2")},
711 "add/new/keys3.txt": {Mode: 0644, Data: []byte("addNewKeys3")},
712 },
713 },
714 },
715 {
716 name: "remove 1",
717 payloads: []map[string]FileProjection{
718 {
719 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
720 "bar//zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
721 "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
722 "zip/zap/zup/fop.txt": {Mode: 0644, Data: []byte("zip/zap/zup/fop.txt")},
723 },
724 {
725 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
726 "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")},
727 },
728 {
729 "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
730 },
731 },
732 },
733 }
734
735 for _, tc := range cases {
736 targetDir, err := utiltesting.MkTmpdir("atomic-write")
737 if err != nil {
738 t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
739 continue
740 }
741 defer os.RemoveAll(targetDir)
742
743 writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
744
745 for _, payload := range tc.payloads {
746 writer.Write(payload, nil)
747
748 checkVolumeContents(targetDir, tc.name, payload, t)
749 }
750 }
751 }
752
753 func checkVolumeContents(targetDir, tcName string, payload map[string]FileProjection, t *testing.T) {
754 dataDirPath := filepath.Join(targetDir, dataDirName)
755
756 observedPayload := make(map[string]FileProjection)
757 visitor := func(path string, info os.FileInfo, _ error) error {
758 if info.IsDir() {
759 return nil
760 }
761
762 relativePath := strings.TrimPrefix(path, dataDirPath)
763 relativePath = strings.TrimPrefix(relativePath, "/")
764 if strings.HasPrefix(relativePath, "..") {
765 return nil
766 }
767
768 content, err := os.ReadFile(path)
769 if err != nil {
770 return err
771 }
772 fileInfo, err := os.Stat(path)
773 if err != nil {
774 return err
775 }
776 mode := int32(fileInfo.Mode())
777
778 observedPayload[relativePath] = FileProjection{Data: content, Mode: mode}
779
780 return nil
781 }
782
783 d, err := os.ReadDir(targetDir)
784 if err != nil {
785 t.Errorf("Unable to read dir %v: %v", targetDir, err)
786 return
787 }
788 for _, info := range d {
789 if strings.HasPrefix(info.Name(), "..") {
790 continue
791 }
792 if info.Type()&os.ModeSymlink != 0 {
793 p := filepath.Join(targetDir, info.Name())
794 actual, err := os.Readlink(p)
795 if err != nil {
796 t.Errorf("Unable to read symlink %v: %v", p, err)
797 continue
798 }
799 if err := filepath.Walk(filepath.Join(targetDir, actual), visitor); err != nil {
800 t.Errorf("%v: unexpected error walking directory: %v", tcName, err)
801 }
802 }
803 }
804
805 cleanPathPayload := make(map[string]FileProjection, len(payload))
806 for k, v := range payload {
807 cleanPathPayload[filepath.Clean(k)] = v
808 }
809
810 if !reflect.DeepEqual(cleanPathPayload, observedPayload) {
811 t.Errorf("%v: payload and observed payload do not match.", tcName)
812 }
813 }
814
815 func TestValidatePayload(t *testing.T) {
816 maxPath := strings.Repeat("a", maxPathLength+1)
817
818 cases := []struct {
819 name string
820 payload map[string]FileProjection
821 expected sets.String
822 valid bool
823 }{
824 {
825 name: "valid payload",
826 payload: map[string]FileProjection{
827 "foo": {},
828 "bar": {},
829 },
830 valid: true,
831 expected: sets.NewString("foo", "bar"),
832 },
833 {
834 name: "payload with path length > 4096 is invalid",
835 payload: map[string]FileProjection{
836 maxPath: {},
837 },
838 valid: false,
839 },
840 {
841 name: "payload with absolute path is invalid",
842 payload: map[string]FileProjection{
843 "/dev/null": {},
844 },
845 valid: false,
846 },
847 {
848 name: "payload with reserved path is invalid",
849 payload: map[string]FileProjection{
850 "..sneaky.txt": {},
851 },
852 valid: false,
853 },
854 {
855 name: "payload with doubledot path is invalid",
856 payload: map[string]FileProjection{
857 "foo/../etc/password": {},
858 },
859 valid: false,
860 },
861 {
862 name: "payload with empty path is invalid",
863 payload: map[string]FileProjection{
864 "": {},
865 },
866 valid: false,
867 },
868 {
869 name: "payload with unclean path should be cleaned",
870 payload: map[string]FileProjection{
871 "foo////bar": {},
872 },
873 valid: true,
874 expected: sets.NewString("foo/bar"),
875 },
876 }
877 getPayloadPaths := func(payload map[string]FileProjection) sets.String {
878 paths := sets.NewString()
879 for path := range payload {
880 paths.Insert(path)
881 }
882 return paths
883 }
884
885 for _, tc := range cases {
886 real, err := validatePayload(tc.payload)
887 if !tc.valid && err == nil {
888 t.Errorf("%v: unexpected success", tc.name)
889 }
890
891 if tc.valid {
892 if err != nil {
893 t.Errorf("%v: unexpected failure: %v", tc.name, err)
894 continue
895 }
896
897 realPaths := getPayloadPaths(real)
898 if !realPaths.Equal(tc.expected) {
899 t.Errorf("%v: unexpected payload paths: %v is not equal to %v", tc.name, realPaths, tc.expected)
900 }
901 }
902
903 }
904 }
905
906 func TestCreateUserVisibleFiles(t *testing.T) {
907 cases := []struct {
908 name string
909 payload map[string]FileProjection
910 expected map[string]string
911 }{
912 {
913 name: "simple path",
914 payload: map[string]FileProjection{
915 "foo": {},
916 "bar": {},
917 },
918 expected: map[string]string{
919 "foo": "..data/foo",
920 "bar": "..data/bar",
921 },
922 },
923 {
924 name: "simple nested path",
925 payload: map[string]FileProjection{
926 "foo/bar": {},
927 "foo/bar/txt": {},
928 "bar/txt": {},
929 },
930 expected: map[string]string{
931 "foo": "..data/foo",
932 "bar": "..data/bar",
933 },
934 },
935 {
936 name: "unclean nested path",
937 payload: map[string]FileProjection{
938 "./bar": {},
939 "foo///bar": {},
940 },
941 expected: map[string]string{
942 "bar": "..data/bar",
943 "foo": "..data/foo",
944 },
945 },
946 }
947
948 for _, tc := range cases {
949 targetDir, err := utiltesting.MkTmpdir("atomic-write")
950 if err != nil {
951 t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
952 continue
953 }
954 defer os.RemoveAll(targetDir)
955
956 dataDirPath := filepath.Join(targetDir, dataDirName)
957 err = os.MkdirAll(dataDirPath, 0755)
958 if err != nil {
959 t.Fatalf("%v: unexpected error creating data path: %v", tc.name, err)
960 }
961
962 writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
963 payload, err := validatePayload(tc.payload)
964 if err != nil {
965 t.Fatalf("%v: unexpected error validating payload: %v", tc.name, err)
966 }
967 err = writer.createUserVisibleFiles(payload)
968 if err != nil {
969 t.Fatalf("%v: unexpected error creating visible files: %v", tc.name, err)
970 }
971
972 for subpath, expectedDest := range tc.expected {
973 visiblePath := filepath.Join(targetDir, subpath)
974 destination, err := os.Readlink(visiblePath)
975 if err != nil && os.IsNotExist(err) {
976 t.Fatalf("%v: visible symlink does not exist: %v", tc.name, visiblePath)
977 } else if err != nil {
978 t.Fatalf("%v: unable to read symlink %v: %v", tc.name, dataDirPath, err)
979 }
980
981 if expectedDest != destination {
982 t.Fatalf("%v: symlink destination %q not same with expected data dir %q", tc.name, destination, expectedDest)
983 }
984 }
985 }
986 }
987
988 func TestSetPerms(t *testing.T) {
989 targetDir, err := utiltesting.MkTmpdir("atomic-write")
990 if err != nil {
991 t.Fatalf("unexpected error creating tmp dir: %v", err)
992 }
993 defer os.RemoveAll(targetDir)
994
995
996 payload1 := map[string]FileProjection{
997 "foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
998 "bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
999 }
1000
1001 var setPermsCalled int
1002 writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
1003 err = writer.Write(payload1, func(subPath string) error {
1004 fileInfo, err := os.Stat(filepath.Join(targetDir, subPath))
1005 if err != nil {
1006 t.Fatalf("unexpected error getting file info: %v", err)
1007 }
1008
1009 if !fileInfo.IsDir() {
1010 t.Fatalf("subPath is not a directory: %v", subPath)
1011 }
1012 setPermsCalled++
1013 return nil
1014 })
1015 if err != nil {
1016 t.Fatalf("unexpected error writing: %v", err)
1017 }
1018 if setPermsCalled != 1 {
1019 t.Fatalf("unexpected number of calls to setPerms: %v", setPermsCalled)
1020 }
1021
1022
1023 payload2 := map[string]FileProjection{
1024 "foo/bar.txt": {Mode: 0644, Data: []byte("foo2")},
1025 "bar/zab.txt": {Mode: 0644, Data: []byte("bar2")},
1026 }
1027
1028 err = writer.Write(payload2, func(_ string) error {
1029 return fmt.Errorf("error in setPerms")
1030 })
1031 if err == nil {
1032 t.Fatalf("expected error while writing but got nil")
1033 }
1034 if !strings.Contains(err.Error(), "error in setPerms") {
1035 t.Fatalf("unexpected error while writing: %v", err)
1036 }
1037 }
1038
1039 func TestWriteAgainAfterUnexpectedExit(t *testing.T) {
1040 testCases := []struct {
1041 name string
1042 payload map[string]FileProjection
1043 simulateFn func(targetDir string, payload map[string]FileProjection) error
1044 }{
1045 {
1046 name: "process killed before creating user visible files",
1047 payload: map[string]FileProjection{
1048 "foo": {Mode: 0644, Data: []byte("foo")},
1049 "bar": {Mode: 0644, Data: []byte("bar")},
1050 },
1051 simulateFn: func(targetDir string, payload map[string]FileProjection) error {
1052 for filename := range payload {
1053 path := filepath.Join(targetDir, filename)
1054 if err := os.RemoveAll(path); err != nil {
1055 return err
1056 }
1057 }
1058 return nil
1059 },
1060 },
1061 }
1062
1063 for _, tc := range testCases {
1064 tc := tc
1065 t.Run(tc.name, func(t *testing.T) {
1066 targetDir, err := utiltesting.MkTmpdir("atomic-write")
1067 if err != nil {
1068 t.Fatalf("unexpected error creating tmp dir: %v", err)
1069 }
1070 defer func() {
1071 err := os.RemoveAll(targetDir)
1072 if err != nil {
1073 t.Errorf("%v: unexpected error removing tmp dir: %v", tc.name, err)
1074 }
1075 }()
1076
1077 writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
1078 err = writer.Write(tc.payload, nil)
1079 if err != nil {
1080 t.Fatalf("unexpected error writing payload: %v", err)
1081 }
1082
1083 err = tc.simulateFn(targetDir, tc.payload)
1084 if err != nil {
1085 t.Fatalf("failed to simulate the unexpected exit: %v", err)
1086 }
1087
1088 err = writer.Write(tc.payload, nil)
1089 if err != nil {
1090 t.Fatalf("unexpected error writing payload again: %v", err)
1091 }
1092 checkVolumeContents(targetDir, tc.name, tc.payload, t)
1093 })
1094 }
1095 }
1096
View as plain text