From 7c046a8a1d8427ba9605fbbe424c003f32802ac8 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 01:42:09 +0200 Subject: [PATCH] Accept and ignore a file's `trusted`, and give each runtime its own time to answer The fourth review (hq issue 339): `trusted` is the catalogue's word and the controller takes it out, but a controller that passes it on, an older one or one rolled back to, must not cost a node its whole declaration. A file now accepts it and nothing reads it; any other resource still refuses it. And a podman that hangs no longer eats docker's ten seconds. --- internal/apply/placement_guard.go | 15 ++++++++------- internal/declaration/declaration.go | 6 ++++++ internal/declaration/trusted_test.go | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 internal/declaration/trusted_test.go diff --git a/internal/apply/placement_guard.go b/internal/apply/placement_guard.go index 1f53994..49c1d83 100644 --- a/internal/apply/placement_guard.go +++ b/internal/apply/placement_guard.go @@ -152,14 +152,15 @@ func readRuntimeRoots(ctx context.Context, run Runner) []string { } } if run != nil { - ask, cancel := context.WithTimeout(ctx, 10*time.Second) - if root, err := run(ask, "docker", "info", "--format", "{{.DockerRootDir}}"); err == nil { - add(root) + // Each runtime has its own ten seconds: one that hangs costs the other nothing. + for _, q := range [][]string{{"docker", "info", "--format", "{{.DockerRootDir}}"}, + {"podman", "info", "--format", "{{.Store.GraphRoot}}"}} { + ask, cancel := context.WithTimeout(ctx, 10*time.Second) + if root, err := run(ask, q[0], q[1:]...); err == nil { + add(root) + } + cancel() } - if root, err := run(ask, "podman", "info", "--format", "{{.Store.GraphRoot}}"); err == nil { - add(root) - } - cancel() } daemonJSON := func(path string) { raw, err := os.ReadFile(path) diff --git a/internal/declaration/declaration.go b/internal/declaration/declaration.go index 2472adf..a66d8ba 100644 --- a/internal/declaration/declaration.go +++ b/internal/declaration/declaration.go @@ -198,6 +198,12 @@ type File struct { // by looking rather than by knowing which field won. Sealed string `json:"sealed,omitempty"` + // Trusted is the catalogue's word on whether the settings this file's content took are trusted (novox/hq + // issue 339). It is the controller's to act on, and a controller takes it out before it sends a declaration. + // Accepted here, and nothing is done with it, so that a controller that passes it on — an older one, or one + // rolled back to — never costs a node its whole declaration. Not part of the file's digest. + Trusted *bool `json:"trusted,omitempty"` + // Secrets are sealed values put into Content where it says `${secret:name}`. // // **The one place a secret and a configuration meet, and it happens on the machine.** A diff --git a/internal/declaration/trusted_test.go b/internal/declaration/trusted_test.go new file mode 100644 index 0000000..c533334 --- /dev/null +++ b/internal/declaration/trusted_test.go @@ -0,0 +1,23 @@ +package declaration + +import "testing" + +// `trusted` is the catalogue's word on a file (novox/hq issue 339): a controller takes it out before it sends a +// declaration, and one that does not — an older one, or one rolled back to — must not cost the node its whole +// declaration. So a file accepts it and the engine does nothing with it; any other resource still refuses it. +func TestAFileAcceptsAndIgnoresTrusted(t *testing.T) { + for _, v := range []string{"true", "false"} { + d, err := Parse([]byte(`{"declaration":1,"resources":[{"id":"m.f","type":"file","path":"/etc/m.conf",` + + `"content":"x","trusted":` + v + `}]}`)) + if err != nil { + t.Fatalf("trusted %s: %v", v, err) + } + if f, ok := d.Resources[0].(*File); !ok || f.Content != "x" { + t.Fatalf("trusted %s: %+v", v, d.Resources[0]) + } + } + if _, err := Parse([]byte(`{"declaration":1,"resources":[{"id":"m.d","type":"directory","path":"/srv/m",` + + `"trusted":true}]}`)); err == nil { + t.Fatal("a directory took trusted") + } +}