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 }