mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +00:00
Runner container hooks Beta (#1853)
* 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>
This commit is contained in:
@@ -151,6 +151,7 @@ namespace GitHub.Runner.Common
|
||||
public static readonly string DiskSpaceWarning = "runner.diskspace.warning";
|
||||
public static readonly string Node12Warning = "DistributedTask.AddWarningToNode12Action";
|
||||
public static readonly string UseContainerPathForTemplate = "DistributedTask.UseContainerPathForTemplate";
|
||||
public static readonly string AllowRunnerContainerHooks = "DistributedTask.AllowRunnerContainerHooks";
|
||||
}
|
||||
|
||||
public static readonly string InternalTelemetryIssueDataKey = "_internal_telemetry";
|
||||
@@ -196,6 +197,7 @@ namespace GitHub.Runner.Common
|
||||
{
|
||||
public static readonly string JobStartedStepName = "Set up runner";
|
||||
public static readonly string JobCompletedStepName = "Complete runner";
|
||||
public static readonly string ContainerHooksPath = "ACTIONS_RUNNER_CONTAINER_HOOKS";
|
||||
}
|
||||
|
||||
public static class Path
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Runtime.Loader;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Logging;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
@@ -641,6 +642,31 @@ namespace GitHub.Runner.Common
|
||||
var handlerFactory = context.GetService<IHttpClientHandlerFactory>();
|
||||
return handlerFactory.CreateClientHandler(context.WebProxy);
|
||||
}
|
||||
|
||||
public static string GetDefaultShellForScript(this IHostContext hostContext, string path, string prependPath)
|
||||
{
|
||||
var trace = hostContext.GetTrace(nameof(GetDefaultShellForScript));
|
||||
switch (Path.GetExtension(path))
|
||||
{
|
||||
case ".sh":
|
||||
// use 'sh' args but prefer bash
|
||||
if (WhichUtil.Which("bash", false, trace, prependPath) != null)
|
||||
{
|
||||
return "bash";
|
||||
}
|
||||
return "sh";
|
||||
case ".ps1":
|
||||
if (WhichUtil.Which("pwsh", false, trace, prependPath) != null)
|
||||
{
|
||||
return "pwsh";
|
||||
}
|
||||
return "powershell";
|
||||
case ".js":
|
||||
return Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}") + " {0}";
|
||||
default:
|
||||
throw new ArgumentException($"{path} is not a valid path to a script. Make sure it ends in '.sh', '.ps1' or '.js'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ShutdownReason
|
||||
|
||||
@@ -424,6 +424,12 @@ namespace GitHub.Runner.Sdk
|
||||
throw new NotSupportedException($"Unable to validate execute permissions for directory '{directory}'. Exceeded maximum iterations.");
|
||||
}
|
||||
|
||||
public static void CreateEmptyFile(string path)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
File.WriteAllText(path, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively enumerates a directory without following directory reparse points.
|
||||
/// </summary>
|
||||
|
||||
@@ -101,6 +101,8 @@ namespace GitHub.Runner.Worker
|
||||
IEnumerable<Pipelines.ActionStep> actions = steps.OfType<Pipelines.ActionStep>();
|
||||
executionContext.Output("Prepare all required actions");
|
||||
var result = await PrepareActionsRecursiveAsync(executionContext, state, actions, depth, rootStepId);
|
||||
if (!FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
||||
{
|
||||
if (state.ImagesToPull.Count > 0)
|
||||
{
|
||||
foreach (var imageToPull in result.ImagesToPull)
|
||||
@@ -133,6 +135,7 @@ namespace GitHub.Runner.Worker
|
||||
containerSetupSteps.Clear();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return new PrepareResult(containerSetupSteps, result.PreStepTracker);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,8 +158,12 @@ namespace GitHub.Runner.Worker
|
||||
// Setup container stephost for running inside the container.
|
||||
if (ExecutionContext.Global.Container != null)
|
||||
{
|
||||
// Make sure required container is already created.
|
||||
// Make sure the required container is already created
|
||||
// Container hooks do not necessarily set 'ContainerId'
|
||||
if (!FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
|
||||
{
|
||||
ArgUtil.NotNullOrEmpty(ExecutionContext.Global.Container.ContainerId, nameof(ExecutionContext.Global.Container.ContainerId));
|
||||
}
|
||||
var containerStepHost = HostContext.CreateService<IContainerStepHost>();
|
||||
containerStepHost.Container = ExecutionContext.Global.Container;
|
||||
stepHost = containerStepHost;
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
using GitHub.Services.WebApi;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container.ContainerHooks
|
||||
{
|
||||
[ServiceLocator(Default = typeof(ContainerHookManager))]
|
||||
public interface IContainerHookManager : IRunnerService
|
||||
{
|
||||
Task PrepareJobAsync(IExecutionContext context, List<ContainerInfo> containers);
|
||||
Task RunContainerStepAsync(IExecutionContext context, ContainerInfo container, string dockerFile);
|
||||
Task RunScriptStepAsync(IExecutionContext context, ContainerInfo container, string workingDirectory, string fileName, string arguments, IDictionary<string, string> environment, string prependPath);
|
||||
Task CleanupJobAsync(IExecutionContext context, List<ContainerInfo> containers);
|
||||
string GetContainerHookData();
|
||||
}
|
||||
|
||||
public class ContainerHookManager : RunnerService, IContainerHookManager
|
||||
{
|
||||
private const string ResponseFolderName = "_runner_hook_responses";
|
||||
private string HookScriptPath;
|
||||
|
||||
public override void Initialize(IHostContext hostContext)
|
||||
{
|
||||
base.Initialize(hostContext);
|
||||
HookScriptPath = $"{Environment.GetEnvironmentVariable(Constants.Hooks.ContainerHooksPath)}";
|
||||
}
|
||||
|
||||
public async Task PrepareJobAsync(IExecutionContext context, List<ContainerInfo> containers)
|
||||
{
|
||||
Trace.Entering();
|
||||
var jobContainer = containers.Where(c => c.IsJobContainer).SingleOrDefault();
|
||||
var serviceContainers = containers.Where(c => !c.IsJobContainer).ToList();
|
||||
|
||||
var input = new HookInput
|
||||
{
|
||||
Command = HookCommand.PrepareJob,
|
||||
ResponseFile = GenerateResponsePath(),
|
||||
Args = new PrepareJobArgs
|
||||
{
|
||||
Container = jobContainer?.GetHookContainer(),
|
||||
Services = serviceContainers.Select(c => c.GetHookContainer()).ToList(),
|
||||
}
|
||||
};
|
||||
|
||||
var prependPath = GetPrependPath(context);
|
||||
var response = await ExecuteHookScript<PrepareJobResponse>(context, input, ActionRunStage.Pre, prependPath);
|
||||
if (jobContainer != null)
|
||||
{
|
||||
jobContainer.IsAlpine = response.IsAlpine.Value;
|
||||
}
|
||||
SaveHookState(context, response.State, input);
|
||||
UpdateJobContext(context, jobContainer, serviceContainers, response);
|
||||
}
|
||||
|
||||
public async Task RunContainerStepAsync(IExecutionContext context, ContainerInfo container, string dockerFile)
|
||||
{
|
||||
Trace.Entering();
|
||||
var hookState = context.Global.ContainerHookState;
|
||||
var containerStepArgs = new ContainerStepArgs(container);
|
||||
if (!string.IsNullOrEmpty(dockerFile))
|
||||
{
|
||||
containerStepArgs.Dockerfile = dockerFile;
|
||||
containerStepArgs.Image = null;
|
||||
}
|
||||
var input = new HookInput
|
||||
{
|
||||
Args = containerStepArgs,
|
||||
Command = HookCommand.RunContainerStep,
|
||||
ResponseFile = GenerateResponsePath(),
|
||||
State = hookState
|
||||
};
|
||||
|
||||
var prependPath = GetPrependPath(context);
|
||||
var response = await ExecuteHookScript<HookResponse>(context, input, ActionRunStage.Pre, prependPath);
|
||||
if (response == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SaveHookState(context, response.State, input);
|
||||
}
|
||||
|
||||
public async Task RunScriptStepAsync(IExecutionContext context, ContainerInfo container, string workingDirectory, string entryPoint, string entryPointArgs, IDictionary<string, string> environmentVariables, string prependPath)
|
||||
{
|
||||
Trace.Entering();
|
||||
var input = new HookInput
|
||||
{
|
||||
Command = HookCommand.RunScriptStep,
|
||||
ResponseFile = GenerateResponsePath(),
|
||||
Args = new ScriptStepArgs
|
||||
{
|
||||
EntryPointArgs = entryPointArgs.Split(' ').Select(arg => arg.Trim()),
|
||||
EntryPoint = entryPoint,
|
||||
EnvironmentVariables = environmentVariables,
|
||||
PrependPath = prependPath,
|
||||
WorkingDirectory = workingDirectory,
|
||||
},
|
||||
State = context.Global.ContainerHookState
|
||||
};
|
||||
|
||||
var response = await ExecuteHookScript<HookResponse>(context, input, ActionRunStage.Pre, prependPath);
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SaveHookState(context, response.State, input);
|
||||
}
|
||||
|
||||
public async Task CleanupJobAsync(IExecutionContext context, List<ContainerInfo> containers)
|
||||
{
|
||||
Trace.Entering();
|
||||
var input = new HookInput
|
||||
{
|
||||
Command = HookCommand.CleanupJob,
|
||||
ResponseFile = GenerateResponsePath(),
|
||||
Args = new CleanupJobArgs(),
|
||||
State = context.Global.ContainerHookState
|
||||
};
|
||||
var prependPath = GetPrependPath(context);
|
||||
await ExecuteHookScript<HookResponse>(context, input, ActionRunStage.Pre, prependPath);
|
||||
}
|
||||
|
||||
public string GetContainerHookData()
|
||||
{
|
||||
return JsonUtility.ToString(new { HookScriptPath });
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteHookScript<T>(IExecutionContext context, HookInput input, ActionRunStage stage, string prependPath) where T : HookResponse
|
||||
{
|
||||
try
|
||||
{
|
||||
ValidateHookExecutable();
|
||||
context.StepTelemetry.ContainerHookData = GetContainerHookData();
|
||||
var scriptDirectory = Path.GetDirectoryName(HookScriptPath);
|
||||
var stepHost = HostContext.CreateService<IDefaultStepHost>();
|
||||
|
||||
Dictionary<string, string> inputs = new()
|
||||
{
|
||||
["standardInInput"] = JsonUtility.ToString(input),
|
||||
["path"] = HookScriptPath,
|
||||
["shell"] = HostContext.GetDefaultShellForScript(HookScriptPath, prependPath)
|
||||
};
|
||||
var handlerFactory = HostContext.GetService<IHandlerFactory>();
|
||||
var handler = handlerFactory.Create(
|
||||
context,
|
||||
null,
|
||||
stepHost,
|
||||
new ScriptActionExecutionData(),
|
||||
inputs,
|
||||
environment: new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
|
||||
context.Global.Variables,
|
||||
actionDirectory: scriptDirectory,
|
||||
localActionContainerSetupSteps: null) as ScriptHandler;
|
||||
handler.PrepareExecution(stage);
|
||||
|
||||
IOUtil.CreateEmptyFile(input.ResponseFile);
|
||||
await handler.RunAsync(stage);
|
||||
if (handler.ExecutionContext.Result == TaskResult.Failed)
|
||||
{
|
||||
throw new Exception($"The hook script at '{HookScriptPath}' running command '{input.Command}' did not execute successfully");
|
||||
}
|
||||
var response = GetResponse<T>(input);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Error(ex);
|
||||
throw new Exception($"Custom container implementation failed with error: {ex.Message} Please contact your self hosted runner administrator.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateResponsePath() => Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Temp), ResponseFolderName, $"{Guid.NewGuid()}.json");
|
||||
|
||||
private static string GetPrependPath(IExecutionContext context) => string.Join(Path.PathSeparator.ToString(), context.Global.PrependPath.Reverse<string>());
|
||||
|
||||
private void ValidateHookExecutable()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(HookScriptPath) && !File.Exists(HookScriptPath))
|
||||
{
|
||||
throw new FileNotFoundException($"File not found at '{HookScriptPath}'. Set {Constants.Hooks.ContainerHooksPath} to the path of an existing file.");
|
||||
}
|
||||
|
||||
var supportedHookExtensions = new string[] { ".js", ".sh", ".ps1" };
|
||||
if (!supportedHookExtensions.Any(extension => HookScriptPath.EndsWith(extension)))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Invalid file extension at '{HookScriptPath}'. {Constants.Hooks.ContainerHooksPath} must be a path to a file with one of the following extensions: {string.Join(", ", supportedHookExtensions)}");
|
||||
}
|
||||
}
|
||||
|
||||
private T GetResponse<T>(HookInput input) where T : HookResponse
|
||||
{
|
||||
if (!File.Exists(input.ResponseFile))
|
||||
{
|
||||
Trace.Info($"Response file for the hook script at '{HookScriptPath}' running command '{input.Command}' not found.");
|
||||
if (input.Args.IsRequireAlpineInResponse())
|
||||
{
|
||||
throw new Exception($"Response file is required but not found for the hook script at '{HookScriptPath}' running command '{input.Command}'");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
T response = IOUtil.LoadObject<T>(input.ResponseFile);
|
||||
Trace.Info($"Response file for the hook script at '{HookScriptPath}' running command '{input.Command}' was processed successfully");
|
||||
IOUtil.DeleteFile(input.ResponseFile);
|
||||
Trace.Info($"Response file for the hook script at '{HookScriptPath}' running command '{input.Command}' was deleted successfully");
|
||||
if (response == null && input.Args.IsRequireAlpineInResponse())
|
||||
{
|
||||
throw new Exception($"Response file could not be read at '{HookScriptPath}' running command '{input.Command}'");
|
||||
}
|
||||
response?.Validate(input);
|
||||
return response;
|
||||
}
|
||||
|
||||
private void SaveHookState(IExecutionContext context, JObject hookState, HookInput input)
|
||||
{
|
||||
if (hookState == null)
|
||||
{
|
||||
Trace.Info($"No 'state' property found in response file for '{input.Command}'. Global variable for 'ContainerHookState' will not be updated.");
|
||||
return;
|
||||
}
|
||||
context.Global.ContainerHookState = hookState;
|
||||
Trace.Info($"Global variable 'ContainerHookState' updated successfully for '{input.Command}' with data found in 'state' property of the response file.");
|
||||
}
|
||||
|
||||
private void UpdateJobContext(IExecutionContext context, ContainerInfo jobContainer, List<ContainerInfo> serviceContainers, PrepareJobResponse response)
|
||||
{
|
||||
if (response.Context == null)
|
||||
{
|
||||
Trace.Info($"The response file does not contain a context. The fields 'jobContext.Container' and 'jobContext.Services' will not be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
var containerId = response.Context.Container?.Id;
|
||||
if (containerId != null)
|
||||
{
|
||||
context.JobContext.Container["id"] = new StringContextData(containerId);
|
||||
jobContainer.ContainerId = containerId;
|
||||
}
|
||||
|
||||
var containerNetwork = response.Context.Container?.Network;
|
||||
if (containerNetwork != null)
|
||||
{
|
||||
context.JobContext.Container["network"] = new StringContextData(containerNetwork);
|
||||
jobContainer.ContainerNetwork = containerNetwork;
|
||||
}
|
||||
|
||||
for (var i = 0; i < response.Context.Services.Count; i++)
|
||||
{
|
||||
var responseContainerInfo = response.Context.Services[i];
|
||||
var globalContainerInfo = serviceContainers[i];
|
||||
globalContainerInfo.ContainerId = responseContainerInfo.Id;
|
||||
globalContainerInfo.ContainerNetwork = responseContainerInfo.Network;
|
||||
|
||||
var service = new DictionaryContextData()
|
||||
{
|
||||
["id"] = new StringContextData(responseContainerInfo.Id),
|
||||
["ports"] = new DictionaryContextData(),
|
||||
["network"] = new StringContextData(responseContainerInfo.Network)
|
||||
};
|
||||
|
||||
globalContainerInfo.AddPortMappings(responseContainerInfo.Ports);
|
||||
foreach (var portMapping in responseContainerInfo.Ports)
|
||||
{
|
||||
(service["ports"] as DictionaryContextData)[portMapping.Key] = new StringContextData(portMapping.Value);
|
||||
}
|
||||
context.JobContext.Services[globalContainerInfo.ContainerNetworkAlias] = service;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/Runner.Worker/Container/ContainerHooks/HookInput.cs
Normal file
113
src/Runner.Worker/Container/ContainerHooks/HookInput.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container.ContainerHooks
|
||||
{
|
||||
public class HookInput
|
||||
{
|
||||
public HookCommand Command { get; set; }
|
||||
public string ResponseFile { get; set; }
|
||||
public IHookArgs Args { get; set; }
|
||||
public JObject State { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum HookCommand
|
||||
{
|
||||
[EnumMember(Value = "prepare_job")]
|
||||
PrepareJob,
|
||||
[EnumMember(Value = "cleanup_job")]
|
||||
CleanupJob,
|
||||
[EnumMember(Value = "run_script_step")]
|
||||
RunScriptStep,
|
||||
[EnumMember(Value = "run_container_step")]
|
||||
RunContainerStep,
|
||||
}
|
||||
public interface IHookArgs
|
||||
{
|
||||
bool IsRequireAlpineInResponse();
|
||||
}
|
||||
|
||||
public class PrepareJobArgs : IHookArgs
|
||||
{
|
||||
public HookContainer Container { get; set; }
|
||||
public IList<HookContainer> Services { get; set; }
|
||||
public bool IsRequireAlpineInResponse() => Container != null;
|
||||
}
|
||||
|
||||
public class ScriptStepArgs : IHookArgs
|
||||
{
|
||||
public IEnumerable<string> EntryPointArgs { get; set; }
|
||||
public string EntryPoint { get; set; }
|
||||
public IDictionary<string, string> EnvironmentVariables { get; set; }
|
||||
public string PrependPath { get; set; }
|
||||
public string WorkingDirectory { get; set; }
|
||||
public bool IsRequireAlpineInResponse() => false;
|
||||
}
|
||||
|
||||
public class ContainerStepArgs : HookContainer, IHookArgs
|
||||
{
|
||||
public bool IsRequireAlpineInResponse() => false;
|
||||
public ContainerStepArgs(ContainerInfo container) : base(container) { }
|
||||
}
|
||||
public class CleanupJobArgs : IHookArgs
|
||||
{
|
||||
public bool IsRequireAlpineInResponse() => false;
|
||||
}
|
||||
|
||||
public class ContainerRegistry
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
}
|
||||
|
||||
public class HookContainer
|
||||
{
|
||||
public string Image { get; set; }
|
||||
public string Dockerfile { get; set; }
|
||||
public IEnumerable<string> EntryPointArgs { get; set; } = new List<string>();
|
||||
public string EntryPoint { get; set; }
|
||||
public string WorkingDirectory { get; set; }
|
||||
public string CreateOptions { get; private set; }
|
||||
public ContainerRegistry Registry { get; set; }
|
||||
public IDictionary<string, string> EnvironmentVariables { get; set; } = new Dictionary<string, string>();
|
||||
public IEnumerable<string> PortMappings { get; set; } = new List<string>();
|
||||
public IEnumerable<MountVolume> SystemMountVolumes { get; set; } = new List<MountVolume>();
|
||||
public IEnumerable<MountVolume> UserMountVolumes { get; set; } = new List<MountVolume>();
|
||||
public HookContainer() { } // For Json deserializer
|
||||
public HookContainer(ContainerInfo container)
|
||||
{
|
||||
Image = container.ContainerImage;
|
||||
EntryPointArgs = container.ContainerEntryPointArgs?.Split(' ').Select(arg => arg.Trim()) ?? new List<string>();
|
||||
EntryPoint = container.ContainerEntryPoint;
|
||||
WorkingDirectory = container.ContainerWorkDirectory;
|
||||
CreateOptions = container.ContainerCreateOptions;
|
||||
if (!string.IsNullOrEmpty(container.RegistryAuthUsername))
|
||||
{
|
||||
Registry = new ContainerRegistry
|
||||
{
|
||||
Username = container.RegistryAuthUsername,
|
||||
Password = container.RegistryAuthPassword,
|
||||
ServerUrl = container.RegistryServer,
|
||||
};
|
||||
}
|
||||
EnvironmentVariables = container.ContainerEnvironmentVariables;
|
||||
PortMappings = container.UserPortMappings.Select(p => p.Value).ToList();
|
||||
SystemMountVolumes = container.SystemMountVolumes;
|
||||
UserMountVolumes = container.UserMountVolumes;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ContainerInfoExtensions
|
||||
{
|
||||
public static HookContainer GetHookContainer(this ContainerInfo containerInfo)
|
||||
{
|
||||
return new HookContainer(containerInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Runner.Worker/Container/ContainerHooks/HookResponse.cs
Normal file
37
src/Runner.Worker/Container/ContainerHooks/HookResponse.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.Runner.Worker.Container.ContainerHooks
|
||||
{
|
||||
public class HookResponse
|
||||
{
|
||||
public JObject State { get; set; }
|
||||
public virtual void Validate(HookInput input) { }
|
||||
}
|
||||
public class PrepareJobResponse : HookResponse
|
||||
{
|
||||
public ResponseContext Context { get; set; }
|
||||
public bool? IsAlpine { get; set; }
|
||||
|
||||
public override void Validate(HookInput input)
|
||||
{
|
||||
bool hasJobContainer = ((PrepareJobArgs)input.Args).Container != null;
|
||||
if (hasJobContainer && IsAlpine == null)
|
||||
{
|
||||
throw new Exception("The property 'isAlpine' is required but was not found in the response file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
public class ResponseContext
|
||||
{
|
||||
public ResponseContainer Container { get; set; }
|
||||
public IList<ResponseContainer> Services { get; set; } = new List<ResponseContainer>();
|
||||
}
|
||||
public class ResponseContainer
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Network { get; set; }
|
||||
public IDictionary<string, string> Ports { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,7 @@ namespace GitHub.Runner.Worker.Container
|
||||
public string RegistryAuthUsername { get; set; }
|
||||
public string RegistryAuthPassword { get; set; }
|
||||
public bool IsJobContainer { get; set; }
|
||||
public bool IsAlpine { get; set; }
|
||||
|
||||
public IDictionary<string, string> ContainerEnvironmentVariables
|
||||
{
|
||||
@@ -232,6 +233,14 @@ namespace GitHub.Runner.Worker.Container
|
||||
}
|
||||
}
|
||||
|
||||
public void AddPortMappings(IDictionary<string, string> portMappings)
|
||||
{
|
||||
foreach (var pair in portMappings)
|
||||
{
|
||||
PortMappings.Add(new PortMapping(pair.Key, pair.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public void AddPathTranslateMapping(string hostCommonPath, string containerCommonPath)
|
||||
{
|
||||
_pathMappings.Insert(0, new PathMapping(hostCommonPath, containerCommonPath));
|
||||
@@ -322,6 +331,12 @@ namespace GitHub.Runner.Worker.Container
|
||||
|
||||
public class PortMapping
|
||||
{
|
||||
public PortMapping(string hostPort, string containerPort)
|
||||
{
|
||||
this.HostPort = hostPort;
|
||||
this.ContainerPort = containerPort;
|
||||
}
|
||||
|
||||
public PortMapping(string hostPort, string containerPort, string protocol)
|
||||
{
|
||||
this.HostPort = hostPort;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.ServiceProcess;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -10,8 +9,12 @@ using GitHub.Services.Common;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using Microsoft.Win32;
|
||||
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
|
||||
{
|
||||
@@ -25,11 +28,13 @@ namespace GitHub.Runner.Worker
|
||||
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)
|
||||
@@ -50,72 +55,15 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
executionContext.Debug($"Register post job cleanup for stopping/deleting containers.");
|
||||
executionContext.RegisterPostJobStep(postJobStep);
|
||||
|
||||
// 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))
|
||||
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
||||
{
|
||||
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}'");
|
||||
// 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");
|
||||
@@ -166,6 +114,12 @@ namespace GitHub.Runner.Worker
|
||||
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);
|
||||
@@ -238,35 +192,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
if (container.IsJobContainer)
|
||||
{
|
||||
// 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);
|
||||
container.ContainerEntryPoint = "tail";
|
||||
container.ContainerEntryPointArgs = "\"-f\" \"/dev/null\"";
|
||||
MountWellKnownDirectories(executionContext, container);
|
||||
}
|
||||
|
||||
container.ContainerId = await _dockerManager.DockerCreate(executionContext, container);
|
||||
@@ -329,6 +255,42 @@ namespace GitHub.Runner.Worker
|
||||
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();
|
||||
@@ -337,7 +299,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
if (!string.IsNullOrEmpty(container.ContainerId))
|
||||
{
|
||||
if(!container.IsJobContainer)
|
||||
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}");
|
||||
@@ -522,5 +484,74 @@ namespace GitHub.Runner.Worker
|
||||
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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1226,7 +1226,7 @@ namespace GitHub.Runner.Worker
|
||||
var value = dict[key].ToString();
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
dict[key] = new StringContextData(stepHost.ResolvePathForStepHost(value));
|
||||
dict[key] = new StringContextData(stepHost.ResolvePathForStepHost(context, value));
|
||||
}
|
||||
}
|
||||
else if (dict[key] is DictionaryContextData)
|
||||
|
||||
15
src/Runner.Worker/FeatureManager.cs
Normal file
15
src/Runner.Worker/FeatureManager.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
public class FeatureManager
|
||||
{
|
||||
public static bool IsContainerHooksEnabled(Variables variables)
|
||||
{
|
||||
var isContainerHookFeatureFlagSet = variables?.GetBoolean(Constants.Runner.Features.AllowRunnerContainerHooks) ?? false;
|
||||
var isContainerHooksPathSet = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(Constants.Hooks.ContainerHooksPath));
|
||||
return isContainerHookFeatureFlagSet && isContainerHooksPathSet;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
@@ -22,5 +23,6 @@ namespace GitHub.Runner.Worker
|
||||
public StepsContext StepsContext { get; set; }
|
||||
public Variables Variables { get; set; }
|
||||
public bool WriteDebug { get; set; }
|
||||
public JObject ContainerHookState { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||
using GitHub.Runner.Worker.Expressions;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker.Handlers
|
||||
@@ -38,6 +39,8 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
AddInputsToEnvironment();
|
||||
|
||||
var dockerManager = HostContext.GetService<IDockerCommandManager>();
|
||||
var containerHookManager = HostContext.GetService<IContainerHookManager>();
|
||||
string dockerFile = null;
|
||||
|
||||
// container image haven't built/pull
|
||||
if (Data.Image.StartsWith("docker://", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -47,9 +50,10 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
else if (Data.Image.EndsWith("Dockerfile") || Data.Image.EndsWith("dockerfile"))
|
||||
{
|
||||
// ensure docker file exist
|
||||
var dockerFile = Path.Combine(ActionDirectory, Data.Image);
|
||||
dockerFile = Path.Combine(ActionDirectory, Data.Image);
|
||||
ArgUtil.File(dockerFile, nameof(Data.Image));
|
||||
|
||||
if (!FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
|
||||
{
|
||||
ExecutionContext.Output($"##[group]Building docker image");
|
||||
ExecutionContext.Output($"Dockerfile for action: '{dockerFile}'.");
|
||||
var imageName = $"{dockerManager.DockerInstanceLabel}:{ExecutionContext.Id.ToString("N")}";
|
||||
@@ -68,6 +72,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
Data.Image = imageName;
|
||||
}
|
||||
}
|
||||
|
||||
string type = Action.Type == Pipelines.ActionSourceType.Repository ? "Dockerfile" : "DockerHub";
|
||||
// Set extra telemetry base on the current context.
|
||||
@@ -220,6 +225,12 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value);
|
||||
}
|
||||
|
||||
if (FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
|
||||
{
|
||||
await containerHookManager.RunContainerStepAsync(ExecutionContext, container, dockerFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager, container))
|
||||
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager, container))
|
||||
{
|
||||
@@ -230,6 +241,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
ExecutionContext.Result = TaskResult.Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||
|
||||
namespace GitHub.Runner.Worker.Handlers
|
||||
{
|
||||
@@ -109,7 +111,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
// 1) Wrap the script file path in double quotes.
|
||||
// 2) Escape double quotes within the script file path. Double-quote is a valid
|
||||
// file name character on Linux.
|
||||
string arguments = StepHost.ResolvePathForStepHost(StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\""")));
|
||||
string arguments = StepHost.ResolvePathForStepHost(ExecutionContext, StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\""")));
|
||||
|
||||
#if OS_WINDOWS
|
||||
// It appears that node.exe outputs UTF8 when not in TTY mode.
|
||||
@@ -142,14 +144,16 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
// Execute the process. Exit code 0 should always be returned.
|
||||
// A non-zero exit code indicates infrastructural failure.
|
||||
// Task failure should be communicated over STDOUT using ## commands.
|
||||
Task<int> step = StepHost.ExecuteAsync(workingDirectory: StepHost.ResolvePathForStepHost(workingDirectory),
|
||||
fileName: StepHost.ResolvePathForStepHost(file),
|
||||
Task<int> step = StepHost.ExecuteAsync(ExecutionContext,
|
||||
workingDirectory: StepHost.ResolvePathForStepHost(ExecutionContext, workingDirectory),
|
||||
fileName: StepHost.ResolvePathForStepHost(ExecutionContext, file),
|
||||
arguments: arguments,
|
||||
environment: Environment,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: outputEncoding,
|
||||
killProcessOnCancel: false,
|
||||
inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding,
|
||||
standardInInput: null,
|
||||
cancellationToken: ExecutionContext.CancellationToken);
|
||||
|
||||
// Wait for either the node exit or force finish through ##vso command
|
||||
|
||||
@@ -8,6 +8,8 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker.Handlers
|
||||
@@ -247,7 +249,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
{
|
||||
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
|
||||
scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
|
||||
resolvedScriptPath = StepHost.ResolvePathForStepHost(scriptFilePath).Replace("\"", "\\\"");
|
||||
resolvedScriptPath = StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -318,6 +320,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
ExecutionContext.Debug($"{fileName} {arguments}");
|
||||
|
||||
Inputs.TryGetValue("standardInInput", out var standardInInput);
|
||||
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
||||
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
||||
{
|
||||
@@ -325,7 +328,8 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
StepHost.ErrorDataReceived += stderrManager.OnDataReceived;
|
||||
|
||||
// Execute
|
||||
int exitCode = await StepHost.ExecuteAsync(workingDirectory: StepHost.ResolvePathForStepHost(workingDirectory),
|
||||
int exitCode = await StepHost.ExecuteAsync(ExecutionContext,
|
||||
workingDirectory: StepHost.ResolvePathForStepHost(ExecutionContext, workingDirectory),
|
||||
fileName: fileName,
|
||||
arguments: arguments,
|
||||
environment: Environment,
|
||||
@@ -333,6 +337,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
outputEncoding: null,
|
||||
killProcessOnCancel: false,
|
||||
inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding,
|
||||
standardInInput: standardInInput,
|
||||
cancellationToken: ExecutionContext.CancellationToken);
|
||||
|
||||
// Error
|
||||
|
||||
@@ -3,10 +3,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
|
||||
namespace GitHub.Runner.Worker.Handlers
|
||||
{
|
||||
internal class ScriptHandlerHelpers
|
||||
internal static class ScriptHandlerHelpers
|
||||
{
|
||||
private static readonly Dictionary<string, string> _defaultArguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -81,27 +83,5 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
throw new ArgumentException($"Failed to parse COMMAND [..ARGS] from {shellOption}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetDefaultShellNameForScript(string path, Common.Tracing trace, string prependPath)
|
||||
{
|
||||
switch (Path.GetExtension(path))
|
||||
{
|
||||
case ".sh":
|
||||
// use 'sh' args but prefer bash
|
||||
if (WhichUtil.Which("bash", false, trace, prependPath) != null)
|
||||
{
|
||||
return "bash";
|
||||
}
|
||||
return "sh";
|
||||
case ".ps1":
|
||||
if (WhichUtil.Which("pwsh", false, trace, prependPath) != null)
|
||||
{
|
||||
return "pwsh";
|
||||
}
|
||||
return "powershell";
|
||||
default:
|
||||
throw new ArgumentException($"{path} is not a valid path to a script. Make sure it ends in '.sh' or '.ps1'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,7 +8,9 @@ using GitHub.Runner.Worker.Container;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||
using System.IO;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace GitHub.Runner.Worker.Handlers
|
||||
{
|
||||
@@ -16,11 +19,12 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
|
||||
event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
|
||||
|
||||
string ResolvePathForStepHost(string path);
|
||||
string ResolvePathForStepHost(IExecutionContext executionContext, string path);
|
||||
|
||||
Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion);
|
||||
|
||||
Task<int> ExecuteAsync(string workingDirectory,
|
||||
Task<int> ExecuteAsync(IExecutionContext context,
|
||||
string workingDirectory,
|
||||
string fileName,
|
||||
string arguments,
|
||||
IDictionary<string, string> environment,
|
||||
@@ -28,6 +32,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
Encoding outputEncoding,
|
||||
bool killProcessOnCancel,
|
||||
bool inheritConsoleHandler,
|
||||
string standardInInput,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -48,7 +53,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
|
||||
public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
|
||||
|
||||
public string ResolvePathForStepHost(string path)
|
||||
public string ResolvePathForStepHost(IExecutionContext executionContext, string path)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
@@ -58,7 +63,8 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
return Task.FromResult<string>(preferredVersion);
|
||||
}
|
||||
|
||||
public async Task<int> ExecuteAsync(string workingDirectory,
|
||||
public async Task<int> ExecuteAsync(IExecutionContext context,
|
||||
string workingDirectory,
|
||||
string fileName,
|
||||
string arguments,
|
||||
IDictionary<string, string> environment,
|
||||
@@ -66,10 +72,17 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
Encoding outputEncoding,
|
||||
bool killProcessOnCancel,
|
||||
bool inheritConsoleHandler,
|
||||
string standardInInput,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
Channel<string> redirectStandardIn = null;
|
||||
if (standardInInput != null)
|
||||
{
|
||||
redirectStandardIn = Channel.CreateUnbounded<string>(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = true });
|
||||
redirectStandardIn.Writer.TryWrite(standardInInput);
|
||||
}
|
||||
processInvoker.OutputDataReceived += OutputDataReceived;
|
||||
processInvoker.ErrorDataReceived += ErrorDataReceived;
|
||||
|
||||
@@ -80,7 +93,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
requireExitCodeZero: requireExitCodeZero,
|
||||
outputEncoding: outputEncoding,
|
||||
killProcessOnCancel: killProcessOnCancel,
|
||||
redirectStandardIn: null,
|
||||
redirectStandardIn: redirectStandardIn,
|
||||
inheritConsoleHandler: inheritConsoleHandler,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
@@ -94,11 +107,15 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
|
||||
public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
|
||||
|
||||
public string ResolvePathForStepHost(string path)
|
||||
public string ResolvePathForStepHost(IExecutionContext executionContext, string path)
|
||||
{
|
||||
// make sure container exist.
|
||||
ArgUtil.NotNull(Container, nameof(Container));
|
||||
if (!FeatureManager.IsContainerHooksEnabled(executionContext.Global?.Variables))
|
||||
{
|
||||
// TODO: Remove nullcheck with executionContext.Global? by setting up ExecutionContext.Global at GitHub.Runner.Common.Tests.Worker.ExecutionContextL0.GetExpressionValues_ContainerStepHost
|
||||
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId));
|
||||
}
|
||||
|
||||
// remove double quotes around the path
|
||||
path = path.Trim('\"');
|
||||
@@ -120,6 +137,19 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext, string preferredVersion)
|
||||
{
|
||||
// Optimistically use the default
|
||||
string nodeExternal = preferredVersion;
|
||||
|
||||
if (FeatureManager.IsContainerHooksEnabled(executionContext.Global.Variables))
|
||||
{
|
||||
if (Container.IsAlpine)
|
||||
{
|
||||
nodeExternal = CheckPlatformForAlpineContainer(executionContext, preferredVersion);
|
||||
}
|
||||
executionContext.Debug($"Running JavaScript Action with default external tool: {nodeExternal}");
|
||||
return nodeExternal;
|
||||
}
|
||||
|
||||
// Best effort to determine a compatible node runtime
|
||||
// There may be more variation in which libraries are linked than just musl/glibc,
|
||||
// so determine based on known distribtutions instead
|
||||
@@ -128,7 +158,6 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
|
||||
var output = new List<string>();
|
||||
var execExitCode = await dockerManager.DockerExec(executionContext, Container.ContainerId, string.Empty, osReleaseIdCmd, output);
|
||||
string nodeExternal;
|
||||
if (execExitCode == 0)
|
||||
{
|
||||
foreach (var line in output)
|
||||
@@ -136,26 +165,17 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
executionContext.Debug(line);
|
||||
if (line.ToLower().Contains("alpine"))
|
||||
{
|
||||
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
|
||||
{
|
||||
var os = Constants.Runner.Platform.ToString();
|
||||
var arch = Constants.Runner.PlatformArchitecture.ToString();
|
||||
var msg = $"JavaScript Actions in Alpine containers are only supported on x64 Linux runners. Detected {os} {arch}";
|
||||
throw new NotSupportedException(msg);
|
||||
}
|
||||
nodeExternal = $"{preferredVersion}_alpine";
|
||||
executionContext.Debug($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
|
||||
nodeExternal = CheckPlatformForAlpineContainer(executionContext, preferredVersion);
|
||||
return nodeExternal;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Optimistically use the default
|
||||
nodeExternal = preferredVersion;
|
||||
executionContext.Debug($"Running JavaScript Action with default external tool: {nodeExternal}");
|
||||
return nodeExternal;
|
||||
}
|
||||
|
||||
public async Task<int> ExecuteAsync(string workingDirectory,
|
||||
public async Task<int> ExecuteAsync(IExecutionContext context,
|
||||
string workingDirectory,
|
||||
string fileName,
|
||||
string arguments,
|
||||
IDictionary<string, string> environment,
|
||||
@@ -163,12 +183,25 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
Encoding outputEncoding,
|
||||
bool killProcessOnCancel,
|
||||
bool inheritConsoleHandler,
|
||||
string standardInInput,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// make sure container exist.
|
||||
ArgUtil.NotNull(Container, nameof(Container));
|
||||
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId));
|
||||
var containerHookManager = HostContext.GetService<IContainerHookManager>();
|
||||
if (FeatureManager.IsContainerHooksEnabled(context.Global.Variables))
|
||||
{
|
||||
TranslateToContainerPath(environment);
|
||||
await containerHookManager.RunScriptStepAsync(context,
|
||||
Container,
|
||||
workingDirectory,
|
||||
fileName,
|
||||
arguments,
|
||||
environment,
|
||||
PrependPath);
|
||||
return (int)(context.Result ?? 0);
|
||||
}
|
||||
|
||||
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId));
|
||||
var dockerManager = HostContext.GetService<IDockerCommandManager>();
|
||||
string dockerClientPath = dockerManager.DockerPath;
|
||||
|
||||
@@ -202,12 +235,7 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
dockerCommandArgs.Add(arguments);
|
||||
|
||||
string dockerCommandArgstring = string.Join(" ", dockerCommandArgs);
|
||||
|
||||
// make sure all env are using container path
|
||||
foreach (var envKey in environment.Keys.ToList())
|
||||
{
|
||||
environment[envKey] = this.Container.TranslateToContainerPath(environment[envKey]);
|
||||
}
|
||||
TranslateToContainerPath(environment);
|
||||
|
||||
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||
{
|
||||
@@ -221,7 +249,6 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
// Let .NET choose the default.
|
||||
outputEncoding = null;
|
||||
#endif
|
||||
|
||||
return await processInvoker.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work),
|
||||
fileName: dockerClientPath,
|
||||
arguments: dockerCommandArgstring,
|
||||
@@ -234,5 +261,28 @@ namespace GitHub.Runner.Worker.Handlers
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private string CheckPlatformForAlpineContainer(IExecutionContext executionContext, string preferredVersion)
|
||||
{
|
||||
string nodeExternal = preferredVersion;
|
||||
if (!Constants.Runner.PlatformArchitecture.Equals(Constants.Architecture.X64))
|
||||
{
|
||||
var os = Constants.Runner.Platform.ToString();
|
||||
var arch = Constants.Runner.PlatformArchitecture.ToString();
|
||||
var msg = $"JavaScript Actions in Alpine containers are only supported on x64 Linux runners. Detected {os} {arch}";
|
||||
throw new NotSupportedException(msg);
|
||||
}
|
||||
nodeExternal = $"{preferredVersion}_alpine";
|
||||
executionContext.Debug($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
|
||||
return nodeExternal;
|
||||
}
|
||||
|
||||
private void TranslateToContainerPath(IDictionary<string, string> environment)
|
||||
{
|
||||
foreach (var envKey in environment.Keys.ToList())
|
||||
{
|
||||
environment[envKey] = this.Container.TranslateToContainerPath(environment[envKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace GitHub.Runner.Worker
|
||||
Dictionary<string, string> inputs = new()
|
||||
{
|
||||
["path"] = hookData.Path,
|
||||
["shell"] = ScriptHandlerHelpers.GetDefaultShellNameForScript(hookData.Path, Trace, prependPath)
|
||||
["shell"] = HostContext.GetDefaultShellForScript(hookData.Path, prependPath)
|
||||
};
|
||||
|
||||
// Create the handler
|
||||
|
||||
@@ -56,5 +56,8 @@ namespace GitHub.DistributedTask.WebApi
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public int? ExecutionTimeInSeconds { get; set; }
|
||||
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public string ContainerHookData { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using GitHub.Runner.Listener.Check;
|
||||
using GitHub.Runner.Listener.Configuration;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Container.ContainerHooks;
|
||||
using GitHub.Runner.Worker.Handlers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -68,7 +69,8 @@ namespace GitHub.Runner.Common.Tests
|
||||
typeof(IStep),
|
||||
typeof(IStepHost),
|
||||
typeof(IDiagnosticLogManager),
|
||||
typeof(IEnvironmentContextData)
|
||||
typeof(IEnvironmentContextData),
|
||||
typeof(IHookArgs),
|
||||
};
|
||||
Validate(
|
||||
assembly: typeof(IStepsRunner).GetTypeInfo().Assembly,
|
||||
|
||||
@@ -100,7 +100,6 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
using (TestHostContext hc = CreateTestContext())
|
||||
{
|
||||
|
||||
// Arrange: Create a job request message.
|
||||
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||
TimelineReference timeline = new TimelineReference();
|
||||
|
||||
@@ -10,6 +10,7 @@ using GitHub.Runner.Worker.Container;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
@@ -24,6 +25,7 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
_ec = new Mock<IExecutionContext>();
|
||||
_ec.SetupAllProperties();
|
||||
_ec.Setup(x => x.Global).Returns(new GlobalContext { WriteDebug = true });
|
||||
_ec.Object.Global.Variables = new Variables(hc, new Dictionary<string, VariableValue>());
|
||||
var trace = hc.GetTrace();
|
||||
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { trace.Info($"[{tag}]{message}"); });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user