The Go compile, the Node tsc/esbuild/npm and the image's buildah bud get the build argument MESH_VERSION from the version the build ask carries. The late publish on version.numbered goes (code, tests, the consumes of the Go and image manifests). The image's label and tag and a program's stamp are open (question to the lead): the design names no key or symbol. Builds: design 47 G §349, §370, §413 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
115 lines
4.2 KiB
Go
115 lines
4.2 KiB
Go
// mesh-build-image: the image build module (novox/hq ADR 0487, ADR 0466 §1, ADR 0469; design 47 R4c-9, issue 1703). A
|
|
// tools bundle the node's runtime launches as the module's own account (runs-as). It builds a module's `image`
|
|
// artifact from the Dockerfile in the module's own directory with buildah, rootless, in a user namespace of its
|
|
// account and with no container runtime socket, and pushes a merged trunk commit's image to the registry from its
|
|
// own process once the build has ended. It refuses a step only when buildah cannot be run or the account has no
|
|
// subordinate ranges, and its health is then unhealthy. What it says, it says on stderr; stdout is the MCP channel.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"strings"
|
|
"time"
|
|
|
|
reg "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
|
|
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage"
|
|
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// The directories the module's manifest places outside its home (ADR 0466 §1): the workspaces, and buildah's image
|
|
// store, so nothing of the home is in a build.
|
|
const (
|
|
WorkDir = "/var/lib/mesh-build-image-work"
|
|
StoreDir = "/var/lib/mesh-build-image-store"
|
|
)
|
|
|
|
func logf(format string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-build-image] "+format+"\n", a...) }
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
m := &Module{
|
|
Node: strings.TrimSpace(os.Getenv("MESH_NODE")),
|
|
Steps: stdio.State(seat.StateSteps).Put,
|
|
ReadSteps: readOf(stdio.State(seat.StateSteps)),
|
|
Pauses: stdio.State(sdkstep.PausesState).Put,
|
|
ReadPauses: readOf(stdio.State(sdkstep.PausesState)),
|
|
ForgetPause: stdio.State(sdkstep.PausesState).Delete,
|
|
WatchPauses: stdio.State(sdkstep.PausesState).Watch,
|
|
Ctx: ctx,
|
|
Say: logf,
|
|
Work: WorkDir,
|
|
Store: StoreDir,
|
|
}
|
|
m.Floor = floorOf(m.Node)
|
|
if u, err := user.Current(); err == nil {
|
|
m.Home, m.Account, m.UID = u.HomeDir, u.Username, u.Uid
|
|
}
|
|
waits := stdio.State(MirrorWaitsState)
|
|
m.Outcomes = &Outcomes{Waits: stateWaits{waits}, Node: m.Node}
|
|
m.ReadWaits = readOf(waits)
|
|
m.Settings = m.Home + "/settings.json"
|
|
m.Run = StepRunner(m.Home)
|
|
// Outside a launch by the runtime — a build, a check — there is no node and no state to write.
|
|
if m.Node != "" {
|
|
go m.Start(ctx)
|
|
}
|
|
go startKeeper(ctx)
|
|
// Every state it keeps, said on the bus before it serves (ADR 0495 §2; issue 1790).
|
|
if err := keepStates(); err != nil {
|
|
logf("announcing the states it keeps: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
steps := m.StepTools()
|
|
booted := time.Now()
|
|
puller := m.Puller(steps, nil)
|
|
m.Held = puller
|
|
if m.Node != "" {
|
|
// A step a previous process left running is ended (issue 1890): one that names another owner, or none and
|
|
// started before booted.
|
|
go m.endLeftRunning(ctx, m.stepScan(), booted, logf)
|
|
}
|
|
if m.Node != "" {
|
|
// The image seat's outcomes, which a copy waits on (the manifest's consumes), and the copies' waits in
|
|
// mirror-waits, where another instance writes an outcome it heard.
|
|
go hearOutcomes(ctx, m.Outcomes)
|
|
go waits.Follow("", m.Outcomes.Changed)
|
|
}
|
|
if m.Node != "" {
|
|
// The seat's asks, pulled once Serve has the runtime's channel; a pull before it is said and tried again.
|
|
go func() { _ = puller.Pull(ctx) }()
|
|
}
|
|
if err := stdio.Serve("", append(m.Tools(steps), m.SeatTools(puller)...)); err != nil {
|
|
logf("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// OutcomeEvents are the image seat's events a copy's end is said by, as the manifest consumes them.
|
|
var OutcomeEvents = []string{reg.EventPublished, reg.EventRefused}
|
|
|
|
// hearOutcomes subscribes to the image seat's published and refused, asking again until the runtime takes each.
|
|
func hearOutcomes(ctx context.Context, o *Outcomes) {
|
|
for _, event := range OutcomeEvents {
|
|
pattern := reg.Seat + "." + event
|
|
go func() {
|
|
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
|
err := stdio.Subscribe(pattern, func(e stdio.Envelope) error { return o.Heard(event, e.Body) })
|
|
if err == nil {
|
|
logf("listening for %s", pattern)
|
|
return
|
|
}
|
|
logf("not hearing %s yet (%v); asking again in %s", pattern, err, wait)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(wait):
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|