Skip to content

Commit

Permalink
Separate Container Workdir from host Workdir (#635)
Browse files Browse the repository at this point in the history
* Separate Container Workdir from Host Workdir

* Add delegated component to MacOS Test

* Lint: Remove leading newline

* Fix trailing path issue
  • Loading branch information
JustinGrote authored May 4, 2021
1 parent 020d6a6 commit 0f04942
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 66 deletions.
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)
}

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)
}
}

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

1 comment on commit 0f04942

@catthehacker
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it breaks copying envs.txt file

[main.yml/lint]   ❗  ::error::Missing file at path: /home/pj/.local/go/src/github.com/moshen/gotermimg/workflow/envs.txt
[main.yml/lint]   ❌  Failure - actions/setup-go@v1
DEBU[0022] exit with `FAILURE`: 1                       
DEBU[0022] exit with `FAILURE`: 1                       
Error: exit with `FAILURE`: 1

Please sign in to comment.