github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/cas/dir/dir.go (about) 1 /* 2 * umoci: Umoci Modifies Open Containers' Images 3 * Copyright (C) 2016-2020 SUSE LLC 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package dir 19 20 import ( 21 "context" 22 "encoding/json" 23 "io" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 28 "github.com/apex/log" 29 "github.com/opencontainers/go-digest" 30 imeta "github.com/opencontainers/image-spec/specs-go" 31 ispec "github.com/opencontainers/image-spec/specs-go/v1" 32 "github.com/opencontainers/umoci/oci/cas" 33 "github.com/opencontainers/umoci/pkg/hardening" 34 "github.com/opencontainers/umoci/pkg/system" 35 "github.com/pkg/errors" 36 "golang.org/x/sys/unix" 37 ) 38 39 const ( 40 // ImageLayoutVersion is the version of the image layout we support. This 41 // value is *not* the same as imagespec.Version, and the meaning of this 42 // field is still under discussion in the spec. For now we'll just hardcode 43 // the value and hope for the best. 44 ImageLayoutVersion = "1.0.0" 45 46 // blobDirectory is the directory inside an OCI image that contains blobs. 47 blobDirectory = "blobs" 48 49 // indexFile is the file inside an OCI image that contains the top-level 50 // index. 51 indexFile = "index.json" 52 53 // layoutFile is the file in side an OCI image the indicates what version 54 // of the OCI spec the image is. 55 layoutFile = "oci-layout" 56 ) 57 58 // blobPath returns the path to a blob given its digest, relative to the root 59 // of the OCI image. The digest must be of the form algorithm:hex. 60 func blobPath(digest digest.Digest) (string, error) { 61 if err := digest.Validate(); err != nil { 62 return "", errors.Wrapf(err, "invalid digest: %q", digest) 63 } 64 65 algo := digest.Algorithm() 66 hash := digest.Hex() 67 68 if algo != cas.BlobAlgorithm { 69 return "", errors.Errorf("unsupported algorithm: %q", algo) 70 } 71 72 return filepath.Join(blobDirectory, algo.String(), hash), nil 73 } 74 75 type dirEngine struct { 76 path string 77 temp string 78 tempFile *os.File 79 } 80 81 func (e *dirEngine) ensureTempDir() error { 82 if e.temp == "" { 83 tempDir, err := ioutil.TempDir(e.path, ".umoci-") 84 if err != nil { 85 return errors.Wrap(err, "create tempdir") 86 } 87 88 // We get an advisory lock to ensure that GC() won't delete our 89 // temporary directory here. Once we get the lock we know it won't do 90 // anything until we unlock it or exit. 91 92 e.tempFile, err = os.Open(tempDir) 93 if err != nil { 94 return errors.Wrap(err, "open tempdir for lock") 95 } 96 if err := unix.Flock(int(e.tempFile.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { 97 return errors.Wrap(err, "lock tempdir") 98 } 99 100 e.temp = tempDir 101 } 102 return nil 103 } 104 105 // verify ensures that the image is valid. 106 func (e *dirEngine) validate() error { 107 content, err := ioutil.ReadFile(filepath.Join(e.path, layoutFile)) 108 if err != nil { 109 if os.IsNotExist(err) { 110 err = cas.ErrInvalid 111 } 112 return errors.Wrap(err, "read oci-layout") 113 } 114 115 var ociLayout ispec.ImageLayout 116 if err := json.Unmarshal(content, &ociLayout); err != nil { 117 return errors.Wrap(err, "parse oci-layout") 118 } 119 120 // XXX: Currently the meaning of this field is not adequately defined by 121 // the spec, nor is the "official" value determined by the spec. 122 if ociLayout.Version != ImageLayoutVersion { 123 return errors.Wrap(cas.ErrInvalid, "layout version is not supported") 124 } 125 126 // Check that "blobs" and "index.json" exist in the image. 127 // FIXME: We also should check that blobs *only* contains a cas.BlobAlgorithm 128 // directory (with no subdirectories) and that refs *only* contains 129 // files (optionally also making sure they're all JSON descriptors). 130 if fi, err := os.Stat(filepath.Join(e.path, blobDirectory)); err != nil { 131 if os.IsNotExist(err) { 132 err = cas.ErrInvalid 133 } 134 return errors.Wrap(err, "check blobdir") 135 } else if !fi.IsDir() { 136 return errors.Wrap(cas.ErrInvalid, "blobdir is not a directory") 137 } 138 139 if fi, err := os.Stat(filepath.Join(e.path, indexFile)); err != nil { 140 if os.IsNotExist(err) { 141 err = cas.ErrInvalid 142 } 143 return errors.Wrap(err, "check index") 144 } else if fi.IsDir() { 145 return errors.Wrap(cas.ErrInvalid, "index is a directory") 146 } 147 148 return nil 149 } 150 151 // PutBlob adds a new blob to the image. This is idempotent; a nil error 152 // means that "the content is stored at DIGEST" without implying "because 153 // of this PutBlob() call". 154 func (e *dirEngine) PutBlob(ctx context.Context, reader io.Reader) (digest.Digest, int64, error) { 155 if err := e.ensureTempDir(); err != nil { 156 return "", -1, errors.Wrap(err, "ensure tempdir") 157 } 158 159 digester := cas.BlobAlgorithm.Digester() 160 161 // We copy this into a temporary file because we need to get the blob hash, 162 // but also to avoid half-writing an invalid blob. 163 fh, err := ioutil.TempFile(e.temp, "blob-") 164 if err != nil { 165 return "", -1, errors.Wrap(err, "create temporary blob") 166 } 167 tempPath := fh.Name() 168 defer fh.Close() 169 170 writer := io.MultiWriter(fh, digester.Hash()) 171 size, err := system.Copy(writer, reader) 172 if err != nil { 173 return "", -1, errors.Wrap(err, "copy to temporary blob") 174 } 175 if err := fh.Close(); err != nil { 176 return "", -1, errors.Wrap(err, "close temporary blob") 177 } 178 179 // Get the digest. 180 path, err := blobPath(digester.Digest()) 181 if err != nil { 182 return "", -1, errors.Wrap(err, "compute blob name") 183 } 184 185 // Move the blob to its correct path. 186 path = filepath.Join(e.path, path) 187 if err := os.Rename(tempPath, path); err != nil { 188 return "", -1, errors.Wrap(err, "rename temporary blob") 189 } 190 191 return digester.Digest(), int64(size), nil 192 } 193 194 // GetBlob returns a reader for retrieving a blob from the image, which the 195 // caller must Close(). Returns ErrNotExist if the digest is not found. 196 // 197 // This function will return a VerifiedReadCloser, meaning that you must call 198 // Close() and check the error returned from Close() in order to ensure that 199 // the hash of the blob is verified. 200 // 201 // Please note that calling Close() on the returned blob will read the entire 202 // from disk and hash it (even if you didn't read any bytes before calling 203 // Close), so if you wish to only check if a blob exists you should use 204 // StatBlob() instead. 205 func (e *dirEngine) GetBlob(ctx context.Context, digest digest.Digest) (io.ReadCloser, error) { 206 path, err := blobPath(digest) 207 if err != nil { 208 return nil, errors.Wrap(err, "compute blob path") 209 } 210 fh, err := os.Open(filepath.Join(e.path, path)) 211 return &hardening.VerifiedReadCloser{ 212 Reader: fh, 213 ExpectedDigest: digest, 214 ExpectedSize: int64(-1), // We don't know the expected size. 215 }, errors.Wrap(err, "open blob") 216 } 217 218 // StatBlob returns whether the specified blob exists in the image. Returns 219 // false if the blob doesn't exist, true if it does, or an error if any error 220 // occurred. 221 // 222 // NOTE: In future this API may change to return additional information. 223 func (e *dirEngine) StatBlob(ctx context.Context, digest digest.Digest) (bool, error) { 224 path, err := blobPath(digest) 225 if err != nil { 226 return false, errors.Wrap(err, "compute blob path") 227 } 228 _, err = os.Stat(path) 229 if os.IsNotExist(err) { 230 return false, nil 231 } 232 if err != nil { 233 return false, errors.Wrap(err, "stat blob path") 234 } 235 return true, nil 236 } 237 238 // PutIndex sets the index of the OCI image to the given index, replacing the 239 // previously existing index. This operation is atomic; any readers attempting 240 // to access the OCI image while it is being modified will only ever see the 241 // new or old index. 242 func (e *dirEngine) PutIndex(ctx context.Context, index ispec.Index) error { 243 if err := e.ensureTempDir(); err != nil { 244 return errors.Wrap(err, "ensure tempdir") 245 } 246 247 // Make sure the index has the mediatype field set. 248 index.MediaType = ispec.MediaTypeImageIndex 249 250 // We copy this into a temporary index to ensure the atomicity of this 251 // operation. 252 fh, err := ioutil.TempFile(e.temp, "index-") 253 if err != nil { 254 return errors.Wrap(err, "create temporary index") 255 } 256 tempPath := fh.Name() 257 defer fh.Close() 258 259 // Encode the index. 260 if err := json.NewEncoder(fh).Encode(index); err != nil { 261 return errors.Wrap(err, "write temporary index") 262 } 263 if err := fh.Close(); err != nil { 264 return errors.Wrap(err, "close temporary index") 265 } 266 267 // Move the blob to its correct path. 268 path := filepath.Join(e.path, indexFile) 269 if err := os.Rename(tempPath, path); err != nil { 270 return errors.Wrap(err, "rename temporary index") 271 } 272 return nil 273 } 274 275 // GetIndex returns the index of the OCI image. Return ErrNotExist if the 276 // digest is not found. If the image doesn't have an index, ErrInvalid is 277 // returned (a valid OCI image MUST have an image index). 278 // 279 // It is not recommended that users of cas.Engine use this interface directly, 280 // due to the complication of properly handling references as well as correctly 281 // handling nested indexes. casext.Engine provides a wrapper for cas.Engine 282 // that implements various reference resolution functions that should work for 283 // most users. 284 func (e *dirEngine) GetIndex(ctx context.Context) (ispec.Index, error) { 285 content, err := ioutil.ReadFile(filepath.Join(e.path, indexFile)) 286 if err != nil { 287 if os.IsNotExist(err) { 288 err = cas.ErrInvalid 289 } 290 return ispec.Index{}, errors.Wrap(err, "read index") 291 } 292 293 var index ispec.Index 294 if err := json.Unmarshal(content, &index); err != nil { 295 return ispec.Index{}, errors.Wrap(err, "parse index") 296 } 297 298 return index, nil 299 } 300 301 // DeleteBlob removes a blob from the image. This is idempotent; a nil 302 // error means "the content is not in the store" without implying "because 303 // of this DeleteBlob() call". 304 func (e *dirEngine) DeleteBlob(ctx context.Context, digest digest.Digest) error { 305 path, err := blobPath(digest) 306 if err != nil { 307 return errors.Wrap(err, "compute blob path") 308 } 309 310 err = os.Remove(filepath.Join(e.path, path)) 311 if err != nil && !os.IsNotExist(err) { 312 return errors.Wrap(err, "remove blob") 313 } 314 return nil 315 } 316 317 // ListBlobs returns the set of blob digests stored in the image. 318 func (e *dirEngine) ListBlobs(ctx context.Context) ([]digest.Digest, error) { 319 digests := []digest.Digest{} 320 blobDir := filepath.Join(e.path, blobDirectory, cas.BlobAlgorithm.String()) 321 322 if err := filepath.Walk(blobDir, func(path string, _ os.FileInfo, _ error) error { 323 // Skip the actual directory. 324 if path == blobDir { 325 return nil 326 } 327 328 // XXX: Do we need to handle multiple-directory-deep cases? 329 digest := digest.NewDigestFromHex(cas.BlobAlgorithm.String(), filepath.Base(path)) 330 digests = append(digests, digest) 331 return nil 332 }); err != nil { 333 return nil, errors.Wrap(err, "walk blobdir") 334 } 335 336 return digests, nil 337 } 338 339 // Clean executes a garbage collection of any non-blob garbage in the store 340 // (this includes temporary files and directories not reachable from the CAS 341 // interface). This MUST NOT remove any blobs or references in the store. 342 func (e *dirEngine) Clean(ctx context.Context) error { 343 // Remove every .umoci directory that isn't flocked. 344 matches, err := filepath.Glob(filepath.Join(e.path, ".umoci-*")) 345 if err != nil { 346 return errors.Wrap(err, "glob .umoci-*") 347 } 348 for _, path := range matches { 349 err = e.cleanPath(ctx, path) 350 if err != nil && err != filepath.SkipDir { 351 return err 352 } 353 } 354 355 return nil 356 } 357 358 func (e *dirEngine) cleanPath(ctx context.Context, path string) error { 359 cfh, err := os.Open(path) 360 if err != nil && !os.IsNotExist(err) { 361 return errors.Wrap(err, "open for locking") 362 } 363 defer cfh.Close() 364 365 if err := unix.Flock(int(cfh.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil { 366 // If we fail to get a flock(2) then it's probably already locked, 367 // so we shouldn't touch it. 368 return filepath.SkipDir 369 } 370 defer unix.Flock(int(cfh.Fd()), unix.LOCK_UN) 371 372 if err := os.RemoveAll(path); os.IsNotExist(err) { 373 return nil // somebody else beat us to it 374 } else if err != nil { 375 log.Warnf("failed to remove %s: %v", path, err) 376 return filepath.SkipDir 377 } 378 log.Debugf("cleaned %s", path) 379 380 return nil 381 } 382 383 // Close releases all references held by the e. Subsequent operations may 384 // fail. 385 func (e *dirEngine) Close() error { 386 if e.temp != "" { 387 if err := unix.Flock(int(e.tempFile.Fd()), unix.LOCK_UN); err != nil { 388 return errors.Wrap(err, "unlock tempdir") 389 } 390 if err := e.tempFile.Close(); err != nil { 391 return errors.Wrap(err, "close tempdir") 392 } 393 if err := os.RemoveAll(e.temp); err != nil { 394 return errors.Wrap(err, "remove tempdir") 395 } 396 } 397 return nil 398 } 399 400 // Open opens a new reference to the directory-backed OCI image referenced by 401 // the provided path. 402 func Open(path string) (cas.Engine, error) { 403 engine := &dirEngine{ 404 path: path, 405 temp: "", 406 } 407 408 if err := engine.validate(); err != nil { 409 return nil, errors.Wrap(err, "validate") 410 } 411 412 return engine, nil 413 } 414 415 // Create creates a new OCI image layout at the given path. If the path already 416 // exists, os.ErrExist is returned. However, all of the parent components of 417 // the path will be created if necessary. 418 func Create(path string) error { 419 // We need to fail if path already exists, but we first create all of the 420 // parent paths. 421 dir := filepath.Dir(path) 422 if dir != "." { 423 if err := os.MkdirAll(dir, 0755); err != nil { 424 return errors.Wrap(err, "mkdir parent") 425 } 426 } 427 if err := os.Mkdir(path, 0755); err != nil { 428 return errors.Wrap(err, "mkdir") 429 } 430 431 // Create the necessary directories and "oci-layout" file. 432 if err := os.Mkdir(filepath.Join(path, blobDirectory), 0755); err != nil { 433 return errors.Wrap(err, "mkdir blobdir") 434 } 435 if err := os.Mkdir(filepath.Join(path, blobDirectory, cas.BlobAlgorithm.String()), 0755); err != nil { 436 return errors.Wrap(err, "mkdir algorithm") 437 } 438 439 indexFh, err := os.Create(filepath.Join(path, indexFile)) 440 if err != nil { 441 return errors.Wrap(err, "create index.json") 442 } 443 defer indexFh.Close() 444 445 defaultIndex := ispec.Index{ 446 Versioned: imeta.Versioned{ 447 SchemaVersion: 2, // FIXME: This is hardcoded at the moment. 448 }, 449 MediaType: ispec.MediaTypeImageIndex, 450 } 451 if err := json.NewEncoder(indexFh).Encode(defaultIndex); err != nil { 452 return errors.Wrap(err, "encode index.json") 453 } 454 455 layoutFh, err := os.Create(filepath.Join(path, layoutFile)) 456 if err != nil { 457 return errors.Wrap(err, "create oci-layout") 458 } 459 defer layoutFh.Close() 460 461 ociLayout := ispec.ImageLayout{ 462 Version: ImageLayoutVersion, 463 } 464 if err := json.NewEncoder(layoutFh).Encode(ociLayout); err != nil { 465 return errors.Wrap(err, "encode oci-layout") 466 } 467 468 // Everything is now set up. 469 return nil 470 }