mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +00:00
* Added ability to run Dockerfile.SUFFIX ContainerAction * Extracted IsDockerFile method * reformatted, moved from index to Last() * extracted IsDockerfile to DockerUtil with L0 * added check for IsDockerfile to account for docker:// * updated test to clearly show path/dockerfile:tag * fail if Data.Image is not Dockerfile or docker://[image] * Setup noops for JobPrepare and JobCleanup hooks * Add container jobstarted and jobcomplete hooks * Run 'index.js' instead of specific command hooks * Call jobprepare with command arg * Use right command name (hardcoded) Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com> * Invoke hooks with arguments * Add PrepareJob hook to work with jobcontainers Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com> * Rename methods * Use new hookcontainer to run prep and clean hooks * Get path from ENV * Use enums * Use IOUtils.cs * Move container files to folder * Move namespaces * Store "state" between hooks * Remove stdin stream in containerstephosts * Update Constants.cs * Throw if stdin fails * Cleanup obvious nullrefs and unused vars * Cleanup containerhook directory * Call step exec hook * Fix windows build * Remove hook from hookContainer * Rename file * More renamings * Add TODOs * Fix env name * Fix missing imports * Fix imports * Run script step on jobcontainer * Enable feature if env is set * Update ContainerHookManager.cs * Update ContainerHookManager.cs * Hooks allowed to work even when context isn't returned * Custom hooks enabled flag and additional null checks * New line at the end of the FeatureFlagManager.cs * Code refactoring * Supported just in time container building or pulling * Try mock-build for osx * Build all platforms * Run mock on self-hosted * Remove GITHUB prefix * Use ContainerHooksPath instead of CustomHooksPath * Null checks simplified * Code refactoring * Changing condition for image builing/pulling * Code refactoring * TODO comment removed Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Call container step if FF is on * Rename run script function * Use JToken instead of dynamic * Add TODO * Small refactoring + renames + TODOs * Throw on DetermineNodeRuntimeVersion * Fix formatting * Add run-container-step * Supported nodeJS in Alpine containers * Renamed Alpine to IsAlpine in HookResponse * Method for checking platform for alpine container * Added container hooks feature flag check * Update IsHookFeatureEnabled with new params * Rename featureflag method * Finish rename * Set collection null values to empty arrays when JSON serialising them * Disable FF until we merge * Update src/Runner.Worker/Container/ContainerHooks/HookContainer.cs * Fix method name * Change hookargs to superclass from interface * Using only Path.Combine in GenerateResponsePath * fix merge error * EntryPointArgs changed to list of args instead of one args string * Changed List to IEnumerable for EntryPointArgs and MountVolumes * Get ContainerRuntimePath for JobContainers from hooks * Read ContainerEnv from response file * Port mappings saved after creating services * Support case when responseFile doesn't exist * Check if response file exists * Logging in ExecuteHookScript * Save hook state after all 4 hooks * Code refactoring * Remove TODO Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Remove second TODO Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Removing container env changes * Removing containerEnv and dockerManager * Delete mock-build.yml * Update IOUtil.cs * Add comment about containerhooks * Fix merge mistake * Remove solved todo * Determine which shell to use for hooks scenario * Overload for method ExecuteHookScript with prependPath as arg * Adding HostContext to the GetDefaultShellForScript call * prependPath as a mandatory parameter * Improve logging for hooks * Small changes in logging * Allow null for ContainerEntryPointArgs * Changed log messages * Skip setting EntryPoint and EntryPointArgs if hooks are enabled * Throw if IsAlpine is null in PrepareJob * Code refactoring - added GetAndValidateResponse method * Code refactoring * Changes in exception message * Only save hookState if returned * Use FF from server * Empty line * Code refactoring Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Send null instead of string empty * Remove TODO * Code refactoring and some small changes * Allow Globals to be null to pass L0 * Fix setup in StepHostL0 * Throw exception earlier if response file doesn't exist and prepare_job hook is running * Refactoring GetResponse method * Changing exception message if response file is not found Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Chaning exception message if isAlpine is null for prepare_job hook * Rename hook folder * Fail if compatible hookfile not found * Use .Value instead of casting bool? to bool * Format spacing * Formatting * User user and system mvs * Use variables instead of entire context in featuremanager * Update stepTelemetry if step uses containerhooks * Restore import * Remove unneccessary field from HookContainer * Refactor response context and portmappings * Force allow hooks if FF is on * Code refactoring * Revert deleting usings * Better hookContainer defaults and use correct portmapping list * Make GetDefaultShellForScript a HostContext extension method * Generic hookresponse * Code refactoring, unnecessary properties removed - HookContainer moved to the HookInput.cs * Remove empty line * Code refactoring and better exception handling * code refactor, removing unnecessary props * Move hookstate to global ContainerHookState * Trace exception before we throw it for not losing information * Fix for null ref exception in GetResponse * Adding additional check for null response in prepareJob hook * Refactoring GetResponse with additional check * Update error messages * Ports in ResponseContainer changed from IList to IDictionary * Fix port format * Include dockerfile * Send null Registry obj if there's nothing in it * Minor formatting * Check if hookIndexPath exists relocated to the ContainerHookManager * Code refactoring - ValidateHookExecutable added to the ContainerHookManager * check if ContainerHooksPath when AllowRunnerContainerHooks is on * Submit JSON telemetry instead of boolean * Prefix step hooks with "run" * Rename FeatureManager * Fix flipped condition * Unify js shell path getter with ps1 and sh getter * Validate on run, not on instantiation of manager * Cleanup ExecuteAsync methods * Handle exception in executeHookScript * Better exception types * Remove comment * Simplify boolean * Allow jobs without jobContainer to run * Use JObject instead of JToken * Use correct Response type * Format class to move cleanupJobAsync to the end of public methods * Rename HookIndexPath to HookScriptPath * Refactor methods into expression bodies * Fix args class hierarchy * Fix argument order * Formatting * Fix nullref and don't swallow stacktrace * Whilelist HookArgs * Use FF in FeatureManager * Update src/Runner.Worker/ContainerOperationProvider.cs Co-authored-by: Tingluo Huang <tingluohuang@github.com> * Update src/Runner.Worker/ActionRunner.cs Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com> * Update src/Runner.Worker/ActionRunner.cs Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com> * Only mount well known dirs to job containers * Get trace from hostcontext * Use hook execution for setting telemetry Co-authored-by: Nikola Jokic <nikola.jokic@akvelon.com> Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com> Co-authored-by: Nikola Jokic <97525037+nikola-jokic@users.noreply.github.com> Co-authored-by: Stefan Ruvceski <stefan.ruvceski@akvelon.com> Co-authored-by: ruvceskistefan <96768603+ruvceskistefan@users.noreply.github.com> Co-authored-by: Thomas Boop <thboop@github.com> Co-authored-by: stefanruvceski <ruvceskistefan@github.com> Co-authored-by: Tingluo Huang <tingluohuang@github.com> Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
558 lines
28 KiB
C#
558 lines
28 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using GitHub.Runner.Worker.Container;
|
|
using GitHub.Services.Common;
|
|
using GitHub.Runner.Common;
|
|
using GitHub.Runner.Sdk;
|
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
|
using GitHub.Runner.Worker.Container.ContainerHooks;
|
|
#if OS_WINDOWS // keep win specific imports around even through we don't support containers on win at the moment
|
|
using System.ServiceProcess;
|
|
using Microsoft.Win32;
|
|
#endif
|
|
|
|
namespace GitHub.Runner.Worker
|
|
{
|
|
[ServiceLocator(Default = typeof(ContainerOperationProvider))]
|
|
public interface IContainerOperationProvider : IRunnerService
|
|
{
|
|
Task StartContainersAsync(IExecutionContext executionContext, object data);
|
|
Task StopContainersAsync(IExecutionContext executionContext, object data);
|
|
}
|
|
|
|
public class ContainerOperationProvider : RunnerService, IContainerOperationProvider
|
|
{
|
|
private IDockerCommandManager _dockerManager;
|
|
private IContainerHookManager _containerHookManager;
|
|
|
|
public override void Initialize(IHostContext hostContext)
|
|
{
|
|
base.Initialize(hostContext);
|
|
_dockerManager = HostContext.GetService<IDockerCommandManager>();
|
|
_containerHookManager = HostContext.GetService<IContainerHookManager>();
|
|
}
|
|
|
|
public async Task StartContainersAsync(IExecutionContext executionContext, object data)
|
|
{
|
|
Trace.Entering();
|
|
if (!Constants.Runner.Platform.Equals(Constants.OSPlatform.Linux))
|
|
{
|
|
throw new NotSupportedException("Container operations are only supported on Linux runners");
|
|
}
|
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
List<ContainerInfo> containers = data as List<ContainerInfo>;
|
|
ArgUtil.NotNull(containers, nameof(containers));
|
|
|
|
var postJobStep = new JobExtensionRunner(runAsync: this.StopContainersAsync,
|
|
condition: $"{PipelineTemplateConstants.Always}()",
|
|
displayName: "Stop containers",
|
|
data: data);
|
|
|
|
executionContext.Debug($"Register post job cleanup for stopping/deleting containers.");
|
|
executionContext.RegisterPostJobStep(postJobStep);
|
|
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
|
{
|
|
// Initialize the containers
|
|
containers.ForEach(container => UpdateRegistryAuthForGitHubToken(executionContext, container));
|
|
containers.Where(container => container.IsJobContainer).ForEach(container => MountWellKnownDirectories(executionContext, container));
|
|
await _containerHookManager.PrepareJobAsync(executionContext, containers);
|
|
return;
|
|
}
|
|
await AssertCompatibleOS(executionContext);
|
|
|
|
// Clean up containers left by previous runs
|
|
executionContext.Output("##[group]Clean up resources from previous jobs");
|
|
var staleContainers = await _dockerManager.DockerPS(executionContext, $"--all --quiet --no-trunc --filter \"label={_dockerManager.DockerInstanceLabel}\"");
|
|
foreach (var staleContainer in staleContainers)
|
|
{
|
|
int containerRemoveExitCode = await _dockerManager.DockerRemove(executionContext, staleContainer);
|
|
if (containerRemoveExitCode != 0)
|
|
{
|
|
executionContext.Warning($"Delete stale containers failed, docker rm fail with exit code {containerRemoveExitCode} for container {staleContainer}");
|
|
}
|
|
}
|
|
|
|
int networkPruneExitCode = await _dockerManager.DockerNetworkPrune(executionContext);
|
|
if (networkPruneExitCode != 0)
|
|
{
|
|
executionContext.Warning($"Delete stale container networks failed, docker network prune fail with exit code {networkPruneExitCode}");
|
|
}
|
|
executionContext.Output("##[endgroup]");
|
|
|
|
// Create local docker network for this job to avoid port conflict when multiple runners run on same machine.
|
|
// All containers within a job join the same network
|
|
executionContext.Output("##[group]Create local container network");
|
|
var containerNetwork = $"github_network_{Guid.NewGuid().ToString("N")}";
|
|
await CreateContainerNetworkAsync(executionContext, containerNetwork);
|
|
executionContext.JobContext.Container["network"] = new StringContextData(containerNetwork);
|
|
executionContext.Output("##[endgroup]");
|
|
|
|
foreach (var container in containers)
|
|
{
|
|
container.ContainerNetwork = containerNetwork;
|
|
await StartContainerAsync(executionContext, container);
|
|
}
|
|
|
|
executionContext.Output("##[group]Waiting for all services to be ready");
|
|
foreach (var container in containers.Where(c => !c.IsJobContainer))
|
|
{
|
|
await ContainerHealthcheck(executionContext, container);
|
|
}
|
|
executionContext.Output("##[endgroup]");
|
|
}
|
|
|
|
public async Task StopContainersAsync(IExecutionContext executionContext, object data)
|
|
{
|
|
Trace.Entering();
|
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
|
|
List<ContainerInfo> containers = data as List<ContainerInfo>;
|
|
ArgUtil.NotNull(containers, nameof(containers));
|
|
|
|
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
|
{
|
|
await _containerHookManager.CleanupJobAsync(executionContext, containers);
|
|
return;
|
|
}
|
|
|
|
foreach (var container in containers)
|
|
{
|
|
await StopContainerAsync(executionContext, container);
|
|
}
|
|
// Remove the container network
|
|
await RemoveContainerNetworkAsync(executionContext, containers.First().ContainerNetwork);
|
|
}
|
|
|
|
private async Task StartContainerAsync(IExecutionContext executionContext, ContainerInfo container)
|
|
{
|
|
Trace.Entering();
|
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
ArgUtil.NotNull(container, nameof(container));
|
|
ArgUtil.NotNullOrEmpty(container.ContainerImage, nameof(container.ContainerImage));
|
|
|
|
Trace.Info($"Container name: {container.ContainerName}");
|
|
Trace.Info($"Container image: {container.ContainerImage}");
|
|
Trace.Info($"Container options: {container.ContainerCreateOptions}");
|
|
|
|
var groupName = container.IsJobContainer ? "Starting job container" : $"Starting {container.ContainerNetworkAlias} service container";
|
|
executionContext.Output($"##[group]{groupName}");
|
|
|
|
foreach (var port in container.UserPortMappings)
|
|
{
|
|
Trace.Info($"User provided port: {port.Value}");
|
|
}
|
|
foreach (var mount in container.UserMountVolumes)
|
|
{
|
|
Trace.Info($"User provided volume: {mount.UserProvidedValue}");
|
|
if (string.Equals(mount.SourceVolumePath, "/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
executionContext.Warning($"Volume mount {mount.UserProvidedValue} is going to mount '/' into the container which may cause file ownership change in the entire file system and cause Actions Runner to lose permission to access the disk.");
|
|
}
|
|
}
|
|
|
|
UpdateRegistryAuthForGitHubToken(executionContext, container);
|
|
|
|
// Before pulling, generate client authentication if required
|
|
var configLocation = await ContainerRegistryLogin(executionContext, container);
|
|
|
|
// Pull down docker image with retry up to 3 times
|
|
int retryCount = 0;
|
|
int pullExitCode = 0;
|
|
while (retryCount < 3)
|
|
{
|
|
pullExitCode = await _dockerManager.DockerPull(executionContext, container.ContainerImage, configLocation);
|
|
if (pullExitCode == 0)
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
retryCount++;
|
|
if (retryCount < 3)
|
|
{
|
|
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
|
|
executionContext.Warning($"Docker pull failed with exit code {pullExitCode}, back off {backOff.TotalSeconds} seconds before retry.");
|
|
await Task.Delay(backOff);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove credentials after pulling
|
|
ContainerRegistryLogout(configLocation);
|
|
|
|
if (retryCount == 3 && pullExitCode != 0)
|
|
{
|
|
throw new InvalidOperationException($"Docker pull failed with exit code {pullExitCode}");
|
|
}
|
|
|
|
if (container.IsJobContainer)
|
|
{
|
|
MountWellKnownDirectories(executionContext, container);
|
|
}
|
|
|
|
container.ContainerId = await _dockerManager.DockerCreate(executionContext, container);
|
|
ArgUtil.NotNullOrEmpty(container.ContainerId, nameof(container.ContainerId));
|
|
|
|
// Start container
|
|
int startExitCode = await _dockerManager.DockerStart(executionContext, container.ContainerId);
|
|
if (startExitCode != 0)
|
|
{
|
|
throw new InvalidOperationException($"Docker start fail with exit code {startExitCode}");
|
|
}
|
|
|
|
try
|
|
{
|
|
// Make sure container is up and running
|
|
var psOutputs = await _dockerManager.DockerPS(executionContext, $"--all --filter id={container.ContainerId} --filter status=running --no-trunc --format \"{{{{.ID}}}} {{{{.Status}}}}\"");
|
|
if (psOutputs.FirstOrDefault(x => !string.IsNullOrEmpty(x))?.StartsWith(container.ContainerId) != true)
|
|
{
|
|
// container is not up and running, pull docker log for this container.
|
|
await _dockerManager.DockerPS(executionContext, $"--all --filter id={container.ContainerId} --no-trunc --format \"{{{{.ID}}}} {{{{.Status}}}}\"");
|
|
int logsExitCode = await _dockerManager.DockerLogs(executionContext, container.ContainerId);
|
|
if (logsExitCode != 0)
|
|
{
|
|
executionContext.Warning($"Docker logs fail with exit code {logsExitCode}");
|
|
}
|
|
|
|
executionContext.Warning($"Docker container {container.ContainerId} is not in running state.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// pull container log is best effort.
|
|
Trace.Error("Catch exception when check container log and container status.");
|
|
Trace.Error(ex);
|
|
}
|
|
|
|
// Gather runtime container information
|
|
if (!container.IsJobContainer)
|
|
{
|
|
var service = new DictionaryContextData()
|
|
{
|
|
["id"] = new StringContextData(container.ContainerId),
|
|
["ports"] = new DictionaryContextData(),
|
|
["network"] = new StringContextData(container.ContainerNetwork)
|
|
};
|
|
container.AddPortMappings(await _dockerManager.DockerPort(executionContext, container.ContainerId));
|
|
foreach (var port in container.PortMappings)
|
|
{
|
|
(service["ports"] as DictionaryContextData)[port.ContainerPort] = new StringContextData(port.HostPort);
|
|
}
|
|
executionContext.JobContext.Services[container.ContainerNetworkAlias] = service;
|
|
}
|
|
else
|
|
{
|
|
var configEnvFormat = "--format \"{{range .Config.Env}}{{println .}}{{end}}\"";
|
|
var containerEnv = await _dockerManager.DockerInspect(executionContext, container.ContainerId, configEnvFormat);
|
|
container.ContainerRuntimePath = DockerUtil.ParsePathFromConfigEnv(containerEnv);
|
|
executionContext.JobContext.Container["id"] = new StringContextData(container.ContainerId);
|
|
}
|
|
executionContext.Output("##[endgroup]");
|
|
}
|
|
|
|
private void MountWellKnownDirectories(IExecutionContext executionContext, ContainerInfo container)
|
|
{
|
|
// Configure job container - Mount workspace and tools, set up environment, and start long running process
|
|
var githubContext = executionContext.ExpressionValues["github"] as GitHubContext;
|
|
ArgUtil.NotNull(githubContext, nameof(githubContext));
|
|
var workingDirectory = githubContext["workspace"] as StringContextData;
|
|
ArgUtil.NotNullOrEmpty(workingDirectory, nameof(workingDirectory));
|
|
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Work), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Work))));
|
|
#if OS_WINDOWS
|
|
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Externals), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Externals))));
|
|
#else
|
|
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Externals), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Externals)), true));
|
|
#endif
|
|
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Temp), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Temp))));
|
|
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Actions), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Actions))));
|
|
container.MountVolumes.Add(new MountVolume(HostContext.GetDirectory(WellKnownDirectory.Tools), container.TranslateToContainerPath(HostContext.GetDirectory(WellKnownDirectory.Tools))));
|
|
|
|
var tempHomeDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), "_github_home");
|
|
Directory.CreateDirectory(tempHomeDirectory);
|
|
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
|
|
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
|
|
container.ContainerEnvironmentVariables["HOME"] = container.TranslateToContainerPath(tempHomeDirectory);
|
|
|
|
var tempWorkflowDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), "_github_workflow");
|
|
Directory.CreateDirectory(tempWorkflowDirectory);
|
|
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
|
|
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
|
|
|
|
container.ContainerWorkDirectory = container.TranslateToContainerPath(workingDirectory);
|
|
if (!FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
|
{
|
|
container.ContainerEntryPoint = "tail";
|
|
container.ContainerEntryPointArgs = "\"-f\" \"/dev/null\"";
|
|
}
|
|
}
|
|
|
|
private async Task StopContainerAsync(IExecutionContext executionContext, ContainerInfo container)
|
|
{
|
|
Trace.Entering();
|
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
ArgUtil.NotNull(container, nameof(container));
|
|
|
|
if (!string.IsNullOrEmpty(container.ContainerId))
|
|
{
|
|
if (!container.IsJobContainer)
|
|
{
|
|
// Print logs for service container jobs (not the "action" job itself b/c that's already logged).
|
|
executionContext.Output($"Print service container logs: {container.ContainerDisplayName}");
|
|
|
|
int logsExitCode = await _dockerManager.DockerLogs(executionContext, container.ContainerId);
|
|
if (logsExitCode != 0)
|
|
{
|
|
executionContext.Warning($"Docker logs fail with exit code {logsExitCode}");
|
|
}
|
|
}
|
|
|
|
executionContext.Output($"Stop and remove container: {container.ContainerDisplayName}");
|
|
|
|
int rmExitCode = await _dockerManager.DockerRemove(executionContext, container.ContainerId);
|
|
if (rmExitCode != 0)
|
|
{
|
|
executionContext.Warning($"Docker rm fail with exit code {rmExitCode}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#if !OS_WINDOWS
|
|
private async Task<List<string>> ExecuteCommandAsync(IExecutionContext context, string command, string arg)
|
|
{
|
|
context.Command($"{command} {arg}");
|
|
|
|
List<string> outputs = new List<string>();
|
|
object outputLock = new object();
|
|
var processInvoker = HostContext.CreateService<IProcessInvoker>();
|
|
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
|
{
|
|
if (!string.IsNullOrEmpty(message.Data))
|
|
{
|
|
lock (outputLock)
|
|
{
|
|
outputs.Add(message.Data);
|
|
}
|
|
}
|
|
};
|
|
|
|
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
|
{
|
|
if (!string.IsNullOrEmpty(message.Data))
|
|
{
|
|
lock (outputLock)
|
|
{
|
|
outputs.Add(message.Data);
|
|
}
|
|
}
|
|
};
|
|
|
|
await processInvoker.ExecuteAsync(
|
|
workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work),
|
|
fileName: command,
|
|
arguments: arg,
|
|
environment: null,
|
|
requireExitCodeZero: true,
|
|
outputEncoding: null,
|
|
cancellationToken: CancellationToken.None);
|
|
|
|
foreach (var outputLine in outputs)
|
|
{
|
|
context.Output(outputLine);
|
|
}
|
|
|
|
return outputs;
|
|
}
|
|
#endif
|
|
|
|
private async Task CreateContainerNetworkAsync(IExecutionContext executionContext, string network)
|
|
{
|
|
Trace.Entering();
|
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
int networkExitCode = await _dockerManager.DockerNetworkCreate(executionContext, network);
|
|
if (networkExitCode != 0)
|
|
{
|
|
throw new InvalidOperationException($"Docker network create failed with exit code {networkExitCode}");
|
|
}
|
|
}
|
|
|
|
private async Task RemoveContainerNetworkAsync(IExecutionContext executionContext, string network)
|
|
{
|
|
Trace.Entering();
|
|
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
|
ArgUtil.NotNull(network, nameof(network));
|
|
|
|
executionContext.Output($"Remove container network: {network}");
|
|
|
|
int removeExitCode = await _dockerManager.DockerNetworkRemove(executionContext, network);
|
|
if (removeExitCode != 0)
|
|
{
|
|
executionContext.Warning($"Docker network rm failed with exit code {removeExitCode}");
|
|
}
|
|
}
|
|
|
|
private async Task ContainerHealthcheck(IExecutionContext executionContext, ContainerInfo container)
|
|
{
|
|
string healthCheck = "--format=\"{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}\"";
|
|
string serviceHealth = (await _dockerManager.DockerInspect(context: executionContext, dockerObject: container.ContainerId, options: healthCheck)).FirstOrDefault();
|
|
if (string.IsNullOrEmpty(serviceHealth))
|
|
{
|
|
// Container has no HEALTHCHECK
|
|
return;
|
|
}
|
|
var retryCount = 0;
|
|
while (string.Equals(serviceHealth, "starting", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
TimeSpan backoff = BackoffTimerHelper.GetExponentialBackoff(retryCount, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(32), TimeSpan.FromSeconds(2));
|
|
executionContext.Output($"{container.ContainerNetworkAlias} service is starting, waiting {backoff.Seconds} seconds before checking again.");
|
|
await Task.Delay(backoff, executionContext.CancellationToken);
|
|
serviceHealth = (await _dockerManager.DockerInspect(context: executionContext, dockerObject: container.ContainerId, options: healthCheck)).FirstOrDefault();
|
|
retryCount++;
|
|
}
|
|
if (string.Equals(serviceHealth, "healthy", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
executionContext.Output($"{container.ContainerNetworkAlias} service is healthy.");
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException($"Failed to initialize, {container.ContainerNetworkAlias} service is {serviceHealth}.");
|
|
}
|
|
}
|
|
|
|
private async Task<string> ContainerRegistryLogin(IExecutionContext executionContext, ContainerInfo container)
|
|
{
|
|
if (string.IsNullOrEmpty(container.RegistryAuthUsername) || string.IsNullOrEmpty(container.RegistryAuthPassword))
|
|
{
|
|
// No valid client config can be generated
|
|
return "";
|
|
}
|
|
var configLocation = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), $".docker_{Guid.NewGuid()}");
|
|
try
|
|
{
|
|
var dirInfo = Directory.CreateDirectory(configLocation);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new InvalidOperationException($"Failed to create directory to store registry client credentials: {e.Message}");
|
|
}
|
|
var loginExitCode = await _dockerManager.DockerLogin(
|
|
executionContext,
|
|
configLocation,
|
|
container.RegistryServer,
|
|
container.RegistryAuthUsername,
|
|
container.RegistryAuthPassword);
|
|
|
|
if (loginExitCode != 0)
|
|
{
|
|
throw new InvalidOperationException($"Docker login for '{container.RegistryServer}' failed with exit code {loginExitCode}");
|
|
}
|
|
return configLocation;
|
|
}
|
|
|
|
private void ContainerRegistryLogout(string configLocation)
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(configLocation) && Directory.Exists(configLocation))
|
|
{
|
|
Directory.Delete(configLocation, recursive: true);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new InvalidOperationException($"Failed to remove directory containing Docker client credentials: {e.Message}");
|
|
}
|
|
}
|
|
|
|
private void UpdateRegistryAuthForGitHubToken(IExecutionContext executionContext, ContainerInfo container)
|
|
{
|
|
var registryIsTokenCompatible = container.RegistryServer.Equals("ghcr.io", StringComparison.OrdinalIgnoreCase) || container.RegistryServer.Equals("containers.pkg.github.com", StringComparison.OrdinalIgnoreCase);
|
|
var isFallbackTokenFromHostedGithub = HostContext.GetService<IConfigurationStore>().GetSettings().IsHostedServer;
|
|
if (!registryIsTokenCompatible || !isFallbackTokenFromHostedGithub)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var registryCredentialsNotSupplied = string.IsNullOrEmpty(container.RegistryAuthUsername) && string.IsNullOrEmpty(container.RegistryAuthPassword);
|
|
if (registryCredentialsNotSupplied)
|
|
{
|
|
container.RegistryAuthUsername = executionContext.GetGitHubContext("actor");
|
|
container.RegistryAuthPassword = executionContext.GetGitHubContext("token");
|
|
}
|
|
}
|
|
|
|
private async Task AssertCompatibleOS(IExecutionContext executionContext)
|
|
{
|
|
// Check whether we are inside a container.
|
|
// Our container feature requires to map working directory from host to the container.
|
|
// If we are already inside a container, we will not able to find out the real working direcotry path on the host.
|
|
#if OS_WINDOWS
|
|
#pragma warning disable CA1416
|
|
// service CExecSvc is Container Execution Agent.
|
|
ServiceController[] scServices = ServiceController.GetServices();
|
|
if (scServices.Any(x => String.Equals(x.ServiceName, "cexecsvc", StringComparison.OrdinalIgnoreCase) && x.Status == ServiceControllerStatus.Running))
|
|
{
|
|
throw new NotSupportedException("Container feature is not supported when runner is already running inside container.");
|
|
}
|
|
#pragma warning restore CA1416
|
|
#else
|
|
var initProcessCgroup = File.ReadLines("/proc/1/cgroup");
|
|
if (initProcessCgroup.Any(x => x.IndexOf(":/docker/", StringComparison.OrdinalIgnoreCase) >= 0))
|
|
{
|
|
throw new NotSupportedException("Container feature is not supported when runner is already running inside container.");
|
|
}
|
|
#endif
|
|
|
|
#if OS_WINDOWS
|
|
#pragma warning disable CA1416
|
|
// Check OS version (Windows server 1803 is required)
|
|
object windowsInstallationType = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "InstallationType", defaultValue: null);
|
|
ArgUtil.NotNull(windowsInstallationType, nameof(windowsInstallationType));
|
|
object windowsReleaseId = Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "ReleaseId", defaultValue: null);
|
|
ArgUtil.NotNull(windowsReleaseId, nameof(windowsReleaseId));
|
|
executionContext.Debug($"Current Windows version: '{windowsReleaseId} ({windowsInstallationType})'");
|
|
|
|
if (int.TryParse(windowsReleaseId.ToString(), out int releaseId))
|
|
{
|
|
if (!windowsInstallationType.ToString().StartsWith("Server", StringComparison.OrdinalIgnoreCase) || releaseId < 1803)
|
|
{
|
|
throw new NotSupportedException("Container feature requires Windows Server 1803 or higher.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentOutOfRangeException(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ReleaseId");
|
|
}
|
|
#pragma warning restore CA1416
|
|
#endif
|
|
|
|
// Check docker client/server version
|
|
executionContext.Output("##[group]Checking docker version");
|
|
DockerVersion dockerVersion = await _dockerManager.DockerVersion(executionContext);
|
|
executionContext.Output("##[endgroup]");
|
|
|
|
ArgUtil.NotNull(dockerVersion.ServerVersion, nameof(dockerVersion.ServerVersion));
|
|
ArgUtil.NotNull(dockerVersion.ClientVersion, nameof(dockerVersion.ClientVersion));
|
|
|
|
#if OS_WINDOWS
|
|
Version requiredDockerEngineAPIVersion = new Version(1, 30); // Docker-EE version 17.6
|
|
#else
|
|
Version requiredDockerEngineAPIVersion = new Version(1, 35); // Docker-CE version 17.12
|
|
#endif
|
|
|
|
if (dockerVersion.ServerVersion < requiredDockerEngineAPIVersion)
|
|
{
|
|
throw new NotSupportedException($"Min required docker engine API server version is '{requiredDockerEngineAPIVersion}', your docker ('{_dockerManager.DockerPath}') server version is '{dockerVersion.ServerVersion}'");
|
|
}
|
|
if (dockerVersion.ClientVersion < requiredDockerEngineAPIVersion)
|
|
{
|
|
throw new NotSupportedException($"Min required docker engine API client version is '{requiredDockerEngineAPIVersion}', your docker ('{_dockerManager.DockerPath}') client version is '{dockerVersion.ClientVersion}'");
|
|
}
|
|
}
|
|
}
|
|
}
|