Skip to content

Commit 736d4bc

Browse files
authored
fix: ensure volumes can be rebound to new containers (#136)
* chore: wip adding test to verify volume re-use * chore: small refactor in volume creation * chore: sanitize docker resource name * chore: add version negotiation
1 parent 9c2b3df commit 736d4bc

11 files changed

Lines changed: 155 additions & 23 deletions

File tree

framework/docker/container/job.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func (job *Job) Start(ctx context.Context, cmd []string, opts Options) (*Contain
210210
}
211211

212212
var (
213-
containerName = internal.SanitizeContainerName(job.testName + "-" + random.LowerCaseLetterString(6))
213+
containerName = internal.SanitizeDockerResourceName(job.testName + "-" + random.LowerCaseLetterString(6))
214214
hostName = internal.CondenseHostName(containerName)
215215
logger = job.log.With(
216216
zap.String("command", strings.Join(cmd, " ")), //nolint:gosec // testing only so safe to expose credentials in cli

framework/docker/container/node.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package container
33
import (
44
"context"
55
"fmt"
6+
"github.com/celestiaorg/tastora/framework/docker/internal"
67

78
"github.com/celestiaorg/tastora/framework/docker/consts"
89
"github.com/celestiaorg/tastora/framework/docker/file"
@@ -158,28 +159,26 @@ func (n *Node) WriteFile(ctx context.Context, relPath string, content []byte) er
158159
return fw.WriteFile(ctx, n.VolumeName, relPath, content)
159160
}
160161

162+
func (n *Node) GetVolumeName(nodeName string) string {
163+
return internal.SanitizeDockerResourceName(n.TestName + "-" + nodeName + "-vol")
164+
}
165+
161166
// CreateAndSetupVolume creates a Docker volume for the node and sets up proper ownership.
162167
// This consolidates the volume creation pattern used across all node types.
163168
// The nodeName parameter should be the specific name for this node instance.
169+
// Volume names are deterministic based on testName and nodeName to support re-binding.
164170
func (n *Node) CreateAndSetupVolume(ctx context.Context, nodeName string) error {
165-
// create volume with appropriate labels
166-
v, err := n.DockerClient.VolumeCreate(ctx, volumetypes.CreateOptions{
167-
Labels: map[string]string{
168-
consts.CleanupLabel: n.TestName,
169-
consts.NodeOwnerLabel: nodeName,
170-
},
171-
})
172-
if err != nil {
173-
return fmt.Errorf("creating volume for %s: %w", n.nodeType.String(), err)
171+
// volume name should be deterministic based on test name and node name.
172+
volName := n.GetVolumeName(nodeName)
173+
if err := n.ensureVolume(ctx, nodeName, volName); err != nil {
174+
return fmt.Errorf("ensure volume %w", err)
174175
}
175176

176-
n.VolumeName = v.Name
177-
178177
// configure volume ownership
179178
if err := volume.SetOwner(ctx, volume.OwnerOptions{
180179
Log: n.Logger,
181180
Client: n.DockerClient,
182-
VolumeName: v.Name,
181+
VolumeName: volName,
183182
ImageRef: n.Image.Ref(),
184183
TestName: n.TestName,
185184
UidGid: n.Image.UIDGID,
@@ -189,3 +188,26 @@ func (n *Node) CreateAndSetupVolume(ctx context.Context, nodeName string) error
189188

190189
return nil
191190
}
191+
192+
// ensureVolume checks if a volume with the given name exists, and creates it if it doesn't.
193+
func (n *Node) ensureVolume(ctx context.Context, nodeName, volName string) error {
194+
// check if volume already exists
195+
_, err := n.DockerClient.VolumeInspect(ctx, volName)
196+
if err != nil {
197+
// volume doesn't exist, create it
198+
v, createErr := n.DockerClient.VolumeCreate(ctx, volumetypes.CreateOptions{
199+
Name: volName,
200+
Labels: map[string]string{
201+
consts.CleanupLabel: n.TestName,
202+
consts.NodeOwnerLabel: nodeName,
203+
},
204+
})
205+
if createErr != nil {
206+
return fmt.Errorf("creating volume for %s: %w", n.nodeType.String(), createErr)
207+
}
208+
volName = v.Name
209+
}
210+
211+
n.VolumeName = volName
212+
return nil
213+
}

framework/docker/cosmos/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func (cn *ChainNode) GetKeyring() (keyring.Keyring, error) {
242242

243243
// Name of the test node container.
244244
func (cn *ChainNode) Name() string {
245-
return fmt.Sprintf("%s-%s-%d-%s", cn.ChainID, cn.NodeType(), cn.Index, internal.SanitizeContainerName(cn.TestName))
245+
return fmt.Sprintf("%s-%s-%d-%s", cn.ChainID, cn.NodeType(), cn.Index, internal.SanitizeDockerResourceName(cn.TestName))
246246
}
247247

248248
// NodeType returns the type of the ChainNode as a string.

framework/docker/dataavailability/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func NewNode(cfg Config, testName string, image container.Image, index int, node
9595

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

101101
// HostName returns the condensed hostname for the Node.

framework/docker/evstack/node.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func NewNode(cfg Config, testName string, image container.Image, index int, isAg
7373

7474
// Name of the test node container.
7575
func (n *Node) Name() string {
76-
return fmt.Sprintf("%s-evstack-%d-%s", n.cfg.ChainID, n.Index, internal.SanitizeContainerName(n.TestName))
76+
return fmt.Sprintf("%s-evstack-%d-%s", n.cfg.ChainID, n.Index, internal.SanitizeDockerResourceName(n.TestName))
7777
}
7878

7979
// HostName returns the condensed hostname for the Node.
@@ -294,4 +294,3 @@ func (n *Node) isNodeHealthy(client *http.Client, healthURL string) bool {
294294
n.logger().Debug("evstack node not ready yet", zap.String("url", healthURL), zap.Int("status", resp.StatusCode))
295295
return false
296296
}
297-

framework/docker/evstack/reth/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func newNode(ctx context.Context, cfg Config, testName string, index int) (*Node
5454

5555
// Name returns a stable container name
5656
func (n *Node) Name() string {
57-
return fmt.Sprintf("reth-%d-%s", n.Index, internal.SanitizeContainerName(n.TestName))
57+
return fmt.Sprintf("reth-%d-%s", n.Index, internal.SanitizeDockerResourceName(n.TestName))
5858
}
5959

6060
// HostName returns a condensed hostname

framework/docker/ibc/relayer/hermes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func NewHermes(ctx context.Context, dockerClient *dockerclient.Client, testName,
7272

7373
// Name returns the hostname of the docker container.
7474
func (h *Hermes) Name() string {
75-
return fmt.Sprintf("%s-%d-hermes", internal.SanitizeContainerName(h.TestName), h.Index)
75+
return fmt.Sprintf("%s-%d-hermes", internal.SanitizeDockerResourceName(h.TestName), h.Index)
7676
}
7777

7878
// Start starts the Hermes relayer.

framework/docker/internal/strings.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ func CondenseHostName(name string) string {
2424

2525
var validContainerCharsRE = regexp.MustCompile(`[^a-zA-Z0-9_.-]`)
2626

27-
// SanitizeContainerName returns name with any
27+
// SanitizeDockerResourceName returns name with any
2828
// invalid characters replaced with underscores.
2929
// Subtests will include slashes, and there may be other
3030
// invalid characters too.
31-
func SanitizeContainerName(name string) string {
31+
func SanitizeDockerResourceName(name string) string {
3232
return validContainerCharsRE.ReplaceAllLiteralString(name, "_")
3333
}
3434

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package docker
2+
3+
import (
4+
"github.com/celestiaorg/tastora/framework/docker/container"
5+
"github.com/celestiaorg/tastora/framework/docker/internal"
6+
"github.com/celestiaorg/tastora/framework/types"
7+
"github.com/stretchr/testify/require"
8+
"go.uber.org/zap/zaptest"
9+
"testing"
10+
)
11+
12+
func TestNode_VolumeRebinding(t *testing.T) {
13+
if testing.Short() {
14+
t.Skip("skipping due to short mode")
15+
}
16+
t.Parallel()
17+
18+
testCfg := setupDockerTest(t)
19+
20+
logger := zaptest.NewLogger(t)
21+
image := container.Image{
22+
Repository: "alpine",
23+
Version: "latest",
24+
UIDGID: "0:0",
25+
}
26+
27+
createNode := func() *container.Node {
28+
node := container.NewNode(
29+
testCfg.NetworkID,
30+
testCfg.DockerClient,
31+
testCfg.TestName,
32+
image,
33+
"/test",
34+
0,
35+
types.NodeTypeValidator,
36+
logger,
37+
)
38+
node.SetContainerLifecycle(container.NewLifecycle(logger, testCfg.DockerClient, testCfg.TestName))
39+
40+
nodeName := testCfg.TestName + "-test-node-0"
41+
42+
err := node.CreateAndSetupVolume(testCfg.Ctx, nodeName)
43+
require.NoError(t, err)
44+
require.NotEmpty(t, node.VolumeName)
45+
46+
// create a container that will just sleep forever so the test can exec into it.
47+
err = node.CreateContainer(
48+
testCfg.Ctx,
49+
t.Name(),
50+
testCfg.NetworkID,
51+
image,
52+
nil,
53+
"",
54+
[]string{node.GetVolumeName(nodeName) + ":/test"},
55+
nil,
56+
internal.CondenseHostName(nodeName),
57+
[]string{"sleep", "infinity"},
58+
nil,
59+
nil,
60+
)
61+
require.NoError(t, err)
62+
return node
63+
}
64+
65+
node := createNode()
66+
67+
err := node.StartContainer(testCfg.Ctx)
68+
require.NoError(t, err)
69+
70+
volumeName1 := node.VolumeName
71+
72+
// write test files to volume
73+
testContent1 := []byte("test file 1 content")
74+
testContent2 := []byte("test file 2 content")
75+
76+
err = node.WriteFile(testCfg.Ctx, "test1.txt", testContent1)
77+
require.NoError(t, err)
78+
79+
err = node.WriteFile(testCfg.Ctx, "test2.txt", testContent2)
80+
require.NoError(t, err)
81+
82+
// verify files were written
83+
readContent1, err := node.ReadFile(testCfg.Ctx, "test1.txt")
84+
require.NoError(t, err)
85+
require.Equal(t, testContent1, readContent1)
86+
87+
readContent2, err := node.ReadFile(testCfg.Ctx, "test2.txt")
88+
require.NoError(t, err)
89+
require.Equal(t, testContent2, readContent2)
90+
91+
// remove the code but preserver the volumes, this allows us to create a new node on the same volume.
92+
err = node.Remove(testCfg.Ctx, types.WithPreserveVolumes())
93+
require.NoError(t, err)
94+
95+
// re-creating exact same node, which should bind to the same volume.
96+
node = createNode()
97+
98+
volumeName2 := node.VolumeName
99+
100+
// verify that the same volume was used
101+
require.Equal(t, volumeName1, volumeName2, "should reuse the same volume")
102+
103+
// verify that files are still present
104+
readContent1, err = node.ReadFile(testCfg.Ctx, "test1.txt")
105+
require.NoError(t, err)
106+
require.Equal(t, testContent1, readContent1)
107+
108+
readContent2, err = node.ReadFile(testCfg.Ctx, "test2.txt")
109+
require.NoError(t, err)
110+
require.Equal(t, testContent2, readContent2)
111+
}

framework/docker/setup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ var KeepVolumesOnFailure = os.Getenv("ICTEST_SKIP_FAILURE_CLEANUP") != ""
4747
func DockerSetup(t DockerSetupTestingT) (*client.Client, string) {
4848
t.Helper()
4949

50-
cli, err := client.NewClientWithOpts(client.FromEnv)
50+
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
5151
if err != nil {
5252
panic(fmt.Errorf("failed to create docker client: %v", err))
5353
}

0 commit comments

Comments
 (0)