Skip to content

Commit 6a8ca1a

Browse files
authored
fix: sort volume args in DOCKER_COMPAT mode (#417)
Issue #, if available: #418 *Description of changes:* Sort volume args in DOCKER_COMPAT mode *Testing done:* Unit tests and new e2e tests. runfinch/common-tests#66 - [ X ] I've reviewed the guidance in CONTRIBUTING.md #### License Acceptance By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. Signed-off-by: Ziwen Ning <[email protected]>
1 parent b83cc21 commit 6a8ca1a

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed

.github/workflows/ci-docs.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ jobs:
5656
runs-on: ${{ matrix.os }}
5757
steps:
5858
- run: echo "Skipping CI for docs & contrib files"
59+
e2e-tests-for-docker-compat:
60+
strategy:
61+
matrix:
62+
os:
63+
[
64+
[self-hosted, macos, amd64, 13, test],
65+
[self-hosted, macos, arm64, 13, test],
66+
]
67+
runs-on: ${{ matrix.os }}
68+
steps:
69+
- run: echo "Skipping CI for docs & contrib files"
5970
mdlint:
6071
runs-on: ubuntu-latest
6172
steps:

.github/workflows/ci.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,45 @@ jobs:
119119
shell: zsh {0}
120120
- run: make test-e2e
121121
shell: zsh {0}
122+
e2e-tests-for-docker-compat:
123+
strategy:
124+
fail-fast: false
125+
matrix:
126+
os:
127+
[
128+
[self-hosted, macos, amd64, 13, test],
129+
[self-hosted, macos, arm64, 13, test],
130+
]
131+
runs-on: ${{ matrix.os }}
132+
steps:
133+
- uses: actions/checkout@v3
134+
with:
135+
# We need to get all the git tags to make version injection work. See VERSION in Makefile for more detail.
136+
fetch-depth: 0
137+
persist-credentials: false
138+
submodules: true
139+
- name: Clean up previous files
140+
run: |
141+
sudo rm -rf /opt/finch
142+
sudo rm -rf ~/.finch
143+
sudo rm -rf ./_output
144+
if pgrep '^qemu-system'; then
145+
sudo pkill '^qemu-system'
146+
fi
147+
if pgrep '^socket_vmnet'; then
148+
sudo pkill '^socket_vmnet'
149+
fi
150+
- name: Install Rosetta 2
151+
run: echo "A" | softwareupdate --install-rosetta || true
152+
- run: brew install go lz4 automake autoconf libtool
153+
shell: zsh {0}
154+
- name: Build project
155+
run: |
156+
export PATH="/opt/homebrew/opt/libtool/libexec/gnubin:$PATH"
157+
make
158+
shell: zsh {0}
159+
- run: FINCH_DOCKER_COMPAT=1 make test-e2e
160+
shell: zsh {0}
122161
mdlint:
123162
runs-on: ubuntu-latest
124163
steps:

cmd/finch/nerdctl.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package main
66
import (
77
"bufio"
88
"fmt"
9+
"os"
910
"path/filepath"
11+
"sort"
1012
"strings"
1113

1214
dockerops "github.com/docker/docker/opts"
@@ -85,8 +87,12 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error {
8587
var (
8688
nerdctlArgs, envs, fileEnvs []string
8789
skip bool
90+
volumes []string
8891
)
8992

93+
dockerCompat := isDockerCompatEnvSet(nc.systemDeps)
94+
firstVolumeIndex := -1
95+
9096
for i, arg := range args {
9197
// parsing environment values from the command line may pre-fetch and
9298
// consume the next argument; this loop variable will skip these pre-consumed
@@ -121,10 +127,34 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error {
121127
arg = fmt.Sprintf("%s%s", arg[0:11], resolvedIP)
122128
}
123129
nerdctlArgs = append(nerdctlArgs, arg)
130+
case strings.HasPrefix(arg, "-v") || strings.HasPrefix(arg, "--volume"):
131+
// TODO: remove the case branch when the Nerdctl fix is released to Finch.
132+
// Nerdctl tracking issue: https://github.com/containerd/nerdctl/issues/2254.
133+
if dockerCompat {
134+
if firstVolumeIndex == -1 {
135+
firstVolumeIndex = i
136+
}
137+
volume, shouldSkip := getVolume(arg, args[i+1])
138+
volumes = append(volumes, volume)
139+
skip = shouldSkip
140+
} else {
141+
nerdctlArgs = append(nerdctlArgs, arg)
142+
}
124143
default:
125144
nerdctlArgs = append(nerdctlArgs, arg)
126145
}
127146
}
147+
148+
if dockerCompat {
149+
// Need to insert to the middle to prevent messing up the subcommands at the beginning and the image ref at the end.
150+
// The first volume index is a good middle position.
151+
volumes = sortVolumes(volumes)
152+
for i, vol := range volumes {
153+
position := firstVolumeIndex + i*2
154+
nerdctlArgs = append(nerdctlArgs[:position], append([]string{"-v", vol}, nerdctlArgs[position:]...)...)
155+
}
156+
}
157+
128158
// to handle environment variables properly, we add all entries found via
129159
// env-file includes to the map first and then all command line environment
130160
// flags, making sure that command line overrides environment file options,
@@ -300,6 +330,20 @@ func handleEnvFile(fs afero.Fs, systemDeps NerdctlCommandSystemDeps, arg, arg2 s
300330
return skip, envs, nil
301331
}
302332

333+
// getVolume extracts the volume string from a command line argument.
334+
// It handles both "--volume=" and "-v=" prefixes.
335+
// The function returns the extracted volume string and a boolean value indicating whether the volume was found in the next argument.
336+
// If the volume string was found in the provided 'arg' string (with "--volume=" or "-v=" prefix), it returns the volume and 'false'.
337+
// If the volume string wasn't in the 'arg', it assumes it's in the 'nextArg' string and returns 'nextArg' and 'true'.
338+
func getVolume(arg, nextArg string) (string, bool) {
339+
for _, prefix := range []string{"--volume=", "-v="} {
340+
if strings.HasPrefix(arg, prefix) {
341+
return arg[len(prefix):], false
342+
}
343+
}
344+
return nextArg, true
345+
}
346+
303347
func resolveIP(host string, logger flog.Logger) string {
304348
parts := strings.SplitN(host, ":", 2)
305349
// If the IP Address is a string called "host-gateway", replace this value with the IP address that can be used to
@@ -313,6 +357,50 @@ func resolveIP(host string, logger flog.Logger) string {
313357
return host
314358
}
315359

360+
func sortVolumes(volumes []string) []string {
361+
type volume struct {
362+
original string
363+
destination string
364+
}
365+
366+
volumeSlices := make([]volume, 0)
367+
for _, vol := range volumes {
368+
splitVol := strings.Split(vol, ":")
369+
if len(splitVol) >= 2 {
370+
volumeSlices = append(volumeSlices, volume{
371+
original: vol,
372+
destination: splitVol[1],
373+
})
374+
} else {
375+
// Still need to put the volume string back if it is in other format.
376+
volumeSlices = append(volumeSlices, volume{
377+
original: vol,
378+
destination: "",
379+
})
380+
}
381+
}
382+
sort.Slice(volumeSlices, func(i, j int) bool {
383+
// Consistent with the less function in Docker.
384+
// https://github.com/moby/moby/blob/0db417451313474133c5ed62bbf95e2d3c92444d/daemon/volumes.go#L45
385+
c1 := strings.Count(filepath.Clean(volumeSlices[i].destination), string(os.PathSeparator))
386+
c2 := strings.Count(filepath.Clean(volumeSlices[j].destination), string(os.PathSeparator))
387+
return c1 < c2
388+
})
389+
sortedVolumes := make([]string, 0)
390+
for _, vol := range volumeSlices {
391+
sortedVolumes = append(sortedVolumes, vol.original)
392+
}
393+
return sortedVolumes
394+
}
395+
396+
// isDockerCompatEnvSet checks if the FINCH_DOCKER_COMPAT environment variable is set, so that callers can
397+
// modify behavior to match docker instead of nerdctl.
398+
// Intended to be used for temporary workarounds until changes are merged upstream.
399+
func isDockerCompatEnvSet(systemDeps NerdctlCommandSystemDeps) bool {
400+
_, s := systemDeps.LookupEnv("FINCH_DOCKER_COMPAT")
401+
return s
402+
}
403+
316404
var nerdctlCmds = map[string]string{
317405
"build": "Build an image from Dockerfile",
318406
"builder": "Manage builds",

0 commit comments

Comments
 (0)