package main // A git run's copy is bounded by how many entries a tree holds as well as by // its bytes; it never hands git the run's token in an argument or a config // file; and it uses nothing of the agent's tree while the agent's container // may still be running. import ( "encoding/base64" "net/http" "net/http/cgi" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" ) // Many empty files cost nothing in bytes; each still counts against the // copy, so a tree of them is refused and nothing is pushed. func TestAGitRunRefusesATreeOfTooManyEntries(t *testing.T) { f := newGitRunFixture(t) f.setAgent(t, `cd "$1" i=0; while [ $i -lt 40 ]; do mkdir d$i; j=0; while [ $j -lt 100 ]; do : > d$i/f$j; j=$((j+1)); done; i=$((i+1)); done `) saved := maxCopiedTreeBytes maxCopiedTreeBytes = 1 << 20 // 4000 entries at 512 bytes each are 2 MB t.Cleanup(func() { maxCopiedTreeBytes = saved }) res, code := f.run(t) if code == 0 || res["ok"] != false { t.Fatalf("a tree of 4000 empty files was copied under a 1 MiB limit: %v", res) } if e, _ := res["error"].(string); !strings.Contains(e, "too large") { t.Errorf("the error does not say the tree is too large: %q", e) } if branchPushed(f) { t.Error("the branch was pushed") } } // A container the runtime does not confirm removed fails the run before any // of its tree is copied. func TestAGitRunFailsWhenTheAgentsContainerIsNotRemoved(t *testing.T) { f := newGitRunFixture(t) f.setAgent(t, "cd \"$1\" && echo new > edit.txt\n") docker := filepath.Join(filepath.Dir(f.agentFile), "docker") b, err := os.ReadFile(docker) if err != nil { t.Fatal(err) } failing := strings.Replace(string(b), "ps|pull|rm) exit 0;;", "ps|pull) exit 0;;\nrm) echo 'Error: removal stuck' >&2; exit 1;;", 1) if failing == string(b) { t.Fatal("the stand-in runtime changed shape") } if err := os.WriteFile(docker, []byte(failing), 0o755); err != nil { t.Fatal(err) } res, code := f.run(t) if code == 0 || res["ok"] != false { t.Fatalf("a run whose container was not removed reads ok: %v", res) } if e, _ := res["error"].(string); !strings.Contains(e, "could not be removed") || !strings.Contains(e, "removal stuck") { t.Errorf("the error does not say the container was not removed: %q", e) } if branchPushed(f) { t.Error("the branch was pushed") } } // Against a server that needs the token, the run clones and pushes with the // token in no git argument and in no clone's config. func TestAGitRunKeepsTheTokenOutOfEveryArgumentAndConfig(t *testing.T) { f := newGitRunFixture(t) f.setAgent(t, "cd \"$1\" && echo new > edit.txt\n") realGit, err := exec.LookPath("git") if err != nil { t.Skip("no git") } want := "Basic " + base64.StdEncoding.EncodeToString([]byte("harness:"+gitRunToken)) backend := &cgi.Handler{Path: realGit, Args: []string{"http-backend"}, Root: "/git", Env: []string{"GIT_PROJECT_ROOT=" + filepath.Dir(f.origin), "GIT_HTTP_EXPORT_ALL=1", "REMOTE_USER=harness", "GIT_CONFIG_NOSYSTEM=1", "HOME=" + t.TempDir()}} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != want { w.Header().Set("WWW-Authenticate", `Basic realm="test"`) http.Error(w, "auth", http.StatusUnauthorized) return } backend.ServeHTTP(w, r) })) t.Cleanup(srv.Close) logs := t.TempDir() argvLog, cfgLog := filepath.Join(logs, "argv"), filepath.Join(logs, "config") bin := t.TempDir() wrapper := "#!/bin/sh\nprintf '%s\\n' \"$*\" >> " + argvLog + "\n" + "prev=; for a; do if [ \"$prev\" = -C ] && [ -f \"$a/.git/config\" ]; then cat \"$a/.git/config\" >> " + cfgLog + "; fi; prev=$a; done\n" + "exec " + realGit + " \"$@\"\n" if err := os.WriteFile(filepath.Join(bin, "git"), []byte(wrapper), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH")) repoURL := strings.Replace(srv.URL, "http://", "http://harness:"+gitRunToken+"@", 1) + "/git/" + filepath.Base(f.origin) b, code := runGit(harnessSpec{Mode: "git", HarnessID: "h1", Image: "harness:test", RepoURL: repoURL, Branch: "agent-work", MaxMinutes: 1}) if code != 0 { t.Fatalf("the run failed: %s", b) } if !branchPushed(f) { t.Fatal("the branch is not in the origin") } argv, _ := os.ReadFile(argvLog) if !strings.Contains(string(argv), "push origin agent-work") { t.Fatalf("the stand-in git did not see the push:\n%s", argv) } cfg, _ := os.ReadFile(cfgLog) if len(cfg) == 0 { t.Fatal("no clone's config was read") } for name, body := range map[string][]byte{"an argument": argv, "a clone's config": cfg} { for _, secret := range []string{gitRunToken, strings.TrimPrefix(want, "Basic ")} { if strings.Contains(string(body), secret) { t.Errorf("the token is in %s:\n%s", name, body) } } } } // Every git call carries the harness identity: nothing of this machine's user // or host name reaches the clone the agent is handed, and the commit is the // harness's. func TestAGitRunsGitCarriesTheHarnessIdentity(t *testing.T) { f := newGitRunFixture(t) reflog := filepath.Join(t.TempDir(), "reflog") f.setAgent(t, "cp \"$1/.git/logs/HEAD\" "+reflog+" && cd \"$1\" && echo new > edit.txt\n") res, code := f.run(t) if code != 0 { t.Fatalf("the run failed: %v", res) } b, err := os.ReadFile(reflog) if err != nil { t.Fatal(err) } host, _ := os.Hostname() if !strings.Contains(string(b), " harness ") || (host != "" && strings.Contains(string(b), host)) { t.Errorf("the agent's clone records another identity:\n%s", b) } who := strings.TrimSpace(testGit(t, f.origin, "log", "-1", "--format=%an <%ae> / %cn <%ce>", "agent-work")) if who != "harness / harness " { t.Errorf("the commit is by %q", who) } }