package runner
import (
"context"
"encoding/json"
"net/http/httptest"
"os"
"os/user"
"path/filepath"
"strings"
"testing"
"time"
)
// The test binary stands in for `quarry job-env` when the fake sudo
// below starts it with QUARRY_TEST_JOBENV set.
func TestMain(m *testing.M) {
if os.Getenv("QUARRY_TEST_JOBENV") == "1" {
if err := JobEnvMain(os.Args[2:]); err != nil {
os.Stderr.WriteString(err.Error() + "\n")
os.Exit(1)
}
return
}
os.Exit(m.Run())
}
// A JOB RUNS AS ITS SLOT'S USER WITH ONLY ITS OWN ENVIRONMENT, AND GETS
// A SECRET ONLY FROM THE FORGE, MASKED IN THE LOG (RFC 0019 §isolation).
//
// sudo is a fake here that runs the command as the current user,
// keeping every argument but the user switch, and never runs pkill: the
// real reclaim kills the slot user's processes, and on a CI box the
// slot group's name can be the CI user's own.
func TestAJobRunsAsItsSlotWithOnlyItsEnvironment(t *testing.T) {
u, err := user.Current()
if err != nil {
t.Fatal(err)
}
g, err := user.LookupGroupId(u.Gid)
if err != nil {
t.Skipf("no group name for gid %s: %v", u.Gid, err)
}
bin := t.TempDir()
fake := "#!/bin/sh\n# -n -u SLOT -- CMD...\n[ \"$5\" = pkill ] && exit 0\nshift 4\nexec env QUARRY_TEST_JOBENV=1 \"$@\"\n"
if err := os.WriteFile(filepath.Join(bin, "sudo"), []byte(fake), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("QUARRY_RUNNER_TOKEN", "must-not-reach-the-job")
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
job := func(j Job) map[string]string {
b, _ := json.Marshal(j)
return map[string]string{JobKey: string(b)}
}
with := func(id string, meta map[string]string) polled {
p := task(id, "c1")
p.Metadata = meta
return p
}
b := &fakeBridge{queue: map[string][]polled{"t-job": {
with("deploy", job(Job{Env: map[string]string{"STAGE": "prod"}, Secrets: []string{"CF_TOKEN"}, Steps: []JobStep{
{Name: "look", Run: "echo token=$CF_TOKEN stage=$STAGE runner=${QUARRY_RUNNER_TOKEN:-absent} step=${STEP_ONLY:-absent}"},
{Name: "own", Run: "echo step=$STEP_ONLY", Secrets: []string{"STEP_ONLY"}},
{Name: "umask", Run: "touch made && ls -l made | cut -c1-10"},
}})),
with("refused", job(Job{Secrets: []string{"CF_TOKEN"}, Steps: []JobStep{{Run: "echo ran"}}})),
with("plain", job(Job{Steps: []JobStep{{Run: "echo runner=${QUARRY_RUNNER_TOKEN:-absent}"}}})),
}}, resolved: map[string]map[string]any{}, logs: map[string]string{}}
b.secrets = func(taskID string, names []string) (map[string]string, int) {
if taskID == "refused" {
return nil, 403
}
return map[string]string{"CF_TOKEN": "cf-s3cr3t-value", "STEP_ONLY": "step-s3cr3t"}, 200
}
srv := httptest.NewServer(b.handler())
defer srv.Close()
r, err := New(Config{
Bridge: srv.URL, Token: "WT", Tasks: []string{"t-job"}, MaxTasks: 1,
WorkDir: t.TempDir(), Checkout: appCheckout{}, Heartbeat: 50 * time.Millisecond, Logf: t.Logf,
JobUsers: []string{g.Name}, Self: self,
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { r.Run(ctx); close(done) }()
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
b.mu.Lock()
n := len(b.resolved)
b.mu.Unlock()
if n == 3 {
break
}
}
cancel()
<-done
b.mu.Lock()
defer b.mu.Unlock()
if got := b.resolved["deploy"]; got["action"] != "complete" {
t.Fatalf("FAIL: the deploy resolved %v, log %q", got, b.logs["deploy"])
}
log := b.logs["deploy"]
if strings.Contains(log, "s3cr3t") {
t.Errorf("FAIL: a secret value reached the log: %q", log)
}
if !strings.Contains(log, "token=*** stage=prod runner=absent step=absent") {
t.Errorf("FAIL: the first step saw %q; want the job's secret (masked) and env, no runner token, no step-only secret", log)
}
if !strings.Contains(log, "step=***") {
t.Errorf("FAIL: the step with its own secret did not get it: %q", log)
}
if !strings.Contains(log, "-rw-rw----") {
t.Errorf("FAIL: what the job wrote is not private to its group: %q", log)
}
if got := b.resolved["refused"]; got["action"] != "fail" || !strings.Contains(got["error"].(string), "secrets refused") || strings.Contains(b.logs["refused"], "ran") {
t.Errorf("FAIL: a refused delivery resolved %v; want the task failed with the forge's words, unrun", got)
}
if !strings.Contains(b.logs["plain"], "runner=absent") {
t.Errorf("FAIL: a job on an isolating runner saw the runner's token: %q", b.logs["plain"])
}
t.Log("ok: slot user, own env only, secrets scoped and masked, refusals fail the task")
}
// Masking catches a value split across writes and each line of a
// multi-line one.
func TestTheMaskCatchesSplitAndMultilineValues(t *testing.T) {
var out strings.Builder
m := newMasker(&out, map[string]string{"K": "abcdef123", "PEM": "-----BEGIN-----\nsecretline\n-----END-----"})
m.Write([]byte("value abc"))
m.Write([]byte("def123 end\nkey:\nsecretline\n"))
m.Write([]byte("tail abcdef123"))
m.Close()
if s := out.String(); strings.Contains(s, "abcdef123") || strings.Contains(s, "secretline") || !strings.Contains(s, "value *** end") || !strings.HasSuffix(s, "tail ***") {
t.Errorf("FAIL: masked output %q", s)
}
}