Implements novox/hq ADR 0110 and 0111. The seat set lives in internal/catalogue/seats.go: fourteen seats, each with a scope, what occupying it delivers, and the record that made it one. A test asserts the count and a decision per entry, so changing the set means finding the argument, as the host's vocabulary test does. The first set is every seat already claimed — including the-private-network, which the network module claims from a manifest composed in this repository's code, not from any module.json — plus npm-package-registry (ADR 0109) and git (ADR 0111). A test parses every catalogue manifest and this repository's own and fails on any refused claim, so closing the set refuses nothing in use. ParseManifest now refuses a claim on a seat the mesh does not define, a seat claimed at another scope, and a delivering seat claimed by a module that does not provide what it delivers. A malformed claim is refused once, for being malformed. Resolution: among several providers of a mesh provision, a pin still wins; then the holder of the seat that delivers it; then the only provider; otherwise refused as before. ADR 0009's "never guessed" holds — the seat is the choice made once, mesh-wide, rather than a pin per consumer node. A provider now carries the module it came from, because a provider is a (node, module) pair and the pair is what tells a holder from a neighbour on the same machine. The planner's second pass is now given the first pass's holdings. Without them, a node consuming a seat-delivered provision was refused there, and a refused node's own claims dropped out of what the mesh holds — letting a second holder of one of its seats pass unrefused. `seats [--json]` lists every seat, what it delivers, and each holder, derived from assignments every time and never stored. Unheld seats are listed. A stored claim outside the set — possible for a manifest registered before the set closed, since stored manifests are not re-validated — is shown rather than hidden. `build --self <owner>/<repo>` builds from a repository on the git seat's holder. The clone URL is composed at build time from the holder's node and what it serves for git; the recorded source is the path and the seat (migration 0032), never an address, so a moved forge changes nothing recorded. Nobody holding the seat refuses self-hosted builds and says so; external URLs are unchanged. An address passed with --self is refused rather than recorded as a path. Replaces three foundation tests that defended the builder's carried package binding. The catalogue removed that binding when the builder began requiring the registry through a real grant, so the tests were already failing on main; they now assert the builder requires what the npm seat delivers and carries no copy of its own, and that the forge holds the npm and git seats. Verified: go vet clean; the whole suite passes against a throwaway Postgres (make postgres), the new inventory tests included; gofmt clean apart from cmd/mesh-builder/stdout_test.go, which fails on main too.
137 lines
5.0 KiB
Go
137 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// where a build's repository is (novox/hq ADR 0111).
|
|
//
|
|
// A repository is on the mesh's own forge, or it is anywhere else. The first is recorded as its path
|
|
// on the forge holding the git seat, and cloned from wherever that forge runs at the moment of
|
|
// building; the second is a URL, recorded and cloned exactly as given. The build machine is not told
|
|
// the difference — it is handed a URL either way — because only the control plane knows where the
|
|
// seat's holder runs.
|
|
|
|
// gitSeat is the seat a self-hosted repository lives on.
|
|
const gitSeat = "git"
|
|
|
|
// buildSource is where a build's repository is: a URL, or a path on a seat's holder.
|
|
type buildSource struct {
|
|
Repository string
|
|
Seat string
|
|
}
|
|
|
|
// String is the source as a person reads it, which for one on a seat is not the URL: the URL is a
|
|
// fact about where the forge happens to run today.
|
|
func (s buildSource) String() string {
|
|
if s.Seat == "" {
|
|
return s.Repository
|
|
}
|
|
return fmt.Sprintf("%s on the %s seat", s.Repository, s.Seat)
|
|
}
|
|
|
|
// onASeat refuses an address given as a path on the forge.
|
|
//
|
|
// **A URL here would be recorded as a path**, and then composed onto the forge's address as one —
|
|
// cloning `http://forge:3000/https://github.com/…`. Refused by what an address plainly looks like,
|
|
// not repaired: `--self` promises a path, and something that is not one is a mistake to name.
|
|
func onASeat(repository string) error {
|
|
if strings.Contains(repository, ":") || strings.HasPrefix(repository, "/") ||
|
|
strings.Trim(repository, "/") == "" {
|
|
return fmt.Errorf("--self takes the repository's path on the forge, such as novox/mesh-catalog, "+
|
|
"and %q is not one — without --self it is built from exactly what is given", repository)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cloneFrom is the URL a build machine clones for a source.
|
|
//
|
|
// A URL is itself. A path on a seat is composed from the seat's holder as the mesh sees it now —
|
|
// the same view planning takes of every machine, so the forge a build clones from is the forge the
|
|
// mesh says holds the seat.
|
|
func cloneFrom(ctx context.Context, source buildSource) (string, error) {
|
|
if source.Seat == "" {
|
|
return source.Repository, nil
|
|
}
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer open.Close()
|
|
shelf, err := open.inventory.Catalogue(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
world, err := theRestOfTheMesh(ctx, open.inventory, shelf, "")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return clonedFromSeat(world, source.Seat, source.Repository)
|
|
}
|
|
|
|
// clonedFromSeat composes the clone URL for a repository on a seat's holder.
|
|
//
|
|
// **Refused, never defaulted, at every step that has no answer.** Nobody holding the seat is a mesh
|
|
// without a forge of its own: it builds from external repositories and must say so rather than fail
|
|
// to clone. A holder off the private network cannot be reached by any build machine. A holder that
|
|
// serves no scheme or port has nothing to compose from — a default port here would be the forge's
|
|
// address guessed, which is the thing this exists to stop.
|
|
func clonedFromSeat(world catalogue.World, seatName, repository string) (string, error) {
|
|
seat, known := catalogue.SeatNamed(seatName)
|
|
if !known || seat.Delivers == "" {
|
|
return "", fmt.Errorf("%q is not a seat a repository can live on", seatName)
|
|
}
|
|
var holder *catalogue.Held
|
|
for i, h := range world.Held {
|
|
if h.Claim == seat.Name && h.Scope == seat.Scope {
|
|
holder = &world.Held[i]
|
|
break
|
|
}
|
|
}
|
|
if holder == nil {
|
|
return "", fmt.Errorf("nobody holds the %s seat, so %s cannot be cloned from this mesh's "+
|
|
"forge — assign a module that claims it, or build from the repository's URL without --self",
|
|
seat.Name, repository)
|
|
}
|
|
var provider *catalogue.Provider
|
|
for i, p := range world.Offered[seat.Delivers] {
|
|
if p.Node == holder.Node && p.Module == holder.Module {
|
|
provider = &world.Offered[seat.Delivers][i]
|
|
}
|
|
}
|
|
if provider == nil {
|
|
return "", fmt.Errorf("%s on %s holds the %s seat and offers no %q to clone from",
|
|
holder.Module, holder.Node, seat.Name, seat.Delivers)
|
|
}
|
|
if provider.At == "" {
|
|
return "", fmt.Errorf("%s on %s holds the %s seat and is not on the private network, so no "+
|
|
"build machine can reach it", holder.Module, holder.Node, seat.Name)
|
|
}
|
|
scheme, _ := provider.Serves["scheme"].(string)
|
|
port := servedPort(provider.Serves["port"])
|
|
if scheme == "" || port == "" {
|
|
return "", fmt.Errorf("%s on %s holds the %s seat and does not serve a scheme and a port for %q",
|
|
holder.Module, holder.Node, seat.Name, seat.Delivers)
|
|
}
|
|
path := strings.TrimSuffix(strings.Trim(repository, "/"), ".git")
|
|
return fmt.Sprintf("%s://%s:%s/%s.git", scheme, provider.At, port, path), nil
|
|
}
|
|
|
|
// servedPort is a served port as text, however the manifest and the node's settings carried it.
|
|
func servedPort(v any) string {
|
|
switch p := v.(type) {
|
|
case float64:
|
|
return strconv.Itoa(int(p))
|
|
case int:
|
|
return strconv.Itoa(p)
|
|
case string:
|
|
return p
|
|
}
|
|
return ""
|
|
}
|