From e7e647d3dff7134628c15c0c256a0b7bd362d78f Mon Sep 17 00:00:00 2001 From: Marc Garcia Sastre Date: Sat, 24 Apr 2021 07:38:42 +0200 Subject: [PATCH 1/7] Draft: Adds services capabilities --- pkg/container/docker_network.go | 36 +++++++ pkg/container/docker_run.go | 28 +++++- pkg/runner/run_context.go | 133 +++++++++++++++++++------ pkg/runner/runner_test.go | 36 +++++++ pkg/runner/testdata/services/push.yaml | 29 ++++++ 5 files changed, 229 insertions(+), 33 deletions(-) create mode 100644 pkg/container/docker_network.go create mode 100644 pkg/runner/testdata/services/push.yaml diff --git a/pkg/container/docker_network.go b/pkg/container/docker_network.go new file mode 100644 index 00000000000..d7be3dacc46 --- /dev/null +++ b/pkg/container/docker_network.go @@ -0,0 +1,36 @@ +package container + +import ( + "context" + + "github.com/docker/docker/api/types" + "github.com/nektos/act/pkg/common" +) + +func NewDockerNetworkCreateExecutor(name string) common.Executor { + return func(ctx context.Context) error { + cli, err := GetDockerClient(ctx) + if err != nil { + return err + } + + _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{}) + if err != nil { + return err + } + + return nil + } +} + +func NewDockerNetworkRemoveExecutor(name string) common.Executor { + return func(ctx context.Context) error { + cli, err := GetDockerClient(ctx) + if err != nil { + return err + } + + cli.NetworkRemove(ctx, name) + return nil + } +} diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go index bf6d6f378a4..eb2cf85ccaf 100644 --- a/pkg/container/docker_run.go +++ b/pkg/container/docker_run.go @@ -66,6 +66,7 @@ type FileEntry struct { // Container for managing docker run containers type Container interface { Create(capAdd []string, capDrop []string) common.Executor + ConnectToNetwork(name string) common.Executor Copy(destPath string, files ...*FileEntry) common.Executor CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) @@ -105,6 +106,17 @@ func supportsContainerImagePlatform(cli *client.Client) bool { return constraint.Check(sv) } +func (cr *containerReference) ConnectToNetwork(name string) common.Executor { + return common. + NewDebugExecutor("%sdocker network connect %s %s", logPrefix, name, cr.input.Name). + Then( + common.NewPipelineExecutor( + cr.connect(), + cr.connectToNetwork(name), + ).IfNot(common.Dryrun), + ) +} + func (cr *containerReference) Create(capAdd []string, capDrop []string) common.Executor { return common. NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd). @@ -233,6 +245,12 @@ func GetDockerClient(ctx context.Context) (*client.Client, error) { return cli, err } +func (cr *containerReference) connectToNetwork(name string) common.Executor { + return func(ctx context.Context) error { + return cr.cli.NetworkConnect(ctx, name, cr.input.Name, nil) + } +} + func (cr *containerReference) connect() common.Executor { return func(ctx context.Context) error { if cr.cli != nil { @@ -315,13 +333,17 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E config := &container.Config{ Image: input.Image, - Cmd: input.Cmd, - Entrypoint: input.Entrypoint, WorkingDir: input.WorkingDir, Env: input.Env, Tty: isTerminal, Hostname: input.Hostname, } + if len(input.Cmd) > 0 { + config.Cmd = input.Cmd + } + if len(input.Entrypoint) > 0 { + config.Entrypoint = input.Entrypoint + } mounts := make([]mount.Mount, 0) for mountSource, mountTarget := range input.Mounts { @@ -345,6 +367,8 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E OS: desiredPlatform[0], } } + fmt.Printf("%#v", len(input.Cmd)) + fmt.Printf("%#v", config) resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{ CapAdd: capAdd, CapDrop: capDrop, diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index cfe37ac3187..c73d01cd39e 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -27,19 +27,20 @@ const ActPath string = "/var/run/act" // RunContext contains info about current job type RunContext struct { - Name string - Config *Config - Matrix map[string]interface{} - Run *model.Run - EventJSON string - Env map[string]string - ExtraPath []string - CurrentStep string - StepResults map[string]*stepResult - ExprEval ExpressionEvaluator - JobContainer container.Container - OutputMappings map[MappableOutput]MappableOutput - JobName string + Name string + Config *Config + Matrix map[string]interface{} + Run *model.Run + EventJSON string + Env map[string]string + ExtraPath []string + CurrentStep string + StepResults map[string]*stepResult + ExprEval ExpressionEvaluator + JobContainer container.Container + OutputMappings map[MappableOutput]MappableOutput + JobName string + ServiceContainers []container.Container } type MappableOutput struct { @@ -167,25 +168,53 @@ func (rc *RunContext) startJobContainer() common.Executor { envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp")) binds, mounts := rc.GetBindsAndMounts() + // add service containers + for name, spec := range rc.Run.Job().Services { + mergedEnv := envList + for k, v := range spec.Env { + mergedEnv = append(mergedEnv, fmt.Sprintf("%s=%s", k, v)) + } + + mnt := mounts + mnt[name] = filepath.Dir(rc.Config.ContainerWorkdir()) + + c := container.NewContainer(&container.NewContainerInput{ + Name: name, + WorkingDir: rc.Config.ContainerWorkdir(), + Image: spec.Image, + Username: rc.Config.Secrets["DOCKER_USERNAME"], + Password: rc.Config.Secrets["DOCKER_PASSWORD"], + Env: mergedEnv, + Mounts: mnt, + // NetworkMode: "host", + Binds: binds, + Stdout: logWriter, + Stderr: logWriter, + Privileged: rc.Config.Privileged, + UsernsMode: rc.Config.UsernsMode, + Platform: rc.Config.ContainerArchitecture, + }) + rc.ServiceContainers = append(rc.ServiceContainers, c) + } rc.JobContainer = container.NewContainer(&container.NewContainerInput{ - Cmd: nil, - Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"}, - WorkingDir: rc.Config.ContainerWorkdir(), - Image: image, - Username: username, - Password: password, - Name: name, - Env: envList, - Mounts: mounts, - NetworkMode: "host", - Binds: binds, - Stdout: logWriter, - Stderr: logWriter, - Privileged: rc.Config.Privileged, - UsernsMode: rc.Config.UsernsMode, - Platform: rc.Config.ContainerArchitecture, - Hostname: hostname, + Cmd: nil, + Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"}, + WorkingDir: rc.Config.ContainerWorkdir(), + Image: image, + Username: username, + Password: password, + Name: name, + Env: envList, + Mounts: mounts, + // NetworkMode: "host", + Binds: binds, + Stdout: logWriter, + Stderr: logWriter, + Privileged: rc.Config.Privileged, + UsernsMode: rc.Config.UsernsMode, + Platform: rc.Config.ContainerArchitecture, + Hostname: hostname, }) var copyWorkspace bool @@ -197,10 +226,15 @@ func (rc *RunContext) startJobContainer() common.Executor { return common.NewPipelineExecutor( rc.JobContainer.Pull(rc.Config.ForcePull), + rc.stopServiceContainers(), rc.stopJobContainer(), + rc.removeNetwork(), + rc.createNetwork(), + rc.startServiceContainers(), rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), rc.JobContainer.Start(false), rc.JobContainer.UpdateFromImageEnv(&rc.Env), + rc.JobContainer.ConnectToNetwork(defaultNetwork), rc.JobContainer.UpdateFromEnv("/etc/environment", &rc.Env), rc.JobContainer.Exec([]string{"mkdir", "-m", "0777", "-p", ActPath}, rc.Env, "root", ""), rc.JobContainer.CopyDir(copyToPath, rc.Config.Workdir+string(filepath.Separator)+".", rc.Config.UseGitIgnore).IfBool(copyWorkspace), @@ -221,6 +255,20 @@ func (rc *RunContext) startJobContainer() common.Executor { } } +const defaultNetwork = "act_github_actions_network" + +func (rc *RunContext) createNetwork() common.Executor { + return func(ctx context.Context) error { + return container.NewDockerNetworkCreateExecutor(defaultNetwork)(ctx) + } +} + +func (rc *RunContext) removeNetwork() common.Executor { + return func(ctx context.Context) error { + return container.NewDockerNetworkRemoveExecutor(defaultNetwork)(ctx) + } +} + func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user, workdir string) common.Executor { return func(ctx context.Context) error { return rc.JobContainer.Exec(cmd, env, user, workdir)(ctx) @@ -238,7 +286,30 @@ func (rc *RunContext) stopJobContainer() common.Executor { } } -// Prepare the mounts and binds for the worker +func (rc *RunContext) startServiceContainers() common.Executor { + return func(ctx context.Context) error { + execs := []common.Executor{} + for _, c := range rc.ServiceContainers { + execs = append(execs, common.NewPipelineExecutor( + c.Pull(false), + c.Create([]string{}, []string{}), + c.Start(false), + c.ConnectToNetwork(defaultNetwork), + )) + } + return common.NewParallelExecutor(execs...)(ctx) + } +} + +func (rc *RunContext) stopServiceContainers() common.Executor { + return func(ctx context.Context) error { + execs := []common.Executor{} + for _, c := range rc.ServiceContainers { + execs = append(execs, c.Remove()) + } + return common.NewParallelExecutor(execs...)(ctx) + } +} // ActionCacheDir is for rc func (rc *RunContext) ActionCacheDir() string { diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 7ca000c32d7..066a5ccb61a 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -181,6 +181,42 @@ func TestRunEventSecrets(t *testing.T) { assert.Nil(t, err, workflowPath) } +func TestRunWithService(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + log.SetLevel(log.DebugLevel) + ctx := context.Background() + + platforms := map[string]string{ + "ubuntu-latest": "node:12.20.1-buster-slim", + } + + workflowPath := "services" + eventName := "push" + + workdir, err := filepath.Abs("testdata") + assert.Nil(t, err, workflowPath) + + runnerConfig := &Config{ + Workdir: workdir, + EventName: eventName, + Platforms: platforms, + ReuseContainers: false, + } + runner, err := New(runnerConfig) + assert.Nil(t, err, workflowPath) + + planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath), false) + assert.Nil(t, err, workflowPath) + + plan := planner.PlanEvent(eventName) + + err = runner.NewPlanExecutor(plan)(ctx) + assert.Nil(t, err, workflowPath) +} + func TestRunEventPullRequest(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/runner/testdata/services/push.yaml b/pkg/runner/testdata/services/push.yaml new file mode 100644 index 00000000000..d3518df8728 --- /dev/null +++ b/pkg/runner/testdata/services/push.yaml @@ -0,0 +1,29 @@ +name: services + +on: + push: + +jobs: + services: + name: Reproduction of failing Services interpolation + runs-on: ubuntu-latest + services: + postgres: + image: postgres:12 + env: + POSTGRES_USER: runner + POSTGRES_PASSWORD: mysecretdbpass + POSTGRES_DB: mydb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - name: Echo the Postgres service ID / Network / Ports + run: | + echo "id: ${{ job.services.postgres.id }}" + echo "network: ${{ job.services.postgres.network }}" + echo "ports: ${{ job.services.postgres.ports }}" \ No newline at end of file From cf863d5bef522284ae9b6bcbc8d0d792a107b74a Mon Sep 17 00:00:00 2001 From: Marc Garcia Sastre Date: Tue, 4 May 2021 10:22:58 +0200 Subject: [PATCH 2/7] Applies review changes --- pkg/container/docker_network.go | 4 +++- pkg/container/docker_run.go | 2 -- pkg/runner/run_context.go | 23 +++++++++++------------ pkg/runner/testdata/services/push.yaml | 7 ++----- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/pkg/container/docker_network.go b/pkg/container/docker_network.go index d7be3dacc46..af95d0eed33 100644 --- a/pkg/container/docker_network.go +++ b/pkg/container/docker_network.go @@ -2,6 +2,7 @@ package container import ( "context" + "fmt" "github.com/docker/docker/api/types" "github.com/nektos/act/pkg/common" @@ -14,10 +15,11 @@ func NewDockerNetworkCreateExecutor(name string) common.Executor { return err } - _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{}) + network, err := cli.NetworkCreate(ctx, name, types.NetworkCreate{}) if err != nil { return err } + fmt.Printf("%#v", network.ID) return nil } diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go index eb2cf85ccaf..06665057224 100644 --- a/pkg/container/docker_run.go +++ b/pkg/container/docker_run.go @@ -367,8 +367,6 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E OS: desiredPlatform[0], } } - fmt.Printf("%#v", len(input.Cmd)) - fmt.Printf("%#v", config) resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{ CapAdd: capAdd, CapDrop: capDrop, diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index c73d01cd39e..162a8e7a354 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -224,17 +224,18 @@ func (rc *RunContext) startJobContainer() common.Executor { copyToPath = filepath.Join(rc.Config.ContainerWorkdir(), copyToPath) } + networkName := fmt.Sprintf("act-%s-network", rc.Run.JobID) return common.NewPipelineExecutor( rc.JobContainer.Pull(rc.Config.ForcePull), rc.stopServiceContainers(), rc.stopJobContainer(), - rc.removeNetwork(), - rc.createNetwork(), - rc.startServiceContainers(), + rc.removeNetwork(networkName), + rc.createNetwork(networkName), + rc.startServiceContainers(networkName), rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), rc.JobContainer.Start(false), rc.JobContainer.UpdateFromImageEnv(&rc.Env), - rc.JobContainer.ConnectToNetwork(defaultNetwork), + rc.JobContainer.ConnectToNetwork(networkName), rc.JobContainer.UpdateFromEnv("/etc/environment", &rc.Env), rc.JobContainer.Exec([]string{"mkdir", "-m", "0777", "-p", ActPath}, rc.Env, "root", ""), rc.JobContainer.CopyDir(copyToPath, rc.Config.Workdir+string(filepath.Separator)+".", rc.Config.UseGitIgnore).IfBool(copyWorkspace), @@ -255,17 +256,15 @@ func (rc *RunContext) startJobContainer() common.Executor { } } -const defaultNetwork = "act_github_actions_network" - -func (rc *RunContext) createNetwork() common.Executor { +func (rc *RunContext) createNetwork(name string) common.Executor { return func(ctx context.Context) error { - return container.NewDockerNetworkCreateExecutor(defaultNetwork)(ctx) + return container.NewDockerNetworkCreateExecutor(name)(ctx) } } -func (rc *RunContext) removeNetwork() common.Executor { +func (rc *RunContext) removeNetwork(name string) common.Executor { return func(ctx context.Context) error { - return container.NewDockerNetworkRemoveExecutor(defaultNetwork)(ctx) + return container.NewDockerNetworkRemoveExecutor(name)(ctx) } } @@ -286,7 +285,7 @@ func (rc *RunContext) stopJobContainer() common.Executor { } } -func (rc *RunContext) startServiceContainers() common.Executor { +func (rc *RunContext) startServiceContainers(networkName string) common.Executor { return func(ctx context.Context) error { execs := []common.Executor{} for _, c := range rc.ServiceContainers { @@ -294,7 +293,7 @@ func (rc *RunContext) startServiceContainers() common.Executor { c.Pull(false), c.Create([]string{}, []string{}), c.Start(false), - c.ConnectToNetwork(defaultNetwork), + c.ConnectToNetwork(networkName), )) } return common.NewParallelExecutor(execs...)(ctx) diff --git a/pkg/runner/testdata/services/push.yaml b/pkg/runner/testdata/services/push.yaml index d3518df8728..f6ca7bc4c50 100644 --- a/pkg/runner/testdata/services/push.yaml +++ b/pkg/runner/testdata/services/push.yaml @@ -1,8 +1,5 @@ name: services - -on: - push: - +on: push jobs: services: name: Reproduction of failing Services interpolation @@ -26,4 +23,4 @@ jobs: run: | echo "id: ${{ job.services.postgres.id }}" echo "network: ${{ job.services.postgres.network }}" - echo "ports: ${{ job.services.postgres.ports }}" \ No newline at end of file + echo "ports: ${{ job.services.postgres.ports }}" From 6ec8c24e4c331b41d0ca8e2f42126ff40eab35ec Mon Sep 17 00:00:00 2001 From: Marc Garcia Sastre Date: Tue, 4 May 2021 10:59:08 +0200 Subject: [PATCH 3/7] Removes unnecessary log line --- pkg/container/docker_network.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/container/docker_network.go b/pkg/container/docker_network.go index af95d0eed33..d7be3dacc46 100644 --- a/pkg/container/docker_network.go +++ b/pkg/container/docker_network.go @@ -2,7 +2,6 @@ package container import ( "context" - "fmt" "github.com/docker/docker/api/types" "github.com/nektos/act/pkg/common" @@ -15,11 +14,10 @@ func NewDockerNetworkCreateExecutor(name string) common.Executor { return err } - network, err := cli.NetworkCreate(ctx, name, types.NetworkCreate{}) + _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{}) if err != nil { return err } - fmt.Printf("%#v", network.ID) return nil } From faf60a9e5668a17a72a91fdb3c02ab7482bd1b1d Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 4 May 2021 11:10:52 +0200 Subject: [PATCH 4/7] Removes old code induced because of the rebase --- pkg/runner/run_context.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 162a8e7a354..45b103a12c1 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -168,6 +168,7 @@ func (rc *RunContext) startJobContainer() common.Executor { envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp")) binds, mounts := rc.GetBindsAndMounts() + // add service containers for name, spec := range rc.Run.Job().Services { mergedEnv := envList From ada1375def3ad6e1411ddb933ef6c230d5737110 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 4 May 2021 11:24:25 +0200 Subject: [PATCH 5/7] Update pkg/runner/runner_test.go Co-authored-by: Ryan (hackercat) --- pkg/runner/runner_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 066a5ccb61a..8cbe40c3fdc 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -208,7 +208,7 @@ func TestRunWithService(t *testing.T) { runner, err := New(runnerConfig) assert.Nil(t, err, workflowPath) - planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath), false) + planner, err := model.NewWorkflowPlanner(fmt.Sprintf("testdata/%s", workflowPath), true) assert.Nil(t, err, workflowPath) plan := planner.PlanEvent(eventName) From 079c6130210e241baaf8e6b92e90bb975eab440c Mon Sep 17 00:00:00 2001 From: hackercat Date: Tue, 4 May 2021 16:55:45 +0000 Subject: [PATCH 6/7] fix: rework services --- pkg/container/docker_network.go | 43 ++++++- pkg/container/docker_run.go | 101 +++++++++++----- pkg/runner/run_context.go | 158 +++++++++++++++++-------- pkg/runner/runner_test.go | 2 +- pkg/runner/testdata/services/push.yaml | 21 +++- 5 files changed, 234 insertions(+), 91 deletions(-) diff --git a/pkg/container/docker_network.go b/pkg/container/docker_network.go index d7be3dacc46..bad329ba100 100644 --- a/pkg/container/docker_network.go +++ b/pkg/container/docker_network.go @@ -7,15 +7,22 @@ import ( "github.com/nektos/act/pkg/common" ) -func NewDockerNetworkCreateExecutor(name string) common.Executor { +func NewDockerNetworkCreateExecutor(name string, config types.NetworkCreate) common.Executor { return func(ctx context.Context) error { + if common.Dryrun(ctx) { + return nil + } + cli, err := GetDockerClient(ctx) if err != nil { return err } - _, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{}) - if err != nil { + if exists := DockerNetworkExists(ctx, name); exists { + return nil + } + + if _, err = cli.NetworkCreate(ctx, name, config); err != nil { return err } @@ -25,12 +32,40 @@ func NewDockerNetworkCreateExecutor(name string) common.Executor { func NewDockerNetworkRemoveExecutor(name string) common.Executor { return func(ctx context.Context) error { + if common.Dryrun(ctx) { + return nil + } + cli, err := GetDockerClient(ctx) if err != nil { return err } - cli.NetworkRemove(ctx, name) + if err = cli.NetworkRemove(ctx, name); err != nil { + return err + } + return nil } } + +func DockerNetworkExists(ctx context.Context, name string) bool { + if _, exists, _ := GetDockerNetwork(ctx, name); !exists { + return false + } + return true +} + +func GetDockerNetwork(ctx context.Context, name string) (types.NetworkResource, bool, error) { + cli, err := GetDockerClient(ctx) + if err != nil { + return types.NetworkResource{}, false, err + } + + res, err := cli.NetworkInspect(ctx, name, types.NetworkInspectOptions{}) + if err != nil { + return types.NetworkResource{}, false, err + } + + return res, true, nil +} diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go index 06665057224..044fdab32d1 100644 --- a/pkg/container/docker_run.go +++ b/pkg/container/docker_run.go @@ -65,6 +65,7 @@ type FileEntry struct { // Container for managing docker run containers type Container interface { + ID() string Create(capAdd []string, capDrop []string) common.Executor ConnectToNetwork(name string) common.Executor Copy(destPath string, files ...*FileEntry) common.Executor @@ -78,6 +79,7 @@ type Container interface { UpdateFromPath(env *map[string]string) common.Executor Remove() common.Executor Close() common.Executor + SetContainerNetworkMode(mode string) common.Executor } // NewContainer creates a reference to a container @@ -106,15 +108,8 @@ func supportsContainerImagePlatform(cli *client.Client) bool { return constraint.Check(sv) } -func (cr *containerReference) ConnectToNetwork(name string) common.Executor { - return common. - NewDebugExecutor("%sdocker network connect %s %s", logPrefix, name, cr.input.Name). - Then( - common.NewPipelineExecutor( - cr.connect(), - cr.connectToNetwork(name), - ).IfNot(common.Dryrun), - ) +func (cr *containerReference) ID() string { + return cr.id } func (cr *containerReference) Create(capAdd []string, capDrop []string) common.Executor { @@ -158,19 +153,26 @@ func (cr *containerReference) Pull(forcePull bool) common.Executor { } func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.Executor { - return common.NewPipelineExecutor( - cr.connect(), - cr.find(), - cr.copyContent(destPath, files...), - ).IfNot(common.Dryrun) + return common. + NewInfoExecutor("%sdocker cp destination=%s", logPrefix, destPath). + Then( + common.NewPipelineExecutor( + cr.connect(), + cr.find(), + cr.copyContent(destPath, files...), + ).IfNot(common.Dryrun), + ) } func (cr *containerReference) CopyDir(destPath string, srcPath string, useGitIgnore bool) common.Executor { - return common.NewPipelineExecutor( - common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath), - cr.Exec([]string{"mkdir", "-p", destPath}, nil, "", ""), - cr.copyDir(destPath, srcPath, useGitIgnore), - ).IfNot(common.Dryrun) + return common. + NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath). + Then( + common.NewPipelineExecutor( + cr.Exec([]string{"mkdir", "-p", destPath}, nil, "", ""), + cr.copyDir(destPath, srcPath, useGitIgnore), + ).IfNot(common.Dryrun), + ) } func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath string) (io.ReadCloser, error) { @@ -190,22 +192,54 @@ func (cr *containerReference) UpdateFromPath(env *map[string]string) common.Exec return cr.extractPath(env).IfNot(common.Dryrun) } +func (cr *containerReference) SetContainerNetworkMode(mode string) common.Executor { + return common. + NewDebugExecutor("Changed network mode for container '%s' from '%s' to '%s'", cr.input.Name, cr.input.NetworkMode, mode). + Then( + common.NewPipelineExecutor( + func(ctx context.Context) error { + cr.input.NetworkMode = mode + return nil + }, + ).IfNot(common.Dryrun), + ) +} + +func (cr *containerReference) ConnectToNetwork(name string) common.Executor { + return common. + NewInfoExecutor("%sdocker network connect net=%s container=%s", logPrefix, name, cr.input.Name). + Then( + common.NewPipelineExecutor( + cr.connect(), + cr.find(), + cr.connectToNetwork(name), + ).IfNot(common.Dryrun), + ) +} + func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor { - return common.NewPipelineExecutor( - common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir), - cr.connect(), - cr.find(), - cr.exec(command, env, user, workdir), - ).IfNot(common.Dryrun) + return common. + NewInfoExecutor("%sdocker exec cmd=%v user=%s workdir=%s", logPrefix, command, user, workdir). + Then( + common.NewPipelineExecutor( + cr.connect(), + cr.find(), + cr.exec(command, env, user, workdir), + ).IfNot(common.Dryrun), + ) } func (cr *containerReference) Remove() common.Executor { - return common.NewPipelineExecutor( - cr.connect(), - cr.find(), - ).Finally( - cr.remove(), - ).IfNot(common.Dryrun) + return common. + NewInfoExecutor("%sdocker rm %s", logPrefix, cr.id). + Then( + common.NewPipelineExecutor( + cr.connect(), + cr.find(), + ).Finally( + cr.remove(), + ).IfNot(common.Dryrun), + ).IfBool(cr.id != "") } type containerReference struct { @@ -247,7 +281,7 @@ func GetDockerClient(ctx context.Context) (*client.Client, error) { func (cr *containerReference) connectToNetwork(name string) common.Executor { return func(ctx context.Context) error { - return cr.cli.NetworkConnect(ctx, name, cr.input.Name, nil) + return cr.cli.NetworkConnect(ctx, name, cr.id, nil) } } @@ -338,9 +372,11 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E Tty: isTerminal, Hostname: input.Hostname, } + if len(input.Cmd) > 0 { config.Cmd = input.Cmd } + if len(input.Entrypoint) > 0 { config.Entrypoint = input.Entrypoint } @@ -379,6 +415,7 @@ func (cr *containerReference) create(capAdd []string, capDrop []string) common.E if err != nil { return errors.WithStack(err) } + logger.Debugf("Created container name=%s id=%v from image %v (platform: %s)", input.Name, resp.ID, input.Image, input.Platform) logger.Debugf("ENV ==> %v", input.Env) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 45b103a12c1..74cb1555cec 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -10,6 +10,7 @@ import ( "runtime" "strings" + "github.com/docker/docker/api/types" "github.com/google/shlex" "github.com/spf13/pflag" @@ -40,7 +41,7 @@ type RunContext struct { JobContainer container.Container OutputMappings map[MappableOutput]MappableOutput JobName string - ServiceContainers []container.Container + ServiceContainers map[string]container.Container } type MappableOutput struct { @@ -138,7 +139,13 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) { return binds, mounts } +// JobNetworkName returns formatted network name used in main container and its services +func (rc *RunContext) JobNetworkName() string { + return createNetworkName("act", rc.Run.Workflow.Name, rc.Run.JobID, rc.Name) +} + func (rc *RunContext) startJobContainer() common.Executor { + job := rc.Run.Job() image := rc.platformImage() hostname := rc.hostname() @@ -160,6 +167,9 @@ func (rc *RunContext) startJobContainer() common.Executor { common.Logger(ctx).Infof("\U0001f680 Start image=%s", image) name := rc.jobContainerName() + networkName := rc.JobNetworkName() + workdir := rc.Config.ContainerWorkdir() + binds, mounts := rc.GetBindsAndMounts() envList := make([]string, 0) @@ -167,48 +177,47 @@ func (rc *RunContext) startJobContainer() common.Executor { envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_OS", "Linux")) envList = append(envList, fmt.Sprintf("%s=%s", "RUNNER_TEMP", "/tmp")) - binds, mounts := rc.GetBindsAndMounts() + if job.Services != nil { + rc.ServiceContainers = make(map[string]container.Container, len(job.Services)) + for name, spec := range job.Services { + mergedEnv := envList + for k, v := range spec.Env { + mergedEnv = append(mergedEnv, fmt.Sprintf("%s=%s", k, v)) + } - // add service containers - for name, spec := range rc.Run.Job().Services { - mergedEnv := envList - for k, v := range spec.Env { - mergedEnv = append(mergedEnv, fmt.Sprintf("%s=%s", k, v)) + mnt := mounts + mnt[name] = filepath.Dir(workdir) + + c := container.NewContainer(&container.NewContainerInput{ + Name: name, + Hostname: name, + WorkingDir: workdir, + Image: spec.Image, + Username: username, + Password: password, + Env: mergedEnv, + Mounts: mnt, + Binds: binds, + Stdout: logWriter, + Stderr: logWriter, + Privileged: rc.Config.Privileged, + UsernsMode: rc.Config.UsernsMode, + Platform: rc.Config.ContainerArchitecture, + }) + rc.ServiceContainers[name] = c } - - mnt := mounts - mnt[name] = filepath.Dir(rc.Config.ContainerWorkdir()) - - c := container.NewContainer(&container.NewContainerInput{ - Name: name, - WorkingDir: rc.Config.ContainerWorkdir(), - Image: spec.Image, - Username: rc.Config.Secrets["DOCKER_USERNAME"], - Password: rc.Config.Secrets["DOCKER_PASSWORD"], - Env: mergedEnv, - Mounts: mnt, - // NetworkMode: "host", - Binds: binds, - Stdout: logWriter, - Stderr: logWriter, - Privileged: rc.Config.Privileged, - UsernsMode: rc.Config.UsernsMode, - Platform: rc.Config.ContainerArchitecture, - }) - rc.ServiceContainers = append(rc.ServiceContainers, c) } rc.JobContainer = container.NewContainer(&container.NewContainerInput{ Cmd: nil, Entrypoint: []string{"/usr/bin/tail", "-f", "/dev/null"}, - WorkingDir: rc.Config.ContainerWorkdir(), + WorkingDir: workdir, Image: image, Username: username, Password: password, Name: name, Env: envList, Mounts: mounts, - // NetworkMode: "host", Binds: binds, Stdout: logWriter, Stderr: logWriter, @@ -218,25 +227,31 @@ func (rc *RunContext) startJobContainer() common.Executor { Hostname: hostname, }) + if job.Services == nil { + rc.JobContainer.SetContainerNetworkMode("host") + } + var copyWorkspace bool var copyToPath string if !rc.Config.BindWorkdir { copyToPath, copyWorkspace = rc.localCheckoutPath() - copyToPath = filepath.Join(rc.Config.ContainerWorkdir(), copyToPath) + copyToPath = filepath.Join(workdir, copyToPath) } - networkName := fmt.Sprintf("act-%s-network", rc.Run.JobID) return common.NewPipelineExecutor( rc.JobContainer.Pull(rc.Config.ForcePull), rc.stopServiceContainers(), rc.stopJobContainer(), - rc.removeNetwork(networkName), - rc.createNetwork(networkName), - rc.startServiceContainers(networkName), + common.NewPipelineExecutor( + rc.createNetwork(networkName, types.NetworkCreate{CheckDuplicate: true}).IfBool(!container.DockerNetworkExists(ctx, networkName)), + rc.startServiceContainers(networkName), + ).IfBool(job.Services != nil || job.Container() != nil), rc.JobContainer.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop), rc.JobContainer.Start(false), rc.JobContainer.UpdateFromImageEnv(&rc.Env), - rc.JobContainer.ConnectToNetwork(networkName), + common.NewPipelineExecutor( + rc.JobContainer.ConnectToNetwork(networkName), + ).IfBool(job.Services != nil || job.Container() != nil), rc.JobContainer.UpdateFromEnv("/etc/environment", &rc.Env), rc.JobContainer.Exec([]string{"mkdir", "-m", "0777", "-p", ActPath}, rc.Env, "root", ""), rc.JobContainer.CopyDir(copyToPath, rc.Config.Workdir+string(filepath.Separator)+".", rc.Config.UseGitIgnore).IfBool(copyWorkspace), @@ -257,9 +272,9 @@ func (rc *RunContext) startJobContainer() common.Executor { } } -func (rc *RunContext) createNetwork(name string) common.Executor { +func (rc *RunContext) createNetwork(name string, config types.NetworkCreate) common.Executor { return func(ctx context.Context) error { - return container.NewDockerNetworkCreateExecutor(name)(ctx) + return container.NewDockerNetworkCreateExecutor(name, config)(ctx) } } @@ -279,8 +294,11 @@ func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user func (rc *RunContext) stopJobContainer() common.Executor { return func(ctx context.Context) error { if rc.JobContainer != nil && !rc.Config.ReuseContainers { - return rc.JobContainer.Remove(). - Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false))(ctx) + return common.NewPipelineExecutor( + rc.stopServiceContainers(), + rc.JobContainer.Remove().Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false)), + rc.removeNetwork(rc.JobNetworkName()).IfBool((rc.Run.Job().Services != nil || rc.Run.Job().Container() != nil) && !rc.Config.ReuseContainers && container.DockerNetworkExists(ctx, rc.JobNetworkName())), + )(ctx) } return nil } @@ -291,10 +309,10 @@ func (rc *RunContext) startServiceContainers(networkName string) common.Executor execs := []common.Executor{} for _, c := range rc.ServiceContainers { execs = append(execs, common.NewPipelineExecutor( - c.Pull(false), + c.Pull(rc.Config.ForcePull), c.Create([]string{}, []string{}), - c.Start(false), c.ConnectToNetwork(networkName), + c.Start(false), )) } return common.NewParallelExecutor(execs...)(ctx) @@ -551,6 +569,10 @@ func createContainerName(parts ...string) string { return strings.ReplaceAll(strings.Trim(strings.Join(name, "-"), "-"), "--", "-") } +func createNetworkName(parts ...string) string { + return strings.ReplaceAll(strings.Trim(strings.Join(parts, "_"), "-"), "--", "-") +} + func trimToLen(s string, l int) string { if l < 0 { l = 0 @@ -562,17 +584,25 @@ func trimToLen(s string, l int) string { } type jobContext struct { - Status string `json:"status"` - Container struct { - ID string `json:"id"` - Network string `json:"network"` - } `json:"container"` - Services map[string]struct { - ID string `json:"id"` - } `json:"services"` + Status string `json:"status"` + Container jobContainerContext `json:"container"` + Services map[string]jobServiceContext `json:"services"` +} + +type jobContainerContext struct { + ID string `json:"id"` + Network string `json:"network"` +} + +type jobServiceContext struct { + ID string `json:"id"` + Network string `json:"network"` + Ports map[string]string `json:"ports"` } func (rc *RunContext) getJobContext() *jobContext { + job := rc.Run.Job() + jobStatus := "success" for _, stepStatus := range rc.StepResults { if stepStatus.Conclusion == stepStatusFailure { @@ -580,8 +610,34 @@ func (rc *RunContext) getJobContext() *jobContext { break } } + + container := jobContainerContext{} + if job.Container() != nil { + container = jobContainerContext{ + ID: rc.JobContainer.ID(), + Network: "host", + } + } + + services := make(map[string]jobServiceContext, len(job.Services)) + for name, container := range rc.ServiceContainers { + service := job.Services[name] + ports := make(map[string]string, len(service.Ports)) + for _, v := range service.Ports { + split := strings.Split(v, `:`) + ports[split[0]] = split[1] + } + services[name] = jobServiceContext{ + ID: container.ID(), + Network: rc.JobNetworkName(), + Ports: ports, + } + } + return &jobContext{ - Status: jobStatus, + Status: jobStatus, + Container: container, + Services: services, } } diff --git a/pkg/runner/runner_test.go b/pkg/runner/runner_test.go index 8cbe40c3fdc..79f154f6bff 100644 --- a/pkg/runner/runner_test.go +++ b/pkg/runner/runner_test.go @@ -190,7 +190,7 @@ func TestRunWithService(t *testing.T) { ctx := context.Background() platforms := map[string]string{ - "ubuntu-latest": "node:12.20.1-buster-slim", + "ubuntu-latest": baseImage, } workflowPath := "services" diff --git a/pkg/runner/testdata/services/push.yaml b/pkg/runner/testdata/services/push.yaml index f6ca7bc4c50..c519d9315e4 100644 --- a/pkg/runner/testdata/services/push.yaml +++ b/pkg/runner/testdata/services/push.yaml @@ -1,8 +1,7 @@ name: services on: push jobs: - services: - name: Reproduction of failing Services interpolation + postgres: runs-on: ubuntu-latest services: postgres: @@ -23,4 +22,20 @@ jobs: run: | echo "id: ${{ job.services.postgres.id }}" echo "network: ${{ job.services.postgres.network }}" - echo "ports: ${{ job.services.postgres.ports }}" + echo "ports: ${{ toJSON(job.services.postgres.ports) }}" + mongodb: + runs-on: ubuntu-latest + services: + mongodb: + image: mongo:5 + env: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example + ports: + - 27017:27017 + steps: + - name: Echo the Mongodb service ID / Network / Ports + run: | + echo "id: ${{ job.services.mongodb.id }}" + echo "network: ${{ job.services.mongodb.network }}" + echo "ports: ${{ toJSON(job.services.mongodb.ports) }}" From 90a47e9b2f0277fdb6941b635345854b1921acbc Mon Sep 17 00:00:00 2001 From: hackercat Date: Mon, 20 Dec 2021 04:16:49 +0000 Subject: [PATCH 7/7] fixup! fix: rework services Signed-off-by: hackercat --- pkg/runner/run_context.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 74cb1555cec..a6975c14409 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -620,17 +620,19 @@ func (rc *RunContext) getJobContext() *jobContext { } services := make(map[string]jobServiceContext, len(job.Services)) - for name, container := range rc.ServiceContainers { - service := job.Services[name] - ports := make(map[string]string, len(service.Ports)) - for _, v := range service.Ports { - split := strings.Split(v, `:`) - ports[split[0]] = split[1] - } - services[name] = jobServiceContext{ - ID: container.ID(), - Network: rc.JobNetworkName(), - Ports: ports, + if rc.ServiceContainers != nil { + for name, container := range rc.ServiceContainers { + service := job.Services[name] + ports := make(map[string]string, len(service.Ports)) + for _, v := range service.Ports { + split := strings.Split(v, `:`) + ports[split[0]] = split[1] + } + services[name] = jobServiceContext{ + ID: container.ID(), + Network: rc.JobNetworkName(), + Ports: ports, + } } }