registry_import.go
| 1 | package web |
| 2 | |
| 3 | import ( |
| 4 | "archive/tar" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "io" |
| 10 | "os" |
| 11 | "path" |
| 12 | "regexp" |
| 13 | ) |
| 14 | |
| 15 | // dirBlobRe matches a blob file of a containers-image dir: layout. The |
| 16 | // file name is the sha256 hex digest of its content. |
| 17 | var dirBlobRe = regexp.MustCompile(`^[a-f0-9]{64}$`) |
| 18 | |
| 19 | // maxImportEntries caps the files of one imported image. A real image has |
| 20 | // a few dozen. |
| 21 | const maxImportEntries = 1000 |
| 22 | |
| 23 | // ImportImage stores an image that a CI build VM wrote and points tags at |
| 24 | // it. src is a tar of a containers-image dir: layout. The VM is untrusted: |
| 25 | // blobs are staged first, and only those the manifest names are checked |
| 26 | // against their digest and linked. It returns the manifest digest. |
| 27 | func (s *Server) ImportImage(ctx context.Context, repoName, image string, tags []string, src io.Reader) (string, error) { |
| 28 | repo, err := s.DB.RepoByNameFold(ctx, repoName) |
| 29 | if err != nil { |
| 30 | return "", err |
| 31 | } |
| 32 | if repo == nil { |
| 33 | return "", fmt.Errorf("repository %s not found", repoName) |
| 34 | } |
| 35 | |
| 36 | // staged maps a digest to its upload file. Whatever is left in it at |
| 37 | // the end was never committed. |
| 38 | staged := map[string]string{} |
| 39 | defer func() { |
| 40 | for _, p := range staged { |
| 41 | _ = os.Remove(p) |
| 42 | } |
| 43 | }() |
| 44 | var manifest []byte |
| 45 | tr := tar.NewReader(src) |
| 46 | for entries := 0; ; entries++ { |
| 47 | hdr, err := tr.Next() |
| 48 | if errors.Is(err, io.EOF) { |
| 49 | break |
| 50 | } |
| 51 | if err != nil { |
| 52 | return "", fmt.Errorf("read image archive: %w", err) |
| 53 | } |
| 54 | if entries >= maxImportEntries { |
| 55 | return "", fmt.Errorf("image archive has more than %d entries", maxImportEntries) |
| 56 | } |
| 57 | if hdr.Typeflag != tar.TypeReg { |
| 58 | continue |
| 59 | } |
| 60 | switch name := path.Clean(hdr.Name); { |
| 61 | case name == "manifest.json": |
| 62 | manifest, err = io.ReadAll(io.LimitReader(tr, maxManifestBytes+1)) |
| 63 | if err != nil { |
| 64 | return "", err |
| 65 | } |
| 66 | if len(manifest) > maxManifestBytes { |
| 67 | return "", errors.New("manifest too large") |
| 68 | } |
| 69 | case dirBlobRe.MatchString(name): |
| 70 | id, err := s.newUpload() |
| 71 | if err != nil { |
| 72 | return "", err |
| 73 | } |
| 74 | if old, dup := staged["sha256:"+name]; dup { |
| 75 | _ = os.Remove(old) |
| 76 | } |
| 77 | staged["sha256:"+name] = s.uploadPath(id) |
| 78 | if _, err := appendUpload(s.uploadPath(id), tr); err != nil { |
| 79 | return "", err |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | if manifest == nil { |
| 84 | return "", errors.New("image archive has no manifest.json") |
| 85 | } |
| 86 | |
| 87 | var refs manifestRefs |
| 88 | if err := json.Unmarshal(manifest, &refs); err != nil { |
| 89 | return "", errors.New("manifest is not valid JSON") |
| 90 | } |
| 91 | // A digest missing from the archive is left to storeManifest, which |
| 92 | // accepts it only when this image already links it. |
| 93 | for _, d := range refs.blobs() { |
| 94 | p, ok := staged[d] |
| 95 | if !ok { |
| 96 | continue |
| 97 | } |
| 98 | delete(staged, d) |
| 99 | if err := s.commitBlob(ctx, repo.ID, image, p, d); err != nil { |
| 100 | return "", fmt.Errorf("blob %s: %w", d, err) |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | var digest string |
| 105 | for _, tag := range tags { |
| 106 | digest, err = s.storeManifest(ctx, repo, image, manifest, "", tag) |
| 107 | if err != nil { |
| 108 | return "", err |
| 109 | } |
| 110 | } |
| 111 | return digest, nil |
| 112 | } |
| 113 |