//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) } }