Skip to content

Commit 31d7273

Browse files
committed
Add e2e internal package
1 parent 7dbce42 commit 31d7273

5 files changed

Lines changed: 325 additions & 0 deletions

File tree

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/aws/smithy-go v1.23.2
2222
github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf
2323
github.com/buildkite/bintest/v3 v3.3.0
24+
github.com/buildkite/go-buildkite/v4 v4.11.0
2425
github.com/buildkite/go-pipeline v0.16.0
2526
github.com/buildkite/interpolate v0.1.5
2627
github.com/buildkite/roko v1.4.0
@@ -112,6 +113,7 @@ require (
112113
github.com/bitfield/gotestdox v0.2.2 // indirect
113114
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
114115
github.com/buildkite/test-engine-client v1.6.0 // indirect
116+
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf // indirect
115117
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
116118
github.com/cespare/xxhash/v2 v2.3.0 // indirect
117119
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf
132132
github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20220812150832-b6b31c6eeeaf/go.mod h1:CeKhh8xSs3WZAc50xABMxu+FlfAAd5PNumo7NfOv7EE=
133133
github.com/buildkite/bintest/v3 v3.3.0 h1:RTWcSaJRlOT6t/K311ejPf+0J3LE/QEODzVG3vlLnWo=
134134
github.com/buildkite/bintest/v3 v3.3.0/go.mod h1:btqpTsVODiJcb0NMdkkmtMQ6xoFc2W/nY5yy+3I0zcs=
135+
github.com/buildkite/go-buildkite/v4 v4.11.0 h1:rEvvUwITrqv433W9JWf6mj+NkkcM45s+ObhNs6C17i4=
136+
github.com/buildkite/go-buildkite/v4 v4.11.0/go.mod h1:DlebrRJqpZttXDjCW+MJ1QyW9AN++ZWt/UbPtKdbSSk=
135137
github.com/buildkite/go-pipeline v0.16.0 h1:wEgWUMRAgSg1ZnWOoA3AovtYYdTvN0dLY1zwUWmPP+4=
136138
github.com/buildkite/go-pipeline v0.16.0/go.mod h1:VE37qY3X5pmAKKUMoDZvPsHOQuyakB9cmXj9Qn6QasA=
137139
github.com/buildkite/interpolate v0.1.5 h1:v2Ji3voik69UZlbfoqzx+qfcsOKLA61nHdU79VV+tPU=
@@ -144,6 +146,8 @@ github.com/buildkite/test-engine-client v1.6.0 h1:yk/gdkFFU8B1+M16mxPNmxJgVoYffI
144146
github.com/buildkite/test-engine-client v1.6.0/go.mod h1:J6LrqenaJPfVCffiWW1/QxjICFb+OkqCvdCd7qAI0AE=
145147
github.com/buildkite/zstash v0.5.0 h1:e70mf8U2EjEB1eixXR78s6bsLgfo6bWLisVlRv58wCI=
146148
github.com/buildkite/zstash v0.5.0/go.mod h1:h70JfAEa2Ys1GDQQ6CNoKIMfMgJ0LZkNmQnzK710PHQ=
149+
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf h1:yxlp0s+Sge9UsKEK0Bsvjiopb9XRk+vxylmZ9eGBfm8=
150+
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
147151
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
148152
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
149153
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

internal/e2e/basic_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//go:build e2e
2+
3+
package e2e
4+
5+
import (
6+
"syscall"
7+
"testing"
8+
)
9+
10+
func TestBasicE2E(t *testing.T) {
11+
ctx := t.Context()
12+
tc := newTestCase(t, "steps:\n - command: echo hello world\n")
13+
14+
agent := tc.startAgent()
15+
build := tc.triggerBuild()
16+
state, err := tc.waitForBuild(ctx, build)
17+
if err != nil {
18+
t.Fatalf("tc.waitForBuild(build %s) error = %v", build.ID, err)
19+
}
20+
if got, want := state, "passed"; got != want {
21+
t.Errorf("Build state = %q, want %q", got, want)
22+
}
23+
24+
// TODO: add ability to inspect job logs
25+
26+
if err := agent.Process.Signal(syscall.SIGTERM); err != nil {
27+
t.Errorf("agent.Process.Signal(%d) = %v", syscall.SIGTERM, err)
28+
}
29+
}

internal/e2e/doc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Package e2e holds the end-to-end tests and test framework.
2+
// Test files are tagged go:build e2e so they are not run by default
3+
// (e.g. with plain `go test ./...`).
4+
package e2e

internal/e2e/testcase.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
//go:build e2e
2+
3+
package e2e
4+
5+
import (
6+
"cmp"
7+
"context"
8+
"fmt"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"slices"
13+
"strconv"
14+
"strings"
15+
"testing"
16+
"text/template"
17+
"time"
18+
19+
"github.com/buildkite/agent/v3/version"
20+
21+
"github.com/buildkite/go-buildkite/v4"
22+
)
23+
24+
var (
25+
// Filled in from secrets
26+
apiToken = os.Getenv("CI_E2E_TESTS_API_TOKEN")
27+
agentToken = os.Getenv("CI_E2E_TESTS_AGENT_TOKEN")
28+
29+
// E2E testing config
30+
agentPath = os.Getenv("CI_E2E_TESTS_AGENT_PATH")
31+
targetOrg = os.Getenv("CI_E2E_TESTS_TARGET_ORG")
32+
targetCluster = os.Getenv("CI_E2E_TESTS_TARGET_CLUSTER")
33+
34+
// Values from the Buildkite job running the tests
35+
jobID = cmp.Or(
36+
os.Getenv("BUILDKITE_JOB_ID"),
37+
strconv.FormatInt(time.Now().UnixNano(), 10),
38+
)
39+
authorEmail = os.Getenv("BUILDKITE_BUILD_CREATOR_EMAIL")
40+
authorName = os.Getenv("BUILDKITE_BUILD_CREATOR")
41+
)
42+
43+
const pipelineRepo = "https://github.com/buildkite/agent.git"
44+
45+
type cleanupFn = func(context.Context) error
46+
47+
var nopCleanup = func(context.Context) error { return nil }
48+
49+
// testCase bundles the information needed to run an end-to-end test.
50+
// Note that it embeds testing.TB - each test should create its own testCase.
51+
type testCase struct {
52+
testing.TB
53+
54+
fullName string
55+
bkClient *buildkite.Client
56+
pipelineConfig *template.Template
57+
queue *buildkite.ClusterQueue
58+
pipeline *buildkite.Pipeline
59+
}
60+
61+
// newTestCase creates a new test case with a given pipeline config template,
62+
// and sets up the temporary queue and pipeline to run it.
63+
// It also registers cleanups with t.Cleanup so that the queue and pipeline
64+
// are (usually) automatically deleted.
65+
// It calls t.Fatal to end the test early if there was a failure setting up.
66+
func newTestCase(t testing.TB, pipelineConfigTemplate string) *testCase {
67+
t.Helper()
68+
ctx := t.Context()
69+
70+
name := strings.ToLower(t.Name() + "-" + jobID)
71+
72+
tmpl, err := template.New("pipeline").Parse(pipelineConfigTemplate)
73+
if err != nil {
74+
t.Fatalf("template.New(pipeline).Parse(%q) error = %v", pipelineConfigTemplate, err)
75+
}
76+
77+
client, err := buildkite.NewClient(
78+
buildkite.WithTokenAuth(apiToken),
79+
buildkite.WithUserAgent("buildkite-agent-e2e-tests/0 "+version.UserAgent()),
80+
)
81+
if err != nil {
82+
t.Fatalf("buildkite.NewClient(...) error = %v", err)
83+
}
84+
85+
queue, cleanup, err := createQueue(ctx, client, name)
86+
if err != nil {
87+
t.Fatalf("Could not create cluster queue in org %q cluster %q: testHelper.createQueue(ctx, %q) error = %v", targetOrg, targetCluster, name, err)
88+
}
89+
t.Cleanup(func() {
90+
if err := cleanup(ctx); err != nil {
91+
t.Logf("Could not clean up cluster queue %q with id %s in org %q cluster %q: cleanup(ctx) error = %v", name, queue.ID, targetOrg, targetCluster, err)
92+
}
93+
})
94+
95+
var pipelineCfg strings.Builder
96+
tmplInput := map[string]string{"queue": name}
97+
if err := tmpl.Execute(&pipelineCfg, tmplInput); err != nil {
98+
t.Fatalf("Could not execute pipeline config template: tmpl.Execute(%q) error = %v", tmplInput, err)
99+
}
100+
101+
pipeline, cleanup, err := createPipeline(ctx, client, name, pipelineCfg.String())
102+
if err != nil {
103+
t.Fatalf("Could not create pipeline with the following config in org %q: testHelper.createPipeline(%q, pipelineCfg) error = %v\n%s", targetOrg, name, err, pipelineCfg.String())
104+
}
105+
t.Cleanup(func() {
106+
if err := cleanup(ctx); err != nil {
107+
t.Logf("Could not clean up pipeline %q (id = %s) in org %q: %v", name, pipeline.ID, targetOrg, err)
108+
}
109+
})
110+
111+
return &testCase{
112+
TB: t,
113+
fullName: name,
114+
bkClient: client,
115+
pipelineConfig: tmpl,
116+
queue: queue,
117+
pipeline: pipeline,
118+
}
119+
}
120+
121+
// triggerBuild creates a new build in the target pipeline. It returns the
122+
// build object. It also registers cleanups with t.Cleanup so that the build is
123+
// (usually) automatically cancelled if it is still running.
124+
// It calls t.Fatal if there was an error creating the build.
125+
func (tc *testCase) triggerBuild() *buildkite.Build {
126+
tc.Helper()
127+
ctx := tc.Context()
128+
129+
createBuild := buildkite.CreateBuild{
130+
Author: buildkite.Author{
131+
Email: authorEmail,
132+
Name: cmp.Or(authorName, "Agent E2E Tests"),
133+
},
134+
Commit: "HEAD",
135+
Branch: "main",
136+
Message: tc.fullName,
137+
}
138+
139+
build, _, err := tc.bkClient.Builds.Create(ctx, targetOrg, tc.pipeline.Slug, createBuild)
140+
if err != nil {
141+
tc.Fatalf("tc.bkClient.Builds.Create(ctx, %q, %q, %v) error = %v", targetOrg, tc.pipeline.Slug, createBuild, err)
142+
}
143+
144+
tc.Cleanup(func() {
145+
_, err := tc.bkClient.Builds.Cancel(ctx, targetOrg, tc.pipeline.Slug, build.ID)
146+
if err != nil {
147+
reasons := []string{
148+
"already finished",
149+
"already being canceled",
150+
"already been canceled",
151+
"No build found",
152+
}
153+
ignorable := slices.ContainsFunc(reasons, func(r string) bool {
154+
return strings.Contains(err.Error(), r)
155+
})
156+
if ignorable {
157+
return
158+
}
159+
tc.Logf("Couldn't cancel build %s: %v", build.ID, err)
160+
}
161+
})
162+
return &build
163+
}
164+
165+
// waitForBuild waits until the build is in a terminal state (passed, failed, canceled, etc). It polls the build once per second.
166+
func (tc *testCase) waitForBuild(ctx context.Context, build *buildkite.Build) (string, error) {
167+
for {
168+
state, _, err := tc.bkClient.Builds.Get(ctx, targetOrg, tc.pipeline.Slug, build.ID, nil)
169+
if err != nil {
170+
return "", err
171+
}
172+
switch state.State {
173+
case "passed", "failed", "canceled", "canceling":
174+
return state.State, nil
175+
176+
case "scheduled", "running":
177+
select {
178+
case <-time.After(time.Second):
179+
case <-ctx.Done():
180+
return "", ctx.Err()
181+
}
182+
183+
default:
184+
return state.State, fmt.Errorf("unknown build state %q", state.State)
185+
}
186+
}
187+
}
188+
189+
// createQueue creates a cluster queue for running an end-to-end test in.
190+
// The returned cleanup function deletes the queue and should be called after
191+
// the test is finished.
192+
func createQueue(ctx context.Context, client *buildkite.Client, name string) (*buildkite.ClusterQueue, cleanupFn, error) {
193+
cq, _, err := client.ClusterQueues.Create(ctx, targetOrg, targetCluster, buildkite.ClusterQueueCreate{
194+
Key: name,
195+
Description: "Buildkite Agent E2E Test",
196+
})
197+
if err != nil {
198+
return nil, nopCleanup, err
199+
}
200+
201+
cleanup := func(ctx context.Context) error {
202+
_, err := client.ClusterQueues.Delete(ctx, targetOrg, targetCluster, cq.ID)
203+
return err
204+
}
205+
return &cq, cleanup, nil
206+
}
207+
208+
// createPipeline creates a pipeline for running an end-to-end test in.
209+
// The returned cleanup function deletes the pipeline and should be called after
210+
// the test is finished.
211+
func createPipeline(ctx context.Context, client *buildkite.Client, name, config string) (*buildkite.Pipeline, cleanupFn, error) {
212+
p, _, err := client.Pipelines.Create(ctx, targetOrg, buildkite.CreatePipeline{
213+
Name: name,
214+
Repository: pipelineRepo,
215+
Description: "Buildkite Agent E2E Test",
216+
ProviderSettings: &buildkite.GitHubSettings{
217+
TriggerMode: "none",
218+
},
219+
Configuration: config,
220+
ClusterID: targetCluster,
221+
})
222+
if err != nil {
223+
return nil, nopCleanup, err
224+
}
225+
226+
cleanup := func(ctx context.Context) error {
227+
_, err := client.Pipelines.Delete(ctx, targetOrg, p.Slug)
228+
return err
229+
}
230+
return &p, cleanup, nil
231+
}
232+
233+
// startAgent starts a copy of the agent (at agentPath, using agentToken). It
234+
// registers cleanup functions that kill the agent and remove the various
235+
// directories (build path, hooks path, etc).
236+
func (tc *testCase) startAgent(extraArgs ...string) *exec.Cmd {
237+
tc.Helper()
238+
dir, err := os.MkdirTemp(tc.TempDir(), tc.fullName)
239+
if err != nil {
240+
tc.Fatalf("Couldn't create temporary agent dir: os.MkdirTemp(%q, %q) = %v", tc.TempDir(), tc.fullName, err)
241+
}
242+
tc.Cleanup(func() {
243+
if err := os.RemoveAll(dir); err != nil {
244+
tc.Logf("Couldn't clean up temporary agent dir: os.RemoveAll(%q) = %v", dir, err)
245+
}
246+
})
247+
buildPath := filepath.Join(dir, "builds")
248+
hooksPath := filepath.Join(dir, "hooks")
249+
socketsPath := filepath.Join(dir, "sockets")
250+
pluginsPath := filepath.Join(dir, "plugins")
251+
for _, path := range []string{buildPath, hooksPath, socketsPath, pluginsPath} {
252+
if err := os.Mkdir(path, 0o700); err != nil {
253+
tc.Fatalf("Couldn't create dir inside temporary agent dir: os.Mkdir(%q, %o) = %v", path, 0o700, err)
254+
}
255+
}
256+
257+
args := append([]string{
258+
"start",
259+
"--debug", "true",
260+
"--token", agentToken,
261+
"--name", tc.fullName,
262+
"--queue", tc.queue.Key,
263+
"--build-path", buildPath,
264+
"--hooks-path", hooksPath,
265+
"--sockets-path", socketsPath,
266+
"--plugins-path", pluginsPath,
267+
}, extraArgs...)
268+
269+
cmd := exec.CommandContext(tc.Context(), agentPath, args...)
270+
// Ensure minimal environment variable shenanigans by setting only these:
271+
cmd.Env = []string{
272+
"HOME=" + os.Getenv("HOME"),
273+
"PATH=" + os.Getenv("PATH"),
274+
}
275+
cmd.Stdout = os.Stderr
276+
cmd.Stderr = os.Stderr
277+
if err := cmd.Start(); err != nil {
278+
tc.Fatalf("Couldn't start agent command %v: %v", cmd, err)
279+
}
280+
tc.Cleanup(func() {
281+
if err := cmd.Cancel(); err != nil {
282+
tc.Logf("agent.Cancel() error = %v", err)
283+
}
284+
})
285+
return cmd
286+
}

0 commit comments

Comments
 (0)