123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- /*
- Copyright 2016 The Kubernetes Authors.
- 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"
- "runtime"
- "strings"
- "time"
- "k8s.io/klog"
- "k8s.io/apimachinery/pkg/util/sets"
- )
- const (
- maxFileNameLength = 255
- maxPathLength = 4096
- )
- // AtomicWriter handles atomically projecting content for a set of files into
- // a target directory.
- //
- // 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.
- //
- // Consumers of the target directory can monitor the ..data symlink using
- // inotify or fanotify to receive events when the content in the volume is
- // updated.
- type AtomicWriter struct {
- targetDir string
- logContext string
- }
- // FileProjection contains file Data and access Mode
- type FileProjection struct {
- Data []byte
- Mode int32
- }
- // 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 string, logContext string) (*AtomicWriter, error) {
- _, err := os.Stat(targetDir)
- if os.IsNotExist(err) {
- return nil, err
- }
- return &AtomicWriter{targetDir: targetDir, logContext: logContext}, nil
- }
- const (
- 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 current timestamped directory is detected by reading the data directory
- // symlink
- // 3. The old version of the volume is walked to determine whether any
- // portion of the payload was deleted and is still present on disk.
- // 4. The data in the current timestamped directory is compared to the projected
- // data to determine if an update is required.
- // 5. A new timestamped dir is created
- // 6. The payload is written to the new timestamped directory
- // 7. 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 -> ..data/usr
- // <target-dir>/k8s -> ..data/k8s
- //
- // 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/
- // 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. Old paths are removed from the user-visible portion of the target directory
- // 11. The previous timestamped directory is removed, if it exists
- func (w *AtomicWriter) Write(payload map[string]FileProjection) error {
- // (1)
- cleanPayload, err := validatePayload(payload)
- if err != nil {
- klog.Errorf("%s: invalid payload: %v", w.logContext, err)
- return err
- }
- // (2)
- dataDirPath := filepath.Join(w.targetDir, dataDirName)
- oldTsDir, err := os.Readlink(dataDirPath)
- if err != nil {
- if !os.IsNotExist(err) {
- klog.Errorf("%s: error reading link for data directory: %v", w.logContext, err)
- return err
- }
- // although Readlink() returns "" on err, don't be fragile by relying on it (since it's not specified in docs)
- // empty oldTsDir indicates that it didn't exist
- oldTsDir = ""
- }
- oldTsPath := filepath.Join(w.targetDir, oldTsDir)
- var pathsToRemove sets.String
- // if there was no old version, there's nothing to remove
- if len(oldTsDir) != 0 {
- // (3)
- pathsToRemove, err = w.pathsToRemove(cleanPayload, oldTsPath)
- if err != nil {
- klog.Errorf("%s: error determining user-visible files to remove: %v", w.logContext, err)
- return err
- }
- // (4)
- if should, err := shouldWritePayload(cleanPayload, oldTsPath); err != nil {
- klog.Errorf("%s: error determining whether payload should be written to disk: %v", w.logContext, err)
- return err
- } else if !should && len(pathsToRemove) == 0 {
- klog.V(4).Infof("%s: no update required for target directory %v", w.logContext, w.targetDir)
- return nil
- } else {
- klog.V(4).Infof("%s: write required for target directory %v", w.logContext, w.targetDir)
- }
- }
- // (5)
- tsDir, err := w.newTimestampDir()
- if err != nil {
- klog.V(4).Infof("%s: error creating new ts data directory: %v", w.logContext, err)
- return err
- }
- tsDirName := filepath.Base(tsDir)
- // (6)
- if err = w.writePayloadToDir(cleanPayload, tsDir); err != nil {
- klog.Errorf("%s: error writing payload to ts data directory %s: %v", w.logContext, tsDir, err)
- return err
- }
- klog.V(4).Infof("%s: performed write of new data to ts data directory: %s", w.logContext, tsDir)
- // (7)
- if err = w.createUserVisibleFiles(cleanPayload); err != nil {
- klog.Errorf("%s: error creating visible symlinks in %s: %v", w.logContext, w.targetDir, err)
- return err
- }
- // (8)
- newDataDirPath := filepath.Join(w.targetDir, newDataDirName)
- if err = os.Symlink(tsDirName, newDataDirPath); err != nil {
- os.RemoveAll(tsDir)
- klog.Errorf("%s: error creating symbolic link for atomic update: %v", w.logContext, err)
- return err
- }
- // (9)
- if runtime.GOOS == "windows" {
- os.Remove(dataDirPath)
- err = os.Symlink(tsDirName, dataDirPath)
- os.Remove(newDataDirPath)
- } else {
- err = os.Rename(newDataDirPath, dataDirPath)
- }
- if err != nil {
- os.Remove(newDataDirPath)
- os.RemoveAll(tsDir)
- klog.Errorf("%s: error renaming symbolic link for data directory %s: %v", w.logContext, newDataDirPath, err)
- return err
- }
- // (10)
- if err = w.removeUserVisiblePaths(pathsToRemove); err != nil {
- klog.Errorf("%s: error removing old visible symlinks: %v", w.logContext, err)
- return err
- }
- // (11)
- if len(oldTsDir) > 0 {
- if err = os.RemoveAll(oldTsPath); err != nil {
- klog.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]FileProjection) (map[string]FileProjection, error) {
- cleanPayload := make(map[string]FileProjection)
- for k, content := range payload {
- if err := validatePath(k); err != nil {
- return nil, err
- }
- cleanPayload[filepath.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 or equal to %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 or equal to %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 shouldWritePayload(payload map[string]FileProjection, oldTsDir string) (bool, error) {
- for userVisiblePath, fileProjection := range payload {
- shouldWrite, err := shouldWriteFile(filepath.Join(oldTsDir, userVisiblePath), fileProjection.Data)
- 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 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.Equal(content, contentOnFs), nil
- }
- // pathsToRemove walks the current version of the data 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]FileProjection, oldTsDir string) (sets.String, error) {
- paths := sets.NewString()
- visitor := func(path string, info os.FileInfo, err error) error {
- relativePath := strings.TrimPrefix(path, oldTsDir)
- relativePath = strings.TrimPrefix(relativePath, string(os.PathSeparator))
- if relativePath == "" {
- return nil
- }
- paths.Insert(relativePath)
- return nil
- }
- err := filepath.Walk(oldTsDir, visitor)
- if os.IsNotExist(err) {
- return nil, nil
- } else if err != nil {
- return nil, err
- }
- klog.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, string(os.PathSeparator))
- }
- }
- klog.V(5).Infof("%s: new paths: %+v", w.targetDir, newPaths.List())
- result := paths.Difference(newPaths)
- klog.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, time.Now().UTC().Format("..2006_01_02_15_04_05."))
- if err != nil {
- klog.Errorf("%s: unable to create new temp directory: %v", w.logContext, err)
- return "", err
- }
- // 0755 permissions are needed to allow 'group' and 'other' to recurse the
- // directory tree. do a chmod here to ensure that permissions are set correctly
- // regardless of the process' umask.
- err = os.Chmod(tsDir, 0755)
- if err != nil {
- klog.Errorf("%s: unable to set mode on 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]FileProjection, dir string) error {
- for userVisiblePath, fileProjection := range payload {
- content := fileProjection.Data
- mode := os.FileMode(fileProjection.Mode)
- fullPath := filepath.Join(dir, userVisiblePath)
- baseDir, _ := filepath.Split(fullPath)
- err := os.MkdirAll(baseDir, os.ModePerm)
- if err != nil {
- klog.Errorf("%s: unable to create directory %s: %v", w.logContext, baseDir, err)
- return err
- }
- err = ioutil.WriteFile(fullPath, content, mode)
- if err != nil {
- klog.Errorf("%s: unable to write file %s with mode %v: %v", w.logContext, fullPath, mode, err)
- return err
- }
- // Chmod is needed because ioutil.WriteFile() ends up calling
- // open(2) to create the file, so the final mode used is "mode &
- // ~umask". But we want to make sure the specified mode is used
- // in the file no matter what the umask is.
- err = os.Chmod(fullPath, mode)
- if err != nil {
- klog.Errorf("%s: unable to write file %s with mode %v: %v", w.logContext, fullPath, mode, 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 are created:
- // bar -> ..data/bar
- // foo -> ..data/foo
- // baz -> ..data/baz
- func (w *AtomicWriter) createUserVisibleFiles(payload map[string]FileProjection) error {
- for userVisiblePath := range payload {
- slashpos := strings.Index(userVisiblePath, string(os.PathSeparator))
- if slashpos == -1 {
- slashpos = len(userVisiblePath)
- }
- linkname := userVisiblePath[:slashpos]
- _, err := os.Readlink(filepath.Join(w.targetDir, linkname))
- if err != nil && os.IsNotExist(err) {
- // The link into the data directory for this path doesn't exist; create it
- visibleFile := filepath.Join(w.targetDir, linkname)
- dataDirFile := filepath.Join(dataDirName, linkname)
- 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 {
- ps := string(os.PathSeparator)
- var lasterr error
- for p := range paths {
- // only remove symlinks from the volume root directory (i.e. items that don't contain '/')
- if strings.Contains(p, ps) {
- continue
- }
- if err := os.Remove(filepath.Join(w.targetDir, p)); err != nil {
- klog.Errorf("%s: error pruning old user-visible path %s: %v", w.logContext, p, err)
- lasterr = err
- }
- }
- return lasterr
- }
|