Files
mesh-catalog/modules/gitea/cmd/package-registry/seats.go
T
jschoubben bccaf75556 F-catalog-B: npm_incoming and go_incoming read and drop the package seats' transfers; an idle one drops by its key
The package seats kept incoming, transfers and findings with no read or drop tool (tools review F4).
npm_incoming and go_incoming list a seat's open transfers, incoming's fill and the cap its binding carries,
and drop one by hand. The same fault as distribution's (running review F2, its second instance): the
transfers state's own lifetime dropped a transfer's key and left its chunks, and the drop otherwise waited
for the next publish. transfers keeps for good; each open transfer has a key in the new state idle with its
own time to live, whose end drops it. The holder counts against its binding's cap.

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

377 lines
14 KiB
Go

package main
// The two package seats' verbs (novox/hq design 51 *The four registry seats*, *One contract*): publish, publish-part,
// versions and remove on `npm-package-registry` and `go-package-registry`, served as tools named <seat>.<verb> and
// answered in each seat's own contract types (mesh-contracts npmpackageregistry, gopackageregistry; ADR 0620). The
// holding itself is internal/packageseats; this file binds it to the forge's package registry, the runtime's bus state
// and the seats' events.
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"context"
"crypto/sha1"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
goreg "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
npmreg "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/parts"
"gitea/internal/packageseats"
)
// seatStore is the forge's package registry as both seats store into it.
type seatStore struct{ forge func() (*Forge, error) }
func forgeType(seat string) string { return packageseats.Seats[seat] }
// forgeVersion is the version as the forge keeps it: a Go module's carries Go's leading v.
func forgeVersion(seat, version string) string {
if seat == packageseats.GoSeat && !strings.HasPrefix(version, "v") {
return "v" + version
}
return version
}
func (f *Forge) packagePath(typ, name, version string) string {
return fmt.Sprintf("/api/v1/packages/%s/%s/%s/%s", url.PathEscape(f.Owner), typ, url.PathEscape(name), url.PathEscape(version))
}
func (s seatStore) Held(seat, name, version string) (string, bool, error) {
f, err := s.forge()
if err != nil {
return "", false, err
}
var files []struct {
SHA256 string `json:"sha256"`
}
err = f.GetJSON(context.Background(), f.packagePath(forgeType(seat), name, forgeVersion(seat, version))+"/files", &files)
switch {
case errors.Is(err, errNotFound):
return "", false, nil
case err != nil:
return "", false, err
case len(files) == 0:
return "", false, fmt.Errorf("%s@%s is listed with no file", name, version)
}
return "sha256:" + files[0].SHA256, true, nil
}
func (s seatStore) Put(seat string, p packageseats.Publish, file []byte) (string, error) {
f, err := s.forge()
if err != nil {
return "", err
}
w, err := f.write()
if err != nil {
return "", err
}
ctx := context.Background()
if seat == packageseats.GoSeat {
module, err := goModuleOf(file, forgeVersion(seat, p.Version))
if err != nil {
return "", err
}
_, err = w.Send(ctx, http.MethodPut, fmt.Sprintf("/api/packages/%s/go/upload", url.PathEscape(f.Owner)), file, "application/zip", maxBody)
return module, f.wrote(err)
}
doc, err := npmDocument(f.URL, f.Owner, p, file)
if err != nil {
return "", err
}
_, err = w.Do(ctx, http.MethodPut, fmt.Sprintf("/api/packages/%s/npm/%s", url.PathEscape(f.Owner), url.PathEscape(p.Name)), doc, maxBody)
return "", f.wrote(err)
}
func (s seatStore) Versions(seat, name string) ([]packageseats.Version, error) {
f, err := s.forge()
if err != nil {
return nil, err
}
var out []packageseats.Version
for page := 1; page <= 1000; page++ {
var batch []PackageVersion
p := fmt.Sprintf("/api/v1/packages/%s?type=%s&q=%s&page=%d&limit=%d", url.PathEscape(f.Owner), forgeType(seat), url.QueryEscape(name), page, pageSize)
if err := f.GetJSON(context.Background(), p, &batch); err != nil {
return nil, err
}
for _, v := range batch {
if v.Name != name {
continue
}
d, _, err := s.Held(seat, name, v.Version)
if err != nil {
return nil, err
}
out = append(out, packageseats.Version{Version: strings.TrimPrefix(v.Version, "v"), Digest: d, Published: v.CreatedAt})
}
if len(batch) < pageSize {
break
}
}
// Newest first, by when each was published.
for i := 1; i < len(out); i++ {
for j := i; j > 0 && out[j].Published.After(out[j-1].Published); j-- {
out[j], out[j-1] = out[j-1], out[j]
}
}
return out, nil
}
func (s seatStore) Remove(seat, name, version string) error {
f, err := s.forge()
if err != nil {
return err
}
return f.As(forgeType(seat)).Delete(context.Background(), name, forgeVersion(seat, version))
}
// goModuleOf is the Go module path of a module zip whose every file sits under <module>@<version>/, as Go's module
// proxy protocol has it; a zip of another version is refused before the forge sees it.
func goModuleOf(file []byte, version string) (string, error) {
z, err := zip.NewReader(bytes.NewReader(file), int64(len(file)))
if err != nil {
return "", fmt.Errorf("the Go package is not a module zip: %w", err)
}
if len(z.File) == 0 {
return "", errors.New("the Go package's zip holds no file")
}
module, rest, at := strings.Cut(z.File[0].Name, "@")
v, _, ok := strings.Cut(rest, "/")
if !ok || !at || module == "" {
return "", fmt.Errorf("the Go package's zip does not hold <module>@<version>/: %s", z.File[0].Name)
}
if v != version {
return "", fmt.Errorf("the Go package's zip is %s, not %s", v, version)
}
return module, nil
}
// npmDocument is the body npm's publish sends: the package.json of the tarball's package/, its dist, the tarball as
// its one attachment, and the dist-tag: the version's tag for a tagged version, so that it never becomes latest.
func npmDocument(base, owner string, p packageseats.Publish, tgz []byte) (map[string]any, error) {
manifest, err := npmManifest(tgz)
if err != nil {
return nil, err
}
if manifest["name"] != p.Name || manifest["version"] != p.Version {
return nil, fmt.Errorf("the tarball is %v@%v, not %s@%s", manifest["name"], manifest["version"], p.Name, p.Version)
}
file := path.Base(p.Name) + "-" + p.Version + ".tgz"
s1, s512 := sha1.Sum(tgz), sha512.Sum512(tgz)
manifest["_id"] = p.Name + "@" + p.Version
manifest["dist"] = map[string]any{"shasum": hex.EncodeToString(s1[:]), "integrity": "sha512-" + base64.StdEncoding.EncodeToString(s512[:]),
"tarball": fmt.Sprintf("%s/api/packages/%s/npm/%s/-/%s/%s", base, owner, p.Name, p.Version, file)}
tag := "latest"
if p.Tag != "" {
tag = p.Tag
}
return map[string]any{"_id": p.Name, "name": p.Name, "dist-tags": map[string]string{tag: p.Version},
"versions": map[string]any{p.Version: manifest},
"_attachments": map[string]any{file: map[string]any{"content_type": "application/octet-stream", "data": base64.StdEncoding.EncodeToString(tgz), "length": len(tgz)}}}, nil
}
func npmManifest(tgz []byte) (map[string]any, error) {
gz, err := gzip.NewReader(bytes.NewReader(tgz))
if err != nil {
return nil, fmt.Errorf("the npm package is not a gzipped tarball: %w", err)
}
tr := tar.NewReader(gz)
for {
h, err := tr.Next()
if err == io.EOF {
return nil, errors.New("the npm package holds no package/package.json")
}
if err != nil {
return nil, fmt.Errorf("the npm package's tarball cannot be read: %w", err)
}
if h.Name != "package/package.json" {
continue
}
var m map[string]any
if err := json.NewDecoder(io.LimitReader(tr, 1<<20)).Decode(&m); err != nil {
return nil, fmt.Errorf("the npm package's package.json is not JSON: %w", err)
}
return m, nil
}
}
// as is v in T's shape: what crosses the bus is the seat's contract type, read or written through its JSON.
func as[T any](v any) (T, error) {
var out T
raw, err := json.Marshal(v)
if err == nil {
err = json.Unmarshal(raw, &out)
}
return out, err
}
// outcomeBody is an outcome as the seat's contract's Outcome names it.
func outcomeBody(o packageseats.Outcome) map[string]any {
return map[string]any{"name": o.Name, "version": o.Version, "kind": o.Kind, "tag": o.Tag, "digest": o.Digest,
"fingerprint": o.Fingerprint, "commit": o.Commit, "holder": packageseats.Self, "transfer": o.Transfer,
"reference": o.Reference, "reason": o.Reason, "module": o.Module}
}
// say says a seat's event; a test hears it instead.
var say = sayOn
// sayOn says an event on the seat's family, in the seat's contract's Outcome.
func sayOn(seat, event string, o packageseats.Outcome) error {
var body any
var err error
if seat == packageseats.GoSeat {
body, err = as[goreg.Outcome](outcomeBody(o))
} else {
body, err = as[npmreg.Outcome](outcomeBody(o))
}
if err != nil {
return err
}
return stdio.SeatPublish("mesh.seat."+seat+".event."+event, body, "")
}
func finding(kind, subject, words string, raised bool) {
key := health.FindingKey(health.ScopeSeat, packageseats.Self, kind+"."+strings.NewReplacer(".", "_", "@", "_", "/", "_").Replace(subject))
if !raised {
_ = stdio.State(health.FindingsState).Delete(key)
return
}
f := health.Finding{V: health.FindingVersion, Key: key, Kind: kind, Ref: key[strings.LastIndex(key, ".")+1:],
Subject: health.Subject{Scope: health.ScopeSeat, ID: packageseats.Self}, Severity: health.Warning,
Summary: words, Headline: words, Explanation: words, Resolved: "cleared once it no longer holds",
Source: kind, Observer: packageseats.Self, At: time.Now().UTC()}
if _, err := stdio.State(health.FindingsState).Put(key, f); err != nil {
fmt.Fprintf(os.Stderr, "the finding %s could not be written: %v\n", key, err)
}
}
// boundCap is incoming's cap from its binding, read once per bundle.
var boundCap = incomingCap()
// busHolder is the holder over the runtime's bus state, the builder's numbering and the forge.
func busHolder(forge func() (*Forge, error)) (*packageseats.Holder, error) {
callers, err := packageseats.ConfiguredCallers()
if err != nil {
return nil, err
}
return &packageseats.Holder{Callers: callers, Objects: parts.BusObjects(packageseats.IncomingObjects),
Transfers: parts.BusTransfers[packageseats.Opening](packageseats.TransfersState),
Builder: delivery.BuilderVersions{Ask: stdio.Ask}, Store: seatStore{forge}, Say: say, Finding: finding,
Cap: boundCap, Idle: armIdle}, nil
}
// SeatTools are both seats' verbs as tools <seat>.<verb>.
func SeatTools(h func() (*packageseats.Holder, error)) []stdio.Tool {
return append(verbTools[npmreg.PublishInput, npmreg.PublishAnswer, npmreg.PublishPartInput, npmreg.PublishPartAnswer,
npmreg.VersionsInput, npmreg.VersionsAnswer, npmreg.RemoveInput, npmreg.RemoveAnswer](packageseats.NpmSeat, h),
verbTools[goreg.PublishInput, goreg.PublishAnswer, goreg.PublishPartInput, goreg.PublishPartAnswer,
goreg.VersionsInput, goreg.VersionsAnswer, goreg.RemoveInput, goreg.RemoveAnswer](packageseats.GoSeat, h)...)
}
// chunk is publish-part's input as the holder reads it.
type chunk struct {
Data, Part, Transfer string
Last bool
Sequence int
}
// verbTools are one seat's four verbs, each reading its input as the seat's contract type and answering in it.
func verbTools[PI, PA, PPI, PPA, VI, VA, RI, RA any](seat string, holder func() (*packageseats.Holder, error)) []stdio.Tool {
obj := map[string]any{"type": "object"}
run := func(do func(h *packageseats.Holder, args map[string]any) (any, error)) func(map[string]any) (any, error) {
return func(args map[string]any) (any, error) {
h, err := holder()
if err != nil {
return nil, err
}
return do(h, args)
}
}
return []stdio.Tool{
{Name: seat + "." + packageseats.VerbPublish, Input: obj,
Description: "Open a transfer of a package's one file (ADR 0611): its name, the builder's version, kind package, its sha256 digest and the file as its one part; answers the part the forge lacks. A version held with that digest answers held and says published.",
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
in, err := as[PI](args)
if err != nil {
return nil, fmt.Errorf("publish: %w", err)
}
p, err := as[packageseats.Publish](in)
if err != nil {
return nil, err
}
a, err := h.Publish(seat, stdio.CallerOf(args), p)
out, _ := as[PA](map[string]any{"transfer": a.Transfer, "lacks": a.Lacks, "held": a.Ended && len(a.Lacks) == 0})
return out, err
})},
{Name: seat + "." + packageseats.VerbPublishPart, Input: obj,
Description: "Send one chunk (at most 760 KiB before base64) of the package's file, from the caller that opened the transfer; the last chunk ends it and the forge says published or refused.",
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
in, err := as[PPI](args)
if err != nil {
return nil, fmt.Errorf("publish-part: %w", err)
}
c, err := as[chunk](in)
if err != nil {
return nil, err
}
data, err := base64.StdEncoding.DecodeString(c.Data)
if err != nil {
return nil, fmt.Errorf("publish-part: data is not base64: %w", err)
}
a, err := h.PublishPart(seat, stdio.CallerOf(args), c.Transfer, c.Part, c.Sequence, c.Last, data)
out, _ := as[PPA](map[string]any{"transfer": c.Transfer, "part": c.Part, "received": c.Sequence + 1, "complete": a.Ended})
return out, err
})},
{Name: seat + "." + packageseats.VerbVersions, Input: obj,
Description: "The versions the seat holds of one package, newest first, each with its digest.",
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
in, err := as[VI](args)
if err != nil {
return nil, fmt.Errorf("versions: %w", err)
}
q, _ := as[struct{ Name string }](in)
got, err := h.Versions(seat, stdio.CallerOf(args), q.Name)
if err != nil {
return nil, err
}
list := []map[string]any{}
for _, v := range got {
list = append(list, map[string]any{"name": q.Name, "version": v.Version, "digest": v.Digest, "kind": packageseats.KindPackage})
}
return as[VA](map[string]any{"name": q.Name, "versions": list})
})},
{Name: seat + "." + packageseats.VerbRemove, Input: obj,
Description: "Remove a tagged version (the builder, when its branch ended); says removed. An untagged version goes by the forge's retention alone.",
Run: run(func(h *packageseats.Holder, args map[string]any) (any, error) {
in, err := as[RI](args)
if err != nil {
return nil, fmt.Errorf("remove: %w", err)
}
r, _ := as[struct{ Name, Version, Reason string }](in)
o, err := h.Remove(seat, stdio.CallerOf(args), r.Name, r.Version, r.Reason)
if err != nil {
return nil, err
}
return as[RA](map[string]any{"removed": true, "artifact": map[string]any{"name": o.Name, "version": o.Version, "digest": o.Digest, "kind": o.Kind}})
})},
}
}