Skip to content
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
185 changes: 103 additions & 82 deletions pkg/runner/run_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"github.com/actions-oss/act-cli/pkg/model"
"github.com/docker/go-connections/nat"
"github.com/opencontainers/selinux/go-selinux"
"github.com/sirupsen/logrus"
)

// RunContext contains info about current job
Expand Down Expand Up @@ -232,7 +233,13 @@
},
StdOut: logWriter,
}
rc.cleanUpJobContainer = rc.JobContainer.Remove()
networkName, createAndDeleteNetwork, err := rc.prepareServiceContainers(ctx, logger, container.LinuxContainerEnvironmentExtensions{}, logWriter)
if err != nil {
return err
}

Check warning on line 239 in pkg/runner/run_context.go

View check run for this annotation

Codecov / codecov/patch

pkg/runner/run_context.go#L238-L239

Added lines #L238 - L239 were not covered by tests
rc.cleanUpJobContainer = common.Executor(func(ctx context.Context) error {
return rc.cleanupServiceContainer(ctx, logger, createAndDeleteNetwork, networkName)
}).Finally(rc.JobContainer.Remove())
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
if v, ok := v.(string); ok {
rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v
Expand All @@ -257,6 +264,12 @@
Mode: 0o666,
Body: "",
}),
rc.pullServicesImages(rc.Config.ForcePull),
func(ctx context.Context) error {
return rc.cleanupServiceContainer(ctx, logger, createAndDeleteNetwork, networkName)
},
container.NewDockerNetworkCreateExecutor(networkName).IfBool(createAndDeleteNetwork),
rc.startServiceContainers(networkName),
)(ctx)
}
}
Expand Down Expand Up @@ -294,70 +307,9 @@
ext := container.LinuxContainerEnvironmentExtensions{}
binds, mounts := rc.GetBindsAndMounts()

// specify the network to which the container will connect when `docker create` stage. (like execute command line: docker create --network <networkName> <image>)
// if using service containers, will create a new network for the containers.
// and it will be removed after at last.
networkName, createAndDeleteNetwork := rc.networkName()

// add service containers
for serviceID, spec := range rc.Run.Job().Services {
// interpolate env
interpolatedEnvs := make(map[string]string, len(spec.Env))
for k, v := range spec.Env {
interpolatedEnvs[k] = rc.ExprEval.Interpolate(ctx, v)
}
envs := make([]string, 0, len(interpolatedEnvs))
for k, v := range interpolatedEnvs {
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
}
username, password, err = rc.handleServiceCredentials(ctx, spec.Credentials)
if err != nil {
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
}

interpolatedVolumes := make([]string, 0, len(spec.Volumes))
for _, volume := range spec.Volumes {
interpolatedVolumes = append(interpolatedVolumes, rc.ExprEval.Interpolate(ctx, volume))
}
serviceBinds, serviceMounts := rc.GetServiceBindsAndMounts(interpolatedVolumes)

interpolatedPorts := make([]string, 0, len(spec.Ports))
for _, port := range spec.Ports {
interpolatedPorts = append(interpolatedPorts, rc.ExprEval.Interpolate(ctx, port))
}
exposedPorts, portBindings, err := nat.ParsePortSpecs(interpolatedPorts)
if err != nil {
return fmt.Errorf("failed to parse service %s ports: %w", serviceID, err)
}

imageName := rc.ExprEval.Interpolate(ctx, spec.Image)
if imageName == "" {
logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID)
continue
}

serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
c := container.NewContainer(&container.NewContainerInput{
Name: serviceContainerName,
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
Image: imageName,
Username: username,
Password: password,
Env: envs,
Mounts: serviceMounts,
Binds: serviceBinds,
Stdout: logWriter,
Stderr: logWriter,
Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
Options: rc.ExprEval.Interpolate(ctx, spec.Options),
NetworkMode: networkName,
NetworkAliases: []string{serviceID},
ExposedPorts: exposedPorts,
PortBindings: portBindings,
})
rc.ServiceContainers = append(rc.ServiceContainers, c)
networkName, createAndDeleteNetwork, err := rc.prepareServiceContainers(ctx, logger, ext, logWriter)
if err != nil {
return err

Check warning on line 312 in pkg/runner/run_context.go

View check run for this annotation

Codecov / codecov/patch

pkg/runner/run_context.go#L312

Added line #L312 was not covered by tests
}

rc.cleanUpJobContainer = func(ctx context.Context) error {
Expand All @@ -370,23 +322,7 @@
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false)).IfNot(reuseJobContainer).
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName()+"-env", false)).IfNot(reuseJobContainer).
Then(func(ctx context.Context) error {
if len(rc.ServiceContainers) > 0 {
logger.Infof("Cleaning up services for job %s", rc.JobName)
if err := rc.stopServiceContainers()(ctx); err != nil {
logger.Errorf("error while cleaning services: %v", err)
}
if createAndDeleteNetwork {
// clean network if it has been created by act
// if using service containers
// it means that the network to which containers are connecting is created by `act_runner`,
// so, we should remove the network at last.
logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
logger.Errorf("error while cleaning network: %v", err)
}
}
}
return nil
return rc.cleanupServiceContainer(ctx, logger, createAndDeleteNetwork, networkName)
})(ctx)
}
return nil
Expand Down Expand Up @@ -445,6 +381,91 @@
}
}

func (rc *RunContext) cleanupServiceContainer(ctx context.Context, logger logrus.FieldLogger, createAndDeleteNetwork bool, networkName string) error {
if len(rc.ServiceContainers) > 0 {
logger.Infof("Cleaning up services for job %s", rc.JobName)
if err := rc.stopServiceContainers()(ctx); err != nil {
logger.Errorf("error while cleaning services: %v", err)
}

Check warning on line 389 in pkg/runner/run_context.go

View check run for this annotation

Codecov / codecov/patch

pkg/runner/run_context.go#L388-L389

Added lines #L388 - L389 were not covered by tests
if createAndDeleteNetwork {
logger.Infof("Cleaning up network for job %s, and network name is: %s", rc.JobName, networkName)
if err := container.NewDockerNetworkRemoveExecutor(networkName)(ctx); err != nil {
logger.Errorf("error while cleaning network: %v", err)
}

Check warning on line 394 in pkg/runner/run_context.go

View check run for this annotation

Codecov / codecov/patch

pkg/runner/run_context.go#L393-L394

Added lines #L393 - L394 were not covered by tests
}
}
return nil
}

func (rc *RunContext) prepareServiceContainers(ctx context.Context, logger logrus.FieldLogger, ext container.LinuxContainerEnvironmentExtensions, logWriter io.Writer) (string, bool, error) {
// specify the network to which the container will connect when `docker create` stage. (like execute command line: docker create --network <networkName> <image>)
// if using service containers, will create a new network for the containers.
// and it will be removed after at last
networkName, createAndDeleteNetwork := rc.networkName()

// add service containers
for serviceID, spec := range rc.Run.Job().Services {
// interpolate env
interpolatedEnvs := make(map[string]string, len(spec.Env))
for k, v := range spec.Env {
interpolatedEnvs[k] = rc.ExprEval.Interpolate(ctx, v)
}
envs := make([]string, 0, len(interpolatedEnvs))
for k, v := range interpolatedEnvs {
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
}
username, password, err := rc.handleServiceCredentials(ctx, spec.Credentials)
if err != nil {
return "", false, fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
}

Check warning on line 420 in pkg/runner/run_context.go

View check run for this annotation

Codecov / codecov/patch

pkg/runner/run_context.go#L419-L420

Added lines #L419 - L420 were not covered by tests

interpolatedVolumes := make([]string, 0, len(spec.Volumes))
for _, volume := range spec.Volumes {
interpolatedVolumes = append(interpolatedVolumes, rc.ExprEval.Interpolate(ctx, volume))
}

Check warning on line 425 in pkg/runner/run_context.go

View check run for this annotation

Codecov / codecov/patch

pkg/runner/run_context.go#L424-L425

Added lines #L424 - L425 were not covered by tests
serviceBinds, serviceMounts := rc.GetServiceBindsAndMounts(interpolatedVolumes)

interpolatedPorts := make([]string, 0, len(spec.Ports))
for _, port := range spec.Ports {
interpolatedPorts = append(interpolatedPorts, rc.ExprEval.Interpolate(ctx, port))
}
exposedPorts, portBindings, err := nat.ParsePortSpecs(interpolatedPorts)
if err != nil {
return "", false, fmt.Errorf("failed to parse service %s ports: %w", serviceID, err)
}

Check warning on line 435 in pkg/runner/run_context.go

View check run for this annotation

Codecov / codecov/patch

pkg/runner/run_context.go#L434-L435

Added lines #L434 - L435 were not covered by tests

imageName := rc.ExprEval.Interpolate(ctx, spec.Image)
if imageName == "" {
logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID)
continue
}

serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
c := container.NewContainer(&container.NewContainerInput{
Name: serviceContainerName,
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
Image: imageName,
Username: username,
Password: password,
Env: envs,
Mounts: serviceMounts,
Binds: serviceBinds,
Stdout: logWriter,
Stderr: logWriter,
Privileged: rc.Config.Privileged,
UsernsMode: rc.Config.UsernsMode,
Platform: rc.Config.ContainerArchitecture,
Options: rc.ExprEval.Interpolate(ctx, spec.Options),
NetworkMode: networkName,
NetworkAliases: []string{serviceID},
ExposedPorts: exposedPorts,
PortBindings: portBindings,
})
rc.ServiceContainers = append(rc.ServiceContainers, c)
}
return networkName, createAndDeleteNetwork, nil
}

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)
Expand Down
2 changes: 2 additions & 0 deletions pkg/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ func TestRunEvent(t *testing.T) {

// docker action on host executor
{workdir, "docker-action-host-env", "push", "", platforms, secrets},
// docker service on host executor
{workdir, "nginx-service-container-host-mode", "push", "", platforms, secrets},
}

for _, table := range tables {
Expand Down
24 changes: 24 additions & 0 deletions pkg/runner/testdata/nginx-service-container-host-mode/push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Self-Hosted Runner with NGINX Service

on: [push]

jobs:
nginx_service_job:
runs-on: self-hosted
services:
nginx:
image: nginx:latest
ports:
- 8084:80
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Wait for NGINX to be ready
run: |
for i in {1..10}; do
curl -I http://localhost:8084 && break || sleep 3
done

- name: Verify NGINX is running
run: curl -I http://localhost:8084
Loading