Let the Go build seat carry a compose ask and its answer, so the assigner composes a group with its mesh-sdk head (issue 1622)
This commit is contained in:
+76
-8
@@ -1,8 +1,11 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -154,8 +157,8 @@ func checkAskID(id string) error {
|
||||
}
|
||||
|
||||
// StepAsk is the message on a build seat's stream: who asked what, of which kind and language, and the one whole job.
|
||||
// Exactly one of Build and Check is set: a delivery ask is a build; a check ask a build (a candidate's fingerprint) or a
|
||||
// check; a recheck ask a check; an image ask is always a build.
|
||||
// Exactly one of Build, Check and Compose is set: a delivery ask is a build; a check ask a build (a candidate's
|
||||
// fingerprint), a check or, on the Go seat, a compose; a recheck ask a check; an image ask is always a build.
|
||||
type StepAsk struct {
|
||||
V int `json:"v"`
|
||||
ID string `json:"id"`
|
||||
@@ -163,10 +166,48 @@ type StepAsk struct {
|
||||
Language string `json:"language"`
|
||||
// Asker is the module that put the ask (mesh-delivery-builder or mesh-delivery-checker); the stream's grant is what
|
||||
// makes it true, this field only names it to a reader.
|
||||
Asker string `json:"asker"`
|
||||
At time.Time `json:"at"`
|
||||
Build *BuildJob `json:"build,omitempty"`
|
||||
Check *CheckJob `json:"check,omitempty"`
|
||||
Asker string `json:"asker"`
|
||||
At time.Time `json:"at"`
|
||||
Build *BuildJob `json:"build,omitempty"`
|
||||
Check *CheckJob `json:"check,omitempty"`
|
||||
Compose *ComposeJob `json:"compose,omitempty"`
|
||||
}
|
||||
|
||||
// ComposeJob is a compose ask's job (novox/hq ADR 0482 clause 4, issue 1622): fetch the commit, build the composer
|
||||
// command (Package) in the Go module at Dir of the fetched tree, once per ComposerKey, run it in a run step shut off
|
||||
// with Request on its standard input, and answer its standard output whole (ComposeAnswer). The assigner asks it, to
|
||||
// compose a group with its mesh-sdk member's head, and to read the group's manifests with the mesh-sdk the running
|
||||
// controller and node-engine pin (ADR 0482 clause 5, issue 1623).
|
||||
type ComposeJob struct {
|
||||
// Fetch is the fetch tool's arguments: the repository and the full commit whose mesh-sdk composes (mesh-sdk itself
|
||||
// at a head, or a module that requires mesh-sdk, at the version it pins).
|
||||
Fetch map[string]any `json:"fetch"`
|
||||
// Dir is the directory of the Go module the composer is built in, relative to the repository's root ("go" for
|
||||
// mesh-sdk, RootPath for a module at the root); Package is the composer's import path (mesh-sdk composer.Package).
|
||||
Dir string `json:"dir"`
|
||||
Package string `json:"package"`
|
||||
// Request is the composer's request (mesh-sdk composer.Request), its manifests named by their digests in the
|
||||
// indexer's ManifestsBucket: the build module fills them before the run (composer.Fill).
|
||||
Request json.RawMessage `json:"request"`
|
||||
// Reader names, for a reader of the ask, whose composition this is: the mesh-sdk head of a group, or a module whose
|
||||
// mesh-sdk reads the group's manifests (mesh-controller, mesh-host).
|
||||
Reader string `json:"reader,omitempty"`
|
||||
}
|
||||
|
||||
// ComposeAnswer is a compose ask's step result: whether this run built the composer or took the one kept under its
|
||||
// key, the key, and the composer's verdict (mesh-sdk composer.Verdict) whole.
|
||||
type ComposeAnswer struct {
|
||||
Built bool `json:"built"`
|
||||
Key string `json:"key"`
|
||||
Verdict json.RawMessage `json:"verdict"`
|
||||
}
|
||||
|
||||
// ComposerKey is the key a build module keeps a built composer under: the repository, commit, directory and package it
|
||||
// was built from and the toolchain it was built with, hashed (`composer-` and 16 hex digits), so each head is built once
|
||||
// on a node (ADR 0482 clause 4).
|
||||
func ComposerKey(repository, commit, dir, pkg string, toolchain ToolchainUsed) string {
|
||||
sum := sha256.Sum256([]byte(strings.Join([]string{repository, commit, dir, pkg, toolchain.Version, toolchain.Recipe}, "\n")))
|
||||
return "composer-" + hex.EncodeToString(sum[:8])
|
||||
}
|
||||
|
||||
// BuildJob is a build ask's job: fetch the commit (unless every artifact is a copy), then build or copy each artifact,
|
||||
@@ -232,9 +273,17 @@ func (a StepAsk) check() error {
|
||||
if a.At.IsZero() {
|
||||
return fmt.Errorf("delivery: the ask %s names no time", a.ID)
|
||||
}
|
||||
jobs := 0
|
||||
for _, set := range []bool{a.Build != nil, a.Check != nil, a.Compose != nil} {
|
||||
if set {
|
||||
jobs++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case (a.Build == nil) == (a.Check == nil):
|
||||
return fmt.Errorf("delivery: the ask %s holds a build or a check, exactly one", a.ID)
|
||||
case jobs != 1:
|
||||
return fmt.Errorf("delivery: the ask %s holds a build, a check or a compose, exactly one", a.ID)
|
||||
case a.Compose != nil && (a.Kind != AskCheck || a.Language != AskGo):
|
||||
return fmt.Errorf("delivery: the ask %s is a compose, which the Go seat's check stream alone carries", a.ID)
|
||||
case a.Build != nil && a.Kind == AskRecheck:
|
||||
return fmt.Errorf("delivery: the ask %s is a build on the recheck stream, which carries checks alone", a.ID)
|
||||
case a.Check != nil && a.Kind == AskDelivery:
|
||||
@@ -245,9 +294,28 @@ func (a StepAsk) check() error {
|
||||
if a.Build != nil {
|
||||
return a.Build.check(a.ID)
|
||||
}
|
||||
if a.Compose != nil {
|
||||
return a.Compose.check(a.ID)
|
||||
}
|
||||
return a.Check.check(a.ID)
|
||||
}
|
||||
|
||||
func (c ComposeJob) check(id string) error {
|
||||
if err := checkFetchArgs(id, c.Fetch); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Dir == "" || c.Dir != path.Clean(c.Dir) || strings.HasPrefix(c.Dir, "/") || c.Dir == ".." || strings.HasPrefix(c.Dir, "../") {
|
||||
return fmt.Errorf("delivery: the compose ask %s names no clean directory inside the repository (%q)", id, c.Dir)
|
||||
}
|
||||
if c.Package == "" {
|
||||
return fmt.Errorf("delivery: the compose ask %s names no package to build", id)
|
||||
}
|
||||
if !json.Valid(c.Request) || len(c.Request) == 0 || c.Request[0] != '{' {
|
||||
return fmt.Errorf("delivery: the compose ask %s holds no request object", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BuildJob) check(id string) error {
|
||||
if err := checkName("module", b.Module); err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func composeAsk() StepAsk {
|
||||
return StepAsk{ID: "mh3k2x9qa1b2c5", Kind: AskCheck, Language: AskGo, Asker: "mesh-delivery-assigner",
|
||||
At: time.Date(2026, 10, 24, 3, 0, 0, 0, time.UTC),
|
||||
Compose: &ComposeJob{Fetch: map[string]any{"repository": "novox/mesh-sdk", "commit": askCommit},
|
||||
Dir: "go", Package: "git.novox.be/novox/mesh-sdk/go/cmd/mesh-compose",
|
||||
Request: json.RawMessage(`{"v":1,"cases":[{"name":"change","manifests":{"demo":{"digest":"d1"}}}]}`), Reader: "mesh-sdk head"}}
|
||||
}
|
||||
|
||||
// 1622: a compose ask encodes and decodes with every field.
|
||||
func TestAComposeAskRoundTrips(t *testing.T) {
|
||||
a := composeAsk()
|
||||
raw, err := EncodeStepAsk(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := DecodeStepAsk(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.V = StepAskVersion
|
||||
if !reflect.DeepEqual(got, a) {
|
||||
t.Fatalf("round trip:\n got %+v\nwant %+v", got, a)
|
||||
}
|
||||
}
|
||||
|
||||
// 1622: a compose ask goes only on the Go seat's check stream, holds no other job beside it, fetches a full commit,
|
||||
// builds a package in a clean directory of the repository and carries a request object.
|
||||
func TestAComposeAskIsRefusedWhereItDoesNotBelong(t *testing.T) {
|
||||
for name, change := range map[string]func(*StepAsk){
|
||||
"on the delivery stream": func(a *StepAsk) { a.Kind = AskDelivery },
|
||||
"on the recheck stream": func(a *StepAsk) { a.Kind = AskRecheck },
|
||||
"on the Node seat": func(a *StepAsk) { a.Language = AskNode },
|
||||
"beside a check": func(a *StepAsk) { a.Check = checkAsk().Check },
|
||||
"with a short commit": func(a *StepAsk) { a.Compose.Fetch["commit"] = "0123456" },
|
||||
"in no directory": func(a *StepAsk) { a.Compose.Dir = "" },
|
||||
"outside the repository": func(a *StepAsk) { a.Compose.Dir = "../x" },
|
||||
"with no package": func(a *StepAsk) { a.Compose.Package = "" },
|
||||
"with no request": func(a *StepAsk) { a.Compose.Request = nil },
|
||||
"with a request not JSON": func(a *StepAsk) { a.Compose.Request = json.RawMessage(`[1]`) },
|
||||
} {
|
||||
a := composeAsk()
|
||||
change(&a)
|
||||
if _, err := EncodeStepAsk(a); err == nil {
|
||||
t.Errorf("a compose ask %s is encoded", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1622/AC3: a composer is kept under one key per repository, commit, directory, package and toolchain, so two asks of
|
||||
// one head build it once on a node, and another head or toolchain builds its own.
|
||||
func TestAComposerIsKeptByWhatItWasBuiltFrom(t *testing.T) {
|
||||
tc := ToolchainUsed{Version: "go1.26.0", Recipe: "3"}
|
||||
k := ComposerKey("novox/mesh-sdk", askCommit, "go", "p", tc)
|
||||
if k != ComposerKey("novox/mesh-sdk", askCommit, "go", "p", tc) || !strings.HasPrefix(k, "composer-") || len(k) != len("composer-")+16 {
|
||||
t.Fatalf("key = %q", k)
|
||||
}
|
||||
for _, other := range []string{
|
||||
ComposerKey("novox/mesh-sdk", strings.Repeat("1", 40), "go", "p", tc),
|
||||
ComposerKey("novox/mesh-controller", askCommit, ".", "p", tc),
|
||||
ComposerKey("novox/mesh-sdk", askCommit, "go", "p", ToolchainUsed{Version: "go1.26.1", Recipe: "3"}),
|
||||
} {
|
||||
if other == k {
|
||||
t.Errorf("two composers built from different inputs share the key %s", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user