bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+983
View File
@@ -0,0 +1,983 @@
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 })
})
}
}