Skip to content

Commit 16fa0d0

Browse files
authored
Clean the docker runtime (#254)
This PR cleans a few things on the docker runtime: - It moves the application of the overrides (user can override which images/binaries use during execution) out of the runtime since they only modify the manifest ([here](https://github.com/flashbots/builder-playground/pull/254/files#diff-b7bbff9c5fe2d0c8165900e164ed7b2471fc8f3d886ccee8786e1734901ebfa4L124)). - Fixes the component unit tests. The way it was done before, the inputs were not modified since the test framework would apply the default variables. ([here](https://github.com/flashbots/builder-playground/pull/254/files#diff-35602f032417ec14bb4f3092bc1cd29cced0b7a6b1a52b108f39a68227998219R29)). - Moved the logic for the interactive mode out of the docker runtime using the callback interface from #232 ([here](https://github.com/flashbots/builder-playground/pull/254/files#diff-b7bbff9c5fe2d0c8165900e164ed7b2471fc8f3d886ccee8786e1734901ebfa4L235)). It does not make sense that the Docker runtime owns the component that deals with the visualization on the terminal.
1 parent 007d4f1 commit 16fa0d0

File tree

8 files changed

+234
-279
lines changed

8 files changed

+234
-279
lines changed

main.go

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"log"
77
"os"
8-
"os/exec"
98
"sort"
109
"strings"
1110
"time"
@@ -228,28 +227,28 @@ func runIt(recipe playground.Recipe) error {
228227
return nil
229228
}
230229

231-
// validate that override is being applied to a service in the manifest
232-
for k := range overrides {
233-
if _, ok := svcManager.GetService(k); !ok {
234-
return fmt.Errorf("service '%s' in override not found in manifest", k)
235-
}
230+
if err := svcManager.ApplyOverrides(overrides); err != nil {
231+
return err
236232
}
237233

238234
cfg := &playground.RunnerConfig{
239235
Out: artifacts.Out,
240236
Manifest: svcManager,
241-
Overrides: overrides,
242-
Interactive: interactive,
243237
BindHostPortsLocally: !bindExternal,
244238
NetworkName: networkName,
245239
Labels: labels,
246240
LogInternally: !disableLogs,
247241
Platform: platform,
248242
}
249243

244+
if interactive {
245+
i := playground.NewInteractiveDisplay(svcManager)
246+
cfg.Callback = i.HandleUpdate
247+
}
248+
250249
// Add callback to log service updates in debug mode
251250
if logLevel == playground.LevelDebug {
252-
cfg.Callback = func(serviceName, update string) {
251+
cfg.Callback = func(serviceName string, update playground.TaskStatus) {
253252
log.Printf("[DEBUG] [%s] %s\n", serviceName, update)
254253
}
255254
}
@@ -294,7 +293,7 @@ func runIt(recipe playground.Recipe) error {
294293

295294
fmt.Printf("\nWaiting for network to be ready for transactions...\n")
296295
networkReadyStart := time.Now()
297-
if err := playground.CompleteReady(ctx, dockerRunner.Instances()); err != nil {
296+
if err := playground.CompleteReady(ctx, svcManager.Services); err != nil {
298297
dockerRunner.Stop()
299298
return fmt.Errorf("network not ready: %w", err)
300299
}
@@ -316,7 +315,7 @@ func runIt(recipe playground.Recipe) error {
316315
watchdogErr := make(chan error, 1)
317316
if watchdog {
318317
go func() {
319-
if err := playground.RunWatchdog(artifacts.Out, dockerRunner.Instances()); err != nil {
318+
if err := playground.RunWatchdog(artifacts.Out, svcManager.Services); err != nil {
320319
watchdogErr <- fmt.Errorf("watchdog failed: %w", err)
321320
}
322321
}()
@@ -343,26 +342,3 @@ func runIt(recipe playground.Recipe) error {
343342
}
344343
return nil
345344
}
346-
347-
func isExecutableValid(path string) error {
348-
// First check if file exists
349-
_, err := os.Stat(path)
350-
if err != nil {
351-
return fmt.Errorf("file does not exist or is inaccessible: %w", err)
352-
}
353-
354-
// Try to execute with a harmless flag or in a way that won't run the main program
355-
cmd := exec.Command(path, "--version")
356-
// Redirect output to /dev/null
357-
cmd.Stdout = nil
358-
cmd.Stderr = nil
359-
360-
if err := cmd.Start(); err != nil {
361-
return fmt.Errorf("cannot start executable: %w", err)
362-
}
363-
364-
// Immediately kill the process since we just want to test if it starts
365-
cmd.Process.Kill()
366-
367-
return nil
368-
}

playground/components.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,13 @@ func (o *OpGeth) Apply(manifest *Manifest) {
397397
})
398398
}
399399

400-
func opGethReadyFn(ctx context.Context, instance *instance) error {
401-
opGethURL := fmt.Sprintf("http://localhost:%d", instance.service.MustGetPort("http").HostPort)
400+
func opGethReadyFn(ctx context.Context, service *Service) error {
401+
opGethURL := fmt.Sprintf("http://localhost:%d", service.MustGetPort("http").HostPort)
402402
return waitForFirstBlock(ctx, opGethURL, 60*time.Second)
403403
}
404404

405-
func opGethWatchdogFn(out io.Writer, instance *instance, ctx context.Context) error {
406-
gethURL := fmt.Sprintf("http://localhost:%d", instance.service.MustGetPort("http").HostPort)
405+
func opGethWatchdogFn(out io.Writer, service *Service, ctx context.Context) error {
406+
gethURL := fmt.Sprintf("http://localhost:%d", service.MustGetPort("http").HostPort)
407407
return watchChainHead(out, gethURL, 2*time.Second)
408408
}
409409

@@ -456,7 +456,7 @@ func (r *RethEL) Apply(manifest *Manifest) {
456456
"--chain", "/data/genesis.json",
457457
"--datadir", "/data_reth",
458458
"--color", "never",
459-
"--ipcpath", "/data_reth/reth.ipc",
459+
"--ipcdisable",
460460
"--addr", "0.0.0.0",
461461
"--port", `{{Port "rpc" 30303}}`,
462462
// "--disable-discovery",
@@ -480,12 +480,12 @@ func (r *RethEL) Apply(manifest *Manifest) {
480480
logLevelToRethVerbosity(manifest.ctx.LogLevel),
481481
).
482482
WithRelease(rethELRelease).
483-
WithWatchdog(func(out io.Writer, instance *instance, ctx context.Context) error {
484-
rethURL := fmt.Sprintf("http://localhost:%d", instance.service.MustGetPort("http").HostPort)
483+
WithWatchdog(func(out io.Writer, service *Service, ctx context.Context) error {
484+
rethURL := fmt.Sprintf("http://localhost:%d", service.MustGetPort("http").HostPort)
485485
return watchChainHead(out, rethURL, 12*time.Second)
486486
}).
487-
WithReadyFn(func(ctx context.Context, instance *instance) error {
488-
elURL := fmt.Sprintf("http://localhost:%d", instance.service.MustGetPort("http").HostPort)
487+
WithReadyFn(func(ctx context.Context, service *Service) error {
488+
elURL := fmt.Sprintf("http://localhost:%d", service.MustGetPort("http").HostPort)
489489
return waitForFirstBlock(ctx, elURL, 60*time.Second)
490490
}).
491491
WithArtifact("/data/genesis.json", "genesis.json").
@@ -627,8 +627,8 @@ func (m *MevBoostRelay) Apply(manifest *Manifest) {
627627
}
628628
}
629629

630-
func mevboostRelayWatchdogFn(out io.Writer, instance *instance, ctx context.Context) error {
631-
beaconNodeURL := fmt.Sprintf("http://localhost:%d", instance.service.MustGetPort("http").HostPort)
630+
func mevboostRelayWatchdogFn(out io.Writer, service *Service, ctx context.Context) error {
631+
beaconNodeURL := fmt.Sprintf("http://localhost:%d", service.MustGetPort("http").HostPort)
632632

633633
watchGroup := newWatchGroup()
634634
watchGroup.watch(func() error {
@@ -681,8 +681,8 @@ func (o *OpReth) Apply(manifest *Manifest) {
681681
"--addr", "0.0.0.0",
682682
"--port", `{{Port "rpc" 30303}}`).
683683
WithRelease(opRethRelease).
684-
WithWatchdog(func(out io.Writer, instance *instance, ctx context.Context) error {
685-
rethURL := fmt.Sprintf("http://localhost:%d", instance.service.MustGetPort("http").HostPort)
684+
WithWatchdog(func(out io.Writer, service *Service, ctx context.Context) error {
685+
rethURL := fmt.Sprintf("http://localhost:%d", service.MustGetPort("http").HostPort)
686686
return watchChainHead(out, rethURL, 2*time.Second)
687687
}).
688688
WithArtifact("/data/jwtsecret", "jwtsecret").

playground/components_test.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ func TestRecipeOpstackSimple(t *testing.T) {
1919
tt := newTestFramework(t)
2020
defer tt.Close()
2121

22-
tt.test(&OpRecipe{})
22+
tt.test(&OpRecipe{}, nil)
2323
}
2424

2525
func TestRecipeOpstackExternalBuilder(t *testing.T) {
2626
tt := newTestFramework(t)
2727
defer tt.Close()
2828

29-
tt.test(&OpRecipe{
30-
externalBuilder: "http://host.docker.internal:4444",
29+
tt.test(&OpRecipe{}, []string{
30+
"--external-builder", "http://host.docker.internal:4444",
3131
})
3232
}
3333

@@ -36,8 +36,8 @@ func TestRecipeOpstackEnableForkAfter(t *testing.T) {
3636
defer tt.Close()
3737

3838
forkTime := uint64(10)
39-
manifest := tt.test(&OpRecipe{
40-
enableLatestFork: &forkTime,
39+
manifest := tt.test(&OpRecipe{}, []string{
40+
"--enable-latest-fork", "10",
4141
})
4242

4343
elService := manifest.MustGetService("op-geth")
@@ -49,23 +49,23 @@ func TestRecipeL1Simple(t *testing.T) {
4949
tt := newTestFramework(t)
5050
defer tt.Close()
5151

52-
tt.test(&L1Recipe{})
52+
tt.test(&L1Recipe{}, nil)
5353
}
5454

5555
func TestRecipeL1UseNativeReth(t *testing.T) {
5656
tt := newTestFramework(t)
5757
defer tt.Close()
5858

59-
tt.test(&L1Recipe{
60-
useNativeReth: true,
59+
tt.test(&L1Recipe{}, []string{
60+
"--use-native-reth",
6161
})
6262
}
6363

6464
func TestComponentBuilderHub(t *testing.T) {
6565
tt := newTestFramework(t)
6666
defer tt.Close()
6767

68-
tt.test(&BuilderHub{})
68+
tt.test(&BuilderHub{}, nil)
6969

7070
// TODO: Calling the port directly on the host machine will not work once we have multiple
7171
// tests running in parallel
@@ -83,7 +83,7 @@ func newTestFramework(t *testing.T) *testFramework {
8383
return &testFramework{t: t}
8484
}
8585

86-
func (tt *testFramework) test(s ServiceGen) *Manifest {
86+
func (tt *testFramework) test(s ServiceGen, args []string) *Manifest {
8787
t := tt.t
8888

8989
// use the name of the repo and the current timestamp to generate
@@ -104,13 +104,14 @@ func (tt *testFramework) test(s ServiceGen) *Manifest {
104104
}
105105

106106
o := &output{
107-
dst: e2eTestDir,
107+
dst: e2eTestDir,
108+
homeDir: filepath.Join(e2eTestDir, "artifacts"),
108109
}
109110

110111
if recipe, ok := s.(Recipe); ok {
111112
// We have to parse the flags since they are used to set the
112113
// default values for the recipe inputs
113-
err := recipe.Flags().Parse([]string{})
114+
err := recipe.Flags().Parse(args)
114115
require.NoError(t, err)
115116

116117
_, err = recipe.Artifacts().OutputDir(e2eTestDir).Build()
@@ -142,7 +143,7 @@ func (tt *testFramework) test(s ServiceGen) *Manifest {
142143
require.NoError(t, err)
143144

144145
require.NoError(t, dockerRunner.WaitForReady(context.Background(), 20*time.Second))
145-
require.NoError(t, CompleteReady(context.Background(), dockerRunner.Instances()))
146+
require.NoError(t, CompleteReady(context.Background(), svcManager.Services))
146147

147148
return svcManager
148149
}

playground/interactive.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package playground
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
"github.com/charmbracelet/bubbles/spinner"
8+
"github.com/charmbracelet/lipgloss"
9+
)
10+
11+
type InteractiveDisplay struct {
12+
manifest *Manifest
13+
taskUpdateCh chan struct{}
14+
status sync.Map
15+
}
16+
17+
type taskUI struct {
18+
tasks map[string]string
19+
spinners map[string]spinner.Model
20+
style lipgloss.Style
21+
}
22+
23+
func NewInteractiveDisplay(manifest *Manifest) *InteractiveDisplay {
24+
i := &InteractiveDisplay{
25+
manifest: manifest,
26+
taskUpdateCh: make(chan struct{}),
27+
}
28+
29+
go i.printStatus()
30+
return i
31+
}
32+
33+
func (i *InteractiveDisplay) HandleUpdate(serviceName string, status TaskStatus) {
34+
i.status.Store(serviceName, status)
35+
36+
select {
37+
case i.taskUpdateCh <- struct{}{}:
38+
default:
39+
}
40+
}
41+
42+
func (i *InteractiveDisplay) printStatus() {
43+
fmt.Print("\033[s")
44+
lineOffset := 0
45+
46+
// Get ordered service names from manifest
47+
orderedServices := make([]string, 0, len(i.manifest.Services))
48+
for _, svc := range i.manifest.Services {
49+
orderedServices = append(orderedServices, svc.Name)
50+
}
51+
52+
// Initialize UI state
53+
ui := taskUI{
54+
tasks: make(map[string]string),
55+
spinners: make(map[string]spinner.Model),
56+
style: lipgloss.NewStyle(),
57+
}
58+
59+
// Initialize spinners for each service
60+
for _, name := range orderedServices {
61+
sp := spinner.New()
62+
sp.Spinner = spinner.Dot
63+
ui.spinners[name] = sp
64+
}
65+
66+
tickSpinner := func(name string) spinner.Model {
67+
sp := ui.spinners[name]
68+
sp.Tick()
69+
ui.spinners[name] = sp
70+
return sp
71+
}
72+
73+
for {
74+
select {
75+
case <-i.taskUpdateCh:
76+
// Clear the previous lines and move cursor up
77+
if lineOffset > 0 {
78+
fmt.Printf("\033[%dA", lineOffset)
79+
fmt.Print("\033[J")
80+
}
81+
82+
lineOffset = 0
83+
// Use ordered services instead of ranging over map
84+
for _, name := range orderedServices {
85+
status, ok := i.status.Load(name)
86+
if !ok {
87+
status = TaskStatusPending
88+
}
89+
90+
var statusLine string
91+
switch status {
92+
case TaskStatusStarted, TaskStatusHealthy:
93+
sp := tickSpinner(name)
94+
statusLine = ui.style.Foreground(lipgloss.Color("2")).Render(fmt.Sprintf("%s [%s] Running", sp.View(), name))
95+
case TaskStatusDie:
96+
statusLine = ui.style.Foreground(lipgloss.Color("1")).Render(fmt.Sprintf("✗ [%s] Failed", name))
97+
case TaskStatusPulled, TaskStatusPending:
98+
sp := tickSpinner(name)
99+
statusLine = ui.style.Foreground(lipgloss.Color("3")).Render(fmt.Sprintf("%s [%s] Pending", sp.View(), name))
100+
case TaskStatusPulling:
101+
sp := tickSpinner(name)
102+
statusLine = ui.style.Foreground(lipgloss.Color("3")).Render(fmt.Sprintf("%s [%s] Pulling", sp.View(), name))
103+
default:
104+
panic(fmt.Sprintf("BUG: status '%s' not handled", name))
105+
}
106+
107+
fmt.Println(statusLine)
108+
lineOffset++
109+
}
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)