Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate Container Workdir from host Workdir #635

Merged
merged 4 commits into from
May 4, 2021
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ pkg/runner/act/
dist/local/act

coverage.txt

.env
#Store your GITHUB_TOKEN secret here for purposes of local testing of actions/checkout and others
.secrets
71 changes: 45 additions & 26 deletions pkg/runner/run_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,32 @@ func (rc *RunContext) jobContainerName() string {
return createContainerName("act", rc.String())
}

// Returns the binds and mounts for the container, resolving paths as appopriate
func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
name := rc.jobContainerName()

binds := []string{
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
}

mounts := map[string]string{
"act-toolcache": "/toolcache",
"act-actions": "/actions",
}

if rc.Config.BindWorkdir {
bindModifiers := ""
if runtime.GOOS == "darwin" {
bindModifiers = ":delegated"
}
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, rc.Config.ContainerWorkdir(), bindModifiers))
} else {
mounts[name] = rc.Config.ContainerWorkdir()
}

return binds, mounts
}

func (rc *RunContext) startJobContainer() common.Executor {
image := rc.platformImage()

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

envList := make([]string, 0)
bindModifiers := ""
if runtime.GOOS == "darwin" {
bindModifiers = ":delegated"
}

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

binds := []string{
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
}
if rc.Config.BindWorkdir {
binds = append(binds, fmt.Sprintf("%s:%s%s", rc.Config.Workdir, rc.Config.Workdir, bindModifiers))
}
binds, mounts := rc.GetBindsAndMounts()

rc.JobContainer = container.NewContainer(&container.NewContainerInput{
Cmd: nil,
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
WorkingDir: rc.Config.Workdir,
Image: image,
Name: name,
Env: envList,
Mounts: map[string]string{
name: filepath.Dir(rc.Config.Workdir),
"act-toolcache": "/toolcache",
"act-actions": "/actions",
},
Cmd: nil,
Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"},
WorkingDir: rc.Config.ContainerWorkdir(),
Image: image,
Name: name,
Env: envList,
Mounts: mounts,
NetworkMode: "host",
Binds: binds,
Stdout: logWriter,
Expand All @@ -121,7 +134,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
var copyToPath string
if !rc.Config.BindWorkdir {
copyToPath, copyWorkspace = rc.localCheckoutPath()
copyToPath = filepath.Join(rc.Config.Workdir, copyToPath)
copyToPath = filepath.Join(rc.Config.ContainerWorkdir(), copyToPath)
}

return common.NewPipelineExecutor(
Expand All @@ -130,7 +143,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
rc.JobContainer.Create(),
rc.JobContainer.Start(false),
rc.JobContainer.CopyDir(copyToPath, rc.Config.Workdir+string(filepath.Separator)+".", rc.Config.UseGitIgnore).IfBool(copyWorkspace),
rc.JobContainer.Copy(filepath.Dir(rc.Config.Workdir), &container.FileEntry{
rc.JobContainer.Copy(rc.Config.ContainerWorkdir(), &container.FileEntry{
Name: "workflow/event.json",
Mode: 0644,
Body: rc.EventJSON,
Expand Down Expand Up @@ -163,6 +176,8 @@ func (rc *RunContext) stopJobContainer() common.Executor {
}
}

// Prepare the mounts and binds for the worker

// ActionCacheDir is for rc
func (rc *RunContext) ActionCacheDir() string {
var xdgCache string
Expand Down Expand Up @@ -468,14 +483,14 @@ func (rc *RunContext) getGithubContext() *githubContext {
}
ghc := &githubContext{
Event: make(map[string]interface{}),
EventPath: fmt.Sprintf("%s/%s", filepath.Dir(rc.Config.Workdir), "workflow/event.json"),
EventPath: fmt.Sprintf("%s/%s", rc.Config.ContainerWorkdir(), "workflow/event.json"),
Workflow: rc.Run.Workflow.Name,
RunID: runID,
RunNumber: runNumber,
Actor: rc.Config.Actor,
EventName: rc.Config.EventName,
Token: token,
Workspace: rc.Config.Workdir,
Workspace: rc.Config.ContainerWorkdir(),
Action: rc.CurrentStep,
}

Expand Down Expand Up @@ -537,6 +552,10 @@ func (rc *RunContext) getGithubContext() *githubContext {
}

func (ghc *githubContext) isLocalCheckout(step *model.Step) bool {
if step.Type() != model.StepTypeInvalid {
// This will be errored out by the executor later, we need this here to avoid a null panic though
return false
}
if step.Type() != model.StepTypeUsesActionRemote {
return false
}
Expand Down Expand Up @@ -606,7 +625,7 @@ func withDefaultBranch(b string, event map[string]interface{}) map[string]interf
func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string {
github := rc.getGithubContext()
env["CI"] = "true"
env["GITHUB_ENV"] = fmt.Sprintf("%s/%s", filepath.Dir(rc.Config.Workdir), "workflow/envs.txt")
env["GITHUB_ENV"] = fmt.Sprintf("%s/%s", rc.Config.ContainerWorkdir(), "workflow/envs.txt")
env["GITHUB_WORKFLOW"] = github.Workflow
env["GITHUB_RUN_ID"] = github.RunID
env["GITHUB_RUN_NUMBER"] = github.RunNumber
Expand Down
66 changes: 66 additions & 0 deletions pkg/runner/run_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"regexp"
"runtime"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -211,3 +212,68 @@ jobs:
t.Fatal(err)
}
}

func TestRunContext_GetBindsAndMounts(t *testing.T) {
rctemplate := &RunContext{
Name: "TestRCName",
Run: &model.Run{
Workflow: &model.Workflow{
Name: "TestWorkflowName",
},
},
Config: &Config{
BindWorkdir: false,
},
}

tests := []struct {
windowsPath bool
name string
rc *RunContext
wantbind string
wantmount string
}{
{false, "/mnt/linux", rctemplate, "/mnt/linux", "/mnt/linux"},
{false, "/mnt/path with spaces/linux", rctemplate, "/mnt/path with spaces/linux", "/mnt/path with spaces/linux"},
{true, "C:\\Users\\TestPath\\MyTestPath", rctemplate, "/mnt/c/Users/TestPath/MyTestPath", "/mnt/c/Users/TestPath/MyTestPath"},
{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"},
{true, "/LinuxPathOnWindowsShouldFail", rctemplate, "", ""},
}

isWindows := runtime.GOOS == "windows"

for _, testcase := range tests {
// pin for scopelint
testcase := testcase
for _, bindWorkDir := range []bool{true, false} {
// pin for scopelint
bindWorkDir := bindWorkDir
testBindSuffix := ""
if bindWorkDir {
testBindSuffix = "Bind"
}

// Only run windows path tests on windows and non-windows on non-windows
if (isWindows && testcase.windowsPath) || (!isWindows && !testcase.windowsPath) {
t.Run((testcase.name + testBindSuffix), func(t *testing.T) {
config := testcase.rc.Config
config.Workdir = testcase.name
config.BindWorkdir = bindWorkDir
gotbind, gotmount := rctemplate.GetBindsAndMounts()

// Name binds/mounts are either/or
if config.BindWorkdir {
fullBind := testcase.name + ":" + testcase.wantbind
if runtime.GOOS == "darwin" {
fullBind += ":delegated"
}
a.Contains(t, gotbind, fullBind)
} else {
mountkey := testcase.rc.jobContainerName()
a.EqualValues(t, testcase.wantmount, gotmount[mountkey])
}
})
}
}
}
}
44 changes: 44 additions & 0 deletions pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"context"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"runtime"
"strings"

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

// Resolves the equivalent host path inside the container
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
// For use in docker volumes and binds
func (config *Config) containerPath(path string) string {
if runtime.GOOS == "windows" && strings.Contains(path, "/") {
log.Error("You cannot specify linux style local paths (/mnt/etc) on Windows as it does not understand them.")
return ""
}

abspath, err := filepath.Abs(path)
if err != nil {
log.Error(err)
return ""
}

// Test if the path is a windows path
windowsPathRegex := regexp.MustCompile(`^([a-zA-Z]):\\(.+)$`)
windowsPathComponents := windowsPathRegex.FindStringSubmatch(abspath)

// Return as-is if no match
if windowsPathComponents == nil {
return abspath
}

// Convert to WSL2-compatible path if it is a windows path
// NOTE: Cannot use filepath because it will use the wrong path separators assuming we want the path to be windows
// based if running on Windows, and because we are feeding this to Docker, GoLang auto-path-translate doesn't work.
driveLetter := strings.ToLower(windowsPathComponents[1])
translatedPath := strings.ReplaceAll(windowsPathComponents[2], `\`, `/`)
// Should make something like /mnt/c/Users/person/My Folder/MyActProject
result := strings.Join([]string{"/mnt", driveLetter, translatedPath}, `/`)
return result
}

// Resolves the equivalent host path inside the container
// This is required for windows and WSL 2 to translate things like C:\Users\Myproject to /mnt/users/Myproject
func (config *Config) ContainerWorkdir() string {
return config.containerPath(config.Workdir)
}

JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
type runnerImpl struct {
config *Config
eventJSON string
Expand Down
70 changes: 67 additions & 3 deletions pkg/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package runner
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/joho/godotenv"
Expand Down Expand Up @@ -40,19 +43,21 @@ type TestJobFileInfo struct {
containerArchitecture string
}

func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo) {
func runTestJobFile(ctx context.Context, t *testing.T, tjfi TestJobFileInfo, secrets map[string]string) {
t.Run(tjfi.workflowPath, func(t *testing.T) {
workdir, err := filepath.Abs(tjfi.workdir)
assert.NilError(t, err, workdir)
fullWorkflowPath := filepath.Join(workdir, tjfi.workflowPath)
runnerConfig := &Config{
Workdir: workdir,
BindWorkdir: true,
BindWorkdir: false,
EventName: tjfi.eventName,
Platforms: tjfi.platforms,
ReuseContainers: false,
ContainerArchitecture: tjfi.containerArchitecture,
Secrets: secrets,
}

runner, err := New(runnerConfig)
assert.NilError(t, err, tjfi.workflowPath)

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

ctx := context.Background()
secretspath, _ := filepath.Abs("../../.secrets")
secrets, _ := godotenv.Read(secretspath)

for _, table := range tables {
runTestJobFile(ctx, t, table)
runTestJobFile(ctx, t, table, secrets)
JustinGrote marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -189,3 +196,60 @@ func TestRunEventPullRequest(t *testing.T) {
err = runner.NewPlanExecutor(plan)(ctx)
assert.NilError(t, err, workflowPath)
}

func TestContainerPath(t *testing.T) {
type containerPathJob struct {
destinationPath string
sourcePath string
workDir string
}

if runtime.GOOS == "windows" {
cwd, err := os.Getwd()
if err != nil {
log.Error(err)
}

rootDrive := os.Getenv("SystemDrive")
rootDriveLetter := strings.ReplaceAll(strings.ToLower(rootDrive), `:`, "")
for _, v := range []containerPathJob{
{"/mnt/c/Users/act/go/src/github.com/nektos/act", "C:\\Users\\act\\go\\src\\github.com\\nektos\\act\\", ""},
{"/mnt/f/work/dir", `F:\work\dir`, ""},
{"/mnt/c/windows/to/unix", "windows/to/unix", fmt.Sprintf("%s\\", rootDrive)},
{fmt.Sprintf("/mnt/%v/act", rootDriveLetter), "act", fmt.Sprintf("%s\\", rootDrive)},
} {
if v.workDir != "" {
if err := os.Chdir(v.workDir); err != nil {
log.Error(err)
t.Fail()
}
}

runnerConfig := &Config{
Workdir: v.sourcePath,
}

assert.Equal(t, v.destinationPath, runnerConfig.containerPath(runnerConfig.Workdir))
}

if err := os.Chdir(cwd); err != nil {
log.Error(err)
}
} else {
cwd, err := os.Getwd()
if err != nil {
log.Error(err)
}
for _, v := range []containerPathJob{
{"/home/act/go/src/github.com/nektos/act", "/home/act/go/src/github.com/nektos/act", ""},
{"/home/act", `/home/act/`, ""},
{cwd, ".", ""},
} {
runnerConfig := &Config{
Workdir: v.sourcePath,
}

assert.Equal(t, v.destinationPath, runnerConfig.containerPath(runnerConfig.Workdir))
}
}
}
Loading