mesh/merge-gate head 0d12fb11a48b merged into trunk 59e6a6fa2a4a — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 0d12fb11a48b merged into trunk 59e6a6fa2a4a — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/check failed: mesh-build-go: no go build node qualifies: builders disagree: go1.27.1-X:nodwarf5 recipe 74744709d277ded9 on ace, g14, shanks; go1.…
On ace the postgres container was replaced at 22:11:51 mounting superuser.secret, a file the provisioner's client fills only after the apply. The runtime made the missing source a directory, and every fill after it failed with "is a directory". A container whose mount source is a file the declaration declares, not yet on the machine, now waits and is neither created nor replaced. The write of that file (WriteFilled) applies what reads it, as before, so the container is made once the file is there. What the runtime already left is cured by the mesh: a declared file whose path is an empty directory in the mesh's own tree (by its path and by where it leads), owned by the engine's account, has that directory removed before the file is written, and the outcome says so. A directory with anything in it, or outside the tree, is never removed.
122 lines
5.4 KiB
Go
122 lines
5.4 KiB
Go
//go:build linux
|
|
|
|
package apply
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// novox/hq issue 1873, its second fault: on ace the postgres container was replaced at 22:11:51 mounting
|
|
// superuser.secret, a file the provisioner's client fills only after the apply; the runtime made the missing source
|
|
// a directory, and every fill after it failed with "is a directory". A container whose mount source is a file the
|
|
// declaration declares, not yet there, is not created or replaced: it waits, and the write of that file (WriteFilled)
|
|
// applies what reads it, the container among them (readsChange), once the file is there.
|
|
func TestAContainerWaitsForAFileItMountsAndIsMadeWhenTheFillLands(t *testing.T) {
|
|
secret := filepath.Join(t.TempDir(), "postgres", "superuser.secret")
|
|
d := filledDecl(t, secret, `"content":"","mode":"0600","owner-at-runtime":"owned"`,
|
|
`{"id":"postgres.server","type":"container","name":"postgres","image":"`+pinned+`","volumes":["`+
|
|
secret+`:/run/secrets/superuser:ro"]}`)
|
|
m := &machine{containers: map[string]*fakeContainer{}}
|
|
report, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, m.run, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o := outcomeOf(report, "postgres.server"); o.Action != "waiting" || !strings.Contains(o.Detail, secret) {
|
|
t.Errorf("the container mounting a file not yet filled is %+v, not waiting for it", o)
|
|
}
|
|
if m.did("docker run") {
|
|
t.Fatalf("the container was made before the file it mounts was there: %v", m.asked)
|
|
}
|
|
if _, err := os.Lstat(secret); err == nil {
|
|
t.Fatal("something was made at the file's path before the fill")
|
|
}
|
|
|
|
report, _, err = WriteFilled(context.Background(), archHost(t), d, state, secret, []byte("s3cret"), m.run, nil,
|
|
nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !m.did("docker run") {
|
|
t.Fatalf("the container was not made when the file it mounts was filled: %v", m.asked)
|
|
}
|
|
if info, err := os.Lstat(secret); err != nil || !info.Mode().IsRegular() {
|
|
t.Fatalf("the filled file is not a file: %v %v", info, err)
|
|
}
|
|
}
|
|
|
|
// novox/hq issue 1873: what the runtime left on ace is cured by the mesh. A declared file whose path is an empty
|
|
// directory in the mesh's own tree, the engine's account's, is the runtime's missing mount source: the engine
|
|
// removes that empty directory and writes the file, and says so. A directory with anything in it, or one outside the
|
|
// tree, is never removed: the write fails as before.
|
|
func TestAnEmptyDirectoryTheRuntimeLeftAtAFilesPathIsReplacedByTheFile(t *testing.T) {
|
|
rootAlone(t, "")
|
|
root := t.TempDir()
|
|
before := meshDataRoot
|
|
meshDataRoot = filepath.Join(root, "mesh")
|
|
t.Cleanup(func() { meshDataRoot = before })
|
|
|
|
secret := filepath.Join(meshDataRoot, "postgres", "superuser.secret")
|
|
if err := os.MkdirAll(secret, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := filledDecl(t, secret, `"content":"","mode":"0600","owner-at-runtime":"owned"`, "")
|
|
m := &machine{containers: map[string]*fakeContainer{}}
|
|
report, _, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, secret, []byte("s3cret"), m.run,
|
|
nil, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("the file was not written over the empty directory the runtime left: %v", err)
|
|
}
|
|
if info, err := os.Lstat(secret); err != nil || !info.Mode().IsRegular() {
|
|
t.Fatalf("%s is not a file: %v %v", secret, info, err)
|
|
}
|
|
if o := outcomeOf(report, "m.conf"); !strings.Contains(o.Detail, "empty directory") {
|
|
t.Errorf("the write did not say it removed the empty directory: %+v", o)
|
|
}
|
|
|
|
// Not empty: kept, and the write fails.
|
|
full := filepath.Join(meshDataRoot, "postgres", "reader.secret")
|
|
if err := os.MkdirAll(filepath.Join(full, "something"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d = filledDecl(t, full, `"content":"","mode":"0600","owner-at-runtime":"owned"`, "")
|
|
if _, _, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, full, []byte("x"), m.run, nil,
|
|
nil, nil); err == nil {
|
|
t.Error("a directory with something in it was replaced by a file")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(full, "something")); err != nil {
|
|
t.Errorf("what was in the directory is gone: %v", err)
|
|
}
|
|
|
|
// Outside the tree: kept, and the write fails.
|
|
outside := filepath.Join(root, "var-lib", "postgres", "superuser.secret")
|
|
if err := os.MkdirAll(outside, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d = filledDecl(t, outside, `"content":"","mode":"0600","owner-at-runtime":"owned"`, "")
|
|
if _, _, err := WriteFilled(context.Background(), archHost(t), d, store.State{}, outside, []byte("x"), m.run, nil,
|
|
nil, nil); err == nil {
|
|
t.Error("an empty directory outside the mesh's tree was replaced by a file")
|
|
}
|
|
|
|
// Reached through a link out of the tree: kept, and the write fails.
|
|
elsewhere := filepath.Join(root, "elsewhere")
|
|
if err := os.MkdirAll(filepath.Join(elsewhere, "x.secret"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(elsewhere, filepath.Join(meshDataRoot, "linked")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
through := filepath.Join(meshDataRoot, "linked", "x.secret")
|
|
d = filledDecl(t, through, `"content":"","mode":"0600","owner-at-runtime":"owned"`, "")
|
|
_, _, _ = WriteFilled(context.Background(), archHost(t), d, store.State{}, through, []byte("x"), m.run, nil, nil, nil)
|
|
if info, err := os.Lstat(filepath.Join(elsewhere, "x.secret")); err != nil || !info.IsDir() {
|
|
t.Errorf("a directory outside the tree, reached through a link in it, was replaced: %v %v", info, err)
|
|
}
|
|
}
|