2019-10-10 00:52:42 -04:00
using System ;
using System.Collections.Generic ;
2022-06-10 15:51:20 +02:00
using GitHub.DistributedTask.Pipelines.ContextData ;
2019-10-10 00:52:42 -04:00
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using GitHub.Runner.Worker.Container ;
using GitHub.Runner.Common ;
using GitHub.Runner.Sdk ;
using System.Linq ;
2022-06-10 15:51:20 +02:00
using GitHub.Runner.Worker.Container.ContainerHooks ;
using System.IO ;
using System.Threading.Channels ;
2019-10-10 00:52:42 -04:00
namespace GitHub.Runner.Worker.Handlers
{
public interface IStepHost : IRunnerService
{
event EventHandler < ProcessDataReceivedEventArgs > OutputDataReceived ;
event EventHandler < ProcessDataReceivedEventArgs > ErrorDataReceived ;
2022-06-10 15:51:20 +02:00
string ResolvePathForStepHost ( IExecutionContext executionContext , string path ) ;
2019-10-10 00:52:42 -04:00
2021-11-18 15:25:33 -05:00
Task < string > DetermineNodeRuntimeVersion ( IExecutionContext executionContext , string preferredVersion ) ;
2019-10-10 00:52:42 -04:00
2022-06-10 15:51:20 +02:00
Task < int > ExecuteAsync ( IExecutionContext context ,
string workingDirectory ,
2019-10-10 00:52:42 -04:00
string fileName ,
string arguments ,
IDictionary < string , string > environment ,
bool requireExitCodeZero ,
Encoding outputEncoding ,
bool killProcessOnCancel ,
bool inheritConsoleHandler ,
2022-06-10 15:51:20 +02:00
string standardInInput ,
2019-10-10 00:52:42 -04:00
CancellationToken cancellationToken ) ;
}
[ServiceLocator(Default = typeof(ContainerStepHost))]
public interface IContainerStepHost : IStepHost
{
ContainerInfo Container { get ; set ; }
string PrependPath { get ; set ; }
}
[ServiceLocator(Default = typeof(DefaultStepHost))]
public interface IDefaultStepHost : IStepHost
{
}
public sealed class DefaultStepHost : RunnerService , IDefaultStepHost
{
public event EventHandler < ProcessDataReceivedEventArgs > OutputDataReceived ;
public event EventHandler < ProcessDataReceivedEventArgs > ErrorDataReceived ;
2022-06-10 15:51:20 +02:00
public string ResolvePathForStepHost ( IExecutionContext executionContext , string path )
2019-10-10 00:52:42 -04:00
{
return path ;
}
2021-11-18 15:25:33 -05:00
public Task < string > DetermineNodeRuntimeVersion ( IExecutionContext executionContext , string preferredVersion )
2019-10-10 00:52:42 -04:00
{
2021-11-18 15:25:33 -05:00
return Task . FromResult < string > ( preferredVersion ) ;
2019-10-10 00:52:42 -04:00
}
2022-06-10 15:51:20 +02:00
public async Task < int > ExecuteAsync ( IExecutionContext context ,
string workingDirectory ,
2019-10-10 00:52:42 -04:00
string fileName ,
string arguments ,
IDictionary < string , string > environment ,
bool requireExitCodeZero ,
Encoding outputEncoding ,
bool killProcessOnCancel ,
bool inheritConsoleHandler ,
2022-06-10 15:51:20 +02:00
string standardInInput ,
2019-10-10 00:52:42 -04:00
CancellationToken cancellationToken )
{
using ( var processInvoker = HostContext . CreateService < IProcessInvoker > ( ) )
{
2022-06-10 15:51:20 +02:00
Channel < string > redirectStandardIn = null ;
if ( standardInInput ! = null )
{
redirectStandardIn = Channel . CreateUnbounded < string > ( new UnboundedChannelOptions ( ) { SingleReader = true , SingleWriter = true } ) ;
redirectStandardIn . Writer . TryWrite ( standardInInput ) ;
}
2019-10-10 00:52:42 -04:00
processInvoker . OutputDataReceived + = OutputDataReceived ;
processInvoker . ErrorDataReceived + = ErrorDataReceived ;
return await processInvoker . ExecuteAsync ( workingDirectory : workingDirectory ,
fileName : fileName ,
arguments : arguments ,
environment : environment ,
requireExitCodeZero : requireExitCodeZero ,
outputEncoding : outputEncoding ,
killProcessOnCancel : killProcessOnCancel ,
2022-06-10 15:51:20 +02:00
redirectStandardIn : redirectStandardIn ,
2019-10-10 00:52:42 -04:00
inheritConsoleHandler : inheritConsoleHandler ,
cancellationToken : cancellationToken ) ;
}
}
}
public sealed class ContainerStepHost : RunnerService , IContainerStepHost
{
public ContainerInfo Container { get ; set ; }
public string PrependPath { get ; set ; }
public event EventHandler < ProcessDataReceivedEventArgs > OutputDataReceived ;
public event EventHandler < ProcessDataReceivedEventArgs > ErrorDataReceived ;
2022-06-10 15:51:20 +02:00
public string ResolvePathForStepHost ( IExecutionContext executionContext , string path )
2019-10-10 00:52:42 -04:00
{
// make sure container exist.
ArgUtil . NotNull ( Container , nameof ( Container ) ) ;
2022-06-10 15:51:20 +02:00
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 ) ) ;
}
2019-10-10 00:52:42 -04:00
// remove double quotes around the path
path = path . Trim ( '\"' ) ;
// try to resolve path inside container if the request path is part of the mount volume
#if OS_WINDOWS
2020-04-14 14:36:39 -04:00
if ( Container . MountVolumes . Exists ( x = > ! string . IsNullOrEmpty ( x . SourceVolumePath ) & & path . StartsWith ( x . SourceVolumePath , StringComparison . OrdinalIgnoreCase ) ) )
2019-10-10 00:52:42 -04:00
#else
2020-04-14 14:36:39 -04:00
if ( Container . MountVolumes . Exists ( x = > ! string . IsNullOrEmpty ( x . SourceVolumePath ) & & path . StartsWith ( x . SourceVolumePath ) ) )
2019-10-10 00:52:42 -04:00
#endif
{
return Container . TranslateToContainerPath ( path ) ;
}
else
{
return path ;
}
}
2021-11-18 15:25:33 -05:00
public async Task < string > DetermineNodeRuntimeVersion ( IExecutionContext executionContext , string preferredVersion )
2019-10-10 00:52:42 -04:00
{
2022-06-10 15:51:20 +02:00
// 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 ;
}
2019-10-10 00:52:42 -04:00
// 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
var osReleaseIdCmd = "sh -c \"cat /etc/*release | grep ^ID\"" ;
var dockerManager = HostContext . GetService < IDockerCommandManager > ( ) ;
var output = new List < string > ( ) ;
var execExitCode = await dockerManager . DockerExec ( executionContext , Container . ContainerId , string . Empty , osReleaseIdCmd , output ) ;
if ( execExitCode = = 0 )
{
foreach ( var line in output )
{
executionContext . Debug ( line ) ;
if ( line . ToLower ( ) . Contains ( "alpine" ) )
{
2022-06-10 15:51:20 +02:00
nodeExternal = CheckPlatformForAlpineContainer ( executionContext , preferredVersion ) ;
2019-10-10 00:52:42 -04:00
return nodeExternal ;
}
}
}
2020-04-17 11:53:51 -04:00
executionContext . Debug ( $"Running JavaScript Action with default external tool: {nodeExternal}" ) ;
2019-10-10 00:52:42 -04:00
return nodeExternal ;
}
2022-06-10 15:51:20 +02:00
public async Task < int > ExecuteAsync ( IExecutionContext context ,
string workingDirectory ,
2019-10-10 00:52:42 -04:00
string fileName ,
string arguments ,
IDictionary < string , string > environment ,
bool requireExitCodeZero ,
Encoding outputEncoding ,
bool killProcessOnCancel ,
bool inheritConsoleHandler ,
2022-06-10 15:51:20 +02:00
string standardInInput ,
2019-10-10 00:52:42 -04:00
CancellationToken cancellationToken )
{
ArgUtil . NotNull ( Container , nameof ( Container ) ) ;
2022-06-10 15:51:20 +02:00
var containerHookManager = HostContext . GetService < IContainerHookManager > ( ) ;
if ( FeatureManager . IsContainerHooksEnabled ( context . Global . Variables ) )
{
TranslateToContainerPath ( environment ) ;
await containerHookManager . RunScriptStepAsync ( context ,
Container ,
2022-08-23 07:42:29 -07:00
workingDirectory ,
2022-06-10 15:51:20 +02:00
fileName ,
arguments ,
environment ,
PrependPath ) ;
return ( int ) ( context . Result ? ? 0 ) ;
}
2019-10-10 00:52:42 -04:00
2022-06-10 15:51:20 +02:00
ArgUtil . NotNullOrEmpty ( Container . ContainerId , nameof ( Container . ContainerId ) ) ;
2019-10-10 00:52:42 -04:00
var dockerManager = HostContext . GetService < IDockerCommandManager > ( ) ;
string dockerClientPath = dockerManager . DockerPath ;
// Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
IList < string > dockerCommandArgs = new List < string > ( ) ;
dockerCommandArgs . Add ( $"exec" ) ;
// [OPTIONS]
2021-02-09 14:45:33 -05:00
dockerCommandArgs . Add ( $"-i" ) ;
2019-10-10 00:52:42 -04:00
dockerCommandArgs . Add ( $"--workdir {workingDirectory}" ) ;
foreach ( var env in environment )
{
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
// the value directly in the command
2022-08-23 07:42:29 -07:00
dockerCommandArgs . Add ( DockerUtil . CreateEscapedOption ( "-e" , env . Key ) ) ;
2019-10-10 00:52:42 -04:00
}
if ( ! string . IsNullOrEmpty ( PrependPath ) )
{
// Prepend tool paths to container's PATH
var fullPath = ! string . IsNullOrEmpty ( Container . ContainerRuntimePath ) ? $"{PrependPath}:{Container.ContainerRuntimePath}" : PrependPath ;
dockerCommandArgs . Add ( $"-e PATH=\" { fullPath } \ "" ) ;
}
// CONTAINER
dockerCommandArgs . Add ( $"{Container.ContainerId}" ) ;
// COMMAND
dockerCommandArgs . Add ( fileName ) ;
// [ARG...]
dockerCommandArgs . Add ( arguments ) ;
string dockerCommandArgstring = string . Join ( " " , dockerCommandArgs ) ;
2022-06-10 15:51:20 +02:00
TranslateToContainerPath ( environment ) ;
2019-10-10 00:52:42 -04:00
using ( var processInvoker = HostContext . CreateService < IProcessInvoker > ( ) )
{
processInvoker . OutputDataReceived + = OutputDataReceived ;
processInvoker . ErrorDataReceived + = ErrorDataReceived ;
#if OS_WINDOWS
// It appears that node.exe outputs UTF8 when not in TTY mode.
outputEncoding = Encoding . UTF8 ;
#else
// Let .NET choose the default.
outputEncoding = null ;
#endif
return await processInvoker . ExecuteAsync ( workingDirectory : HostContext . GetDirectory ( WellKnownDirectory . Work ) ,
fileName : dockerClientPath ,
arguments : dockerCommandArgstring ,
environment : environment ,
requireExitCodeZero : requireExitCodeZero ,
outputEncoding : outputEncoding ,
killProcessOnCancel : killProcessOnCancel ,
redirectStandardIn : null ,
inheritConsoleHandler : inheritConsoleHandler ,
cancellationToken : cancellationToken ) ;
}
}
2022-06-10 15:51:20 +02:00
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 ] ) ;
}
}
2019-10-10 00:52:42 -04:00
}
}