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