Files
mesh-controller/cmd/mesh-controller/plan_keys_test.go
T

191 lines
8.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) {
was := askABuild
askABuild = func(context.Context, buildSource, string, string) (string, error) {
t.Fatal("a walk asked build-agent for a module that is not the builder's own")
return "", nil
}
t.Cleanup(func() { askABuild = was })
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)
}
if takesOutcome(*p, &inventory.PlanModule{Key: true, State: "asked", AskedAt: s.AskedAt}, time.Now(), "build-1") {
t.Fatal("a build-agent outcome answers a module taken from a key")
}
}
// 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) {
asksOfNone := askABuild
askABuild = func(context.Context, buildSource, string, string) (string, error) {
t.Fatal("a walk asked a build")
return "", nil
}
t.Cleanup(func() { askABuild = asksOfNone })
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)
}
}