The first build of a module a merge adds on the operator's Approve passes the guard of a build through a verb on the warrant it checked (issue 1146, AC9)
This commit is contained in:
@@ -473,7 +473,7 @@ func buildOneAsked(ctx context.Context, source buildSource, path, ref string, wa
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Through a verb, only a repository the catalogue builds from (novox/hq ADR 0266).
|
// Through a verb, only a repository the catalogue builds from (novox/hq ADR 0266).
|
||||||
if err := verbMayAsk(ctx, source, repository); err != nil {
|
if err := verbMayAsk(ctx, source, repository, path); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,7 +812,7 @@ func buildAndShow(ctx context.Context, source buildSource, path, ref string, wai
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := verbMayAsk(ctx, source, repository); err != nil {
|
if err := verbMayAsk(ctx, source, repository, path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ident, err := openIdentity(ctx)
|
ident, err := openIdentity(ctx)
|
||||||
|
|||||||
@@ -567,13 +567,49 @@ func sourceWords(repository, seat string) string {
|
|||||||
return buildSource{Repository: repository, Seat: seat}.String()
|
return buildSource{Repository: repository, Seat: seat}.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// firstBuildWarrant is what the controller checked before it asks the first build of the modules a merge adds to a
|
||||||
|
// repository nothing is built from: the merge was made on the operator's Approve of its head (approvedAnnounced). It
|
||||||
|
// travels in the context of that ask alone, in this process, so no verb carries one.
|
||||||
|
type firstBuildWarrant struct {
|
||||||
|
repository string // owner/name on the git seat
|
||||||
|
paths map[string]bool
|
||||||
|
why string
|
||||||
|
}
|
||||||
|
|
||||||
|
type firstBuildWarrantKey struct{}
|
||||||
|
|
||||||
|
// withFirstBuildWarrant is ctx carrying the warrant for asking the first builds of paths in repository.
|
||||||
|
func withFirstBuildWarrant(ctx context.Context, repository string, paths []string, why string) context.Context {
|
||||||
|
w := &firstBuildWarrant{repository: repository, paths: map[string]bool{}, why: why}
|
||||||
|
for _, p := range paths {
|
||||||
|
w.paths[strings.Trim(p, "/")] = true
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, firstBuildWarrantKey{}, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstBuildWarrantIn(ctx context.Context) *firstBuildWarrant {
|
||||||
|
w, _ := ctx.Value(firstBuildWarrantKey{}).(*firstBuildWarrant)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// covers says the warrant is for exactly this ask: the repository on the git seat, and a module path the merge holds.
|
||||||
|
func (w *firstBuildWarrant) covers(source buildSource, path string) bool {
|
||||||
|
return w != nil && source.Seat == gitSeat && strings.EqualFold(strings.Trim(source.Repository, "/"), w.repository) &&
|
||||||
|
w.paths[strings.Trim(path, "/")]
|
||||||
|
}
|
||||||
|
|
||||||
// verbMayAsk refuses, for a call through a verb, a build of a repository the catalogue builds no module from
|
// verbMayAsk refuses, for a call through a verb, a build of a repository the catalogue builds no module from
|
||||||
// (novox/hq ADR 0266): the build node would run what an agent wrote, and its outcome could never be
|
// (novox/hq ADR 0266): the build node would run what an agent wrote, and its outcome could never be
|
||||||
// registered anyway. url is the repository as it is cloned. At the terminal anything may be asked.
|
// registered anyway. url is the repository as it is cloned. At the terminal anything may be asked.
|
||||||
func verbMayAsk(ctx context.Context, source buildSource, url string) error {
|
func verbMayAsk(ctx context.Context, source buildSource, url, path string) error {
|
||||||
if startedAtTheTerminal() {
|
if startedAtTheTerminal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// **Or the first build of a module a merge adds on the operator's Approve** (novox/hq ADR 0432, issue 1146): the
|
||||||
|
// controller's own ask, carrying the warrant it checked, for that repository and the module paths the merge holds.
|
||||||
|
if firstBuildWarrantIn(ctx).covers(source, path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
open, err := openStores(ctx)
|
open, err := openStores(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -134,18 +134,18 @@ func TestAVerbAsksNoBuildOfARepositoryTheCatalogueDoesNotBuildFrom(t *testing.T)
|
|||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
t.Setenv(verbVar, "build")
|
t.Setenv(verbVar, "build")
|
||||||
t.Setenv(link.CallerVar, "node-tools.anchor, through the mesh-controller seat")
|
t.Setenv(link.CallerVar, "node-tools.anchor, through the mesh-controller seat")
|
||||||
err := verbMayAsk(ctx, buildSource{Repository: "agent/sudo", Seat: "git"}, "http://forge.internal:20000/agent/sudo.git")
|
err := verbMayAsk(ctx, buildSource{Repository: "agent/sudo", Seat: "git"}, "http://forge.internal:20000/agent/sudo.git", "")
|
||||||
var policy *heldAtTheTerminal
|
var policy *heldAtTheTerminal
|
||||||
if !errors.As(err, &policy) {
|
if !errors.As(err, &policy) {
|
||||||
t.Fatalf("a verb's build of an agent's repository was asked: %v", err)
|
t.Fatalf("a verb's build of an agent's repository was asked: %v", err)
|
||||||
}
|
}
|
||||||
if err := verbMayAsk(ctx, buildSource{Repository: "novox/mesh-catalog", Seat: "git"},
|
if err := verbMayAsk(ctx, buildSource{Repository: "novox/mesh-catalog", Seat: "git"},
|
||||||
"http://forge.internal:20000/novox/mesh-catalog.git"); err != nil {
|
"http://forge.internal:20000/novox/mesh-catalog.git", ""); err != nil {
|
||||||
t.Fatalf("a verb's build of the catalogue repository was refused: %v", err)
|
t.Fatalf("a verb's build of the catalogue repository was refused: %v", err)
|
||||||
}
|
}
|
||||||
t.Setenv(verbVar, "")
|
t.Setenv(verbVar, "")
|
||||||
t.Setenv(link.CallerVar, "")
|
t.Setenv(link.CallerVar, "")
|
||||||
if err := verbMayAsk(ctx, buildSource{Repository: "agent/sudo", Seat: "git"}, ""); err != nil {
|
if err := verbMayAsk(ctx, buildSource{Repository: "agent/sudo", Seat: "git"}, "", ""); err != nil {
|
||||||
t.Fatalf("the terminal was refused: %v", err)
|
t.Fatalf("the terminal was refused: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||||
|
"github.com/novox/mesh-controller/internal/broker"
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
"github.com/novox/mesh-controller/internal/link"
|
||||||
|
)
|
||||||
|
|
||||||
|
// aForgeAndABuildSeat gives a test what a real ask of a build needs: the forge holding the git seat, assigned to anchor, so
|
||||||
|
// the clone URL composes; the build seat's queue on a real bus; and the environment of the serving controller, which is
|
||||||
|
// never the controller's terminal (a verb's, in effect: startedAtTheTerminal is false).
|
||||||
|
func aForgeAndABuildSeat(t *testing.T, open *stores) *broker.JetStream {
|
||||||
|
t.Helper()
|
||||||
|
js, _ := aBuildQueue(t)
|
||||||
|
forge, err := catalogue.ParseManifest([]byte(`{"module":"gitea","version":"1",
|
||||||
|
"claims":[{"name":"git","scope":"mesh"}],"provides":[{"name":"git","scope":"mesh"}],
|
||||||
|
"serves":{"git":{"scheme":"http","port":3000}}}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
register(t, open, forge)
|
||||||
|
if _, err := assign(t.Context(), open, "anchor", "gitea"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv(servedVar, "1")
|
||||||
|
return js
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1146/AC9, through the real ask (askABuild, buildOneAsked, verbMayAsk) and a real bus: the first build of a module a
|
||||||
|
// merge adds on the operator's Approve is queued on the build seat; the same ask without that warrant, and the warrant's
|
||||||
|
// ask for a path the merge does not hold, are refused by the guard of a build through a verb.
|
||||||
|
func TestTheApprovedFirstBuildPassesTheGuardOfAVerbAndIsQueued(t *testing.T) {
|
||||||
|
const head, commit = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "0177ea7a00000000"
|
||||||
|
open := aMesh(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
js := aForgeAndABuildSeat(t, open)
|
||||||
|
|
||||||
|
a := aDeliveryMergeOf(t, head, map[string]string{"novox/mesh-delivery@a1a1a1a1a1a1": head})
|
||||||
|
wasKept, wasAsk := keptDeliveryMerge, routerAskOf
|
||||||
|
keptDeliveryMerge = func(context.Context, string, string, int, string) (deliveryMerge, bool, error) {
|
||||||
|
return a.merge, true, nil
|
||||||
|
}
|
||||||
|
routerAskOf = func(context.Context, catalogueReader, string) (*asks.Ask, *asks.Warrant, error) {
|
||||||
|
return &a.ask, &a.warrant, nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { keptDeliveryMerge, routerAskOf = wasKept, wasAsk })
|
||||||
|
|
||||||
|
// Without the warrant the guard refuses, whatever the repository holds.
|
||||||
|
_, err := askABuild(ctx, buildSource{Repository: "novox/mesh-delivery", Seat: "git"}, "", "main")
|
||||||
|
var policy *heldAtTheTerminal
|
||||||
|
if !errors.As(err, &policy) {
|
||||||
|
t.Fatalf("1146/AC9: a first build asked without the warrant was not refused: %v", err)
|
||||||
|
}
|
||||||
|
// And a path the merge does not hold is refused on the warrant.
|
||||||
|
warranted := withFirstBuildWarrant(ctx, "novox/mesh-delivery", []string{""}, "the Approve")
|
||||||
|
if _, err := askABuild(warranted, buildSource{Repository: "novox/mesh-delivery", Seat: "git"}, "other", "main"); !errors.As(err, &policy) {
|
||||||
|
t.Fatalf("1146/AC9: the warrant covered a path the merge does not hold: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-delivery", Base: "main", HeadSHA: head, Commit: commit, Number: 7,
|
||||||
|
MergedAt: time.Now().Add(-time.Hour).UTC().Format(time.RFC3339Nano), Paths: []string{"module.json", "main.go"},
|
||||||
|
ModuleDirs: []string{"."}, ModuleDirsSaid: true}
|
||||||
|
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
queue, err := link.ReadQueue(ctx, js, link.TheBuildMachine)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(queue.Asks) != 1 {
|
||||||
|
t.Fatalf("1146/AC9: the build queue holds %+v, want the one first build", queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// 1146/AC9: the warrant the controller checked covers exactly the repository on the git seat and the module paths the
|
||||||
|
// merge holds; nothing else passes verbMayAsk on it.
|
||||||
|
func TestTheFirstBuildWarrantCoversOnlyItsRepositoryAndItsModulePaths(t *testing.T) {
|
||||||
|
ctx := withFirstBuildWarrant(t.Context(), "novox/mesh-delivery", []string{"", "view"}, "the Approve")
|
||||||
|
w := firstBuildWarrantIn(ctx)
|
||||||
|
for _, c := range []struct {
|
||||||
|
source buildSource
|
||||||
|
path string
|
||||||
|
covers bool
|
||||||
|
}{
|
||||||
|
{buildSource{Repository: "novox/mesh-delivery", Seat: "git"}, "", true},
|
||||||
|
{buildSource{Repository: "novox/mesh-delivery", Seat: "git"}, "/view/", true},
|
||||||
|
{buildSource{Repository: "novox/mesh-delivery", Seat: "git"}, "other", false},
|
||||||
|
{buildSource{Repository: "agent/mesh-delivery", Seat: "git"}, "", false},
|
||||||
|
{buildSource{Repository: "novox/mesh-delivery"}, "", false},
|
||||||
|
{buildSource{Repository: "novox/mesh-delivery", Seat: "elsewhere"}, "", false},
|
||||||
|
} {
|
||||||
|
if got := w.covers(c.source, c.path); got != c.covers {
|
||||||
|
t.Errorf("1146/AC9: %v at %q covered = %v, want %v", c.source, c.path, got, c.covers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if firstBuildWarrantIn(t.Context()).covers(buildSource{Repository: "novox/mesh-delivery", Seat: "git"}, "") {
|
||||||
|
t.Error("1146/AC9: a context with no warrant covers an ask")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,7 +76,16 @@ func askFirstBuilds(ctx context.Context, inv *inventory.Inventory, m link.Source
|
|||||||
}
|
}
|
||||||
fmt.Printf("%s merged into %s (%.8s) on the operator's Approve (%s): the mesh builds nothing from it yet, so the "+
|
fmt.Printf("%s merged into %s (%.8s) on the operator's Approve (%s): the mesh builds nothing from it yet, so the "+
|
||||||
"first build of %s is asked\n", repository, m.Base, m.Commit, why, strings.Join(added, ", "))
|
"first build of %s is asked\n", repository, m.Base, m.Commit, why, strings.Join(added, ", "))
|
||||||
askBuilds(ctx, inv, m, buildSource{Repository: repository, Seat: gitSeat}, added)
|
// The ask carries the warrant just checked, for this repository and these module paths alone: the guard of a build
|
||||||
|
// through a verb (verbMayAsk) lets exactly that through.
|
||||||
|
paths := make([]string, 0, len(added))
|
||||||
|
for _, dir := range added {
|
||||||
|
if dir == "." {
|
||||||
|
dir = ""
|
||||||
|
}
|
||||||
|
paths = append(paths, dir)
|
||||||
|
}
|
||||||
|
askBuilds(withFirstBuildWarrant(ctx, repository, paths, why), inv, m, buildSource{Repository: repository, Seat: gitSeat}, added)
|
||||||
// Answered: it no longer waits, so the word's arrival again asks nothing twice.
|
// Answered: it no longer waits, so the word's arrival again asks nothing twice.
|
||||||
return inv.AnswerMerge(ctx, repository, m.Commit, inventory.WaitingRecord, false)
|
return inv.AnswerMerge(ctx, repository, m.Commit, inventory.WaitingRecord, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,20 @@ func TestAMergeOnTheApproveIntoANewRepositoryAsksTheFirstBuildAndItsOutcomeRegis
|
|||||||
open := aMesh(t)
|
open := aMesh(t)
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
inv := open.inventory
|
inv := open.inventory
|
||||||
asked := asksWithPaths(t)
|
// The real ask, guard and queue included: only its answers are kept, so the build's outcome can be matched.
|
||||||
|
aForgeAndABuildSeat(t, open)
|
||||||
|
asked := &[][3]string{}
|
||||||
|
var ids []string
|
||||||
|
realAsk := askABuild
|
||||||
|
askABuild = func(c context.Context, source buildSource, path, ref string) (string, error) {
|
||||||
|
id, err := realAsk(c, source, path, ref)
|
||||||
|
if err == nil {
|
||||||
|
*asked = append(*asked, [3]string{source.Repository, path, ref})
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { askABuild = realAsk })
|
||||||
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "operator-channel", Version: "1",
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "operator-channel", Version: "1",
|
||||||
DefinesSeats: []catalogue.SeatDeclaration{{Name: broker.AsksSeat, Records: []string{"asks"}}}},
|
DefinesSeats: []catalogue.SeatDeclaration{{Name: broker.AsksSeat, Records: []string{"asks"}}}},
|
||||||
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/operator-channel", BuiltFrom: "c0ffee"}); err != nil {
|
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/operator-channel", BuiltFrom: "c0ffee"}); err != nil {
|
||||||
@@ -129,7 +142,7 @@ func TestAMergeOnTheApproveIntoANewRepositoryAsksTheFirstBuildAndItsOutcomeRegis
|
|||||||
if len(*asked) != 1 || (*asked)[0] != want {
|
if len(*asked) != 1 || (*asked)[0] != want {
|
||||||
t.Fatalf("1146/AC7: the merge asked %v, want %v once", *asked, want)
|
t.Fatalf("1146/AC7: the merge asked %v, want %v once", *asked, want)
|
||||||
}
|
}
|
||||||
build := onTrunk("b-", "novox/mesh-delivery", "git", "", map[string]any{"module": "mesh-delivery-view", "version": "1"})
|
build := onTrunk(ids[0], "novox/mesh-delivery", "git", "", map[string]any{"module": "mesh-delivery-view", "version": "1"})
|
||||||
build.Commit = commit
|
build.Commit = commit
|
||||||
if _, _, err := takeIn(ctx, inv, build); err != nil {
|
if _, _, err := takeIn(ctx, inv, build); err != nil {
|
||||||
t.Fatalf("1146/AC7: the outcome of the first build was refused: %v", err)
|
t.Fatalf("1146/AC7: the outcome of the first build was refused: %v", err)
|
||||||
|
|||||||
Vendored
+1
@@ -165,3 +165,4 @@
|
|||||||
./cmd/mesh-controller TestAComposedDeclarationCarriesModulesAndBuildsOnlyForANodeThatReadsThem
|
./cmd/mesh-controller TestAComposedDeclarationCarriesModulesAndBuildsOnlyForANodeThatReadsThem
|
||||||
./cmd/mesh-controller TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToTheOthers
|
./cmd/mesh-controller TestAPlanPassesATierWhoseMachineNeverReportsAfterTheBoundAndSendsTheNextToTheOthers
|
||||||
./cmd/mesh-controller TestAMergeOnTheApproveIntoANewRepositoryAsksTheFirstBuildAndItsOutcomeRegistersTheModule
|
./cmd/mesh-controller TestAMergeOnTheApproveIntoANewRepositoryAsksTheFirstBuildAndItsOutcomeRegistersTheModule
|
||||||
|
./cmd/mesh-controller TestTheApprovedFirstBuildPassesTheGuardOfAVerbAndIsQueued
|
||||||
|
|||||||
Reference in New Issue
Block a user