k8s.io/kubernetes@v1.29.3/pkg/volume/util/atomic_writer.go (about)

     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 util
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"runtime"
    26  	"strings"
    27  	"time"
    28  
    29  	"k8s.io/klog/v2"
    30  
    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  	logContext string
    61  }
    62  
    63  // FileProjection contains file Data and access Mode
    64  type FileProjection struct {
    65  	Data   []byte
    66  	Mode   int32
    67  	FsUser *int64
    68  }
    69  
    70  // NewAtomicWriter creates a new AtomicWriter configured to write to the given
    71  // target directory, or returns an error if the target directory does not exist.
    72  func NewAtomicWriter(targetDir string, logContext string) (*AtomicWriter, error) {
    73  	_, err := os.Stat(targetDir)
    74  	if os.IsNotExist(err) {
    75  		return nil, err
    76  	}
    77  
    78  	return &AtomicWriter{targetDir: targetDir, logContext: logContext}, nil
    79  }
    80  
    81  const (
    82  	dataDirName    = "..data"
    83  	newDataDirName = "..data_tmp"
    84  )
    85  
    86  // Write does an atomic projection of the given payload into the writer's target
    87  // directory.  Input paths must not begin with '..'.
    88  // setPerms is an optional pointer to a function that caller can provide to set the
    89  // permissions of the newly created files before they are published. The function is
    90  // passed subPath which is the name of the timestamped directory that was created
    91  // under target directory.
    92  //
    93  // The Write algorithm is:
    94  //
    95  //  1. The payload is validated; if the payload is invalid, the function returns
    96  //
    97  //  2. The current timestamped directory is detected by reading the data directory
    98  //     symlink
    99  //
   100  //  3. The old version of the volume is walked to determine whether any
   101  //     portion of the payload was deleted and is still present on disk.
   102  //
   103  //  4. The data in the current timestamped directory is compared to the projected
   104  //     data to determine if an update is required.
   105  //
   106  //  5. A new timestamped dir is created.
   107  //
   108  //  6. The payload is written to the new timestamped directory.
   109  //
   110  //  7. Permissions are set (if setPerms is not nil) on the new timestamped directory and files.
   111  //
   112  //  8. A symlink to the new timestamped directory ..data_tmp is created that will
   113  //     become the new data directory.
   114  //
   115  //  9. The new data directory symlink is renamed to the data directory; rename is atomic.
   116  //
   117  //  10. Symlinks and directory for new user-visible files are created (if needed).
   118  //
   119  //     For example, consider the files:
   120  //     <target-dir>/podName
   121  //     <target-dir>/user/labels
   122  //     <target-dir>/k8s/annotations
   123  //
   124  //     The user visible files are symbolic links into the internal data directory:
   125  //     <target-dir>/podName         -> ..data/podName
   126  //     <target-dir>/usr -> ..data/usr
   127  //     <target-dir>/k8s -> ..data/k8s
   128  //
   129  //     The data directory itself is a link to a timestamped directory with
   130  //     the real data:
   131  //     <target-dir>/..data          -> ..2016_02_01_15_04_05.12345678/
   132  //     NOTE(claudiub): We need to create these symlinks AFTER we've finished creating and
   133  //     linking everything else. On Windows, if a target does not exist, the created symlink
   134  //     will not work properly if the target ends up being a directory.
   135  //
   136  //  11. Old paths are removed from the user-visible portion of the target directory.
   137  //
   138  //  12. The previous timestamped directory is removed, if it exists.
   139  func (w *AtomicWriter) Write(payload map[string]FileProjection, setPerms func(subPath string) error) error {
   140  	// (1)
   141  	cleanPayload, err := validatePayload(payload)
   142  	if err != nil {
   143  		klog.Errorf("%s: invalid payload: %v", w.logContext, err)
   144  		return err
   145  	}
   146  
   147  	// (2)
   148  	dataDirPath := filepath.Join(w.targetDir, dataDirName)
   149  	oldTsDir, err := os.Readlink(dataDirPath)
   150  	if err != nil {
   151  		if !os.IsNotExist(err) {
   152  			klog.Errorf("%s: error reading link for data directory: %v", w.logContext, err)
   153  			return err
   154  		}
   155  		// although Readlink() returns "" on err, don't be fragile by relying on it (since it's not specified in docs)
   156  		// empty oldTsDir indicates that it didn't exist
   157  		oldTsDir = ""
   158  	}
   159  	oldTsPath := filepath.Join(w.targetDir, oldTsDir)
   160  
   161  	var pathsToRemove sets.String
   162  	// if there was no old version, there's nothing to remove
   163  	if len(oldTsDir) != 0 {
   164  		// (3)
   165  		pathsToRemove, err = w.pathsToRemove(cleanPayload, oldTsPath)
   166  		if err != nil {
   167  			klog.Errorf("%s: error determining user-visible files to remove: %v", w.logContext, err)
   168  			return err
   169  		}
   170  
   171  		// (4)
   172  		if should, err := shouldWritePayload(cleanPayload, oldTsPath); err != nil {
   173  			klog.Errorf("%s: error determining whether payload should be written to disk: %v", w.logContext, err)
   174  			return err
   175  		} else if !should && len(pathsToRemove) == 0 {
   176  			klog.V(4).Infof("%s: no update required for target directory %v", w.logContext, w.targetDir)
   177  			return nil
   178  		} else {
   179  			klog.V(4).Infof("%s: write required for target directory %v", w.logContext, w.targetDir)
   180  		}
   181  	}
   182  
   183  	// (5)
   184  	tsDir, err := w.newTimestampDir()
   185  	if err != nil {
   186  		klog.V(4).Infof("%s: error creating new ts data directory: %v", w.logContext, err)
   187  		return err
   188  	}
   189  	tsDirName := filepath.Base(tsDir)
   190  
   191  	// (6)
   192  	if err = w.writePayloadToDir(cleanPayload, tsDir); err != nil {
   193  		klog.Errorf("%s: error writing payload to ts data directory %s: %v", w.logContext, tsDir, err)
   194  		return err
   195  	}
   196  	klog.V(4).Infof("%s: performed write of new data to ts data directory: %s", w.logContext, tsDir)
   197  
   198  	// (7)
   199  	if setPerms != nil {
   200  		if err := setPerms(tsDirName); err != nil {
   201  			klog.Errorf("%s: error applying ownership settings: %v", w.logContext, err)
   202  			return err
   203  		}
   204  	}
   205  
   206  	// (8)
   207  	newDataDirPath := filepath.Join(w.targetDir, newDataDirName)
   208  	if err = os.Symlink(tsDirName, newDataDirPath); err != nil {
   209  		os.RemoveAll(tsDir)
   210  		klog.Errorf("%s: error creating symbolic link for atomic update: %v", w.logContext, err)
   211  		return err
   212  	}
   213  
   214  	// (9)
   215  	if runtime.GOOS == "windows" {
   216  		os.Remove(dataDirPath)
   217  		err = os.Symlink(tsDirName, dataDirPath)
   218  		os.Remove(newDataDirPath)
   219  	} else {
   220  		err = os.Rename(newDataDirPath, dataDirPath)
   221  	}
   222  	if err != nil {
   223  		os.Remove(newDataDirPath)
   224  		os.RemoveAll(tsDir)
   225  		klog.Errorf("%s: error renaming symbolic link for data directory %s: %v", w.logContext, newDataDirPath, err)
   226  		return err
   227  	}
   228  
   229  	// (10)
   230  	if err = w.createUserVisibleFiles(cleanPayload); err != nil {
   231  		klog.Errorf("%s: error creating visible symlinks in %s: %v", w.logContext, w.targetDir, err)
   232  		return err
   233  	}
   234  
   235  	// (11)
   236  	if err = w.removeUserVisiblePaths(pathsToRemove); err != nil {
   237  		klog.Errorf("%s: error removing old visible symlinks: %v", w.logContext, err)
   238  		return err
   239  	}
   240  
   241  	// (12)
   242  	if len(oldTsDir) > 0 {
   243  		if err = os.RemoveAll(oldTsPath); err != nil {
   244  			klog.Errorf("%s: error removing old data directory %s: %v", w.logContext, oldTsDir, err)
   245  			return err
   246  		}
   247  	}
   248  
   249  	return nil
   250  }
   251  
   252  // validatePayload returns an error if any path in the payload returns a copy of the payload with the paths cleaned.
   253  func validatePayload(payload map[string]FileProjection) (map[string]FileProjection, error) {
   254  	cleanPayload := make(map[string]FileProjection)
   255  	for k, content := range payload {
   256  		if err := validatePath(k); err != nil {
   257  			return nil, err
   258  		}
   259  
   260  		cleanPayload[filepath.Clean(k)] = content
   261  	}
   262  
   263  	return cleanPayload, nil
   264  }
   265  
   266  // validatePath validates a single path, returning an error if the path is
   267  // invalid.  paths may not:
   268  //
   269  // 1. be absolute
   270  // 2. contain '..' as an element
   271  // 3. start with '..'
   272  // 4. contain filenames larger than 255 characters
   273  // 5. be longer than 4096 characters
   274  func validatePath(targetPath string) error {
   275  	// TODO: somehow unify this with the similar api validation,
   276  	// validateVolumeSourcePath; the error semantics are just different enough
   277  	// from this that it was time-prohibitive trying to find the right
   278  	// refactoring to re-use.
   279  	if targetPath == "" {
   280  		return fmt.Errorf("invalid path: must not be empty: %q", targetPath)
   281  	}
   282  	if path.IsAbs(targetPath) {
   283  		return fmt.Errorf("invalid path: must be relative path: %s", targetPath)
   284  	}
   285  
   286  	if len(targetPath) > maxPathLength {
   287  		return fmt.Errorf("invalid path: must be less than or equal to %d characters", maxPathLength)
   288  	}
   289  
   290  	items := strings.Split(targetPath, string(os.PathSeparator))
   291  	for _, item := range items {
   292  		if item == ".." {
   293  			return fmt.Errorf("invalid path: must not contain '..': %s", targetPath)
   294  		}
   295  		if len(item) > maxFileNameLength {
   296  			return fmt.Errorf("invalid path: filenames must be less than or equal to %d characters", maxFileNameLength)
   297  		}
   298  	}
   299  	if strings.HasPrefix(items[0], "..") && len(items[0]) > 2 {
   300  		return fmt.Errorf("invalid path: must not start with '..': %s", targetPath)
   301  	}
   302  
   303  	return nil
   304  }
   305  
   306  // shouldWritePayload returns whether the payload should be written to disk.
   307  func shouldWritePayload(payload map[string]FileProjection, oldTsDir string) (bool, error) {
   308  	for userVisiblePath, fileProjection := range payload {
   309  		shouldWrite, err := shouldWriteFile(filepath.Join(oldTsDir, userVisiblePath), fileProjection.Data)
   310  		if err != nil {
   311  			return false, err
   312  		}
   313  
   314  		if shouldWrite {
   315  			return true, nil
   316  		}
   317  	}
   318  
   319  	return false, nil
   320  }
   321  
   322  // shouldWriteFile returns whether a new version of a file should be written to disk.
   323  func shouldWriteFile(path string, content []byte) (bool, error) {
   324  	_, err := os.Lstat(path)
   325  	if os.IsNotExist(err) {
   326  		return true, nil
   327  	}
   328  
   329  	contentOnFs, err := os.ReadFile(path)
   330  	if err != nil {
   331  		return false, err
   332  	}
   333  
   334  	return !bytes.Equal(content, contentOnFs), nil
   335  }
   336  
   337  // pathsToRemove walks the current version of the data directory and
   338  // determines which paths should be removed (if any) after the payload is
   339  // written to the target directory.
   340  func (w *AtomicWriter) pathsToRemove(payload map[string]FileProjection, oldTsDir string) (sets.String, error) {
   341  	paths := sets.NewString()
   342  	visitor := func(path string, info os.FileInfo, err error) error {
   343  		relativePath := strings.TrimPrefix(path, oldTsDir)
   344  		relativePath = strings.TrimPrefix(relativePath, string(os.PathSeparator))
   345  		if relativePath == "" {
   346  			return nil
   347  		}
   348  
   349  		paths.Insert(relativePath)
   350  		return nil
   351  	}
   352  
   353  	err := filepath.Walk(oldTsDir, visitor)
   354  	if os.IsNotExist(err) {
   355  		return nil, nil
   356  	} else if err != nil {
   357  		return nil, err
   358  	}
   359  	klog.V(5).Infof("%s: current paths:   %+v", w.targetDir, paths.List())
   360  
   361  	newPaths := sets.NewString()
   362  	for file := range payload {
   363  		// add all subpaths for the payload to the set of new paths
   364  		// to avoid attempting to remove non-empty dirs
   365  		for subPath := file; subPath != ""; {
   366  			newPaths.Insert(subPath)
   367  			subPath, _ = filepath.Split(subPath)
   368  			subPath = strings.TrimSuffix(subPath, string(os.PathSeparator))
   369  		}
   370  	}
   371  	klog.V(5).Infof("%s: new paths:       %+v", w.targetDir, newPaths.List())
   372  
   373  	result := paths.Difference(newPaths)
   374  	klog.V(5).Infof("%s: paths to remove: %+v", w.targetDir, result)
   375  
   376  	return result, nil
   377  }
   378  
   379  // newTimestampDir creates a new timestamp directory
   380  func (w *AtomicWriter) newTimestampDir() (string, error) {
   381  	tsDir, err := os.MkdirTemp(w.targetDir, time.Now().UTC().Format("..2006_01_02_15_04_05."))
   382  	if err != nil {
   383  		klog.Errorf("%s: unable to create new temp directory: %v", w.logContext, err)
   384  		return "", err
   385  	}
   386  
   387  	// 0755 permissions are needed to allow 'group' and 'other' to recurse the
   388  	// directory tree.  do a chmod here to ensure that permissions are set correctly
   389  	// regardless of the process' umask.
   390  	err = os.Chmod(tsDir, 0755)
   391  	if err != nil {
   392  		klog.Errorf("%s: unable to set mode on new temp directory: %v", w.logContext, err)
   393  		return "", err
   394  	}
   395  
   396  	return tsDir, nil
   397  }
   398  
   399  // writePayloadToDir writes the given payload to the given directory.  The
   400  // directory must exist.
   401  func (w *AtomicWriter) writePayloadToDir(payload map[string]FileProjection, dir string) error {
   402  	for userVisiblePath, fileProjection := range payload {
   403  		content := fileProjection.Data
   404  		mode := os.FileMode(fileProjection.Mode)
   405  		fullPath := filepath.Join(dir, userVisiblePath)
   406  		baseDir, _ := filepath.Split(fullPath)
   407  
   408  		if err := os.MkdirAll(baseDir, os.ModePerm); err != nil {
   409  			klog.Errorf("%s: unable to create directory %s: %v", w.logContext, baseDir, err)
   410  			return err
   411  		}
   412  
   413  		if err := os.WriteFile(fullPath, content, mode); err != nil {
   414  			klog.Errorf("%s: unable to write file %s with mode %v: %v", w.logContext, fullPath, mode, err)
   415  			return err
   416  		}
   417  		// Chmod is needed because os.WriteFile() ends up calling
   418  		// open(2) to create the file, so the final mode used is "mode &
   419  		// ~umask". But we want to make sure the specified mode is used
   420  		// in the file no matter what the umask is.
   421  		if err := os.Chmod(fullPath, mode); err != nil {
   422  			klog.Errorf("%s: unable to change file %s with mode %v: %v", w.logContext, fullPath, mode, err)
   423  			return err
   424  		}
   425  
   426  		if fileProjection.FsUser == nil {
   427  			continue
   428  		}
   429  		if err := os.Chown(fullPath, int(*fileProjection.FsUser), -1); err != nil {
   430  			klog.Errorf("%s: unable to change file %s with owner %v: %v", w.logContext, fullPath, int(*fileProjection.FsUser), err)
   431  			return err
   432  		}
   433  	}
   434  
   435  	return nil
   436  }
   437  
   438  // createUserVisibleFiles creates the relative symlinks for all the
   439  // files configured in the payload. If the directory in a file path does not
   440  // exist, it is created.
   441  //
   442  // Viz:
   443  // For files: "bar", "foo/bar", "baz/bar", "foo/baz/blah"
   444  // the following symlinks are created:
   445  // bar -> ..data/bar
   446  // foo -> ..data/foo
   447  // baz -> ..data/baz
   448  func (w *AtomicWriter) createUserVisibleFiles(payload map[string]FileProjection) error {
   449  	for userVisiblePath := range payload {
   450  		slashpos := strings.Index(userVisiblePath, string(os.PathSeparator))
   451  		if slashpos == -1 {
   452  			slashpos = len(userVisiblePath)
   453  		}
   454  		linkname := userVisiblePath[:slashpos]
   455  		_, err := os.Readlink(filepath.Join(w.targetDir, linkname))
   456  		if err != nil && os.IsNotExist(err) {
   457  			// The link into the data directory for this path doesn't exist; create it
   458  			visibleFile := filepath.Join(w.targetDir, linkname)
   459  			dataDirFile := filepath.Join(dataDirName, linkname)
   460  
   461  			err = os.Symlink(dataDirFile, visibleFile)
   462  			if err != nil {
   463  				return err
   464  			}
   465  		}
   466  	}
   467  	return nil
   468  }
   469  
   470  // removeUserVisiblePaths removes the set of paths from the user-visible
   471  // portion of the writer's target directory.
   472  func (w *AtomicWriter) removeUserVisiblePaths(paths sets.String) error {
   473  	ps := string(os.PathSeparator)
   474  	var lasterr error
   475  	for p := range paths {
   476  		// only remove symlinks from the volume root directory (i.e. items that don't contain '/')
   477  		if strings.Contains(p, ps) {
   478  			continue
   479  		}
   480  		if err := os.Remove(filepath.Join(w.targetDir, p)); err != nil {
   481  			klog.Errorf("%s: error pruning old user-visible path %s: %v", w.logContext, p, err)
   482  			lasterr = err
   483  		}
   484  	}
   485  
   486  	return lasterr
   487  }