984 lines
42 KiB
Go
984 lines
42 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// holdActionOp puts an action op in the pairing's store, as op_sync would.
|
|
func holdActionOp(t *testing.T, dcn, man, program string) (string, *opManifest) {
|
|
t.Helper()
|
|
m, err := parseOpManifest([]byte(man))
|
|
if err != nil {
|
|
t.Fatalf("%v\n%s", err, man)
|
|
}
|
|
files := []opFile{
|
|
{Path: "op.yaml", Mode: 0o644, B64: base64.StdEncoding.EncodeToString([]byte(man))},
|
|
{Path: "run", Mode: 0o755, B64: base64.StdEncoding.EncodeToString([]byte("#!/bin/sh\n" + program + "\n"))},
|
|
}
|
|
sha, err := opDirDigest(files)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := opInstall(dcn, sha, files); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return sha, m
|
|
}
|
|
|
|
// A whole ship, for real, on this machine: a build whose receipt carries the
|
|
// digest, a root roll that takes it from that receipt (adopting the live image
|
|
// as the first roll), and a check that fails; the failed check's receipt
|
|
// undoes the roll, to the image that was live before it.
|
|
func TestAShipPlanRunsEachLineOnceAndUndoesOnAFailedCheck(t *testing.T) {
|
|
requireActionSandbox(t)
|
|
dcn := planBox(t)
|
|
useAncestry(t, func(_, _, _ string, _ []string) error { return nil })
|
|
commit := strings.Repeat("7", 40)
|
|
digest := "sha256:" + strings.Repeat("5", 64)
|
|
old := tImage + "@sha256:" + strings.Repeat("4", 64)
|
|
f := &fakeKube{containers: map[string]string{"bridge": old}}
|
|
useFakeKube(t, f)
|
|
|
|
head := `version: 1.0.0
|
|
summary: A step of the ship under test.
|
|
kind: action
|
|
target: machine
|
|
interpreter: /bin/sh
|
|
time_limit: 30
|
|
`
|
|
res := fmt.Sprintf(`{"images":[{"image":"%s","digest":"%s","source":"%s","commit":"%s"}]}`, tImage, digest, tRepo, commit)
|
|
buildSHA, _ := holdActionOp(t, dcn, "name: bridge-build\n"+head+"privilege: user\nparams:\n commit: {type: string, max_len: 40}\nmodes: [dry_run, apply, verify]\nundo: none\n",
|
|
`[ "$PRIVATAE_OP_MODE" = apply ] && printf '%s' '`+res+`' > "$PRIVATAE_OP_RESULT"; true`)
|
|
rollSHA, rollM := holdActionOp(t, dcn, "name: bridge-roll\n"+head+"privilege: root\nparams:\n digest: {type: string, max_len: 71}\n"+
|
|
"verbs:\n - {verb: k8s.set_image, namespace: ds, kind: deployment, name: bridge, container: bridge, image: "+tImage+", digest: \"{{digest}}\", repo: \""+tRepo+"\"}\n"+
|
|
"modes: [dry_run, apply, verify]\nundo: none\n", "true")
|
|
// The check: its apply succeeds, its verify (the probe) fails — a failed
|
|
// CHECK, which is what may start an undo of an earlier line.
|
|
checkSHA, _ := holdActionOp(t, dcn, "name: bridge-check\n"+head+"privilege: user\nparams: {}\nmodes: [dry_run, apply, verify]\nundo: none\n",
|
|
`[ "$PRIVATAE_OP_MODE" = verify ] && { echo "the bridge does not answer"; exit 1; }; true`)
|
|
|
|
hn := planHost()
|
|
rollLine := shipPlanLine{N: 2, Step: "roll", Machine: hn, Op: "bridge-roll", OpSHA256: rollSHA, Params: `{}`,
|
|
Inputs: map[string]string{"digest": "build.digest"}, Undo: true, AdoptLive: true}
|
|
rollLine.Verbs = planVerbsForShow(rollM, &rollLine)
|
|
draft := map[string]any{"kind": shipPlanKind, "plan": map[string]any{
|
|
"pipeline": "bridge", "commit": commit, "expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339),
|
|
"lines": []any{
|
|
shipPlanLine{N: 1, Step: "build", Machine: hn, Op: "bridge-build", OpSHA256: buildSHA, Params: fmt.Sprintf(`{"commit":"%s"}`, commit)},
|
|
rollLine,
|
|
shipPlanLine{N: 3, Step: "check", Machine: hn, Op: "bridge-check", OpSHA256: checkSHA, Params: `{}`},
|
|
}}}
|
|
// Through JSON, as the pod sends it.
|
|
raw, _ := json.Marshal(draft)
|
|
var details map[string]any
|
|
_ = json.Unmarshal(raw, &details)
|
|
b64, p := signPlan(t, dcn, details, planCode(t))
|
|
if !p.Root || !p.CodeGiven {
|
|
t.Fatal("a plan with a roll is root, approved with the code")
|
|
}
|
|
encode := func(rs ...any) string {
|
|
b, _ := json.Marshal(rs)
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
receiptOf := func(out map[string]any) opSignedReceipt {
|
|
return out["receipt"].(opSignedReceipt)
|
|
}
|
|
|
|
// 1. The build: its receipt names the image it pushed, at the plan's commit.
|
|
out1, err := planRunLine(dcn, b64, "1", "", nil)
|
|
if err != nil || out1["ok"] != true {
|
|
t.Fatalf("the build ran: %v %v", out1["error"], err)
|
|
}
|
|
r1 := receiptOf(out1)
|
|
// The roll cannot run before the build's receipt is handed to it...
|
|
if _, err := planRunLine(dcn, b64, "2", "", nil); err == nil || !strings.Contains(err.Error(), "runs only after line 1") {
|
|
t.Fatalf("the roll before its build: %v", err)
|
|
}
|
|
// 2. ...and with it, rolls exactly the digest the build reported.
|
|
out2, err := planRunLine(dcn, b64, "2", encode(r1), nil)
|
|
if err != nil || out2["ok"] != true {
|
|
t.Fatalf("the roll ran: %v %v", out2["error"], err)
|
|
}
|
|
if f.containers["bridge"] != tImage+"@"+digest {
|
|
t.Fatalf("the workload runs the built digest: %s", f.containers["bridge"])
|
|
}
|
|
if _, _, c, ok := opRolledCommit(digest); !ok || c != commit {
|
|
t.Fatal("the roll is recorded, so the next plan moves forward from it")
|
|
}
|
|
// A replay of the same line is refused: it succeeded, so no attempt is left
|
|
// to it, whatever the plan allowed.
|
|
if _, err := planRunLine(dcn, b64, "2", encode(r1), nil); err == nil || !strings.Contains(err.Error(), "already succeeded") {
|
|
t.Fatalf("a replayed line: %v", err)
|
|
}
|
|
// 3. The check (after the build and the roll, their receipts with it)
|
|
// fails, and says so in its signed receipt.
|
|
r2 := receiptOf(out2)
|
|
out3, err := planRunLine(dcn, b64, "3", encode(r1, r2), nil)
|
|
if err != nil || out3["ok"] != true || !strings.Contains(fmt.Sprint(out3["verify_error"]), "verify exited 1") {
|
|
t.Fatalf("the check ran and its verify failed: %v %v", out3, err)
|
|
}
|
|
// The pod that lost an answer reads it back, and nothing is spent.
|
|
if path, _ := planReceiptsPath(p.Nonce, 3); path != "" {
|
|
raw, _ := os.ReadFile(path)
|
|
var kept []opSignedReceipt
|
|
_ = json.Unmarshal(raw, &kept)
|
|
if len(kept) != 1 {
|
|
t.Fatalf("line 3's receipt is kept for op_plan_status: %d", len(kept))
|
|
}
|
|
}
|
|
failed, _ := json.Marshal(receiptOf(out3))
|
|
// The failed check undoes the roll, to the image live before it.
|
|
undo, err := planUndoLine(b64, "2", base64.StdEncoding.EncodeToString(failed))
|
|
if err != nil || undo["ok"] != true {
|
|
t.Fatalf("the undo ran: %v %v", undo, err)
|
|
}
|
|
if f.containers["bridge"] != old {
|
|
t.Fatalf("the workload is back on %s, got %s", old, f.containers["bridge"])
|
|
}
|
|
path, _ := planRunPath(p.Nonce, 2)
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatal("the roll's record is kept for its undo")
|
|
}
|
|
// A plan whose roll names a digest no step of it built (bound in its
|
|
// params, not taken from a receipt) rolls nothing.
|
|
foreign := "sha256:" + strings.Repeat("6", 64)
|
|
stray := shipPlanLine{N: 1, Step: "roll", Machine: hn, Op: "bridge-roll", OpSHA256: rollSHA,
|
|
Params: fmt.Sprintf(`{"digest":"%s"}`, foreign), AdoptLive: true}
|
|
stray.Verbs = planVerbsForShow(rollM, &stray)
|
|
raw, _ = json.Marshal(map[string]any{"kind": shipPlanKind, "plan": map[string]any{
|
|
"pipeline": "bridge", "commit": commit, "expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339),
|
|
"lines": []any{stray}}})
|
|
details = nil
|
|
_ = json.Unmarshal(raw, &details)
|
|
b64b, _ := signPlan(t, dcn, details, planCodeAt(1)) // the first code is spent
|
|
outS, err := planRunLine(dcn, b64b, "1", "", nil)
|
|
if err != nil || outS["ok"] != false || !strings.Contains(fmt.Sprint(outS["error"]), "no step of this plan built") {
|
|
t.Fatalf("a digest no step of the plan built: %v %v", outS["error"], err)
|
|
}
|
|
if f.containers["bridge"] != old {
|
|
t.Fatalf("nothing was rolled: %s", f.containers["bridge"])
|
|
}
|
|
}
|
|
|
|
// An undo takes back only what the line left: a workload or tree that moved
|
|
// since is someone else's change, and is left alone.
|
|
func TestAnUndoLeavesWhatMovedSinceAlone(t *testing.T) {
|
|
setupBox(t)
|
|
was, set, other := tImage+"@sha256:"+strings.Repeat("1", 64), tImage+"@sha256:"+strings.Repeat("2", 64), tImage+"@sha256:"+strings.Repeat("3", 64)
|
|
f := &fakeKube{containers: map[string]string{"bridge": other}}
|
|
useFakeKube(t, f)
|
|
v := setImageLine("sha256:" + strings.Repeat("2", 64))
|
|
if _, err := planUndoVerb(context.Background(), v, was, set); err == nil || !strings.Contains(err.Error(), "moved since") {
|
|
t.Fatalf("a workload that moved since: %v", err)
|
|
}
|
|
if len(f.sets()) != 0 {
|
|
t.Fatal("it was set anyway")
|
|
}
|
|
f.containers["bridge"] = was
|
|
if note, err := planUndoVerb(context.Background(), v, was, set); err != nil || !strings.Contains(note, "already") {
|
|
t.Fatalf("already back where it started: %q %v", note, err)
|
|
}
|
|
f.containers["bridge"] = set
|
|
if _, err := planUndoVerb(context.Background(), v, was, set); err != nil || f.containers["bridge"] != was {
|
|
t.Fatalf("what the line left is taken back: %v %s", err, f.containers["bridge"])
|
|
}
|
|
}
|
|
|
|
// An undo puts the state back first and only then restarts the unit, so the
|
|
// unit starts on what was put back, never on the failed code.
|
|
func TestAnUndoRestartsTheUnitOnlyAfterPuttingTheStateBack(t *testing.T) {
|
|
dcn := planBox(t)
|
|
was, set := tImage+"@sha256:"+strings.Repeat("1", 64), tImage+"@sha256:"+strings.Repeat("2", 64)
|
|
var order []string
|
|
f := &fakeKube{containers: map[string]string{"bridge": set}}
|
|
prevK := opKubectl
|
|
opKubectl = func(ctx context.Context, args ...string) (string, error) {
|
|
if len(args) >= 3 && args[2] == "set" {
|
|
order = append(order, "set image")
|
|
}
|
|
return f.run(ctx, args...)
|
|
}
|
|
prevS := opSystemctl
|
|
opSystemctl = func(_ context.Context, args ...string) (string, error) {
|
|
if args[0] == "show" {
|
|
return "LoadState=loaded\nActiveState=active\nSubState=running\nMainPID=1\n", nil
|
|
}
|
|
order = append(order, "systemctl "+args[0])
|
|
return "", nil
|
|
}
|
|
t.Cleanup(func() { opKubectl, opSystemctl = prevK, prevS })
|
|
d := planDraft(false)
|
|
lines := d["plan"].(map[string]any)["lines"].([]any)
|
|
lines = append(lines, map[string]any{"n": 3, "step": "check", "machine": planHost(), "op": "bridge-check",
|
|
"op_sha256": strings.Repeat("e", 64), "params": `{}`})
|
|
d["plan"].(map[string]any)["lines"] = lines
|
|
b64, p := signPlan(t, dcn, d, "")
|
|
raw, _ := base64.StdEncoding.DecodeString(b64)
|
|
var e shipPlanEnvelope
|
|
_ = json.Unmarshal(raw, &e)
|
|
_, sha, _ := planOpen(e)
|
|
// Line 2 set the image, then restarted the unit.
|
|
rec := planRunRecord{Plan: p.Nonce, N: 2, Attempt: 1, State: planStatePassed,
|
|
Verbs: []opVerbLine{setImageLine("sha256:" + strings.Repeat("2", 64)), {Verb: "unit.restart", Args: map[string]string{"unit": "app.service"}}},
|
|
Froms: []string{was, ""}, Tos: []string{set, ""}}
|
|
path, _ := planRunPath(p.Nonce, 2)
|
|
body, _ := json.Marshal(rec)
|
|
_ = os.WriteFile(path, body, 0o600)
|
|
failed, _ := lineReceiptSign(lineReceipt{Plan: p.Nonce, PlanSHA256: sha, Pipeline: p.Pipeline, Commit: p.Commit,
|
|
N: 3, Step: "check", Machine: planHost(), OK: true, VerifyError: "verify exited 1", Attempt: 1})
|
|
fb, _ := json.Marshal(failed)
|
|
out, err := planUndoLine(b64, "2", base64.StdEncoding.EncodeToString(fb))
|
|
if err != nil || out["ok"] != true {
|
|
t.Fatalf("the undo ran: %v %v", out, err)
|
|
}
|
|
if strings.Join(order, ", ") != "set image, systemctl restart" {
|
|
t.Fatalf("the image goes back before the unit restarts: %v", order)
|
|
}
|
|
if f.containers["bridge"] != was {
|
|
t.Fatal("the image is back")
|
|
}
|
|
}
|
|
|
|
// Moving a tree back never overwrites a file the machine keeps.
|
|
func TestAnUndoOfATreeNeverOverwritesAnIgnoredFile(t *testing.T) {
|
|
if os.Geteuid() != 0 {
|
|
t.Skip("needs root for the tree's ownership checks")
|
|
}
|
|
// A name is matched as itself, never as a pathspec: ":(literal)k.env"
|
|
// read as one names k.env, and would miss the file.
|
|
for _, name := range []string{".env", "[ab].env", ":(literal)k.env"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
setupBox(t)
|
|
useFileRepos(t)
|
|
// c1 tracks the file; c2 stops tracking it (it is ignored from then on).
|
|
origin, tree, c1, c2 := twoCommitRepo(t, func(o string) {
|
|
_ = os.WriteFile(filepath.Join(o, name), []byte("FROM-REPO\n"), 0o644)
|
|
_ = os.WriteFile(filepath.Join(o, ".gitignore"), []byte("*.env\n"), 0o644)
|
|
gitT(t, o, "--literal-pathspecs", "add", "-f", "--", name)
|
|
}, func(o string) {
|
|
gitT(t, o, "--literal-pathspecs", "rm", "-q", "--cached", "--", name)
|
|
_ = 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}}
|
|
if _, err := opTreeDo(context.Background(), line, nil, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = os.WriteFile(filepath.Join(tree, name), []byte("LOCAL-SECRET\n"), 0o600)
|
|
if _, err := planUndoVerb(context.Background(), line, c1, c2); err == nil || !strings.Contains(err.Error(), "a file this machine keeps") {
|
|
t.Fatalf("an undo over an ignored file: %v", err)
|
|
}
|
|
if b, _ := os.ReadFile(filepath.Join(tree, name)); string(b) != "LOCAL-SECRET\n" {
|
|
t.Fatalf("the kept file was overwritten: %q", b)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// shipRig is a three-line ship on this machine: build (its receipt carries
|
|
// the digest), roll (root, sets the image, undoable, two attempts), check.
|
|
// rollProgram and checkProgram are the roll's and the check's programs.
|
|
func shipRig(t *testing.T, buildProgram, rollProgram, checkProgram string, checkAttempts int) (dcn, b64 string, f *fakeKube, old, built string) {
|
|
t.Helper()
|
|
requireActionSandbox(t)
|
|
dcn = planBox(t)
|
|
useAncestry(t, func(_, _, _ string, _ []string) error { return nil })
|
|
commit := strings.Repeat("7", 40)
|
|
digest := "sha256:" + strings.Repeat("5", 64)
|
|
old = tImage + "@sha256:" + strings.Repeat("4", 64)
|
|
built = tImage + "@" + digest
|
|
f = &fakeKube{containers: map[string]string{"bridge": old}}
|
|
useFakeKube(t, f)
|
|
head := "version: 1.0.0\nsummary: A step of the ship under test.\nkind: action\ntarget: machine\ninterpreter: /bin/sh\ntime_limit: 30\n"
|
|
res := fmt.Sprintf(`{"images":[{"image":"%s","digest":"%s","source":"%s","commit":"%s"}]}`, tImage, digest, tRepo, commit)
|
|
if buildProgram == "" {
|
|
buildProgram = `[ "$PRIVATAE_OP_MODE" = apply ] && printf '%s' '` + res + `' > "$PRIVATAE_OP_RESULT"; true`
|
|
} else {
|
|
buildProgram = strings.ReplaceAll(buildProgram, "RESULT", res)
|
|
}
|
|
buildSHA, _ := holdActionOp(t, dcn, "name: bridge-build\n"+head+"privilege: user\nparams:\n commit: {type: string, max_len: 40}\nmodes: [dry_run, apply, verify]\nundo: none\n", buildProgram)
|
|
rollSHA, rollM := holdActionOp(t, dcn, "name: bridge-roll\n"+head+"privilege: root\nparams:\n digest: {type: string, max_len: 71}\n"+
|
|
"verbs:\n - {verb: k8s.set_image, namespace: ds, kind: deployment, name: bridge, container: bridge, image: "+tImage+", digest: \"{{digest}}\", repo: \""+tRepo+"\"}\n"+
|
|
"modes: [dry_run, apply, verify]\nundo: none\n", rollProgram)
|
|
checkSHA, _ := holdActionOp(t, dcn, "name: bridge-check\n"+head+"privilege: user\nparams: {}\nmodes: [dry_run, apply, verify]\nundo: none\n", checkProgram)
|
|
hn := planHost()
|
|
rollLine := shipPlanLine{N: 2, Step: "roll", Machine: hn, Op: "bridge-roll", OpSHA256: rollSHA, Params: `{}`,
|
|
Inputs: map[string]string{"digest": "build.digest"}, Undo: true, AdoptLive: true, Attempts: 2}
|
|
rollLine.Verbs = planVerbsForShow(rollM, &rollLine)
|
|
draft := map[string]any{"kind": shipPlanKind, "plan": map[string]any{
|
|
"pipeline": "bridge", "commit": commit, "expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339),
|
|
"lines": []any{
|
|
shipPlanLine{N: 1, Step: "build", Machine: hn, Op: "bridge-build", OpSHA256: buildSHA, Params: fmt.Sprintf(`{"commit":"%s"}`, commit), Attempts: 2},
|
|
rollLine,
|
|
shipPlanLine{N: 3, Step: "check", Machine: hn, Op: "bridge-check", OpSHA256: checkSHA, Params: `{}`, Attempts: checkAttempts},
|
|
}}}
|
|
raw, _ := json.Marshal(draft)
|
|
var details map[string]any
|
|
_ = json.Unmarshal(raw, &details)
|
|
b64, _ = signPlan(t, dcn, details, planCode(t))
|
|
return dcn, b64, f, old, built
|
|
}
|
|
|
|
func rigReceipts(outs ...map[string]any) string {
|
|
var rs []opSignedReceipt
|
|
for _, o := range outs {
|
|
rs = append(rs, o["receipt"].(opSignedReceipt))
|
|
}
|
|
b, _ := json.Marshal(rs)
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
|
|
func rigReceipt(o map[string]any) string {
|
|
b, _ := json.Marshal(o["receipt"].(opSignedReceipt))
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
|
|
// failKubeAt makes the listed kubectl calls of a kind (set, rollout) fail,
|
|
// counted from 1.
|
|
func failKubeAt(t *testing.T, kind string, calls ...int) {
|
|
t.Helper()
|
|
count := 0
|
|
prev := opKubectl
|
|
opKubectl = func(ctx context.Context, args ...string) (string, error) {
|
|
if len(args) >= 3 && args[2] == kind {
|
|
count++
|
|
for _, n := range calls {
|
|
if count == n {
|
|
return "boom", fmt.Errorf("exit 1")
|
|
}
|
|
}
|
|
}
|
|
return prev(ctx, args...)
|
|
}
|
|
t.Cleanup(func() { opKubectl = prev })
|
|
}
|
|
|
|
// Two calls for the same line at once: the second is refused while the first
|
|
// runs, and once the first passed nothing runs the line again.
|
|
func TestALineRunsOneAttemptAtATime(t *testing.T) {
|
|
dcn, b64, _, _, _ := shipRig(t, `[ "$PRIVATAE_OP_MODE" = apply ] && { sleep 3; printf '%s' 'RESULT' > "$PRIVATAE_OP_RESULT"; }; true`, "true", "true", 1)
|
|
type res struct {
|
|
out map[string]any
|
|
err error
|
|
}
|
|
first := make(chan res, 1)
|
|
go func() {
|
|
out, err := planRunLine(dcn, b64, "1", "", nil)
|
|
first <- res{out, err}
|
|
}()
|
|
time.Sleep(700 * time.Millisecond)
|
|
if _, err := planRunLine(dcn, b64, "1", "", nil); err == nil || !strings.Contains(err.Error(), "attempt 1 of line 1 is running here") {
|
|
t.Fatalf("a second attempt while the first runs: %v", err)
|
|
}
|
|
r := <-first
|
|
if r.err != nil || r.out["ok"] != true {
|
|
t.Fatalf("the first attempt: %v %v", r.out, r.err)
|
|
}
|
|
if _, err := planRunLine(dcn, b64, "1", "", nil); err == nil || !strings.Contains(err.Error(), "already succeeded") {
|
|
t.Fatalf("an attempt after one passed: %v", err)
|
|
}
|
|
}
|
|
|
|
// A roll whose first attempt set the image and failed, and whose retry
|
|
// passed; the check then fails: the undo goes back to the image before the
|
|
// FIRST attempt, not to where the failed attempt left it — and the line, once
|
|
// undone, never runs again.
|
|
func TestAnUndoAfterARetryGoesBackToBeforeTheFirstAttempt(t *testing.T) {
|
|
dcn, b64, f, old, built := shipRig(t, "", "true", `[ "$PRIVATAE_OP_MODE" = verify ] && { echo "the bridge does not answer"; exit 1; }; true`, 1)
|
|
failKubeAt(t, "rollout", 1)
|
|
b1, err := planRunLine(dcn, b64, "1", "", nil)
|
|
if err != nil || b1["ok"] != true {
|
|
t.Fatalf("build: %v %v", b1, err)
|
|
}
|
|
a1, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || a1["ok"] != false || f.containers["bridge"] != built {
|
|
t.Fatalf("roll attempt 1 sets the image and fails its rollout: %v %v %s", a1, err, f.containers["bridge"])
|
|
}
|
|
a2, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || a2["ok"] != true {
|
|
t.Fatalf("roll attempt 2: %v %v", a2, err)
|
|
}
|
|
c, err := planRunLine(dcn, b64, "3", rigReceipts(b1, a2), nil)
|
|
if err != nil || c["verify_error"] == "" {
|
|
t.Fatalf("the check fails: %v %v", c, err)
|
|
}
|
|
u, err := planUndoLine(b64, "2", rigReceipt(c))
|
|
if err != nil || u["ok"] != true {
|
|
t.Fatalf("the undo: %v %v", u, err)
|
|
}
|
|
if f.containers["bridge"] != old {
|
|
t.Fatalf("the undo went back to %s, not to %s from before the first attempt", f.containers["bridge"], old)
|
|
}
|
|
if _, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil); err == nil || !strings.Contains(err.Error(), "was undone") {
|
|
t.Fatalf("a line run after its undo: %v", err)
|
|
}
|
|
}
|
|
|
|
// Only the latest attempt's failure undoes the line itself, and never once an
|
|
// attempt passed.
|
|
func TestOnlyTheLatestAttemptsFailureUndoesTheLine(t *testing.T) {
|
|
t.Run("an attempt passed after it", func(t *testing.T) {
|
|
dcn, b64, f, _, built := shipRig(t, "", "true", "true", 1)
|
|
failKubeAt(t, "set", 1)
|
|
b1, _ := planRunLine(dcn, b64, "1", "", nil)
|
|
a1, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || a1["ok"] != false {
|
|
t.Fatalf("roll attempt 1 fails: %v %v", a1, err)
|
|
}
|
|
a2, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || a2["ok"] != true {
|
|
t.Fatalf("roll attempt 2 passes: %v %v", a2, err)
|
|
}
|
|
if _, err := planUndoLine(b64, "2", rigReceipt(a1)); err == nil || !strings.Contains(err.Error(), "passed here") {
|
|
t.Fatalf("attempt 1's failure after attempt 2 passed: %v", err)
|
|
}
|
|
if f.containers["bridge"] != built {
|
|
t.Fatal("the shipped image stays")
|
|
}
|
|
})
|
|
t.Run("both attempts failed", func(t *testing.T) {
|
|
dcn, b64, f, old, built := shipRig(t, "", "true", "true", 1)
|
|
failKubeAt(t, "rollout", 1, 2)
|
|
b1, _ := planRunLine(dcn, b64, "1", "", nil)
|
|
a1, _ := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
a2, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || a1["ok"] != false || a2["ok"] != false || f.containers["bridge"] != built {
|
|
t.Fatalf("both attempts fail with the image set: %v %v %v", a1, a2, err)
|
|
}
|
|
if _, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil); err == nil || !strings.Contains(err.Error(), "all it may") {
|
|
t.Fatalf("a third attempt of two: %v", err)
|
|
}
|
|
if _, err := planUndoLine(b64, "2", rigReceipt(a1)); err == nil || !strings.Contains(err.Error(), "latest attempt here is 2") {
|
|
t.Fatalf("an earlier attempt's failure: %v", err)
|
|
}
|
|
u, err := planUndoLine(b64, "2", rigReceipt(a2))
|
|
if err != nil || u["ok"] != true || f.containers["bridge"] != old {
|
|
t.Fatalf("the latest attempt's failure undoes to before the first: %v %v %s", u, err, f.containers["bridge"])
|
|
}
|
|
})
|
|
}
|
|
|
|
// A later line's failed check undoes an earlier line only on its last
|
|
// allowed attempt: a retry of the check may still pass.
|
|
func TestALaterLinesCheckUndoesOnlyOnItsLastAttempt(t *testing.T) {
|
|
dcn, b64, f, old, _ := shipRig(t, "", "true", `[ "$PRIVATAE_OP_MODE" = verify ] && exit 1; true`, 2)
|
|
b1, _ := planRunLine(dcn, b64, "1", "", nil)
|
|
r1, _ := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
c1, err := planRunLine(dcn, b64, "3", rigReceipts(b1, r1), nil)
|
|
if err != nil || c1["verify_error"] == "" {
|
|
t.Fatalf("check attempt 1 fails: %v %v", c1, err)
|
|
}
|
|
if _, err := planUndoLine(b64, "2", rigReceipt(c1)); err == nil || !strings.Contains(err.Error(), "only its last attempt") {
|
|
t.Fatalf("attempt 1 of 2 of the check: %v", err)
|
|
}
|
|
c2, err := planRunLine(dcn, b64, "3", rigReceipts(b1, r1), nil)
|
|
if err != nil || c2["verify_error"] == "" {
|
|
t.Fatalf("check attempt 2 fails: %v %v", c2, err)
|
|
}
|
|
if u, err := planUndoLine(b64, "2", rigReceipt(c2)); err != nil || u["ok"] != true || f.containers["bridge"] != old {
|
|
t.Fatalf("the last attempt's failed check undoes the roll: %v %v", u, err)
|
|
}
|
|
}
|
|
|
|
// A verb the machine cannot record for its undo does not run.
|
|
func TestAVerbThatCannotBeRecordedDoesNotRun(t *testing.T) {
|
|
dcn, b64, f, old, _ := shipRig(t, "", `[ "$PRIVATAE_OP_MODE" = apply ] && sleep 2; true`, "true", 1)
|
|
b1, _ := planRunLine(dcn, b64, "1", "", nil)
|
|
p, _, _, _ := planDecodeArgs([]string{b64, "2"})
|
|
go func() {
|
|
// While the roll's program runs, the record stops being this attempt's.
|
|
time.Sleep(700 * time.Millisecond)
|
|
planLineMu.Lock()
|
|
defer planLineMu.Unlock()
|
|
rec, _ := planLoadRun(p.Nonce, 2)
|
|
rec.Attempt = 9
|
|
_ = planSaveRun(rec)
|
|
}()
|
|
r, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || r["ok"] != false || !strings.Contains(fmt.Sprint(r["error"]), "could not be recorded") {
|
|
t.Fatalf("the roll: %v %v", r, err)
|
|
}
|
|
if f.containers["bridge"] != old {
|
|
t.Fatal("the image was set with no record of it")
|
|
}
|
|
}
|
|
|
|
// An attempt the bastion died in: settled at startup from its kept receipt,
|
|
// or, with none, as failed with a signed receipt saying so; the next attempt
|
|
// may then run, and the pod reads the settled receipt.
|
|
func TestAnAttemptTheBastionDiedInIsSettled(t *testing.T) {
|
|
dcn := planBox(t)
|
|
b64, p := signPlan(t, dcn, planDraft(false), "")
|
|
raw, _ := base64.StdEncoding.DecodeString(b64)
|
|
var e shipPlanEnvelope
|
|
_ = json.Unmarshal(raw, &e)
|
|
_, sha, _ := planOpen(e)
|
|
l := &p.Lines[1]
|
|
l.Attempts = 3
|
|
tmpl := lineReceipt{Plan: p.Nonce, PlanSHA256: sha, Pipeline: p.Pipeline, Commit: p.Commit, N: 2, Step: l.Step, Machine: planHost()}
|
|
k, err := planBeginAttempt(p, l, tmpl)
|
|
if err != nil || k != 1 {
|
|
t.Fatalf("attempt 1: %d %v", k, err)
|
|
}
|
|
if _, err := planBeginAttempt(p, l, tmpl); err == nil || !strings.Contains(err.Error(), "is running here") {
|
|
t.Fatalf("a second attempt while this process runs the first: %v", err)
|
|
}
|
|
died := func() {
|
|
planLineMu.Lock()
|
|
defer planLineMu.Unlock()
|
|
rec, _ := planLoadRun(p.Nonce, 2)
|
|
rec.Proc = "gone:1:1"
|
|
_ = planSaveRun(rec)
|
|
}
|
|
died()
|
|
planSettleStopped()
|
|
got := planKeptReceipt(p.Nonce, 2, 1)
|
|
if got == nil || !got.failed() || !strings.Contains(got.Error, "stopped when the bastion") {
|
|
t.Fatalf("the stopped attempt's receipt: %+v", got)
|
|
}
|
|
if k, err := planBeginAttempt(p, l, tmpl); err != nil || k != 2 {
|
|
t.Fatalf("attempt 2 after a settled attempt 1: %d %v", k, err)
|
|
}
|
|
// Attempt 2 passed and its receipt was kept, and the process died before
|
|
// it recorded the end: settled as passed, from the receipt.
|
|
pass := tmpl
|
|
pass.Attempt, pass.OK = 2, true
|
|
s, _ := lineReceiptSign(pass)
|
|
if err := planKeepReceipt(p.Nonce, 2, s); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
died()
|
|
if _, err := planBeginAttempt(p, l, tmpl); err == nil || !strings.Contains(err.Error(), "already succeeded") {
|
|
t.Fatalf("an attempt after a passed one the bastion died before recording: %v", err)
|
|
}
|
|
}
|
|
|
|
// The undo's rules against the line's record, each on its own.
|
|
func TestAnUndoMatchesTheReceiptToTheLinesRecord(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
rec planRunRecord
|
|
fr lineReceipt
|
|
later bool
|
|
want string
|
|
}{
|
|
{"while an attempt runs", planRunRecord{N: 2, Attempt: 1, State: planStateRunning}, lineReceipt{N: 3, Attempt: 1, OK: true, VerifyError: "x"}, true, "running here"},
|
|
{"twice", planRunRecord{N: 2, Attempt: 1, State: planStateUndone}, lineReceipt{N: 2, Attempt: 1}, false, "undone here already"},
|
|
{"a later failure of a line that did not pass", planRunRecord{N: 2, Attempt: 2, State: planStateFailed}, lineReceipt{N: 3, Attempt: 1, OK: true, VerifyError: "x"}, true, "did not pass here"},
|
|
{"its own failure once an attempt passed", planRunRecord{N: 2, Attempt: 2, State: planStatePassed}, lineReceipt{N: 2, Attempt: 2}, false, "passed here"},
|
|
{"an earlier attempt's failure", planRunRecord{N: 2, Attempt: 2, State: planStateFailed}, lineReceipt{N: 2, Attempt: 1}, false, "latest attempt here is 2"},
|
|
}
|
|
for _, c := range cases {
|
|
if err := planUndoAllowed(&c.rec, &c.fr, c.later); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: %v", c.name, err)
|
|
}
|
|
}
|
|
if err := planUndoAllowed(&planRunRecord{N: 2, Attempt: 2, State: planStateFailed}, &lineReceipt{N: 2, Attempt: 2}, false); err != nil {
|
|
t.Errorf("the latest attempt's failure: %v", err)
|
|
}
|
|
if err := planUndoAllowed(&planRunRecord{N: 2, Attempt: 1, State: planStatePassed}, &lineReceipt{N: 3, Attempt: 1, OK: true, VerifyError: "x"}, true); err != nil {
|
|
t.Errorf("a later failure of a line that passed: %v", err)
|
|
}
|
|
}
|
|
|
|
// An attempt the spent ledger and the record disagree on does not run, and a
|
|
// line's record outlives its plan only by planKeepAfterExpiry.
|
|
func TestALinesRecordAndLedgerAgreeAndAreLetGo(t *testing.T) {
|
|
dcn := planBox(t)
|
|
_, p := signPlan(t, dcn, planDraft(false), "")
|
|
l := &p.Lines[1]
|
|
l.Attempts = 2
|
|
if _, err := planSpendAttempt(p, l); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := planBeginAttempt(p, l, lineReceipt{}); err == nil || !strings.Contains(err.Error(), "out of order") {
|
|
t.Fatalf("an attempt spent with no record of it: %v", err)
|
|
}
|
|
keep := func(nonce, exp string) (string, string) {
|
|
rec := &planRunRecord{Plan: nonce, N: 1, Attempt: 1, State: planStatePassed, Expires: exp}
|
|
if err := planSaveRun(rec); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = planKeepReceipt(nonce, 1, opSignedReceipt{})
|
|
a, _ := planRunPath(nonce, 1)
|
|
b, _ := planReceiptsPath(nonce, 1)
|
|
return a, b
|
|
}
|
|
oldRec, oldRcpt := keep("oldplan", time.Now().Add(-planKeepAfterExpiry-time.Hour).UTC().Format(time.RFC3339))
|
|
newRec, newRcpt := keep("newplan", time.Now().Add(-time.Hour).UTC().Format(time.RFC3339))
|
|
planSettleStopped()
|
|
for _, f := range []string{oldRec, oldRcpt} {
|
|
if _, err := os.Stat(f); err == nil {
|
|
t.Errorf("%s outlived its plan", f)
|
|
}
|
|
}
|
|
for _, f := range []string{newRec, newRcpt} {
|
|
if _, err := os.Stat(f); err != nil {
|
|
t.Errorf("%s was let go early", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A retry whose target something else moved since the first attempt does not
|
|
// run: an undo to before that attempt would take the other change back.
|
|
func TestARetryLeavesATargetSomethingElseMovedAlone(t *testing.T) {
|
|
dcn, b64, f, _, _ := shipRig(t, "", "true", "true", 1)
|
|
failKubeAt(t, "set", 1)
|
|
b1, _ := planRunLine(dcn, b64, "1", "", nil)
|
|
a1, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || a1["ok"] != false {
|
|
t.Fatalf("roll attempt 1 fails before it sets anything: %v %v", a1, err)
|
|
}
|
|
// Another pipeline rolls the workload (a security fix, say).
|
|
fix := tImage + "@sha256:" + strings.Repeat("3", 64)
|
|
f.containers["bridge"] = fix
|
|
a2, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || a2["ok"] != false || !strings.Contains(fmt.Sprint(a2["error"]), "something else moved it since") {
|
|
t.Fatalf("the retry over a moved target: %v %v", a2, err)
|
|
}
|
|
if f.containers["bridge"] != fix {
|
|
t.Fatalf("the retry rolled over the other change: %s", f.containers["bridge"])
|
|
}
|
|
u, err := planUndoLine(b64, "2", rigReceipt(a2))
|
|
if err != nil {
|
|
t.Fatalf("the undo on the retry's failure: %v", err)
|
|
}
|
|
if u["ok"] != false || f.containers["bridge"] != fix {
|
|
t.Fatalf("the undo took the other change back: %v %s", u, f.containers["bridge"])
|
|
}
|
|
}
|
|
|
|
// Only a process known to be gone settles its attempt: a /proc that cannot
|
|
// be read is no answer.
|
|
func TestOnlyAProcessKnownToBeGoneIsSettled(t *testing.T) {
|
|
if planProc == "" || planProcGone(planProc) {
|
|
t.Fatalf("this process reads as gone: %q", planProc)
|
|
}
|
|
parts := strings.Split(planProc, ":")
|
|
if !planProcGone(parts[0] + ":" + parts[1] + ":1") {
|
|
t.Error("another process under this pid is not this one")
|
|
}
|
|
if !planProcGone(parts[0] + ":999999999:1") {
|
|
t.Error("a pid that is not there")
|
|
}
|
|
prev := planProcRoot
|
|
t.Cleanup(func() { planProcRoot = prev })
|
|
planProcRoot = t.TempDir() // no boot id, no processes: nothing reads
|
|
if planProcGone(planProc) || planProcGone(parts[0]+":999999999:1") {
|
|
t.Error("a /proc that does not read settled an attempt")
|
|
}
|
|
}
|
|
|
|
// A target something else moves while the line's program runs is left
|
|
// alone: the verb refuses the state it finds, so no undo can take the other
|
|
// change back.
|
|
func TestATargetMovedWhileTheProgramRunsIsLeftAlone(t *testing.T) {
|
|
dcn, b64, f, _, _ := shipRig(t, "", `[ "$PRIVATAE_OP_MODE" = apply ] && sleep 3; true`, `[ "$PRIVATAE_OP_MODE" = verify ] && exit 1; true`, 1)
|
|
b1, _ := planRunLine(dcn, b64, "1", "", nil)
|
|
fix := tImage + "@sha256:" + strings.Repeat("3", 64)
|
|
var mu sync.Mutex
|
|
prev := opKubectl
|
|
opKubectl = func(ctx context.Context, args ...string) (string, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return prev(ctx, args...)
|
|
}
|
|
t.Cleanup(func() { opKubectl = prev })
|
|
go func() {
|
|
time.Sleep(1500 * time.Millisecond)
|
|
mu.Lock()
|
|
f.containers["bridge"] = fix // another pipeline rolls it
|
|
mu.Unlock()
|
|
}()
|
|
r, err := planRunLine(dcn, b64, "2", rigReceipts(b1), nil)
|
|
if err != nil || r["ok"] != false || !strings.Contains(fmt.Sprint(r["error"]), "something else moved it since") {
|
|
t.Fatalf("the roll over a target moved while its program ran: %v %v", r, err)
|
|
}
|
|
if f.containers["bridge"] != fix {
|
|
t.Fatalf("the roll set over the other change: %s", f.containers["bridge"])
|
|
}
|
|
// Its own failure is the latest attempt's: the undo finds nothing of
|
|
// this line on the workload, and leaves the other change in place.
|
|
if _, err := planUndoLine(b64, "2", rigReceipt(r)); err != nil {
|
|
t.Fatalf("the undo: %v", err)
|
|
}
|
|
if f.containers["bridge"] != fix {
|
|
t.Fatalf("the undo took the other change back: %s", f.containers["bridge"])
|
|
}
|
|
}
|
|
|
|
// A verb refuses a target in any other state than the one it was approved
|
|
// and recorded to move from.
|
|
func TestAVerbMovesOnlyFromTheStateItWasApprovedToMoveFrom(t *testing.T) {
|
|
if err := opMovedSince("a", "a"); err != nil {
|
|
t.Errorf("the approved state: %v", err)
|
|
}
|
|
if err := opMovedSince("b", ""); err != nil {
|
|
t.Errorf("a verb with no state (a unit): %v", err)
|
|
}
|
|
if err := opMovedSince("b", "a"); err == nil {
|
|
t.Error("another state")
|
|
}
|
|
setupBox(t)
|
|
live, next := "sha256:"+strings.Repeat("1", 64), "sha256:"+strings.Repeat("2", 64)
|
|
issueReceipt(t, live, strings.Repeat("a", 40))
|
|
issueReceipt(t, next, strings.Repeat("b", 40))
|
|
if err := opRecordRoll(live, opRollRec{Image: tImage, Source: tRepo, Commit: strings.Repeat("a", 40)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
useAncestry(t, func(_, _, _ string, _ []string) error { return nil })
|
|
f := &fakeKube{containers: map[string]string{"bridge": tImage + "@" + live}}
|
|
useFakeKube(t, f)
|
|
if _, err := opImageDo(context.Background(), setImageLine(next), nil, nil, tImage+"@sha256:"+strings.Repeat("9", 64)); err == nil || !strings.Contains(err.Error(), "something else moved it since") || len(f.sets()) != 0 {
|
|
t.Fatalf("an image in another state than approved: %v %v", err, f.sets())
|
|
}
|
|
// Something else rolls it while the ancestry fetch runs: read again
|
|
// right before the write, and nothing is set.
|
|
fix := tImage + "@sha256:" + strings.Repeat("3", 64)
|
|
useAncestry(t, func(_, _, _ string, _ []string) error { f.containers["bridge"] = fix; return nil })
|
|
if _, err := opImageDo(context.Background(), setImageLine(next), nil, nil, tImage+"@"+live); err == nil || !strings.Contains(err.Error(), "something else moved it since") || len(f.sets()) != 0 || f.containers["bridge"] != fix {
|
|
t.Fatalf("an image rolled while the ancestry was fetched: %v %v", err, f.sets())
|
|
}
|
|
f.containers["bridge"] = tImage + "@" + live
|
|
useAncestry(t, func(_, _, _ string, _ []string) error { return nil })
|
|
if _, err := opImageDo(context.Background(), setImageLine(next), nil, nil, tImage+"@"+live); err != nil || len(f.sets()) != 1 {
|
|
t.Fatalf("the image in the approved state: %v %v", err, f.sets())
|
|
}
|
|
}
|
|
|
|
// The same for a tree: a move approved from another commit than the tree is
|
|
// at does not run.
|
|
func TestATreeMovesOnlyFromTheCommitItWasApprovedToMoveFrom(t *testing.T) {
|
|
if os.Geteuid() != 0 {
|
|
t.Skip("needs root for the tree's ownership checks")
|
|
}
|
|
setupBox(t)
|
|
useFileRepos(t)
|
|
origin, tree, c1, c2 := twoCommitRepo(t, func(string) {}, func(o string) {
|
|
_ = 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}}
|
|
if _, err := opTreeDo(context.Background(), line, nil, strings.Repeat("9", 40)); err == nil || !strings.Contains(err.Error(), "something else moved it since") {
|
|
t.Fatalf("a tree at another commit than approved: %v", err)
|
|
}
|
|
if head := gitT(t, tree, "rev-parse", "HEAD"); head != c1 {
|
|
t.Fatalf("the tree moved: %s", head)
|
|
}
|
|
// Something else moves it while the fetch runs: read again right before
|
|
// the move, and it is left where the other change put it.
|
|
prev := opTreeBeforeMove
|
|
var other string
|
|
opTreeBeforeMove = func(path string) {
|
|
gitT(t, path, "commit", "-q", "--allow-empty", "-m", "someone else")
|
|
other = gitT(t, path, "rev-parse", "HEAD")
|
|
}
|
|
if _, err := opTreeDo(context.Background(), line, nil, c1); err == nil || !strings.Contains(err.Error(), "something else moved it since") {
|
|
t.Fatalf("a tree moved while the fetch ran: %v", err)
|
|
}
|
|
opTreeBeforeMove = prev
|
|
if head := gitT(t, tree, "rev-parse", "HEAD"); head != other {
|
|
t.Fatalf("the other change was moved over: %s", head)
|
|
}
|
|
gitT(t, tree, "reset", "-q", "--keep", c1)
|
|
if _, err := opTreeDo(context.Background(), line, nil, c1); err != nil {
|
|
t.Fatalf("the tree at the approved commit: %v", err)
|
|
}
|
|
}
|
|
|
|
// Two changes this bastion makes to one workload never interleave between a
|
|
// read and a write: the second waits for the first, then finds it moved.
|
|
func TestTwoChangesToOneTargetNeverInterleave(t *testing.T) {
|
|
setupBox(t)
|
|
live, a, b := "sha256:"+strings.Repeat("1", 64), "sha256:"+strings.Repeat("2", 64), "sha256:"+strings.Repeat("3", 64)
|
|
issueReceipt(t, live, strings.Repeat("a", 40))
|
|
issueReceipt(t, a, strings.Repeat("b", 40))
|
|
issueReceipt(t, b, strings.Repeat("c", 40))
|
|
if err := opRecordRoll(live, opRollRec{Image: tImage, Source: tRepo, Commit: strings.Repeat("a", 40)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
useAncestry(t, func(_, _, _ string, _ []string) error { return nil })
|
|
f := &fakeKube{containers: map[string]string{"bridge": tImage + "@" + live}}
|
|
var mu sync.Mutex
|
|
gets := 0
|
|
paused, release := make(chan struct{}), make(chan struct{})
|
|
prev := opKubectl
|
|
opKubectl = func(ctx context.Context, args ...string) (string, error) {
|
|
mu.Lock()
|
|
out, err := f.run(ctx, args...)
|
|
pause := false
|
|
if len(args) >= 3 && args[2] == "get" {
|
|
gets++
|
|
pause = gets == 2 // the first roll's second read, one command before its write
|
|
}
|
|
mu.Unlock()
|
|
if pause {
|
|
close(paused)
|
|
<-release
|
|
}
|
|
return out, err
|
|
}
|
|
t.Cleanup(func() { opKubectl = prev })
|
|
first := make(chan error, 1)
|
|
go func() {
|
|
_, err := opImageDo(context.Background(), setImageLine(a), nil, nil, tImage+"@"+live)
|
|
first <- err
|
|
}()
|
|
<-paused
|
|
second := make(chan error, 1)
|
|
go func() {
|
|
_, err := opImageDo(context.Background(), setImageLine(b), nil, nil, tImage+"@"+live)
|
|
second <- err
|
|
}()
|
|
time.Sleep(300 * time.Millisecond)
|
|
mu.Lock()
|
|
early := gets
|
|
mu.Unlock()
|
|
close(release)
|
|
if err := <-first; err != nil {
|
|
t.Fatalf("the first roll: %v", err)
|
|
}
|
|
err := <-second
|
|
if early != 2 {
|
|
t.Fatalf("the second roll read the workload while the first was between its read and its write (%d reads)", early)
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), "something else moved it since") || f.containers["bridge"] != tImage+"@"+a {
|
|
t.Fatalf("the second roll after the first: %v, live %s", err, f.containers["bridge"])
|
|
}
|
|
}
|
|
|
|
// An undo reads its target again right before it writes: a change made
|
|
// since its first read is left alone.
|
|
func TestAnUndoReadsItsTargetAgainBeforeItWrites(t *testing.T) {
|
|
t.Run("an image", func(t *testing.T) {
|
|
from, to := tImage+"@sha256:"+strings.Repeat("1", 64), tImage+"@sha256:"+strings.Repeat("2", 64)
|
|
fix := tImage + "@sha256:" + strings.Repeat("3", 64)
|
|
f := &fakeKube{containers: map[string]string{"bridge": to}}
|
|
gets := 0
|
|
prev := opKubectl
|
|
opKubectl = func(ctx context.Context, args ...string) (string, error) {
|
|
out, err := f.run(ctx, args...)
|
|
if len(args) >= 3 && args[2] == "get" {
|
|
if gets++; gets == 1 {
|
|
f.containers["bridge"] = fix // rolled right after the undo's first read
|
|
}
|
|
}
|
|
return out, err
|
|
}
|
|
t.Cleanup(func() { opKubectl = prev })
|
|
if _, err := planUndoVerb(context.Background(), setImageLine("sha256:"+strings.Repeat("2", 64)), from, to); err == nil || !strings.Contains(err.Error(), "something else moved it since") {
|
|
t.Fatalf("an undo over a roll made after its first read: %v", err)
|
|
}
|
|
if f.containers["bridge"] != fix || len(f.sets()) != 0 {
|
|
t.Fatalf("the other roll was taken back: %s %v", f.containers["bridge"], f.sets())
|
|
}
|
|
})
|
|
t.Run("a tree", func(t *testing.T) {
|
|
if os.Geteuid() != 0 {
|
|
t.Skip("needs root for the tree's ownership checks")
|
|
}
|
|
setupBox(t)
|
|
useFileRepos(t)
|
|
origin, tree, c1, c2 := twoCommitRepo(t, func(string) {}, func(o string) {
|
|
_ = 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}}
|
|
if _, err := opTreeDo(context.Background(), line, nil, c1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prev := opTreeBeforeMove
|
|
t.Cleanup(func() { opTreeBeforeMove = prev })
|
|
var other string
|
|
opTreeBeforeMove = func(path string) {
|
|
gitT(t, path, "commit", "-q", "--allow-empty", "-m", "someone else")
|
|
other = gitT(t, path, "rev-parse", "HEAD")
|
|
}
|
|
if _, err := planUndoVerb(context.Background(), line, c1, c2); err == nil || !strings.Contains(err.Error(), "something else moved it since") {
|
|
t.Fatalf("an undo over a commit made after its first read: %v", err)
|
|
}
|
|
if head := gitT(t, tree, "rev-parse", "HEAD"); head != other {
|
|
t.Fatalf("the other commit was taken back: %s", head)
|
|
}
|
|
})
|
|
}
|
|
|
|
// A tree move and an undo wait for a change in flight on the same target.
|
|
func TestAChangeWaitsForOneInFlightOnTheSameTarget(t *testing.T) {
|
|
waits := func(t *testing.T, line opVerbLine, run func() error, reached func() bool) {
|
|
t.Helper()
|
|
unlock := opLockTarget(line)
|
|
done := make(chan error, 1)
|
|
go func() { done <- run() }()
|
|
time.Sleep(300 * time.Millisecond)
|
|
if reached() {
|
|
unlock()
|
|
<-done
|
|
t.Fatal("it read the target while another change held it")
|
|
}
|
|
unlock()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(30 * time.Second):
|
|
t.Fatal("it never ran once the other change was done")
|
|
}
|
|
}
|
|
t.Run("an image undo", func(t *testing.T) {
|
|
line := setImageLine("sha256:" + strings.Repeat("2", 64))
|
|
f := &fakeKube{containers: map[string]string{"bridge": tImage + "@sha256:" + strings.Repeat("2", 64)}}
|
|
var mu sync.Mutex
|
|
prev := opKubectl
|
|
opKubectl = func(ctx context.Context, args ...string) (string, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return f.run(ctx, args...)
|
|
}
|
|
t.Cleanup(func() { opKubectl = prev })
|
|
waits(t, line, func() error {
|
|
_, err := planUndoVerb(context.Background(), line, tImage+"@sha256:"+strings.Repeat("1", 64), tImage+"@sha256:"+strings.Repeat("2", 64))
|
|
return err
|
|
}, func() bool { mu.Lock(); defer mu.Unlock(); return len(f.calls) > 0 })
|
|
})
|
|
for _, undo := range []bool{false, true} {
|
|
name := "a tree move"
|
|
if undo {
|
|
name = "a tree undo"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
if os.Geteuid() != 0 {
|
|
t.Skip("needs root for the tree's ownership checks")
|
|
}
|
|
setupBox(t)
|
|
useFileRepos(t)
|
|
origin, tree, c1, c2 := twoCommitRepo(t, func(string) {}, func(o string) {
|
|
_ = 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}}
|
|
if undo {
|
|
if _, err := opTreeDo(context.Background(), line, nil, c1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var mu sync.Mutex
|
|
hit := false
|
|
prev := opTreeBeforeMove
|
|
t.Cleanup(func() { opTreeBeforeMove = prev })
|
|
opTreeBeforeMove = func(string) { mu.Lock(); hit = true; mu.Unlock() }
|
|
waits(t, line, func() error {
|
|
if undo {
|
|
_, err := planUndoVerb(context.Background(), line, c1, c2)
|
|
return err
|
|
}
|
|
_, err := opTreeDo(context.Background(), line, nil, c1)
|
|
return err
|
|
}, func() bool { mu.Lock(); defer mu.Unlock(); return hit })
|
|
})
|
|
}
|
|
}
|