Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75b2676d32 | ||
|
|
1363a2fe27 |
@@ -61,7 +61,7 @@ func knownFor(m Manifest, needs []Needed, node string) (map[string]map[string]st
|
|||||||
"as": as,
|
"as": as,
|
||||||
}
|
}
|
||||||
// What the provider derives for this consumer rather than for all of them
|
// What the provider derives for this consumer rather than for all of them
|
||||||
// (novox/hq ADR 0201). Filled here, the one place a provision and the module
|
// (novox/hq ADR 0202). Filled here, the one place a provision and the module
|
||||||
// requiring it are both in hand.
|
// requiring it are both in hand.
|
||||||
served, err := ServedTo(n.Serves, as)
|
served, err := ServedTo(n.Serves, as)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// What a provider derives for one consumer, said once in the provider's definition and delivered
|
// What a provider derives for one consumer, said once in the provider's definition and delivered
|
||||||
// to both ends (novox/hq ADR 0201, issue 124).
|
// to both ends (novox/hq ADR 0202, issue 124).
|
||||||
//
|
//
|
||||||
// A `serves` block is otherwise literal: the same values for every consumer. Where the provider
|
// A `serves` block is otherwise literal: the same values for every consumer. Where the provider
|
||||||
// *names the resource* — a bucket, a database, a vhost — the name is derived from who is asking,
|
// *names the resource* — a bucket, a database, a vhost — the name is derived from who is asking,
|
||||||
@@ -165,7 +165,7 @@ func sortedAnyKeys(values map[string]any) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// derivedFor is what the provider on this machine derives for one consumer of one provision
|
// derivedFor is what the provider on this machine derives for one consumer of one provision
|
||||||
// (novox/hq ADR 0201).
|
// (novox/hq ADR 0202).
|
||||||
//
|
//
|
||||||
// Settled first, then derived: an operator may set a prefix on what the provider serves and the
|
// Settled first, then derived: an operator may set a prefix on what the provider serves and the
|
||||||
// mesh still fills the consumer's half of it ([ADR 0174]). Only the keys that actually name the
|
// mesh still fills the consumer's half of it ([ADR 0174]). Only the keys that actually name the
|
||||||
@@ -213,7 +213,7 @@ func (r Resolution) derivedFor(provision, as, consumer, local string, settings S
|
|||||||
"%s keeps several holders of %s (this one is %q), and %s derives %s for each "+
|
"%s keeps several holders of %s (this one is %q), and %s derives %s for each "+
|
||||||
"consumer from the login the mesh minted. Each holder has its own login, and a "+
|
"consumer from the login the mesh minted. Each holder has its own login, and a "+
|
||||||
"consumer is told one value per requirement — so the two ends would name "+
|
"consumer is told one value per requirement — so the two ends would name "+
|
||||||
"different things and nothing would compare them (novox/hq ADR 0201)",
|
"different things and nothing would compare them (novox/hq ADR 0202)",
|
||||||
consumer, local, provision, m.Module, orNothing(sortedAnyKeys(names)))
|
consumer, local, provision, m.Module, orNothing(sortedAnyKeys(names)))
|
||||||
}
|
}
|
||||||
settled, err := Settle(names, settings[m.Module])
|
settled, err := Settle(names, settings[m.Module])
|
||||||
@@ -230,7 +230,7 @@ func (r Resolution) derivedFor(provision, as, consumer, local string, settings S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// notTranscribed refuses a consumer's file that writes out the value its provider derives for it,
|
// notTranscribed refuses a consumer's file that writes out the value its provider derives for it,
|
||||||
// instead of asking for it (novox/hq ADR 0201, issue 124).
|
// instead of asking for it (novox/hq ADR 0202, issue 124).
|
||||||
//
|
//
|
||||||
// **What would have caught the one wrong instance.** The object store's three consumers each wrote
|
// **What would have caught the one wrong instance.** The object store's three consumers each wrote
|
||||||
// their bucket into their own configuration by hand. One of them named a predecessor's bucket, and
|
// their bucket into their own configuration by hand. One of them named a predecessor's bucket, and
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
}
|
}
|
||||||
as := ConsumerIdentity(r.Node, IdentitySource(m.Slug, m.Module))
|
as := ConsumerIdentity(r.Node, IdentitySource(m.Slug, m.Module))
|
||||||
// What the provider derives for THIS consumer, filled here where the consumer is
|
// What the provider derives for THIS consumer, filled here where the consumer is
|
||||||
// known (novox/hq ADR 0201). The same fill knownFor does below, so the binding file
|
// known (novox/hq ADR 0202). The same fill knownFor does below, so the binding file
|
||||||
// and the module's `${bound:…}` substitutions cannot say different things.
|
// and the module's `${bound:…}` substitutions cannot say different things.
|
||||||
told := *found
|
told := *found
|
||||||
told.Serves, err = ServedTo(told.Serves, as)
|
told.Serves, err = ServedTo(told.Serves, as)
|
||||||
@@ -777,7 +777,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
// And the machine underneath, which no binding of its own can tell it.
|
// And the machine underneath, which no binding of its own can tell it.
|
||||||
thisMachine := machineFacts(r, with.Names, with.MeshRange)
|
thisMachine := machineFacts(r, with.Names, with.MeshRange)
|
||||||
|
|
||||||
// **A definition that already holds the answer transcribed it** (novox/hq ADR 0201).
|
// **A definition that already holds the answer transcribed it** (novox/hq ADR 0202).
|
||||||
// Judged over what the module itself declares, and before anything is substituted: the
|
// Judged over what the module itself declares, and before anything is substituted: the
|
||||||
// mesh's own generated files — the binding, the contributions — legitimately carry the
|
// mesh's own generated files — the binding, the contributions — legitimately carry the
|
||||||
// derived value, and after substitution so does every consumer's file, so this is the one
|
// derived value, and after substitution so does every consumer's file, so this is the one
|
||||||
@@ -907,6 +907,15 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
if renamed := reflectsRenamed(m.Module, resource["reload-on"]); renamed != nil {
|
if renamed := reflectsRenamed(m.Module, resource["reload-on"]); renamed != nil {
|
||||||
copied["reload-on"] = renamed
|
copied["reload-on"] = renamed
|
||||||
}
|
}
|
||||||
|
// And which of its module's containers a scheduled step holds still (novox/hq ADR 0189).
|
||||||
|
// **The loudest of the three when it is missed.** An unprefixed `restart-on` matches
|
||||||
|
// nothing and a service quietly never restarts; an unprefixed `while-stopped` names a
|
||||||
|
// container the declaration does not contain, and the host refuses the whole
|
||||||
|
// declaration — so the machine takes nothing at all, for every push, until this is
|
||||||
|
// right. That is what it did on the control node (2026-10-04).
|
||||||
|
if renamed := reflectsRenamed(m.Module, resource[WhileStopped]); renamed != nil {
|
||||||
|
copied[WhileStopped] = renamed
|
||||||
|
}
|
||||||
// And what a process replaces (novox/hq issue 213): a resource of this module's that it
|
// And what a process replaces (novox/hq issue 213): a resource of this module's that it
|
||||||
// no longer declares, named as the host recorded it, or the host hands nothing over and
|
// no longer declares, named as the host recorded it, or the host hands nothing over and
|
||||||
// removes it first.
|
// removes it first.
|
||||||
@@ -1203,7 +1212,7 @@ type Contribution struct {
|
|||||||
// is what makes swapping one for another cost nothing.
|
// is what makes swapping one for another cost nothing.
|
||||||
Values map[string]any `json:"values"`
|
Values map[string]any `json:"values"`
|
||||||
// Derived is what this provider's own definition said it derives for this consumer, already
|
// Derived is what this provider's own definition said it derives for this consumer, already
|
||||||
// derived (novox/hq ADR 0201).
|
// derived (novox/hq ADR 0202).
|
||||||
//
|
//
|
||||||
// **The provider is told, rather than recomputing it.** A served value may name the consumer's
|
// **The provider is told, rather than recomputing it.** A served value may name the consumer's
|
||||||
// identity — a bucket named for who is asking, a database prefixed with it — and before this
|
// identity — a bucket named for who is asking, a database prefixed with it — and before this
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// What a provider derives for each consumer, said once and delivered to both ends
|
// What a provider derives for each consumer, said once and delivered to both ends
|
||||||
// (novox/hq ADR 0201, issue 124).
|
// (novox/hq ADR 0202, issue 124).
|
||||||
//
|
//
|
||||||
// The failure these are written against: the object store's provisioner derived each consumer's
|
// The failure these are written against: the object store's provisioner derived each consumer's
|
||||||
// bucket from the login the mesh minted, in its own code, and the mesh had no channel to tell the
|
// bucket from the login the mesh minted, in its own code, and the mesh had no channel to tell the
|
||||||
|
|||||||
@@ -1439,7 +1439,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
"%s serves %q to whoever requires it, and does not provide it", m.Module, to))
|
"%s serves %q to whoever requires it, and does not provide it", m.Module, to))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A served value may be derived for the consumer it is served to (novox/hq ADR 0201). Read
|
// A served value may be derived for the consumer it is served to (novox/hq ADR 0202). Read
|
||||||
// here, where the definition is, rather than when somebody first requires it: a rule that
|
// here, where the definition is, rather than when somebody first requires it: a rule that
|
||||||
// would be refused at the first consumer is wrong from the moment it is written.
|
// would be refused at the first consumer is wrong from the moment it is written.
|
||||||
problems = append(problems, CheckServes(m)...)
|
problems = append(problems, CheckServes(m)...)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -87,3 +88,58 @@ func TestAMaintenanceWindowIsRefusedWhereTheDefinitionShowsItCannotMean(t *testi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A composed declaration names the step's held containers the way the machine knows them.
|
||||||
|
//
|
||||||
|
// **The gap that let a bug through to the control node.** The manifest says `while-stopped:
|
||||||
|
// ["store"]`, because a module names its own resources locally; the declaration a machine
|
||||||
|
// receives calls that container `distribution.store`, because every resource is composed under
|
||||||
|
// its module. `restart-on` and `reload-on` are rewritten for exactly this reason, and
|
||||||
|
// `while-stopped` was not — so the host found no container by that id and refused the whole
|
||||||
|
// declaration, every push, until it was fixed.
|
||||||
|
//
|
||||||
|
// It passed every test on both sides: the controller's tests read manifests, the host's read
|
||||||
|
// hand-written declarations with bare ids. Only composing one and judging the result catches it.
|
||||||
|
func TestAComposedWindowNamesTheContainerAsTheMachineKnowsIt(t *testing.T) {
|
||||||
|
store := Manifest{
|
||||||
|
Module: "distribution", Version: "1",
|
||||||
|
Provides: FromAnywhere("artifact-store"),
|
||||||
|
Listens: []Listening{{Port: 5000, Protocol: "tcp", From: FromMesh}},
|
||||||
|
Serves: map[string]map[string]any{"artifact-store": {"port": 5000}},
|
||||||
|
Resources: []map[string]any{
|
||||||
|
{"id": "store", "type": "container", "name": "mesh-registry",
|
||||||
|
"image": "registry@sha256:" + strings.Repeat("a", 64), "ports": []any{"5000"}},
|
||||||
|
{"id": "collect", "type": "container", "name": "mesh-registry-collect",
|
||||||
|
"image": "registry@sha256:" + strings.Repeat("a", 64),
|
||||||
|
"schedule": "30 3 * * *", WhileStopped: []any{"store"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r, err := Resolve(shelf(store), []string{"distribution"}, reachable(), World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := r.Declaration(Rendering{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
collect := fileNamed(out, "distribution.collect")
|
||||||
|
if collect == nil {
|
||||||
|
for _, res := range out {
|
||||||
|
if res["id"] == "distribution.collect" {
|
||||||
|
collect = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if collect == nil {
|
||||||
|
t.Fatalf("the step was not composed at all: %v", out)
|
||||||
|
}
|
||||||
|
held, _ := collect[WhileStopped].([]any)
|
||||||
|
if len(held) != 1 {
|
||||||
|
t.Fatalf("the composed step holds %v still; want one container", collect[WhileStopped])
|
||||||
|
}
|
||||||
|
if got := fmt.Sprint(held[0]); got != "distribution.store" {
|
||||||
|
t.Fatalf("the composed step says it holds %q still, and the machine's container is "+
|
||||||
|
"called %q — the host refuses a declaration naming a container it does not have, "+
|
||||||
|
"whole, so the machine would take nothing at all", got, "distribution.store")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user