Files
mesh-catalog/modules/distribution/internal/holder/oci.go
T
jschoubben 9ad22ad1e1 CAT-D1..D5 (holder core): distribution's holding of the two image seats, its caller list, incoming, the store's front and the two windows
The holder answers publish and publish-part into incoming (parts of at most 1 MiB, resumed from what is held,
hashed, only the missing layers sent, 20 GiB with its 80 % finding and a refusal at the cap, a transfer idle 24
hours dropped), asks builder_versions before it stores, and says published or refused (design 51 *Publishing*,
ADR 0605 §4, ADR 0611). versions, remove (a tagged version; untagged only for the sweep's retention, which a
hold keeps), hold and release (ADR 0617 §2: the sweep alone; the operator's session by hand; never
distribution's own account), tags and mirror. The caller list is a trusted config value checked against the
caller the tool runner proved. The store's port is the program's: reads open, writes refused but while the
direct-write window is open (never over a tag published through the seat) or with the bootstrap's one-off
credential. The old-name and direct-write windows read every node, away ones included, close once, and name a
node holding them seven days.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:52:37 +01:00

263 lines
8.2 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
}
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
}
// Mirror is not done by the store itself: mesh-build-image sends an upstream image's parts through publish of kind
// upstream, named `upstream/<host>/<path>`; mirror answers where a held copy is, and refuses one the store lacks so
// the build module publishes it.
func (o *OCI) Mirror(source, digest string) (string, error) {
name := "upstream/" + source
if i := strings.LastIndexAny(name, ":@"); i > strings.LastIndex(name, "/") {
name = name[:i]
}
resp, _, err := o.do(http.MethodHead, "/v2/"+name+"/manifests/"+digest, "", nil, "application/vnd.oci.image.index.v1+json",
mediaManifest, "application/vnd.docker.distribution.manifest.list.v2+json", "application/vnd.docker.distribution.manifest.v2+json")
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("the store holds no copy of %s@%s: publish it with kind upstream under %s", source, digest, name)
}
return o.Registry + "/" + name + "@" + digest, nil
}