k8s.io/kubernetes@v1.29.3/pkg/volume/util/atomic_writer_test.go (about) 1 //go:build linux 2 // +build linux 3 4 /* 5 Copyright 2016 The Kubernetes Authors. 6 7 Licensed under the Apache License, Version 2.0 (the "License"); 8 you may not use this file except in compliance with the License. 9 You may obtain a copy of the License at 10 11 http://www.apache.org/licenses/LICENSE-2.0 12 13 Unless required by applicable law or agreed to in writing, software 14 distributed under the License is distributed on an "AS IS" BASIS, 15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 See the License for the specific language governing permissions and 17 limitations under the License. 18 */ 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 // $1 if you can tell me what this binary is 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 // use filepath.Walk to reconstruct the payload, then deep equal 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 // Test that setPerms() is called once and with valid timestamp directory. 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 // Ensure that given timestamp directory really exists. 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 // Test that errors from setPerms() are propagated. 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 }