registry_import.go
⎇
Raw
1package web
2
3import (
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.
17var 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.
21const 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.
27func (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 var digests []string
92 if refs.Config != nil {
93 digests = append(digests, refs.Config.Digest)
94 }
95 for _, l := range refs.Layers {
96 digests = append(digests, l.Digest)
97 }
98 // A digest missing from the archive is left to storeManifest, which
99 // accepts it only when this image already links it.
100 for _, d := range digests {
101 p, ok := staged[d]
102 if !ok {
103 continue
104 }
105 delete(staged, d)
106 if err := s.commitBlob(ctx, repo.ID, image, p, d); err != nil {
107 return "", fmt.Errorf("blob %s: %w", d, err)
108 }
109 }
110
111 var digest string
112 for _, tag := range tags {
113 digest, err = s.storeManifest(ctx, repo, image, manifest, "", tag)
114 if err != nil {
115 return "", err
116 }
117 }
118 return digest, nil
119}
120