Files
runner/src/Runner.Worker/Container/ContainerInfo.cs
Ferenc Hammerl 591f8c3510 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>
2022-06-10 13:51:20 +00:00

376 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using Pipelines = GitHub.DistributedTask.Pipelines;
using System.Collections.ObjectModel;
using System.Linq;
namespace GitHub.Runner.Worker.Container
{
public class ContainerInfo
{
private List<MountVolume> _mountVolumes;
private IDictionary<string, string> _userPortMappings;
private List<PortMapping> _portMappings;
private IDictionary<string, string> _environmentVariables;
private List<PathMapping> _pathMappings = new List<PathMapping>();
public ContainerInfo()
{
}
public ContainerInfo(IHostContext hostContext)
{
UpdateWebProxyEnv(hostContext.WebProxy);
}
public ContainerInfo(IHostContext hostContext, Pipelines.JobContainer container, bool isJobContainer = true, string networkAlias = null)
{
this.ContainerName = container.Alias;
string containerImage = container.Image;
ArgUtil.NotNullOrEmpty(containerImage, nameof(containerImage));
this.ContainerImage = containerImage;
this.ContainerDisplayName = $"{container.Alias}_{Pipelines.Validation.NameValidation.Sanitize(containerImage)}_{Guid.NewGuid().ToString("N").Substring(0, 6)}";
this.ContainerCreateOptions = container.Options;
_environmentVariables = container.Environment;
this.IsJobContainer = isJobContainer;
this.ContainerNetworkAlias = networkAlias;
this.RegistryAuthUsername = container.Credentials?.Username;
this.RegistryAuthPassword = container.Credentials?.Password;
this.RegistryServer = DockerUtil.ParseRegistryHostnameFromImageName(this.ContainerImage);
#if OS_WINDOWS
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Work), "C:\\__w"));
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Tools), "C:\\__t")); // Tool cache folder may come from ENV, so we need a unique folder to avoid collision
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Externals), "C:\\__e"));
// add -v '\\.\pipe\docker_engine:\\.\pipe\docker_engine' when they are available (17.09)
#else
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Work), "/__w"));
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Tools), "/__t")); // Tool cache folder may come from ENV, so we need a unique folder to avoid collision
_pathMappings.Add(new PathMapping(hostContext.GetDirectory(WellKnownDirectory.Externals), "/__e"));
if (this.IsJobContainer)
{
this.MountVolumes.Add(new MountVolume("/var/run/docker.sock", "/var/run/docker.sock"));
}
#endif
if (container.Ports?.Count > 0)
{
foreach (var port in container.Ports)
{
UserPortMappings[port] = port;
}
}
if (container.Volumes?.Count > 0)
{
foreach (var volume in container.Volumes)
{
MountVolumes.Add(new MountVolume(volume, isUserProvided: true));
}
}
UpdateWebProxyEnv(hostContext.WebProxy);
}
public string ContainerId { get; set; }
public string ContainerDisplayName { get; set; }
public string ContainerNetwork { get; set; }
public string ContainerNetworkAlias { get; set; }
public string ContainerImage { get; set; }
public string ContainerName { get; set; }
public string ContainerEntryPointArgs { get; set; }
public string ContainerEntryPoint { get; set; }
public string ContainerWorkDirectory { get; set; }
public string ContainerCreateOptions { get; private set; }
public string ContainerRuntimePath { get; set; }
public string RegistryServer { get; set; }
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
{
get
{
if (_environmentVariables == null)
{
_environmentVariables = new Dictionary<string, string>();
}
return _environmentVariables;
}
}
public ReadOnlyCollection<MountVolume> UserMountVolumes
{
get
{
return MountVolumes.Where(v => !string.IsNullOrEmpty(v.UserProvidedValue)).ToList().AsReadOnly();
}
}
public ReadOnlyCollection<MountVolume> SystemMountVolumes
{
get
{
return MountVolumes.Where(v => string.IsNullOrEmpty(v.UserProvidedValue)).ToList().AsReadOnly();
}
}
public List<MountVolume> MountVolumes
{
get
{
if (_mountVolumes == null)
{
_mountVolumes = new List<MountVolume>();
}
return _mountVolumes;
}
}
public IDictionary<string, string> UserPortMappings
{
get
{
if (_userPortMappings == null)
{
_userPortMappings = new Dictionary<string, string>();
}
return _userPortMappings;
}
}
public List<PortMapping> PortMappings
{
get
{
if (_portMappings == null)
{
_portMappings = new List<PortMapping>();
}
return _portMappings;
}
}
public string TranslateToContainerPath(string path)
{
if (!string.IsNullOrEmpty(path))
{
foreach (var mapping in _pathMappings)
{
#if OS_WINDOWS
if (string.Equals(path, mapping.HostPath, StringComparison.OrdinalIgnoreCase))
{
return mapping.ContainerPath;
}
if (path.StartsWith(mapping.HostPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
path.StartsWith(mapping.HostPath + Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
return mapping.ContainerPath + path.Remove(0, mapping.HostPath.Length);
}
#else
if (string.Equals(path, mapping.HostPath))
{
return mapping.ContainerPath;
}
if (path.StartsWith(mapping.HostPath + Path.DirectorySeparatorChar))
{
return mapping.ContainerPath + path.Remove(0, mapping.HostPath.Length);
}
#endif
}
}
return path;
}
public string TranslateToHostPath(string path)
{
if (!string.IsNullOrEmpty(path))
{
foreach (var mapping in _pathMappings)
{
#if OS_WINDOWS
if (string.Equals(path, mapping.ContainerPath, StringComparison.OrdinalIgnoreCase))
{
return mapping.HostPath;
}
if (path.StartsWith(mapping.ContainerPath + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
path.StartsWith(mapping.ContainerPath + Path.AltDirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
return mapping.HostPath + path.Remove(0, mapping.ContainerPath.Length);
}
#else
if (string.Equals(path, mapping.ContainerPath))
{
return mapping.HostPath;
}
if (path.StartsWith(mapping.ContainerPath + Path.DirectorySeparatorChar))
{
return mapping.HostPath + path.Remove(0, mapping.ContainerPath.Length);
}
#endif
}
}
return path;
}
public void AddPortMappings(List<PortMapping> portMappings)
{
foreach (var port in portMappings)
{
PortMappings.Add(port);
}
}
public void 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));
}
private void UpdateWebProxyEnv(RunnerWebProxy webProxy)
{
// Set common forms of proxy variables if configured in Runner and not set directly by container.env
if (!String.IsNullOrEmpty(webProxy.HttpProxyAddress))
{
ContainerEnvironmentVariables.TryAdd("HTTP_PROXY", webProxy.HttpProxyAddress);
ContainerEnvironmentVariables.TryAdd("http_proxy", webProxy.HttpProxyAddress);
}
if (!String.IsNullOrEmpty(webProxy.HttpsProxyAddress))
{
ContainerEnvironmentVariables.TryAdd("HTTPS_PROXY", webProxy.HttpsProxyAddress);
ContainerEnvironmentVariables.TryAdd("https_proxy", webProxy.HttpsProxyAddress);
}
if (!String.IsNullOrEmpty(webProxy.NoProxyString))
{
ContainerEnvironmentVariables.TryAdd("NO_PROXY", webProxy.NoProxyString);
ContainerEnvironmentVariables.TryAdd("no_proxy", webProxy.NoProxyString);
}
}
}
public class MountVolume
{
public string UserProvidedValue { get; set; }
public MountVolume(string sourceVolumePath, string targetVolumePath, bool readOnly = false)
{
this.SourceVolumePath = sourceVolumePath;
this.TargetVolumePath = targetVolumePath;
this.ReadOnly = readOnly;
}
public MountVolume(string fromString)
{
ParseVolumeString(fromString);
}
public MountVolume(string fromString, bool isUserProvided)
{
ParseVolumeString(fromString);
if (isUserProvided)
{
UserProvidedValue = fromString;
}
}
private void ParseVolumeString(string volume)
{
var volumeSplit = volume.Split(":");
if (volumeSplit.Length == 3)
{
// source:target:ro
SourceVolumePath = volumeSplit[0];
TargetVolumePath = volumeSplit[1];
ReadOnly = String.Equals(volumeSplit[2], "ro", StringComparison.OrdinalIgnoreCase);
}
else if (volumeSplit.Length == 2)
{
if (String.Equals(volumeSplit[1], "ro", StringComparison.OrdinalIgnoreCase))
{
// target:ro
TargetVolumePath = volumeSplit[0];
ReadOnly = true;
}
else
{
// source:target
SourceVolumePath = volumeSplit[0];
TargetVolumePath = volumeSplit[1];
ReadOnly = false;
}
}
else
{
// target - or, default to passing straight through
TargetVolumePath = volume;
ReadOnly = false;
}
}
public string SourceVolumePath { get; set; }
public string TargetVolumePath { get; set; }
public bool ReadOnly { get; set; }
}
public class PortMapping
{
public PortMapping(string hostPort, string containerPort)
{
this.HostPort = hostPort;
this.ContainerPort = containerPort;
}
public PortMapping(string hostPort, string containerPort, string protocol)
{
this.HostPort = hostPort;
this.ContainerPort = containerPort;
this.Protocol = protocol;
}
public string HostPort { get; set; }
public string ContainerPort { get; set; }
public string Protocol { get; set; }
}
public class DockerVersion
{
public DockerVersion(Version serverVersion, Version clientVersion)
{
this.ServerVersion = serverVersion;
this.ClientVersion = clientVersion;
}
public Version ServerVersion { get; set; }
public Version ClientVersion { get; set; }
}
public class PathMapping
{
public PathMapping(string hostPath, string containerPath)
{
this.HostPath = hostPath;
this.ContainerPath = containerPath;
}
public string HostPath { get; set; }
public string ContainerPath { get; set; }
}
}