mirror of
https://github.com/actions/runner.git
synced 2025-12-19 00:36:55 +00:00
GitHub Actions Runner
This commit is contained in:
318
src/Runner.Worker/Container/ContainerInfo.cs
Normal file
318
src/Runner.Worker/Container/ContainerInfo.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container
|
||||
{
|
||||
public class ContainerInfo
|
||||
{
|
||||
private IDictionary<string, string> _userMountVolumes;
|
||||
private List<MountVolume> _mountVolumes;
|
||||
private IDictionary<string, string> _userPortMappings;
|
||||
private List<PortMapping> _portMappings;
|
||||
private IDictionary<string, string> _environmentVariables;
|
||||
private List<PathMapping> _pathMappings = new List<PathMapping>();
|
||||
|
||||
public ContainerInfo()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ContainerInfo(IHostContext hostContext, Pipelines.JobContainer container, bool isJobContainer = true, string networkAlias = null)
|
||||
{
|
||||
this.ContainerName = container.Alias;
|
||||
|
||||
string containerImage = container.Image;
|
||||
ArgUtil.NotNullOrEmpty(containerImage, nameof(containerImage));
|
||||
|
||||
this.ContainerImage = containerImage;
|
||||
this.ContainerDisplayName = $"{container.Alias}_{Pipelines.Validation.NameValidation.Sanitize(containerImage)}_{Guid.NewGuid().ToString("N").Substring(0, 6)}";
|
||||
this.ContainerCreateOptions = container.Options;
|
||||
_environmentVariables = container.Environment;
|
||||
this.IsJobContainer = isJobContainer;
|
||||
this.ContainerNetworkAlias = networkAlias;
|
||||
|
||||
#if OS_WINDOWS
|
||||
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Work), "C:\\__w"));
|
||||
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Tools), "C:\\__t")); // Tool cache folder may come from ENV, so we need a unique folder to avoid collision
|
||||
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Externals), "C:\\__e"));
|
||||
// add -v '\\.\pipe\docker_engine:\\.\pipe\docker_engine' when they are available (17.09)
|
||||
#else
|
||||
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Work), "/__w"));
|
||||
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Tools), "/__t")); // Tool cache folder may come from ENV, so we need a unique folder to avoid collision
|
||||
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Externals), "/__e"));
|
||||
if (this.IsJobContainer)
|
||||
{
|
||||
this.MountVolumes.Add(new MountVolume("/var/run/docker.sock", "/var/run/docker.sock"));
|
||||
}
|
||||
#endif
|
||||
if (container.Ports?.Count > 0)
|
||||
{
|
||||
foreach (var port in container.Ports)
|
||||
{
|
||||
UserPortMappings[port] = port;
|
||||
}
|
||||
}
|
||||
if (container.Volumes?.Count > 0)
|
||||
{
|
||||
foreach (var volume in container.Volumes)
|
||||
{
|
||||
UserMountVolumes[volume] = volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ContainerId { get; set; }
|
||||
public string ContainerDisplayName { get; set; }
|
||||
public string ContainerNetwork { get; set; }
|
||||
public string ContainerNetworkAlias { get; set; }
|
||||
public string ContainerImage { get; set; }
|
||||
public string ContainerName { get; set; }
|
||||
public string ContainerEntryPointArgs { get; set; }
|
||||
public string ContainerEntryPoint { get; set; }
|
||||
public string ContainerWorkDirectory { get; set; }
|
||||
public string ContainerCreateOptions { get; private set; }
|
||||
public string ContainerRuntimePath { get; set; }
|
||||
public bool IsJobContainer { get; set; }
|
||||
|
||||
public IDictionary<string, string> ContainerEnvironmentVariables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_environmentVariables == null)
|
||||
{
|
||||
_environmentVariables = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
return _environmentVariables;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, string> UserMountVolumes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_userMountVolumes == null)
|
||||
{
|
||||
_userMountVolumes = new Dictionary<string, string>();
|
||||
}
|
||||
return _userMountVolumes;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MountVolume> MountVolumes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_mountVolumes == null)
|
||||
{
|
||||
_mountVolumes = new List<MountVolume>();
|
||||
}
|
||||
|
||||
return _mountVolumes;
|
||||
}
|
||||
}
|
||||
|
||||
public IDictionary<string, string> UserPortMappings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_userPortMappings == null)
|
||||
{
|
||||
_userPortMappings = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
return _userPortMappings;
|
||||
}
|
||||
}
|
||||
|
||||
public List<PortMapping> PortMappings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_portMappings == null)
|
||||
{
|
||||
_portMappings = new List<PortMapping>();
|
||||
}
|
||||
|
||||
return _portMappings;
|
||||
}
|
||||
}
|
||||
|
||||
public string TranslateToContainerPath(string path)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
foreach (var mapping in _pathMappings)
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
if (string.Equals(path, mapping.HostPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return mapping.ContainerPath;
|
||||
}
|
||||
|
||||
if (path.StartsWith(mapping.HostPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith(mapping.HostPath + Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return mapping.ContainerPath + path.Remove(0, mapping.HostPath.Length);
|
||||
}
|
||||
#else
|
||||
if (string.Equals(path, mapping.HostPath))
|
||||
{
|
||||
return mapping.ContainerPath;
|
||||
}
|
||||
|
||||
if (path.StartsWith(mapping.HostPath + Path.DirectorySeparatorChar))
|
||||
{
|
||||
return mapping.ContainerPath + path.Remove(0, mapping.HostPath.Length);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public string TranslateToHostPath(string path)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
foreach (var mapping in _pathMappings)
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
if (string.Equals(path, mapping.ContainerPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return mapping.HostPath;
|
||||
}
|
||||
|
||||
if (path.StartsWith(mapping.ContainerPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith(mapping.ContainerPath + Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return mapping.HostPath + path.Remove(0, mapping.ContainerPath.Length);
|
||||
}
|
||||
#else
|
||||
if (string.Equals(path, mapping.ContainerPath))
|
||||
{
|
||||
return mapping.HostPath;
|
||||
}
|
||||
|
||||
if (path.StartsWith(mapping.ContainerPath + Path.DirectorySeparatorChar))
|
||||
{
|
||||
return mapping.HostPath + path.Remove(0, mapping.ContainerPath.Length);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public void AddPortMappings(List<PortMapping> portMappings)
|
||||
{
|
||||
foreach (var port in portMappings)
|
||||
{
|
||||
PortMappings.Add(port);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddPathTranslateMapping(string hostCommonPath, string containerCommonPath)
|
||||
{
|
||||
_pathMappings.Insert(0, new PathMapping(hostCommonPath, containerCommonPath));
|
||||
}
|
||||
}
|
||||
|
||||
public class MountVolume
|
||||
{
|
||||
public MountVolume(string sourceVolumePath, string targetVolumePath, bool readOnly = false)
|
||||
{
|
||||
this.SourceVolumePath = sourceVolumePath;
|
||||
this.TargetVolumePath = targetVolumePath;
|
||||
this.ReadOnly = readOnly;
|
||||
}
|
||||
|
||||
public MountVolume(string fromString)
|
||||
{
|
||||
ParseVolumeString(fromString);
|
||||
}
|
||||
|
||||
private void ParseVolumeString(string volume)
|
||||
{
|
||||
var volumeSplit = volume.Split(":");
|
||||
if (volumeSplit.Length == 3)
|
||||
{
|
||||
// source:target:ro
|
||||
SourceVolumePath = volumeSplit[0];
|
||||
TargetVolumePath = volumeSplit[1];
|
||||
ReadOnly = String.Equals(volumeSplit[2], "ro", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (volumeSplit.Length == 2)
|
||||
{
|
||||
if (String.Equals(volumeSplit[1], "ro", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// target:ro
|
||||
TargetVolumePath = volumeSplit[0];
|
||||
ReadOnly = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// source:target
|
||||
SourceVolumePath = volumeSplit[0];
|
||||
TargetVolumePath = volumeSplit[1];
|
||||
ReadOnly = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// target - or, default to passing straight through
|
||||
TargetVolumePath = volume;
|
||||
ReadOnly = false;
|
||||
}
|
||||
}
|
||||
|
||||
public string SourceVolumePath { get; set; }
|
||||
public string TargetVolumePath { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
}
|
||||
|
||||
public class PortMapping
|
||||
{
|
||||
public PortMapping(string hostPort, string containerPort, string protocol)
|
||||
{
|
||||
this.HostPort = hostPort;
|
||||
this.ContainerPort = containerPort;
|
||||
this.Protocol = protocol;
|
||||
}
|
||||
|
||||
public string HostPort { get; set; }
|
||||
public string ContainerPort { get; set; }
|
||||
public string Protocol { get; set; }
|
||||
}
|
||||
|
||||
public class DockerVersion
|
||||
{
|
||||
public DockerVersion(Version serverVersion, Version clientVersion)
|
||||
{
|
||||
this.ServerVersion = serverVersion;
|
||||
this.ClientVersion = clientVersion;
|
||||
}
|
||||
|
||||
public Version ServerVersion { get; set; }
|
||||
public Version ClientVersion { get; set; }
|
||||
}
|
||||
|
||||
public class PathMapping
|
||||
{
|
||||
public PathMapping(string hostPath, string containerPath)
|
||||
{
|
||||
this.HostPath = hostPath;
|
||||
this.ContainerPath = containerPath;
|
||||
}
|
||||
|
||||
public string HostPath { get; set; }
|
||||
public string ContainerPath { get; set; }
|
||||
}
|
||||
}
|
||||
433
src/Runner.Worker/Container/DockerCommandManager.cs
Normal file
433
src/Runner.Worker/Container/DockerCommandManager.cs
Normal file
@@ -0,0 +1,433 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container
|
||||
{
|
||||
[ServiceLocator(Default = typeof(DockerCommandManager))]
|
||||
public interface IDockerCommandManager : IRunnerService
|
||||
{
|
||||
string DockerPath { get; }
|
||||
string DockerInstanceLabel { get; }
|
||||
Task<DockerVersion> DockerVersion(IExecutionContext context);
|
||||
Task<int> DockerPull(IExecutionContext context, string image);
|
||||
Task<int> DockerBuild(IExecutionContext context, string workingDirectory, string dockerFile, string tag);
|
||||
Task<string> DockerCreate(IExecutionContext context, ContainerInfo container);
|
||||
Task<int> DockerRun(IExecutionContext context, ContainerInfo container, EventHandler<ProcessDataReceivedEventArgs> stdoutDataReceived, EventHandler<ProcessDataReceivedEventArgs> stderrDataReceived);
|
||||
Task<int> DockerStart(IExecutionContext context, string containerId);
|
||||
Task<int> DockerLogs(IExecutionContext context, string containerId);
|
||||
Task<List<string>> DockerPS(IExecutionContext context, string options);
|
||||
Task<int> DockerRemove(IExecutionContext context, string containerId);
|
||||
Task<int> DockerNetworkCreate(IExecutionContext context, string network);
|
||||
Task<int> DockerNetworkRemove(IExecutionContext context, string network);
|
||||
Task<int> DockerNetworkPrune(IExecutionContext context);
|
||||
Task<int> DockerExec(IExecutionContext context, string containerId, string options, string command);
|
||||
Task<int> DockerExec(IExecutionContext context, string containerId, string options, string command, List<string> outputs);
|
||||
Task<List<string>> DockerInspect(IExecutionContext context, string dockerObject, string options);
|
||||
Task<List<PortMapping>> DockerPort(IExecutionContext context, string containerId);
|
||||
}
|
||||
|
||||
public class DockerCommandManager : RunnerService, IDockerCommandManager
|
||||
{
|
||||
public string DockerPath { get; private set; }
|
||||
|
||||
public string DockerInstanceLabel { get; private set; }
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
DockerPath = WhichUtil.Which("docker", true, Trace);
|
||||
DockerInstanceLabel = IOUtil.GetPathHash(hostContext.GetDirectory(WellKnownDirectory.Root)).Substring(0, 6);
|
||||
}
|
||||
|
||||
public async Task<DockerVersion> DockerVersion(IExecutionContext context)
|
||||
{
|
||||
string serverVersionStr = (await ExecuteDockerCommandAsync(context, "version", "--format '{{.Server.APIVersion}}'")).FirstOrDefault();
|
||||
ArgUtil.NotNullOrEmpty(serverVersionStr, "Docker.Server.Version");
|
||||
context.Output($"Docker daemon API version: {serverVersionStr}");
|
||||
|
||||
string clientVersionStr = (await ExecuteDockerCommandAsync(context, "version", "--format '{{.Client.APIVersion}}'")).FirstOrDefault();
|
||||
ArgUtil.NotNullOrEmpty(serverVersionStr, "Docker.Client.Version");
|
||||
context.Output($"Docker client API version: {clientVersionStr}");
|
||||
|
||||
// we interested about major.minor.patch version
|
||||
Regex verRegex = new Regex("\\d+\\.\\d+(\\.\\d+)?", RegexOptions.IgnoreCase);
|
||||
|
||||
Version serverVersion = null;
|
||||
var serverVersionMatchResult = verRegex.Match(serverVersionStr);
|
||||
if (serverVersionMatchResult.Success && !string.IsNullOrEmpty(serverVersionMatchResult.Value))
|
||||
{
|
||||
if (!Version.TryParse(serverVersionMatchResult.Value, out serverVersion))
|
||||
{
|
||||
serverVersion = null;
|
||||
}
|
||||
}
|
||||
|
||||
Version clientVersion = null;
|
||||
var clientVersionMatchResult = verRegex.Match(serverVersionStr);
|
||||
if (clientVersionMatchResult.Success && !string.IsNullOrEmpty(clientVersionMatchResult.Value))
|
||||
{
|
||||
if (!Version.TryParse(clientVersionMatchResult.Value, out clientVersion))
|
||||
{
|
||||
clientVersion = null;
|
||||
}
|
||||
}
|
||||
|
||||
return new DockerVersion(serverVersion, clientVersion);
|
||||
}
|
||||
|
||||
public async Task<int> DockerPull(IExecutionContext context, string image)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "pull", image, context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> DockerBuild(IExecutionContext context, string workingDirectory, string dockerFile, string tag)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "build", $"-t {tag} \"{dockerFile}\"", workingDirectory, context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string> DockerCreate(IExecutionContext context, ContainerInfo container)
|
||||
{
|
||||
IList<string> dockerOptions = new List<string>();
|
||||
// OPTIONS
|
||||
dockerOptions.Add($"--name {container.ContainerDisplayName}");
|
||||
dockerOptions.Add($"--label {DockerInstanceLabel}");
|
||||
if (!string.IsNullOrEmpty(container.ContainerWorkDirectory))
|
||||
{
|
||||
dockerOptions.Add($"--workdir {container.ContainerWorkDirectory}");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(container.ContainerNetwork))
|
||||
{
|
||||
dockerOptions.Add($"--network {container.ContainerNetwork}");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(container.ContainerNetworkAlias))
|
||||
{
|
||||
dockerOptions.Add($"--network-alias {container.ContainerNetworkAlias}");
|
||||
}
|
||||
foreach (var port in container.UserPortMappings)
|
||||
{
|
||||
dockerOptions.Add($"-p {port.Value}");
|
||||
}
|
||||
dockerOptions.Add($"{container.ContainerCreateOptions}");
|
||||
foreach (var env in container.ContainerEnvironmentVariables)
|
||||
{
|
||||
if (String.IsNullOrEmpty(env.Value))
|
||||
{
|
||||
dockerOptions.Add($"-e \"{env.Key}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
dockerOptions.Add($"-e \"{env.Key}={env.Value.Replace("\"", "\\\"")}\"");
|
||||
}
|
||||
}
|
||||
|
||||
// Watermark for GitHub Action environment
|
||||
dockerOptions.Add("-e GITHUB_ACTIONS=true");
|
||||
|
||||
foreach (var volume in container.MountVolumes)
|
||||
{
|
||||
// replace `"` with `\"` and add `"{0}"` to all path.
|
||||
String volumeArg;
|
||||
if (String.IsNullOrEmpty(volume.SourceVolumePath))
|
||||
{
|
||||
// Anonymous docker volume
|
||||
volumeArg = $"-v \"{volume.TargetVolumePath.Replace("\"", "\\\"")}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Named Docker volume / host bind mount
|
||||
volumeArg = $"-v \"{volume.SourceVolumePath.Replace("\"", "\\\"")}\":\"{volume.TargetVolumePath.Replace("\"", "\\\"")}\"";
|
||||
}
|
||||
if (volume.ReadOnly)
|
||||
{
|
||||
volumeArg += ":ro";
|
||||
}
|
||||
dockerOptions.Add(volumeArg);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(container.ContainerEntryPoint))
|
||||
{
|
||||
dockerOptions.Add($"--entrypoint \"{container.ContainerEntryPoint}\"");
|
||||
}
|
||||
// IMAGE
|
||||
dockerOptions.Add($"{container.ContainerImage}");
|
||||
|
||||
// COMMAND
|
||||
// Intentionally blank. Always overwrite ENTRYPOINT and/or send ARGs
|
||||
|
||||
// [ARG...]
|
||||
dockerOptions.Add($"{container.ContainerEntryPointArgs}");
|
||||
|
||||
var optionsString = string.Join(" ", dockerOptions);
|
||||
List<string> outputStrings = await ExecuteDockerCommandAsync(context, "create", optionsString);
|
||||
|
||||
return outputStrings.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<int> DockerRun(IExecutionContext context, ContainerInfo container, EventHandler<ProcessDataReceivedEventArgs> stdoutDataReceived, EventHandler<ProcessDataReceivedEventArgs> stderrDataReceived)
|
||||
{
|
||||
IList<string> dockerOptions = new List<string>();
|
||||
// OPTIONS
|
||||
dockerOptions.Add($"--name {container.ContainerDisplayName}");
|
||||
dockerOptions.Add($"--label {DockerInstanceLabel}");
|
||||
|
||||
dockerOptions.Add($"--workdir {container.ContainerWorkDirectory}");
|
||||
dockerOptions.Add($"--rm");
|
||||
|
||||
foreach (var env in container.ContainerEnvironmentVariables)
|
||||
{
|
||||
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
|
||||
// the value directly in the command
|
||||
dockerOptions.Add($"-e {env.Key}");
|
||||
}
|
||||
|
||||
// Watermark for GitHub Action environment
|
||||
dockerOptions.Add("-e GITHUB_ACTIONS=true");
|
||||
|
||||
if (!string.IsNullOrEmpty(container.ContainerEntryPoint))
|
||||
{
|
||||
dockerOptions.Add($"--entrypoint \"{container.ContainerEntryPoint}\"");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(container.ContainerNetwork))
|
||||
{
|
||||
dockerOptions.Add($"--network {container.ContainerNetwork}");
|
||||
}
|
||||
|
||||
foreach (var volume in container.MountVolumes)
|
||||
{
|
||||
// replace `"` with `\"` and add `"{0}"` to all path.
|
||||
String volumeArg;
|
||||
if (String.IsNullOrEmpty(volume.SourceVolumePath))
|
||||
{
|
||||
// Anonymous docker volume
|
||||
volumeArg = $"-v \"{volume.TargetVolumePath.Replace("\"", "\\\"")}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Named Docker volume / host bind mount
|
||||
volumeArg = $"-v \"{volume.SourceVolumePath.Replace("\"", "\\\"")}\":\"{volume.TargetVolumePath.Replace("\"", "\\\"")}\"";
|
||||
}
|
||||
if (volume.ReadOnly)
|
||||
{
|
||||
volumeArg += ":ro";
|
||||
}
|
||||
dockerOptions.Add(volumeArg);
|
||||
}
|
||||
// IMAGE
|
||||
dockerOptions.Add($"{container.ContainerImage}");
|
||||
|
||||
// COMMAND
|
||||
// Intentionally blank. Always overwrite ENTRYPOINT and/or send ARGs
|
||||
|
||||
// [ARG...]
|
||||
dockerOptions.Add($"{container.ContainerEntryPointArgs}");
|
||||
|
||||
var optionsString = string.Join(" ", dockerOptions);
|
||||
return await ExecuteDockerCommandAsync(context, "run", optionsString, container.ContainerEnvironmentVariables, stdoutDataReceived, stderrDataReceived, context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> DockerStart(IExecutionContext context, string containerId)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "start", containerId, context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> DockerRemove(IExecutionContext context, string containerId)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "rm", $"--force {containerId}", context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> DockerLogs(IExecutionContext context, string containerId)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "logs", $"--details {containerId}", context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<string>> DockerPS(IExecutionContext context, string options)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "ps", options);
|
||||
}
|
||||
|
||||
public async Task<int> DockerNetworkCreate(IExecutionContext context, string network)
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
return await ExecuteDockerCommandAsync(context, "network", $"create --label {DockerInstanceLabel} {network} --driver nat", context.CancellationToken);
|
||||
#else
|
||||
return await ExecuteDockerCommandAsync(context, "network", $"create --label {DockerInstanceLabel} {network}", context.CancellationToken);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async Task<int> DockerNetworkRemove(IExecutionContext context, string network)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "network", $"rm {network}", context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> DockerNetworkPrune(IExecutionContext context)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "network", $"prune --force --filter \"label={DockerInstanceLabel}\"", context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> DockerExec(IExecutionContext context, string containerId, string options, string command)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "exec", $"{options} {containerId} {command}", context.CancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> DockerExec(IExecutionContext context, string containerId, string options, string command, List<string> output)
|
||||
{
|
||||
ArgUtil.NotNull(output, nameof(output));
|
||||
|
||||
string arg = $"exec {options} {containerId} {command}".Trim();
|
||||
context.Command($"{DockerPath} {arg}");
|
||||
|
||||
object outputLock = new object();
|
||||
var processInvoker = HostContext.CreateService<IProcessInvoker>();
|
||||
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
output.Add(message.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message.Data))
|
||||
{
|
||||
lock (outputLock)
|
||||
{
|
||||
output.Add(message.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#if OS_WINDOWS || OS_OSX
|
||||
throw new NotSupportedException($"Container operation is only supported on Linux");
|
||||
#else
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work),
|
||||
fileName: DockerPath,
|
||||
arguments: arg,
|
||||
environment: null,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: null,
|
||||
cancellationToken: CancellationToken.None);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async Task<List<string>> DockerInspect(IExecutionContext context, string dockerObject, string options)
|
||||
{
|
||||
return await ExecuteDockerCommandAsync(context, "inspect", $"{options} {dockerObject}");
|
||||
}
|
||||
|
||||
public async Task<List<PortMapping>> DockerPort(IExecutionContext context, string containerId)
|
||||
{
|
||||
List<string> portMappingLines = await ExecuteDockerCommandAsync(context, "port", containerId);
|
||||
return DockerUtil.ParseDockerPort(portMappingLines);
|
||||
}
|
||||
|
||||
private Task<int> ExecuteDockerCommandAsync(IExecutionContext context, string command, string options, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return ExecuteDockerCommandAsync(context, command, options, null, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteDockerCommandAsync(IExecutionContext context, string command, string options, IDictionary<string, string> environment, EventHandler<ProcessDataReceivedEventArgs> stdoutDataReceived, EventHandler<ProcessDataReceivedEventArgs> stderrDataReceived, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
string arg = $"{command} {options}".Trim();
|
||||
context.Command($"{DockerPath} {arg}");
|
||||
|
||||
var processInvoker = HostContext.CreateService<IProcessInvoker>();
|
||||
processInvoker.OutputDataReceived += stdoutDataReceived;
|
||||
processInvoker.ErrorDataReceived += stderrDataReceived;
|
||||
|
||||
|
||||
#if OS_WINDOWS || OS_OSX
|
||||
throw new NotSupportedException($"Container operation is only supported on Linux");
|
||||
#else
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: context.GetGitHubContext("workspace"),
|
||||
fileName: DockerPath,
|
||||
arguments: arg,
|
||||
environment: environment,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: null,
|
||||
killProcessOnCancel: false,
|
||||
cancellationToken: cancellationToken);
|
||||
#endif
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteDockerCommandAsync(IExecutionContext context, string command, string options, string workingDirectory, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
string arg = $"{command} {options}".Trim();
|
||||
context.Command($"{DockerPath} {arg}");
|
||||
|
||||
var processInvoker = HostContext.CreateService<IProcessInvoker>();
|
||||
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
#if OS_WINDOWS || OS_OSX
|
||||
throw new NotSupportedException($"Container operation is only supported on Linux");
|
||||
#else
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: workingDirectory ?? context.GetGitHubContext("workspace"),
|
||||
fileName: DockerPath,
|
||||
arguments: arg,
|
||||
environment: null,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: null,
|
||||
killProcessOnCancel: false,
|
||||
redirectStandardIn: null,
|
||||
cancellationToken: cancellationToken);
|
||||
#endif
|
||||
}
|
||||
|
||||
private async Task<List<string>> ExecuteDockerCommandAsync(IExecutionContext context, string command, string options)
|
||||
{
|
||||
string arg = $"{command} {options}".Trim();
|
||||
context.Command($"{DockerPath} {arg}");
|
||||
|
||||
List<string> output = new List<string>();
|
||||
var processInvoker = HostContext.CreateService<IProcessInvoker>();
|
||||
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message.Data))
|
||||
{
|
||||
output.Add(message.Data);
|
||||
context.Output(message.Data);
|
||||
}
|
||||
};
|
||||
|
||||
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(message.Data))
|
||||
{
|
||||
context.Output(message.Data);
|
||||
}
|
||||
};
|
||||
|
||||
await processInvoker.ExecuteAsync(
|
||||
workingDirectory: context.GetGitHubContext("workspace"),
|
||||
fileName: DockerPath,
|
||||
arguments: arg,
|
||||
environment: null,
|
||||
requireExitCodeZero: true,
|
||||
outputEncoding: null,
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/Runner.Worker/Container/DockerUtil.cs
Normal file
49
src/Runner.Worker/Container/DockerUtil.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container
|
||||
{
|
||||
public class DockerUtil
|
||||
{
|
||||
public static List<PortMapping> ParseDockerPort(IList<string> portMappingLines)
|
||||
{
|
||||
const string targetPort = "targetPort";
|
||||
const string proto = "proto";
|
||||
const string host = "host";
|
||||
const string hostPort = "hostPort";
|
||||
|
||||
//"TARGET_PORT/PROTO -> HOST:HOST_PORT"
|
||||
string pattern = $"^(?<{targetPort}>\\d+)/(?<{proto}>\\w+) -> (?<{host}>.+):(?<{hostPort}>\\d+)$";
|
||||
|
||||
List<PortMapping> portMappings = new List<PortMapping>();
|
||||
foreach(var line in portMappingLines)
|
||||
{
|
||||
Match m = Regex.Match(line, pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
|
||||
if (m.Success)
|
||||
{
|
||||
portMappings.Add(new PortMapping(
|
||||
m.Groups[hostPort].Value,
|
||||
m.Groups[targetPort].Value,
|
||||
m.Groups[proto].Value
|
||||
));
|
||||
}
|
||||
}
|
||||
return portMappings;
|
||||
}
|
||||
|
||||
public static string ParsePathFromConfigEnv(IList<string> configEnvLines)
|
||||
{
|
||||
// Config format is VAR=value per line
|
||||
foreach (var line in configEnvLines)
|
||||
{
|
||||
var keyValue = line.Split("=", 2);
|
||||
if (keyValue.Length == 2 && string.Equals(keyValue[0], "PATH"))
|
||||
{
|
||||
return keyValue[1];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user