node-engine: typed layouts for the states have (both forms), composed (both forms, wire names), data and health, with fixtures; Candidates typed as CandidateAnswer (ADR 0634 §1, §6; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:50:04 +01:00
parent 99adc0eb98
commit f00aac3847
5 changed files with 2290 additions and 151 deletions
+430 -60
View File
@@ -185,6 +185,37 @@ func DecodeComposedByNodeModuleFingerprintValue(raw []byte) (ComposedByNodeModul
return v, nil
}
// ComposedByNodeNodeKey is the key of one value of the state composed under the form "<node>.node".
func ComposedByNodeNodeKey(node string) string { return node + ".node" }
// ParseComposedByNodeNodeKey reads the parts of a key of the form "<node>.node", in the order of the form; the last result is false for a key of another form.
func ParseComposedByNodeNodeKey(key string) (string, bool) {
rest := key
v0, after0, found0 := strings.Cut(rest, ".node")
if !found0 || v0 == "" {
return "", false
}
rest = after0
if rest != "" {
return "", false
}
return v0, true
}
// DecodeComposedByNodeNodeValue reads one value of the state composed under the form "<node>.node", refusing a field ComposedByNodeNodeValue does not know and any text after the value.
func DecodeComposedByNodeNodeValue(raw []byte) (ComposedByNodeNodeValue, error) {
var v ComposedByNodeNodeValue
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&v); err != nil {
return v, err
}
if dec.More() {
return v, errors.New("text after the value")
}
return v, nil
}
// DataByNodeModuleKey is the key of one value of the state data under the form "<node>.<module>".
func DataByNodeModuleKey(node, module string) string { return node + "." + module }
@@ -314,6 +345,17 @@ func DecodeFindingsByKeyValue(raw []byte) (FindingsByKeyValue, error) {
return v, nil
}
// AccountsHanded is the hashes of the users of the list, sealed to the vault.
type AccountsHanded struct {
// when
At string `json:"at"`
Box SealedBox `json:"box"`
From string `json:"from"`
// the fingerprint of the vault's key
To string `json:"to"`
Users int64 `json:"users"`
}
// Act is one act asked through the seat, as acts reads it (ADR 0563 §9).
type Act struct {
// the act: hand-over, access-act, judge-directories or setuid-search
@@ -395,6 +437,14 @@ type Acts struct {
Node string `json:"node"`
}
// Adoption is the node is adopted and which of its modules are taken.
type Adoption struct {
// the modules taken
Taken []string `json:"taken"`
// by module, the resources left untaken
Untaken map[string][]string `json:"untaken,omitempty"`
}
// Asked is the answer of an act the operator is asked.
type Asked struct {
// the ask's id: acts shows where it stands
@@ -405,6 +455,112 @@ type Asked struct {
Says string `json:"says"`
}
// AttachedNetwork is a network attached to a link.
type AttachedNetwork struct {
Link string `json:"link"`
Network string `json:"network"`
}
// CandidateAdded is a module the candidate adds.
type CandidateAdded struct {
Composed string `json:"composed,omitempty"`
Module string `json:"module"`
Unissued []string `json:"unissued,omitempty"`
}
// CandidateAnswer is the node's answer to one placement candidate naming it.
type CandidateAnswer struct {
Added []CandidateAdded `json:"added,omitempty"`
Differs []CandidateDiffers `json:"differs,omitempty"`
Held []CandidateRefusal `json:"held,omitempty"`
Note string `json:"note,omitempty"`
Proposal string `json:"proposal"`
Refusals []CandidateRefusal `json:"refusals,omitempty"`
// why the node cannot compose with the candidate at all
Refused string `json:"refused,omitempty"`
// modules the candidate removes
Removed []string `json:"removed,omitempty"`
Returns string `json:"returns,omitempty"`
Revision int64 `json:"revision"`
// modules whose composition is the one running
Same []string `json:"same,omitempty"`
ValuesDiffer int64 `json:"values_differ,omitempty"`
// same, differs or refused
Verdict string `json:"verdict"`
}
// The values of CandidateAnswer's verdict.
const (
CandidateAnswerVerdictSame = "same"
CandidateAnswerVerdictDiffers = "differs"
CandidateAnswerVerdictRefused = "refused"
)
// CandidateBodies is the bodies of one resource that differ.
type CandidateBodies struct {
Composed string `json:"composed,omitempty"`
ID string `json:"id"`
Paths []string `json:"paths,omitempty"`
Running string `json:"running,omitempty"`
Why string `json:"why"`
}
// CandidateDiffers is a module whose composition with the candidate differs from the one running.
type CandidateDiffers struct {
Bodies []CandidateBodies `json:"bodies,omitempty"`
Composed string `json:"composed,omitempty"`
Module string `json:"module"`
Resource string `json:"resource,omitempty"`
Running string `json:"running,omitempty"`
Why string `json:"why"`
}
// CandidateRefusal is a module refused or held with the candidate.
type CandidateRefusal struct {
Module string `json:"module"`
Today string `json:"today,omitempty"`
Why string `json:"why"`
}
// DeclaredGroup is a group.
type DeclaredGroup struct {
Gid int64 `json:"gid"`
Name string `json:"name"`
}
// DeclaredIdentity is the identity of a module assigned to the node.
type DeclaredIdentity struct {
Gid int64 `json:"gid"`
Groups []int64 `json:"groups,omitempty"`
Module string `json:"module"`
Name string `json:"name"`
Runner string `json:"runner,omitempty"`
Uid int64 `json:"uid"`
}
// DeclaredLibrary is a library the node is reached by.
type DeclaredLibrary struct {
Gid int64 `json:"gid"`
Group string `json:"group"`
Home bool `json:"home"`
Library string `json:"library"`
Paths []string `json:"paths,omitempty"`
Root string `json:"root"`
Runner string `json:"runner,omitempty"`
Taken bool `json:"taken,omitempty"`
}
// FailedUnit is a unit that failed.
type FailedUnit struct {
Load string `json:"load,omitempty"`
Resource string `json:"resource,omitempty"`
Result string `json:"result,omitempty"`
Scope string `json:"scope"`
// since when
Since string `json:"since"`
Unit string `json:"unit"`
}
// FindingAction is one answer the operator may give from a notification.
type FindingAction struct {
Arguments map[string]string `json:"arguments,omitempty"`
@@ -439,6 +595,15 @@ type FindingSubject struct {
Scope string `json:"scope"`
}
// LoginFallback is a login that fell back to another node.
type LoginFallback struct {
// when
At string `json:"at"`
From string `json:"from"`
Node string `json:"node"`
Why string `json:"why"`
}
// ManifestReadsChanged is what a node-engine says when what it reads changed or its node returned (ADR 0618 §4).
type ManifestReadsChanged struct {
// when it says it, RFC 3339
@@ -482,6 +647,115 @@ const (
ModuleStateHeld = "held"
)
// NetworkHealth is the node's network.
type NetworkHealth struct {
Parts []NetworkPart `json:"parts"`
// since when
Since string `json:"since"`
// running, degraded or unknown
State string `json:"state"`
}
// NetworkPart is one part of the node's network.
type NetworkPart struct {
Owner string `json:"owner,omitempty"`
Part string `json:"part"`
Reason string `json:"reason,omitempty"`
Said string `json:"said,omitempty"`
// since when
Since string `json:"since"`
State string `json:"state"`
Streak int64 `json:"streak,omitempty"`
Targets []string `json:"targets,omitempty"`
Toward []string `json:"toward,omitempty"`
Writer string `json:"writer,omitempty"`
}
// NodeAccount is an account the node needs to log in again.
type NodeAccount struct {
Account string `json:"account"`
Module string `json:"module,omitempty"`
Reason string `json:"reason"`
Resource string `json:"resource,omitempty"`
// since when
Since string `json:"since"`
}
// NodeDirectory is a directory found where the node expected none.
type NodeDirectory struct {
Module string `json:"module"`
Path string `json:"path"`
Reason string `json:"reason"`
Resource string `json:"resource"`
// since when
Since string `json:"since"`
}
// NodeOwn is what a node's own composition carries beside its resources.
type NodeOwn struct {
Adoption *Adoption `json:"adoption,omitempty"`
Groups []DeclaredGroup `json:"groups,omitempty"`
Identities []DeclaredIdentity `json:"identities,omitempty"`
Libraries []DeclaredLibrary `json:"libraries,omitempty"`
// the account the provisioner's client runs as on the node
ProvisionerAccount string `json:"provisioner_account,omitempty"`
}
// NodeRecord is what the machine observes of itself, for every node and composing part to read.
type NodeRecord struct {
Authority string `json:"authority,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
Certificate string `json:"certificate,omitempty"`
Contract int64 `json:"contract,omitempty"`
ManifestReads []string `json:"manifest-reads,omitempty"`
Networks []AttachedNetwork `json:"networks,omitempty"`
NetworksSaid bool `json:"networks-said,omitempty"`
Node string `json:"node"`
OutwardLinks []string `json:"outward-links,omitempty"`
SealingKey string `json:"sealing-key,omitempty"`
ServingKey string `json:"serving-key,omitempty"`
Trusts []string `json:"trusts,omitempty"`
TunnelKey string `json:"tunnel-key,omitempty"`
V int64 `json:"v"`
}
// PeerSeen is a tunnel peer and whether it is seen.
type PeerSeen struct {
Addresses []string `json:"addresses,omitempty"`
Key string `json:"key"`
Seen bool `json:"seen"`
}
// PlacedDataItem is one data item a module declares, as placed on a node.
type PlacedDataItem struct {
Class string `json:"class"`
// the path whose snapshot keeps it, empty when it is not backed up
CoveredBy string `json:"covered_by,omitempty"`
Item string `json:"item"`
// walk, dataset or shallow
Measure string `json:"measure,omitempty"`
Module string `json:"module"`
// the item's place on the node
Path string `json:"path"`
// backup, redundancy, both or none
Protection string `json:"protection,omitempty"`
}
// PullWait is the pull from a store a failed apply waits on.
type PullWait struct {
// when the pull began
Began string `json:"began"`
// that seat's holder, whose settling the apply waits for
Holder string `json:"holder"`
Image string `json:"image"`
// what triggered the last try again, in words
Retried string `json:"retried,omitempty"`
// when the apply was last tried again
RetriedAt string `json:"retried-at,omitempty"`
// the seat the pull came through
Seat string `json:"seat,omitempty"`
}
// Revision is one revision of a composition key.
type Revision struct {
// its digest
@@ -498,6 +772,17 @@ type Revision struct {
Revision int64 `json:"revision,omitempty"`
}
// RootAccountVerdict is the engine's verdict of whether an account's resource is root-free, as the node's health lists it.
type RootAccountVerdict struct {
Account string `json:"account"`
Module string `json:"module"`
Reason string `json:"reason,omitempty"`
Resource string `json:"resource"`
// since when
Since string `json:"since"`
State string `json:"state"`
}
// RootVerdict is the engine's root-free verdict (ADR 0563 §6).
type RootVerdict struct {
// the machine is root-free
@@ -518,6 +803,15 @@ type Said struct {
Said string `json:"said"`
}
// SealedBox is a value sealed to a key.
type SealedBox struct {
Ct string `json:"ct"`
Eph string `json:"eph"`
Iv string `json:"iv"`
Tag string `json:"tag"`
V int64 `json:"v"`
}
// SeatListStatus is a type of the contract.
type SeatListStatus struct {
// the seat mesh-catalog has answered
@@ -554,6 +848,37 @@ const (
SeatListHeldSourceBootstrap = "bootstrap"
)
// UnitsHealth is the node's units.
type UnitsHealth struct {
Away []string `json:"away,omitempty"`
Failed []FailedUnit `json:"failed"`
State string `json:"state"`
Unread []string `json:"unread,omitempty"`
}
// UserListWritten is the bus's user list as the engine of the bus node wrote it to the bus module's file.
type UserListWritten struct {
// when
At string `json:"at"`
// the digest of the file on the machine after the write
Digest string `json:"digest,omitempty"`
// why a write failed
Error string `json:"error,omitempty"`
// which list the node follows now
Follows string `json:"follows,omitempty"`
Handed *AccountsHanded `json:"handed,omitempty"`
// the bus node's sealing key, its public half
Key string `json:"key,omitempty"`
Revision int64 `json:"revision"`
// the key the list was taken from
Source string `json:"source"`
// why the bus node does not watch the grants part's key now
Unwatched string `json:"unwatched,omitempty"`
Users int64 `json:"users"`
// the digest of the list the source holds, sha256:<hex>
Wanted string `json:"wanted"`
}
// HandOverInput is what hand-over takes.
type HandOverInput struct {
// the directory's absolute path, as the engine states it
@@ -905,75 +1230,119 @@ type ManifestReadsChangedEvent = ManifestReadsChanged
// HaveByNodeModuleValue is one value of the state have under the key <node>.<module>.
type HaveByNodeModuleValue struct {
Build string `json:"build,omitempty"`
BuildAlone bool `json:"build-alone,omitempty"`
// the composition's digest
Build string `json:"build,omitempty"`
// the move changes the build alone
BuildAlone bool `json:"build-alone,omitempty"`
// the digest of the composition the node runs
Composed string `json:"composed,omitempty"`
Detail string `json:"detail,omitempty"`
// the fingerprint held
Fp string `json:"fp,omitempty"`
// the accounts handed
Handed map[string]any `json:"handed,omitempty"`
// how it is
Health string `json:"health"`
Judgement string `json:"judgement,omitempty"`
Kept []string `json:"kept,omitempty"`
Fp string `json:"fp,omitempty"`
Handed *AccountsHanded `json:"handed,omitempty"`
// what the module's own check says
Health string `json:"health"`
// in phase waiting-yes, the judgement of the move
Judgement string `json:"judgement,omitempty"`
// what the node keeps of the module as it runs
Kept []string `json:"kept,omitempty"`
// how far it has come
Phase string `json:"phase"`
// a pull the engine waits for
PullWait map[string]any `json:"pull-wait,omitempty"`
Sealed []string `json:"sealed,omitempty"`
SealedAt map[string]any `json:"sealed-at,omitempty"`
// the user list written
UserList map[string]any `json:"user-list,omitempty"`
V int64 `json:"v"`
// what it waits for
Phase string `json:"phase"`
PullWait *PullWait `json:"pull-wait,omitempty"`
// each value sealed to the node's key
Sealed []string `json:"sealed,omitempty"`
// the sealed values by slot
SealedAt map[string]string `json:"sealed-at,omitempty"`
UserList *UserListWritten `json:"user-list,omitempty"`
V int64 `json:"v"`
// in phase waiting-yes, the digest of the composition that waits
Waits string `json:"waits,omitempty"`
}
// The values of HaveByNodeModuleValue's health.
const (
HaveByNodeModuleValueHealthOk = "ok"
HaveByNodeModuleValueHealthFailed = "failed"
HaveByNodeModuleValueHealthWaits = "waits"
HaveByNodeModuleValueHealthUnknown = "unknown"
)
// The values of HaveByNodeModuleValue's judgement.
const (
HaveByNodeModuleValueJudgementRecreates = "recreates"
HaveByNodeModuleValueJudgementMovesData = "moves-data"
HaveByNodeModuleValueJudgementCannotJudge = "cannot-judge"
)
// The values of HaveByNodeModuleValue's phase.
const (
HaveByNodeModuleValuePhaseBackingUp = "backing-up"
HaveByNodeModuleValuePhaseApplying = "applying"
HaveByNodeModuleValuePhaseChecking = "checking"
HaveByNodeModuleValuePhaseRunning = "running"
HaveByNodeModuleValuePhaseFailed = "failed"
HaveByNodeModuleValuePhaseRemoved = "removed"
HaveByNodeModuleValuePhaseHeld = "held"
HaveByNodeModuleValuePhaseWaitingYes = "waiting-yes"
)
// HaveByNodeNodeValue is one value of the state have under the key <node>.node.
type HaveByNodeNodeValue struct {
Busy map[string]any `json:"busy,omitempty"`
Candidates map[string]map[string]any `json:"candidates,omitempty"`
Composed string `json:"composed,omitempty"`
// own, or the controller's
Composes string `json:"composes,omitempty"`
ComposesWhy string `json:"composes-why,omitempty"`
Filters map[string]any `json:"filters,omitempty"`
Profile map[string]any `json:"profile,omitempty"`
// the node's record: its tunnel and certificate
Record map[string]any `json:"record,omitempty"`
Sealed []string `json:"sealed,omitempty"`
SealedAt map[string]any `json:"sealed-at,omitempty"`
Strays map[string]any `json:"strays,omitempty"`
V int64 `json:"v"`
Windows map[string]any `json:"windows,omitempty"`
Busy map[string]any `json:"busy,omitempty"`
// the node's answer to each placement candidate naming it, by proposal id
Candidates map[string]CandidateAnswer `json:"candidates,omitempty"`
// the digest of the node's own composition it runs
Composed string `json:"composed,omitempty"`
// own, or none (empty)
Composes string `json:"composes,omitempty"`
ComposesWhy string `json:"composes-why,omitempty"`
Filters map[string]any `json:"filters,omitempty"`
Profile map[string]any `json:"profile,omitempty"`
Record *NodeRecord `json:"record,omitempty"`
Sealed []string `json:"sealed,omitempty"`
SealedAt map[string]string `json:"sealed-at,omitempty"`
Strays map[string]any `json:"strays,omitempty"`
V int64 `json:"v"`
Windows map[string]any `json:"windows,omitempty"`
}
// ComposedByNodeModuleFingerprintValue is one value of the state composed under the key <node>.<module>.<fingerprint>.
type ComposedByNodeModuleFingerprintValue struct {
Data bool `json:"Data,omitempty"`
FP string `json:"FP"`
Filled []string `json:"Filled,omitempty"`
Module string `json:"Module"`
Node string `json:"Node"`
// the node's own adoption, identities, groups, libraries and provisioner account
Own map[string]any `json:"Own,omitempty"`
// the module's plan
Plan map[string]any `json:"Plan,omitempty"`
// why the composition was refused
Refused string `json:"Refused,omitempty"`
Resources []map[string]any `json:"Resources,omitempty"`
V int64 `json:"V,omitempty"`
// the module declares data
Data bool `json:"data,omitempty"`
// the files the provisioner fills
FilledFiles []string `json:"filled_files,omitempty"`
Fp string `json:"fp"`
Module string `json:"module"`
Node string `json:"node"`
// the module's whole provisioning plan, catalogue.Plan, kept whole: it is the manifest layer's type
Plan map[string]any `json:"plan,omitempty"`
// why the module cannot be composed at this fingerprint
Refused string `json:"refused,omitempty"`
// the declaration resources, each saying its module
Resources []map[string]any `json:"resources"`
V int64 `json:"v"`
}
// ComposedByNodeNodeValue is one value of the state composed under the key <node>.node.
type ComposedByNodeNodeValue struct {
// the module declares data
Data bool `json:"data,omitempty"`
// the files the provisioner fills
FilledFiles []string `json:"filled_files,omitempty"`
Node string `json:"node"`
Own *NodeOwn `json:"own,omitempty"`
// the declaration resources, each saying its module
Resources []map[string]any `json:"resources"`
V int64 `json:"v"`
}
// DataByNodeModuleValue is one value of the state data under the key <node>.<module>.
type DataByNodeModuleValue struct {
Digest string `json:"digest"`
Items []map[string]any `json:"items"`
Items []PlacedDataItem `json:"items"`
Module string `json:"module"`
Node string `json:"node"`
V int64 `json:"v,omitempty"`
V int64 `json:"v"`
}
// PackagesByNodeValuePackagesItem is a type of the contract.
@@ -991,17 +1360,18 @@ type PackagesByNodeValue struct {
// HealthByNodeValue is one value of the state health under the key <node>.
type HealthByNodeValue struct {
At string `json:"at"`
Contract int64 `json:"contract,omitempty"`
Fallback map[string]any `json:"fallback,omitempty"`
Found []map[string]any `json:"found,omitempty"`
Network map[string]any `json:"network,omitempty"`
Node string `json:"node"`
Peers []map[string]any `json:"peers,omitempty"`
Relogin []map[string]any `json:"relogin,omitempty"`
Root []map[string]any `json:"root,omitempty"`
Units map[string]any `json:"units,omitempty"`
V int64 `json:"v"`
// when
At string `json:"at"`
Contract int64 `json:"contract,omitempty"`
Fallback *LoginFallback `json:"fallback,omitempty"`
Found []NodeDirectory `json:"found,omitempty"`
Network *NetworkHealth `json:"network,omitempty"`
Node string `json:"node"`
Peers []PeerSeen `json:"peers,omitempty"`
Relogin []NodeAccount `json:"relogin,omitempty"`
Root []RootAccountVerdict `json:"root,omitempty"`
Units *UnitsHealth `json:"units,omitempty"`
V int64 `json:"v"`
}
// FindingsByKeyValue is one value of the state findings under the key <key>.