115 lines
5.4 KiB
Go
115 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// The controller hands a moved module to the provisioner, and counts it applied only once its files are placed
|
|
// (design 57 P6, issue 672; novox/hq ADR 0349 §4, §15, ADR 0362).
|
|
|
|
// A declaration that hands files says which, and whose client fills them; one that hands none is byte for byte
|
|
// what it was.
|
|
func TestADeclarationNamesTheFilledFilesOnlyWhenItHandsAny(t *testing.T) {
|
|
plain := sendable{Resources: []map[string]any{{"id": "notify.configuration", "type": "file", "path": "/x",
|
|
"content": "a"}}, Sequence: 4}
|
|
before, err := plain.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(before), "filled_files") || strings.Contains(string(before), "provisioner_account") {
|
|
t.Fatalf("a declaration handing nothing names filled files: %s", before)
|
|
}
|
|
handing := plain
|
|
handing.Filled, handing.Provisioner = []string{"/x"}, "mesh-provisioner-client"
|
|
body, err := handing.Body()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var envelope struct {
|
|
Filled []string `json:"filled_files"`
|
|
Provisioner string `json:"provisioner_account"`
|
|
}
|
|
if err := json.Unmarshal(body, &envelope); err != nil || len(envelope.Filled) != 1 || envelope.Filled[0] != "/x" ||
|
|
envelope.Provisioner != "mesh-provisioner-client" {
|
|
t.Fatalf("the handing declaration says %s", body)
|
|
}
|
|
}
|
|
|
|
func TestTheProvisionersAccountIsTheClientsProcessUser(t *testing.T) {
|
|
client := catalogue.Manifest{Module: "mesh-provisioner-client",
|
|
OwnSecrets: catalogue.OwnSecrets{"provisioner-broker": {Path: "/var/lib/x/broker"}},
|
|
Claims: []catalogue.Claim{{Name: "node-provisioner", Scope: "node"}},
|
|
Resources: []map[string]any{{"id": "account", "type": "user", "name": "mesh-provisioner-client"},
|
|
{"id": "client", "type": "process", "name": "mesh-provisioner-client", "user": "mesh-provisioner-client"}}}
|
|
if got := provisionerAccount(catalogue.Resolution{Modules: []catalogue.Manifest{client}}); got != "mesh-provisioner-client" {
|
|
t.Fatalf("the client's account is %q", got)
|
|
}
|
|
if got := provisionerAccount(catalogue.Resolution{}); got != "" {
|
|
t.Fatalf("a machine without a client names %q", got)
|
|
}
|
|
}
|
|
|
|
// The first-machine gate counts a handed module healthy only once its node's prov.applied for the new build has
|
|
// come (ADR 0349 §15, kept by ADR 0362 for a walk finishing at its gate); a failure there fails it.
|
|
func TestTheGateWaitsForTheProvisionersReport(t *testing.T) {
|
|
now := time.Date(2026, 10, 14, 23, 0, 0, 0, time.UTC)
|
|
since := now.Add(-time.Minute)
|
|
reported := now.Add(-50 * time.Second)
|
|
digest := "sha256:" + strings.Repeat("d", 64)
|
|
facts := func(said map[string]inventory.ProvReport) gateFacts {
|
|
return gateFacts{now: now,
|
|
reports: map[string]inventory.Reported{"g14": {Node: "g14", Outcome: inventory.OutcomeApplied,
|
|
At: &reported, Current: true}},
|
|
sent: map[string]inventory.SentDeclaration{"g14": {Digest: "sent", At: since,
|
|
Provisioned: map[string]string{"dunst": digest}}},
|
|
provisioned: func(node, module string) (inventory.ProvReport, bool) {
|
|
r, ok := said[node+"."+module]
|
|
return r, ok
|
|
}}
|
|
}
|
|
m := catalogue.Manifest{Module: "dunst"}
|
|
if h, why := judgeHealth("dunst", "", m, "g14", since, facts(nil)); h != healthNotYet ||
|
|
!strings.Contains(why, "has not reported dunst's files placed") {
|
|
t.Fatalf("judged without the provisioner's report: %v %q", h, why)
|
|
}
|
|
failed := map[string]inventory.ProvReport{"g14.dunst": {Status: inventory.ProvFailed, Templates: digest,
|
|
Reason: "a value is not there", At: now.Add(-10 * time.Second)}}
|
|
if h, why := judgeHealth("dunst", "", m, "g14", since, facts(failed)); h != healthBroken ||
|
|
!strings.Contains(why, "a value is not there") {
|
|
t.Fatalf("a failed placing is not the build's failure there: %v %q", h, why)
|
|
}
|
|
placed := map[string]inventory.ProvReport{"g14.dunst": {Status: inventory.ProvApplied, Templates: digest,
|
|
At: now.Add(-10 * time.Second)}}
|
|
if h, why := judgeHealth("dunst", "", m, "g14", since, facts(placed)); strings.Contains(why, "provisioner") {
|
|
t.Fatalf("placed, the module is still held on the provisioner: %v %q", h, why)
|
|
}
|
|
// A module the send did not hand is judged as before.
|
|
if h, why := judgeHealth("picom", "", catalogue.Manifest{Module: "picom"}, "g14", since, facts(nil)); strings.Contains(why, "provisioner") {
|
|
t.Fatalf("a module not handed waits on the provisioner: %v %q", h, why)
|
|
}
|
|
}
|
|
|
|
// A machine whose provisioner client failed to place a module's files has a failed apply, said as apply-failing
|
|
// once the failure outlasts an apply interval, with the client's reason (ADR 0362 §3).
|
|
func TestAFailedPlacingRaisesApplyFailing(t *testing.T) {
|
|
now := time.Date(2026, 10, 4, 14, 0, 0, 0, time.UTC)
|
|
f := calm(now)
|
|
f.provFailed = []inventory.ProvReport{{Node: "anchor", Module: "dunst", Status: inventory.ProvFailed,
|
|
Reason: "the templates cannot be fetched", At: now.Add(-time.Minute)}}
|
|
if got := watchApplyFailing(f); len(got) != 0 {
|
|
t.Fatalf("raised within an apply interval: %+v", got)
|
|
}
|
|
f.provFailed[0].At = now.Add(-10 * time.Minute)
|
|
got := watchApplyFailing(f)
|
|
if len(got) != 1 || got[0].Key() != "machine.anchor.apply-failing" ||
|
|
!strings.Contains(got[0].Summary, "dunst") || !strings.Contains(got[0].Said, "the templates cannot be fetched") {
|
|
t.Fatalf("a failed placing is not raised as anchor's apply-failing: %+v", got)
|
|
}
|
|
}
|