Build Go with the installed go and name no Go version (issue 1572, ADR 0477)
This commit is contained in:
@@ -145,8 +145,8 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
|
||||
return BuildResult{Artifact: art.Name, Binary: binary, Digest: digest, Size: len(archive), Path: path}, nil
|
||||
}
|
||||
|
||||
// buildEnv is the environment of the compile: the build agent's (CGO_ENABLED=0, GOFLAGS) with the recipe's Go release
|
||||
// alone, the modules the fetch put in the module's cache and no way to fetch another (GOPROXY=off), so a build never
|
||||
// buildEnv is the environment of the compile: the build agent's (CGO_ENABLED=0, GOFLAGS) with the installed go
|
||||
// alone (GOTOOLCHAIN=local), the modules the fetch put in the module's cache and no way to fetch another (GOPROXY=off), so a build never
|
||||
// reaches the network. A directory that vendors is built from its vendor directory.
|
||||
func (m *Module) buildEnv(dir string) []string {
|
||||
mod := "-mod=mod"
|
||||
@@ -156,7 +156,7 @@ func (m *Module) buildEnv(dir string) []string {
|
||||
return []string{
|
||||
"CGO_ENABLED=0",
|
||||
"GOFLAGS=" + recipeFlags + " " + mod,
|
||||
"GOTOOLCHAIN=" + RecipeGo,
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOMODCACHE=" + filepath.Join(m.Cache, "gomod"),
|
||||
"GOCACHE=" + filepath.Join(m.Cache, "gobuild"),
|
||||
"GOPROXY=off",
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
// DigestVersion is the form of the digest's text: changed whenever what the digest hashes changes in kind.
|
||||
const DigestVersion = "1"
|
||||
const DigestVersion = "2" // 2: no Go version in the text; the toolchain enters a fingerprint as the agreed version (ADR 0477 §3)
|
||||
|
||||
// goPackage is the part of `go list -json` a digest reads.
|
||||
type goPackage struct {
|
||||
@@ -106,7 +106,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) {
|
||||
return nil, fmt.Errorf("no workspace %s: it was never fetched here, or it is older than a day and was removed", workspace)
|
||||
}
|
||||
var tags []string
|
||||
header := []string{"digest " + DigestVersion, "level " + level, "go " + m.Recipe.Go}
|
||||
header := []string{"digest " + DigestVersion, "level " + level}
|
||||
switch level {
|
||||
case LevelUnit:
|
||||
case LevelIntegration:
|
||||
|
||||
@@ -112,13 +112,13 @@ func (m *Module) goModDirs(ctx context.Context, tree string) ([]string, error) {
|
||||
return dirs, nil
|
||||
}
|
||||
|
||||
// goEnv is what a go command of a step is given beyond the base environment: the recipe's Go release (ADR 0476: the probe put it in the module's cache; a step never fetches it),
|
||||
// goEnv is what a go command of a step is given beyond the base environment: the installed go alone (GOTOOLCHAIN=local, ADR 0477 §1: no toolchain is ever downloaded),
|
||||
// modules resolved by their go.mod and go.sum, the module's own caches, the mesh's own modules fetched directly (by
|
||||
// git, through the account's rewrite of the forge's address to ssh), and nothing that asks a question. For a directory
|
||||
// that vendors, see goEnvFor.
|
||||
func (m *Module) goEnv() []string {
|
||||
return []string{
|
||||
"GOTOOLCHAIN=" + RecipeGo,
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOFLAGS=-mod=mod",
|
||||
"GOMODCACHE=" + filepath.Join(m.Cache, "gomod"),
|
||||
"GOCACHE=" + filepath.Join(m.Cache, "gobuild"),
|
||||
@@ -165,8 +165,7 @@ func (m *Module) clock() time.Time {
|
||||
func (m *Module) now() time.Time { return m.clock() }
|
||||
|
||||
// causeLine is the first line of a go command's output that says something other than Go's own progress: `go:
|
||||
// downloading ...` comes first when Go fetches a toolchain or a module, and the cause of a failure on a later line
|
||||
// (issue 1560: a refusal read only "go: downloading go1.27.1 (linux/amd64)").
|
||||
// downloading ...` comes first when Go fetches a module, and the cause of a failure on a later line.
|
||||
func causeLine(s string) string {
|
||||
for _, l := range strings.Split(s, "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "go: downloading ") {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// mesh-build-go: the Go build module (novox/hq ADR 0405, ADR 0466 §1, ADR 0469, ADR 0470; design 47 R4c-3, issue
|
||||
// 1483). A Go tools bundle the node's runtime launches as the module's own account (runs-as). It names the one Go
|
||||
// toolchain version it builds with, writes the version it finds to <node>.go of its `toolchains` state at start and
|
||||
// before every step, and refuses every step on a node that has another: its health is then unhealthy, so the
|
||||
// controller raises the node's condition. What it says, it says on stderr; stdout is the MCP channel.
|
||||
// 1483). A Go tools bundle the node's runtime launches as the module's own account (runs-as). It builds with the
|
||||
// go its node has installed (GOTOOLCHAIN=local, ADR 0477 §1; issue 1572), names no Go version, writes the version it
|
||||
// finds to <node>.go of its `toolchains` state at start and before every step, and refuses a step only when go
|
||||
// cannot be run: its health is then unhealthy, so the controller raises the node's condition. What it says, it says on stderr; stdout is the MCP channel.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -45,14 +45,21 @@ type Module struct {
|
||||
StartBus func(ctx context.Context, dir string) (url string, stop func(), err error)
|
||||
}
|
||||
|
||||
func (m *Module) guard() buildstep.Guard {
|
||||
return buildstep.Guard{Language: "go", Want: m.Recipe.Go, Recipe: m.Recipe.Version(), Probe: m.Probe, Node: m.Node, Put: m.reportingPut()}
|
||||
// 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
|
||||
// (buildstep.Guard.Check). It runs at start, before every step, at every health look and every CheckRound.
|
||||
// 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 {
|
||||
return m.guard().Check(ctx)
|
||||
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.
|
||||
@@ -70,22 +77,20 @@ func (m *Module) Start(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Health is the module's health (the runtime-health tool, ADR 0366): unhealthy while the toolchain installed is not
|
||||
// the recipe's, saying both versions, so the controller raises the node's condition. It looks again each time it is
|
||||
// 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 {
|
||||
err := m.Check(ctx)
|
||||
switch {
|
||||
case err == nil:
|
||||
return health.Healthy(fmt.Sprintf("%s installed, as the recipe %s builds with", m.Recipe.Go, m.Recipe.Version()))
|
||||
default:
|
||||
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 that refuses ends every step
|
||||
// refused with both versions named.
|
||||
// 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) {
|
||||
@@ -118,7 +123,7 @@ 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(), "go": m.Recipe.Go, "installed": installed,
|
||||
"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 {
|
||||
|
||||
@@ -10,52 +10,34 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
||||
)
|
||||
|
||||
// ProbeTimeout bounds `go version` when the release is in the cache; FetchTimeout bounds it while the release is
|
||||
// missing and the installed Go downloads it (about seventy megabytes).
|
||||
const (
|
||||
ProbeTimeout = 15 * time.Second
|
||||
FetchTimeout = 5 * time.Minute
|
||||
)
|
||||
// ProbeTimeout bounds `go version`: it reads the installed go and fetches nothing (ADR 0477 §1).
|
||||
const ProbeTimeout = 15 * time.Second
|
||||
|
||||
// ProbeGo is the probe of the guard (novox/hq ADR 0476 §2, §3; issue 1560): it runs `go version` with
|
||||
// GOTOOLCHAIN=RecipeGo, so it reads the version of Go's own release of the recipe's version, which is what the
|
||||
// module builds with, and the third word of its answer is that version. It runs with network and the module's own
|
||||
// Go module cache (cache/gomod): while the release is missing from it, the installed Go downloads it there, verified
|
||||
// against Go's checksum database. No step fetches it; a step runs behind this probe and refuses when it is missing.
|
||||
// ProbeGo is the probe of the guard (novox/hq ADR 0477 §1, §2; issue 1572): it runs `go version` with
|
||||
// GOTOOLCHAIN=local, so it reads the version of the go the node's package manager installed, which is what the
|
||||
// module builds with, and the third word of its answer is that version. It downloads nothing and needs no network.
|
||||
// The environment is built from scratch like a step's (StepRunner), so the module's bus credentials are not in it.
|
||||
func ProbeGo(home, cache string) func(ctx context.Context) (string, error) {
|
||||
run := forgekey.ExecRunnerClean(0)
|
||||
return func(ctx context.Context) (string, error) { return probeGo(ctx, run, home, cache) }
|
||||
}
|
||||
|
||||
// probeEnv is what the probe gives `go version` beyond the base environment: the recipe's release as the toolchain,
|
||||
// the module's cache for it to land in (and the build cache the go command wants), and Go's own proxy and checksum
|
||||
// database, which this module's steps never leave at their default except here and in fetch.
|
||||
// probeEnv is what the probe gives `go version` beyond the base environment: the installed go alone, and the
|
||||
// module's caches, so nothing is written to the account's home.
|
||||
func probeEnv(cache string) []string {
|
||||
return []string{
|
||||
"GOTOOLCHAIN=" + RecipeGo,
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOMODCACHE=" + filepath.Join(cache, "gomod"),
|
||||
"GOCACHE=" + filepath.Join(cache, "gobuild"),
|
||||
}
|
||||
}
|
||||
|
||||
// releaseCached says whether the release is in the module cache: golang.org/toolchain@v0.0.1-<version>.<os>-<arch>.
|
||||
func releaseCached(cache string) bool {
|
||||
dirs, _ := filepath.Glob(filepath.Join(cache, "gomod", "golang.org", "toolchain@v0.0.1-"+RecipeGo+".*"))
|
||||
return len(dirs) > 0
|
||||
}
|
||||
|
||||
func probeGo(ctx context.Context, run forgekey.Runner, home, cache string) (string, error) {
|
||||
bound := ProbeTimeout
|
||||
if !releaseCached(cache) {
|
||||
bound = FetchTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, bound)
|
||||
ctx, cancel := context.WithTimeout(ctx, ProbeTimeout)
|
||||
defer cancel()
|
||||
r := run(ctx, append(baseEnv(home), probeEnv(cache)...), "go", "version")
|
||||
if r.Status != 0 || r.Err != "" {
|
||||
return "", fmt.Errorf("`go version` with GOTOOLCHAIN=%s failed (the recipe builds with %s): %s", RecipeGo, RecipeGo,
|
||||
causeLine(r.Err+"\n"+r.Stderr))
|
||||
return "", fmt.Errorf("`go version` with GOTOOLCHAIN=local failed: %s", causeLine(r.Err+"\n"+r.Stderr))
|
||||
}
|
||||
return parseGoVersion(r.Stdout)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package main
|
||||
|
||||
// The recipe of the Go build module (novox/hq ADR 0405 §1 and §7, ADR 0469 §1, ADR 0470 §4; issue 1483): the one
|
||||
// Go toolchain version it builds with, the one postgres major version its integration level tests with, and the
|
||||
// recipe's own version. A node whose installed Go differs from the recipe's refuses every step (guard.go), so all
|
||||
// builders run this version or none builds.
|
||||
// postgres major version its integration level tests with, and the recipe's own version. The module names no Go
|
||||
// version (ADR 0477 §1, §2; issue 1572): it builds with the go its node has installed (GOTOOLCHAIN=local), reports
|
||||
// that version to the toolchains state, and the indexer takes the version the build nodes agree on.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -15,15 +15,11 @@ import (
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
)
|
||||
|
||||
// RecipeGo is the Go version the recipe builds with: the text `go version` prints for it, which is what the guard
|
||||
// compares and what the indexer reads from <node>.go of the toolchains state.
|
||||
const RecipeGo = "go1.27.1"
|
||||
|
||||
// PostgresMajor is the postgres major version the integration level tests with (ADR 0470 §4).
|
||||
const PostgresMajor = 18
|
||||
|
||||
// RecipeSteps is the recipe's own version: changed whenever the steps of fetch, test or build change in a way
|
||||
// that can change what they produce (ADR 0405 §7). It is one of the four values the recipe version hashes.
|
||||
// that can change what they produce (ADR 0405 §7). It is one of the three values the recipe version hashes.
|
||||
const RecipeSteps = "1"
|
||||
|
||||
// natsModule is the library the integration level's bus is started from (ADR 0470 §2).
|
||||
@@ -33,8 +29,6 @@ const natsModule = "github.com/nats-io/nats-server/v2"
|
||||
type Recipe struct {
|
||||
// Steps is RecipeSteps.
|
||||
Steps string
|
||||
// Go is the toolchain's version as `go version` prints it.
|
||||
Go string
|
||||
// PostgresMajor is the integration level's postgres major version.
|
||||
PostgresMajor int
|
||||
// NatsServer is the nats-server library's version, from the program's build information.
|
||||
@@ -43,16 +37,17 @@ type Recipe struct {
|
||||
|
||||
// Current is the recipe of this build of the module.
|
||||
func Current() Recipe {
|
||||
return Recipe{Steps: RecipeSteps, Go: RecipeGo, PostgresMajor: PostgresMajor, NatsServer: natsServerVersion()}
|
||||
return Recipe{Steps: RecipeSteps, PostgresMajor: PostgresMajor, NatsServer: natsServerVersion()}
|
||||
}
|
||||
|
||||
// Version is the recipe version: a hash of the recipe's own version, the Go version, the postgres major version and
|
||||
// the nats-server version, so a change of any of the four moves it and with it every fingerprint that holds it
|
||||
// Version is the recipe version: a hash of the recipe's own version, the postgres major version and the
|
||||
// nats-server version, never a Go version (ADR 0477 §3): the toolchain enters each fingerprint once, as the version
|
||||
// the build nodes agree on (ADR 0465). A change of any of the three moves it and with it every fingerprint that holds it
|
||||
// (ADR 0405 §7, ADR 0465 §2). The fields are named in the hashed text, so no two recipes share a text.
|
||||
func (r Recipe) Version() string {
|
||||
h := sha256.New()
|
||||
for _, f := range [][2]string{
|
||||
{"steps", r.Steps}, {"go", r.Go}, {"postgres", strconv.Itoa(r.PostgresMajor)}, {"nats-server", r.NatsServer},
|
||||
{"steps", r.Steps}, {"postgres", strconv.Itoa(r.PostgresMajor)}, {"nats-server", r.NatsServer},
|
||||
} {
|
||||
h.Write([]byte(f[0] + "=" + f[1] + "\n"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user