Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
182 lines
6.6 KiB
Go
182 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"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) {
|
|
base, err := seatBase(world, seatName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
path := strings.TrimSuffix(strings.Trim(repository, "/"), ".git")
|
|
return fmt.Sprintf("%s/%s.git", base, path), nil
|
|
}
|
|
|
|
// seatBase is `scheme://host:port` of a seat's holder as the mesh reaches it, for cloning.
|
|
func seatBase(world catalogue.World, seatName 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 nothing can be cloned from this mesh's "+
|
|
"forge — assign a module that claims it, or build from the repository's URL without --self",
|
|
seat.Name)
|
|
}
|
|
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)
|
|
}
|
|
return fmt.Sprintf("%s://%s:%s", scheme, provider.At, port), nil
|
|
}
|
|
|
|
// seatBases is the clone base of every seat a recipe's context may name, for a build request
|
|
// (novox/hq ADR 0155). A seat nobody holds is left out rather than refused here: the build may not
|
|
// name it at all, and if it does the builder refuses with the seat's name.
|
|
//
|
|
// **What cannot be read is said, not passed over as no seats** (novox/hq to-be 45 Phase 2, the
|
|
// empty-on-error lint): the build still goes ahead — one that names no seat needs none — and one that
|
|
// does is refused naming it, but the reason is the store, and that is said here where it is known.
|
|
func seatBases(ctx context.Context) map[string]string {
|
|
unread := func(what string, err error) map[string]string {
|
|
fmt.Fprintf(os.Stderr, "could not read %s, so a build naming a seat's clone base will be told that "+
|
|
"seat is not held: %v\n", what, err)
|
|
return nil
|
|
}
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return unread("the mesh's store", err)
|
|
}
|
|
defer open.Close()
|
|
shelf, err := open.inventory.Catalogue(ctx)
|
|
if err != nil {
|
|
return unread("the catalogue", err)
|
|
}
|
|
world, err := theRestOfTheMesh(ctx, open.inventory, shelf, "")
|
|
if err != nil {
|
|
return unread("who holds which seat", err)
|
|
}
|
|
bases := map[string]string{}
|
|
for _, seatName := range []string{gitSeat} {
|
|
if base, err := seatBase(world, seatName); err == nil {
|
|
bases[seatName] = base
|
|
}
|
|
}
|
|
return bases
|
|
}
|
|
|
|
// 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 ""
|
|
}
|