internal/control/runnerattach_test.go

f9845abbc93ead5d74684a56f2e677be71c69587
gitbay/internal/control/runnerattach_test.go history · blame · raw

187 lines · 6680 bytes

  1package control
  2
  3import (
  4	"bytes"
  5	"strconv"
  6	"strings"
  7	"testing"
  8
  9	"gitbay.org/gitbay/internal/config"
 10	"gitbay.org/gitbay/internal/protocol"
 11	"gitbay.org/gitbay/internal/store"
 12)
 13
 14// attachFixture: alice (not admin) owns alice/app with a build queued;
 15// mallory (not admin) owns mallory/evil with an older build queued. Each
 16// has a runner-scoped key. The Ctx polls as the given user with the given
 17// key, which is what the SSH listener produces.
 18type attachFixture struct {
 19	st                   *store.Store
 20	alice, mallory       int64
 21	aliceKey, malloryKey store.SSHKey
 22	app, evil            store.Repo
 23	appBuild, evilBuild  int64
 24}
 25
 26func newAttachFixture(t *testing.T) attachFixture {
 27	t.Helper()
 28	st, err := store.Open(":memory:")
 29	if err != nil {
 30		t.Fatal(err)
 31	}
 32	t.Cleanup(func() { st.Close() })
 33	if err := st.MigrateUp(); err != nil {
 34		t.Fatal(err)
 35	}
 36	var f attachFixture
 37	f.st = st
 38	mk := func(name, fp string) (int64, store.SSHKey, store.Repo, string) {
 39		uid, err := st.CreateUser(name, false)
 40		if err != nil {
 41			t.Fatal(err)
 42		}
 43		if err := st.AddSSHKey(uid, fp, "ssh-ed25519", []byte(fp), "runner"); err != nil {
 44			t.Fatal(err)
 45		}
 46		k, _ := st.SSHKeyByFingerprint(fp)
 47		repoName := map[string]string{"alice": "app", "mallory": "evil"}[name]
 48		rid, err := st.CreateRepo("user", uid, repoName, "public")
 49		if err != nil {
 50			t.Fatal(err)
 51		}
 52		repo, _ := st.RepoByID(rid)
 53		return uid, k, repo, repoName
 54	}
 55	f.mallory, f.malloryKey, f.evil, _ = mk("mallory", "SHA256:mallory")
 56	f.alice, f.aliceKey, f.app, _ = mk("alice", "SHA256:alice")
 57	// mallory's build is older, so an unrestricted claim would take it.
 58	f.evilBuild, err = st.CreateBuild(f.evil.ID, "unit", "aaa111", "main", "[]", "", "", true)
 59	if err != nil {
 60		t.Fatal(err)
 61	}
 62	f.appBuild, err = st.CreateBuild(f.app.ID, "unit", "bbb222", "main", "[]", "", "", true)
 63	if err != nil {
 64		t.Fatal(err)
 65	}
 66	return f
 67}
 68
 69func (f attachFixture) ctx(uid int64, key store.SSHKey, admin bool) (*Ctx, *bytes.Buffer) {
 70	var out bytes.Buffer
 71	name := "alice"
 72	if uid == f.mallory {
 73		name = "mallory"
 74	}
 75	return &Ctx{
 76		User:   store.User{ID: uid, Username: name, IsAdmin: admin},
 77		Scope:  key.Scope,
 78		Source: key.Fingerprint,
 79		Store:  f.st,
 80		Cfg:    config.Config{Server: config.Server{Root: "/nonexistent", SiteURL: "https://x.test"}},
 81		Stdin:  strings.NewReader(""),
 82		Stdout: &out,
 83		Stderr: &out,
 84	}, &out
 85}
 86
 87// A runner key with no attachment claims nothing, whatever is queued.
 88func TestRunnerNextUnattachedClaimsNothing(t *testing.T) {
 89	f := newAttachFixture(t)
 90	c, out := f.ctx(f.alice, f.aliceKey, false)
 91	if code := runRunnerNext(c, nil); code != protocol.ExitOK || !strings.Contains(out.String(), "no pending builds") {
 92		t.Fatalf("exit %d: %s", code, out.String())
 93	}
 94	b, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild)
 95	if b.Status != "pending" {
 96		t.Fatalf("unattached key claimed a build: %s", b.Status)
 97	}
 98}
 99
100// An attached key claims its repository's build and not the older one
101// queued elsewhere; naming a repository outside the attachments is refused.
102func TestRunnerNextAttachedClaimsOwnRepoOnly(t *testing.T) {
103	f := newAttachFixture(t)
104	if err := f.st.AttachRunner(f.aliceKey.ID, f.app.ID); err != nil {
105		t.Fatal(err)
106	}
107	c, out := f.ctx(f.alice, f.aliceKey, false)
108	if code := runRunnerNext(c, nil); code != protocol.ExitOK || !strings.Contains(out.String(), "alice/app") {
109		t.Fatalf("exit %d: %s", code, out.String())
110	}
111	if b, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild); b.Status != "pending" {
112		t.Fatalf("mallory's build was touched: %s", b.Status)
113	}
114	c, out = f.ctx(f.alice, f.aliceKey, false)
115	if code := runRunnerNext(c, []string{"mallory/evil"}); code != protocol.ExitDenied {
116		t.Fatalf("naming an unattached repo: exit %d, want %d: %s", code, protocol.ExitDenied, out.String())
117	}
118}
119
120// The heartbeat is recorded against the key, and admin runners shows it
121// with its fingerprint and attachments.
122func TestAdminRunnersShowsKeyAndAttachments(t *testing.T) {
123	f := newAttachFixture(t)
124	if err := f.st.AttachRunner(f.aliceKey.ID, f.app.ID); err != nil {
125		t.Fatal(err)
126	}
127	c, _ := f.ctx(f.alice, f.aliceKey, false)
128	runRunnerNext(c, nil)
129	admin, out := f.ctx(f.alice, f.aliceKey, true)
130	admin.Scope = "full"
131	if code := runAdminRunners(admin, nil); code != protocol.ExitOK {
132		t.Fatalf("admin runners: exit %d: %s", code, out.String())
133	}
134	if !strings.Contains(out.String(), "alice\tSHA256:alice\t") || !strings.Contains(out.String(), "\talice/app\t") {
135		t.Fatalf("row lacks fingerprint or attachments:\n%s", out.String())
136	}
137}
138
139// Untrusted builds are skipped unless the runner asks.
140func TestRunnerNextUntrustedFlag(t *testing.T) {
141	f := newAttachFixture(t)
142	if err := f.st.AttachRunner(f.aliceKey.ID, f.app.ID); err != nil {
143		t.Fatal(err)
144	}
145	c, _ := f.ctx(f.alice, f.aliceKey, false)
146	runRunnerNext(c, nil) // takes the trusted build
147	fork, err := f.st.CreateBuild(f.app.ID, "unit", "ccc333", "refs/merge-requests/1/head", "[]", "", "", false)
148	if err != nil {
149		t.Fatal(err)
150	}
151	c, out := f.ctx(f.alice, f.aliceKey, false)
152	runRunnerNext(c, nil)
153	if !strings.Contains(out.String(), "no pending builds") {
154		t.Fatalf("fork head claimed without --untrusted: %s", out.String())
155	}
156	c, out = f.ctx(f.alice, f.aliceKey, false)
157	if code := runRunnerNext(c, []string{"--untrusted"}); code != protocol.ExitOK || !strings.Contains(out.String(), "alice/app") {
158		t.Fatalf("--untrusted did not claim the fork head: exit %d %s", code, out.String())
159	}
160	if b, _ := f.st.BuildByNumber(f.app.ID, fork); b.Status != "running" {
161		t.Fatalf("fork build is %s, want running", b.Status)
162	}
163}
164
165// runner done and runner log on a build whose repository is not attached
166// to the key are refused.
167func TestRunnerDoneRefusedForUnattachedBuild(t *testing.T) {
168	f := newAttachFixture(t)
169	if err := f.st.AttachRunner(f.malloryKey.ID, f.evil.ID); err != nil {
170		t.Fatal(err)
171	}
172	c, _ := f.ctx(f.mallory, f.malloryKey, false)
173	runRunnerNext(c, nil) // mallory holds her own build
174	evil, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild)
175	c, out := f.ctx(f.alice, f.aliceKey, false)
176	id := strconv.FormatInt(evil.ID, 10)
177	if code := runRunnerDone(c, []string{id, "success"}); code != protocol.ExitDenied {
178		t.Fatalf("done on an unattached build: exit %d, want %d: %s", code, protocol.ExitDenied, out.String())
179	}
180	c, out = f.ctx(f.alice, f.aliceKey, false)
181	if code := runRunnerLog(c, []string{id}); code != protocol.ExitDenied {
182		t.Fatalf("log on an unattached build: exit %d, want %d: %s", code, protocol.ExitDenied, out.String())
183	}
184	if b, _ := f.st.BuildByNumber(f.evil.ID, f.evilBuild); b.Status != "running" {
185		t.Fatalf("build was finished by a foreign key: %s", b.Status)
186	}
187}