package main import ( "context" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // ── k8s.set_image, against a stand-in kubectl ─────────────────────────────── type fakeKube struct { containers map[string]string // container -> image calls [][]string failSet bool } func (f *fakeKube) run(_ context.Context, args ...string) (string, error) { f.calls = append(f.calls, args) switch { case len(args) >= 3 && args[2] == "get": type c struct { Name string `json:"name"` Image string `json:"image"` } var cs []c for n, im := range f.containers { cs = append(cs, c{n, im}) } raw, _ := json.Marshal(map[string]any{"spec": map[string]any{"template": map[string]any{"spec": map[string]any{"containers": cs}}}}) return string(raw), nil case len(args) >= 3 && args[2] == "set": if f.failSet { return "boom", errors.New("exit 1") } // set image =: the workload now runs it. if len(args) >= 6 { if c, ref, ok := strings.Cut(args[5], "="); ok { f.containers[c] = ref } } return "", nil } return "", nil } func (f *fakeKube) sets() [][]string { var out [][]string for _, c := range f.calls { if len(c) >= 3 && c[2] == "set" { out = append(out, c) } } return out } func useFakeKube(t *testing.T, f *fakeKube) { prev := opKubectl opKubectl = f.run t.Cleanup(func() { opKubectl = prev }) } func useAncestry(t *testing.T, fn func(source, older, newer string, env []string) error) { prev := opAncestry opAncestry = fn t.Cleanup(func() { opAncestry = prev }) } const tImage = "forge.example.com/acme/web" const tRepo = "https://forge.example.com/acme/app.git" func setImageLine(digest string) opVerbLine { return opVerbLine{Verb: "k8s.set_image", Args: map[string]string{"namespace": "ds", "kind": "deployment", "name": "bridge", "container": "bridge", "image": tImage, "digest": digest, "repo": tRepo}} } func issueReceiptFrom(t *testing.T, digest, source, commit string) { t.Helper() if _, err := opIssueReceipts("r", "bridge-build", strings.Repeat("e", 64), "{}", []opResultImage{{Image: tImage, Digest: digest, Source: source, Commit: commit}}); err != nil { t.Fatal(err) } } func issueReceipt(t *testing.T, digest, commit string) { issueReceiptFrom(t, digest, tRepo, commit) } // The gate's workload cases: only the named container of the named workload // is changed, only to a digest this machine holds a receipt for, only forward // from a live image it has a record of. func TestSetImageTouchesOnlyTheNamedContainerAndOnlyForward(t *testing.T) { setupBox(t) live := "sha256:" + strings.Repeat("1", 64) next := "sha256:" + strings.Repeat("2", 64) unknown := "sha256:" + strings.Repeat("3", 64) foreign := "sha256:" + strings.Repeat("4", 64) oldC, newC := strings.Repeat("a", 40), strings.Repeat("b", 40) issueReceipt(t, live, oldC) issueReceipt(t, next, newC) issueReceiptFrom(t, foreign, "https://forge.example.com/someone/else.git", newC) ctx := context.Background() // A digest without a receipt: refused at the plan, nothing set. f := &fakeKube{containers: map[string]string{"bridge": tImage + "@" + live, "sidecar": "other@" + live}} useFakeKube(t, f) useAncestry(t, func(_, _, _ string, _ []string) error { return nil }) if _, err := opImagePlan(ctx, setImageLine(unknown), nil); err == nil || !strings.Contains(err.Error(), "no receipt") { t.Fatalf("a digest without a receipt: %v", err) } if _, err := opImageDo(ctx, setImageLine(unknown), nil, nil, ""); err == nil || len(f.sets()) != 0 { t.Fatalf("and never set: %v %v", err, f.sets()) } // A container the workload does not have: refused, nothing set. other := setImageLine(next) other.Args["container"] = "nope" if _, err := opImageDo(ctx, other, nil, nil, ""); err == nil || !strings.Contains(err.Error(), "no container nope") || len(f.sets()) != 0 { t.Fatalf("a container that is not there: %v", err) } // A receipt that names another repository than the manifest's: refused // (the ancestry check would fetch whatever the receipt said). if _, err := opImagePlan(ctx, setImageLine(foreign), nil); err == nil || !strings.Contains(err.Error(), "says it was built from") { t.Fatalf("a receipt from another repository: %v", err) } // A live image this machine holds a receipt for but never rolled: no // record, so it asks — a receipt alone never makes an image "live". if _, err := opImageDo(ctx, setImageLine(next), nil, nil, ""); err == nil || !strings.Contains(err.Error(), "no record of the commit behind the live image") { t.Fatalf("a live image this machine never rolled: %v", err) } if err := opRecordRoll(live, opRollRec{Image: tImage, Source: tRepo, Commit: oldC}); err != nil { t.Fatal(err) } // A live image this machine has no record of: refused, and it says so. f.containers["bridge"] = tImage + "@" + unknown if _, err := opImageDo(ctx, setImageLine(next), nil, nil, ""); err == nil || !strings.Contains(err.Error(), "no record of the commit behind the live image") { t.Fatalf("an unknown live image: %v", err) } f.containers["bridge"] = tImage + ":latest" if _, err := opImageDo(ctx, setImageLine(next), nil, nil, ""); err == nil || !strings.Contains(err.Error(), "no record") { t.Fatalf("a live image by tag: %v", err) } // A workload running another image: refused. f.containers["bridge"] = "forge.example.com/acme/other@" + live if _, err := opImageDo(ctx, setImageLine(next), nil, nil, ""); err == nil || !strings.Contains(err.Error(), "runs forge.example.com/acme/other") { t.Fatalf("another image: %v", err) } // Backwards: refused by the ancestry check, nothing set. f.containers["bridge"] = tImage + "@" + live useAncestry(t, func(_, older, newer string, _ []string) error { return fmt.Errorf("the new image's commit %s does not descend from the live image's %s", newer, older) }) if _, err := opImageDo(ctx, setImageLine(next), nil, nil, ""); err == nil || !strings.Contains(err.Error(), "does not descend") || len(f.sets()) != 0 { t.Fatalf("backwards: %v", err) } // Forward: exactly one set, of the named container, then the rollout. var asked []string useAncestry(t, func(source, older, newer string, _ []string) error { asked = []string{source, older, newer} return nil }) f.calls = nil note, err := opImageDo(ctx, setImageLine(next), nil, nil, "") if err != nil { t.Fatal(err) } if strings.Join(asked, " ") != "https://forge.example.com/acme/app.git "+oldC+" "+newC { t.Fatalf("ancestry was asked of the receipts' commits: %v", asked) } sets := f.sets() if len(sets) != 1 || strings.Join(sets[0], " ") != "-n ds set image deployment/bridge bridge="+tImage+"@"+next { t.Fatalf("one set of the named container: %v", sets) } if last := f.calls[len(f.calls)-1]; strings.Join(last[:4], " ") != "-n ds rollout status" { t.Fatalf("then the rollout is waited on: %v", last) } if _, _, c, ok := opRolledCommit(next); !ok || c != newC || !strings.Contains(note, "rolled out") { t.Fatal("the roll is recorded, so the next roll knows what is live") } // The same digest again: nothing is set, and the rollout is still waited // on — a set image is not a rolled-out one (a retry after a rollout that // did not finish lands here). f.containers["bridge"] = tImage + "@" + next f.calls = nil if note, err := opImageDo(ctx, setImageLine(next), nil, nil, ""); err != nil || note != "already live, rolled out" || len(f.sets()) != 0 { t.Fatalf("already live: %q %v", note, err) } if last := f.calls[len(f.calls)-1]; len(last) < 4 || last[2] != "rollout" || last[3] != "status" { t.Fatalf("an image already set still waits for its rollout: %v", f.calls) } prev := opKubectl opKubectl = func(ctx context.Context, args ...string) (string, error) { if len(args) >= 3 && args[2] == "rollout" { return "timed out", errors.New("exit 1") } return f.run(ctx, args...) } defer func() { opKubectl = prev }() if _, err := opImageDo(ctx, setImageLine(next), nil, nil, ""); err == nil || !strings.Contains(err.Error(), "rollout did not finish") { t.Fatalf("an image already set whose rollout does not finish: %v", err) } } // ── unit verbs and log checks, against stand-ins ──────────────────────────── func TestUnitVerbsRestartTheNamedUnitAndCheckItCameBack(t *testing.T) { var calls [][]string state := map[string]string{"LoadState": "loaded", "ActiveState": "active", "SubState": "running", "MainPID": "42"} prev := opSystemctl opSystemctl = func(_ context.Context, args ...string) (string, error) { calls = append(calls, args) if args[0] == "show" { var b strings.Builder for k, v := range state { fmt.Fprintf(&b, "%s=%s\n", k, v) } return b.String(), nil } return "", nil } t.Cleanup(func() { opSystemctl = prev }) ctx := context.Background() v := opVerbLine{Verb: "unit.restart", Args: map[string]string{"unit": "app.service"}} c, err := opUnitPlan(ctx, v) if err != nil || c.Note != "active/running → restarted" { t.Fatalf("plan: %+v %v", c, err) } if _, err := opUnitDo(ctx, v); err != nil { t.Fatal(err) } if strings.Join(calls[len(calls)-2], " ") != "restart app.service" { t.Fatalf("restart of the named unit only: %v", calls) } state["ActiveState"], state["SubState"] = "failed", "failed" if _, err := opUnitDo(ctx, v); err == nil || !strings.Contains(err.Error(), "failed/failed") { t.Fatalf("a unit that did not come back fails the verb: %v", err) } state["LoadState"] = "not-found" if _, err := opUnitPlan(ctx, v); err == nil || !strings.Contains(err.Error(), "not a unit") { t.Fatalf("a unit that is not there: %v", err) } } func TestLogsCheckGivesAVerdictAndOneLine(t *testing.T) { var since []string prev := opJournalctl opJournalctl = func(_ context.Context, args ...string) (string, error) { since = args return "starting\nconnected to db password=hunter2\nTraceback (most recent call last)\nready\n", nil } t.Cleanup(func() { opJournalctl = prev }) at := time.Unix(1700000000, 0) ok, line, err := opLogsCheck(context.Background(), opVerbLine{Verb: opVerbLogs, Args: map[string]string{"unit": "a.service", "must": "^ready$"}}, at) if err != nil || !ok || line != "ready" { t.Fatalf("must: %v %q %v", ok, line, err) } if strings.Join(since[:4], " ") != "-u a.service --since @1700000000" { t.Fatalf("only what came after the apply began: %v", since) } ok, line, _ = opLogsCheck(context.Background(), opVerbLine{Verb: opVerbLogs, Args: map[string]string{"unit": "a.service", "must_not": "Traceback"}}, at) if ok || !strings.Contains(line, "Traceback (most recent") || strings.Contains(line, "hunter2") { t.Fatalf("must_not reports the one line that decided it, nothing else: %v %q", ok, line) } ok, line, _ = opLogsCheck(context.Background(), opVerbLine{Verb: opVerbLogs, Args: map[string]string{"unit": "a.service", "must": "never"}}, at) if ok || line != "no line showed /never/" { t.Fatalf("a must that never showed: %v %q", ok, line) } } // ── tree.checkout, against real repositories ──────────────────────────────── func gitT(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.Command("git", append([]string{"-C", dir, "-c", "user.name=t", "-c", "user.email=t@t", "-c", "init.defaultBranch=main", "-c", "protocol.file.allow=always"}, args...)...) cmd.Env = append(os.Environ(), "GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_NOSYSTEM=1") out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) } func useFileRepos(t *testing.T) { prevP, prevR := opTreeProtocols, opRepoURLOK opTreeProtocols = []string{"file"} opRepoURLOK = func(s string) error { if !strings.HasPrefix(s, "file:///") { return errors.New("not a test repo") } return nil } t.Cleanup(func() { opTreeProtocols, opRepoURLOK = prevP, prevR }) } // A tree moves forward to the exact commit, with no hook of its own running, // leaves untracked files and nested repositories alone, and refuses to go // back, to move with local changes, or to run git under a config that could // run a program. func TestTreeCheckoutMovesOnlyForward(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } setupBox(t) useFileRepos(t) base := t.TempDir() origin := filepath.Join(base, "origin") _ = os.MkdirAll(origin, 0o755) gitT(t, origin, "init", "-q") _ = os.WriteFile(filepath.Join(origin, "a.txt"), []byte("one\n"), 0o644) gitT(t, origin, "add", "a.txt") gitT(t, origin, "commit", "-qm", "one") c1 := gitT(t, origin, "rev-parse", "HEAD") tree := filepath.Join(base, "tree") gitT(t, base, "clone", "-q", "file://"+origin, tree) nested := filepath.Join(origin, "nested") _ = os.MkdirAll(nested, 0o755) gitT(t, nested, "init", "-q") gitT(t, nested, "commit", "-q", "--allow-empty", "-m", "n") gitT(t, origin, "add", "nested") _ = os.WriteFile(filepath.Join(origin, "a.txt"), []byte("two\n"), 0o644) gitT(t, origin, "commit", "-qam", "two") c2 := gitT(t, origin, "rev-parse", "HEAD") gitT(t, origin, "checkout", "-q", "-b", "side", c1) _ = os.WriteFile(filepath.Join(origin, "b.txt"), []byte("side\n"), 0o644) gitT(t, origin, "add", "b.txt") gitT(t, origin, "commit", "-qm", "side") side := gitT(t, origin, "rev-parse", "HEAD") gitT(t, origin, "checkout", "-q", "main") // A hook planted in the tree must not run as root. marker := filepath.Join(base, "hook-ran") _ = os.WriteFile(filepath.Join(tree, ".git/hooks/post-merge"), []byte("#!/bin/sh\ntouch "+marker+"\n"), 0o755) _ = os.WriteFile(filepath.Join(tree, "untracked.log"), []byte("keep\n"), 0o644) line := func(commit string) opVerbLine { return opVerbLine{Verb: "tree.checkout", Args: map[string]string{"path": tree, "repo": "file://" + origin, "commit": commit}} } ctx := context.Background() if c, err := opTreePlan(ctx, line(c2)); err != nil || c.Note != c1+" → "+c2 { t.Fatalf("plan: %+v %v", c, err) } note, err := opTreeDo(ctx, line(c2), nil, "") if err != nil { t.Fatal(err) } if gitT(t, tree, "rev-parse", "HEAD") != c2 || !strings.Contains(note, "nested") { t.Fatalf("moved to %s and named the nested repository: %q", c2, note) } if _, err := os.Stat(marker); err == nil { t.Fatal("the tree's own hook ran") } if b, _ := os.ReadFile(filepath.Join(tree, "untracked.log")); string(b) != "keep\n" { t.Fatal("an untracked file was touched") } // Backwards and sideways: refused, the tree stays. for _, bad := range []string{c1, side} { if _, err := opTreeDo(ctx, line(bad), nil, ""); err == nil || !strings.Contains(err.Error(), "only moves forward") { t.Fatalf("%s: %v", bad, err) } if _, err := opTreePlan(ctx, line(bad)); err == nil { t.Fatalf("the plan refuses %s already, the commit being here", bad) } } if gitT(t, tree, "rev-parse", "HEAD") != c2 { t.Fatal("the tree moved on a refusal") } // Another origin: refused. wrong := line(c2) wrong.Args["repo"] = "file://" + base + "/elsewhere" if _, err := opTreePlan(ctx, wrong); err == nil || !strings.Contains(err.Error(), "origin is not") { t.Fatalf("another origin: %v", err) } // A local change to a tracked file: refused. _ = os.WriteFile(filepath.Join(tree, "a.txt"), []byte("local\n"), 0o644) if _, err := opTreePlan(ctx, line(c2)); err == nil || !strings.Contains(err.Error(), "local changes") { t.Fatalf("local changes: %v", err) } gitT(t, tree, "checkout", "-q", "--", "a.txt") // A config that runs a program when git does: refused by name. for _, kv := range [][2]string{{"core.fsmonitor", "touch " + marker}, {"filter.x.smudge", "touch " + marker}, {"core.worktree", "/etc"}, {"include.path", "/nonexistent/privatae-optest.inc"}} { gitT(t, tree, "config", kv[0], kv[1]) if _, err := opTreePlan(ctx, line(c2)); err == nil || !strings.Contains(err.Error(), "will not run git under") { t.Fatalf("%s: %v", kv[0], err) } gitT(t, tree, "config", "--unset", kv[0]) } if _, err := os.Stat(marker); err == nil { t.Fatal("a configured program ran") } } // twoCommitRepo makes an origin with two commits and a clone at the first. // before runs in the origin before the first commit, after before the second. func twoCommitRepo(t *testing.T, before, after func(origin string)) (origin, tree, c1, c2 string) { t.Helper() base := t.TempDir() origin = filepath.Join(base, "origin") _ = os.MkdirAll(origin, 0o755) gitT(t, origin, "init", "-q") _ = os.WriteFile(filepath.Join(origin, "a.txt"), []byte("one\n"), 0o644) before(origin) gitT(t, origin, "add", "-A") gitT(t, origin, "commit", "-qm", "one") c1 = gitT(t, origin, "rev-parse", "HEAD") tree = filepath.Join(base, "tree") gitT(t, base, "clone", "-q", "file://"+origin, tree) after(origin) gitT(t, origin, "commit", "-qam", "two") c2 = gitT(t, origin, "rev-parse", "HEAD") return origin, tree, c1, c2 } // Git runs as root only in a tree no one else controls: a tree, .git or // config another user owns or can write is refused before git runs there. // And a move never overwrites an ignored file the machine keeps (a .env). func TestTreeCheckoutRefusesATreeRootDoesNotControlAndKeepsIgnoredFiles(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } if os.Geteuid() != 0 { t.Skip("needs root to own and hand over the fixtures") } setupBox(t) useFileRepos(t) origin, tree, _, c2 := twoCommitRepo(t, func(o string) { _ = os.WriteFile(filepath.Join(o, ".gitignore"), []byte(".env\n"), 0o644) }, func(o string) { _ = os.WriteFile(filepath.Join(o, ".env"), []byte("FROM-REPO\n"), 0o644) gitT(t, o, "add", "-f", ".env") _ = os.WriteFile(filepath.Join(o, "a.txt"), []byte("two\n"), 0o644) }) line := opVerbLine{Verb: "tree.checkout", Args: map[string]string{"path": tree, "repo": "file://" + origin, "commit": c2}} ctx := context.Background() for _, c := range []struct { what string apply func() undo func() want string }{ {"a tree another user owns", func() { _ = os.Chown(tree, 65534, 65534) }, func() { _ = os.Chown(tree, 0, 0) }, "is not root's"}, {"a .git another user owns", func() { _ = os.Chown(tree+"/.git", 65534, 0) }, func() { _ = os.Chown(tree+"/.git", 0, 0) }, "is not root's"}, {"a ref deep in .git another user owns", func() { _ = os.Chown(tree+"/.git/HEAD", 65534, 0) }, func() { _ = os.Chown(tree+"/.git/HEAD", 0, 0) }, "is not root's"}, {"a config anyone can write", func() { _ = os.Chmod(tree+"/.git/config", 0o666) }, func() { _ = os.Chmod(tree+"/.git/config", 0o644) }, "written by anyone"}, {"a config another group can write", func() { _ = os.Chown(tree+"/.git/config", 0, 65534) _ = os.Chmod(tree+"/.git/config", 0o664) }, func() { _ = os.Chown(tree+"/.git/config", 0, 0) _ = os.Chmod(tree+"/.git/config", 0o644) }, "group other than root's"}, } { c.apply() if _, err := opTreePlan(ctx, line); err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("%s: want %q, got %v", c.what, c.want, err) } if _, err := opTreeDo(ctx, line, nil, ""); err == nil { t.Errorf("%s: the move ran", c.what) } c.undo() } // The machine's own ignored .env, which the target commit starts to // track: the move is refused and the file is untouched. _ = os.WriteFile(filepath.Join(tree, ".env"), []byte("LOCAL-SECRET\n"), 0o600) if _, err := opTreeDo(ctx, line, nil, ""); err == nil || !strings.Contains(err.Error(), "could not move the tree") { t.Fatalf("an ignored file in the way: %v", err) } if b, _ := os.ReadFile(filepath.Join(tree, ".env")); string(b) != "LOCAL-SECRET\n" { t.Fatalf("the ignored file was overwritten: %q", b) } } // The ancestry check k8s.set_image makes, run for real against a repository. func TestAncestryByFetchRefusesABackwardOrSidewaysCommit(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } setupBox(t) useFileRepos(t) origin, _, c1, c2 := twoCommitRepo(t, func(string) {}, func(o string) { _ = os.WriteFile(filepath.Join(o, "a.txt"), []byte("two\n"), 0o644) }) gitT(t, origin, "checkout", "-q", "-b", "side", c1) _ = os.WriteFile(filepath.Join(origin, "b.txt"), []byte("side\n"), 0o644) gitT(t, origin, "add", "b.txt") gitT(t, origin, "commit", "-qm", "side") side := gitT(t, origin, "rev-parse", "HEAD") gitT(t, origin, "checkout", "-q", "main") src := "file://" + origin if err := opAncestryByFetch(src, c1, c2, nil); err != nil { t.Fatalf("forward: %v", err) } if err := opAncestryByFetch(src, c2, c2, nil); err != nil { t.Fatalf("the same commit (a rebuild): %v", err) } for _, bad := range [][2]string{{c2, c1}, {c2, side}} { if err := opAncestryByFetch(src, bad[0], bad[1], nil); err == nil || !strings.Contains(err.Error(), "only moves forward") { t.Fatalf("%s → %s: %v", bad[0], bad[1], err) } } if err := opAncestryByFetch(src, c1, strings.Repeat("9", 40), nil); err == nil || !strings.Contains(err.Error(), "is not in") { t.Fatalf("a commit the repository does not have: %v", err) } } func TestAVerbsCredentialNeverReachesTheProgram(t *testing.T) { verbs := []opVerbLine{{Verb: "tree.checkout", Args: map[string]string{"credential": "forge-read"}}, {Verb: "unit.restart", Args: map[string]string{}}} got := opProgramCreds(verbs, map[string]string{"forge-read": "f", "registry": "r"}) if len(got) != 1 || got["registry"] != "r" { t.Fatalf("only the program's own credential: %v", got) } } // A tree may hold a folder another user owns (a service's workspace). A move // that would have git write into it, as root, is refused; one that leaves it // alone goes ahead. func TestTreeCheckoutNeverWritesIntoAFolderRootDoesNotControl(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } if os.Geteuid() != 0 { t.Skip("needs root to hand a folder to another user") } setupBox(t) useFileRepos(t) origin, tree, _, c2 := twoCommitRepo(t, func(o string) { _ = os.MkdirAll(filepath.Join(o, "workspace/deep"), 0o755) _ = os.WriteFile(filepath.Join(o, "workspace/deep/f.txt"), []byte("one\n"), 0o644) }, func(o string) { _ = os.WriteFile(filepath.Join(o, "workspace/deep/f.txt"), []byte("two\n"), 0o644) }) ctx := context.Background() line := func(c string) opVerbLine { return opVerbLine{Verb: "tree.checkout", Args: map[string]string{"path": tree, "repo": "file://" + origin, "commit": c}} } _ = os.Chown(filepath.Join(tree, "workspace"), 65534, 65534) if _, err := opTreeDo(ctx, line(c2), nil, ""); err == nil || !strings.Contains(err.Error(), "writes into workspace") { t.Fatalf("a move into another user's folder: %v", err) } if b, _ := os.ReadFile(filepath.Join(tree, "workspace/deep/f.txt")); string(b) != "one\n" { t.Fatal("the file was written") } // A folder whose name begins with a space is checked by its real name. gitT(t, tree, "reset", "-q", "--hard") _ = os.Chown(filepath.Join(tree, "workspace"), 0, 0) sp := " sp" _ = os.MkdirAll(filepath.Join(origin, sp), 0o755) _ = os.WriteFile(filepath.Join(origin, sp, "f.txt"), []byte("one\n"), 0o644) gitT(t, origin, "add", "-A") gitT(t, origin, "commit", "-qm", "space") cs1 := gitT(t, origin, "rev-parse", "HEAD") _ = os.WriteFile(filepath.Join(origin, sp, "f.txt"), []byte("two\n"), 0o644) gitT(t, origin, "commit", "-qam", "space two") cs2 := gitT(t, origin, "rev-parse", "HEAD") gitT(t, tree, "fetch", "-q", "origin") gitT(t, tree, "merge", "-q", "--ff-only", cs1) _ = os.Chown(filepath.Join(tree, sp), 65534, 65534) if err := opTreeWriteDirs(tree, cs1, cs2); err == nil || !strings.Contains(err.Error(), "writes into sp") { t.Fatalf("a folder named with a leading space: %v", err) } _ = os.Chown(filepath.Join(tree, sp), 0, 0) // A commit that changes only a.txt goes ahead with that folder there. _ = os.WriteFile(filepath.Join(origin, "a.txt"), []byte("three\n"), 0o644) gitT(t, origin, "commit", "-qam", "three") c3 := gitT(t, origin, "rev-parse", "HEAD") gitT(t, tree, "fetch", "-q", "origin") gitT(t, tree, "merge", "-q", "--ff-only", c2) // as a person at the machine would have _ = os.Chown(filepath.Join(tree, "workspace"), 65534, 65534) if _, err := opTreeDo(ctx, line(c3), nil, ""); err != nil { t.Fatalf("a move that leaves the folder alone: %v", err) } } // Every command a verb runs has a deadline, so one that never answers // cannot hold its target's lock for good. func TestEveryCommandAVerbRunsHasADeadline(t *testing.T) { for _, c := range []struct { name string args []string want time.Duration }{ {"kubectl", []string{"-n", "ds", "get", "deployment", "web"}, time.Minute}, {"kubectl", []string{"-n", "ds", "set", "image", "deployment/web", "web=x"}, time.Minute}, {"kubectl", []string{"-n", "ds", "rollout", "status", "deployment/web", "--timeout=5m"}, 6 * time.Minute}, {"systemctl", []string{"restart", "a.service"}, 6 * time.Minute}, {"journalctl", []string{"-u", "a.service"}, time.Minute}, } { if got := opCmdDeadline(c.name, c.args); got != c.want { t.Errorf("%s %v: %v, want %v", c.name, c.args, got, c.want) } } prev := opCmdDeadline t.Cleanup(func() { opCmdDeadline = prev }) opCmdDeadline = func(string, []string) time.Duration { return 200 * time.Millisecond } start := time.Now() if _, err := opCmd(context.Background(), "sleep", "30"); err == nil || time.Since(start) > 10*time.Second { t.Fatalf("a command past its deadline: %v after %v", err, time.Since(start)) } }