Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ $ builder-playground cook l1 --latest-fork --output ~/my-builder-testnet --genes
## Common Options

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

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

## Clean

Removes a recipe running in the background

```bash
$ builder-playground clean [--output ./output]
```

## Internals

### Execution Flow
Expand Down
48 changes: 32 additions & 16 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"time"

"github.com/flashbots/builder-playground/playground"
"github.com/flashbots/builder-playground/playground/cmd"
"github.com/google/uuid"
"github.com/spf13/cobra"
)

Expand All @@ -34,7 +34,7 @@ var platform string
var contenderEnabled bool
var contenderArgs []string
var contenderTarget string
var readyzPort int
var detached bool

var rootCmd = &cobra.Command{
Use: "playground",
Expand All @@ -57,6 +57,22 @@ var cookCmd = &cobra.Command{
},
}

var cleanCmd = &cobra.Command{
Use: "clean",
Short: "Clean a recipe",
RunE: func(cmd *cobra.Command, args []string) error {
manifest, err := playground.ReadManifest(outputFlag)
if err != nil {
return err
}
if err := playground.StopContainersBySessionID(manifest.ID); err != nil {
return err
}
fmt.Println("The recipe has been stopped and cleaned.")
return nil
},
}

var artifactsCmd = &cobra.Command{
Use: "artifacts",
Short: "List available artifacts",
Expand Down Expand Up @@ -187,7 +203,7 @@ func main() {
recipeCmd.Flags().BoolVar(&contenderEnabled, "contender", false, "spam nodes with contender")
recipeCmd.Flags().StringArrayVar(&contenderArgs, "contender.arg", []string{}, "add/override contender CLI flags")
recipeCmd.Flags().StringVar(&contenderTarget, "contender.target", "", "override the node that contender spams -- accepts names like \"el\"")
recipeCmd.Flags().IntVar(&readyzPort, "readyz-port", 0, "port for readyz HTTP endpoint (0 to disable)")
recipeCmd.Flags().BoolVar(&detached, "detached", false, "Detached mode: Run the recipes in the background")

cookCmd.AddCommand(recipeCmd)
}
Expand All @@ -196,13 +212,13 @@ func main() {
artifactsCmd.Flags().StringVar(&outputFlag, "output", "", "Output folder for the artifacts")
artifactsAllCmd.Flags().StringVar(&outputFlag, "output", "", "Output folder for the artifacts")

cmd.InitWaitReadyCmd()

rootCmd.AddCommand(cookCmd)
rootCmd.AddCommand(artifactsCmd)
rootCmd.AddCommand(artifactsAllCmd)
rootCmd.AddCommand(inspectCmd)
rootCmd.AddCommand(cmd.WaitReadyCmd)

rootCmd.AddCommand(cleanCmd)
cleanCmd.Flags().StringVar(&outputFlag, "output", "", "Output folder for the artifacts")

if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
Expand Down Expand Up @@ -245,6 +261,8 @@ func runIt(recipe playground.Recipe) error {
TargetChain: contenderTarget,
},
}, artifacts)
svcManager.ID = uuid.New().String()

if err := svcManager.Validate(); err != nil {
return fmt.Errorf("failed to validate manifest: %w", err)
}
Expand All @@ -255,6 +273,10 @@ func runIt(recipe playground.Recipe) error {
return err
}

if err := svcManager.Validate(); err != nil {
return fmt.Errorf("failed to validate manifest: %w", err)
}

// save the manifest.json file
if err := svcManager.SaveJson(); err != nil {
return fmt.Errorf("failed to save manifest: %w", err)
Expand Down Expand Up @@ -302,16 +324,6 @@ func runIt(recipe playground.Recipe) error {
cancel()
}()

var readyzServer *playground.ReadyzServer
if readyzPort > 0 {
readyzServer = playground.NewReadyzServer(dockerRunner.Instances(), readyzPort)
if err := readyzServer.Start(); err != nil {
return fmt.Errorf("failed to start readyz server: %w", err)
}
defer readyzServer.Stop()
fmt.Printf("Readyz endpoint available at http://localhost:%d/readyz\n", readyzPort)
}

if err := dockerRunner.Run(); err != nil {
dockerRunner.Stop()
return fmt.Errorf("failed to run docker: %w", err)
Expand Down Expand Up @@ -360,6 +372,10 @@ func runIt(recipe playground.Recipe) error {
}
}

if detached {
return nil
}

watchdogErr := make(chan error, 1)
if watchdog {
go func() {
Expand Down
93 changes: 0 additions & 93 deletions playground/cmd/wait_ready.go

This file was deleted.

55 changes: 46 additions & 9 deletions playground/local_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/ethereum/go-ethereum/log"
"github.com/google/uuid"
"gopkg.in/yaml.v2"
)

Expand Down Expand Up @@ -68,10 +67,6 @@ type LocalRunner struct {
// wether to bind the ports to the local interface
bindHostPortsLocally bool

// sessionID is a random sequence that is used to identify the session
// it is used to identify the containers in the cleanup process
sessionID string

// networkName is the name of the network to use for the services
networkName string

Expand Down Expand Up @@ -227,7 +222,6 @@ func NewLocalRunner(cfg *RunnerConfig) (*LocalRunner, error) {
taskUpdateCh: make(chan struct{}),
exitErr: make(chan error, 2),
bindHostPortsLocally: cfg.BindHostPortsLocally,
sessionID: uuid.New().String(),
networkName: cfg.NetworkName,
instances: instances,
labels: cfg.Labels,
Expand Down Expand Up @@ -385,7 +379,7 @@ func (d *LocalRunner) ExitErr() <-chan error {
func (d *LocalRunner) Stop() error {
// only stop the containers that belong to this session
containers, err := d.client.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", d.sessionID))),
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", d.manifest.ID))),
})
if err != nil {
return fmt.Errorf("error getting container list: %w", err)
Expand Down Expand Up @@ -622,7 +616,7 @@ func (d *LocalRunner) toDockerComposeService(s *Service) (map[string]interface{}
// It is important to use the playground label to identify the containers
// during the cleanup process
"playground": "true",
"playground.session": d.sessionID,
"playground.session": d.manifest.ID,
"service": s.Name,
}

Expand Down Expand Up @@ -892,7 +886,7 @@ func (d *LocalRunner) trackLogs(serviceName string, containerID string) error {

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

for {
Expand Down Expand Up @@ -1040,3 +1034,46 @@ func (d *LocalRunner) Run() error {
}
return nil
}

// StopContainersBySessionID removes all Docker containers associated with a specific playground session ID.
// This is a standalone utility function used by the clean command to stop containers without requiring
// a LocalRunner instance or manifest reference.
//
// TODO: Refactor to reduce code duplication with LocalRunner.Stop()
// Consider creating a shared dockerClient wrapper with helper methods for container management
// that both LocalRunner and this function can use.
func StopContainersBySessionID(id string) error {
client, err := newDockerClient()
if err != nil {
return err
}

containers, err := client.ContainerList(context.Background(), container.ListOptions{
Filters: filters.NewArgs(filters.Arg("label", fmt.Sprintf("playground.session=%s", id))),
})
if err != nil {
return fmt.Errorf("error getting container list: %w", err)
}

var wg sync.WaitGroup

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioning also in this PR - https://pkg.go.dev/golang.org/x/sync/errgroup is usually great for this. 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good! Will add and merge.

wg.Add(len(containers))

var errCh chan error
errCh = make(chan error, len(containers))

for _, cont := range containers {
go func(contID string) {
defer wg.Done()
if err := client.ContainerRemove(context.Background(), contID, container.RemoveOptions{
RemoveVolumes: true,
RemoveLinks: false,
Force: true,
}); err != nil {
errCh <- fmt.Errorf("error removing container: %w", err)
}
}(cont.ID)
}

wg.Wait()
return nil
}
1 change: 1 addition & 0 deletions playground/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Recipe interface {
// Manifest describes a list of services and their dependencies
type Manifest struct {
ctx *ExContext
ID string `json:"session_id"`

// list of Services
Services []*Service `json:"services"`
Expand Down
Loading