diff --git a/playground/manifest.go b/playground/manifest.go index f0732f5..021216b 100644 --- a/playground/manifest.go +++ b/playground/manifest.go @@ -87,6 +87,16 @@ func (p *Component) AddService(ctx *ExContext, srv ComponentGen) { p.Inner = append(p.Inner, srv.Apply(ctx)) } +// WalkServices invokes fn for every service in the component tree. +func (p *Component) WalkServices(fn func(*Service)) { + for _, svc := range p.Services { + fn(svc) + } + for _, inner := range p.Inner { + inner.WalkServices(fn) + } +} + // FindService finds a service by name in the component tree func (p *Component) FindService(name string) *Service { for _, svc := range p.Services { @@ -102,16 +112,37 @@ func (p *Component) FindService(name string) *Service { return nil } -// RemoveService removes a service by name from the component tree +// RemoveService removes a service by name from the component tree. +// It also clears any health-check-sidecar labels that reference the removed +// service so manifest validation does not fail on a dangling sidecar pointer. func (p *Component) RemoveService(name string) { + p.removeServiceRecursive(name) + p.clearSidecarLabel(name) +} + +func (p *Component) removeServiceRecursive(name string) bool { for i, svc := range p.Services { if svc.Name == name { p.Services = append(p.Services[:i], p.Services[i+1:]...) - return + return true } } for _, inner := range p.Inner { - inner.RemoveService(name) + if inner.removeServiceRecursive(name) { + return true + } + } + return false +} + +func (p *Component) clearSidecarLabel(sidecarName string) { + for _, svc := range p.Services { + if svc.Labels[healthCheckSidecarLabel] == sidecarName { + delete(svc.Labels, healthCheckSidecarLabel) + } + } + for _, inner := range p.Inner { + inner.clearSidecarLabel(sidecarName) } } @@ -663,6 +694,20 @@ func (s *Service) DependsOnRunning(name string) *Service { return s } +// ReplaceDependency updates the condition of an existing depends-on edge to +// the named service. It is a no-op if no such edge exists. Useful when a +// downstream recipe knows that a target service can never reach the strict +// condition declared by an upstream component (e.g. a beacon that only goes +// healthy after its consumer connects). +func (s *Service) ReplaceDependency(name string, condition DependsOnCondition) *Service { + for _, dep := range s.DependsOn { + if dep.Name == name { + dep.Condition = condition + } + } + return s +} + func applyTemplate(templateStr string) (string, []Port, []NodeRef) { // TODO: Can we remove the return argument string? diff --git a/playground/recipe_buildernet.go b/playground/recipe_buildernet.go index 9d5b77b..64a6e70 100644 --- a/playground/recipe_buildernet.go +++ b/playground/recipe_buildernet.go @@ -44,7 +44,8 @@ func (b *BuilderNetRecipe) Apply(ctx *ExContext) *Component { // Apply beacon service overrides for buildernet. // We need these for letting the builder connect to the beacon node. // Basically, the beacon node can never be healthy until the builder - // connects. + // connects, so we also drop its healthmon and downgrade any consumer's + // dependency on beacon from healthy to running. if beacon := component.FindService("beacon"); beacon != nil { beacon.ReplaceArgs(map[string]string{ "--target-peers": "1", @@ -54,10 +55,15 @@ func (b *BuilderNetRecipe) Apply(ctx *ExContext) *Component { if mevBoostRelay := component.FindService("mev-boost-relay"); mevBoostRelay != nil { mevBoostRelay.DependsOnNone() } - // Remove beacon healthmon - doesn't work with --target-peers=1 which is required for builder VM component.RemoveService("beacon_healthmon") - - component.RunContenderIfEnabled(ctx) + // Beacon never reaches healthy in buildernet, so any consumer that asked + // for healthy must accept running instead. + component.WalkServices(func(svc *Service) { + svc.ReplaceDependency("beacon", DependsOnConditionRunning) + }) + + // Note: contender is already added by L1Recipe.Apply, so we do not call + // RunContenderIfEnabled here. return component }