...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/cert/writer/atomic/atomic_writer.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/cert/writer/atomic

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     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  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    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  // AtomicWriter handles atomically projecting content for a set of files into
    40  // a target directory.
    41  //
    42  // Note:
    43  //
    44  //  1. AtomicWriter reserves the set of pathnames starting with `..`.
    45  //  2. AtomicWriter offers no concurrency guarantees and must be synchronized
    46  //     by the caller.
    47  //
    48  // The visible files in this volume are symlinks to files in the writer's data
    49  // directory.  Actual files are stored in a hidden timestamped directory which
    50  // is symlinked to by the data directory. The timestamped directory and
    51  // data directory symlink are created in the writer's target dir.  This scheme
    52  // allows the files to be atomically updated by changing the target of the
    53  // data directory symlink.
    54  //
    55  // Consumers of the target directory can monitor the ..data symlink using
    56  // inotify or fanotify to receive events when the content in the volume is
    57  // updated.
    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  // NewAtomicWriter creates a new AtomicWriter configured to write to the given
    69  // target directory, or returns an error if the target directory does not exist.
    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  // Write does an atomic projection of the given payload into the writer's target
    85  // directory.  Input paths must not begin with '..'.
    86  //
    87  // The Write algorithm is:
    88  //
    89  //  1. The payload is validated; if the payload is invalid, the function returns
    90  //     2.  The current timestamped directory is detected by reading the data directory
    91  //     symlink
    92  //
    93  //  3. The old version of the volume is walked to determine whether any
    94  //     portion of the payload was deleted and is still present on disk.
    95  //
    96  //  4. The data in the current timestamped directory is compared to the projected
    97  //     data to determine if an update is required.
    98  //     5.  A new timestamped dir is created
    99  //
   100  //  6. The payload is written to the new timestamped directory
   101  //     7.  Symlinks and directory for new user-visible files are created (if needed).
   102  //
   103  //     For example, consider the files:
   104  //     <target-dir>/podName
   105  //     <target-dir>/user/labels
   106  //     <target-dir>/k8s/annotations
   107  //
   108  //     The user visible files are symbolic links into the internal data directory:
   109  //     <target-dir>/podName         -> ..data/podName
   110  //     <target-dir>/usr -> ..data/usr
   111  //     <target-dir>/k8s -> ..data/k8s
   112  //
   113  //     The data directory itself is a link to a timestamped directory with
   114  //     the real data:
   115  //     <target-dir>/..data          -> ..2016_02_01_15_04_05.12345678/
   116  //     8.  A symlink to the new timestamped directory ..data_tmp is created that will
   117  //     become the new data directory
   118  //     9.  The new data directory symlink is renamed to the data directory; rename is atomic
   119  //
   120  // 10.  Old paths are removed from the user-visible portion of the target directory
   121  // 11.  The previous timestamped directory is removed, if it exists
   122  func (w *AtomicWriter) Write(payload map[string]FileProjection) error {
   123  	// (1)
   124  	cleanPayload, err := validatePayload(payload)
   125  	if err != nil {
   126  		w.log.Error(err, "invalid payload")
   127  		return err
   128  	}
   129  
   130  	// (2)
   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  		// although Readlink() returns "" on err, don't be fragile by relying on it (since it's not specified in docs)
   139  		// empty oldTsDir indicates that it didn't exist
   140  		oldTsDir = ""
   141  	}
   142  	oldTsPath := path.Join(w.targetDir, oldTsDir)
   143  
   144  	var pathsToRemove sets.String
   145  	// if there was no old version, there's nothing to remove
   146  	if len(oldTsDir) != 0 {
   147  		// (3)
   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  		// (4)
   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  	// (5)
   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  	// (6)
   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  	// (7)
   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  	// (8)
   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  	// (9)
   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  	// (10)
   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  	// (11)
   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  // validatePayload returns an error if any path in the payload  returns a copy of the payload with the paths cleaned.
   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  // validatePath validates a single path, returning an error if the path is
   243  // invalid.  paths may not:
   244  //
   245  // 1. be absolute
   246  // 2. contain '..' as an element
   247  // 3. start with '..'
   248  // 4. contain filenames larger than 255 characters
   249  // 5. be longer than 4096 characters
   250  func validatePath(targetPath string) error {
   251  	// TODO: somehow unify this with the similar api validation,
   252  	// validateVolumeSourcePath; the error semantics are just different enough
   253  	// from this that it was time-prohibitive trying to find the right
   254  	// refactoring to re-use.
   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  // shouldWritePayload returns whether the payload should be written to disk.
   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  // shouldWriteFile returns whether a new version of a file should be written to disk.
   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  // pathsToRemove walks the current version of the data directory and
   314  // determines which paths should be removed (if any) after the payload is
   315  // written to the target directory.
   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  		// add all subpaths for the payload to the set of new paths
   340  		// to avoid attempting to remove non-empty dirs
   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  // newTimestampDir creates a new timestamp directory
   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  	// 0755 permissions are needed to allow 'group' and 'other' to recurse the
   364  	// directory tree.  do a chmod here to ensure that permissions are set correctly
   365  	// regardless of the process' umask.
   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  // writePayloadToDir writes the given payload to the given directory.  The
   376  // directory must exist.
   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  		// Chmod is needed because ioutil.WriteFile() ends up calling
   396  		// open(2) to create the file, so the final mode used is "mode &
   397  		// ~umask". But we want to make sure the specified mode is used
   398  		// in the file no matter what the umask is.
   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  // createUserVisibleFiles creates the relative symlinks for all the
   409  // files configured in the payload. If the directory in a file path does not
   410  // exist, it is created.
   411  //
   412  // Viz:
   413  // For files: "bar", "foo/bar", "baz/bar", "foo/baz/blah"
   414  // the following symlinks are created:
   415  // bar -> ..data/bar
   416  // foo -> ..data/foo
   417  // baz -> ..data/baz
   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  			// The link into the data directory for this path doesn't exist; create it
   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  // removeUserVisiblePaths removes the set of paths from the user-visible
   441  // portion of the writer's target directory.
   442  func (w *AtomicWriter) removeUserVisiblePaths(paths sets.String) error {
   443  	ps := string(os.PathSeparator)
   444  	var lasterr error
   445  	for p := range paths {
   446  		// only remove symlinks from the volume root directory (i.e. items that don't contain '/')
   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