Files
mesh-catalog/modules/distribution/internal/holder/oci.go
T
jschoubben 75986b2960 CAT-D3: distribution's mirror copies an outside image by its pinned digest into upstream/<host>/<path>
Until now mirror only answered a copy the store already held, so mesh-build-image had nothing to call for an upstream
base (design 51: an upstream image is copied by mirror with its pinned digest). The copy takes an anonymous pull
token, checks every manifest's and blob's bytes against its digest, copies an index's every platform first, and
fetches only the blobs the store lacks. tags was already served.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:48:29 +01:00

246 lines
7.4 KiB
Go

package holder
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
)
// OCI is the artifact store as the OCI distribution protocol answers it on its node's loopback, written by
// distribution's own process alone: the Store and the Catalog the verbs use. Registry is the reach the seat answers in a
// reference (`<registry>/<module>/<artifact>@sha256:<digest>`, ADR 0605 §9).
type OCI struct {
Base *url.URL
Registry string
Client *http.Client
// Upstream reads the outside registries `mirror` copies from.
Upstream *Upstream
}
const (
mediaManifest = "application/vnd.oci.image.manifest.v1+json"
mediaEmptyConfig = "application/vnd.oci.empty.v1+json"
mediaLayer = "application/vnd.oci.image.layer.v1.tar"
emptyConfig = "{}"
)
func (o *OCI) client() *http.Client {
if o.Client != nil {
return o.Client
}
return http.DefaultClient
}
func (o *OCI) url(path string) string { return strings.TrimSuffix(o.Base.String(), "/") + path }
func (o *OCI) do(method, path, contentType string, body []byte, accept ...string) (*http.Response, []byte, error) {
req, err := http.NewRequest(method, o.url(path), bytes.NewReader(body))
if err != nil {
return nil, nil, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
for _, a := range accept {
req.Header.Add("Accept", a)
}
resp, err := o.client().Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
b, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
return resp, b, nil
}
// Has is whether name holds a blob.
func (o *OCI) Has(name, digest string) (bool, error) {
resp, _, err := o.do(http.MethodHead, "/v2/"+name+"/blobs/"+digest, "", nil)
if err != nil {
return false, err
}
return resp.StatusCode == http.StatusOK, nil
}
func (o *OCI) putBlob(name, digest string, data []byte) error {
resp, b, err := o.do(http.MethodPost, "/v2/"+name+"/blobs/uploads/", "", nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("the store did not open an upload of %s (%d): %s", digest, resp.StatusCode, b)
}
loc, err := url.Parse(resp.Header.Get("Location"))
if err != nil {
return err
}
q := loc.Query()
q.Set("digest", digest)
loc.RawQuery = q.Encode()
path := loc.Path
if loc.RawQuery != "" {
path += "?" + loc.RawQuery
}
resp, b, err = o.do(http.MethodPut, path, "application/octet-stream", data)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("the store did not take %s (%d): %s", digest, resp.StatusCode, b)
}
return nil
}
func (o *OCI) putManifest(name, ref, mediaType string, data []byte) (string, error) {
resp, b, err := o.do(http.MethodPut, "/v2/"+name+"/manifests/"+ref, mediaType, data)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("the store did not take the manifest %s:%s (%d): %s", name, ref, resp.StatusCode, b)
}
return "sha256:" + sum(data), nil
}
// Put stores a transfer: an image's layers and config as blobs and its manifest (the part whose media type is a
// manifest's) under its version; a bundle's or an archive's one blob under an artifact manifest of an empty config and
// one layer (the shape the artifact sweep recognises), under its version, or by its digest alone when unnumbered.
func (o *OCI) Put(t Transfer, parts map[string][]byte) (string, error) {
var manifest *Part
digests := make([]string, 0, len(parts))
for d := range parts {
digests = append(digests, d)
}
sort.Strings(digests)
for i, p := range t.Parts {
if strings.Contains(p.MediaType, "manifest") {
manifest = &t.Parts[i]
continue
}
if data, ok := parts[p.Digest]; ok {
if err := o.putBlob(t.Name, p.Digest, data); err != nil {
return "", err
}
}
}
ref := t.Version
switch t.Kind {
case KindImage, KindUpstream:
if manifest == nil {
return "", fmt.Errorf("an image's publish names no manifest part")
}
data, ok := parts[manifest.Digest]
if !ok {
// Held already: fetch it to tag it under the version.
resp, b, err := o.do(http.MethodGet, "/v2/"+t.Name+"/manifests/"+manifest.Digest, "", nil, manifest.MediaType)
if err != nil || resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("the held manifest %s cannot be read: %v", manifest.Digest, err)
}
data = b
}
if ref == "" {
ref = manifest.Digest
}
d, err := o.putManifest(t.Name, ref, manifest.MediaType, data)
if err != nil {
return "", err
}
return o.Registry + "/" + t.Name + "@" + d, nil
default:
if err := o.putBlob(t.Name, "sha256:"+sum([]byte(emptyConfig)), []byte(emptyConfig)); err != nil {
return "", err
}
size := int64(0)
for _, p := range t.Parts {
if p.Digest == t.Digest {
size = p.Size
}
}
m, _ := json.Marshal(map[string]any{
"schemaVersion": 2, "mediaType": mediaManifest,
"config": map[string]any{"mediaType": mediaEmptyConfig, "digest": "sha256:" + sum([]byte(emptyConfig)), "size": len(emptyConfig)},
"layers": []map[string]any{{"mediaType": mediaLayer, "digest": t.Digest, "size": size}},
})
if ref == "" {
ref = "sha256:" + sum(m)
}
if _, err := o.putManifest(t.Name, ref, mediaManifest, m); err != nil {
return "", err
}
return o.Registry + "/" + t.Name + "@" + t.Digest, nil
}
}
// Held is the reference of name at version when the store holds digest under it.
func (o *OCI) Held(name, version, digest string) (string, bool, error) {
d, err := o.Digest(name, version)
if err != nil || d == "" {
return "", false, err
}
if d != digest {
return "", false, nil
}
return o.Registry + "/" + name + "@" + d, true, nil
}
// Versions are name's tags.
func (o *OCI) Versions(name string) ([]string, error) {
resp, b, err := o.do(http.MethodGet, "/v2/"+name+"/tags/list", "", nil)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusNotFound {
return []string{}, nil
}
var out struct {
Tags []string `json:"tags"`
}
if resp.StatusCode != http.StatusOK || json.Unmarshal(b, &out) != nil {
return nil, fmt.Errorf("the store's tags of %s (%d): %s", name, resp.StatusCode, b)
}
sort.Strings(out.Tags)
return out.Tags, nil
}
// Digest is the manifest digest name holds at version, empty when none.
func (o *OCI) Digest(name, version string) (string, error) {
resp, _, err := o.do(http.MethodHead, "/v2/"+name+"/manifests/"+version, "", nil, mediaManifest,
"application/vnd.oci.image.index.v1+json", "application/vnd.docker.distribution.manifest.v2+json")
if err != nil {
return "", err
}
if resp.StatusCode == http.StatusNotFound {
return "", nil
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("the store answered %d for %s:%s", resp.StatusCode, name, version)
}
return resp.Header.Get("Docker-Content-Digest"), nil
}
// Remove deletes the manifest name holds at version.
func (o *OCI) Remove(name, version string) error {
d, err := o.Digest(name, version)
if err != nil || d == "" {
return err
}
return o.RemoveDigest(name, d)
}
// RemoveDigest deletes a manifest by its digest; the store's nightly collector frees its blobs.
func (o *OCI) RemoveDigest(name, digest string) error {
resp, b, err := o.do(http.MethodDelete, "/v2/"+name+"/manifests/"+digest, "", nil)
if err != nil {
return err
}
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusNotFound {
return fmt.Errorf("the store did not delete %s@%s (%d): %s", name, digest, resp.StatusCode, b)
}
return nil
}