Write into a file the machine shares instead of over it, and reload a service that re-reads its configuration instead of restarting it (hq ADR 0102)

This commit is contained in:
2026-09-22 17:48:57 +02:00
parent 6eabed63eb
commit 0f126d137c
8 changed files with 575 additions and 2 deletions
+34
View File
@@ -154,6 +154,13 @@ type File struct {
// (ADR 0030), and it does not overwrite that either.
CreateOnce bool `json:"create-once,omitempty"`
// Into says the file is shared with software the mesh did not install, and the content is
// the mesh's part of it: written into what is there, never over it (novox/hq ADR 0102). Only
// "json" is spoken — the content is a JSON object whose keys the host sets in the file's
// object, keeping every other key as it found it and recording what each of its keys held
// before, so undeclaring the file gives those back.
Into string `json:"into,omitempty"`
// Sealed is content encrypted to this node's sealing key, for a file the mesh must deliver
// without being able to read.
//
@@ -224,6 +231,24 @@ func (f *File) validate(where string, _ bool) []string {
if f.Path == "" {
problems = append(problems, where+": a file needs a path")
}
switch f.Into {
case "":
case IntoJSON:
var object map[string]json.RawMessage
if err := json.Unmarshal([]byte(f.Content), &object); err != nil || object == nil {
problems = append(problems, where+
": a file written into JSON carries a JSON object of the keys it sets")
}
if f.Sealed != "" || f.Bytes != "" || len(f.Secrets) > 0 || f.CreateOnce {
problems = append(problems, where+
": a file written into says only its keys, in content — not sealed, bytes, "+
"secrets or create-once")
}
default:
problems = append(problems, fmt.Sprintf(
"%s: into %q; a file is written into \"json\", or omits it to be written whole",
where, f.Into))
}
var said []string
for name, value := range map[string]string{
"content": f.Content, "sealed": f.Sealed, "bytes": f.Bytes,
@@ -586,6 +611,12 @@ type Service struct {
// would be an action, and the link may not carry one (novox/hq ADR 0005) — so this is not a
// way around that rule, it is the shape the rule leaves.
RestartOn []string `json:"restart-on,omitempty"`
// ReloadOn names resources whose change means this service must be reloaded — for a service
// that re-reads its configuration when told to, where a restart would stop what it runs: the
// container runtime, whose restart stops every container on the machine (novox/hq ADR 0102).
// A change that is also in RestartOn restarts it, which covers a reload.
ReloadOn []string `json:"reload-on,omitempty"`
}
func (s *Service) Identity() string { return s.ID }
@@ -609,6 +640,9 @@ func (s *Service) validate(where string, _ bool) []string {
return problems
}
// IntoJSON is the one structured format a file is written into.
const IntoJSON = "json"
// Opening is a port reachable on an adopted node, from where, and on which path.
//
// **From** is everywhere or mesh — the private network, by its interface. **Path** is incoming,
+35
View File
@@ -0,0 +1,35 @@
package declaration
import (
"strings"
"testing"
)
// Defends novox/hq ADR 0102: a file written into carries only a JSON object of its keys, in a
// format the host speaks, and a service may name what it is reloaded on.
func TestAFileWrittenIntoIsRefusedUnlessItIsAnObjectOfKeys(t *testing.T) {
for name, c := range map[string]struct{ resource, refusal string }{
"another format": {`{"id":"f","type":"file","path":"/etc/x","into":"toml","content":"a = 1"}`, `into "toml"`},
"not an object": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"[1,2]"}`, "JSON object"},
"with create-once": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","create-once":true}`, "create-once"},
} {
_, err := Parse([]byte(`{"declaration":1,"resources":[` + c.resource + `]}`))
if err == nil || !strings.Contains(err.Error(), c.refusal) {
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
}
}
d, err := Parse([]byte(`{"declaration":1,"resources":[
{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{\"k\":1}"},
{"id":"s","type":"service","unit":"docker.service","state":"running","reload-on":["f"]}
]}`))
if err != nil {
t.Fatal(err)
}
if f := d.Resources[0].(*File); f.Into != IntoJSON {
t.Errorf("into was read as %q", f.Into)
}
if s := d.Resources[1].(*Service); len(s.ReloadOn) != 1 || s.ReloadOn[0] != "f" {
t.Errorf("reload-on was read as %v", s.ReloadOn)
}
}