Skip to content

Commit 0f04942

Browse files
authored
Separate Container Workdir from host Workdir (#635)
* Separate Container Workdir from Host Workdir * Add delegated component to MacOS Test * Lint: Remove leading newline * Fix trailing path issue
1 parent 020d6a6 commit 0f04942

File tree

8 files changed

+263
-66
lines changed

8 files changed

+263
-66
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ pkg/runner/act/
2525
dist/local/act
2626

2727
coverage.txt
28+
29+
.env
30+
#Store your GITHUB_TOKEN secret here for purposes of local testing of actions/checkout and others
31+
.secrets

pkg/runner/run_context.go

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ func (rc *RunContext) jobContainerName() string {
6262
return createContainerName("act", rc.String())
6363
}
6464

65+
// Returns the binds and mounts for the container, resolving paths as appopriate
66+
func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
67+
name := rc.jobContainerName()
68+
69+
binds := []string{
70+
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
71+
}
72+
73+
mounts := map[string]string{
74+
"act-toolcache": "/toolcache",
75+
"act-actions": "/actions",
76+
}
77+
78+
if rc.Config.BindWorkdir {
79+
bindModifiers := ""
80+
if runtime.GOOS == "darwin" {
81+
bindModifiers = ":delegated"
82+
}
83+
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, rc.Config.ContainerWorkdir(), bindModifiers))
84+
} else {
85+
mounts[name] = rc.Config.ContainerWorkdir()
86+
}
87+
88+
return binds, mounts
89+
}
90+
6591
func (rc *RunContext) startJobContainer() common.Executor {
6692
image := rc.platformImage()
6793

@@ -80,34 +106,21 @@ func (rc *RunContext) startJobContainer() common.Executor {
80106
name := rc.jobContainerName()
81107

82108
envList := make([]string, 0)
83-
bindModifiers := ""
84-
if runtime.GOOS == "darwin" {
85-
bindModifiers = ":delegated"
86-
}
87109

88110
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TOOL_CACHE", "/opt/hostedtoolcache"))
89111
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux"))
90112
envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp"))
91113

92-
binds := []string{
93-
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
94-
}
95-
if rc.Config.BindWorkdir {
96-
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, rc.Config.Workdir, bindModifiers))
97-
}
114+
binds, mounts := rc.GetBindsAndMounts()
98115

99116
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
100-
Cmd: nil,
101-
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
102-
WorkingDir: rc.Config.Workdir,
103-
Image: image,
104-
Name: name,
105-
Env: envList,
106-
Mounts: map[string]string{
107-
name: filepath.Dir(rc.Config.Workdir),
108-
"act-toolcache": "/toolcache",
109-
"act-actions": "/actions",
110-
},
117+
Cmd: nil,
118+
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
119+
WorkingDir: rc.Config.ContainerWorkdir(),
120+
Image: image,
121+
Name: name,
122+
Env: envList,
123+
Mounts: mounts,
111124
NetworkMode: "host",
112125
Binds: binds,
113126
Stdout: logWriter,
@@ -121,7 +134,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
121134
var copyToPath string
122135
if !rc.Config.BindWorkdir {
123136
copyToPath, copyWorkspace = rc.localCheckoutPath()
124-
copyToPath = filepath.Join(rc.Config.Workdir, copyToPath)
137+
copyToPath = filepath.Join(rc.Config.ContainerWorkdir(), copyToPath)
125138
}
126139

127140
return common.NewPipelineExecutor(
@@ -130,7 +143,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
130143
rc.JobContainer.Create(),
131144
rc.JobContainer.Start(false),
132145
rc.JobContainer.CopyDir(copyToPath, rc.Config.Workdir+string(filepath.Separator)+".", rc.Config.UseGitIgnore).IfBool(copyWorkspace),
133-
rc.JobContainer.Copy(filepath.Dir(rc.Config.Workdir), &container.FileEntry{
146+
rc.JobContainer.Copy(rc.Config.ContainerWorkdir(), &container.FileEntry{
134147
Name: "workflow/event.json",
135148
Mode: 0644,
136149
Body: rc.EventJSON,
@@ -163,6 +176,8 @@ func (rc *RunContext) stopJobContainer() common.Executor {
163176
}
164177
}
165178

179+
// Prepare the mounts and binds for the worker
180+
166181
// ActionCacheDir is for rc
167182
func (rc *RunContext) ActionCacheDir() string {
168183
var xdgCache string
@@ -468,14 +483,14 @@ func (rc *RunContext) getGithubContext() *githubContext {
468483
}
469484
ghc := &githubContext{
470485
Event: make(map[string]interface{}),
471-
EventPath: fmt.Sprintf("%s/%s", filepath.Dir(rc.Config.Workdir), "workflow/event.json"),
486+
EventPath: fmt.Sprintf("%s/%s", rc.Config.ContainerWorkdir(), "workflow/event.json"),
472487
Workflow: rc.Run.Workflow.Name,
473488
RunID: runID,
474489
RunNumber: runNumber,
475490
Actor: rc.Config.Actor,
476491
EventName: rc.Config.EventName,
477492
Token: token,
478-
Workspace: rc.Config.Workdir,
493+
Workspace: rc.Config.ContainerWorkdir(),
479494
Action: rc.CurrentStep,
480495
}
481496

@@ -537,6 +552,10 @@ func (rc *RunContext) getGithubContext() *githubContext {
537552
}
538553

539554
func (ghc *githubContext) isLocalCheckout(step *model.Step) bool {
555+
if step.Type() != model.StepTypeInvalid {
556+
// This will be errored out by the executor later, we need this here to avoid a null panic though
557+
return false
558+
}
540559
if step.Type() != model.StepTypeUsesActionRemote {
541560
return false
542561
}
@@ -606,7 +625,7 @@ func withDefaultBranch(b string, event map[string]interface{}) map[string]interf
606625
func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string {
607626
github := rc.getGithubContext()
608627
env["CI"] = "true"
609-
env["GITHUB_ENV"] = fmt.Sprintf("%s/%s", filepath.Dir(rc.Config.Workdir), "workflow/envs.txt")
628+
env["GITHUB_ENV"] = fmt.Sprintf("%s/%s", rc.Config.ContainerWorkdir(), "workflow/envs.txt")
610629
env["GITHUB_WORKFLOW"] = github.Workflow
611630
env["GITHUB_RUN_ID"] = github.RunID
612631
env["GITHUB_RUN_NUMBER"] = github.RunNumber

pkg/runner/run_context_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"regexp"
7+
"runtime"
78
"sort"
89
"strings"
910
"testing"
@@ -211,3 +212,68 @@ jobs:
211212
t.Fatal(err)
212213
}
213214
}
215+
216+
func TestRunContext_GetBindsAndMounts(t *testing.T) {
217+
rctemplate := &RunContext{
218+
Name: "TestRCName",
219+
Run: &model.Run{
220+
Workflow: &model.Workflow{
221+
Name: "TestWorkflowName",
222+
},
223+
},
224+
Config: &Config{
225+
BindWorkdir: false,
226+
},
227+
}
228+
229+
tests := []struct {
230+
windowsPath bool
231+
name string
232+
rc *RunContext
233+
wantbind string
234+
wantmount string
235+
}{
236+
{false, "/mnt/linux", rctemplate, "/mnt/linux", "/mnt/linux"},
237+
{false, "/mnt/path with spaces/linux", rctemplate, "/mnt/path with spaces/linux", "/mnt/path with spaces/linux"},
238+
{true, "C:\\Users\\TestPath\\MyTestPath", rctemplate, "/mnt/c/Users/TestPath/MyTestPath", "/mnt/c/Users/TestPath/MyTestPath"},
239+
{true, "C:\\Users\\Test Path with Spaces\\MyTestPath", rctemplate, "/mnt/c/Users/Test Path with Spaces/MyTestPath", "/mnt/c/Users/Test Path with Spaces/MyTestPath"},
240+
{true, "/LinuxPathOnWindowsShouldFail", rctemplate, "", ""},
241+
}
242+
243+
isWindows := runtime.GOOS == "windows"
244+
245+
for _, testcase := range tests {
246+
// pin for scopelint
247+
testcase := testcase
248+
for _, bindWorkDir := range []bool{true, false} {
249+
// pin for scopelint
250+
bindWorkDir := bindWorkDir
251+
testBindSuffix := ""
252+
if bindWorkDir {
253+
testBindSuffix = "Bind"
254+
}
255+
256+
// Only run windows path tests on windows and non-windows on non-windows
257+
if (isWindows && testcase.windowsPath) || (!isWindows && !testcase.windowsPath) {
258+
t.Run((testcase.name + testBindSuffix), func(t *testing.T) {
259+
config := testcase.rc.Config
260+
config.Workdir = testcase.name
261+
config.BindWorkdir = bindWorkDir
262+
gotbind, gotmount := rctemplate.GetBindsAndMounts()
263+
264+
// Name binds/mounts are either/or
265+
if config.BindWorkdir {
266+
fullBind := testcase.name + ":" + testcase.wantbind
267+
if runtime.GOOS == "darwin" {
268+
fullBind += ":delegated"
269+
}
270+
a.Contains(t, gotbind, fullBind)
271+
} else {
272+
mountkey := testcase.rc.jobContainerName()
273+
a.EqualValues(t, testcase.wantmount, gotmount[mountkey])
274+
}
275+
})
276+
}
277+
}
278+
}
279+
}

pkg/runner/runner.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import (
44
"context"
55
"fmt"
66
"io/ioutil"
7+
"path/filepath"
8+
"regexp"
9+
"runtime"
10+
"strings"
711

812
"github.com/nektos/act/pkg/common"
913
"github.com/nektos/act/pkg/model"
@@ -36,6 +40,46 @@ type Config struct {
3640
UseGitIgnore bool // controls if paths in .gitignore should not be copied into container, default true
3741
}
3842

43+
// Resolves the equivalent host path inside the container
44+
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
45+
// For use in docker volumes and binds
46+
func (config *Config) containerPath(path string) string {
47+
if runtime.GOOS == "windows" && strings.Contains(path, "/") {
48+
log.Error("You cannot specify linux style local paths (/mnt/etc) on Windows as it does not understand them.")
49+
return ""
50+
}
51+
52+
abspath, err := filepath.Abs(path)
53+
if err != nil {
54+
log.Error(err)
55+
return ""
56+
}
57+
58+
// Test if the path is a windows path
59+
windowsPathRegex := regexp.MustCompile(`^([a-zA-Z]):\\(.+)$`)
60+
windowsPathComponents := windowsPathRegex.FindStringSubmatch(abspath)
61+
62+
// Return as-is if no match
63+
if windowsPathComponents == nil {
64+
return abspath
65+
}
66+
67+
// Convert to WSL2-compatible path if it is a windows path
68+
// NOTE: Cannot use filepath because it will use the wrong path separators assuming we want the path to be windows
69+
// based if running on Windows, and because we are feeding this to Docker, GoLang auto-path-translate doesn't work.
70+
driveLetter := strings.ToLower(windowsPathComponents[1])
71+
translatedPath := strings.ReplaceAll(windowsPathComponents[2], `\`, `/`)
72+
// Should make something like /mnt/c/Users/person/My Folder/MyActProject
73+
result := strings.Join([]string{"/mnt", driveLetter, translatedPath}, `/`)
74+
return result
75+
}
76+
77+
// Resolves the equivalent host path inside the container
78+
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
79+
func (config *Config) ContainerWorkdir() string {
80+
return config.containerPath(config.Workdir)
81+
}
82+
3983
type runnerImpl struct {
4084
config *Config
4185
eventJSON string

pkg/runner/runner_test.go

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package runner
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"path/filepath"
8+
"runtime"
9+
"strings"
710
"testing"
811

912
"github.com/joho/godotenv"
@@ -40,19 +43,21 @@ type TestJobFileInfo struct {
4043
containerArchitecture string
4144
}
4245

43-
func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
46+
func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo, secrets map[string]string) {
4447
t.Run(tjfi.workflowPath, func(t *testing.T) {
4548
workdir, err := filepath.Abs(tjfi.workdir)
4649
assert.NilError(t, err, workdir)
4750
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
4851
runnerConfig := &Config{
4952
Workdir: workdir,
50-
BindWorkdir: true,
53+
BindWorkdir: false,
5154
EventName: tjfi.eventName,
5255
Platforms: tjfi.platforms,
5356
ReuseContainers: false,
5457
ContainerArchitecture: tjfi.containerArchitecture,
58+
Secrets: secrets,
5559
}
60+
5661
runner, err := New(runnerConfig)
5762
assert.NilError(t, err, tjfi.workflowPath)
5863

@@ -106,9 +111,11 @@ func TestRunEvent(t *testing.T) {
106111
log.SetLevel(log.DebugLevel)
107112

108113
ctx := context.Background()
114+
secretspath, _ := filepath.Abs("../../.secrets")
115+
secrets, _ := godotenv.Read(secretspath)
109116

110117
for _, table := range tables {
111-
runTestJobFile(ctx, t, table)
118+
runTestJobFile(ctx, t, table, secrets)
112119
}
113120
}
114121

@@ -189,3 +196,60 @@ func TestRunEventPullRequest(t *testing.T) {
189196
err = runner.NewPlanExecutor(plan)(ctx)
190197
assert.NilError(t, err, workflowPath)
191198
}
199+
200+
func TestContainerPath(t *testing.T) {
201+
type containerPathJob struct {
202+
destinationPath string
203+
sourcePath string
204+
workDir string
205+
}
206+
207+
if runtime.GOOS == "windows" {
208+
cwd, err := os.Getwd()
209+
if err != nil {
210+
log.Error(err)
211+
}
212+
213+
rootDrive := os.Getenv("SystemDrive")
214+
rootDriveLetter := strings.ReplaceAll(strings.ToLower(rootDrive), `:`, "")
215+
for _, v := range []containerPathJob{
216+
{"/mnt/c/Users/act/go/src/github.com/nektos/act", "C:\\Users\\act\\go\\src\\github.com\\nektos\\act\\", ""},
217+
{"/mnt/f/work/dir", `F:\work\dir`, ""},
218+
{"/mnt/c/windows/to/unix", "windows/to/unix", fmt.Sprintf("%s\\", rootDrive)},
219+
{fmt.Sprintf("/mnt/%v/act", rootDriveLetter), "act", fmt.Sprintf("%s\\", rootDrive)},
220+
} {
221+
if v.workDir != "" {
222+
if err := os.Chdir(v.workDir); err != nil {
223+
log.Error(err)
224+
t.Fail()
225+
}
226+
}
227+
228+
runnerConfig := &Config{
229+
Workdir: v.sourcePath,
230+
}
231+
232+
assert.Equal(t, v.destinationPath, runnerConfig.containerPath(runnerConfig.Workdir))
233+
}
234+
235+
if err := os.Chdir(cwd); err != nil {
236+
log.Error(err)
237+
}
238+
} else {
239+
cwd, err := os.Getwd()
240+
if err != nil {
241+
log.Error(err)
242+
}
243+
for _, v := range []containerPathJob{
244+
{"/home/act/go/src/github.com/nektos/act", "/home/act/go/src/github.com/nektos/act", ""},
245+
{"/home/act", `/home/act/`, ""},
246+
{cwd, ".", ""},
247+
} {
248+
runnerConfig := &Config{
249+
Workdir: v.sourcePath,
250+
}
251+
252+
assert.Equal(t, v.destinationPath, runnerConfig.containerPath(runnerConfig.Workdir))
253+
}
254+
}
255+
}

0 commit comments

Comments
 (0)