mirror of https://gitea.com/gitea/act_runner.git
feat: service container in host mode (#95)
This commit is contained in:
parent
a3c8116dee
commit
8536279ece
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/actions-oss/act-cli/pkg/model"
|
"github.com/actions-oss/act-cli/pkg/model"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/opencontainers/selinux/go-selinux"
|
"github.com/opencontainers/selinux/go-selinux"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunContext contains info about current job
|
// RunContext contains info about current job
|
||||||
|
|
@ -232,7 +233,13 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||||
},
|
},
|
||||||
StdOut: logWriter,
|
StdOut: logWriter,
|
||||||
}
|
}
|
||||||
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
networkName, createAndDeleteNetwork, err := rc.prepareServiceContainers(ctx, logger, container.LinuxContainerEnvironmentExtensions{}, logWriter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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) {
|
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
||||||
if v, ok := v.(string); ok {
|
if v, ok := v.(string); ok {
|
||||||
rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v
|
rc.Env[fmt.Sprintf("RUNNER_%s", strings.ToUpper(k))] = v
|
||||||
|
|
@ -257,6 +264,12 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||||
Mode: 0o666,
|
Mode: 0o666,
|
||||||
Body: "",
|
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)
|
)(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -294,70 +307,9 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||||
binds, mounts := rc.GetBindsAndMounts()
|
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>)
|
networkName, createAndDeleteNetwork, err := rc.prepareServiceContainers(ctx, logger, ext, logWriter)
|
||||||
// if using service containers, will create a new network for the containers.
|
if err != nil {
|
||||||
// and it will be removed after at last.
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rc.cleanUpJobContainer = func(ctx context.Context) error {
|
rc.cleanUpJobContainer = func(ctx context.Context) error {
|
||||||
|
|
@ -370,23 +322,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||||
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false)).IfNot(reuseJobContainer).
|
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName(), false)).IfNot(reuseJobContainer).
|
||||||
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName()+"-env", false)).IfNot(reuseJobContainer).
|
Then(container.NewDockerVolumeRemoveExecutor(rc.jobContainerName()+"-env", false)).IfNot(reuseJobContainer).
|
||||||
Then(func(ctx context.Context) error {
|
Then(func(ctx context.Context) error {
|
||||||
if len(rc.ServiceContainers) > 0 {
|
return rc.cleanupServiceContainer(ctx, logger, createAndDeleteNetwork, networkName)
|
||||||
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
|
|
||||||
})(ctx)
|
})(ctx)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -445,6 +381,91 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "", false, 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)
|
||||||
|
}
|
||||||
|
return networkName, createAndDeleteNetwork, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user, workdir string) common.Executor {
|
func (rc *RunContext) execJobContainer(cmd []string, env map[string]string, user, workdir string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
return rc.JobContainer.Exec(cmd, env, user, workdir)(ctx)
|
return rc.JobContainer.Exec(cmd, env, user, workdir)(ctx)
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,8 @@ func TestRunEvent(t *testing.T) {
|
||||||
|
|
||||||
// docker action on host executor
|
// docker action on host executor
|
||||||
{workdir, "docker-action-host-env", "push", "", platforms, secrets},
|
{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 {
|
for _, table := range tables {
|
||||||
|
|
|
||||||
|
|
@ -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…
Reference in New Issue