Read the forge's address again before calling a source not on the forge (issue 1277)
This commit is contained in:
@@ -75,35 +75,86 @@ func markServed() {
|
||||
type sourceForms struct {
|
||||
bases map[string]string // the seat's clone base, `scheme://host:port`, by seat; "" where it is not known
|
||||
base func(seat string) string
|
||||
read func(seat string) (string, error) // base with the reason it is unknown; nil where only base is given
|
||||
}
|
||||
|
||||
// newSourceForms reads the seats' bases from the mesh once, when first needed.
|
||||
// newSourceForms reads the seats' bases from the mesh when first needed. A base that could not be read is not
|
||||
// kept as if it were known: it is read again at the next ask (novox/hq issue 1277).
|
||||
func newSourceForms(ctx context.Context, inv *inventory.Inventory) *sourceForms {
|
||||
return sourceFormsRead(func() (catalogue.World, error) {
|
||||
shelf, err := inv.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return catalogue.World{}, fmt.Errorf("the catalogue cannot be read: %w", err)
|
||||
}
|
||||
read, err := restOfTheMeshOf(ctx, inv, shelf, "")
|
||||
if err != nil {
|
||||
return catalogue.World{}, fmt.Errorf("the mesh cannot be read: %w", err)
|
||||
}
|
||||
return read, nil
|
||||
})
|
||||
}
|
||||
|
||||
// sourceFormsRead is newSourceForms on how the mesh is read, handed in.
|
||||
func sourceFormsRead(readWorld func() (catalogue.World, error)) *sourceForms {
|
||||
f := &sourceForms{bases: map[string]string{}}
|
||||
var world *catalogue.World
|
||||
f.base = func(seat string) string {
|
||||
f.read = func(seat string) (string, error) {
|
||||
if b, known := f.bases[seat]; known {
|
||||
return b
|
||||
return b, nil
|
||||
}
|
||||
if world == nil {
|
||||
w := catalogue.World{}
|
||||
if shelf, err := inv.Catalogue(ctx); err == nil {
|
||||
if read, err := restOfTheMeshOf(ctx, inv, shelf, ""); err == nil {
|
||||
w = read
|
||||
}
|
||||
read, err := readWorld()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
world = &w
|
||||
world = &read
|
||||
}
|
||||
b, err := seatBase(*world, seat)
|
||||
if err != nil {
|
||||
b = ""
|
||||
world = nil // the holder may be changing now: read the mesh again at the next ask
|
||||
return "", err
|
||||
}
|
||||
f.bases[seat] = b
|
||||
return b, nil
|
||||
}
|
||||
f.base = func(seat string) string {
|
||||
b, _ := f.read(seat)
|
||||
return b
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// forgeAddressWait is the registration's wait for the forge's address (novox/hq issue 1277): a fresh read every
|
||||
// 5 s for at most 60 s, as the wait for the forge's answer (issue 486). A variable so a test waits in no real time.
|
||||
var forgeAddressWait = waitOut{bound: 60 * time.Second, first: 5 * time.Second, most: 5 * time.Second,
|
||||
now: time.Now, sleep: sleepOrDone}
|
||||
|
||||
// forgeAddress is the git seat's base, read again within w's bound while it cannot be read. Where the forms have
|
||||
// no reader of their own (a test's), base says it.
|
||||
func (f *sourceForms) forgeAddress(ctx context.Context, w waitOut) (string, error) {
|
||||
if f.read == nil {
|
||||
return f.base(gitSeat), nil
|
||||
}
|
||||
began := w.now()
|
||||
for {
|
||||
b, err := f.read(gitSeat)
|
||||
if err == nil {
|
||||
return b, nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return "", err
|
||||
}
|
||||
if w.now().Sub(began)+w.first > w.bound {
|
||||
return "", fmt.Errorf("after %s: %w", w.now().Sub(began).Round(time.Second), err)
|
||||
}
|
||||
fmt.Printf("the forge's address cannot be read now (%v); reading it again in %s, for at most %s in all "+
|
||||
"(novox/hq issue 1277)\n", err, w.first, w.bound)
|
||||
if serr := w.sleep(ctx, w.first); serr != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// canonical is one spelling of a repository: lower case, no `.git`, no trailing slash, and a seat's path as
|
||||
// the URL its holder serves it at where that is known.
|
||||
func (f *sourceForms) canonical(repository, seat string) string {
|
||||
@@ -478,6 +529,19 @@ func mayRegisterIn(ctx context.Context, inv registrations, forms *sourceForms, m
|
||||
return refuse(why)
|
||||
}
|
||||
|
||||
if built.Seat == "" {
|
||||
// A URL is judged against the forge's address: an address that cannot be read is no proof the source is
|
||||
// elsewhere (novox/hq issue 1277).
|
||||
if _, err := forms.forgeAddress(ctx, forgeAddressWait); err != nil {
|
||||
if terminal {
|
||||
fmt.Printf("%s: the forge's address could not be read (%v) — registered, as asked at the controller's "+
|
||||
"terminal\n", buildID, err)
|
||||
return 0, nil
|
||||
}
|
||||
return refuse(fmt.Sprintf("the forge's address could not be read (%v), so whether %s's trunk is "+
|
||||
"protected cannot be read", err, sourceWords(built.Repository, built.Seat)))
|
||||
}
|
||||
}
|
||||
owner, name, onForge := forms.onTheForge(built.Repository, built.Seat)
|
||||
if !onForge {
|
||||
if terminal {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
)
|
||||
|
||||
// A world reader that fails the first `failing` reads, as the mesh does while the forge's seat holder changes in
|
||||
// the plan's tier (novox/hq issue 1277), then answers with the forge held.
|
||||
func aWorldFailing(failing int) (func() (catalogue.World, error), *int) {
|
||||
reads := 0
|
||||
return func() (catalogue.World, error) {
|
||||
reads++
|
||||
if reads <= failing {
|
||||
return catalogue.World{}, errors.New("the mesh cannot be read: nothing serves")
|
||||
}
|
||||
return forgeHolding(float64(20000)), nil
|
||||
}, &reads
|
||||
}
|
||||
|
||||
const aRepoOnTheForge = "http://anchor.internal:20000/novox/mesh-delivery.git"
|
||||
|
||||
// 1277/AC1: a base that could not be read is not remembered as known.
|
||||
func TestAnUnreadForgeAddressIsNotKeptAsKnown(t *testing.T) {
|
||||
read, _ := aWorldFailing(1)
|
||||
forms := sourceFormsRead(read)
|
||||
if _, _, ok := forms.onTheForge(aRepoOnTheForge, ""); ok {
|
||||
t.Fatal("1277/AC1: the source was on the forge with the address unread")
|
||||
}
|
||||
owner, name, ok := forms.onTheForge(aRepoOnTheForge, "")
|
||||
if !ok || owner != "novox" || name != "mesh-delivery" {
|
||||
t.Fatalf("1277/AC1: the second ask answered %q %q %v, not the owner and name: the empty base was kept", owner, name, ok)
|
||||
}
|
||||
}
|
||||
|
||||
type aMeshRegisteredFromTheURL struct {
|
||||
anEmptyMesh
|
||||
from string
|
||||
}
|
||||
|
||||
func (m *aMeshRegisteredFromTheURL) SourceOf(context.Context, string) (inventory.Source, error) {
|
||||
return inventory.Source{Repository: m.from}, nil
|
||||
}
|
||||
|
||||
// The wait on a clock of its own, in place of the registration's.
|
||||
func withFakeAddressWait(t *testing.T) *[]time.Duration {
|
||||
w, paused := aFakeWait(60 * time.Second)
|
||||
w.first, w.most = 5*time.Second, 5*time.Second
|
||||
was := forgeAddressWait
|
||||
forgeAddressWait = w
|
||||
t.Cleanup(func() { forgeAddressWait = was })
|
||||
return paused
|
||||
}
|
||||
|
||||
func registerFrom(t *testing.T, forms *sourceForms) (int64, error) {
|
||||
mesh := &aMeshRegisteredFromTheURL{anEmptyMesh{asked: true,
|
||||
request: inventory.BuildRequest{ID: "build-1", Repository: aRepoOnTheForge, For: "build"}}, aRepoOnTheForge}
|
||||
return mayRegisterIn(t.Context(), mesh, forms, "mesh-delivery-view", inventory.Source{Repository: aRepoOnTheForge},
|
||||
"build-1", "", "main")
|
||||
}
|
||||
|
||||
// 1277/AC2: an address unread twice, then read, is waited out and the forge is asked.
|
||||
func TestAForgeAddressUnreadForAMomentIsWaitedOut(t *testing.T) {
|
||||
paused := withFakeAddressWait(t)
|
||||
read, reads := aWorldFailing(2)
|
||||
id, err := registerFrom(t, sourceFormsRead(read))
|
||||
if err != nil || id == 0 {
|
||||
t.Fatalf("1277/AC2: the registration was refused (id %d): %v", id, err)
|
||||
}
|
||||
if *reads != 3 || len(*paused) != 2 {
|
||||
t.Fatalf("1277/AC2: %d reads and %d pauses, want 3 and 2", *reads, len(*paused))
|
||||
}
|
||||
}
|
||||
|
||||
// 1277/AC3: an address unread for the whole bound is said so, with the read's error.
|
||||
func TestAForgeAddressUnreadForTheWholeBoundIsSaidSo(t *testing.T) {
|
||||
paused := withFakeAddressWait(t)
|
||||
read, _ := aWorldFailing(1 << 30)
|
||||
_, err := registerFrom(t, sourceFormsRead(read))
|
||||
if !errors.Is(err, errNotItsSource) {
|
||||
t.Fatalf("1277/AC3: not refused: %v", err)
|
||||
}
|
||||
for _, want := range []string{"the forge's address could not be read", "nothing serves"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("1277/AC3: the refusal lacks %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
if strings.Contains(err.Error(), "not on the mesh's forge") {
|
||||
t.Errorf("1277/AC3: the refusal says the source is not on the forge: %v", err)
|
||||
}
|
||||
var total time.Duration
|
||||
for _, d := range *paused {
|
||||
total += d
|
||||
}
|
||||
if total > 60*time.Second || total < 50*time.Second {
|
||||
t.Errorf("1277/AC3: waited %s, want about the 60 s bound", total)
|
||||
}
|
||||
}
|
||||
|
||||
// 1277/AC4: a known address and a URL on another host is still not on the forge.
|
||||
func TestAKnownAddressAndAnotherHostIsStillNotOnTheForge(t *testing.T) {
|
||||
withFakeAddressWait(t)
|
||||
read, _ := aWorldFailing(0)
|
||||
const elsewhere = "http://elsewhere.example/x/y.git"
|
||||
mesh := &aMeshRegisteredFromTheURL{anEmptyMesh{asked: true, request: inventory.BuildRequest{ID: "build-1", For: "build"}}, elsewhere}
|
||||
_, err := mayRegisterIn(t.Context(), mesh, sourceFormsRead(read), "mesh-delivery-view",
|
||||
inventory.Source{Repository: elsewhere}, "build-1", "", "main")
|
||||
if !errors.Is(err, errNotItsSource) || !strings.Contains(err.Error(), "is not on the mesh's forge") {
|
||||
t.Fatalf("1277/AC4: the refusal changed: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user