Generalize atomically updating projections into volumes
This commit is contained in:
		
							
								
								
									
										457
									
								
								pkg/volume/util/atomic_writer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										457
									
								
								pkg/volume/util/atomic_writer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,457 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2016 The Kubernetes Authors All rights reserved.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package util
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang/glog"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/kubernetes/pkg/util/sets"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	maxFileNameLength = 255
 | 
			
		||||
	maxPathLength     = 4096
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AtomicWriter handles atomically projecting content for a set of files into
 | 
			
		||||
// a target directory.  AtomicWriter maintains a sentinel file named
 | 
			
		||||
// "..sentinel" in the target directory which is updated after new data is
 | 
			
		||||
// projected into the target directory, allowing consumers of the data to
 | 
			
		||||
// listen for updates by monitoring the sentinel file with inotify or
 | 
			
		||||
// fanotify.
 | 
			
		||||
//
 | 
			
		||||
// Note:
 | 
			
		||||
//
 | 
			
		||||
// 1. AtomicWriter reserves the set of pathnames starting with `..`.
 | 
			
		||||
// 2. AtomicWriter offers no concurrency guarantees and must be synchronized
 | 
			
		||||
//    by the caller.
 | 
			
		||||
//
 | 
			
		||||
// The visible files in this volume are symlinks to files in the writer's data
 | 
			
		||||
// directory.  Actual files are stored in a hidden timestamped directory which
 | 
			
		||||
// is symlinked to by the data directory. The timestamped directory and
 | 
			
		||||
// data directory symlink are created in the writer's target dir.  This scheme
 | 
			
		||||
// allows the files to be atomically updated by changing the target of the
 | 
			
		||||
// data directory symlink.
 | 
			
		||||
type AtomicWriter struct {
 | 
			
		||||
	targetDir  string
 | 
			
		||||
	logContext string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAtomicWriter creates a new AtomicWriter configured to write to the given
 | 
			
		||||
// target directory, or returns an error if the target directory does not exist.
 | 
			
		||||
func NewAtomicWriter(targetDir, logContext string) (*AtomicWriter, error) {
 | 
			
		||||
	_, err := os.Stat(targetDir)
 | 
			
		||||
	if os.IsNotExist(err) {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &AtomicWriter{targetDir: targetDir, logContext: logContext}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	sentinelFileName = "..sentinel"
 | 
			
		||||
	dataDirName      = "..data"
 | 
			
		||||
	newDataDirName   = "..data_tmp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Write does an atomic projection of the given payload into the writer's target
 | 
			
		||||
// directory.  Input paths must not begin with '..'.
 | 
			
		||||
//
 | 
			
		||||
// The Write algorithm is:
 | 
			
		||||
//
 | 
			
		||||
//  1.  The payload is validated; if the payload is invalid, the function returns
 | 
			
		||||
//  2.  The user-visible portion of the volume is walked to determine whether any
 | 
			
		||||
//      portion of the payload was deleted and is still present on disk.
 | 
			
		||||
//      If the payload is already present on disk and there are no deleted files,
 | 
			
		||||
//      the function returns
 | 
			
		||||
//  3.  A check is made to determine whether data present in the payload has changed
 | 
			
		||||
//  4.  A new timestamped dir is created
 | 
			
		||||
//  5.  The payload is written to the new timestamped directory
 | 
			
		||||
//  6.  Symlinks and directory for new user-visible files are created (if needed).
 | 
			
		||||
//
 | 
			
		||||
//      For example consider the files:
 | 
			
		||||
//        <target-dir>/podName
 | 
			
		||||
//        <target-dir>/user/labels
 | 
			
		||||
//        <target-dir>/k8s/annotations
 | 
			
		||||
//
 | 
			
		||||
//      The user visible files are symbolic links into the internal data directory:
 | 
			
		||||
//        <target-dir>/podName         -> ..data/podName
 | 
			
		||||
//        <target-dir>/usr/labels      -> ../..data/usr/labels
 | 
			
		||||
//        <target-dir>/k8s/annotations -> ../..data/k8s/annotations
 | 
			
		||||
//
 | 
			
		||||
//      Relative links are created into the data directory for files in subdirectories.
 | 
			
		||||
//
 | 
			
		||||
//      The data directory itself is a link to a timestamped directory with
 | 
			
		||||
//      the real data:
 | 
			
		||||
//        <target-dir>/..data          -> ..2016_02_01_15_04_05.12345678/
 | 
			
		||||
//  7.  The current timestamped directory is detected by reading the data directory
 | 
			
		||||
//      symlink
 | 
			
		||||
//  8.  A symlink to the new timestamped directory ..data_tmp is created that will
 | 
			
		||||
//      become the new data directory
 | 
			
		||||
//  9.  The new data directory symlink is renamed to the data directory; rename is atomic
 | 
			
		||||
// 10.  The sentinel file modification and access times are updated (file is created if it does not
 | 
			
		||||
//      already exist)
 | 
			
		||||
// 11.  Old paths are removed from the user-visible portion of the target directory
 | 
			
		||||
// 12.  The previous timestamped directory is removed, if it exists
 | 
			
		||||
func (w *AtomicWriter) Write(payload map[string][]byte) error {
 | 
			
		||||
	// (1)
 | 
			
		||||
	cleanPayload, err := validatePayload(payload)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		glog.Errorf("%s: invalid payload: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (2)
 | 
			
		||||
	pathsToRemove, err := w.pathsToRemove(cleanPayload)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		glog.Errorf("%s: error determining user-visible files to remove: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (3)
 | 
			
		||||
	if should, err := w.shouldWritePayload(cleanPayload); err != nil {
 | 
			
		||||
		glog.Errorf("%s: error determining whether payload should be written to disk: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	} else if !should && len(pathsToRemove) == 0 {
 | 
			
		||||
		glog.V(5).Infof("%s: no update required for target directory %v", w.logContext, w.targetDir)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (4)
 | 
			
		||||
	tsDir, err := w.newTimestampDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		glog.V(5).Infof("%s: error creating new ts data directory: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (5)
 | 
			
		||||
	if err = w.writePayloadToDir(cleanPayload, tsDir); err != nil {
 | 
			
		||||
		glog.Errorf("%s: error writing payload to ts data directory %s: %v", w.logContext, tsDir, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (6)
 | 
			
		||||
	if err = w.createUserVisibleFiles(cleanPayload); err != nil {
 | 
			
		||||
		glog.Errorf("%s: error creating visible symlinks in %s: %v", w.logContext, w.targetDir, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (7)
 | 
			
		||||
	_, tsDirName := filepath.Split(tsDir)
 | 
			
		||||
	dataDirPath := path.Join(w.targetDir, dataDirName)
 | 
			
		||||
	oldTsDir, err := os.Readlink(dataDirPath)
 | 
			
		||||
	if err != nil && !os.IsNotExist(err) {
 | 
			
		||||
		glog.Errorf("%s: error reading link for data directory: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (8)
 | 
			
		||||
	newDataDirPath := path.Join(w.targetDir, newDataDirName)
 | 
			
		||||
	if err = os.Symlink(tsDirName, newDataDirPath); err != nil {
 | 
			
		||||
		os.RemoveAll(tsDir)
 | 
			
		||||
		glog.Errorf("%s: error creating symbolic link for atomic update: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (9)
 | 
			
		||||
	if err = os.Rename(newDataDirPath, dataDirPath); err != nil {
 | 
			
		||||
		os.Remove(newDataDirPath)
 | 
			
		||||
		os.RemoveAll(tsDir)
 | 
			
		||||
		glog.Errorf("%s: error renaming symbolic link for data directory %s: %v", w.logContext, newDataDirPath, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (10)
 | 
			
		||||
	if err = w.touchSentinelFile(); err != nil {
 | 
			
		||||
		glog.Errorf("%s: error touching sentinel file: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (11)
 | 
			
		||||
	if err = w.removeUserVisiblePaths(pathsToRemove); err != nil {
 | 
			
		||||
		glog.Errorf("%s: error removing old visible symlinks: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// (12)
 | 
			
		||||
	if len(oldTsDir) > 0 {
 | 
			
		||||
		if err = os.RemoveAll(path.Join(w.targetDir, oldTsDir)); err != nil {
 | 
			
		||||
			glog.Errorf("%s: error removing old data directory %s: %v", w.logContext, oldTsDir, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// validatePayload returns an error if any path in the payload  returns a copy of the payload with the paths cleaned.
 | 
			
		||||
func validatePayload(payload map[string][]byte) (map[string][]byte, error) {
 | 
			
		||||
	cleanPayload := make(map[string][]byte)
 | 
			
		||||
	for k, content := range payload {
 | 
			
		||||
		if err := validatePath(k); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cleanPayload[path.Clean(k)] = content
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return cleanPayload, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// validatePath validates a single path, returning an error if the path is
 | 
			
		||||
// invalid.  paths may not:
 | 
			
		||||
//
 | 
			
		||||
// 1. be absolute
 | 
			
		||||
// 2. contain '..' as an element
 | 
			
		||||
// 3. start with '..'
 | 
			
		||||
// 4. contain filenames larger than 255 characters
 | 
			
		||||
// 5. be longer than 4096 characters
 | 
			
		||||
func validatePath(targetPath string) error {
 | 
			
		||||
	// TODO: somehow unify this with the similar api validation,
 | 
			
		||||
	// validateVolumeSourcePath; the error semantics are just different enough
 | 
			
		||||
	// from this that it was time-prohibitive trying to find the right
 | 
			
		||||
	// refactoring to re-use.
 | 
			
		||||
	if targetPath == "" {
 | 
			
		||||
		return fmt.Errorf("invalid path: must not be empty: %q", targetPath)
 | 
			
		||||
	}
 | 
			
		||||
	if path.IsAbs(targetPath) {
 | 
			
		||||
		return fmt.Errorf("invalid path: must be relative path: %s", targetPath)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(targetPath) > maxPathLength {
 | 
			
		||||
		return fmt.Errorf("invalid path: must be less than %d characters", maxPathLength)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	items := strings.Split(targetPath, string(os.PathSeparator))
 | 
			
		||||
	for _, item := range items {
 | 
			
		||||
		if item == ".." {
 | 
			
		||||
			return fmt.Errorf("invalid path: must not contain '..': %s", targetPath)
 | 
			
		||||
		}
 | 
			
		||||
		if len(item) > maxFileNameLength {
 | 
			
		||||
			return fmt.Errorf("invalid path: filenames must be less than %d characters", maxFileNameLength)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(items[0], "..") && len(items[0]) > 2 {
 | 
			
		||||
		return fmt.Errorf("invalid path: must not start with '..': %s", targetPath)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// shouldWritePayload returns whether the payload should be written to disk.
 | 
			
		||||
func (w *AtomicWriter) shouldWritePayload(payload map[string][]byte) (bool, error) {
 | 
			
		||||
	for userVisiblePath, content := range payload {
 | 
			
		||||
		shouldWrite, err := w.shouldWriteFile(path.Join(w.targetDir, userVisiblePath), content)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if shouldWrite {
 | 
			
		||||
			return true, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// shouldWriteFile returns whether a new version of a file should be written to disk.
 | 
			
		||||
func (w *AtomicWriter) shouldWriteFile(path string, content []byte) (bool, error) {
 | 
			
		||||
	_, err := os.Lstat(path)
 | 
			
		||||
	if os.IsNotExist(err) {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contentOnFs, err := ioutil.ReadFile(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return (bytes.Compare(content, contentOnFs) != 0), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// pathsToRemove walks the user-visible portion of the target directory and
 | 
			
		||||
// determines which paths should be removed (if any) after the payload is
 | 
			
		||||
// written to the target directory.
 | 
			
		||||
func (w *AtomicWriter) pathsToRemove(payload map[string][]byte) (sets.String, error) {
 | 
			
		||||
	paths := sets.NewString()
 | 
			
		||||
	visitor := func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		if path == w.targetDir {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		relativePath := strings.TrimPrefix(path, w.targetDir)
 | 
			
		||||
		relativePath = strings.TrimPrefix(relativePath, "/")
 | 
			
		||||
		if strings.HasPrefix(relativePath, "..") {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		paths.Insert(relativePath)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := filepath.Walk(w.targetDir, visitor)
 | 
			
		||||
	if os.IsNotExist(err) {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	glog.V(5).Infof("%s: current paths:   %+v", w.targetDir, paths.List())
 | 
			
		||||
 | 
			
		||||
	newPaths := sets.NewString()
 | 
			
		||||
	for file := range payload {
 | 
			
		||||
		// add all subpaths for the payload to the set of new paths
 | 
			
		||||
		// to avoid attempting to remove non-empty dirs
 | 
			
		||||
		for subPath := file; subPath != ""; {
 | 
			
		||||
			newPaths.Insert(subPath)
 | 
			
		||||
			subPath, _ = filepath.Split(subPath)
 | 
			
		||||
			subPath = strings.TrimSuffix(subPath, "/")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	glog.V(5).Infof("%s: new paths:       %+v", w.targetDir, newPaths.List())
 | 
			
		||||
 | 
			
		||||
	result := paths.Difference(newPaths)
 | 
			
		||||
	glog.V(5).Infof("%s: paths to remove: %+v", w.targetDir, result)
 | 
			
		||||
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// newTimestampDir creates a new timestamp directory
 | 
			
		||||
func (w *AtomicWriter) newTimestampDir() (string, error) {
 | 
			
		||||
	tsDir, err := ioutil.TempDir(w.targetDir, fmt.Sprintf("..%s.", time.Now().Format("1981_02_01_15_04_05")))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		glog.Errorf("%s: unable to create new temp directory: %v", w.logContext, err)
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tsDir, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// writePayloadToDir writes the given payload to the given directory.  The
 | 
			
		||||
// directory must exist.
 | 
			
		||||
func (w *AtomicWriter) writePayloadToDir(payload map[string][]byte, dir string) error {
 | 
			
		||||
	for userVisiblePath, content := range payload {
 | 
			
		||||
		fullPath := path.Join(dir, userVisiblePath)
 | 
			
		||||
		baseDir, _ := filepath.Split(fullPath)
 | 
			
		||||
 | 
			
		||||
		err := os.MkdirAll(baseDir, os.ModePerm)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			glog.Errorf("%s: unable to create directory %s: %v", w.logContext, baseDir, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = ioutil.WriteFile(fullPath, content, 0644)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			glog.Errorf("%s: unable to write file %s: %v", w.logContext, fullPath, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// createUserVisibleFiles creates the relative symlinks for all the
 | 
			
		||||
// files configured in the payload. If the directory in a file path does not
 | 
			
		||||
// exist, it is created.
 | 
			
		||||
//
 | 
			
		||||
// Viz:
 | 
			
		||||
// For files: "bar", "foo/bar", "baz/bar", "foo/baz/blah"
 | 
			
		||||
// the following symlinks and subdirectories are created:
 | 
			
		||||
// bar          -> ..data/bar
 | 
			
		||||
// foo/bar      -> ../..data/foo/bar
 | 
			
		||||
// baz/bar      -> ../..data/baz/bar
 | 
			
		||||
// foo/baz/blah -> ../../..data/foo/baz/blah
 | 
			
		||||
func (w *AtomicWriter) createUserVisibleFiles(payload map[string][]byte) error {
 | 
			
		||||
	for userVisiblePath := range payload {
 | 
			
		||||
		dir, _ := filepath.Split(userVisiblePath)
 | 
			
		||||
		subDirs := 0
 | 
			
		||||
		if len(dir) > 0 {
 | 
			
		||||
			// If dir is not empty, the projection path contains at least one
 | 
			
		||||
			// subdirectory (example: userVisiblePath := "foo/bar").
 | 
			
		||||
			// Since filepath.Split leaves a trailing path separator, in this
 | 
			
		||||
			// example, dir = "foo/".  In order to calculate the number of
 | 
			
		||||
			// subdirectories, we must subtract 1 from the number returned by split.
 | 
			
		||||
			subDirs = len(strings.Split(dir, "/")) - 1
 | 
			
		||||
			err := os.MkdirAll(path.Join(w.targetDir, dir), os.ModePerm)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		_, err := os.Readlink(path.Join(w.targetDir, userVisiblePath))
 | 
			
		||||
		if err != nil && os.IsNotExist(err) {
 | 
			
		||||
			// The link into the data directory for this path doesn't exist; create it,
 | 
			
		||||
			// respecting the number of subdirectories necessary to link
 | 
			
		||||
			// correctly back into the data directory.
 | 
			
		||||
			visibleFile := path.Join(w.targetDir, userVisiblePath)
 | 
			
		||||
			dataDirFile := path.Join(strings.Repeat("../", subDirs), dataDirName, userVisiblePath)
 | 
			
		||||
 | 
			
		||||
			err = os.Symlink(dataDirFile, visibleFile)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// removeUserVisiblePaths removes the set of paths from the user-visible
 | 
			
		||||
// portion of the writer's target directory.
 | 
			
		||||
func (w *AtomicWriter) removeUserVisiblePaths(paths sets.String) error {
 | 
			
		||||
	orderedPaths := paths.List()
 | 
			
		||||
	for ii := len(orderedPaths) - 1; ii >= 0; ii-- {
 | 
			
		||||
		if err := os.Remove(path.Join(w.targetDir, orderedPaths[ii])); err != nil {
 | 
			
		||||
			glog.Errorf("%s: error pruning old user-visible path %s: %v", w.logContext, orderedPaths[ii], err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// touchSentinelFile touches the sentinel file or creates it if it doesn't exist.
 | 
			
		||||
func (w *AtomicWriter) touchSentinelFile() error {
 | 
			
		||||
	sentinelFilePath := path.Join(w.targetDir, sentinelFileName)
 | 
			
		||||
	_, err := os.Stat(sentinelFilePath)
 | 
			
		||||
	if err != nil && os.IsNotExist(err) {
 | 
			
		||||
		file, err := os.Create(sentinelFilePath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			glog.Errorf("%s: unexpected error creating sentinel file %s: %v", w.logContext, sentinelFilePath, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		file.Close()
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts := time.Now()
 | 
			
		||||
	err = os.Chtimes(sentinelFilePath, ts, ts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		glog.Errorf("%s: error updating sentinel file mod time: %v", w.logContext, err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										852
									
								
								pkg/volume/util/atomic_writer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										852
									
								
								pkg/volume/util/atomic_writer_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,852 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2016 The Kubernetes Authors All rights reserved.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package util
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/kubernetes/pkg/util/sets"
 | 
			
		||||
	utiltesting "k8s.io/kubernetes/pkg/util/testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestNewAtomicWriter(t *testing.T) {
 | 
			
		||||
	targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error creating tmp dir: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = NewAtomicWriter(targetDir, "-test-")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error creating writer for existing target dir: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nonExistentDir, err := utiltesting.MkTmpdir("atomic-write")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error creating tmp dir: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	err = os.Remove(nonExistentDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("unexpected error ensuring dir %v does not exist: %v", nonExistentDir, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = NewAtomicWriter(nonExistentDir, "-test-")
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		t.Fatalf("unexpected success creating writer for nonexistent target dir: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestValidatePath(t *testing.T) {
 | 
			
		||||
	maxPath := strings.Repeat("a", maxPathLength+1)
 | 
			
		||||
	maxFile := strings.Repeat("a", maxFileNameLength+1)
 | 
			
		||||
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name  string
 | 
			
		||||
		path  string
 | 
			
		||||
		valid bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:  "valid 1",
 | 
			
		||||
			path:  "i/am/well/behaved.txt",
 | 
			
		||||
			valid: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "valid 2",
 | 
			
		||||
			path:  "keepyourheaddownandfollowtherules.txt",
 | 
			
		||||
			valid: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "max path length",
 | 
			
		||||
			path:  maxPath,
 | 
			
		||||
			valid: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "max file length",
 | 
			
		||||
			path:  maxFile,
 | 
			
		||||
			valid: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "absolute failure",
 | 
			
		||||
			path:  "/dev/null",
 | 
			
		||||
			valid: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "reserved path",
 | 
			
		||||
			path:  "..sneaky.txt",
 | 
			
		||||
			valid: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "contains doubledot 1",
 | 
			
		||||
			path:  "hello/there/../../../../../../etc/passwd",
 | 
			
		||||
			valid: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "contains doubledot 2",
 | 
			
		||||
			path:  "hello/../etc/somethingbad",
 | 
			
		||||
			valid: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:  "empty",
 | 
			
		||||
			path:  "",
 | 
			
		||||
			valid: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		err := validatePath(tc.path)
 | 
			
		||||
		if tc.valid && err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected failure: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !tc.valid && err == nil {
 | 
			
		||||
			t.Errorf("%v: unexpected success", tc.name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPathsToRemove(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		payload1 map[string][]byte
 | 
			
		||||
		payload2 map[string][]byte
 | 
			
		||||
		expected sets.String
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "simple",
 | 
			
		||||
			payload1: map[string][]byte{
 | 
			
		||||
				"foo.txt": []byte("foo"),
 | 
			
		||||
				"bar.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			payload2: map[string][]byte{
 | 
			
		||||
				"foo.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			expected: sets.NewString("bar.txt"),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "simple 2",
 | 
			
		||||
			payload1: map[string][]byte{
 | 
			
		||||
				"foo.txt":     []byte("foo"),
 | 
			
		||||
				"zip/bar.txt": []byte("zip/bar"),
 | 
			
		||||
			},
 | 
			
		||||
			payload2: map[string][]byte{
 | 
			
		||||
				"foo.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			expected: sets.NewString("zip/bar.txt", "zip"),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirs 1",
 | 
			
		||||
			payload1: map[string][]byte{
 | 
			
		||||
				"foo.txt":         []byte("foo"),
 | 
			
		||||
				"zip/zap/bar.txt": []byte("zip/bar"),
 | 
			
		||||
			},
 | 
			
		||||
			payload2: map[string][]byte{
 | 
			
		||||
				"foo.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			expected: sets.NewString("zip/zap/bar.txt", "zip", "zip/zap"),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirs 2",
 | 
			
		||||
			payload1: map[string][]byte{
 | 
			
		||||
				"foo.txt":             []byte("foo"),
 | 
			
		||||
				"zip/1/2/3/4/bar.txt": []byte("zip/bar"),
 | 
			
		||||
			},
 | 
			
		||||
			payload2: map[string][]byte{
 | 
			
		||||
				"foo.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			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"),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirs 3",
 | 
			
		||||
			payload1: map[string][]byte{
 | 
			
		||||
				"foo.txt":             []byte("foo"),
 | 
			
		||||
				"zip/1/2/3/4/bar.txt": []byte("zip/bar"),
 | 
			
		||||
				"zap/a/b/c/bar.txt":   []byte("zap/bar"),
 | 
			
		||||
			},
 | 
			
		||||
			payload2: map[string][]byte{
 | 
			
		||||
				"foo.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			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"),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirs 4",
 | 
			
		||||
			payload1: map[string][]byte{
 | 
			
		||||
				"foo.txt":             []byte("foo"),
 | 
			
		||||
				"zap/1/2/3/4/bar.txt": []byte("zip/bar"),
 | 
			
		||||
				"zap/1/2/c/bar.txt":   []byte("zap/bar"),
 | 
			
		||||
				"zap/1/2/magic.txt":   []byte("indigo"),
 | 
			
		||||
			},
 | 
			
		||||
			payload2: map[string][]byte{
 | 
			
		||||
				"foo.txt":           []byte("foo"),
 | 
			
		||||
				"zap/1/2/magic.txt": []byte("indigo"),
 | 
			
		||||
			},
 | 
			
		||||
			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"),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirs 5",
 | 
			
		||||
			payload1: map[string][]byte{
 | 
			
		||||
				"foo.txt":             []byte("foo"),
 | 
			
		||||
				"zap/1/2/3/4/bar.txt": []byte("zip/bar"),
 | 
			
		||||
				"zap/1/2/c/bar.txt":   []byte("zap/bar"),
 | 
			
		||||
			},
 | 
			
		||||
			payload2: map[string][]byte{
 | 
			
		||||
				"foo.txt":           []byte("foo"),
 | 
			
		||||
				"zap/1/2/magic.txt": []byte("indigo"),
 | 
			
		||||
			},
 | 
			
		||||
			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"),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | 
			
		||||
		err = writer.Write(tc.payload1)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error writing: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		actual, err := writer.pathsToRemove(tc.payload2)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error determining paths to remove: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if e, a := tc.expected, actual; !e.Equal(a) {
 | 
			
		||||
			t.Errorf("%v: unexpected paths to remove:\nexpected: %v\n     got: %v", tc.name, e, a)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestWriteOnce(t *testing.T) {
 | 
			
		||||
	// $1 if you can tell me what this binary is
 | 
			
		||||
	encodedMysteryBinary := `f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAAB
 | 
			
		||||
AAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAfQAAAAAAAAB9AAAAAAAAAAAA
 | 
			
		||||
IAAAAAAAsDyZDwU=`
 | 
			
		||||
 | 
			
		||||
	mysteryBinaryBytes := make([]byte, base64.StdEncoding.DecodedLen(len(encodedMysteryBinary)))
 | 
			
		||||
	numBytes, err := base64.StdEncoding.Decode(mysteryBinaryBytes, []byte(encodedMysteryBinary))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Unexpected error decoding binary payload: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if numBytes != 125 {
 | 
			
		||||
		t.Fatalf("Unexpected decoded binary size: expected 125, got %v", numBytes)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		payload map[string][]byte
 | 
			
		||||
		success bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "invalid payload 1",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo":        []byte("foo"),
 | 
			
		||||
				"..bar":      []byte("bar"),
 | 
			
		||||
				"binary.bin": mysteryBinaryBytes,
 | 
			
		||||
			},
 | 
			
		||||
			success: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "invalid payload 2",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo/../bar": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			success: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "basic 1",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo": []byte("foo"),
 | 
			
		||||
				"bar": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			success: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "basic 2",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"binary.bin":  mysteryBinaryBytes,
 | 
			
		||||
				".binary.bin": mysteryBinaryBytes,
 | 
			
		||||
			},
 | 
			
		||||
			success: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "dotfiles",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo":           []byte("foo"),
 | 
			
		||||
				"bar":           []byte("bar"),
 | 
			
		||||
				".dotfile":      []byte("dotfile"),
 | 
			
		||||
				".dotfile.file": []byte("dotfile.file"),
 | 
			
		||||
			},
 | 
			
		||||
			success: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirectories 1",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo/bar"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar/zab.txt"),
 | 
			
		||||
			},
 | 
			
		||||
			success: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirectories 2",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo//bar.txt":      []byte("foo//bar"),
 | 
			
		||||
				"bar///bar/zab.txt": []byte("bar/../bar/zab.txt"),
 | 
			
		||||
			},
 | 
			
		||||
			success: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirectories 3",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":      []byte("foo/bar"),
 | 
			
		||||
				"bar/zab.txt":      []byte("bar/zab.txt"),
 | 
			
		||||
				"foo/blaz/bar.txt": []byte("foo/blaz/bar"),
 | 
			
		||||
				"bar/zib/zab.txt":  []byte("bar/zib/zab.txt"),
 | 
			
		||||
			},
 | 
			
		||||
			success: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "kitchen sink",
 | 
			
		||||
			payload: map[string][]byte{
 | 
			
		||||
				"foo.log":                           []byte("foo"),
 | 
			
		||||
				"bar.zap":                           []byte("bar"),
 | 
			
		||||
				".dotfile":                          []byte("dotfile"),
 | 
			
		||||
				"foo/bar.txt":                       []byte("foo/bar"),
 | 
			
		||||
				"bar/zab.txt":                       []byte("bar/zab.txt"),
 | 
			
		||||
				"foo/blaz/bar.txt":                  []byte("foo/blaz/bar"),
 | 
			
		||||
				"bar/zib/zab.txt":                   []byte("bar/zib/zab.txt"),
 | 
			
		||||
				"1/2/3/4/5/6/7/8/9/10/.dotfile.lib": []byte("1-2-3-dotfile"),
 | 
			
		||||
			},
 | 
			
		||||
			success: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | 
			
		||||
		err = writer.Write(tc.payload)
 | 
			
		||||
		if err != nil && tc.success {
 | 
			
		||||
			t.Errorf("%v: unexpected error writing payload: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		} else if err == nil && !tc.success {
 | 
			
		||||
			t.Errorf("%v: unexpected success", tc.name)
 | 
			
		||||
			continue
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		checkVolumeContents(targetDir, tc.name, tc.payload, t)
 | 
			
		||||
		checkSentinelFile(targetDir, t)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUpdate(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name        string
 | 
			
		||||
		first       map[string][]byte
 | 
			
		||||
		next        map[string][]byte
 | 
			
		||||
		shouldWrite bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "update",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo": []byte("foo"),
 | 
			
		||||
				"bar": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo": []byte("foo2"),
 | 
			
		||||
				"bar": []byte("bar2"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no update",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo": []byte("foo"),
 | 
			
		||||
				"bar": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo": []byte("foo"),
 | 
			
		||||
				"bar": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "no update 2",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "add 1",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar"),
 | 
			
		||||
				"blu/zip.txt": []byte("zip"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "add 2",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":             []byte("foo"),
 | 
			
		||||
				"bar/zab.txt":             []byte("bar"),
 | 
			
		||||
				"blu/two/2/3/4/5/zip.txt": []byte("zip"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "add 3",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":         []byte("foo"),
 | 
			
		||||
				"bar/zab.txt":         []byte("bar"),
 | 
			
		||||
				"bar/2/3/4/5/zip.txt": []byte("zip"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "delete 1",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
				"bar/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "delete 2",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":       []byte("foo"),
 | 
			
		||||
				"bar/1/2/3/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "delete 3",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":       []byte("foo"),
 | 
			
		||||
				"bar/1/2/sip.txt":   []byte("sip"),
 | 
			
		||||
				"bar/1/2/3/zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":     []byte("foo"),
 | 
			
		||||
				"bar/1/2/sip.txt": []byte("sip"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "delete 4",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":            []byte("foo"),
 | 
			
		||||
				"bar/1/2/sip.txt":        []byte("sip"),
 | 
			
		||||
				"bar/1/2/3/4/5/6zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":     []byte("foo"),
 | 
			
		||||
				"bar/1/2/sip.txt": []byte("sip"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "delete all",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt":            []byte("foo"),
 | 
			
		||||
				"bar/1/2/sip.txt":        []byte("sip"),
 | 
			
		||||
				"bar/1/2/3/4/5/6zab.txt": []byte("bar"),
 | 
			
		||||
			},
 | 
			
		||||
			next:        map[string][]byte{},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "add and delete 1",
 | 
			
		||||
			first: map[string][]byte{
 | 
			
		||||
				"foo/bar.txt": []byte("foo"),
 | 
			
		||||
			},
 | 
			
		||||
			next: map[string][]byte{
 | 
			
		||||
				"bar/baz.txt": []byte("baz"),
 | 
			
		||||
			},
 | 
			
		||||
			shouldWrite: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | 
			
		||||
 | 
			
		||||
		err = writer.Write(tc.first)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error writing: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		checkVolumeContents(targetDir, tc.name, tc.first, t)
 | 
			
		||||
		if !tc.shouldWrite {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		oldTs := checkSentinelFile(targetDir, t)
 | 
			
		||||
 | 
			
		||||
		err = writer.Write(tc.next)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if tc.shouldWrite {
 | 
			
		||||
				t.Errorf("%v: unexpected error writing: %v", tc.name, err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		} else if !tc.shouldWrite {
 | 
			
		||||
			t.Errorf("%v: unexpected success", tc.name)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		checkVolumeContents(targetDir, tc.name, tc.next, t)
 | 
			
		||||
 | 
			
		||||
		ts := checkSentinelFile(targetDir, t)
 | 
			
		||||
		if !ts.After(oldTs) {
 | 
			
		||||
			t.Errorf("Unexpected timestamp on sentinel file; expected %v to be after %v", ts, oldTs)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMultipleUpdates(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		payloads      []map[string][]byte
 | 
			
		||||
		clearSentinel bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "update 1",
 | 
			
		||||
			payloads: []map[string][]byte{
 | 
			
		||||
				{
 | 
			
		||||
					"foo": []byte("foo"),
 | 
			
		||||
					"bar": []byte("bar"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo": []byte("foo2"),
 | 
			
		||||
					"bar": []byte("bar2"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo": []byte("foo3"),
 | 
			
		||||
					"bar": []byte("bar3"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "update 2",
 | 
			
		||||
			payloads: []map[string][]byte{
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt": []byte("foo/bar"),
 | 
			
		||||
					"bar/zab.txt": []byte("bar/zab.txt"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt": []byte("foo/bar2"),
 | 
			
		||||
					"bar/zab.txt": []byte("bar/zab.txt2"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "clear sentinel",
 | 
			
		||||
			payloads: []map[string][]byte{
 | 
			
		||||
				{
 | 
			
		||||
					"foo": []byte("foo"),
 | 
			
		||||
					"bar": []byte("bar"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo": []byte("foo2"),
 | 
			
		||||
					"bar": []byte("bar2"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo": []byte("foo3"),
 | 
			
		||||
					"bar": []byte("bar3"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo": []byte("foo4"),
 | 
			
		||||
					"bar": []byte("bar4"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			clearSentinel: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "subdirectories 2",
 | 
			
		||||
			payloads: []map[string][]byte{
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt":      []byte("foo/bar"),
 | 
			
		||||
					"bar/zab.txt":      []byte("bar/zab.txt"),
 | 
			
		||||
					"foo/blaz/bar.txt": []byte("foo/blaz/bar"),
 | 
			
		||||
					"bar/zib/zab.txt":  []byte("bar/zib/zab.txt"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt":      []byte("foo/bar2"),
 | 
			
		||||
					"bar/zab.txt":      []byte("bar/zab.txt2"),
 | 
			
		||||
					"foo/blaz/bar.txt": []byte("foo/blaz/bar2"),
 | 
			
		||||
					"bar/zib/zab.txt":  []byte("bar/zib/zab.txt2"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "add 1",
 | 
			
		||||
			payloads: []map[string][]byte{
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt":            []byte("foo/bar"),
 | 
			
		||||
					"bar//zab.txt":           []byte("bar/zab.txt"),
 | 
			
		||||
					"foo/blaz/bar.txt":       []byte("foo/blaz/bar"),
 | 
			
		||||
					"bar/zib////zib/zab.txt": []byte("bar/zib/zab.txt"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt":      []byte("foo/bar2"),
 | 
			
		||||
					"bar/zab.txt":      []byte("bar/zab.txt2"),
 | 
			
		||||
					"foo/blaz/bar.txt": []byte("foo/blaz/bar2"),
 | 
			
		||||
					"bar/zib/zab.txt":  []byte("bar/zib/zab.txt2"),
 | 
			
		||||
					"add/new/keys.txt": []byte("addNewKeys"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "add 2",
 | 
			
		||||
			payloads: []map[string][]byte{
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt":      []byte("foo/bar2"),
 | 
			
		||||
					"bar/zab.txt":      []byte("bar/zab.txt2"),
 | 
			
		||||
					"foo/blaz/bar.txt": []byte("foo/blaz/bar2"),
 | 
			
		||||
					"bar/zib/zab.txt":  []byte("bar/zib/zab.txt2"),
 | 
			
		||||
					"add/new/keys.txt": []byte("addNewKeys"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt":       []byte("foo/bar2"),
 | 
			
		||||
					"bar/zab.txt":       []byte("bar/zab.txt2"),
 | 
			
		||||
					"foo/blaz/bar.txt":  []byte("foo/blaz/bar2"),
 | 
			
		||||
					"bar/zib/zab.txt":   []byte("bar/zib/zab.txt2"),
 | 
			
		||||
					"add/new/keys.txt":  []byte("addNewKeys"),
 | 
			
		||||
					"add/new/keys2.txt": []byte("addNewKeys2"),
 | 
			
		||||
					"add/new/keys3.txt": []byte("addNewKeys3"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "remove 1",
 | 
			
		||||
			payloads: []map[string][]byte{
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt":         []byte("foo/bar"),
 | 
			
		||||
					"bar//zab.txt":        []byte("bar/zab.txt"),
 | 
			
		||||
					"foo/blaz/bar.txt":    []byte("foo/blaz/bar"),
 | 
			
		||||
					"zip/zap/zup/fop.txt": []byte("zip/zap/zup/fop.txt"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt": []byte("foo/bar2"),
 | 
			
		||||
					"bar/zab.txt": []byte("bar/zab.txt2"),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					"foo/bar.txt": []byte("foo/bar"),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var oldTs *time.Time = nil
 | 
			
		||||
		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | 
			
		||||
 | 
			
		||||
		for ii, payload := range tc.payloads {
 | 
			
		||||
			writer.Write(payload)
 | 
			
		||||
 | 
			
		||||
			checkVolumeContents(targetDir, tc.name, payload, t)
 | 
			
		||||
			ts := checkSentinelFile(targetDir, t)
 | 
			
		||||
 | 
			
		||||
			if oldTs != nil && !ts.After(*oldTs) {
 | 
			
		||||
				t.Errorf("%v[%v] unexpected timestamp on sentinel file; expected %v to be after %v", tc.name, ii, ts, oldTs)
 | 
			
		||||
			}
 | 
			
		||||
			oldTs = &ts
 | 
			
		||||
 | 
			
		||||
			if tc.clearSentinel {
 | 
			
		||||
				clearSentinelFile(targetDir, t)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSentinelFileModTimeIncreasing(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name              string
 | 
			
		||||
		iterations        int
 | 
			
		||||
		clearSentinelFile bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:       "5 iters",
 | 
			
		||||
			iterations: 5,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "50 iters",
 | 
			
		||||
			iterations: 50,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "1000 iters",
 | 
			
		||||
			iterations: 1000,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:              "1000 clear sentinel",
 | 
			
		||||
			iterations:        1000,
 | 
			
		||||
			clearSentinelFile: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:              "10000 clear sentinel",
 | 
			
		||||
			iterations:        10000,
 | 
			
		||||
			clearSentinelFile: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var oldTs *time.Time = nil
 | 
			
		||||
		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | 
			
		||||
 | 
			
		||||
		for i := 0; i < tc.iterations; i++ {
 | 
			
		||||
			err = writer.touchSentinelFile()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Errorf("%v: unexpected error touching sentinel file: %v", tc.name, err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			ts := checkSentinelFile(targetDir, t)
 | 
			
		||||
			if oldTs != nil && !ts.After(*oldTs) {
 | 
			
		||||
				t.Errorf("%v: unexpected timestamp on sentinel file; expected %v to be after %v", tc.name, ts, oldTs)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			oldTs = &ts
 | 
			
		||||
 | 
			
		||||
			if tc.clearSentinelFile {
 | 
			
		||||
				clearSentinelFile(targetDir, t)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkVolumeContents(targetDir, tcName string, payload map[string][]byte, t *testing.T) {
 | 
			
		||||
	// use filepath.Walk to reconstruct the payload, then deep equal
 | 
			
		||||
	observedPayload := map[string][]byte{}
 | 
			
		||||
	visitor := func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		if info.Mode().IsRegular() || info.IsDir() {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		relativePath := strings.TrimPrefix(path, targetDir)
 | 
			
		||||
		relativePath = strings.TrimPrefix(relativePath, "/")
 | 
			
		||||
		if strings.HasPrefix(relativePath, "..") {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content, err := ioutil.ReadFile(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		observedPayload[relativePath] = content
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := filepath.Walk(targetDir, visitor)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("%v: unexpected error walking directory: %v", tcName, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cleanPathPayload := make(map[string][]byte, len(payload))
 | 
			
		||||
	for k, v := range payload {
 | 
			
		||||
		cleanPathPayload[path.Clean(k)] = v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(cleanPathPayload, observedPayload) {
 | 
			
		||||
		t.Errorf("%v: payload and observed payload do not match.", tcName)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkSentinelFile(targetDir string, t *testing.T) time.Time {
 | 
			
		||||
	sentinelFilePath := filepath.Join(targetDir, sentinelFileName)
 | 
			
		||||
	info, err := os.Stat(sentinelFilePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Couldn't stat sentinel file for dir %v: %v", targetDir, err)
 | 
			
		||||
		return time.Now()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return info.ModTime()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func clearSentinelFile(targetDir string, t *testing.T) {
 | 
			
		||||
	sentinelFilePath := filepath.Join(targetDir, sentinelFileName)
 | 
			
		||||
	_, err := os.Stat(sentinelFilePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Couldn't stat sentinel file for dir %v: %v", targetDir, err)
 | 
			
		||||
	}
 | 
			
		||||
	err = os.Remove(sentinelFilePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Error removing sentinel file: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user