Read a node's adoption from every declaration, so the host knows which modules are untaken (hq ADR 0100)
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
package declaration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defends novox/hq ADR 0100: every declaration says whether the node is adopted and which of its
|
||||||
|
// modules are taken, and the host refuses one it cannot read that from unambiguously.
|
||||||
|
|
||||||
|
const adoptedResources = `"resources":[
|
||||||
|
{"id":"hello-web.page","type":"file","path":"/var/lib/hello-web/index.html","content":"a\n"},
|
||||||
|
{"id":"hello-web.server","type":"container","name":"hello-web","image":"sha256:` + sixtyFour + `"},
|
||||||
|
{"id":"hello-web.data","type":"directory","path":"/var/lib/hello-web"}
|
||||||
|
]`
|
||||||
|
|
||||||
|
const sixtyFour = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
func TestAnAdoptionIsReadWithTheDeclaration(t *testing.T) {
|
||||||
|
d, err := Parse([]byte(`{"adoption":{"taken":["postgres"],"untaken":{"hello-web":["hello-web.page","hello-web.server"]}},
|
||||||
|
"declaration":1,` + adoptedResources + `}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if d.Adoption == nil {
|
||||||
|
t.Fatal("the adoption was dropped")
|
||||||
|
}
|
||||||
|
if len(d.Adoption.Taken) != 1 || d.Adoption.Taken[0] != "postgres" {
|
||||||
|
t.Errorf("taken read as %v", d.Adoption.Taken)
|
||||||
|
}
|
||||||
|
if module, ok := d.Adoption.UntakenModuleOf("hello-web.server"); !ok || module != "hello-web" {
|
||||||
|
t.Errorf("the container's untaken module read as %q, %v", module, ok)
|
||||||
|
}
|
||||||
|
if _, ok := d.Adoption.UntakenModuleOf("hello-web.data"); ok {
|
||||||
|
t.Error("a resource the adoption does not name was said to be untaken")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestADeclarationWithNoAdoptionIsConverged(t *testing.T) {
|
||||||
|
d, err := Parse([]byte(`{"declaration":1,` + adoptedResources + `}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if d.Adoption != nil {
|
||||||
|
t.Errorf("a declaration saying nothing about adoption read as adopted: %+v", d.Adoption)
|
||||||
|
}
|
||||||
|
if _, ok := d.Adoption.UntakenModuleOf("hello-web.page"); ok {
|
||||||
|
t.Error("a converged node has an untaken module")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnAdoptionNamingAnUnknownIDIsRefused(t *testing.T) {
|
||||||
|
refusal := refusalFor(t, `{"adoption":{"taken":[],"untaken":{"hello-web":["hello-web.missing"]}},
|
||||||
|
"declaration":1,`+adoptedResources+`}`)
|
||||||
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "hello-web.missing") {
|
||||||
|
t.Errorf("the unknown id was not named: %v", refusal.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnAdoptionMayOnlyHoldFilesAndContainers(t *testing.T) {
|
||||||
|
refusal := refusalFor(t, `{"adoption":{"taken":[],"untaken":{"hello-web":["hello-web.data"]}},
|
||||||
|
"declaration":1,`+adoptedResources+`}`)
|
||||||
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "only a file or a container") {
|
||||||
|
t.Errorf("a directory was accepted as holdable: %v", refusal.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnIDUnderTwoModulesIsRefused(t *testing.T) {
|
||||||
|
refusal := refusalFor(t, `{"adoption":{"taken":[],"untaken":{"a":["hello-web.page"],"b":["hello-web.page"]}},
|
||||||
|
"declaration":1,`+adoptedResources+`}`)
|
||||||
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "both") {
|
||||||
|
t.Errorf("an id under two modules was accepted: %v", refusal.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAModuleBothTakenAndUntakenIsRefused(t *testing.T) {
|
||||||
|
refusal := refusalFor(t, `{"adoption":{"taken":["hello-web"],"untaken":{"hello-web":["hello-web.page"]}},
|
||||||
|
"declaration":1,`+adoptedResources+`}`)
|
||||||
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "both taken and untaken") {
|
||||||
|
t.Errorf("a module both taken and untaken was accepted: %v", refusal.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTheMeshsOwnResourcesAreNeverUntaken(t *testing.T) {
|
||||||
|
refusal := refusalFor(t, `{"adoption":{"taken":[],"untaken":{"x":["adoption.guard"]}},
|
||||||
|
"declaration":1,"resources":[
|
||||||
|
{"id":"adoption.guard","type":"file","path":"/etc/mesh/guard.nft","content":"x"}]}`)
|
||||||
|
if !strings.Contains(strings.Join(refusal.Problems, "\n"), "belongs to no module") {
|
||||||
|
t.Errorf("an adoption. id was accepted as untaken: %v", refusal.Problems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnAdoptionWithAnUnknownFieldIsRefused(t *testing.T) {
|
||||||
|
refusalFor(t, `{"adoption":{"taken":[],"held":["x"]},"declaration":1,`+adoptedResources+`}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestACarriedBundleCannotSayTheNodeIsAdopted(t *testing.T) {
|
||||||
|
_, err := ParseTrusted([]byte(`{"adoption":{"taken":[]},"declaration":1,` + adoptedResources + `}`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "only the mesh can say") {
|
||||||
|
t.Fatalf("a bundle claiming adoption was not refused: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -832,6 +832,103 @@ type Declaration struct {
|
|||||||
// Resources, in the order they are applied. The host does not sort them: ordering is a
|
// Resources, in the order they are applied. The host does not sort them: ordering is a
|
||||||
// decision, and deciding is not what the host does (novox/hq ADR 0005).
|
// decision, and deciding is not what the host does (novox/hq ADR 0005).
|
||||||
Resources []Resource
|
Resources []Resource
|
||||||
|
|
||||||
|
// Adoption says this node is adopted, and which of its modules have been taken. Nil is a
|
||||||
|
// converged node — which is every node the mesh raised before adoption existed, and so the
|
||||||
|
// only form an older controller ever sends (novox/hq ADR 0100).
|
||||||
|
Adoption *Adoption
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adoption is a node's mode, as the controller records it: the node is adopted, and these are
|
||||||
|
// the modules taken on it so far (novox/hq ADR 0100).
|
||||||
|
//
|
||||||
|
// **Authoritative, and only ever stated by the controller.** A host does not work out whether it
|
||||||
|
// is adopted; it is told, in every declaration, so a host restarted from the declaration it kept
|
||||||
|
// is in the same mode it was in before.
|
||||||
|
//
|
||||||
|
// Untaken names, per module assigned here and not yet taken, the ids of its file and container
|
||||||
|
// resources — the only shapes a predecessor can already have on the machine. The host cannot
|
||||||
|
// split a resource id into its module, because module names may contain dots, so the controller
|
||||||
|
// says which ids belong to which module rather than leaving the host to guess.
|
||||||
|
type Adoption struct {
|
||||||
|
Taken []string `json:"taken"`
|
||||||
|
Untaken map[string][]string `json:"untaken,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdoptionPrefix is the id prefix of what the mesh itself declares because a node is adopted —
|
||||||
|
// its openings and its guard. Nothing under it belongs to a module, so none of it is ever held.
|
||||||
|
const AdoptionPrefix = "adoption."
|
||||||
|
|
||||||
|
// UntakenModuleOf says which untaken module declares a resource, if any.
|
||||||
|
func (a *Adoption) UntakenModuleOf(id string) (string, bool) {
|
||||||
|
if a == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for module, ids := range a.Untaken {
|
||||||
|
if slices.Contains(ids, id) {
|
||||||
|
return module, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAdoption holds what an adoption says against the resources beside it. Every problem is a
|
||||||
|
// refusal: a host that misread which module is untaken would replace a predecessor's service the
|
||||||
|
// operator never took.
|
||||||
|
func checkAdoption(a *Adoption, resources []Resource, allowActions bool) []string {
|
||||||
|
if a == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if allowActions {
|
||||||
|
// The bundle is carried with the binary and raises a foundation before any mesh exists.
|
||||||
|
// Whether a node is adopted is the controller's record, and a bundle that claimed it would
|
||||||
|
// be the host deciding its own mode (novox/hq ADR 0100).
|
||||||
|
return []string{"a carried bundle says the node is adopted, and only the mesh can say " +
|
||||||
|
"that: a node's mode is the controller's record, sent in every declaration"}
|
||||||
|
}
|
||||||
|
kinds := map[string]Type{}
|
||||||
|
for _, r := range resources {
|
||||||
|
kinds[r.Identity()] = r.Kind()
|
||||||
|
}
|
||||||
|
var problems []string
|
||||||
|
for _, module := range a.Taken {
|
||||||
|
if _, both := a.Untaken[module]; both {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"adoption: the module %q is said to be both taken and untaken", module))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owner := map[string]string{}
|
||||||
|
modules := make([]string, 0, len(a.Untaken))
|
||||||
|
for module := range a.Untaken {
|
||||||
|
modules = append(modules, module)
|
||||||
|
}
|
||||||
|
sort.Strings(modules)
|
||||||
|
for _, module := range modules {
|
||||||
|
for _, id := range a.Untaken[module] {
|
||||||
|
if strings.HasPrefix(id, AdoptionPrefix) {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"adoption: %q is the mesh's own and belongs to no module, so it cannot be untaken", id))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if first, twice := owner[id]; twice {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"adoption: %q is said to belong to both %q and %q", id, first, module))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
owner[id] = module
|
||||||
|
kind, declared := kinds[id]
|
||||||
|
switch {
|
||||||
|
case !declared:
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"adoption: %q of the untaken module %q is not in this declaration", id, module))
|
||||||
|
case kind != TypeFile && kind != TypeContainer:
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"adoption: %q of the untaken module %q is a %s, and only a file or a "+
|
||||||
|
"container can be found on a machine", id, module, kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefusalError refuses a whole declaration, naming every problem at once.
|
// RefusalError refuses a whole declaration, naming every problem at once.
|
||||||
@@ -872,6 +969,7 @@ func ParseTrusted(raw []byte) (*Declaration, error) { return parse(raw, true) }
|
|||||||
type envelope struct {
|
type envelope struct {
|
||||||
Version int `json:"declaration"`
|
Version int `json:"declaration"`
|
||||||
For string `json:"for,omitempty"`
|
For string `json:"for,omitempty"`
|
||||||
|
Adoption *Adoption `json:"adoption,omitempty"`
|
||||||
Resources []json.RawMessage `json:"resources"`
|
Resources []json.RawMessage `json:"resources"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,7 +987,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
|||||||
env.Version, Version)}}
|
env.Version, Version)}}
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Declaration{Version: env.Version, For: env.For}
|
d := &Declaration{Version: env.Version, For: env.For, Adoption: env.Adoption}
|
||||||
var problems []string
|
var problems []string
|
||||||
|
|
||||||
if len(env.Resources) == 0 {
|
if len(env.Resources) == 0 {
|
||||||
@@ -952,6 +1050,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
|||||||
problems = append(problems, resource.validate(where, allowActions)...)
|
problems = append(problems, resource.validate(where, allowActions)...)
|
||||||
d.Resources = append(d.Resources, resource)
|
d.Resources = append(d.Resources, resource)
|
||||||
}
|
}
|
||||||
|
problems = append(problems, checkAdoption(env.Adoption, d.Resources, allowActions)...)
|
||||||
|
|
||||||
if len(problems) > 0 {
|
if len(problems) > 0 {
|
||||||
return nil, &RefusalError{Problems: problems}
|
return nil, &RefusalError{Problems: problems}
|
||||||
|
|||||||
Reference in New Issue
Block a user