atomic_writer_test.go 27 KB

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