Skip to content

Commit b0879dc

Browse files
authored
Improve custom recipe experience and testing (#379)
- Add recipe flags to `generate` cmd to support conditional recipe generation - Add `--retries` flag to `test` command and improve timeout flag usage - Add `setup` field to custom YAML recipes to setup the environment first before running the playground - arrives with `--skip-setup` option - Resolve relative `host_path`s to absolute paths to run local binaries from custom recipes - Enable port overriding from the custom recipe These are improvements made and discovered during the experimentation with op-rbuilder repo, for running locally and in CI.
1 parent 222d0a5 commit b0879dc

File tree

6 files changed

+85
-14
lines changed

6 files changed

+85
-14
lines changed

main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ var (
5454
contenderArgs []string
5555
contenderTarget string
5656
detached bool
57+
skipSetup bool
5758
prefundedAccounts []string
5859
followFlag bool
5960
generateForce bool
6061
testRPCURL string
6162
testELRPCURL string
6263
testTimeout time.Duration
64+
testRetries int
6365
testInsecure bool
6466
portListFlag bool
6567
)
@@ -515,6 +517,7 @@ var testCmd = &cobra.Command{
515517
cfg.RPCURL = testRPCURL
516518
cfg.ELRPCURL = testELRPCURL
517519
cfg.Timeout = testTimeout
520+
cfg.Retries = testRetries
518521
cfg.Insecure = testInsecure
519522

520523
// Suggest --insecure flag if any RPC URL uses https without insecure mode
@@ -557,6 +560,7 @@ func main() {
557560
cmd.Flags().StringArrayVar(&contenderArgs, "contender.arg", []string{}, "add/override contender CLI flags")
558561
cmd.Flags().StringVar(&contenderTarget, "contender.target", "", "override the node that contender spams")
559562
cmd.Flags().BoolVar(&detached, "detached", false, "Detached mode: Run the recipes in the background")
563+
cmd.Flags().BoolVar(&skipSetup, "skip-setup", false, "Skip the setup commands defined in the YAML recipe")
560564
cmd.Flags().StringArrayVar(&prefundedAccounts, "prefunded-accounts", []string{}, "Fund this account in addition to static prefunded accounts")
561565
}
562566

@@ -605,6 +609,7 @@ func main() {
605609
},
606610
}
607611
recipeCmd.Flags().AddFlagSet(recipe.Flags())
612+
generateCmd.Flags().AddFlagSet(recipe.Flags())
608613
addCommonRecipeFlags(recipeCmd)
609614
startCmd.AddCommand(recipeCmd)
610615
}
@@ -633,7 +638,8 @@ func main() {
633638
rootCmd.AddCommand(recipesCmd)
634639
testCmd.Flags().StringVar(&testRPCURL, "rpc", "http://localhost:8545", "Target RPC URL for sending transactions")
635640
testCmd.Flags().StringVar(&testELRPCURL, "el-rpc", "", "EL RPC URL for chain queries (default: same as --rpc)")
636-
testCmd.Flags().DurationVar(&testTimeout, "timeout", 2*time.Minute, "Timeout for waiting for transaction receipt")
641+
testCmd.Flags().DurationVar(&testTimeout, "timeout", time.Minute, "Timeout for waiting for transaction receipt (0 means no timeout - default: 1m)")
642+
testCmd.Flags().IntVar(&testRetries, "retries", 0, "Max number of failed receipt requests before giving up (0 means retry forever - default: 0)")
637643
testCmd.Flags().BoolVar(&testInsecure, "insecure", false, "Skip TLS certificate verification (for self-signed certs)")
638644
rootCmd.AddCommand(testCmd)
639645

@@ -700,6 +706,9 @@ func runIt(recipe playground.Recipe) error {
700706
components := recipe.Apply(exCtx)
701707
svcManager := playground.NewManifest(sessionID, components)
702708
svcManager.Bootnode = exCtx.Bootnode
709+
if yamlRecipe, ok := recipe.(*playground.YAMLRecipe); ok && !skipSetup {
710+
svcManager.Setup, svcManager.SetupDir = yamlRecipe.SetupCommands()
711+
}
703712

704713
// generate the dot graph
705714
slog.Debug("Generating dot graph...")

playground/local_runner.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,11 @@ func (d *LocalRunner) Run(ctx context.Context) error {
11871187
return fmt.Errorf("failed to write docker-compose.yaml: %w", err)
11881188
}
11891189

1190+
// Run setup commands before launching any services
1191+
if err := d.runSetupCommands(ctx); err != nil {
1192+
return err
1193+
}
1194+
11901195
// Pull all required images in parallel
11911196
if err := d.pullNotAvailableImages(ctx); err != nil {
11921197
return err

playground/local_runner_lifecycle.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,30 @@ func (d *LocalRunner) runAllLifecycleStopCommands() {
160160
d.runLifecycleStopCommands(info.svc, info.logFile, info.logPath)
161161
}
162162
}
163+
164+
// runSetupCommands runs the setup commands from the manifest before any services
165+
// are launched. Commands run sequentially via "sh -c" in the recipe directory;
166+
// each must exit 0 or an error is returned.
167+
func (d *LocalRunner) runSetupCommands(ctx context.Context) error {
168+
if len(d.manifest.Setup) == 0 {
169+
return nil
170+
}
171+
172+
dir := d.manifest.SetupDir
173+
if dir == "" {
174+
dir = d.out.sessionDir
175+
}
176+
177+
for i, command := range d.manifest.Setup {
178+
slog.Info("Running setup command", "index", i, "command", command)
179+
cmd := exec.CommandContext(ctx, "sh", "-c", command)
180+
cmd.Dir = dir
181+
cmd.Stdout = os.Stdout
182+
cmd.Stderr = os.Stderr
183+
if err := cmd.Run(); err != nil {
184+
return fmt.Errorf("setup command %d failed: %q: %w", i, command, err)
185+
}
186+
}
187+
188+
return nil
189+
}

playground/manifest.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ type Manifest struct {
4747

4848
Bootnode *BootnodeRef `json:"bootnode,omitempty"`
4949

50+
// Setup contains shell commands to run before any services are launched.
51+
// Commands run sequentially in SetupDir; each must exit 0.
52+
Setup []string `json:"setup,omitempty"`
53+
54+
// SetupDir is the working directory for setup commands.
55+
SetupDir string `json:"setup_dir,omitempty"`
56+
5057
// overrides is a map of service name to the path of the executable to run
5158
// on the host machine instead of a container.
5259
overrides map[string]string
@@ -515,12 +522,13 @@ func (s *Service) WithPort(name string, portNumber int, protocolVar ...string) *
515522
protocol = protocolVar[0]
516523
}
517524

518-
// add the port if not already present with the same name.
519-
// if preset with the same name, they must have same port number
525+
// add or replace the ports
520526
for _, p := range s.Ports {
521527
if p.Name == name {
522528
if p.Port != portNumber {
523-
panic(fmt.Sprintf("port %s already defined with different port number (existing: %d, new: %d) on service %s", name, p.Port, portNumber, s.Name))
529+
p.Port = portNumber
530+
p.Protocol = protocol
531+
return s
524532
}
525533
if p.Protocol != protocol {
526534
// If they have different protocols they are different ports

playground/recipe_yaml.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ type YAMLRecipeConfig struct {
2222
// Description is an optional description of the recipe
2323
Description string `yaml:"description,omitempty"`
2424

25+
// Setup is a list of shell commands to run before any services are launched.
26+
// Commands run sequentially in the recipe's directory; each must exit 0.
27+
Setup []string `yaml:"setup,omitempty"`
28+
2529
// Recipe contains the component/service hierarchy to apply as overrides or additions
2630
Recipe map[string]*YAMLComponentConfig `yaml:"recipe"`
2731
}
@@ -228,6 +232,11 @@ func (y *YAMLRecipe) Output(manifest *Manifest) map[string]interface{} {
228232
return y.baseRecipe.Output(manifest)
229233
}
230234

235+
// SetupCommands returns the setup commands and the directory to run them in.
236+
func (y *YAMLRecipe) SetupCommands() ([]string, string) {
237+
return y.config.Setup, y.recipeDir
238+
}
239+
231240
// applyModifications applies the YAML recipe modifications to the component tree
232241
func (y *YAMLRecipe) applyModifications(ctx *ExContext, component *Component) {
233242
if y.config.Recipe == nil {
@@ -464,7 +473,11 @@ func applyServiceOverrides(svc *Service, config *YAMLServiceConfig, root *Compon
464473
applyDependsOn(svc, config.DependsOn, root)
465474
}
466475
if config.HostPath != "" {
467-
svc.HostPath = config.HostPath
476+
if filepath.IsAbs(config.HostPath) {
477+
svc.HostPath = config.HostPath
478+
} else {
479+
svc.HostPath, _ = filepath.Abs(filepath.Join(recipeDir, config.HostPath))
480+
}
468481
svc.UseHostExecution()
469482
}
470483
if config.Release != nil {
@@ -641,7 +654,11 @@ func createServiceFromConfig(name string, config *YAMLServiceConfig, root *Compo
641654
applyDependsOn(svc, config.DependsOn, root)
642655
}
643656
if config.HostPath != "" {
644-
svc.HostPath = config.HostPath
657+
if filepath.IsAbs(config.HostPath) {
658+
svc.HostPath = config.HostPath
659+
} else {
660+
svc.HostPath, _ = filepath.Abs(filepath.Join(recipeDir, config.HostPath))
661+
}
645662
svc.UseHostExecution()
646663
}
647664
if config.Release != nil {

playground/test_tx.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ type TestTxConfig struct {
6767
Value *big.Int
6868
GasLimit uint64
6969
GasPrice *big.Int
70-
Timeout time.Duration // Timeout for waiting for receipt. If 0, defaults to 2 minutes
70+
Timeout time.Duration // Timeout for waiting for receipt. If 0, no timeout.
71+
Retries int // Max failed receipt requests before giving up. If 0, retry forever.
7172
Insecure bool // Skip TLS certificate verification
7273
}
7374

@@ -209,23 +210,23 @@ func SendTestTransaction(ctx context.Context, cfg *TestTxConfig) error {
209210
txHash := signedTx.Hash()
210211
fmt.Printf("TX Hash: %s\n", txHash.Hex())
211212

212-
// Wait for receipt with timeout
213-
timeout := cfg.Timeout
214-
if timeout == 0 {
215-
timeout = 2 * time.Minute
213+
// Apply timeout if configured
214+
if cfg.Timeout > 0 {
215+
var cancel context.CancelFunc
216+
ctx, cancel = context.WithTimeout(ctx, cfg.Timeout)
217+
defer cancel()
216218
}
217-
ctx, cancel := context.WithTimeout(ctx, timeout)
218-
defer cancel()
219219

220220
fmt.Println("Waiting for receipt...")
221221
ticker := time.NewTicker(1 * time.Second)
222222
defer ticker.Stop()
223223

224+
failedAttempts := 0
224225
for {
225226
select {
226227
case <-ctx.Done():
227228
if ctx.Err() == context.DeadlineExceeded {
228-
return fmt.Errorf("timeout waiting for transaction receipt after %s", timeout)
229+
return fmt.Errorf("timeout waiting for transaction receipt after %s", cfg.Timeout)
229230
}
230231
return ctx.Err()
231232
case <-ticker.C:
@@ -243,6 +244,10 @@ func SendTestTransaction(ctx context.Context, cfg *TestTxConfig) error {
243244
}
244245
return nil
245246
}
247+
failedAttempts++
248+
if cfg.Retries > 0 && failedAttempts >= cfg.Retries {
249+
return fmt.Errorf("failed to get transaction receipt after %d attempts", cfg.Retries)
250+
}
246251
}
247252
}
248253
}

0 commit comments

Comments
 (0)