Two gaps. The bundle's contents are per system even though its mechanism is not, so there are now three: substrate-arch.lock, substrate-alpine.lock and substrate-android.lock. All three are embedded and a host reads only the one it was built for. Arch and Alpine remain placeholders -- the closure for a one-node mesh is still research 011/012's open question, and inventing it here would be worse than an honest placeholder. Android's is not a placeholder. It says a partial host cannot raise a mesh and why: every step of a bootstrap is a package, a container, or an action against one, and those are exactly the shapes it refuses. So a partial host can JOIN a mesh and cannot BE the first node. That belongs where somebody looking for the android bundle will find it. Also separated two things that were being conflated: "this system has no bundle" and "this system was never built". Loading a bundle for debian is not ErrEmpty, and the test asserts they differ. 0062 -- a host may be episodic. There is no way to keep a process running on an ordinary Android device: init needs root, a foreground service can be killed for memory. The answer is not to fight that. It is that being killed IS disconnection, which ADR 0036 already made an ordinary situation -- and everything the design does for a laptop that closes is what an episodic host needs, at a shorter period. An authoritative local store, reconcile on start, last-heard-from reported without an alarm. So the gap closes by requiring less rather than building something. No keep- alive, no Android daemon, no fighting the platform's process management. Two consequences recorded rather than glossed. Last-heard-from is a much weaker signal on an episodic host, so a healthy phone reads as a dead server unless the reader knows which kind it is looking at. And a declaration may take a long time to land, which makes 0058's separation of outstanding from failed load-bearing rather than tidy. Left open deliberately: how an episodic host is actually started, and -- first -- what an Android node is for. Building the start mechanism before deciding that would be building it for nobody.
100 lines
3.7 KiB
Go
100 lines
3.7 KiB
Go
// Package bundle is the declaration the host carries.
|
|
//
|
|
// novox/hq ADR 0038: the host has one behaviour and two sources of declaration — the control
|
|
// plane when a mesh is reachable, and this when none is. The first node is not a different
|
|
// kind of node; it is a node whose mesh is not up yet, and this is what it applies until it is.
|
|
//
|
|
// Carried inside the binary rather than beside it, because "copy it onto a machine and run it
|
|
// is the whole installation" (ADR 0041) stops being true the moment a second file has to
|
|
// arrive with it.
|
|
package bundle
|
|
|
|
import (
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// One bundle per operating system, because its CONTENTS are per system even though its
|
|
// mechanism is not: package names, unit names and service names all differ
|
|
// (novox/hq ADR 0060). All three are embedded and the host applies the one it was built for —
|
|
// an arch host never reads the alpine bundle.
|
|
//
|
|
// A host whose bundle is only comments carries nothing, and says so rather than applying
|
|
// nothing and reporting success — a host that silently did nothing on a first node would look
|
|
// exactly like one that worked.
|
|
//
|
|
//go:embed substrate-arch.lock
|
|
var archLock []byte
|
|
|
|
//go:embed substrate-alpine.lock
|
|
var alpineLock []byte
|
|
|
|
//go:embed substrate-android.lock
|
|
var androidLock []byte
|
|
|
|
var locks = map[string][]byte{
|
|
"arch": archLock,
|
|
"alpine": alpineLock,
|
|
"android": androidLock,
|
|
}
|
|
|
|
// ErrEmpty means this host carries no bundle.
|
|
var ErrEmpty = errors.New(
|
|
"this host carries no bundle. A host without one cannot raise a first node, and applying " +
|
|
"nothing would look exactly like applying something")
|
|
|
|
// Raw returns the carried bytes, for inspection.
|
|
func Raw(system string) []byte { return locks[system] }
|
|
|
|
// IsEmpty reports whether anything was built in. A bundle of only comments and whitespace is
|
|
// empty for this purpose: a placeholder is a comment, and treating it as content would mean a
|
|
// host claims to carry a substrate it does not.
|
|
func IsEmpty(system string) bool {
|
|
for _, line := range strings.Split(string(locks[system]), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" && !strings.HasPrefix(line, "//") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Load parses the carried bundle.
|
|
//
|
|
// The same parser the link will use. A bundle that reaches a machine and is then refused by the
|
|
// host that carries it would be a build-time mistake discovered at the worst possible moment,
|
|
// which is why `mesh-host bundle` exists to ask before it matters.
|
|
func Load(system string) (*declaration.Declaration, error) {
|
|
if _, known := locks[system]; !known {
|
|
return nil, fmt.Errorf("no bundle is built for %q", system)
|
|
}
|
|
if IsEmpty(system) {
|
|
return nil, ErrEmpty
|
|
}
|
|
// ParseTrusted: the bundle arrives with the binary, so it may carry actions the link may
|
|
// not (novox/hq ADR 0047). The bootstrap needs them — creating the control plane's database
|
|
// happens before there is any mesh to ask for one.
|
|
return declaration.ParseTrusted(stripComments(locks[system]))
|
|
}
|
|
|
|
// stripComments removes whole-line `//` comments so a bundle can be annotated.
|
|
//
|
|
// It is JSON on the wire and a pinned, hand-authored artefact here, and a pinned thing nobody
|
|
// can annotate is a pinned thing nobody can review. Only whole lines: anything cleverer would
|
|
// need to know where strings begin and end, and a parser that half-understands its input is
|
|
// worse than one that does not try.
|
|
func stripComments(raw []byte) []byte {
|
|
var kept []string
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
if strings.HasPrefix(strings.TrimSpace(line), "//") {
|
|
continue
|
|
}
|
|
kept = append(kept, line)
|
|
}
|
|
return []byte(strings.Join(kept, "\n"))
|
|
}
|