1
16
17 package atomic
18
19 import (
20 "bytes"
21 "fmt"
22 "io/ioutil"
23 "os"
24 "path"
25 "path/filepath"
26 "runtime"
27 "strings"
28 "time"
29
30 "github.com/go-logr/logr"
31 "k8s.io/apimachinery/pkg/util/sets"
32 )
33
34 const (
35 maxFileNameLength = 255
36 maxPathLength = 4096
37 )
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 type AtomicWriter struct {
59 targetDir string
60 log logr.Logger
61 }
62
63 type FileProjection struct {
64 Data []byte
65 Mode int32
66 }
67
68
69
70 func NewAtomicWriter(targetDir string, log logr.Logger) (*AtomicWriter, error) {
71 _, err := os.Stat(targetDir)
72 if os.IsNotExist(err) {
73 return nil, err
74 }
75
76 return &AtomicWriter{targetDir: targetDir, log: log}, nil
77 }
78
79 const (
80 dataDirName = "..data"
81 newDataDirName = "..data_tmp"
82 )
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122 func (w *AtomicWriter) Write(payload map[string]FileProjection) error {
123
124 cleanPayload, err := validatePayload(payload)
125 if err != nil {
126 w.log.Error(err, "invalid payload")
127 return err
128 }
129
130
131 dataDirPath := path.Join(w.targetDir, dataDirName)
132 oldTsDir, err := os.Readlink(dataDirPath)
133 if err != nil {
134 if !os.IsNotExist(err) {
135 w.log.Error(err, "unable to read link for data directory")
136 return err
137 }
138
139
140 oldTsDir = ""
141 }
142 oldTsPath := path.Join(w.targetDir, oldTsDir)
143
144 var pathsToRemove sets.String
145
146 if len(oldTsDir) != 0 {
147
148 pathsToRemove, err = w.pathsToRemove(cleanPayload, oldTsPath)
149 if err != nil {
150 w.log.Error(err, "unable to determine user-visible files to remove")
151 return err
152 }
153
154
155 if should, err := shouldWritePayload(cleanPayload, oldTsPath); err != nil {
156 w.log.Error(err, "unable to determine whether payload should be written to disk")
157 return err
158 } else if !should && len(pathsToRemove) == 0 {
159 w.log.V(1).Info("no update required for target directory", "directory", w.targetDir)
160 return nil
161 } else {
162 w.log.V(1).Info("write required for target directory", "directory", w.targetDir)
163 }
164 }
165
166
167 tsDir, err := w.newTimestampDir()
168 if err != nil {
169 w.log.Error(err, "error creating new ts data directory")
170 return err
171 }
172 tsDirName := filepath.Base(tsDir)
173
174
175 if err = w.writePayloadToDir(cleanPayload, tsDir); err != nil {
176 w.log.Error(err, "unable to write payload to ts data directory", "ts directory", tsDir)
177 return err
178 } else {
179 w.log.V(1).Info("performed write of new data to ts data directory", "ts directory", tsDir)
180 }
181
182
183 if err = w.createUserVisibleFiles(cleanPayload); err != nil {
184 w.log.Error(err, "unable to create visible symlinks in target directory", "target directory", w.targetDir)
185 return err
186 }
187
188
189 newDataDirPath := path.Join(w.targetDir, newDataDirName)
190 if err = os.Symlink(tsDirName, newDataDirPath); err != nil {
191 os.RemoveAll(tsDir)
192 w.log.Error(err, "unable to create symbolic link for atomic update")
193 return err
194 }
195
196
197 if runtime.GOOS == "windows" {
198 os.Remove(dataDirPath)
199 err = os.Symlink(tsDirName, dataDirPath)
200 os.Remove(newDataDirPath)
201 } else {
202 err = os.Rename(newDataDirPath, dataDirPath)
203 }
204 if err != nil {
205 os.Remove(newDataDirPath)
206 os.RemoveAll(tsDir)
207 w.log.Error(err, "unable to rename symbolic link for data directory", "data directory", newDataDirPath)
208 return err
209 }
210
211
212 if err = w.removeUserVisiblePaths(pathsToRemove); err != nil {
213 w.log.Error(err, "unable to remove old visible symlinks")
214 return err
215 }
216
217
218 if len(oldTsDir) > 0 {
219 if err = os.RemoveAll(oldTsPath); err != nil {
220 w.log.Error(err, "unable to remove old data directory", "data directory", oldTsDir)
221 return err
222 }
223 }
224
225 return nil
226 }
227
228
229 func validatePayload(payload map[string]FileProjection) (map[string]FileProjection, error) {
230 cleanPayload := make(map[string]FileProjection)
231 for k, content := range payload {
232 if err := validatePath(k); err != nil {
233 return nil, err
234 }
235
236 cleanPayload[filepath.Clean(k)] = content
237 }
238
239 return cleanPayload, nil
240 }
241
242
243
244
245
246
247
248
249
250 func validatePath(targetPath string) error {
251
252
253
254
255 if targetPath == "" {
256 return fmt.Errorf("invalid path: must not be empty: %q", targetPath)
257 }
258 if path.IsAbs(targetPath) {
259 return fmt.Errorf("invalid path: must be relative path: %s", targetPath)
260 }
261
262 if len(targetPath) > maxPathLength {
263 return fmt.Errorf("invalid path: must be less than or equal to %d characters", maxPathLength)
264 }
265
266 items := strings.Split(targetPath, string(os.PathSeparator))
267 for _, item := range items {
268 if item == ".." {
269 return fmt.Errorf("invalid path: must not contain '..': %s", targetPath)
270 }
271 if len(item) > maxFileNameLength {
272 return fmt.Errorf("invalid path: filenames must be less than or equal to %d characters", maxFileNameLength)
273 }
274 }
275 if strings.HasPrefix(items[0], "..") && len(items[0]) > 2 {
276 return fmt.Errorf("invalid path: must not start with '..': %s", targetPath)
277 }
278
279 return nil
280 }
281
282
283 func shouldWritePayload(payload map[string]FileProjection, oldTsDir string) (bool, error) {
284 for userVisiblePath, fileProjection := range payload {
285 shouldWrite, err := shouldWriteFile(path.Join(oldTsDir, userVisiblePath), fileProjection.Data)
286 if err != nil {
287 return false, err
288 }
289
290 if shouldWrite {
291 return true, nil
292 }
293 }
294
295 return false, nil
296 }
297
298
299 func shouldWriteFile(path string, content []byte) (bool, error) {
300 _, err := os.Lstat(path)
301 if os.IsNotExist(err) {
302 return true, nil
303 }
304
305 contentOnFs, err := ioutil.ReadFile(path)
306 if err != nil {
307 return false, err
308 }
309
310 return (bytes.Compare(content, contentOnFs) != 0), nil
311 }
312
313
314
315
316 func (w *AtomicWriter) pathsToRemove(payload map[string]FileProjection, oldTsDir string) (sets.String, error) {
317 paths := sets.NewString()
318 visitor := func(path string, info os.FileInfo, err error) error {
319 relativePath := strings.TrimPrefix(path, oldTsDir)
320 relativePath = strings.TrimPrefix(relativePath, string(os.PathSeparator))
321 if relativePath == "" {
322 return nil
323 }
324
325 paths.Insert(relativePath)
326 return nil
327 }
328
329 err := filepath.Walk(oldTsDir, visitor)
330 if os.IsNotExist(err) {
331 return nil, nil
332 } else if err != nil {
333 return nil, err
334 }
335 w.log.V(1).Info("current paths", "target directory", w.targetDir, "paths", paths.List())
336
337 newPaths := sets.NewString()
338 for file := range payload {
339
340
341 for subPath := file; subPath != ""; {
342 newPaths.Insert(subPath)
343 subPath, _ = filepath.Split(subPath)
344 subPath = strings.TrimSuffix(subPath, string(os.PathSeparator))
345 }
346 }
347 w.log.V(1).Info("new paths", "target directory", w.targetDir, "paths", newPaths.List())
348
349 result := paths.Difference(newPaths)
350 w.log.V(1).Info("paths to remove", "target directory", w.targetDir, "paths", result)
351
352 return result, nil
353 }
354
355
356 func (w *AtomicWriter) newTimestampDir() (string, error) {
357 tsDir, err := ioutil.TempDir(w.targetDir, time.Now().UTC().Format("..2006_01_02_15_04_05."))
358 if err != nil {
359 w.log.Error(err, "unable to create new temp directory")
360 return "", err
361 }
362
363
364
365
366 err = os.Chmod(tsDir, 0755)
367 if err != nil {
368 w.log.Error(err, "unable to set mode on new temp directory")
369 return "", err
370 }
371
372 return tsDir, nil
373 }
374
375
376
377 func (w *AtomicWriter) writePayloadToDir(payload map[string]FileProjection, dir string) error {
378 for userVisiblePath, fileProjection := range payload {
379 content := fileProjection.Data
380 mode := os.FileMode(fileProjection.Mode)
381 fullPath := path.Join(dir, userVisiblePath)
382 baseDir, _ := filepath.Split(fullPath)
383
384 err := os.MkdirAll(baseDir, os.ModePerm)
385 if err != nil {
386 w.log.Error(err, "unable to create directory", "directory", baseDir)
387 return err
388 }
389
390 err = ioutil.WriteFile(fullPath, content, mode)
391 if err != nil {
392 w.log.Error(err, "unable to write file", "file", fullPath, "mode", mode)
393 return err
394 }
395
396
397
398
399 err = os.Chmod(fullPath, mode)
400 if err != nil {
401 w.log.Error(err, "unable to write file", "file", fullPath, "mode", mode)
402 }
403 }
404
405 return nil
406 }
407
408
409
410
411
412
413
414
415
416
417
418 func (w *AtomicWriter) createUserVisibleFiles(payload map[string]FileProjection) error {
419 for userVisiblePath := range payload {
420 slashpos := strings.Index(userVisiblePath, string(os.PathSeparator))
421 if slashpos == -1 {
422 slashpos = len(userVisiblePath)
423 }
424 linkname := userVisiblePath[:slashpos]
425 _, err := os.Readlink(path.Join(w.targetDir, linkname))
426 if err != nil && os.IsNotExist(err) {
427
428 visibleFile := path.Join(w.targetDir, linkname)
429 dataDirFile := path.Join(dataDirName, linkname)
430
431 err = os.Symlink(dataDirFile, visibleFile)
432 if err != nil {
433 return err
434 }
435 }
436 }
437 return nil
438 }
439
440
441
442 func (w *AtomicWriter) removeUserVisiblePaths(paths sets.String) error {
443 ps := string(os.PathSeparator)
444 var lasterr error
445 for p := range paths {
446
447 if strings.Contains(p, ps) {
448 continue
449 }
450 if err := os.Remove(path.Join(w.targetDir, p)); err != nil {
451 w.log.Error(err, "unable to prune old user-visible path", "path", p)
452 lasterr = err
453 }
454 }
455
456 return lasterr
457 }
458
View as plain text