Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/module.go
T

162 lines
7.0 KiB
Go

package main
import (
"context"
"fmt"
"net/http"
"time"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
"git.novox.be/novox/mesh-sdk/go/health"
)
// CheckRound is how often the module reads the installed toolchain again, beside the check before every step and
// the one every health look makes.
const CheckRound = 10 * time.Minute
// Module is the Go build module's one running state: the node it runs on, its recipe, the toolchain guard and the
// states it writes (toolchains, steps).
type Module struct {
Node string
Recipe Recipe
// Probe reads the installed Go version (ProbeGo).
Probe func(ctx context.Context) (string, error)
// Toolchains and Steps are the Put of the states of those names.
Toolchains, Steps buildstep.Put
// ReadSteps and ReadToolchainsState read those two states back for the read tools (issue 1562).
ReadSteps, ReadToolchainsState StateRead
// Emit tells the bus an event (stdio.Emit); nil emits nothing. The guard emits toolchain.reported after each write of
// the toolchains key, so the indexer, which holds no timer, learns that the bucket exists (issue 1444).
Emit func(eventType string, body any) error
// Ctx is the process's context, which a step runs under (the tool call's is cut at 30 s, the step's is not).
Ctx context.Context
Say func(format string, a ...any)
// Run runs the commands of a step (StepRunner); Work holds the workspaces and Cache the module and build caches,
// both outside the account's home (ADR 0466 §1); Home is the account's home.
Run forgekey.Runner
Home, Work, Cache string
// RemoteBase is where repositories are fetched from, "ssh://mesh-forge/" when empty; FetchOptions are the options of
// the SDK's fetch and merge. A test points them at a local repository (buildstep.AllowFileRemote).
RemoteBase string
FetchOptions []buildstep.Option
// HTTP fetches the files an archive artifact is pinned to (url and sha256); a client of its own with a fifteen-minute
// timeout when nil. A test serves its own.
HTTP *http.Client
// UnitBound is the unit level's bound; zero is UnitBound. A test lowers it to see a level over its bound.
UnitBound time.Duration
// IntegrationBounds are the integration level's bounds; zero values are IntegrationSetup and IntegrationRun.
IntegrationBounds Bounds
// Now is the clock the levels' times are read from; time.Now when nil.
Now func() time.Time
// StartBus starts the throwaway bus of a run in dir and answers its address and what stops it (the module's own
// program, StartBus in bus.go, when nil).
StartBus func(ctx context.Context, dir string) (url string, stop func(), err error)
}
// guard is the SDK guard for the version the probe just read: the module names no Go version (ADR 0477 §2), so the
// version wanted is the one installed and the guard refuses only when go cannot be run at all, writing the installed
// version to <node>.go of the toolchains state every time.
func (m *Module) guard(installed string, probeErr error) buildstep.Guard {
return buildstep.Guard{Language: "go", Want: installed, Recipe: m.Recipe.Version(), Node: m.Node, Put: m.reportingPut(),
Probe: func(context.Context) (string, error) { return installed, probeErr }}
}
// Check reads the installed toolchain, writes <node>.go of the toolchains state and answers nil, or the refusal when
// go cannot be run (buildstep.Guard.Check). A version other than another node's is not a refusal: the indexer waits
// while build nodes disagree (ADR 0477 §2). It runs at start, before every step, at every health look and every
// CheckRound.
func (m *Module) Check(ctx context.Context) error {
installed, err := m.Probe(ctx)
return m.guard(installed, err).Check(ctx)
}
// Start checks once, as the module starts, and then every CheckRound until ctx ends.
func (m *Module) Start(ctx context.Context) {
for {
if err := m.Check(ctx); err != nil && m.Say != nil {
m.Say("the toolchain: %v", err)
}
m.CleanWorkspaces(time.Now())
select {
case <-ctx.Done():
return
case <-time.After(CheckRound):
}
}
}
// Health is the module's health (the runtime-health tool, ADR 0366): unhealthy only while go cannot be run, saying why, so the
// controller raises the node's condition; healthy it names the installed version (ADR 0477 §2). It looks again each time it is
// asked.
func (m *Module) Health(ctx context.Context) health.Answer {
installed, perr := m.Probe(ctx)
if err := m.guard(installed, perr).Check(ctx); err != nil {
return health.Unhealthy(err.Error())
}
return health.Healthy(fmt.Sprintf("%s installed, the version the module builds with (recipe %s)", installed, m.Recipe.Version()))
}
// Long is a tool that runs a long step (ADR 0469 §2): it answers at once with the step's id, and the step ends on
// its key in the steps state. The guard runs inside the step, before run, so a node whose go cannot be run ends every step
// refused with the cause named.
func (m *Module) Long(tool string, run func(ctx context.Context, args map[string]any) (any, error)) func(map[string]any) (any, error) {
return func(args map[string]any) (any, error) {
id, err := buildstep.Start(m.Ctx, m.Steps, m.Node, tool, func(ctx context.Context) (any, error) {
if err := m.Check(ctx); err != nil {
return nil, err
}
return run(ctx, args)
})
if err != nil {
return nil, err
}
key, _ := delivery.StepKey(m.Node, id)
return map[string]any{"step": id, "key": key, "state": delivery.StepRunning}, nil
}
}
// Short is a tool that answers in its own call; the guard runs before it, and a refusal is the call's error.
func (m *Module) Short(run func(ctx context.Context, args map[string]any) (any, error)) func(map[string]any) (any, error) {
return func(args map[string]any) (any, error) {
if err := m.Check(m.Ctx); err != nil {
return nil, err
}
return run(m.Ctx, args)
}
}
// Status is what the module reports of itself: the node, the recipe, the toolchain found and why it refuses, if it
// does.
func (m *Module) Status(ctx context.Context) map[string]any {
err := m.Check(ctx)
installed, _ := m.Probe(ctx)
out := map[string]any{
"node": m.Node, "recipe": m.Recipe.Version(), "installed": installed,
"postgres-major": m.Recipe.PostgresMajor, "nats-server": m.Recipe.NatsServer, "refusing": err != nil,
}
if err != nil {
out["why"] = err.Error()
}
return out
}
// ToolchainReported is the event the guard emits after it writes the toolchains key.
const ToolchainReported = "toolchain.reported"
// reportingPut is the toolchains Put that then emits toolchain.reported with the node and the language. A write that
// failed emits nothing; an emit that failed is said and does not refuse the step.
func (m *Module) reportingPut() buildstep.Put {
return func(key string, value any) (uint64, error) {
rev, err := m.Toolchains(key, value)
if err != nil || m.Emit == nil {
return rev, err
}
if eerr := m.Emit(ToolchainReported, map[string]string{"node": m.Node, "language": "go"}); eerr != nil && m.Say != nil {
m.Say("the event %s could not be emitted: %v", ToolchainReported, eerr)
}
return rev, nil
}
}