Skip to content
Merged
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
6 changes: 3 additions & 3 deletions framework/docker/chain_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestChainNodeHostName(t *testing.T) {
AdditionalStartArgs: []string{},
EncodingConfig: &testutil.TestEncodingConfig{},
}
node1 := cosmos.NewChainNode(logger, &tastoraclient.Client{}, "test-network", testName, container.Image{}, "/test/home", 0, chainParams1)
node1 := cosmos.NewChainNode(logger, &tastoraclient.Client{}, "test-network", testName, container.Image{}, "/test/home", 0, chainParams1) //nolint:staticcheck // testing deprecated API

chainParams2 := cosmos.ChainNodeParams{
Validator: true,
Expand All @@ -43,7 +43,7 @@ func TestChainNodeHostName(t *testing.T) {
AdditionalStartArgs: []string{},
EncodingConfig: &testutil.TestEncodingConfig{},
}
node2 := cosmos.NewChainNode(logger, &tastoraclient.Client{}, "test-network", testName, container.Image{}, "/test/home", 1, chainParams2)
node2 := cosmos.NewChainNode(logger, &tastoraclient.Client{}, "test-network", testName, container.Image{}, "/test/home", 1, chainParams2) //nolint:staticcheck // testing deprecated API

chainParams3 := cosmos.ChainNodeParams{
Validator: false,
Expand All @@ -56,7 +56,7 @@ func TestChainNodeHostName(t *testing.T) {
AdditionalStartArgs: []string{},
EncodingConfig: &testutil.TestEncodingConfig{},
}
node3 := cosmos.NewChainNode(logger, &tastoraclient.Client{}, "test-network", testName, container.Image{}, "/test/home", 2, chainParams3)
node3 := cosmos.NewChainNode(logger, &tastoraclient.Client{}, "test-network", testName, container.Image{}, "/test/home", 2, chainParams3) //nolint:staticcheck // testing deprecated API

// get hostnames
hostname1 := node1.HostName()
Expand Down
92 changes: 92 additions & 0 deletions framework/docker/container/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Node struct {
Logger *zap.Logger
}

// Deprecated: use NewNodeBuilder instead.
// NewNode creates a new Node instance with the required parameters.
func NewNode(
networkID string,
Expand All @@ -53,6 +54,96 @@ func NewNode(
}
}

// NodeBuilder provides a fluent API for constructing and initializing a Node.
type NodeBuilder struct {
networkID string
dockerClient types.TastoraDockerClient
testName string
image Image
homeDir string
index int
nodeType types.NodeType
logger *zap.Logger
volumeName string
hostNetwork bool
}

// NewNodeBuilder creates a NodeBuilder with the minimum required fields.
func NewNodeBuilder(dockerClient types.TastoraDockerClient, testName string, image Image, logger *zap.Logger) *NodeBuilder {
return &NodeBuilder{
dockerClient: dockerClient,
testName: testName,
image: image,
logger: logger,
}
}

func (b *NodeBuilder) WithNetworkID(networkID string) *NodeBuilder {
b.networkID = networkID
return b
}

func (b *NodeBuilder) WithHomeDir(homeDir string) *NodeBuilder {
b.homeDir = homeDir
return b
}

func (b *NodeBuilder) WithIndex(index int) *NodeBuilder {
b.index = index
return b
}

func (b *NodeBuilder) WithNodeType(nodeType types.NodeType) *NodeBuilder {
b.nodeType = nodeType
return b
}

// WithHostNetwork enables host network mode on the container lifecycle.
func (b *NodeBuilder) WithHostNetwork(hostNetwork bool) *NodeBuilder {
b.hostNetwork = hostNetwork
return b
}

// WithVolumeName overrides the name used for volume creation.
// By default, Build uses the containerName for the volume. Use this to share
// a volume with another node (e.g. a deployer and its agent).
func (b *NodeBuilder) WithVolumeName(volumeName string) *NodeBuilder {
b.volumeName = volumeName
return b
}

// Build creates the Node with its lifecycle and volume fully initialized.
// The containerName is used for the lifecycle and (unless WithVolumeName was called)
// for the volume. Callers should pass the name that the embedding type will use
// as its container name.
func (b *NodeBuilder) Build(ctx context.Context, containerName string) (*Node, error) {
if containerName == "" {
return nil, fmt.Errorf("containerName cannot be empty")
}
if b.homeDir == "" {
return nil, fmt.Errorf("homeDir cannot be empty")
}
if !b.hostNetwork && b.networkID == "" {
return nil, fmt.Errorf("networkID cannot be empty when host network is disabled")
}

n := NewNode(b.networkID, b.dockerClient, b.testName, b.image, b.homeDir, b.index, b.nodeType, b.logger)
lc := NewLifecycle(b.logger, b.dockerClient, containerName)
if b.hostNetwork {
lc.SetHostNetwork(b.hostNetwork)
}
n.ContainerLifecycle = lc
volName := containerName
if b.volumeName != "" {
volName = b.volumeName
}
if err := n.CreateAndSetupVolume(ctx, volName); err != nil {
return nil, fmt.Errorf("setup node %q: %w", containerName, err)
}
return n, nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Deprecated: use NewNodeBuilder instead.
// SetContainerLifecycle sets the container lifecycle for the node
func (n *Node) SetContainerLifecycle(lifecycle *Lifecycle) {
n.ContainerLifecycle = lifecycle
Expand Down Expand Up @@ -164,6 +255,7 @@ func (n *Node) GetVolumeName(nodeName string) string {
return internal.SanitizeDockerResourceName(n.TestName + "-" + nodeName + "-vol")
}

// Deprecated: use NewNodeBuilder instead.
// CreateAndSetupVolume creates a Docker volume for the node and sets up proper ownership.
// This consolidates the volume creation pattern used across all node types.
// The nodeName parameter should be the specific name for this node instance.
Expand Down
17 changes: 17 additions & 0 deletions framework/docker/cosmos/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,23 @@ func (c *Chain) startAllNodes(ctx context.Context) error {
return eg.Wait()
}

// Restart stops, removes, and recreates all node containers while preserving volumes.
func (c *Chain) Restart(ctx context.Context) error {
if err := c.Remove(ctx, types.WithPreserveVolumes()); err != nil {
return fmt.Errorf("failed to remove containers for restart: %w", err)
}
return c.startAllNodes(ctx)
}

// RestartNode restarts a single node by index without affecting other nodes.
func (c *Chain) RestartNode(ctx context.Context, index int) error {
nodes := c.Nodes()
if index < 0 || index >= len(nodes) {
return fmt.Errorf("node index %d out of range [0, %d)", index, len(nodes))
}
return nodes[index].Restart(ctx)
}

// Stop stops all nodes in the chain without removing them.
func (c *Chain) Stop(ctx context.Context) error {
var eg errgroup.Group
Expand Down
61 changes: 37 additions & 24 deletions framework/docker/cosmos/chain_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,26 +482,6 @@ func (b *ChainBuilder) newChainNode(
nodeConfig ChainNodeConfig,
index int,
) (*ChainNode, error) {
// Construct the ChainNode first so we can access its name.
// The ChainNode's VolumeName cannot be set until after we create the volume.
tn := b.newDockerChainNode(b.logger, nodeConfig, index)

// create and setup volume using shared logic
if err := tn.CreateAndSetupVolume(ctx, tn.Name()); err != nil {
return nil, err
}

// if this is a validator and we have a genesis keyring, preload the keys using a one-shot container
if nodeConfig.nodeType == types.NodeTypeValidator && tn.GenesisKeyring != nil {
if err := preloadKeyringToVolume(ctx, tn, nodeConfig); err != nil {
return nil, fmt.Errorf("failed to preload keyring to volume: %w", err)
}
}

return tn, nil
}

func (b *ChainBuilder) newDockerChainNode(log *zap.Logger, nodeConfig ChainNodeConfig, index int) *ChainNode {
homeDir := b.homeDir
if homeDir == "" {
homeDir = "/var/cosmos-chain"
Expand All @@ -510,9 +490,15 @@ func (b *ChainBuilder) newDockerChainNode(log *zap.Logger, nodeConfig ChainNodeC
}
}

// default unspecified node type to full node
nodeType := nodeConfig.nodeType
if nodeType == 0 {
nodeType = types.NodeTypeConsensusFull
}

chainParams := ChainNodeParams{
Validator: nodeConfig.nodeType == types.NodeTypeValidator,
NodeType: nodeConfig.nodeType,
Validator: nodeType == types.NodeTypeValidator,
NodeType: nodeType,
ChainID: b.chainID,
BinaryName: b.binaryName,
CoinType: b.coinType,
Expand All @@ -528,10 +514,37 @@ func (b *ChainBuilder) newDockerChainNode(log *zap.Logger, nodeConfig ChainNodeC
AdditionalExposedPorts: b.additionalExposedPorts,
}

// Get the appropriate image using fallback logic
imageToUse := b.getImage(nodeConfig)

return NewChainNode(log, b.dockerClient, b.dockerNetworkID, b.testName, imageToUse, homeDir, index, chainParams)
log := b.logger.With(
zap.Bool("validator", chainParams.Validator),
zap.Int("i", index),
)

name := chainNodeName(b.testName, index, b.chainID, nodeType)
baseNode, err := container.NewNodeBuilder(b.dockerClient, b.testName, imageToUse, log).
WithNetworkID(b.dockerNetworkID).
WithHomeDir(homeDir).
WithIndex(index).
WithNodeType(nodeType).
Build(ctx, name)
if err != nil {
return nil, err
}

tn := &ChainNode{
ChainNodeParams: chainParams,
Node: baseNode,
}

// if this is a validator and we have a genesis keyring, preload the keys using a one-shot container
if nodeConfig.nodeType == types.NodeTypeValidator && tn.GenesisKeyring != nil {
if err := preloadKeyringToVolume(ctx, tn, nodeConfig); err != nil {
return nil, fmt.Errorf("failed to preload keyring to volume: %w", err)
}
}

return tn, nil
}

// preloadKeyringToVolume copies validator keys from genesis keyring to the node's volume
Expand Down
19 changes: 18 additions & 1 deletion framework/docker/cosmos/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type ChainNodeParams struct {
AdditionalExposedPorts []string
}

// Deprecated: use container.NewNodeBuilder instead.
// NewChainNode creates a new ChainNode with injected dependencies
func NewChainNode(
logger *zap.Logger,
Expand Down Expand Up @@ -248,9 +249,13 @@ func (cn *ChainNode) GetKeyring() (keyring.Keyring, error) {
return internal.NewDockerKeyring(cn.DockerClient, cn.ContainerLifecycle.ContainerID(), containerKeyringDir, cn.EncodingConfig.Codec), nil
}

func chainNodeName(testName string, index int, chainID string, nodeType types.ConsensusNodeType) string {
return fmt.Sprintf("%s-%s-%d-%s", chainID, nodeType.String(), index, internal.SanitizeDockerResourceName(testName))
}

// Name of the test node container.
func (cn *ChainNode) Name() string {
return fmt.Sprintf("%s-%s-%d-%s", cn.ChainID, cn.NodeType(), cn.Index, internal.SanitizeDockerResourceName(cn.TestName))
return chainNodeName(cn.TestName, cn.Index, cn.ChainID, cn.ChainNodeParams.NodeType)
}

// NodeType returns the type of the ChainNode as a string.
Expand Down Expand Up @@ -460,6 +465,18 @@ func (cn *ChainNode) setPeers(ctx context.Context, peers string) error {
})
}

// Restart stops, removes, and recreates the node container while preserving volumes.
// The node rejoins the network with its existing state.
func (cn *ChainNode) Restart(ctx context.Context) error {
if err := cn.Remove(ctx, types.WithPreserveVolumes()); err != nil {
return fmt.Errorf("failed to remove container for restart: %w", err)
}
if err := cn.createNodeContainer(ctx); err != nil {
return fmt.Errorf("failed to create container for restart: %w", err)
}
return cn.startContainer(ctx)
}

// createNodeContainer initializes but does not start a container for the ChainNode with the specified configuration and context.
func (cn *ChainNode) createNodeContainer(ctx context.Context) error {
cmd := []string{cn.BinaryName, "start", "--home", cn.HomeDir()}
Expand Down
13 changes: 13 additions & 0 deletions framework/docker/dataavailability/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ func (n *Network) RemoveNodes(ctx context.Context, nodeNames ...string) error {
return nil
}

// Restart stops and restarts all nodes in the data availability network.
func (n *Network) Restart(ctx context.Context) error {
nodes := n.GetNodes()
var eg errgroup.Group
for _, nd := range nodes {
nd := nd
eg.Go(func() error {
return nd.Restart(ctx)
})
}
return eg.Wait()
}

// Stop stops all nodes in the data availability network without removing them.
func (n *Network) Stop(ctx context.Context) error {
nodes := n.GetNodes()
Expand Down
25 changes: 22 additions & 3 deletions framework/docker/dataavailability/network_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,32 @@ func (b *NetworkBuilder) newNode(ctx context.Context, nodeConfig NodeConfig, ind
nodeConfig.Env = b.env
}

node := NewNode(cfg, b.testName, imageToUse, index, nodeConfig)
homeDir := cfg.HomeDir
if homeDir == "" {
homeDir = DefaultHomeDir()
}

// Create and setup volume using shared logic
if err := node.CreateAndSetupVolume(ctx, node.Name()); err != nil {
log := b.logger.With(zap.String("node_type", nodeConfig.NodeType.String()))
name := daNodeName(b.testName, index, nodeConfig.NodeType)
baseNode, err := container.NewNodeBuilder(b.dockerClient, b.testName, imageToUse, log).
WithNetworkID(b.dockerNetworkID).
WithHomeDir(homeDir).
WithIndex(index).
WithNodeType(nodeConfig.NodeType).
Build(ctx, name)
if err != nil {
return nil, err
}

node := &Node{
cfg: cfg,
nodeType: nodeConfig.NodeType,
additionalStartArgs: nodeConfig.AdditionalStartArgs,
configModifications: nodeConfig.ConfigModifications,
internalPorts: initializeDANodePorts(nodeConfig.InternalPorts),
Node: baseNode,
}

// Run post-init functions if any
for _, postInitFn := range nodeConfig.postInit {
if err := postInitFn(ctx, node); err != nil {
Expand Down
15 changes: 14 additions & 1 deletion framework/docker/dataavailability/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Node struct {
externalPorts types.Ports
}

// Deprecated: use container.NewNodeBuilder instead.
func NewNode(cfg Config, testName string, image container.Image, index int, nodeConfig NodeConfig) *Node {
homeDir := cfg.HomeDir
if homeDir == "" {
Expand All @@ -97,9 +98,13 @@ func NewNode(cfg Config, testName string, image container.Image, index int, node
return node
}

func daNodeName(testName string, index int, nodeType types.DANodeType) string {
return fmt.Sprintf("da-%s-%d-%s", nodeType.String(), index, internal.SanitizeDockerResourceName(testName))
}

// Name returns the container name for the Node.
func (n *Node) Name() string {
return fmt.Sprintf("da-%s-%d-%s", n.nodeType.String(), n.Index, internal.SanitizeDockerResourceName(n.TestName))
return daNodeName(n.TestName, n.Index, n.nodeType)
}

// HostName returns the condensed hostname for the Node.
Expand Down Expand Up @@ -153,6 +158,14 @@ func (n *Node) GetNetworkInfo(ctx context.Context) (types.NetworkInfo, error) {
}, nil
}

// Restart stops and restarts the node container, preserving its existing state.
func (n *Node) Restart(ctx context.Context) error {
if err := n.StopContainer(ctx); err != nil {
return fmt.Errorf("failed to stop container for restart: %w", err)
}
return n.StartContainer(ctx)
}

// Start initializes and starts the Node with the provided options in the given context.
// It returns an error if the node initialization or startup fails.
func (n *Node) Start(ctx context.Context, opts ...StartOption) error {
Expand Down
Loading
Loading