Skip to content

Commit 6efaae8

Browse files
authored
feat: git clone commits and docker data dirs to work with zfs/overlayfs (#77)
1 parent 0710005 commit 6efaae8

5 files changed

Lines changed: 176 additions & 18 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
2525
# Final stage
2626
FROM alpine:3.21
2727

28-
RUN apk add --no-cache ca-certificates tzdata git
28+
RUN apk add --no-cache ca-certificates tzdata git zfs fuse-overlayfs
2929

3030
WORKDIR /app
3131

action.yaml

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,72 @@ runs:
135135
CONFIGEOF
136136
fi
137137
138+
# Extract unique filesystem mount points from datadir source_dir values
139+
DATADIR_MOUNT_POINTS=""
140+
CONFIG_FILES=()
141+
for cfg in "${{ runner.temp }}"/benchmarkoor-config-url-*.yaml; do
142+
[ -f "$cfg" ] && CONFIG_FILES+=("$cfg")
143+
done
144+
if [ -f "${{ runner.temp }}/benchmarkoor-config-inline.yaml" ]; then
145+
CONFIG_FILES+=("${{ runner.temp }}/benchmarkoor-config-inline.yaml")
146+
fi
147+
148+
if [ ${#CONFIG_FILES[@]} -gt 0 ]; then
149+
ALL_SOURCE_DIRS=""
150+
for cfg in "${CONFIG_FILES[@]}"; do
151+
# Extract source_dir from client.datadirs map values and client.instances[].datadir
152+
DIRS=$(yq -r '
153+
[
154+
(.client.datadirs // {} | .[].source_dir // empty),
155+
(.client.instances // [] | .[].datadir.source_dir // empty)
156+
] | .[]
157+
' "$cfg" 2>/dev/null || true)
158+
if [ -n "$DIRS" ]; then
159+
ALL_SOURCE_DIRS="${ALL_SOURCE_DIRS}${DIRS}"$'\n'
160+
fi
161+
done
162+
163+
# Resolve each source_dir to its filesystem mountpoint, deduplicate
164+
if [ -n "$ALL_SOURCE_DIRS" ]; then
165+
DATADIR_MOUNT_POINTS=$(echo "$ALL_SOURCE_DIRS" | while IFS= read -r dir; do
166+
dir=$(echo "$dir" | xargs)
167+
[ -z "$dir" ] && continue
168+
if [ -d "$dir" ]; then
169+
stat --format=%m "$dir" 2>/dev/null || true
170+
else
171+
echo "Warning: datadir source_dir '$dir' does not exist, skipping"
172+
fi
173+
done | sort -u)
174+
fi
175+
fi
176+
177+
if [ -n "$DATADIR_MOUNT_POINTS" ]; then
178+
echo "Detected datadir filesystem mount points for rshared propagation:"
179+
echo "$DATADIR_MOUNT_POINTS"
180+
fi
181+
138182
# Build docker run command
139183
DOCKER_CMD="docker run --network=host --rm --name=benchmarkoor-run"
140184
DOCKER_CMD="$DOCKER_CMD -v /var/run/docker.sock:/var/run/docker.sock"
141185
DOCKER_CMD="$DOCKER_CMD -v ${{ runner.temp }}/results:/app/results"
142186
DOCKER_CMD="$DOCKER_CMD -v ${{ runner.temp }}:/app/configs"
143-
DOCKER_CMD="$DOCKER_CMD -v /tmp:/tmp"
144-
DOCKER_CMD="$DOCKER_CMD --cap-add=SYS_ADMIN"
187+
DOCKER_CMD="$DOCKER_CMD --mount type=bind,source=/tmp,target=/tmp,bind-propagation=rshared"
188+
DOCKER_CMD="$DOCKER_CMD --privileged"
145189
DOCKER_CMD="$DOCKER_CMD -v /proc/sys/vm/drop_caches:/host_drop_caches"
146190
DOCKER_CMD="$DOCKER_CMD -v /sys/devices/system/cpu:/host_sys_cpu"
147191
DOCKER_CMD="$DOCKER_CMD -e BENCHMARKOOR_GLOBAL_DROP_CACHES_PATH=/host_drop_caches"
148192
DOCKER_CMD="$DOCKER_CMD -e BENCHMARKOOR_GLOBAL_CPU_SYSFS_PATH=/host_sys_cpu"
149193
DOCKER_CMD="$DOCKER_CMD -e BENCHMARKOOR_BENCHMARK_RESULTS_DIR=/app/results"
150194
DOCKER_CMD="$DOCKER_CMD -e BENCHMARKOOR_BENCHMARK_RESULTS_OWNER=$(id -u):$(id -g)"
195+
196+
# Add rshared mounts for datadir filesystem mount points (ZFS/overlayfs)
197+
if [ -n "$DATADIR_MOUNT_POINTS" ]; then
198+
while IFS= read -r mp; do
199+
[ -z "$mp" ] && continue
200+
DOCKER_CMD="$DOCKER_CMD --mount type=bind,source=${mp},target=${mp},bind-propagation=rshared"
201+
done <<< "$DATADIR_MOUNT_POINTS"
202+
fi
203+
151204
DOCKER_CMD="$DOCKER_CMD ${IMAGE}"
152205
DOCKER_CMD="$DOCKER_CMD run"
153206

docker-compose.yaml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ services:
44
build:
55
context: .
66
dockerfile: Dockerfile
7-
cap_add:
8-
# Required for dropping memory caches between tests (config.yaml#client.config.drop_memory_caches)
9-
- SYS_ADMIN
7+
# Required for dropping memory caches, ZFS/overlayfs datadir methods, etc.
8+
privileged: true
109
environment:
1110
- BENCHMARKOOR_GLOBAL_DROP_CACHES_PATH=/host_drop_caches
1211
- BENCHMARKOOR_GLOBAL_CPU_SYSFS_PATH=/host_sys_cpu
@@ -25,7 +24,18 @@ services:
2524
# Config file
2625
- ./${BENCHMARKOOR_CONFIG:-config.example.docker.yaml}:/app/config.yaml:ro
2726
# Temp directory (For fetching temporary files and node data dirs)
28-
- ${PWD}/tmp:${PWD}/tmp
27+
# rshared propagation ensures overlay mounts are visible to sibling containers
28+
- type: bind
29+
source: ${PWD}/tmp
30+
target: ${PWD}/tmp
31+
bind:
32+
propagation: rshared
33+
# For ZFS datadir methods, mount your ZFS data path with rshared propagation:
34+
#- type: bind
35+
# source: /data-zfs
36+
# target: /data-zfs
37+
# bind:
38+
# propagation: rshared
2939
# Results directory
3040
- ./results:/app/results
3141
working_dir: /app

pkg/executor/source.go

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,17 +206,24 @@ func (s *GitSource) prepareRepo(ctx context.Context) (string, error) {
206206
return "", fmt.Errorf("creating cache directory: %w", err)
207207
}
208208

209-
// Shallow clone with specific branch/tag.
210-
cmd := exec.CommandContext(ctx, "git", "clone",
211-
"--depth=1",
212-
"--branch", s.cfg.Version,
213-
"--single-branch",
214-
s.cfg.Repo, localPath)
215-
cmd.Stdout = os.Stdout
216-
cmd.Stderr = os.Stderr
217-
218-
if err := cmd.Run(); err != nil {
219-
return "", fmt.Errorf("cloning repository: %w", err)
209+
if looksLikeCommitHash(s.cfg.Version) {
210+
// Commit hashes can't be used with --branch, so we init + fetch instead.
211+
if err := s.cloneByCommitHash(ctx, localPath); err != nil {
212+
return "", err
213+
}
214+
} else {
215+
// Shallow clone with specific branch/tag.
216+
cmd := exec.CommandContext(ctx, "git", "clone",
217+
"--depth=1",
218+
"--branch", s.cfg.Version,
219+
"--single-branch",
220+
s.cfg.Repo, localPath)
221+
cmd.Stdout = os.Stdout
222+
cmd.Stderr = os.Stderr
223+
224+
if err := cmd.Run(); err != nil {
225+
return "", fmt.Errorf("cloning repository: %w", err)
226+
}
220227
}
221228
} else {
222229
log.Info("Updating cached repository")
@@ -279,6 +286,66 @@ func (s *GitSource) GetSourceInfo() (*SuiteSource, error) {
279286
return &SuiteSource{Git: git}, nil
280287
}
281288

289+
// cloneByCommitHash initializes a repo and fetches a specific commit hash.
290+
func (s *GitSource) cloneByCommitHash(ctx context.Context, localPath string) error {
291+
// git init
292+
cmd := exec.CommandContext(ctx, "git", "init", localPath)
293+
cmd.Stdout = os.Stdout
294+
cmd.Stderr = os.Stderr
295+
296+
if err := cmd.Run(); err != nil {
297+
return fmt.Errorf("initializing repository: %w", err)
298+
}
299+
300+
// git remote add origin <repo>
301+
cmd = exec.CommandContext(ctx, "git", "-C", localPath,
302+
"remote", "add", "origin", s.cfg.Repo)
303+
cmd.Stdout = os.Stdout
304+
cmd.Stderr = os.Stderr
305+
306+
if err := cmd.Run(); err != nil {
307+
return fmt.Errorf("adding remote: %w", err)
308+
}
309+
310+
// git fetch --depth=1 origin <hash>
311+
cmd = exec.CommandContext(ctx, "git", "-C", localPath,
312+
"fetch", "--depth=1", "origin", s.cfg.Version)
313+
cmd.Stdout = os.Stdout
314+
cmd.Stderr = os.Stderr
315+
316+
if err := cmd.Run(); err != nil {
317+
return fmt.Errorf("fetching commit %s: %w", s.cfg.Version, err)
318+
}
319+
320+
// git checkout FETCH_HEAD
321+
cmd = exec.CommandContext(ctx, "git", "-C", localPath,
322+
"checkout", "FETCH_HEAD")
323+
cmd.Stdout = os.Stdout
324+
cmd.Stderr = os.Stderr
325+
326+
if err := cmd.Run(); err != nil {
327+
return fmt.Errorf("checking out commit %s: %w", s.cfg.Version, err)
328+
}
329+
330+
return nil
331+
}
332+
333+
// looksLikeCommitHash returns true if s looks like a git commit hash
334+
// (7-40 lowercase/uppercase hex characters).
335+
func looksLikeCommitHash(s string) bool {
336+
if len(s) < 7 || len(s) > 40 {
337+
return false
338+
}
339+
340+
for _, c := range s {
341+
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
342+
return false
343+
}
344+
}
345+
346+
return true
347+
}
348+
282349
// hashRepoURL creates a hash of the repository URL for caching.
283350
func hashRepoURL(url string) string {
284351
hash := sha256.Sum256([]byte(url))

pkg/executor/source_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,31 @@ func TestDiscoverTestsFromConfig_PreRunStepsNotFiltered(t *testing.T) {
5656
assert.Len(t, result.Tests, 1, "only bn128 test should match filter")
5757
assert.Contains(t, result.Tests[0].Name, "bn128")
5858
}
59+
60+
func TestLooksLikeCommitHash(t *testing.T) {
61+
tests := []struct {
62+
name string
63+
input string
64+
expected bool
65+
}{
66+
{name: "full sha1", input: "e5011aa5f75d7a1722481f25408347fadfb7fd3c", expected: true},
67+
{name: "short hash 7 chars", input: "e5011aa", expected: true},
68+
{name: "short hash 8 chars", input: "e5011aa5", expected: true},
69+
{name: "uppercase hex", input: "E5011AA5F75D7A17", expected: true},
70+
{name: "mixed case hex", input: "e5011AA5f75d", expected: true},
71+
{name: "branch name", input: "main", expected: false},
72+
{name: "branch with slash", input: "feature/foo", expected: false},
73+
{name: "tag semver", input: "v1.0.0", expected: false},
74+
{name: "too short 6 chars", input: "e5011a", expected: false},
75+
{name: "too long 41 chars", input: "e5011aa5f75d7a1722481f25408347fadfb7fd3c0", expected: false},
76+
{name: "empty string", input: "", expected: false},
77+
{name: "hex with non-hex char", input: "e5011gg", expected: false},
78+
{name: "7 char all digits", input: "1234567", expected: true},
79+
}
80+
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
assert.Equal(t, tt.expected, looksLikeCommitHash(tt.input))
84+
})
85+
}
86+
}

0 commit comments

Comments
 (0)