package web import ( "archive/tar" "context" "encoding/json" "errors" "fmt" "io" "os" "path" "regexp" ) // dirBlobRe matches a blob file of a containers-image dir: layout. The // file name is the sha256 hex digest of its content. var dirBlobRe = regexp.MustCompile(`^[a-f0-9]{64}$`) // maxImportEntries caps the files of one imported image. A real image has // a few dozen. const maxImportEntries = 1000 // ImportImage stores an image that a CI build VM wrote and points tags at // it. src is a tar of a containers-image dir: layout. The VM is untrusted: // blobs are staged first, and only those the manifest names are checked // against their digest and linked. It returns the manifest digest. func (s *Server) ImportImage(ctx context.Context, repoName, image string, tags []string, src io.Reader) (string, error) { repo, err := s.DB.RepoByNameFold(ctx, repoName) if err != nil { return "", err } if repo == nil { return "", fmt.Errorf("repository %s not found", repoName) } // staged maps a digest to its upload file. Whatever is left in it at // the end was never committed. staged := map[string]string{} defer func() { for _, p := range staged { _ = os.Remove(p) } }() var manifest []byte tr := tar.NewReader(src) for entries := 0; ; entries++ { hdr, err := tr.Next() if errors.Is(err, io.EOF) { break } if err != nil { return "", fmt.Errorf("read image archive: %w", err) } if entries >= maxImportEntries { return "", fmt.Errorf("image archive has more than %d entries", maxImportEntries) } if hdr.Typeflag != tar.TypeReg { continue } switch name := path.Clean(hdr.Name); { case name == "manifest.json": manifest, err = io.ReadAll(io.LimitReader(tr, maxManifestBytes+1)) if err != nil { return "", err } if len(manifest) > maxManifestBytes { return "", errors.New("manifest too large") } case dirBlobRe.MatchString(name): id, err := s.newUpload() if err != nil { return "", err } if old, dup := staged["sha256:"+name]; dup { _ = os.Remove(old) } staged["sha256:"+name] = s.uploadPath(id) if _, err := appendUpload(s.uploadPath(id), tr); err != nil { return "", err } } } if manifest == nil { return "", errors.New("image archive has no manifest.json") } var refs manifestRefs if err := json.Unmarshal(manifest, &refs); err != nil { return "", errors.New("manifest is not valid JSON") } // A digest missing from the archive is left to storeManifest, which // accepts it only when this image already links it. for _, d := range refs.blobs() { p, ok := staged[d] if !ok { continue } delete(staged, d) if err := s.commitBlob(ctx, repo.ID, image, p, d); err != nil { return "", fmt.Errorf("blob %s: %w", d, err) } } var digest string for _, tag := range tags { digest, err = s.storeManifest(ctx, repo, image, manifest, "", tag) if err != nil { return "", err } } return digest, nil }