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:
@@ -45,6 +45,63 @@ func (c Claim) At() string {
|
||||
return c.Scope
|
||||
}
|
||||
|
||||
// Offer is something a module provides, and where the answer to it may live.
|
||||
//
|
||||
// **The distinction this exists for:** a shell, a display server and a private network have to be
|
||||
// on the machine that needs them. A database, an object store and an identity provider do not —
|
||||
// they run somewhere in the mesh and are reached over it. Treating the second as the first
|
||||
// installs PostgreSQL on every machine that runs a web application, which is what happened until
|
||||
// this field existed.
|
||||
//
|
||||
// Written as a bare string in the ordinary case, because nearly everything is node-scoped and
|
||||
// making every manifest say so would bury the few that are not:
|
||||
//
|
||||
// "provides": ["shell"]
|
||||
// "provides": [{"name": "database", "scope": "mesh"}]
|
||||
type Offer struct {
|
||||
Name string `json:"name"`
|
||||
// Scope defaults to the node, which is where most things must be to be usable.
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// At is this offer's scope, with the default applied.
|
||||
func (o Offer) At() string {
|
||||
if o.Scope == "" {
|
||||
return ScopeNode
|
||||
}
|
||||
return o.Scope
|
||||
}
|
||||
|
||||
// UnmarshalJSON accepts a plain name as well as an object.
|
||||
func (o *Offer) UnmarshalJSON(raw []byte) error {
|
||||
var plain string
|
||||
if err := json.Unmarshal(raw, &plain); err == nil {
|
||||
o.Name, o.Scope = plain, ""
|
||||
return nil
|
||||
}
|
||||
var full struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &full); err != nil {
|
||||
return fmt.Errorf("a provided name is either a string or {name, scope}: %w", err)
|
||||
}
|
||||
o.Name, o.Scope = full.Name, full.Scope
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON writes back the short form when there is nothing else to say, so a manifest that
|
||||
// went through the mesh comes out looking like the one that went in.
|
||||
func (o Offer) MarshalJSON() ([]byte, error) {
|
||||
if o.Scope == "" {
|
||||
return json.Marshal(o.Name)
|
||||
}
|
||||
return json.Marshal(struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
}{o.Name, o.Scope})
|
||||
}
|
||||
|
||||
// Manifest is everything a module says about itself.
|
||||
type Manifest struct {
|
||||
Module string `json:"module"`
|
||||
@@ -52,7 +109,7 @@ type Manifest struct {
|
||||
|
||||
// Provides are the names other modules may require. A module always provides its own name;
|
||||
// this is for the rest — `zsh` provides `shell`, `xorg` provides `display-server`.
|
||||
Provides []string `json:"provides,omitempty"`
|
||||
Provides []Offer `json:"provides,omitempty"`
|
||||
|
||||
// Requires are names that must be provided by something assigned to the same node.
|
||||
Requires []string `json:"requires,omitempty"`
|
||||
@@ -148,10 +205,19 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%q is not a usable module name: lower-case letters, digits, dashes and dots", m.Module))
|
||||
}
|
||||
for _, p := range m.Provides {
|
||||
for _, offer := range m.Provides {
|
||||
p := offer.Name
|
||||
if !name.MatchString(p) {
|
||||
problems = append(problems, fmt.Sprintf("%q is not a usable name to provide", p))
|
||||
}
|
||||
if s := offer.At(); s != ScopeNode && s != ScopeMesh {
|
||||
// Site scope is meaningful for a claim — one DHCP server per segment — and is not
|
||||
// yet meaningful for a provision, because nothing knows how to reach "the one at my
|
||||
// site". Refused rather than silently treated as mesh-wide.
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s provides %q at scope %q; a provision is %q or %q",
|
||||
m.Module, p, s, ScopeNode, ScopeMesh))
|
||||
}
|
||||
if p == m.Module {
|
||||
// Harmless and worth saying: a module always provides its own name, so writing it
|
||||
// suggests the author expected it not to.
|
||||
@@ -238,7 +304,26 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
|
||||
// Offers is everything this module can satisfy: its own name, and what it provides.
|
||||
func (m Manifest) Offers() []string {
|
||||
out := append([]string{m.Module}, m.Provides...)
|
||||
out := []string{m.Module}
|
||||
for _, p := range m.Provides {
|
||||
out = append(out, p.Name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// OffersAt is what this module provides at one scope, with its own name counted as node-scoped:
|
||||
// a module is only ever itself on the machine it is installed on.
|
||||
func (m Manifest) OffersAt(scope string) []string {
|
||||
var out []string
|
||||
if scope == ScopeNode {
|
||||
out = append(out, m.Module)
|
||||
}
|
||||
for _, p := range m.Provides {
|
||||
if p.At() == scope {
|
||||
out = append(out, p.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user