Where the answer to a requirement is allowed to live
Two different things were both written `requires`. A shell, a display
server and a private network have to be on the machine that needs them.
A database does not — it runs somewhere and is reached over the network.
Both were answered the same way, so requiring a database installed
PostgreSQL on every machine that ran a web application.
What a module provides now carries a scope, the same idea claims already
use, written short in the ordinary case:
"provides": ["shell"]
"provides": [{"name": "database", "scope": "mesh"}]
A mesh-scoped requirement is answered by finding the node already running
it — never by installing it here. Choosing a machine to put a database on
is a decision with consequences, and nothing resolving a web application
should make it silently. With nothing anywhere it refuses and says which
module to assign; with two it refuses and says how to choose.
Choosing is `pin <node> <provision> <from>`, kept per node because that
is the granularity the choice has. A pin at a machine that does not
provide it refuses rather than falling back — a fallback would quietly
move somebody's data. One provider does not overrule a pin either.
Resolving a node now needs to know what the others offer, and working
that out needs them resolved, so it is two passes: the first answers only
what each node offers, the second answers everything. Nothing is ever
declared from the first.
A node's plan says what it takes from elsewhere. It is the only part of a
set that stops working when a different machine goes away, and nothing
else in that output would have said so. It is also where a credential
will hang once there is a mechanism for handing one back.
One test found passing for the wrong reason: it read pins through a join
on the provider, which hides a dangling row whether or not it was cleaned
up. It counts rows now, and bites when the cascade is removed.
This commit is contained in:
@@ -32,13 +32,13 @@ func (f *fake) Resources(node string) ([]map[string]any, bool, error) {
|
||||
|
||||
func computedShelf() map[string]Manifest {
|
||||
return shelf(Manifest{Module: "mesh-network", Computed: "mesh-network",
|
||||
Provides: []string{"private-network"}})
|
||||
Provides: Offers("private-network")})
|
||||
}
|
||||
|
||||
func TestAComputedModuleIsAskedAboutTheNodeItIsFor(t *testing.T) {
|
||||
// The generator gets a node name, not a plan. Everything it needs is the whole mesh, which
|
||||
// it was built with — this is the one thing a node could never work out for itself.
|
||||
got, err := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), nil)
|
||||
got, err := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func TestAComputedModuleIsAskedAboutTheNodeItIsFor(t *testing.T) {
|
||||
func TestAMachineNobodyGaveItGetsNothing(t *testing.T) {
|
||||
// The whole point of making the network a module. Before this, every machine with an address
|
||||
// was on the private network and there was no way to say one should stay off.
|
||||
got, err := Resolve(computedShelf(), nil, workstation(), nil)
|
||||
got, err := Resolve(computedShelf(), nil, workstation(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func TestAMachineNobodyGaveItGetsNothing(t *testing.T) {
|
||||
func TestAssignedAndNotYetPartOfItIsNotAFailure(t *testing.T) {
|
||||
// A machine given the network module before it has a place on it. Brief and ordinary — the
|
||||
// answer is "nothing yet", and treating it as an error would make an ordering a fault.
|
||||
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), nil)
|
||||
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
|
||||
gen := &fake{on: map[string]bool{}}
|
||||
out, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": gen}})
|
||||
if err != nil {
|
||||
@@ -92,8 +92,8 @@ func TestAssignedAndNotYetPartOfItIsNotAFailure(t *testing.T) {
|
||||
func TestAGeneratorThisControlPlaneDoesNotHaveIsRefused(t *testing.T) {
|
||||
// Sending a machine a module with no files would look like it worked. Named in the message,
|
||||
// because the only fix is a control plane that has it.
|
||||
got, _ := Resolve(shelf(Manifest{Module: "weather", Computed: "the-weather"}),
|
||||
[]string{"weather"}, workstation(), nil)
|
||||
got, _ := Resolve(shelf(Manifest{Module: "weather", Computed: "the-weather"}), []string{"weather"}, workstation(), World{})
|
||||
|
||||
_, err := got.Declaration(Rendering{Generators: map[string]Generator{"mesh-network": &fake{}}})
|
||||
if err == nil {
|
||||
t.Fatal("a module computed by nothing was accepted")
|
||||
@@ -119,7 +119,7 @@ func TestAModuleIsEitherWrittenOrComputedNotBoth(t *testing.T) {
|
||||
func TestSettingsApplyToAComputedModuleToo(t *testing.T) {
|
||||
// Being computed is about where the files come from, not about whether they are configurable.
|
||||
// A line drawn there would be arbitrary and nobody could predict it.
|
||||
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), nil)
|
||||
got, _ := Resolve(computedShelf(), []string{"mesh-network"}, workstation(), World{})
|
||||
gen := &fake{on: map[string]bool{"workstation": true}}
|
||||
out, err := got.Declaration(Rendering{
|
||||
Generators: map[string]Generator{"mesh-network": gen},
|
||||
@@ -143,10 +143,11 @@ func TestTwoWaysToBeOnAPrivateNetworkRefuseAndNameBoth(t *testing.T) {
|
||||
// both is caught rather than producing a machine on two networks that each half-work.
|
||||
_, err := Resolve(shelf(
|
||||
Manifest{Module: "mesh-network", Computed: "mesh-network",
|
||||
Provides: []string{"private-network"}},
|
||||
Manifest{Module: "tailscale", Provides: []string{"private-network"}},
|
||||
Provides: Offers("private-network")},
|
||||
Manifest{Module: "tailscale", Provides: Offers("private-network")},
|
||||
Manifest{Module: "backups", Requires: []string{"private-network"}},
|
||||
), []string{"backups"}, workstation(), nil)
|
||||
), []string{"backups"}, workstation(), World{})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("two answers to one requirement were taken silently")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user