Skip to content

Commit 6bafbb7

Browse files
feat(cli): add prebuilds scaletest command (#20600)
Closes coder/internal#914
1 parent b31d098 commit 6bafbb7

File tree

2 files changed

+298
-0
lines changed

2 files changed

+298
-0
lines changed

cli/exp_scaletest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func (r *RootCmd) scaletestCmd() *serpent.Command {
6565
r.scaletestAutostart(),
6666
r.scaletestNotifications(),
6767
r.scaletestSMTP(),
68+
r.scaletestPrebuilds(),
6869
},
6970
}
7071

cli/exp_scaletest_prebuilds.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
//go:build !slim
2+
3+
package cli
4+
5+
import (
6+
"fmt"
7+
"net/http"
8+
"os/signal"
9+
"strconv"
10+
"sync"
11+
"time"
12+
13+
"github.com/prometheus/client_golang/prometheus"
14+
"github.com/prometheus/client_golang/prometheus/promhttp"
15+
"golang.org/x/xerrors"
16+
17+
"github.com/coder/coder/v2/codersdk"
18+
"github.com/coder/coder/v2/scaletest/harness"
19+
"github.com/coder/coder/v2/scaletest/prebuilds"
20+
"github.com/coder/quartz"
21+
"github.com/coder/serpent"
22+
)
23+
24+
func (r *RootCmd) scaletestPrebuilds() *serpent.Command {
25+
var (
26+
numTemplates int64
27+
numPresets int64
28+
numPresetPrebuilds int64
29+
templateVersionJobTimeout time.Duration
30+
prebuildWorkspaceTimeout time.Duration
31+
noCleanup bool
32+
33+
tracingFlags = &scaletestTracingFlags{}
34+
timeoutStrategy = &timeoutFlags{}
35+
cleanupStrategy = newScaletestCleanupStrategy()
36+
output = &scaletestOutputFlags{}
37+
prometheusFlags = &scaletestPrometheusFlags{}
38+
)
39+
40+
cmd := &serpent.Command{
41+
Use: "prebuilds",
42+
Short: "Creates prebuild workspaces on the Coder server.",
43+
Handler: func(inv *serpent.Invocation) error {
44+
ctx := inv.Context()
45+
client, err := r.InitClient(inv)
46+
if err != nil {
47+
return err
48+
}
49+
50+
notifyCtx, stop := signal.NotifyContext(ctx, StopSignals...)
51+
defer stop()
52+
ctx = notifyCtx
53+
54+
me, err := requireAdmin(ctx, client)
55+
if err != nil {
56+
return err
57+
}
58+
59+
client.HTTPClient = &http.Client{
60+
Transport: &codersdk.HeaderTransport{
61+
Transport: http.DefaultTransport,
62+
Header: map[string][]string{
63+
codersdk.BypassRatelimitHeader: {"true"},
64+
},
65+
},
66+
}
67+
68+
if numTemplates <= 0 {
69+
return xerrors.Errorf("--num-templates must be greater than 0")
70+
}
71+
if numPresets <= 0 {
72+
return xerrors.Errorf("--num-presets must be greater than 0")
73+
}
74+
if numPresetPrebuilds <= 0 {
75+
return xerrors.Errorf("--num-preset-prebuilds must be greater than 0")
76+
}
77+
78+
outputs, err := output.parse()
79+
if err != nil {
80+
return xerrors.Errorf("parse output flags: %w", err)
81+
}
82+
83+
tracerProvider, closeTracing, tracingEnabled, err := tracingFlags.provider(ctx)
84+
if err != nil {
85+
return xerrors.Errorf("create tracer provider: %w", err)
86+
}
87+
defer func() {
88+
_, _ = fmt.Fprintln(inv.Stderr, "\nUploading traces...")
89+
if err := closeTracing(ctx); err != nil {
90+
_, _ = fmt.Fprintf(inv.Stderr, "\nError uploading traces: %+v\n", err)
91+
}
92+
_, _ = fmt.Fprintf(inv.Stderr, "Waiting %s for prometheus metrics to be scraped\n", prometheusFlags.Wait)
93+
<-time.After(prometheusFlags.Wait)
94+
}()
95+
tracer := tracerProvider.Tracer(scaletestTracerName)
96+
97+
reg := prometheus.NewRegistry()
98+
metrics := prebuilds.NewMetrics(reg)
99+
100+
logger := inv.Logger
101+
prometheusSrvClose := ServeHandler(ctx, logger, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), prometheusFlags.Address, "prometheus")
102+
defer prometheusSrvClose()
103+
104+
err = client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{
105+
ReconciliationPaused: true,
106+
})
107+
if err != nil {
108+
return xerrors.Errorf("pause prebuilds: %w", err)
109+
}
110+
111+
setupBarrier := new(sync.WaitGroup)
112+
setupBarrier.Add(int(numTemplates))
113+
creationBarrier := new(sync.WaitGroup)
114+
creationBarrier.Add(int(numTemplates))
115+
deletionSetupBarrier := new(sync.WaitGroup)
116+
deletionSetupBarrier.Add(1)
117+
deletionBarrier := new(sync.WaitGroup)
118+
deletionBarrier.Add(int(numTemplates))
119+
120+
th := harness.NewTestHarness(timeoutStrategy.wrapStrategy(harness.ConcurrentExecutionStrategy{}), cleanupStrategy.toStrategy())
121+
122+
for i := range numTemplates {
123+
id := strconv.Itoa(int(i))
124+
cfg := prebuilds.Config{
125+
OrganizationID: me.OrganizationIDs[0],
126+
NumPresets: int(numPresets),
127+
NumPresetPrebuilds: int(numPresetPrebuilds),
128+
TemplateVersionJobTimeout: templateVersionJobTimeout,
129+
PrebuildWorkspaceTimeout: prebuildWorkspaceTimeout,
130+
Metrics: metrics,
131+
SetupBarrier: setupBarrier,
132+
CreationBarrier: creationBarrier,
133+
DeletionSetupBarrier: deletionSetupBarrier,
134+
DeletionBarrier: deletionBarrier,
135+
Clock: quartz.NewReal(),
136+
}
137+
err := cfg.Validate()
138+
if err != nil {
139+
return xerrors.Errorf("validate config: %w", err)
140+
}
141+
142+
var runner harness.Runnable = prebuilds.NewRunner(client, cfg)
143+
if tracingEnabled {
144+
runner = &runnableTraceWrapper{
145+
tracer: tracer,
146+
spanName: fmt.Sprintf("prebuilds/%s", id),
147+
runner: runner,
148+
}
149+
}
150+
151+
th.AddRun("prebuilds", id, runner)
152+
}
153+
154+
_, _ = fmt.Fprintf(inv.Stderr, "Creating %d templates with %d presets and %d prebuilds per preset...\n",
155+
numTemplates, numPresets, numPresetPrebuilds)
156+
_, _ = fmt.Fprintf(inv.Stderr, "Total expected prebuilds: %d\n", numTemplates*numPresets*numPresetPrebuilds)
157+
158+
testCtx, testCancel := timeoutStrategy.toContext(ctx)
159+
defer testCancel()
160+
161+
runErrCh := make(chan error, 1)
162+
go func() {
163+
runErrCh <- th.Run(testCtx)
164+
}()
165+
166+
_, _ = fmt.Fprintln(inv.Stderr, "Waiting for all templates to be created...")
167+
setupBarrier.Wait()
168+
_, _ = fmt.Fprintln(inv.Stderr, "All templates created")
169+
170+
err = client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{
171+
ReconciliationPaused: false,
172+
})
173+
if err != nil {
174+
return xerrors.Errorf("resume prebuilds: %w", err)
175+
}
176+
177+
_, _ = fmt.Fprintln(inv.Stderr, "Waiting for all prebuilds to be created...")
178+
creationBarrier.Wait()
179+
_, _ = fmt.Fprintln(inv.Stderr, "All prebuilds created")
180+
181+
err = client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{
182+
ReconciliationPaused: true,
183+
})
184+
if err != nil {
185+
return xerrors.Errorf("pause prebuilds before deletion: %w", err)
186+
}
187+
188+
_, _ = fmt.Fprintln(inv.Stderr, "Prebuilds paused, signaling runners to prepare for deletion")
189+
deletionSetupBarrier.Done()
190+
191+
_, _ = fmt.Fprintln(inv.Stderr, "Waiting for all templates to be updated with 0 prebuilds...")
192+
deletionBarrier.Wait()
193+
_, _ = fmt.Fprintln(inv.Stderr, "All templates updated")
194+
195+
err = client.PutPrebuildsSettings(ctx, codersdk.PrebuildsSettings{
196+
ReconciliationPaused: false,
197+
})
198+
if err != nil {
199+
return xerrors.Errorf("resume prebuilds for deletion: %w", err)
200+
}
201+
202+
_, _ = fmt.Fprintln(inv.Stderr, "Waiting for all prebuilds to be deleted...")
203+
err = <-runErrCh
204+
if err != nil {
205+
return xerrors.Errorf("run test harness (harness failure, not a test failure): %w", err)
206+
}
207+
208+
// If the command was interrupted, skip cleanup & stats
209+
if notifyCtx.Err() != nil {
210+
return notifyCtx.Err()
211+
}
212+
213+
res := th.Results()
214+
for _, o := range outputs {
215+
err = o.write(res, inv.Stdout)
216+
if err != nil {
217+
return xerrors.Errorf("write output %q to %q: %w", o.format, o.path, err)
218+
}
219+
}
220+
221+
if !noCleanup {
222+
_, _ = fmt.Fprintln(inv.Stderr, "\nStarting cleanup (deleting templates)...")
223+
224+
cleanupCtx, cleanupCancel := cleanupStrategy.toContext(ctx)
225+
defer cleanupCancel()
226+
227+
err = th.Cleanup(cleanupCtx)
228+
if err != nil {
229+
return xerrors.Errorf("cleanup tests: %w", err)
230+
}
231+
232+
// If the cleanup was interrupted, skip stats
233+
if notifyCtx.Err() != nil {
234+
return notifyCtx.Err()
235+
}
236+
}
237+
238+
if res.TotalFail > 0 {
239+
return xerrors.New("prebuild creation test failed, see above for more details")
240+
}
241+
242+
return nil
243+
},
244+
}
245+
246+
cmd.Options = serpent.OptionSet{
247+
{
248+
Flag: "num-templates",
249+
Env: "CODER_SCALETEST_PREBUILDS_NUM_TEMPLATES",
250+
Default: "1",
251+
Description: "Number of templates to create for the test.",
252+
Value: serpent.Int64Of(&numTemplates),
253+
},
254+
{
255+
Flag: "num-presets",
256+
Env: "CODER_SCALETEST_PREBUILDS_NUM_PRESETS",
257+
Default: "1",
258+
Description: "Number of presets per template.",
259+
Value: serpent.Int64Of(&numPresets),
260+
},
261+
{
262+
Flag: "num-preset-prebuilds",
263+
Env: "CODER_SCALETEST_PREBUILDS_NUM_PRESET_PREBUILDS",
264+
Default: "1",
265+
Description: "Number of prebuilds per preset.",
266+
Value: serpent.Int64Of(&numPresetPrebuilds),
267+
},
268+
{
269+
Flag: "template-version-job-timeout",
270+
Env: "CODER_SCALETEST_PREBUILDS_TEMPLATE_VERSION_JOB_TIMEOUT",
271+
Default: "5m",
272+
Description: "Timeout for template version provisioning jobs.",
273+
Value: serpent.DurationOf(&templateVersionJobTimeout),
274+
},
275+
{
276+
Flag: "prebuild-workspace-timeout",
277+
Env: "CODER_SCALETEST_PREBUILDS_WORKSPACE_TIMEOUT",
278+
Default: "10m",
279+
Description: "Timeout for all prebuild workspaces to be created/deleted.",
280+
Value: serpent.DurationOf(&prebuildWorkspaceTimeout),
281+
},
282+
{
283+
Flag: "skip-cleanup",
284+
Env: "CODER_SCALETEST_PREBUILDS_SKIP_CLEANUP",
285+
Description: "Skip cleanup (deletion test) and leave resources intact.",
286+
Value: serpent.BoolOf(&noCleanup),
287+
},
288+
}
289+
290+
tracingFlags.attach(&cmd.Options)
291+
timeoutStrategy.attach(&cmd.Options)
292+
cleanupStrategy.attach(&cmd.Options)
293+
output.attach(&cmd.Options)
294+
prometheusFlags.attach(&cmd.Options)
295+
296+
return cmd
297+
}

0 commit comments

Comments
 (0)