The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
206 lines
9.6 KiB
Go
206 lines
9.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/novox/mesh-controller/internal/builderkeys"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// planKeysOf is the indexer and the builder as a walk reads them, for a test: values by module and commit, keys by
|
|
// module.
|
|
type planKeysOf struct {
|
|
values map[string]sdkdelivery.Module // by module + "@" + commit
|
|
keys map[string]builderkeys.Key
|
|
}
|
|
|
|
func (k planKeysOf) ModuleAt(_ context.Context, module, commit string) (sdkdelivery.Module, error) {
|
|
if v, ok := k.values[module+"@"+commit]; ok {
|
|
return v, nil
|
|
}
|
|
return sdkdelivery.Module{}, builderkeys.ErrNoValue
|
|
}
|
|
|
|
func (k planKeysOf) Build(_ context.Context, module, fp string) (builderkeys.Key, error) {
|
|
if key, ok := k.keys[module]; ok && key.Fingerprint == fp {
|
|
return key, nil
|
|
}
|
|
return builderkeys.Key{Module: module, Fingerprint: fp}, nil
|
|
}
|
|
|
|
// recordsOf is the build records and refusals a settle reads, for a test.
|
|
type recordsOf struct {
|
|
builds map[string]inventory.Build
|
|
refused map[string]string
|
|
sources map[string]inventory.Source
|
|
}
|
|
|
|
func (r recordsOf) BuildByID(_ context.Context, id string) (inventory.Build, bool, error) {
|
|
b, ok := r.builds[id]
|
|
return b, ok, nil
|
|
}
|
|
|
|
func (r recordsOf) RefusedOf(_ context.Context, ids []string) (map[string]string, error) {
|
|
out := map[string]string{}
|
|
for _, id := range ids {
|
|
if why, ok := r.refused[id]; ok {
|
|
out[id] = why
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r recordsOf) SourceOf(_ context.Context, module string) (inventory.Source, error) {
|
|
if s, ok := r.sources[module]; ok {
|
|
return s, nil
|
|
}
|
|
return inventory.Source{}, inventory.ErrNoSuchModule
|
|
}
|
|
|
|
func aWalkOfOne(module string) (*inventory.Plan, map[string]inventory.Entry) {
|
|
p := &inventory.Plan{ID: "plan-1", State: inventory.PlanBuilding, Tiers: [][]string{{module}},
|
|
Modules: map[string]*inventory.PlanModule{}, Repository: "novox/mesh-catalog", Commit: "c0ffee0123456789",
|
|
Delivery: &inventory.PlanDelivery{Merges: []inventory.PlanMerge{{Repository: "novox/mesh-catalog", Commit: "c0ffee0123456789"}}}}
|
|
byName := map[string]inventory.Entry{module: {Source: inventory.Source{Repository: "novox/mesh-catalog", Seat: "git",
|
|
Path: "modules/" + module, Ref: "main"}}}
|
|
return p, byName
|
|
}
|
|
|
|
// A6 (ADR 0503 §6, ADR 0498 check §7): a walk whose module has a done key goes asked to built with no build asked: the
|
|
// ask stub is never called, the key is taken in once, and `plans` says it was taken from the builder's key.
|
|
func TestAWalkTakesADoneKeyAndAsksNoBuild(t *testing.T) {
|
|
p, byName := aWalkOfOne("sudo")
|
|
askModule(t.Context(), p, "sudo", byName)
|
|
s := p.Modules["sudo"]
|
|
if s.State != "asked" || !s.Key || s.Build != "" || s.KeyAt != "c0ffee0123456789" {
|
|
t.Fatalf("the module is not taken from a key: %+v", s)
|
|
}
|
|
fp := "src2:" + strings.Repeat("a", 64)
|
|
k := builderkeys.Key{Module: "sudo", Fingerprint: fp, Found: true, Revision: 2, Build: sdkdelivery.BuilderBuild{
|
|
State: sdkdelivery.BuildDone, Manifest: strings.Repeat("b", 64), Artifacts: []sdkdelivery.BuiltArtifact{{Name: "a",
|
|
Digest: "sha256:1", Reference: "artifact-store://sudo/a/blobs/sha256:1"}}}}
|
|
keys := planKeysOf{values: map[string]sdkdelivery.Module{"sudo@c0ffee0123456789": {FP2: fp, Commit: "c0ffee0123456789"}},
|
|
keys: map[string]builderkeys.Key{"sudo": k}}
|
|
records := recordsOf{builds: map[string]inventory.Build{}, sources: map[string]inventory.Source{}}
|
|
var taken []string
|
|
take := func(m string, _ sdkdelivery.Module, id string) { taken = append(taken, m+" "+id) }
|
|
t.Cleanup(func() {
|
|
keyTakes.Lock()
|
|
keyTakes.running, keyTakes.waits, keyTakes.taken = map[string]bool{}, map[string]string{}, map[string]bool{}
|
|
keyTakes.Unlock()
|
|
})
|
|
|
|
changed, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take)
|
|
if err != nil || !changed || len(taken) != 1 || s.State != "asked" || s.Fingerprint != fp {
|
|
t.Fatalf("a done key not recorded yet is taken in once: %v %v %v %+v", changed, err, taken, s)
|
|
}
|
|
// A second look while the take-in runs starts no second one.
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil || len(taken) != 1 {
|
|
t.Fatalf("a second look took the key in again: %v %v", taken, err)
|
|
}
|
|
// The take-in recorded and registered it: the look settles it built from the record.
|
|
id := keyBuildID(k)
|
|
records.builds[id] = inventory.Build{ID: id, Module: "sudo", Commit: "c0ffee0123456789", At: time.Now()}
|
|
records.sources["sudo"] = inventory.Source{BuiltFrom: "c0ffee0123456789"}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.State != "built" || s.Build != id || !strings.Contains(s.Made, "taken from the builder's key") {
|
|
t.Fatalf("the module is not built from its key: %+v", s)
|
|
}
|
|
}
|
|
|
|
// A7: a key building waits and says so, then moves on when done, with no timer and no ask; a failed key fails the walk
|
|
// in the builder's words; a value the indexer has not written yet waits, named.
|
|
func TestAWalkWaitsForAKeyAndFailsWithItsWords(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
keyTakes.Lock()
|
|
keyTakes.running, keyTakes.waits, keyTakes.taken = map[string]bool{}, map[string]string{}, map[string]bool{}
|
|
keyTakes.Unlock()
|
|
})
|
|
p, byName := aWalkOfOne("view")
|
|
askModule(t.Context(), p, "view", byName)
|
|
s := p.Modules["view"]
|
|
records := recordsOf{builds: map[string]inventory.Build{}}
|
|
take := func(string, sdkdelivery.Module, string) {}
|
|
keys := planKeysOf{values: map[string]sdkdelivery.Module{}, keys: map[string]builderkeys.Key{}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil ||
|
|
!strings.Contains(s.Why, "waits for the indexer's value of view at c0ffee01") {
|
|
t.Fatalf("no indexer value yet: %q %v", s.Why, err)
|
|
}
|
|
fp := "src2:" + strings.Repeat("c", 64)
|
|
keys.values["view@c0ffee0123456789"] = sdkdelivery.Module{FP2: fp}
|
|
keys.keys["view"] = builderkeys.Key{Module: "view", Fingerprint: fp, Found: true,
|
|
Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildBuilding, By: "g14"}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil ||
|
|
s.State != "asked" || !strings.Contains(s.Why, "building on g14") || p.State != inventory.PlanBuilding {
|
|
t.Fatalf("a building key: %+v %v", s, err)
|
|
}
|
|
keys.keys["view"] = builderkeys.Key{Module: "view", Fingerprint: fp, Found: true,
|
|
Build: sdkdelivery.BuilderBuild{State: sdkdelivery.BuildFailed, Error: "not stored: tools: the store answered 500"}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], take); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.State != "failed" || p.State != inventory.PlanFailed || !strings.Contains(p.Note, "not stored: tools") {
|
|
t.Fatalf("a failed key does not fail the walk in its words: %+v %q", s, p.Note)
|
|
}
|
|
}
|
|
|
|
// A7: a take-in refused (a module from another repository, a manifest refused) fails the walk with the refusal.
|
|
func TestARefusedTakeInFailsTheWalk(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
keyTakes.Lock()
|
|
keyTakes.running, keyTakes.waits, keyTakes.taken = map[string]bool{}, map[string]string{}, map[string]bool{}
|
|
keyTakes.Unlock()
|
|
})
|
|
p, byName := aWalkOfOne("sudo")
|
|
askModule(t.Context(), p, "sudo", byName)
|
|
fp := "src2:" + strings.Repeat("d", 64)
|
|
k := builderkeys.Key{Module: "sudo", Fingerprint: fp, Found: true, Revision: 1, Build: sdkdelivery.BuilderBuild{
|
|
State: sdkdelivery.BuildDone, Manifest: strings.Repeat("b", 64), Artifacts: []sdkdelivery.BuiltArtifact{{Name: "a",
|
|
Digest: "sha256:1", Reference: "artifact-store://sudo/a/blobs/sha256:1"}}}}
|
|
keys := planKeysOf{values: map[string]sdkdelivery.Module{"sudo@c0ffee0123456789": {FP2: fp}},
|
|
keys: map[string]builderkeys.Key{"sudo": k}}
|
|
records := recordsOf{refused: map[string]string{keyBuildID(k): "sudo is built from novox/mesh-catalog, and this build is of agent/sudo"}}
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], func(string, sdkdelivery.Module, string) {}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p.State != inventory.PlanFailed || !strings.Contains(p.Note, "agent/sudo") {
|
|
t.Fatalf("a refused take-in did not fail the walk: %s %q", p.State, p.Note)
|
|
}
|
|
}
|
|
|
|
// A6: a walk's module whose fingerprint did not change since an earlier merge has its key recorded at that merge's
|
|
// commit: the walk takes it in again, so the module is registered at the commit the walk carries, before it is built.
|
|
func TestAKeyRecordedAtAnEarlierCommitIsTakenInAgain(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
keyTakes.Lock()
|
|
keyTakes.running, keyTakes.waits, keyTakes.taken = map[string]bool{}, map[string]string{}, map[string]bool{}
|
|
keyTakes.Unlock()
|
|
})
|
|
p, byName := aWalkOfOne("sudo")
|
|
askModule(t.Context(), p, "sudo", byName)
|
|
fp := "src2:" + strings.Repeat("e", 64)
|
|
k := builderkeys.Key{Module: "sudo", Fingerprint: fp, Found: true, Revision: 1, Build: sdkdelivery.BuilderBuild{
|
|
State: sdkdelivery.BuildDone, Manifest: strings.Repeat("b", 64), Artifacts: []sdkdelivery.BuiltArtifact{{Name: "a",
|
|
Digest: "sha256:1", Reference: "artifact-store://sudo/a/blobs/sha256:1"}}}}
|
|
keys := planKeysOf{values: map[string]sdkdelivery.Module{"sudo@c0ffee0123456789": {FP2: fp, Commit: "c0ffee0123456789"}},
|
|
keys: map[string]builderkeys.Key{"sudo": k}}
|
|
id := keyBuildID(k)
|
|
records := recordsOf{builds: map[string]inventory.Build{id: {ID: id, Module: "sudo", Commit: "0ld0ld0ld0ld"}},
|
|
sources: map[string]inventory.Source{"sudo": {BuiltFrom: "0ld0ld0ld0ld"}}}
|
|
var taken []string
|
|
if _, err := settleKeys(t.Context(), records, keys, p, p.Tiers[0], func(m string, _ sdkdelivery.Module, _ string) {
|
|
taken = append(taken, m)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s := p.Modules["sudo"]; s.State != "asked" || len(taken) != 1 {
|
|
t.Fatalf("a key recorded at an earlier commit settled the walk at that commit: %+v, taken %v", s, taken)
|
|
}
|
|
}
|