Files
mesh-catalog/modules/distribution/internal/holder/oci.go
T
jschoubben d9d1119a72 CAT-BG2: a bundle or archive the builder numbers later is published late, sending nothing
mesh-build-go consumes the builder's version.numbered and publishes the held
digest under its version, naming its one part by digest only, since the packed
bytes left the node; distribution tags it at the size its store holds, so the
version's manifest names the blob truly.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:17:53 +01:00

276 lines
8.5 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. It answers a reference's path,
// `<name>@sha256:<digest>`; the holder puts the seat's name before it (SeatReference, ADR 0605 §9).
type OCI struct {
Base *url.URL
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 PublishRequest, 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 {
// An image's manifest is the part whose digest is the publish's (the seat's contract names no media type); a
// part naming a manifest's media type is one too.
if (t.Kind == KindImage || t.Kind == KindUpstream) && (p.Digest == t.Digest || 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, accepted...)
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
}
mt := manifest.MediaType
var probe struct {
MediaType string `json:"mediaType"`
}
if mt == "" && json.Unmarshal(data, &probe) == nil {
mt = probe.MediaType
}
if mt == "" {
mt = mediaManifest
}
d, err := o.putManifest(t.Name, ref, mt, data)
if err != nil {
return "", err
}
return t.Name + "@" + d, nil
default:
if err := o.putBlob(t.Name, "sha256:"+sum([]byte(emptyConfig)), []byte(emptyConfig)); err != nil {
return "", err
}
size, err := o.layerSize(t, parts)
if err != nil {
return "", err
}
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 t.Name + "@" + t.Digest, nil
}
}
// layerSize is the size of a bundle's or an archive's one blob: its bytes' when they travelled, else the size its part
// names, else (a late publish of a held digest names it with no size, its bytes having left the build node) the store's.
func (o *OCI) layerSize(t PublishRequest, parts map[string][]byte) (int64, error) {
if data, ok := parts[t.Digest]; ok {
return int64(len(data)), nil
}
for _, p := range t.Parts {
if p.Digest == t.Digest && p.Size > 0 {
return p.Size, nil
}
}
resp, _, err := o.do(http.MethodHead, "/v2/"+t.Name+"/blobs/"+t.Digest, "", nil)
if err != nil {
return 0, err
}
if resp.StatusCode != http.StatusOK || resp.ContentLength < 0 {
return 0, fmt.Errorf("the store holds no blob %s of %s to tag (%d)", t.Digest, t.Name, resp.StatusCode)
}
return resp.ContentLength, 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 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
}