Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
4 changed files with 207 additions and 5 deletions
Showing only changes of commit 8c248e3d7f - Show all commits
+24
View File
@@ -369,6 +369,27 @@ func applyFile(r *declaration.File, previous store.Applied, unseal Unseal) (Outc
}
content = string(opened)
}
// Sealed values into the holes the content left for them. **The host is the only thing that
// ever holds both** — the mesh discarded the value, and the module wrote the document without
// it (novox/hq ADR 0024).
if len(r.Secrets) > 0 {
if unseal == nil {
return out, fmt.Errorf(
"%s needs %d sealed value(s) and this node has no sealing key",
r.Path, len(r.Secrets))
}
for _, name := range r.SecretsUsed() {
opened, err := unseal(r.Secrets[name])
if err != nil {
return out, fmt.Errorf("cannot open the secret %q for %s: %w", name, r.Path, err)
}
content = strings.ReplaceAll(content, "${secret:"+name+"}", string(opened))
}
// A file carrying a credential is not world-readable, whatever else it also carries.
if r.Mode == "" {
fallback = 0o600
}
}
out.wrote = digestOf(content)
mode, err := modeOf(r.Mode, fallback)
if err != nil {
@@ -887,6 +908,9 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner) (
}
args := []string{"run", "--detach", "--name", r.Name, "--restart", "unless-stopped"}
for _, file := range r.EnvFile {
args = append(args, "--env-file", file)
}
if r.Network != "" {
args = append(args, "--network", r.Network)
}
+86
View File
@@ -13,6 +13,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
@@ -317,3 +318,88 @@ func TestANetworkAlreadyThereIsNotRebuilt(t *testing.T) {
"name and not the thing, so it does not tear one down and rebuild it", created)
}
}
// A secret inside a configuration file, substituted on the machine.
//
// **The one place a credential and a configuration meet.** A program wanting its token inside a
// JSON document cannot be handed a file that is entirely a token, and the mesh cannot compose the
// document because it discarded the value. So the module supplies the document with a hole, the
// mesh delivers the value sealed, and the host is the only thing that ever holds both.
func TestASealedValueIsPutIntoTheFileThatNamesIt(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "settings.json")
d := declare(t, `{"id":"settings","type":"file","path":"`+path+`",`+
`"content":"{\"tracking\":\"on\",\"token\":\"${secret:atlassian}\"}",`+
`"secrets":{"atlassian":"SEALED"}}`)
open := func(blob string) ([]byte, error) {
if blob != "SEALED" {
return nil, fmt.Errorf("asked to open %q", blob)
}
return []byte("the-real-token"), nil
}
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{},
store.OriginDeclared, nil, nil, open); err != nil {
t.Fatal(err)
}
written, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(written), `"token":"the-real-token"`) {
t.Fatalf("the secret was not put in: %s", written)
}
if strings.Contains(string(written), "secret:") {
t.Fatalf("a placeholder survived into the file: %s", written)
}
// The rest of the document is untouched — this is substitution, not replacement.
if !strings.Contains(string(written), `"tracking":"on"`) {
t.Fatalf("the content around the secret was lost: %s", written)
}
// And it carries a credential, so it is not world-readable.
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Errorf("a file holding a credential is %v", info.Mode().Perm())
}
}
// Defends the reason env-file exists: a credential may not travel in `env`.
//
// A declaration reaches a node over the broker and `env` is plain text in it, so a password there
// is a password the broker sees. A sealed file arrives unreadable, the host writes it, and the
// runtime reads it.
func TestAContainerIsGivenItsEnvironmentFiles(t *testing.T) {
var ran []string
run := func(_ context.Context, name string, args ...string) (string, error) {
ran = append(ran, name+" "+strings.Join(args, " "))
if len(args) > 0 && args[0] == "inspect" {
return "", fmt.Errorf("no such container")
}
return "", nil
}
d := declare(t, `{"id":"app","type":"container","name":"umami",`+
`"image":"umami@sha256:0000000000000000000000000000000000000000000000000000000000000000",`+
`"env-file":["/var/lib/umami/database.env","/var/lib/umami/app.env"]}`)
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{},
store.OriginDeclared, run, nil, nil)
var started string
for _, line := range ran {
if strings.Contains(line, "run ") {
started = line
}
}
for _, want := range []string{
"--env-file /var/lib/umami/database.env",
"--env-file /var/lib/umami/app.env",
} {
if !strings.Contains(started, want) {
t.Errorf("the container was started without %q:\n%s", want, started)
}
}
}
+74 -5
View File
@@ -12,6 +12,8 @@ import (
"fmt"
"io"
"reflect"
"regexp"
"slices"
"sort"
"strings"
)
@@ -118,6 +120,23 @@ type File struct {
// by looking rather than by knowing which field won.
Sealed string `json:"sealed,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
// program that wants its token inside a JSON document cannot be given a file that is entirely
// a token, and the mesh cannot compose the document itself — it discarded the value
// (novox/hq ADR 0024). So the module supplies the document with a hole in it, the mesh
// delivers the value sealed, and the host is the only thing that ever sees both.
//
// **Substitution is textual and the host learns no formats.** That is deliberate: a mechanism
// that understood JSON would be asked to understand YAML next, and then INI, which is how the
// arrangement this replaces became something nobody could hold in their head. The module knows
// its own format, because it wrote the rest of the file.
//
// The sharp edge, stated rather than discovered: a value containing a quote or a backslash
// will not be escaped for whatever syntax surrounds it.
Secrets map[string]string `json:"secrets,omitempty"`
// Bytes is content that is not text, base64-encoded — a wallpaper, a font, an icon.
//
// A third way of saying what is in a file, and the three are exclusive. It would have been
@@ -137,6 +156,22 @@ func (f *File) Identity() string { return f.ID }
func (f *File) Kind() Type { return TypeFile }
func (f *File) Target() string { return f.Path }
// placeholder is what Content says where a sealed value belongs: ${secret:name}.
var placeholder = regexp.MustCompile(`\$\{secret:([a-z0-9][a-z0-9-]*)\}`)
// SecretsUsed are the names Content asks for, in the order they first appear.
func (f *File) SecretsUsed() []string {
var used []string
seen := map[string]bool{}
for _, m := range placeholder.FindAllStringSubmatch(f.Content, -1) {
if !seen[m[1]] {
seen[m[1]] = true
used = append(used, m[1])
}
}
return used
}
func (f *File) validate(where string, _ bool) []string {
var problems []string
if f.Path == "" {
@@ -157,6 +192,29 @@ func (f *File) validate(where string, _ bool) []string {
strings.Join(said, " and ")+
" — otherwise nobody can tell by looking which one landed on the machine")
}
// A file whose content names a secret must be given exactly the secrets it names.
//
// **Both directions, and both are refusals rather than warnings.** A placeholder with nothing
// to fill it would write `${secret:x}` into a configuration file, which the program reads as
// a value and fails on somewhere unrelated. A secret nobody uses means whoever wrote this
// believes a credential is in a file where it is not.
if len(f.Secrets) > 0 && f.Content == "" {
problems = append(problems, where+
": secrets were given and there is no content to put them in")
}
used := f.SecretsUsed()
for _, name := range used {
if f.Secrets[name] == "" {
problems = append(problems, fmt.Sprintf(
"%s: the content asks for the secret %q and none was given", where, name))
}
}
for name := range f.Secrets {
if !slices.Contains(used, name) {
problems = append(problems, fmt.Sprintf(
"%s: the secret %q was given and the content never asks for it", where, name))
}
}
return append(problems, checkMode(where, f.Mode)...)
}
@@ -364,11 +422,22 @@ type Container struct {
Type Type `json:"type"`
Name string `json:"name"`
// Image is pinned by digest (novox/hq ADR 0006) — a tag moves and a digest does not.
Image string `json:"image"`
Env map[string]string `json:"env,omitempty"`
Ports []string `json:"ports,omitempty"`
Volumes []string `json:"volumes,omitempty"`
Args []string `json:"args,omitempty"`
Image string `json:"image"`
Env map[string]string `json:"env,omitempty"`
// EnvFile names files the runtime reads environment from, in order.
//
// **Because a secret may not travel in Env.** A declaration reaches a node over the broker,
// and `env` is plain text in it — so a password there is a password the broker sees, which is
// the transitive trust refused everywhere else (novox/hq ADR 0004). A sealed file reaches the
// machine unreadable, the host writes it, and the runtime reads it: the mesh never holds it
// and neither does anything between them.
//
// It is also simply how third-party software takes credentials. Nothing that ships in a
// container will read a path the mesh invented; every one of them reads its environment.
EnvFile []string `json:"env-file,omitempty"`
Ports []string `json:"ports,omitempty"`
Volumes []string `json:"volumes,omitempty"`
Args []string `json:"args,omitempty"`
// Names this container can reach, as `name:address`.
//
// **Because a container does not inherit the machine's names.** It gets its own `/etc/hosts`
+23
View File
@@ -359,3 +359,26 @@ func TestANetworkNameIsRefusedIfItIsNotOne(t *testing.T) {
}
}
}
// A placeholder with nothing to fill it is refused before anything is written.
//
// The alternative is a configuration file containing the literal `${secret:x}`, which the program
// reads as a value and fails on somewhere with no connection to this.
func TestAFileAskingForASecretItWasNotGivenIsRefused(t *testing.T) {
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"c","type":"file","path":"/tmp/x","content":"token=${secret:absent}"}
]}`)
if len(refusal.Problems) == 0 {
t.Fatal("a file naming a secret nobody gave it was accepted")
}
}
// And a secret nobody asked for, because somebody believes it is in a file where it is not.
func TestASecretTheContentNeverUsesIsRefused(t *testing.T) {
refusal := refusalFor(t, `{"declaration":1,"resources":[
{"id":"c","type":"file","path":"/tmp/x","content":"nothing here","secrets":{"spare":"S"}}
]}`)
if len(refusal.Problems) == 0 {
t.Fatal("a secret the content never mentions was accepted")
}
}