diff --git a/framework/docker/container/job.go b/framework/docker/container/job.go index d0c6645..15e35fe 100644 --- a/framework/docker/container/job.go +++ b/framework/docker/container/job.go @@ -14,10 +14,10 @@ import ( "time" "github.com/containerd/errdefs" + "github.com/moby/moby/api/pkg/stdcopy" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/mount" "github.com/moby/moby/api/types/network" - "github.com/moby/moby/api/pkg/stdcopy" "github.com/moby/moby/client" "go.uber.org/zap" ) diff --git a/framework/docker/container/lifecycle.go b/framework/docker/container/lifecycle.go index fb4f1c1..1ecaa0f 100644 --- a/framework/docker/container/lifecycle.go +++ b/framework/docker/container/lifecycle.go @@ -96,20 +96,25 @@ func (c *Lifecycle) CreateContainer( } } + containerCfg := &container.Config{ + Image: imageRef, + + Entrypoint: entrypoint, + Cmd: cmd, + Env: env, + Hostname: hostName, + Labels: map[string]string{consts.CleanupLabel: c.client.CleanupLabel()}, + ExposedPorts: pS, + } + if image.UIDGID != "" { + containerCfg.User = image.UIDGID + } + cc, err := c.client.ContainerCreate( ctx, client.ContainerCreateOptions{ - Name: c.containerName, - Config: &container.Config{ - Image: imageRef, - - Entrypoint: entrypoint, - Cmd: cmd, - Env: env, - Hostname: hostName, - Labels: map[string]string{consts.CleanupLabel: c.client.CleanupLabel()}, - ExposedPorts: pS, - }, + Name: c.containerName, + Config: containerCfg, HostConfig: &container.HostConfig{ Binds: volumeBinds, PortBindings: pb, diff --git a/framework/docker/container/node.go b/framework/docker/container/node.go index e1ad6a9..6182005 100644 --- a/framework/docker/container/node.go +++ b/framework/docker/container/node.go @@ -69,6 +69,7 @@ func (n *Node) Exec(ctx context.Context, logger *zap.Logger, cmd []string, env [ opts := Options{ Env: env, Binds: n.Bind(), + User: n.Image.UIDGID, } res := job.Run(ctx, cmd, opts) if res.Err != nil { diff --git a/framework/docker/cosmos/chain_builder.go b/framework/docker/cosmos/chain_builder.go index 0b3e1ca..f45fa7d 100644 --- a/framework/docker/cosmos/chain_builder.go +++ b/framework/docker/cosmos/chain_builder.go @@ -158,6 +158,8 @@ type ChainBuilder struct { // blockWaitTimeout is the timeout for waiting for blocks after starting the chain. // If zero, defaults to 120 seconds. blockWaitTimeout time.Duration + // homeDir overrides the default home directory inside the container + homeDir string } // NewChainBuilder initializes and returns a new ChainBuilder with default values for testing purposes. @@ -315,6 +317,12 @@ func (b *ChainBuilder) WithImage(image container.Image) *ChainBuilder { return b } +// WithHomeDir overrides the default home directory inside the container. +func (b *ChainBuilder) WithHomeDir(homeDir string) *ChainBuilder { + b.homeDir = homeDir + return b +} + // WithAdditionalStartArgs sets the default additional start arguments for all nodes in the chain func (b *ChainBuilder) WithAdditionalStartArgs(args ...string) *ChainBuilder { b.additionalStartArgs = args @@ -441,11 +449,11 @@ func (b *ChainBuilder) Build(ctx context.Context) (*Chain, error) { Env: b.env, GenesisFileBz: b.genesisBz, }, - t: b.t, - Validators: validators, - FullNodes: fullNodes, - cdc: cdc, - log: b.logger, + t: b.t, + Validators: validators, + FullNodes: fullNodes, + cdc: cdc, + log: b.logger, faucetWallet: b.faucetWallet, skipInit: b.skipInit, blockWaitTimeout: b.blockWaitTimeout, @@ -494,10 +502,12 @@ func (b *ChainBuilder) newChainNode( } func (b *ChainBuilder) newDockerChainNode(log *zap.Logger, nodeConfig ChainNodeConfig, index int) *ChainNode { - // use a default home directory if name is not set - homeDir := "/var/cosmos-chain" - if b.name != "" { - homeDir = path.Join("/var/cosmos-chain", b.name) + homeDir := b.homeDir + if homeDir == "" { + homeDir = "/var/cosmos-chain" + if b.name != "" { + homeDir = path.Join("/var/cosmos-chain", b.name) + } } chainParams := ChainNodeParams{ diff --git a/framework/docker/da_network_test.go b/framework/docker/da_network_test.go index 470ded8..b263e62 100644 --- a/framework/docker/da_network_test.go +++ b/framework/docker/da_network_test.go @@ -124,75 +124,75 @@ func TestDANetworkCreation(t *testing.T) { // TestDANetworkStopAndRestart ensures a DA network can be stopped and then restarted, // and nodes continue to respond to RPC after restart. func TestDANetworkStopAndRestart(t *testing.T) { - if testing.Short() { - t.Skip("skipping due to short mode") - } - t.Parallel() - configureBech32PrefixOnce() - - // Setup isolated docker environment for this test - testCfg := setupDockerTest(t) - - chain, err := testCfg.ChainBuilder.Build(testCfg.Ctx) - require.NoError(t, err) - - err = chain.Start(testCfg.Ctx) - require.NoError(t, err) - defer func() { _ = chain.Remove(testCfg.Ctx) }() - - // Default image for the DA network - defaultImage := container.Image{ - Repository: "ghcr.io/celestiaorg/celestia-node", - Version: "v0.26.4", - UIDGID: "10001:10001", - } - - // Create bridge node config - bridgeNodeConfig := da.NewNodeBuilder(). - WithNodeType(types.BridgeNode). - Build() - - // Create DA network with a single bridge node - daNetwork, err := testCfg.DANetworkBuilder. - WithChainID(chain.GetChainID()). - WithImage(defaultImage). - WithNodes(bridgeNodeConfig). - Build(testCfg.Ctx) - require.NoError(t, err) - - // Start the bridge node - bridgeNode := daNetwork.GetBridgeNodes()[0] - - chainNetworkInfo, err := chain.GetNodes()[0].GetNetworkInfo(testCfg.Ctx) - require.NoError(t, err, "failed to get network info") - hostname := chainNetworkInfo.Internal.Hostname - - chainID := chain.GetChainID() - genesisHash, err := getGenesisHash(testCfg.Ctx, chain) - require.NoError(t, err) - - require.NoError(t, bridgeNode.Start(testCfg.Ctx, - da.WithChainID(chainID), - da.WithAdditionalStartArguments("--p2p.network", chainID, "--core.ip", hostname, "--rpc.addr", "0.0.0.0"), - da.WithEnvironmentVariables(map[string]string{ - "CELESTIA_CUSTOM": types.BuildCelestiaCustomEnvVar(chainID, genesisHash, ""), - "P2P_NETWORK": chainID, - }), - )) - - // Verify it is responding - _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) - require.NoError(t, err, "failed to get bridge node p2p info before stop") - - // Stop the entire DA network - require.NoError(t, daNetwork.Stop(testCfg.Ctx)) - - // Start the entire DA network again - require.NoError(t, daNetwork.Start(testCfg.Ctx)) - - // Verify RPC works after restart - _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) - require.NoError(t, err, "failed to get bridge node p2p info after restart") + if testing.Short() { + t.Skip("skipping due to short mode") + } + t.Parallel() + configureBech32PrefixOnce() + + // Setup isolated docker environment for this test + testCfg := setupDockerTest(t) + + chain, err := testCfg.ChainBuilder.Build(testCfg.Ctx) + require.NoError(t, err) + + err = chain.Start(testCfg.Ctx) + require.NoError(t, err) + defer func() { _ = chain.Remove(testCfg.Ctx) }() + + // Default image for the DA network + defaultImage := container.Image{ + Repository: "ghcr.io/celestiaorg/celestia-node", + Version: "v0.26.4", + UIDGID: "10001:10001", + } + + // Create bridge node config + bridgeNodeConfig := da.NewNodeBuilder(). + WithNodeType(types.BridgeNode). + Build() + + // Create DA network with a single bridge node + daNetwork, err := testCfg.DANetworkBuilder. + WithChainID(chain.GetChainID()). + WithImage(defaultImage). + WithNodes(bridgeNodeConfig). + Build(testCfg.Ctx) + require.NoError(t, err) + + // Start the bridge node + bridgeNode := daNetwork.GetBridgeNodes()[0] + + chainNetworkInfo, err := chain.GetNodes()[0].GetNetworkInfo(testCfg.Ctx) + require.NoError(t, err, "failed to get network info") + hostname := chainNetworkInfo.Internal.Hostname + + chainID := chain.GetChainID() + genesisHash, err := getGenesisHash(testCfg.Ctx, chain) + require.NoError(t, err) + + require.NoError(t, bridgeNode.Start(testCfg.Ctx, + da.WithChainID(chainID), + da.WithAdditionalStartArguments("--p2p.network", chainID, "--core.ip", hostname, "--rpc.addr", "0.0.0.0"), + da.WithEnvironmentVariables(map[string]string{ + "CELESTIA_CUSTOM": types.BuildCelestiaCustomEnvVar(chainID, genesisHash, ""), + "P2P_NETWORK": chainID, + }), + )) + + // Verify it is responding + _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) + require.NoError(t, err, "failed to get bridge node p2p info before stop") + + // Stop the entire DA network + require.NoError(t, daNetwork.Stop(testCfg.Ctx)) + + // Start the entire DA network again + require.NoError(t, daNetwork.Start(testCfg.Ctx)) + + // Verify RPC works after restart + _, err = bridgeNode.GetP2PInfo(testCfg.Ctx) + require.NoError(t, err, "failed to get bridge node p2p info after restart") } // TestModifyConfigFileDANetwork ensures modification of config files is possible by diff --git a/framework/docker/dataavailability/config.go b/framework/docker/dataavailability/config.go index d464b3f..6f41941 100644 --- a/framework/docker/dataavailability/config.go +++ b/framework/docker/dataavailability/config.go @@ -24,4 +24,11 @@ type Config struct { Image container.Image // AdditionalStartArgs are additional arguments passed to all nodes when starting AdditionalStartArgs []string + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string +} + +// DefaultHomeDir returns the default home directory for DA node containers. +func DefaultHomeDir() string { + return "/home/celestia" } diff --git a/framework/docker/dataavailability/network.go b/framework/docker/dataavailability/network.go index 00d3b54..c6f6fa2 100644 --- a/framework/docker/dataavailability/network.go +++ b/framework/docker/dataavailability/network.go @@ -67,7 +67,7 @@ func (n *Network) GetBridgeNodes() []*Node { // GetLightNodes returns only the light nodes in the network. func (n *Network) GetLightNodes() []*Node { - return n.GetNodesByType(types.LightNode) + return n.GetNodesByType(types.LightNode) } // AddNodes adds one or more nodes to the DA network with the given configurations. @@ -151,41 +151,41 @@ func (n *Network) RemoveNodes(ctx context.Context, nodeNames ...string) error { // Stop stops all nodes in the data availability network without removing them. func (n *Network) Stop(ctx context.Context) error { - nodes := n.GetNodes() - var eg errgroup.Group - for _, nd := range nodes { - nd := nd - eg.Go(func() error { - return nd.Stop(ctx) - }) - } - return eg.Wait() + nodes := n.GetNodes() + var eg errgroup.Group + for _, nd := range nodes { + nd := nd + eg.Go(func() error { + return nd.Stop(ctx) + }) + } + return eg.Wait() } // Remove stops and removes all nodes in the data availability network. // Matches the semantics of cosmos.Chain.Remove by operating on all components concurrently. func (n *Network) Remove(ctx context.Context, opts ...types.RemoveOption) error { - nodes := n.GetNodes() - var eg errgroup.Group - for _, nd := range nodes { - nd := nd - eg.Go(func() error { - return nd.Remove(ctx, opts...) - }) - } - return eg.Wait() + nodes := n.GetNodes() + var eg errgroup.Group + for _, nd := range nodes { + nd := nd + eg.Go(func() error { + return nd.Remove(ctx, opts...) + }) + } + return eg.Wait() } // Start starts all nodes in the data availability network. // If nodes were previously initialized and only stopped, this will only start their containers. func (n *Network) Start(ctx context.Context) error { - nodes := n.GetNodes() - var eg errgroup.Group - for _, nd := range nodes { - nd := nd - eg.Go(func() error { - return nd.Start(ctx) - }) - } - return eg.Wait() + nodes := n.GetNodes() + var eg errgroup.Group + for _, nd := range nodes { + nd := nd + eg.Go(func() error { + return nd.Start(ctx) + }) + } + return eg.Wait() } diff --git a/framework/docker/dataavailability/network_builder.go b/framework/docker/dataavailability/network_builder.go index 4695dc3..a800446 100644 --- a/framework/docker/dataavailability/network_builder.go +++ b/framework/docker/dataavailability/network_builder.go @@ -35,6 +35,8 @@ type NetworkBuilder struct { chainID string // binaryName is the name of the Node binary executable (e.g., "celestia") binaryName string + // homeDir overrides the default home directory inside the container + homeDir string } // NewNetworkBuilder initializes and returns a new NetworkBuilder with default values for testing purposes @@ -108,6 +110,12 @@ func (b *NetworkBuilder) WithDockerNetworkID(networkID string) *NetworkBuilder { return b } +// WithHomeDir overrides the default home directory inside the container. +func (b *NetworkBuilder) WithHomeDir(homeDir string) *NetworkBuilder { + b.homeDir = homeDir + return b +} + // WithImage sets the default Docker image for all nodes in the network func (b *NetworkBuilder) WithImage(image container.Image) *NetworkBuilder { b.dockerImage = &image @@ -214,6 +222,7 @@ func (b *NetworkBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, ind ChainID: b.chainID, Bin: b.binaryName, Image: imageToUse, + HomeDir: b.homeDir, // Env and AdditionalStartArgs provide default set of values for all nodes, but can // be individually overridden by nodeConfig. Env: b.env, diff --git a/framework/docker/dataavailability/node.go b/framework/docker/dataavailability/node.go index fd85643..07fb651 100644 --- a/framework/docker/dataavailability/node.go +++ b/framework/docker/dataavailability/node.go @@ -77,6 +77,10 @@ type Node struct { } func NewNode(cfg Config, testName string, image container.Image, index int, nodeConfig NodeConfig) *Node { + homeDir := cfg.HomeDir + if homeDir == "" { + homeDir = DefaultHomeDir() + } logger := cfg.Logger.With( zap.String("node_type", nodeConfig.NodeType.String()), ) @@ -86,7 +90,7 @@ func NewNode(cfg Config, testName string, image container.Image, index int, node additionalStartArgs: nodeConfig.AdditionalStartArgs, configModifications: nodeConfig.ConfigModifications, internalPorts: initializeDANodePorts(nodeConfig.InternalPorts), - Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, "/home/celestia", index, nodeConfig.NodeType, logger), + Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, nodeConfig.NodeType, logger), } node.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, node.Name())) diff --git a/framework/docker/evstack/chain_builder.go b/framework/docker/evstack/chain_builder.go index b231367..900566a 100644 --- a/framework/docker/evstack/chain_builder.go +++ b/framework/docker/evstack/chain_builder.go @@ -37,6 +37,8 @@ type ChainBuilder struct { binaryName string // aggregatorPassphrase is the passphrase used for aggregator nodes aggregatorPassphrase string + // homeDir overrides the default home directory inside the container + homeDir string } // NewChainBuilder initializes and returns a new ChainBuilder with default values for testing purposes @@ -101,6 +103,12 @@ func (b *ChainBuilder) WithDockerNetworkID(networkID string) *ChainBuilder { return b } +// WithHomeDir overrides the default home directory inside the container. +func (b *ChainBuilder) WithHomeDir(homeDir string) *ChainBuilder { + b.homeDir = homeDir + return b +} + // WithImage sets the default Docker image for all nodes in the chain func (b *ChainBuilder) WithImage(image container.Image) *ChainBuilder { b.dockerImage = &image @@ -185,6 +193,7 @@ func (b *ChainBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, index Bin: b.binaryName, AggregatorPassphrase: b.aggregatorPassphrase, Image: imageToUse, + HomeDir: b.homeDir, } node := NewNode(cfg, b.testName, imageToUse, index, nodeConfig.IsAggregator, b.getAdditionalStartArgs(nodeConfig)) diff --git a/framework/docker/evstack/config.go b/framework/docker/evstack/config.go index 495285b..3d0085a 100644 --- a/framework/docker/evstack/config.go +++ b/framework/docker/evstack/config.go @@ -24,4 +24,11 @@ type Config struct { AggregatorPassphrase string // Image specifies the Docker image used for the evstack nodes. Image container.Image + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string +} + +// DefaultHomeDir returns the default home directory for evstack containers. +func DefaultHomeDir() string { + return "/var/evstack" } diff --git a/framework/docker/evstack/evmsingle/builder.go b/framework/docker/evstack/evmsingle/builder.go index 8207b71..312b2bb 100644 --- a/framework/docker/evstack/evmsingle/builder.go +++ b/framework/docker/evstack/evmsingle/builder.go @@ -25,6 +25,7 @@ type ChainBuilder struct { addlArgs []string nodes []NodeConfig name string + homeDir string } func NewChainBuilder(t *testing.T) *ChainBuilder { @@ -100,6 +101,11 @@ func (b *ChainBuilder) WithNodes(cfgs ...NodeConfig) *ChainBuilder { return b } +func (b *ChainBuilder) WithHomeDir(homeDir string) *ChainBuilder { + b.homeDir = homeDir + return b +} + func (b *ChainBuilder) WithName(name string) *ChainBuilder { if err := internal.ValidateDockerHostnamePart(name); err != nil { panic(fmt.Sprintf("invalid evmsingle chain name: %v", err)) @@ -110,12 +116,18 @@ func (b *ChainBuilder) WithName(name string) *ChainBuilder { // Build constructs a Chain with nodes created and volumes initialized (not isInitialized) func (b *ChainBuilder) Build(ctx context.Context) (*Chain, error) { + homeDir := b.homeDir + if homeDir == "" { + homeDir = DefaultHomeDir() + } + cfg := Config{ Logger: b.logger, DockerClient: b.dockerClient, DockerNetworkID: b.networkID, Image: b.image, Bin: b.bin, + HomeDir: homeDir, Env: b.env, AdditionalStartArgs: b.addlArgs, } diff --git a/framework/docker/evstack/evmsingle/config.go b/framework/docker/evstack/evmsingle/config.go index 7e8887c..8e24027 100644 --- a/framework/docker/evstack/evmsingle/config.go +++ b/framework/docker/evstack/evmsingle/config.go @@ -15,6 +15,8 @@ type Config struct { Image container.Image // Bin is the executable name (default: evm-single) Bin string + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string // Env are default environment variables applied to all nodes Env []string // AdditionalStartArgs are default start arguments applied to all nodes @@ -23,13 +25,17 @@ type Config struct { AdditionalInitArgs []string } +// DefaultHomeDir returns the default home directory for ev-node-evm containers. +func DefaultHomeDir() string { + return "/home/ev-node/.evm" +} + // DefaultImage returns the default container image for ev-node-evm. func DefaultImage() container.Image { - // Default ev-node tag pinned for reproducibility - return container.Image{Repository: "ghcr.io/evstack/ev-node-evm", Version: "v1.0.0-rc.4"} + return container.Image{Repository: "ghcr.io/evstack/ev-node-evm", Version: "v1.0.0-rc.4", UIDGID: "10001:10001"} } // DefaultBinary returns the default binary name for ev-node-evm. func DefaultBinary() string { - return "evm" + return "evm" } diff --git a/framework/docker/evstack/evmsingle/node.go b/framework/docker/evstack/evmsingle/node.go index 5483e22..2e40895 100644 --- a/framework/docker/evstack/evmsingle/node.go +++ b/framework/docker/evstack/evmsingle/node.go @@ -42,11 +42,8 @@ func newNode(ctx context.Context, cfg Config, testName string, index int, nodeCf log := cfg.Logger.With(zap.String("component", "evm-single"), zap.Int("i", index)) - // ev-node-evm default home - homeDir := "/root/.evm" - n := &Node{cfg: cfg, nodeCfg: nodeCfg, logger: log, internal: ports, chainName: chainName} - n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, NodeType, log) + n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, cfg.HomeDir, index, NodeType, log) n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { return nil, err diff --git a/framework/docker/evstack/node.go b/framework/docker/evstack/node.go index facb14f..cbae849 100644 --- a/framework/docker/evstack/node.go +++ b/framework/docker/evstack/node.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "path" "path/filepath" "sync" "time" @@ -55,6 +54,10 @@ type Node struct { } func NewNode(cfg Config, testName string, image container.Image, index int, isAggregator bool, additionalStartArgs []string) *Node { + homeDir := cfg.HomeDir + if homeDir == "" { + homeDir = DefaultHomeDir() + } logger := cfg.Logger.With( zap.Int("i", index), zap.Bool("aggregator", isAggregator), @@ -63,7 +66,7 @@ func NewNode(cfg Config, testName string, image container.Image, index int, isAg cfg: cfg, isAggregatorFlag: isAggregator, additionalStartArgs: additionalStartArgs, - Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, path.Join("/var", "evstack"), index, EvstackType, logger), + Node: container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, EvstackType, logger), } node.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, node.Name())) diff --git a/framework/docker/evstack/reth/builder.go b/framework/docker/evstack/reth/builder.go index a407a4a..51d1c3f 100644 --- a/framework/docker/evstack/reth/builder.go +++ b/framework/docker/evstack/reth/builder.go @@ -26,6 +26,7 @@ type NodeBuilder struct { genesis []byte jwtSecretHex string name string + homeDir string hyperlaneChainName string hyperlaneChainID uint64 hyperlaneDomainID uint32 @@ -104,6 +105,11 @@ func (b *NodeBuilder) WithName(name string) *NodeBuilder { return b } +func (b *NodeBuilder) WithHomeDir(homeDir string) *NodeBuilder { + b.homeDir = homeDir + return b +} + func (b *NodeBuilder) WithHyperlaneChainName(name string) *NodeBuilder { b.hyperlaneChainName = name return b @@ -121,12 +127,18 @@ func (b *NodeBuilder) WithHyperlaneDomainID(domainID uint32) *NodeBuilder { // Build constructs the Node and initializes its Docker volume but does not start the container. func (b *NodeBuilder) Build(ctx context.Context) (*Node, error) { + homeDir := b.homeDir + if homeDir == "" { + homeDir = DefaultHomeDir() + } + cfg := Config{ Logger: b.logger, DockerClient: b.dockerClient, DockerNetworkID: b.networkID, Image: b.image, Bin: b.bin, + HomeDir: homeDir, Env: b.env, AdditionalStartArgs: b.additionalStartArgs, JWTSecretHex: b.jwtSecretHex, diff --git a/framework/docker/evstack/reth/config.go b/framework/docker/evstack/reth/config.go index 766b2fb..a23d676 100644 --- a/framework/docker/evstack/reth/config.go +++ b/framework/docker/evstack/reth/config.go @@ -21,6 +21,8 @@ type Config struct { Image container.Image // Bin is the executable name (default: ev-reth) Bin string + // HomeDir is the home directory inside the container. Defaults to DefaultHomeDir(). + HomeDir string // Env are default environment variables applied to all nodes Env []string @@ -51,8 +53,13 @@ func (c Config) Validate() error { return nil } +// DefaultHomeDir returns the default home directory for Reth containers. +func DefaultHomeDir() string { + return "/home/ev-reth" +} + // DefaultImage returns the default container image for Reth nodes. func DefaultImage() container.Image { - // Pin default to a known stable release for local/E2E reproducibility - return container.Image{Repository: "ghcr.io/evstack/ev-reth", Version: "v0.2.2"} + // Pin default to a known stable release for local/E2E reproducibility + return container.Image{Repository: "ghcr.io/evstack/ev-reth", Version: "v0.2.2"} } diff --git a/framework/docker/evstack/reth/node.go b/framework/docker/evstack/reth/node.go index 90271ea..93181d5 100644 --- a/framework/docker/evstack/reth/node.go +++ b/framework/docker/evstack/reth/node.go @@ -38,13 +38,12 @@ func newNode(ctx context.Context, cfg Config, testName string, index int, name s log := cfg.Logger.With(zap.String("component", "reth-node"), zap.Int("i", index)) - homeDir := "/home/ev-reth" n := &Node{ cfg: cfg, logger: log, name: name, } - n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, homeDir, index, NodeType, log) + n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, image, cfg.HomeDir, index, NodeType, log) n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { diff --git a/framework/docker/evstack/spamoor/builder.go b/framework/docker/evstack/spamoor/builder.go index ce7753b..0f68ba5 100644 --- a/framework/docker/evstack/spamoor/builder.go +++ b/framework/docker/evstack/spamoor/builder.go @@ -1,49 +1,53 @@ package spamoor import ( - "context" + "context" - "github.com/celestiaorg/tastora/framework/docker/container" - "github.com/celestiaorg/tastora/framework/types" - "go.uber.org/zap" + "github.com/celestiaorg/tastora/framework/docker/container" + "github.com/celestiaorg/tastora/framework/types" + "go.uber.org/zap" ) type Builder struct { - testName string - dockerClient types.TastoraDockerClient - dockerNetwork string - logger *zap.Logger - image container.Image - - rpcHosts []string - privKey string - nameSuffix string + testName string + dockerClient types.TastoraDockerClient + dockerNetwork string + logger *zap.Logger + image container.Image + homeDir string + + rpcHosts []string + privKey string + nameSuffix string } func NewNodeBuilder(testName string) *Builder { - return &Builder{ - testName: testName, - image: container.NewImage("ethpandaops/spamoor", "latest", ""), - } + return &Builder{ + testName: testName, + image: container.NewImage("ethpandaops/spamoor", "latest", ""), + } } -func (b *Builder) WithDockerClient(c types.TastoraDockerClient) *Builder { b.dockerClient = c; return b } +func (b *Builder) WithDockerClient(c types.TastoraDockerClient) *Builder { + b.dockerClient = c + return b +} func (b *Builder) WithDockerNetworkID(id string) *Builder { b.dockerNetwork = id; return b } func (b *Builder) WithLogger(l *zap.Logger) *Builder { b.logger = l; return b } func (b *Builder) WithImage(img container.Image) *Builder { b.image = img; return b } +func (b *Builder) WithHomeDir(homeDir string) *Builder { b.homeDir = homeDir; return b } func (b *Builder) WithRPCHosts(hosts ...string) *Builder { b.rpcHosts = hosts; return b } func (b *Builder) WithPrivateKey(pk string) *Builder { b.privKey = pk; return b } func (b *Builder) WithNameSuffix(s string) *Builder { b.nameSuffix = s; return b } func (b *Builder) Build(ctx context.Context) (*Node, error) { - cfg := Config{ - DockerClient: b.dockerClient, - DockerNetworkID: b.dockerNetwork, - Logger: b.logger, - Image: b.image, - RPCHosts: b.rpcHosts, - PrivateKey: b.privKey, - } - return newNode(ctx, cfg, b.testName, 0, b.nameSuffix) + cfg := Config{ + DockerClient: b.dockerClient, + DockerNetworkID: b.dockerNetwork, + Logger: b.logger, + Image: b.image, + RPCHosts: b.rpcHosts, + PrivateKey: b.privKey, + } + return newNode(ctx, cfg, b.testName, 0, b.nameSuffix, b.homeDir) } - diff --git a/framework/docker/evstack/spamoor/node.go b/framework/docker/evstack/spamoor/node.go index 1abcc3e..7ca3a76 100644 --- a/framework/docker/evstack/spamoor/node.go +++ b/framework/docker/evstack/spamoor/node.go @@ -1,20 +1,20 @@ package spamoor import ( - "context" - "fmt" - "net/http" - "strings" - "sync" - "time" - - "github.com/celestiaorg/tastora/framework/docker/container" - "github.com/celestiaorg/tastora/framework/docker/internal" - "github.com/celestiaorg/tastora/framework/types" - "net/netip" - - "github.com/moby/moby/api/types/network" - "go.uber.org/zap" + "context" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/celestiaorg/tastora/framework/docker/container" + "github.com/celestiaorg/tastora/framework/docker/internal" + "github.com/celestiaorg/tastora/framework/types" + "net/netip" + + "github.com/moby/moby/api/types/network" + "go.uber.org/zap" ) type nodeType int @@ -22,146 +22,153 @@ type nodeType int func (nodeType) String() string { return "spamoor" } type Ports struct { - Web string // web UI + /metrics + Web string // web UI + /metrics } func defaultInternalPorts() Ports { return Ports{Web: "8080"} } type Config struct { - DockerClient types.TastoraDockerClient - DockerNetworkID string - Logger *zap.Logger - Image container.Image + DockerClient types.TastoraDockerClient + DockerNetworkID string + Logger *zap.Logger + Image container.Image - RPCHosts []string - PrivateKey string + RPCHosts []string + PrivateKey string } type Node struct { - *container.Node - - cfg Config - logger *zap.Logger - started bool - mu sync.Mutex - external types.Ports // HTTP field stores web/metrics host port - name string + *container.Node + + cfg Config + logger *zap.Logger + started bool + mu sync.Mutex + external types.Ports // HTTP field stores web/metrics host port + name string } -func newNode(ctx context.Context, cfg Config, testName string, index int, name string) (*Node, error) { - log := cfg.Logger.With(zap.String("component", "spamoor-daemon"), zap.Int("i", index)) - n := &Node{cfg: cfg, logger: log, name: name} - n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, cfg.Image, "/home/spamoor", index, nodeType(0), log) - n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) - if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { - return nil, err - } - return n, nil +// DefaultHomeDir returns the default home directory for spamoor containers. +func DefaultHomeDir() string { + return "/home/spamoor" +} + +func newNode(ctx context.Context, cfg Config, testName string, index int, name string, homeDir string) (*Node, error) { + if homeDir == "" { + homeDir = DefaultHomeDir() + } + log := cfg.Logger.With(zap.String("component", "spamoor-daemon"), zap.Int("i", index)) + n := &Node{cfg: cfg, logger: log, name: name} + n.Node = container.NewNode(cfg.DockerNetworkID, cfg.DockerClient, testName, cfg.Image, homeDir, index, nodeType(0), log) + n.SetContainerLifecycle(container.NewLifecycle(cfg.Logger, cfg.DockerClient, n.Name())) + if err := n.CreateAndSetupVolume(ctx, n.Name()); err != nil { + return nil, err + } + return n, nil } func (n *Node) Name() string { - if n.name != "" { - return fmt.Sprintf("spamoor-%s-%d-%s", n.name, n.Index, internal.SanitizeDockerResourceName(n.TestName)) - } - return fmt.Sprintf("spamoor-%d-%s", n.Index, internal.SanitizeDockerResourceName(n.TestName)) + if n.name != "" { + return fmt.Sprintf("spamoor-%s-%d-%s", n.name, n.Index, internal.SanitizeDockerResourceName(n.TestName)) + } + return fmt.Sprintf("spamoor-%d-%s", n.Index, internal.SanitizeDockerResourceName(n.TestName)) } func (n *Node) HostName() string { return internal.CondenseHostName(n.Name()) } func (n *Node) GetNetworkInfo(ctx context.Context) (types.NetworkInfo, error) { - internalIP, err := internal.GetContainerInternalIP(ctx, n.DockerClient, n.ContainerLifecycle.ContainerID()) - if err != nil { - return types.NetworkInfo{}, err - } - return types.NetworkInfo{ - Internal: types.Network{Hostname: n.HostName(), IP: internalIP, Ports: types.Ports{HTTP: defaultInternalPorts().Web}}, - External: types.Network{Hostname: "0.0.0.0", Ports: n.external}, - }, nil + internalIP, err := internal.GetContainerInternalIP(ctx, n.DockerClient, n.ContainerLifecycle.ContainerID()) + if err != nil { + return types.NetworkInfo{}, err + } + return types.NetworkInfo{ + Internal: types.Network{Hostname: n.HostName(), IP: internalIP, Ports: types.Ports{HTTP: defaultInternalPorts().Web}}, + External: types.Network{Hostname: "0.0.0.0", Ports: n.external}, + }, nil } func (n *Node) Start(ctx context.Context) error { - n.mu.Lock() - defer n.mu.Unlock() - if n.started { - return n.StartContainer(ctx) - } - if err := n.createNodeContainer(ctx); err != nil { - return err - } - if err := n.ContainerLifecycle.StartContainer(ctx); err != nil { - return err - } - hostPorts, err := n.ContainerLifecycle.GetHostPorts(ctx, defaultInternalPorts().Web+"/tcp") - if err != nil { - return err - } - mapped := internal.MustExtractPort(hostPorts[0]) - n.external = types.Ports{HTTP: mapped} - n.started = true - // readiness wait for /metrics endpoint (best-effort) - waitHTTP(fmt.Sprintf("http://127.0.0.1:%s/metrics", n.external.HTTP), 20*time.Second) - return nil + n.mu.Lock() + defer n.mu.Unlock() + if n.started { + return n.StartContainer(ctx) + } + if err := n.createNodeContainer(ctx); err != nil { + return err + } + if err := n.ContainerLifecycle.StartContainer(ctx); err != nil { + return err + } + hostPorts, err := n.ContainerLifecycle.GetHostPorts(ctx, defaultInternalPorts().Web+"/tcp") + if err != nil { + return err + } + mapped := internal.MustExtractPort(hostPorts[0]) + n.external = types.Ports{HTTP: mapped} + n.started = true + // readiness wait for /metrics endpoint (best-effort) + waitHTTP(fmt.Sprintf("http://127.0.0.1:%s/metrics", n.external.HTTP), 20*time.Second) + return nil } // API returns a client bound to this node's exposed HTTP port. func (n *Node) API() *API { - base := fmt.Sprintf("http://127.0.0.1:%s", n.external.HTTP) - return NewAPI(base) + base := fmt.Sprintf("http://127.0.0.1:%s", n.external.HTTP) + return NewAPI(base) } func (n *Node) createNodeContainer(ctx context.Context) error { - p := defaultInternalPorts() - - // Daemon flags only; entrypoint will be spamoor-daemon - dbPath := fmt.Sprintf("%s/%s", n.HomeDir(), "spamoor.db") - binds := n.Bind() - cmd := []string{ - "--privkey", n.cfg.PrivateKey, - "--port", p.Web, - "--db", dbPath, - } - for _, h := range n.cfg.RPCHosts { - if s := strings.TrimSpace(h); s != "" { - cmd = append(cmd, "--rpchost", s) - } - } - - port := network.MustParsePort(p.Web + "/tcp") - ports := network.PortMap{ - port: []network.PortBinding{{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: ""}}, - } - - // IMPORTANT: override entrypoint to the daemon (absolute path inside image) - return n.CreateContainer( - ctx, - n.TestName, - n.NetworkID, - n.cfg.Image, - ports, - "", - binds, - nil, - n.HostName(), - cmd, - nil, - []string{"/app/spamoor-daemon"}, // entrypoint override - ) + p := defaultInternalPorts() + + // Daemon flags only; entrypoint will be spamoor-daemon + dbPath := fmt.Sprintf("%s/%s", n.HomeDir(), "spamoor.db") + binds := n.Bind() + cmd := []string{ + "--privkey", n.cfg.PrivateKey, + "--port", p.Web, + "--db", dbPath, + } + for _, h := range n.cfg.RPCHosts { + if s := strings.TrimSpace(h); s != "" { + cmd = append(cmd, "--rpchost", s) + } + } + + port := network.MustParsePort(p.Web + "/tcp") + ports := network.PortMap{ + port: []network.PortBinding{{HostIP: netip.MustParseAddr("0.0.0.0"), HostPort: ""}}, + } + + // IMPORTANT: override entrypoint to the daemon (absolute path inside image) + return n.CreateContainer( + ctx, + n.TestName, + n.NetworkID, + n.cfg.Image, + ports, + "", + binds, + nil, + n.HostName(), + cmd, + nil, + []string{"/app/spamoor-daemon"}, // entrypoint override + ) } // waitHTTP polls a URL until it succeeds or the timeout elapses. func waitHTTP(url string, timeout time.Duration) { - deadline := time.Now().Add(timeout) - client := &http.Client{Timeout: 500 * time.Millisecond} - for time.Now().Before(deadline) { - resp, err := client.Get(url) - if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 500 { - _ = resp.Body.Close() - return - } - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - time.Sleep(100 * time.Millisecond) - } + deadline := time.Now().Add(timeout) + client := &http.Client{Timeout: 500 * time.Millisecond} + for time.Now().Before(deadline) { + resp, err := client.Get(url) + if err == nil && resp.StatusCode >= 200 && resp.StatusCode < 500 { + _ = resp.Body.Close() + return + } + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + time.Sleep(100 * time.Millisecond) + } } - diff --git a/framework/docker/hyperlane/hyperlane_config.go b/framework/docker/hyperlane/hyperlane_config.go index f0903ba..c69dfe3 100644 --- a/framework/docker/hyperlane/hyperlane_config.go +++ b/framework/docker/hyperlane/hyperlane_config.go @@ -24,7 +24,8 @@ func DefaultDeployerImage() container.Image { } func DefaultForwardRelayerImage() container.Image { - return container.NewImage("ghcr.io/celestiaorg/forwarding-relayer", "v0.1.0", "1000:1000") + // binary writes to /app/storage/ which is outside the volume, requires root + return container.NewImage("ghcr.io/celestiaorg/forwarding-relayer", "v0.1.0", "0:0") } // CosmosConfig contains the IDs of all deployed cosmos-native hyperlane components diff --git a/framework/docker/ibc/relayer/hermes_types.go b/framework/docker/ibc/relayer/hermes_types.go index cecd8f1..fbb341f 100644 --- a/framework/docker/ibc/relayer/hermes_types.go +++ b/framework/docker/ibc/relayer/hermes_types.go @@ -33,4 +33,4 @@ type CreateConnectionResult struct { // ConnectionSide captures the connection ID for each side type ConnectionSide struct { ConnectionID string `json:"connection_id"` -} \ No newline at end of file +} diff --git a/framework/docker/ibc_test.go b/framework/docker/ibc_test.go index 0a9d691..7a27e5b 100644 --- a/framework/docker/ibc_test.go +++ b/framework/docker/ibc_test.go @@ -56,7 +56,8 @@ func createSimappChain(t *testing.T, ctx context.Context, client types.TastoraDo // use the simapp from ibc-go as a simple app with basic wiring and no token filters. // TODO: this is a custom built simapp that has the bech32prefix as "celestia" as a workaround for the global // SDK config not being usable when 2 chains have a different beck32 preix (e.g. "celestia" and "cosmos" ) if it is sealed. - WithImage(container.NewImage("ghcr.io/chatton/ibc-go-simd", "v8.5.0", "1000:1000")). + // simd writes to $HOME/.simapp on startup, requires root + WithImage(container.NewImage("ghcr.io/chatton/ibc-go-simd", "v8.5.0", "0:0")). WithBinaryName("simd"). WithBech32Prefix("celestia"). WithDenom("stake"). diff --git a/framework/docker/internal/keyring.go b/framework/docker/internal/keyring.go index 4f8f57b..b14d9a9 100644 --- a/framework/docker/internal/keyring.go +++ b/framework/docker/internal/keyring.go @@ -11,10 +11,10 @@ import ( "path/filepath" "github.com/cosmos/cosmos-sdk/codec" - "github.com/moby/moby/client" codectypes "github.com/cosmos/cosmos-sdk/codec/types" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/moby/moby/client" ) // NewLocalKeyringFromDockerContainer copies the contents of the given container directory into a specified local directory. diff --git a/framework/docker/reth_test.go b/framework/docker/reth_test.go index b61b787..6f4b251 100644 --- a/framework/docker/reth_test.go +++ b/framework/docker/reth_test.go @@ -1,12 +1,12 @@ package docker import ( - "math/big" - "strings" - "testing" - "time" + "math/big" + "strings" + "testing" + "time" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/require" ) // TestRethNode_LivenessAndGenesis verifies the first-class reth resource by @@ -20,8 +20,8 @@ func TestRethNode_LivenessAndGenesis(t *testing.T) { testCfg := setupDockerTest(t) - // Build a single Reth node from pre-configured builder - node, err := testCfg.RethBuilder.Build(testCfg.Ctx) + // Build a single Reth node from pre-configured builder + node, err := testCfg.RethBuilder.Build(testCfg.Ctx) require.NoError(t, err) t.Cleanup(func() {