Skip to content

Commit 60e9818

Browse files
authored
Add detached mode (#214)
This PR adds a detached mode to the recipe execution to run the recipe on the background. It also adds a helper `clean` command that removes the recipe.
1 parent de2eb70 commit 60e9818

File tree

8 files changed

+85
-243
lines changed

8 files changed

+85
-243
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ $ builder-playground cook l1 --latest-fork --output ~/my-builder-testnet --genes
6262
## Common Options
6363

6464
- `--output` (string): The directory where the chain data and artifacts are stored. Defaults to `$HOME/.playground/devnet`
65+
- `--detached` (bool): Run the recipes in the background. Defaults to `false`.
6566
- `--genesis-delay` (int): The delay in seconds before the genesis block is created. Defaults to `10` seconds
6667
- `--watchdog` (bool): Enable the watchdog service to monitor the specific chain
6768
- `--dry-run` (bool): Generates the artifacts and manifest but does not deploy anything (also enabled with the `--mise-en-place` flag)
@@ -150,6 +151,14 @@ $ builder-playground inspect op-geth authrpc
150151

151152
This command starts a `tcpflow` container in the same network interface as the service and captures the traffic to the specified port.
152153

154+
## Clean
155+
156+
Removes a recipe running in the background
157+
158+
```bash
159+
$ builder-playground clean [--output ./output]
160+
```
161+
153162
## Internals
154163

155164
### Execution Flow

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/spf13/pflag v1.0.6
2020
github.com/stretchr/testify v1.10.0
2121
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.3
22+
golang.org/x/sync v0.13.0
2223
gopkg.in/yaml.v2 v2.4.0
2324
)
2425

@@ -152,7 +153,6 @@ require (
152153
golang.org/x/crypto v0.37.0 // indirect
153154
golang.org/x/net v0.38.0 // indirect
154155
golang.org/x/oauth2 v0.26.0 // indirect
155-
golang.org/x/sync v0.13.0 // indirect
156156
golang.org/x/sys v0.32.0 // indirect
157157
golang.org/x/term v0.31.0 // indirect
158158
golang.org/x/text v0.24.0 // indirect

main.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
"time"
1414

1515
"github.com/flashbots/builder-playground/playground"
16-
"github.com/flashbots/builder-playground/playground/cmd"
16+
"github.com/google/uuid"
1717
"github.com/spf13/cobra"
1818
)
1919

@@ -34,7 +34,7 @@ var platform string
3434
var contenderEnabled bool
3535
var contenderArgs []string
3636
var contenderTarget string
37-
var readyzPort int
37+
var detached bool
3838

3939
var rootCmd = &cobra.Command{
4040
Use: "playground",
@@ -57,6 +57,22 @@ var cookCmd = &cobra.Command{
5757
},
5858
}
5959

60+
var cleanCmd = &cobra.Command{
61+
Use: "clean",
62+
Short: "Clean a recipe",
63+
RunE: func(cmd *cobra.Command, args []string) error {
64+
manifest, err := playground.ReadManifest(outputFlag)
65+
if err != nil {
66+
return err
67+
}
68+
if err := playground.StopContainersBySessionID(manifest.ID); err != nil {
69+
return err
70+
}
71+
fmt.Println("The recipe has been stopped and cleaned.")
72+
return nil
73+
},
74+
}
75+
6076
var inspectCmd = &cobra.Command{
6177
Use: "inspect",
6278
Short: "Inspect a connection between two services",
@@ -120,16 +136,16 @@ func main() {
120136
recipeCmd.Flags().BoolVar(&contenderEnabled, "contender", false, "spam nodes with contender")
121137
recipeCmd.Flags().StringArrayVar(&contenderArgs, "contender.arg", []string{}, "add/override contender CLI flags")
122138
recipeCmd.Flags().StringVar(&contenderTarget, "contender.target", "", "override the node that contender spams -- accepts names like \"el\"")
123-
recipeCmd.Flags().IntVar(&readyzPort, "readyz-port", 0, "port for readyz HTTP endpoint (0 to disable)")
139+
recipeCmd.Flags().BoolVar(&detached, "detached", false, "Detached mode: Run the recipes in the background")
124140

125141
cookCmd.AddCommand(recipeCmd)
126142
}
127143

128-
cmd.InitWaitReadyCmd()
129-
130144
rootCmd.AddCommand(cookCmd)
131145
rootCmd.AddCommand(inspectCmd)
132-
rootCmd.AddCommand(cmd.WaitReadyCmd)
146+
147+
rootCmd.AddCommand(cleanCmd)
148+
cleanCmd.Flags().StringVar(&outputFlag, "output", "", "Output folder for the artifacts")
133149

134150
if err := rootCmd.Execute(); err != nil {
135151
fmt.Println(err)
@@ -172,7 +188,10 @@ func runIt(recipe playground.Recipe) error {
172188
TargetChain: contenderTarget,
173189
},
174190
}
191+
175192
svcManager := playground.NewManifest(exCtx, artifacts.Out)
193+
svcManager.ID = uuid.New().String()
194+
176195
recipe.Apply(svcManager)
177196
if err := svcManager.Validate(); err != nil {
178197
return fmt.Errorf("failed to validate manifest: %w", err)
@@ -184,6 +203,10 @@ func runIt(recipe playground.Recipe) error {
184203
return err
185204
}
186205

206+
if err := svcManager.Validate(); err != nil {
207+
return fmt.Errorf("failed to validate manifest: %w", err)
208+
}
209+
187210
// save the manifest.json file
188211
if err := svcManager.SaveJson(); err != nil {
189212
return fmt.Errorf("failed to save manifest: %w", err)
@@ -231,16 +254,6 @@ func runIt(recipe playground.Recipe) error {
231254
cancel()
232255
}()
233256

234-
var readyzServer *playground.ReadyzServer
235-
if readyzPort > 0 {
236-
readyzServer = playground.NewReadyzServer(dockerRunner.Instances(), readyzPort)
237-
if err := readyzServer.Start(); err != nil {
238-
return fmt.Errorf("failed to start readyz server: %w", err)
239-
}
240-
defer readyzServer.Stop()
241-
fmt.Printf("Readyz endpoint available at http://localhost:%d/readyz\n", readyzPort)
242-
}
243-
244257
if err := dockerRunner.Run(); err != nil {
245258
dockerRunner.Stop()
246259
return fmt.Errorf("failed to run docker: %w", err)
@@ -289,6 +302,10 @@ func runIt(recipe playground.Recipe) error {
289302
}
290303
}
291304

305+
if detached {
306+
return nil
307+
}
308+
292309
watchdogErr := make(chan error, 1)
293310
if watchdog {
294311
go func() {

playground/cmd/wait_ready.go

Lines changed: 0 additions & 93 deletions
This file was deleted.

playground/local_runner.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/docker/docker/client"
2424
"github.com/docker/docker/pkg/stdcopy"
2525
"github.com/ethereum/go-ethereum/log"
26-
"github.com/google/uuid"
26+
"golang.org/x/sync/errgroup"
2727
"gopkg.in/yaml.v2"
2828
)
2929

@@ -69,10 +69,6 @@ type LocalRunner struct {
6969
// wether to bind the ports to the local interface
7070
bindHostPortsLocally bool
7171

72-
// sessionID is a random sequence that is used to identify the session
73-
// it is used to identify the containers in the cleanup process
74-
sessionID string
75-
7672
// networkName is the name of the network to use for the services
7773
networkName string
7874

@@ -220,7 +216,6 @@ func NewLocalRunner(cfg *RunnerConfig) (*LocalRunner, error) {
220216
taskUpdateCh: make(chan struct{}),
221217
exitErr: make(chan error, 2),
222218
bindHostPortsLocally: cfg.BindHostPortsLocally,
223-
sessionID: uuid.New().String(),
224219
networkName: cfg.NetworkName,
225220
instances: instances,
226221
labels: cfg.Labels,
@@ -378,7 +373,7 @@ func (d *LocalRunner) ExitErr() <-chan error {
378373
func (d *LocalRunner) Stop() error {
379374
// only stop the containers that belong to this session
380375
containers, err := d.client.ContainerList(context.Background(), container.ListOptions{
381-
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", d.sessionID))),
376+
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", d.manifest.ID))),
382377
})
383378
if err != nil {
384379
return fmt.Errorf("error getting container list: %w", err)
@@ -615,7 +610,7 @@ func (d *LocalRunner) toDockerComposeService(s *Service) (map[string]interface{}
615610
// It is important to use the playground label to identify the containers
616611
// during the cleanup process
617612
"playground": "true",
618-
"playground.session": d.sessionID,
613+
"playground.session": d.manifest.ID,
619614
"service": s.Name,
620615
}
621616

@@ -883,7 +878,7 @@ func (d *LocalRunner) trackLogs(serviceName string, containerID string) error {
883878

884879
func (d *LocalRunner) trackContainerStatusAndLogs() {
885880
eventCh, errCh := d.client.Events(context.Background(), events.ListOptions{
886-
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", d.sessionID))),
881+
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", d.manifest.ID))),
887882
})
888883

889884
for {
@@ -1030,3 +1025,40 @@ func (d *LocalRunner) Run() error {
10301025
}
10311026
return nil
10321027
}
1028+
1029+
// StopContainersBySessionID removes all Docker containers associated with a specific playground session ID.
1030+
// This is a standalone utility function used by the clean command to stop containers without requiring
1031+
// a LocalRunner instance or manifest reference.
1032+
//
1033+
// TODO: Refactor to reduce code duplication with LocalRunner.Stop()
1034+
// Consider creating a shared dockerClient wrapper with helper methods for container management
1035+
// that both LocalRunner and this function can use.
1036+
func StopContainersBySessionID(id string) error {
1037+
client, err := newDockerClient()
1038+
if err != nil {
1039+
return err
1040+
}
1041+
1042+
containers, err := client.ContainerList(context.Background(), container.ListOptions{
1043+
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", id))),
1044+
})
1045+
if err != nil {
1046+
return fmt.Errorf("error getting container list: %w", err)
1047+
}
1048+
1049+
g := new(errgroup.Group)
1050+
for _, cont := range containers {
1051+
g.Go(func() error {
1052+
if err := client.ContainerRemove(context.Background(), cont.ID, container.RemoveOptions{
1053+
RemoveVolumes: true,
1054+
RemoveLinks: false,
1055+
Force: true,
1056+
}); err != nil {
1057+
return fmt.Errorf("error removing container: %w", err)
1058+
}
1059+
return nil
1060+
})
1061+
}
1062+
1063+
return g.Wait()
1064+
}

playground/manifest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Recipe interface {
2828
// Manifest describes a list of services and their dependencies
2929
type Manifest struct {
3030
ctx *ExContext
31+
ID string `json:"session_id"`
3132

3233
// list of Services
3334
Services []*Service `json:"services"`

0 commit comments

Comments
 (0)