2020-04-23 17:02:13 -04:00
using System ;
2019-10-10 00:52:42 -04:00
using System.Collections.Generic ;
using System.IO ;
using System.IO.Compression ;
using System.Linq ;
2020-04-23 17:02:13 -04:00
using System.Net ;
2019-10-10 00:52:42 -04:00
using System.Net.Http ;
using System.Net.Http.Headers ;
using System.Text ;
2020-04-23 17:02:13 -04:00
using System.Threading ;
using System.Threading.Tasks ;
2019-10-10 00:52:42 -04:00
using GitHub.DistributedTask.ObjectTemplating.Tokens ;
using GitHub.Runner.Common ;
using GitHub.Runner.Sdk ;
using GitHub.Runner.Worker.Container ;
using GitHub.Services.Common ;
2020-06-09 08:53:28 -04:00
using WebApi = GitHub . DistributedTask . WebApi ;
2019-10-10 00:52:42 -04:00
using Pipelines = GitHub . DistributedTask . Pipelines ;
using PipelineTemplateConstants = GitHub . DistributedTask . Pipelines . ObjectTemplating . PipelineTemplateConstants ;
namespace GitHub.Runner.Worker
{
2020-04-13 21:46:30 -04:00
public class PrepareResult
{
public PrepareResult ( List < JobExtensionRunner > containerSetupSteps , Dictionary < Guid , IActionRunner > preStepTracker )
{
this . ContainerSetupSteps = containerSetupSteps ;
this . PreStepTracker = preStepTracker ;
}
public List < JobExtensionRunner > ContainerSetupSteps { get ; set ; }
public Dictionary < Guid , IActionRunner > PreStepTracker { get ; set ; }
}
2019-10-10 00:52:42 -04:00
[ServiceLocator(Default = typeof(ActionManager))]
public interface IActionManager : IRunnerService
{
Dictionary < Guid , ContainerInfo > CachedActionContainers { get ; }
2020-04-13 21:46:30 -04:00
Task < PrepareResult > PrepareActionsAsync ( IExecutionContext executionContext , IEnumerable < Pipelines . JobStep > steps );
2019-10-10 00:52:42 -04:00
Definition LoadAction ( IExecutionContext executionContext , Pipelines . ActionStep action );
}
public sealed class ActionManager : RunnerService , IActionManager
{
private const int _defaultFileStreamBufferSize = 4096 ;
2020-01-20 18:22:59 +01:00
//81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k).
2019-10-10 00:52:42 -04:00
private const int _defaultCopyBufferSize = 81920 ;
2020-05-11 12:23:02 -04:00
private const string _dotcomApiUrl = "https://api.github.com" ;
2019-10-10 00:52:42 -04:00
private readonly Dictionary < Guid , ContainerInfo > _cachedActionContainers = new Dictionary < Guid , ContainerInfo >();
public Dictionary < Guid , ContainerInfo > CachedActionContainers => _cachedActionContainers ;
2020-04-13 21:46:30 -04:00
public async Task < PrepareResult > PrepareActionsAsync ( IExecutionContext executionContext , IEnumerable < Pipelines . JobStep > steps )
2019-10-10 00:52:42 -04:00
{
ArgUtil . NotNull ( executionContext , nameof ( executionContext ));
ArgUtil . NotNull ( steps , nameof ( steps ));
executionContext . Output ( "Prepare all required actions" );
Dictionary < string , List < Guid >> imagesToPull = new Dictionary < string , List < Guid >>( StringComparer . OrdinalIgnoreCase );
Dictionary < string , List < Guid >> imagesToBuild = new Dictionary < string , List < Guid >>( StringComparer . OrdinalIgnoreCase );
Dictionary < string , ActionContainer > imagesToBuildInfo = new Dictionary < string , ActionContainer >( StringComparer . OrdinalIgnoreCase );
List < JobExtensionRunner > containerSetupSteps = new List < JobExtensionRunner >();
2020-04-13 21:46:30 -04:00
Dictionary < Guid , IActionRunner > preStepTracker = new Dictionary < Guid , IActionRunner >();
2019-10-10 00:52:42 -04:00
IEnumerable < Pipelines . ActionStep > actions = steps . OfType < Pipelines . ActionStep >();
2020-03-30 07:45:06 -04:00
// TODO: Deprecate the PREVIEW_ACTION_TOKEN
2019-10-10 00:52:42 -04:00
// Log even if we aren't using it to ensure users know.
if (! string . IsNullOrEmpty ( executionContext . Variables . Get ( "PREVIEW_ACTION_TOKEN" )))
{
2020-03-30 07:45:06 -04:00
executionContext . Warning ( "The 'PREVIEW_ACTION_TOKEN' secret is deprecated. Please remove it from the repository's secrets" );
2019-10-10 00:52:42 -04:00
}
2020-03-24 21:51:37 -04:00
// Clear the cache (for self-hosted runners)
2020-04-23 17:02:13 -04:00
IOUtil . DeleteDirectory ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), executionContext . CancellationToken );
2019-10-28 11:56:12 -04:00
2020-06-02 17:21:50 -04:00
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
var newActionMetadata = executionContext . Variables . GetBoolean ( "DistributedTask.NewActionMetadata" ) ?? false ;
var repositoryActions = new List < Pipelines . ActionStep >();
2019-10-10 00:52:42 -04:00
foreach ( var action in actions )
{
if ( action . Reference . Type == Pipelines . ActionSourceType . ContainerRegistry )
{
ArgUtil . NotNull ( action , nameof ( action ));
var containerReference = action . Reference as Pipelines . ContainerRegistryReference ;
ArgUtil . NotNull ( containerReference , nameof ( containerReference ));
ArgUtil . NotNullOrEmpty ( containerReference . Image , nameof ( containerReference . Image ));
if (! imagesToPull . ContainsKey ( containerReference . Image ))
{
imagesToPull [ containerReference . Image ] = new List < Guid >();
}
Trace . Info ( $"Action {action.Name} ({action.Id}) needs to pull image '{containerReference.Image}'" );
imagesToPull [ containerReference . Image ]. Add ( action . Id );
}
2020-06-02 17:21:50 -04:00
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
else if ( action . Reference . Type == Pipelines . ActionSourceType . Repository && ! newActionMetadata )
2019-10-10 00:52:42 -04:00
{
// only download the repository archive
await DownloadRepositoryActionAsync ( executionContext , action );
// more preparation base on content in the repository (action.yml)
var setupInfo = PrepareRepositoryActionAsync ( executionContext , action );
if ( setupInfo != null )
{
if (! string . IsNullOrEmpty ( setupInfo . Image ))
{
if (! imagesToPull . ContainsKey ( setupInfo . Image ))
{
imagesToPull [ setupInfo . Image ] = new List < Guid >();
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to pull image '{setupInfo.Image}'" );
imagesToPull [ setupInfo . Image ]. Add ( action . Id );
}
else
{
ArgUtil . NotNullOrEmpty ( setupInfo . ActionRepository , nameof ( setupInfo . ActionRepository ));
if (! imagesToBuild . ContainsKey ( setupInfo . ActionRepository ))
{
imagesToBuild [ setupInfo . ActionRepository ] = new List < Guid >();
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to build image '{setupInfo.Dockerfile}'" );
imagesToBuild [ setupInfo . ActionRepository ]. Add ( action . Id );
imagesToBuildInfo [ setupInfo . ActionRepository ] = setupInfo ;
}
}
2020-04-13 21:46:30 -04:00
2020-06-02 17:21:50 -04:00
var repoAction = action . Reference as Pipelines . RepositoryPathReference ;
if ( repoAction . RepositoryType != Pipelines . PipelineConstants . SelfAlias )
{
var definition = LoadAction ( executionContext , action );
if ( definition . Data . Execution . HasPre )
{
var actionRunner = HostContext . CreateService < IActionRunner >();
actionRunner . Action = action ;
actionRunner . Stage = ActionRunStage . Pre ;
actionRunner . Condition = definition . Data . Execution . InitCondition ;
Trace . Info ( $"Add 'pre' execution for {action.Id}" );
preStepTracker [ action . Id ] = actionRunner ;
}
}
}
else if ( action . Reference . Type == Pipelines . ActionSourceType . Repository && newActionMetadata )
{
repositoryActions . Add ( action );
}
}
if ( repositoryActions . Count > 0 )
{
// Get the download info
var downloadInfos = await GetDownloadInfoAsync ( executionContext , repositoryActions );
// Download each action
foreach ( var action in repositoryActions )
{
var lookupKey = GetDownloadInfoLookupKey ( action );
if ( string . IsNullOrEmpty ( lookupKey ))
{
continue ;
}
if (! downloadInfos . TryGetValue ( lookupKey , out var downloadInfo ))
{
throw new Exception ( $"Missing download info for {lookupKey}" );
}
await DownloadRepositoryActionAsync ( executionContext , downloadInfo );
}
// More preparation based on content in the repository (action.yml)
foreach ( var action in repositoryActions )
{
var setupInfo = PrepareRepositoryActionAsync ( executionContext , action );
if ( setupInfo != null )
{
if (! string . IsNullOrEmpty ( setupInfo . Image ))
{
if (! imagesToPull . ContainsKey ( setupInfo . Image ))
{
imagesToPull [ setupInfo . Image ] = new List < Guid >();
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to pull image '{setupInfo.Image}'" );
imagesToPull [ setupInfo . Image ]. Add ( action . Id );
}
else
{
ArgUtil . NotNullOrEmpty ( setupInfo . ActionRepository , nameof ( setupInfo . ActionRepository ));
if (! imagesToBuild . ContainsKey ( setupInfo . ActionRepository ))
{
imagesToBuild [ setupInfo . ActionRepository ] = new List < Guid >();
}
Trace . Info ( $"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to build image '{setupInfo.Dockerfile}'" );
imagesToBuild [ setupInfo . ActionRepository ]. Add ( action . Id );
imagesToBuildInfo [ setupInfo . ActionRepository ] = setupInfo ;
}
}
2020-04-13 21:46:30 -04:00
var repoAction = action . Reference as Pipelines . RepositoryPathReference ;
if ( repoAction . RepositoryType != Pipelines . PipelineConstants . SelfAlias )
{
var definition = LoadAction ( executionContext , action );
if ( definition . Data . Execution . HasPre )
{
var actionRunner = HostContext . CreateService < IActionRunner >();
actionRunner . Action = action ;
actionRunner . Stage = ActionRunStage . Pre ;
actionRunner . Condition = definition . Data . Execution . InitCondition ;
Trace . Info ( $"Add 'pre' execution for {action.Id}" );
preStepTracker [ action . Id ] = actionRunner ;
}
}
2019-10-10 00:52:42 -04:00
}
}
if ( imagesToPull . Count > 0 )
{
foreach ( var imageToPull in imagesToPull )
{
Trace . Info ( $"{imageToPull.Value.Count} steps need to pull image '{imageToPull.Key}'" );
containerSetupSteps . Add ( new JobExtensionRunner ( runAsync : this . PullActionContainerAsync ,
condition : $"{PipelineTemplateConstants.Success}()" ,
displayName : $"Pull {imageToPull.Key}" ,
data : new ContainerSetupInfo ( imageToPull . Value , imageToPull . Key )));
}
}
if ( imagesToBuild . Count > 0 )
{
foreach ( var imageToBuild in imagesToBuild )
{
var setupInfo = imagesToBuildInfo [ imageToBuild . Key ];
Trace . Info ( $"{imageToBuild.Value.Count} steps need to build image from '{setupInfo.Dockerfile}'" );
containerSetupSteps . Add ( new JobExtensionRunner ( runAsync : this . BuildActionContainerAsync ,
condition : $"{PipelineTemplateConstants.Success}()" ,
displayName : $"Build {setupInfo.ActionRepository}" ,
data : new ContainerSetupInfo ( imageToBuild . Value , setupInfo . Dockerfile , setupInfo . WorkingDirectory )));
}
}
#if ! OS_LINUX
if ( containerSetupSteps . Count > 0 )
{
executionContext . Output ( "Container action is only supported on Linux, skip pull and build docker images." );
containerSetupSteps . Clear ();
}
#endif
2020-04-13 21:46:30 -04:00
return new PrepareResult ( containerSetupSteps , preStepTracker );
2019-10-10 00:52:42 -04:00
}
public Definition LoadAction ( IExecutionContext executionContext , Pipelines . ActionStep action )
{
// Validate args.
Trace . Entering ();
ArgUtil . NotNull ( action , nameof ( action ));
// Initialize the definition wrapper object.
var definition = new Definition ()
{
Data = new ActionDefinitionData ()
};
if ( action . Reference . Type == Pipelines . ActionSourceType . ContainerRegistry )
{
Trace . Info ( "Load action that reference container from registry." );
CachedActionContainers . TryGetValue ( action . Id , out var container );
ArgUtil . NotNull ( container , nameof ( container ));
definition . Data . Execution = new ContainerActionExecutionData ()
{
Image = container . ContainerImage
};
Trace . Info ( $"Using action container image: {container.ContainerImage}." );
}
else if ( action . Reference . Type == Pipelines . ActionSourceType . Repository )
{
string actionDirectory = null ;
var repoAction = action . Reference as Pipelines . RepositoryPathReference ;
if ( string . Equals ( repoAction . RepositoryType , Pipelines . PipelineConstants . SelfAlias , StringComparison . OrdinalIgnoreCase ))
{
actionDirectory = executionContext . GetGitHubContext ( "workspace" );
if (! string . IsNullOrEmpty ( repoAction . Path ))
{
actionDirectory = Path . Combine ( actionDirectory , repoAction . Path );
}
}
else
{
actionDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), repoAction . Name . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ), repoAction . Ref );
if (! string . IsNullOrEmpty ( repoAction . Path ))
{
actionDirectory = Path . Combine ( actionDirectory , repoAction . Path );
}
}
Trace . Info ( $"Load action that reference repository from '{actionDirectory}'" );
definition . Directory = actionDirectory ;
2020-01-20 18:22:59 +01:00
string manifestFile = Path . Combine ( actionDirectory , Constants . Path . ActionManifestYmlFile );
string manifestFileYaml = Path . Combine ( actionDirectory , Constants . Path . ActionManifestYamlFile );
2019-10-10 00:52:42 -04:00
string dockerFile = Path . Combine ( actionDirectory , "Dockerfile" );
string dockerFileLowerCase = Path . Combine ( actionDirectory , "dockerfile" );
2020-01-20 18:22:59 +01:00
if ( File . Exists ( manifestFile ) || File . Exists ( manifestFileYaml ))
2019-10-10 00:52:42 -04:00
{
var manifestManager = HostContext . GetService < IActionManifestManager >();
2020-01-20 18:22:59 +01:00
if ( File . Exists ( manifestFile ))
{
definition . Data = manifestManager . Load ( executionContext , manifestFile );
}
else
{
definition . Data = manifestManager . Load ( executionContext , manifestFileYaml );
}
2019-10-10 00:52:42 -04:00
Trace . Verbose ( $"Action friendly name: '{definition.Data.Name}'" );
Trace . Verbose ( $"Action description: '{definition.Data.Description}'" );
if ( definition . Data . Inputs != null )
{
foreach ( var input in definition . Data . Inputs )
{
Trace . Verbose ( $"Action input: '{input.Key.ToString()}' default to '{input.Value.ToString()}'" );
}
}
if ( definition . Data . Execution . ExecutionType == ActionExecutionType . Container )
{
var containerAction = definition . Data . Execution as ContainerActionExecutionData ;
Trace . Info ( $"Action container Dockerfile/image: {containerAction.Image}." );
if ( containerAction . Arguments != null )
{
Trace . Info ( $"Action container args: {StringUtil.ConvertToJson(containerAction.Arguments)}." );
}
if ( containerAction . Environment != null )
{
Trace . Info ( $"Action container env: {StringUtil.ConvertToJson(containerAction.Environment)}." );
}
2020-04-13 21:46:30 -04:00
if (! string . IsNullOrEmpty ( containerAction . Pre ))
{
Trace . Info ( $"Action container pre entrypoint: {containerAction.Pre}." );
}
2019-10-10 00:52:42 -04:00
if (! string . IsNullOrEmpty ( containerAction . EntryPoint ))
{
Trace . Info ( $"Action container entrypoint: {containerAction.EntryPoint}." );
}
2020-04-13 21:46:30 -04:00
if (! string . IsNullOrEmpty ( containerAction . Post ))
2019-10-10 00:52:42 -04:00
{
2020-04-13 21:46:30 -04:00
Trace . Info ( $"Action container post entrypoint: {containerAction.Post}." );
2019-10-10 00:52:42 -04:00
}
if ( CachedActionContainers . TryGetValue ( action . Id , out var container ))
{
Trace . Info ( $"Image '{containerAction.Image}' already built/pulled, use image: {container.ContainerImage}." );
containerAction . Image = container . ContainerImage ;
}
}
else if ( definition . Data . Execution . ExecutionType == ActionExecutionType . NodeJS )
{
var nodeAction = definition . Data . Execution as NodeJSActionExecutionData ;
2020-04-13 21:46:30 -04:00
Trace . Info ( $"Action pre node.js file: {nodeAction.Pre ?? " N / A "}." );
2019-10-10 00:52:42 -04:00
Trace . Info ( $"Action node.js file: {nodeAction.Script}." );
2020-04-13 21:46:30 -04:00
Trace . Info ( $"Action post node.js file: {nodeAction.Post ?? " N / A "}." );
2019-10-10 00:52:42 -04:00
}
else if ( definition . Data . Execution . ExecutionType == ActionExecutionType . Plugin )
{
var pluginAction = definition . Data . Execution as PluginActionExecutionData ;
var pluginManager = HostContext . GetService < IRunnerPluginManager >();
var plugin = pluginManager . GetPluginAction ( pluginAction . Plugin );
ArgUtil . NotNull ( plugin , pluginAction . Plugin );
ArgUtil . NotNullOrEmpty ( plugin . PluginTypeName , pluginAction . Plugin );
pluginAction . Plugin = plugin . PluginTypeName ;
Trace . Info ( $"Action plugin: {plugin.PluginTypeName}." );
if (! string . IsNullOrEmpty ( plugin . PostPluginTypeName ))
{
2020-04-13 21:46:30 -04:00
pluginAction . Post = plugin . PostPluginTypeName ;
2019-10-10 00:52:42 -04:00
Trace . Info ( $"Action cleanup plugin: {plugin.PluginTypeName}." );
}
}
else
{
throw new NotSupportedException ( definition . Data . Execution . ExecutionType . ToString ());
}
}
else if ( File . Exists ( dockerFile ))
{
if ( CachedActionContainers . TryGetValue ( action . Id , out var container ))
{
definition . Data . Execution = new ContainerActionExecutionData ()
{
Image = container . ContainerImage
};
}
else
{
definition . Data . Execution = new ContainerActionExecutionData ()
{
Image = dockerFile
};
}
}
else if ( File . Exists ( dockerFileLowerCase ))
{
if ( CachedActionContainers . TryGetValue ( action . Id , out var container ))
{
definition . Data . Execution = new ContainerActionExecutionData ()
{
Image = container . ContainerImage
};
}
else
{
definition . Data . Execution = new ContainerActionExecutionData ()
{
Image = dockerFileLowerCase
};
}
}
else
{
var fullPath = IOUtil . ResolvePath ( actionDirectory , "." ); // resolve full path without access filesystem.
2020-01-20 18:22:59 +01:00
throw new NotSupportedException ( $"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?" );
2019-10-10 00:52:42 -04:00
}
}
else if ( action . Reference . Type == Pipelines . ActionSourceType . Script )
{
definition . Data . Execution = new ScriptActionExecutionData ();
definition . Data . Name = "Run" ;
definition . Data . Description = "Execute a script" ;
}
else
{
throw new NotSupportedException ( action . Reference . Type . ToString ());
}
return definition ;
}
private async Task PullActionContainerAsync ( IExecutionContext executionContext , object data )
{
var setupInfo = data as ContainerSetupInfo ;
ArgUtil . NotNull ( setupInfo , nameof ( setupInfo ));
ArgUtil . NotNullOrEmpty ( setupInfo . Container . Image , nameof ( setupInfo . Container . Image ));
executionContext . Output ( $"Pull down action image '{setupInfo.Container.Image}'" );
// Pull down docker image with retry up to 3 times
var dockerManger = HostContext . GetService < IDockerCommandManager >();
int retryCount = 0 ;
int pullExitCode = 0 ;
while ( retryCount < 3 )
{
pullExitCode = await dockerManger . DockerPull ( executionContext , setupInfo . Container . Image );
if ( pullExitCode == 0 )
{
break ;
}
else
{
retryCount ++;
if ( retryCount < 3 )
{
var backOff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 1 ), TimeSpan . FromSeconds ( 10 ));
executionContext . Warning ( $"Docker pull failed with exit code {pullExitCode}, back off {backOff.TotalSeconds} seconds before retry." );
await Task . Delay ( backOff );
}
}
}
if ( retryCount == 3 && pullExitCode != 0 )
{
throw new InvalidOperationException ( $"Docker pull failed with exit code {pullExitCode}" );
}
foreach ( var stepId in setupInfo . StepIds )
{
CachedActionContainers [ stepId ] = new ContainerInfo () { ContainerImage = setupInfo . Container . Image };
Trace . Info ( $"Prepared docker image '{setupInfo.Container.Image}' for action {stepId} ({setupInfo.Container.Image})" );
}
}
private async Task BuildActionContainerAsync ( IExecutionContext executionContext , object data )
{
var setupInfo = data as ContainerSetupInfo ;
ArgUtil . NotNull ( setupInfo , nameof ( setupInfo ));
ArgUtil . NotNullOrEmpty ( setupInfo . Container . Dockerfile , nameof ( setupInfo . Container . Dockerfile ));
executionContext . Output ( $"Build container for action use: '{setupInfo.Container.Dockerfile}'." );
// Build docker image with retry up to 3 times
var dockerManger = HostContext . GetService < IDockerCommandManager >();
int retryCount = 0 ;
int buildExitCode = 0 ;
var imageName = $"{dockerManger.DockerInstanceLabel}:{Guid.NewGuid().ToString(" N ")}" ;
while ( retryCount < 3 )
{
2020-05-11 13:57:31 -04:00
buildExitCode = await dockerManger . DockerBuild (
executionContext ,
setupInfo . Container . WorkingDirectory ,
setupInfo . Container . Dockerfile ,
Directory . GetParent ( setupInfo . Container . Dockerfile ). FullName ,
imageName );
2019-10-10 00:52:42 -04:00
if ( buildExitCode == 0 )
{
break ;
}
else
{
retryCount ++;
if ( retryCount < 3 )
{
var backOff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 1 ), TimeSpan . FromSeconds ( 10 ));
executionContext . Warning ( $"Docker build failed with exit code {buildExitCode}, back off {backOff.TotalSeconds} seconds before retry." );
await Task . Delay ( backOff );
}
}
}
if ( retryCount == 3 && buildExitCode != 0 )
{
throw new InvalidOperationException ( $"Docker build failed with exit code {buildExitCode}" );
}
foreach ( var stepId in setupInfo . StepIds )
{
CachedActionContainers [ stepId ] = new ContainerInfo () { ContainerImage = imageName };
Trace . Info ( $"Prepared docker image '{imageName}' for action {stepId} ({setupInfo.Container.Dockerfile})" );
}
}
2020-06-02 17:21:50 -04:00
// This implementation is temporary and will be replaced with a REST API call to the service to resolve
2020-06-09 08:53:28 -04:00
private async Task < IDictionary < string , WebApi . ActionDownloadInfo >> GetDownloadInfoAsync ( IExecutionContext executionContext , List < Pipelines . ActionStep > actions )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
executionContext . Output ( "Getting action download info" );
2020-06-02 17:21:50 -04:00
2020-06-09 08:53:28 -04:00
// Convert to action reference
var actionReferences = actions
. GroupBy ( x => GetDownloadInfoLookupKey ( x ))
. Where ( x => ! string . IsNullOrEmpty ( x . Key ))
. Select ( x =>
{
var action = x . First ();
var repositoryReference = action . Reference as Pipelines . RepositoryPathReference ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ));
return new WebApi . ActionReference
{
NameWithOwner = repositoryReference . Name ,
Ref = repositoryReference . Ref ,
};
})
. ToList ();
// Nothing to resolve?
if ( actionReferences . Count == 0 )
{
return new Dictionary < string , WebApi . ActionDownloadInfo >();
}
// Resolve download info
var jobServer = HostContext . GetService < IJobServer >();
var actionDownloadInfos = default ( WebApi . ActionDownloadInfoCollection );
for ( var attempt = 1 ; attempt <= 3 ; attempt ++)
{
try
{
actionDownloadInfos = await jobServer . ResolveActionDownloadInfoAsync ( executionContext . Plan . ScopeIdentifier , executionContext . Plan . PlanType , executionContext . Plan . PlanId , new WebApi . ActionReferenceList { Actions = actionReferences }, executionContext . CancellationToken );
break ;
}
catch ( Exception ex ) when ( attempt < 3 )
{
executionContext . Output ( $"Failed to resolve action download info. Error: {ex.Message}" );
executionContext . Debug ( ex . ToString ());
if ( String . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF" )))
{
var backoff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 10 ), TimeSpan . FromSeconds ( 30 ));
executionContext . Output ( $"Retrying in {backoff.TotalSeconds} seconds" );
await Task . Delay ( backoff );
}
}
}
ArgUtil . NotNull ( actionDownloadInfos , nameof ( actionDownloadInfos ));
ArgUtil . NotNull ( actionDownloadInfos . Actions , nameof ( actionDownloadInfos . Actions ));
var apiUrl = GetApiUrl ( executionContext );
var defaultAccessToken = executionContext . GetGitHubContext ( "token" );
2020-06-02 17:21:50 -04:00
var configurationStore = HostContext . GetService < IConfigurationStore >();
var runnerSettings = configurationStore . GetSettings ();
2020-06-09 08:53:28 -04:00
foreach ( var actionDownloadInfo in actionDownloadInfos . Actions . Values )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
// Add secret
HostContext . SecretMasker . AddValue ( actionDownloadInfo . Authentication ?. Token );
2020-06-02 17:21:50 -04:00
2020-06-15 13:13:47 -04:00
// Default auth token
if ( string . IsNullOrEmpty ( actionDownloadInfo . Authentication ?. Token ))
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
actionDownloadInfo . Authentication = new WebApi . ActionDownloadAuthentication { Token = defaultAccessToken };
2020-06-02 17:21:50 -04:00
}
}
2020-06-09 08:53:28 -04:00
return actionDownloadInfos . Actions ;
2020-06-02 17:21:50 -04:00
}
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
2019-10-10 00:52:42 -04:00
private async Task DownloadRepositoryActionAsync ( IExecutionContext executionContext , Pipelines . ActionStep repositoryAction )
{
Trace . Entering ();
ArgUtil . NotNull ( executionContext , nameof ( executionContext ));
var repositoryReference = repositoryAction . Reference as Pipelines . RepositoryPathReference ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ));
if ( string . Equals ( repositoryReference . RepositoryType , Pipelines . PipelineConstants . SelfAlias , StringComparison . OrdinalIgnoreCase ))
{
Trace . Info ( $"Repository action is in 'self' repository." );
return ;
}
if (! string . Equals ( repositoryReference . RepositoryType , Pipelines . RepositoryTypes . GitHub , StringComparison . OrdinalIgnoreCase ))
{
throw new NotSupportedException ( repositoryReference . RepositoryType );
}
ArgUtil . NotNullOrEmpty ( repositoryReference . Name , nameof ( repositoryReference . Name ));
ArgUtil . NotNullOrEmpty ( repositoryReference . Ref , nameof ( repositoryReference . Ref ));
string destDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), repositoryReference . Name . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ), repositoryReference . Ref );
2020-04-23 17:02:13 -04:00
string watermarkFile = GetWatermarkFilePath ( destDirectory );
2020-03-24 21:51:37 -04:00
if ( File . Exists ( watermarkFile ))
2019-10-10 00:52:42 -04:00
{
executionContext . Debug ( $"Action '{repositoryReference.Name}@{repositoryReference.Ref}' already downloaded at '{destDirectory}'." );
return ;
}
else
{
2019-10-28 11:56:12 -04:00
// make sure we get a clean folder ready to use.
2019-10-10 00:52:42 -04:00
IOUtil . DeleteDirectory ( destDirectory , executionContext . CancellationToken );
Directory . CreateDirectory ( destDirectory );
executionContext . Output ( $"Download action repository '{repositoryReference.Name}@{repositoryReference.Ref}'" );
}
2020-04-23 17:02:13 -04:00
var configurationStore = HostContext . GetService < IConfigurationStore >();
var isHostedServer = configurationStore . GetSettings (). IsHostedServer ;
if ( isHostedServer )
{
string apiUrl = GetApiUrl ( executionContext );
string archiveLink = BuildLinkToActionArchive ( apiUrl , repositoryReference . Name , repositoryReference . Ref );
2020-05-11 12:23:02 -04:00
var downloadDetails = new ActionDownloadDetails ( archiveLink , ConfigureAuthorizationFromContext );
2020-06-02 17:21:50 -04:00
await DownloadRepositoryActionAsync ( executionContext , downloadDetails , null , destDirectory );
2020-04-23 17:02:13 -04:00
return ;
}
else
{
string apiUrl = GetApiUrl ( executionContext );
2019-10-10 00:52:42 -04:00
2020-04-23 17:02:13 -04:00
// URLs to try:
2020-05-11 12:23:02 -04:00
var downloadAttempts = new List < ActionDownloadDetails > {
2020-04-23 17:02:13 -04:00
// A built-in action or an action the user has created, on their GHES instance
// Example: https://my-ghes/api/v3/repos/my-org/my-action/tarball/v1
2020-05-11 12:23:02 -04:00
new ActionDownloadDetails (
BuildLinkToActionArchive ( apiUrl , repositoryReference . Name , repositoryReference . Ref ),
ConfigureAuthorizationFromContext ),
2020-04-23 17:02:13 -04:00
2020-05-11 12:23:02 -04:00
// The same action, on GitHub.com
// Example: https://api.github.com/repos/my-org/my-action/tarball/v1
new ActionDownloadDetails (
BuildLinkToActionArchive ( _dotcomApiUrl , repositoryReference . Name , repositoryReference . Ref ),
configureAuthorization : ( e , h ) => { /* no authorization for dotcom */ })
2020-04-23 17:02:13 -04:00
};
2020-05-11 12:23:02 -04:00
foreach ( var downloadAttempt in downloadAttempts )
2020-04-23 17:02:13 -04:00
{
try
{
2020-06-02 17:21:50 -04:00
await DownloadRepositoryActionAsync ( executionContext , downloadAttempt , null , destDirectory );
2020-04-23 17:02:13 -04:00
return ;
}
catch ( ActionNotFoundException )
{
2020-05-11 12:23:02 -04:00
Trace . Info ( $"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}' at {downloadAttempt.ArchiveLink}" );
2020-04-23 17:02:13 -04:00
continue ;
}
}
2020-05-11 12:23:02 -04:00
throw new ActionNotFoundException ( $"Failed to find the action '{repositoryReference.Name}' at ref '{repositoryReference.Ref}'. Paths attempted: {string.Join(" , ", downloadAttempts.Select(d => d.ArchiveLink))}" );
2020-04-23 17:02:13 -04:00
}
}
2020-06-09 08:53:28 -04:00
private async Task DownloadRepositoryActionAsync ( IExecutionContext executionContext , WebApi . ActionDownloadInfo downloadInfo )
2020-06-02 17:21:50 -04:00
{
Trace . Entering ();
ArgUtil . NotNull ( executionContext , nameof ( executionContext ));
ArgUtil . NotNull ( downloadInfo , nameof ( downloadInfo ));
ArgUtil . NotNullOrEmpty ( downloadInfo . NameWithOwner , nameof ( downloadInfo . NameWithOwner ));
ArgUtil . NotNullOrEmpty ( downloadInfo . Ref , nameof ( downloadInfo . Ref ));
string destDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), downloadInfo . NameWithOwner . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ), downloadInfo . Ref );
string watermarkFile = GetWatermarkFilePath ( destDirectory );
if ( File . Exists ( watermarkFile ))
{
executionContext . Debug ( $"Action '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}' already downloaded at '{destDirectory}'." );
return ;
}
else
{
// make sure we get a clean folder ready to use.
IOUtil . DeleteDirectory ( destDirectory , executionContext . CancellationToken );
Directory . CreateDirectory ( destDirectory );
executionContext . Output ( $"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'" );
}
await DownloadRepositoryActionAsync ( executionContext , null , downloadInfo , destDirectory );
}
2020-04-23 17:02:13 -04:00
private string GetApiUrl ( IExecutionContext executionContext )
{
string apiUrl = executionContext . GetGitHubContext ( "api_url" );
if (! string . IsNullOrEmpty ( apiUrl ))
{
return apiUrl ;
}
// Once the api_url is set for hosted, we can remove this fallback (it doesn't make sense for GHES)
2020-05-11 12:23:02 -04:00
return _dotcomApiUrl ;
2020-04-23 17:02:13 -04:00
}
private static string BuildLinkToActionArchive ( string apiUrl , string repository , string @ref )
{
#if OS_WINDOWS
return $"{apiUrl}/repos/{repository}/zipball/{@ref}" ;
#else
return $"{apiUrl}/repos/{repository}/tarball/{@ref}" ;
#endif
}
2020-06-02 17:21:50 -04:00
// todo: Remove the parameter "actionDownloadDetails" when feature flag DistributedTask.NewActionMetadata is removed
2020-06-09 08:53:28 -04:00
private async Task DownloadRepositoryActionAsync ( IExecutionContext executionContext , ActionDownloadDetails actionDownloadDetails , WebApi . ActionDownloadInfo downloadInfo , string destDirectory )
2020-04-23 17:02:13 -04:00
{
2019-10-10 00:52:42 -04:00
//download and extract action in a temp folder and rename it on success
string tempDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), "_temp_" + Guid . NewGuid ());
Directory . CreateDirectory ( tempDirectory );
#if OS_WINDOWS
string archiveFile = Path . Combine ( tempDirectory , $"{Guid.NewGuid()}.zip" );
2020-06-09 08:53:28 -04:00
string link = downloadInfo ?. ZipballUrl ?? actionDownloadDetails . ArchiveLink ;
2019-10-10 00:52:42 -04:00
#else
string archiveFile = Path . Combine ( tempDirectory , $"{Guid.NewGuid()}.tar.gz" );
2020-06-09 08:53:28 -04:00
string link = downloadInfo ?. TarballUrl ?? actionDownloadDetails . ArchiveLink ;
2019-10-10 00:52:42 -04:00
#endif
2020-04-23 17:02:13 -04:00
Trace . Info ( $"Save archive '{link}' into {archiveFile}." );
2019-10-10 00:52:42 -04:00
try
{
int retryCount = 0 ;
2020-01-20 18:22:59 +01:00
// Allow up to 20 * 60s for any action to be downloaded from github graph.
2019-10-10 00:52:42 -04:00
int timeoutSeconds = 20 * 60 ;
while ( retryCount < 3 )
{
using ( var actionDownloadTimeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( timeoutSeconds )))
using ( var actionDownloadCancellation = CancellationTokenSource . CreateLinkedTokenSource ( actionDownloadTimeout . Token , executionContext . CancellationToken ))
{
try
{
//open zip stream in async mode
using ( FileStream fs = new FileStream ( archiveFile , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize : _defaultFileStreamBufferSize , useAsync : true ))
using ( var httpClientHandler = HostContext . CreateHttpClientHandler ())
using ( var httpClient = new HttpClient ( httpClientHandler ))
{
2020-06-02 17:21:50 -04:00
// Legacy
if ( downloadInfo == null )
{
actionDownloadDetails . ConfigureAuthorization ( executionContext , httpClient );
}
// FF DistributedTask.NewActionMetadata
else
{
2020-06-09 08:53:28 -04:00
httpClient . DefaultRequestHeaders . Authorization = CreateAuthHeader ( downloadInfo . Authentication ?. Token );
2020-06-02 17:21:50 -04:00
}
2019-10-10 00:52:42 -04:00
2020-04-09 14:33:16 -04:00
httpClient . DefaultRequestHeaders . UserAgent . AddRange ( HostContext . UserAgents );
2020-04-23 17:02:13 -04:00
using ( var response = await httpClient . GetAsync ( link ))
2019-10-10 00:52:42 -04:00
{
2020-04-23 17:02:13 -04:00
if ( response . IsSuccessStatusCode )
{
using ( var result = await response . Content . ReadAsStreamAsync ())
{
await result . CopyToAsync ( fs , _defaultCopyBufferSize , actionDownloadCancellation . Token );
await fs . FlushAsync ( actionDownloadCancellation . Token );
2019-10-10 00:52:42 -04:00
2020-04-23 17:02:13 -04:00
// download succeed, break out the retry loop.
break ;
}
}
else if ( response . StatusCode == HttpStatusCode . NotFound )
{
// It doesn't make sense to retry in this case, so just stop
throw new ActionNotFoundException ( new Uri ( link ));
}
else
{
// Something else bad happened, let's go to our retry logic
response . EnsureSuccessStatusCode ();
}
2019-10-10 00:52:42 -04:00
}
}
}
catch ( OperationCanceledException ) when ( executionContext . CancellationToken . IsCancellationRequested )
{
2020-04-23 17:02:13 -04:00
Trace . Info ( "Action download has been cancelled." );
throw ;
}
catch ( ActionNotFoundException )
{
Trace . Info ( $"The action at '{link}' does not exist" );
2019-10-10 00:52:42 -04:00
throw ;
}
catch ( Exception ex ) when ( retryCount < 2 )
{
retryCount ++;
2020-04-23 17:02:13 -04:00
Trace . Error ( $"Fail to download archive '{link}' -- Attempt: {retryCount}" );
2019-10-10 00:52:42 -04:00
Trace . Error ( ex );
if ( actionDownloadTimeout . Token . IsCancellationRequested )
{
// action download didn't finish within timeout
2020-04-23 17:02:13 -04:00
executionContext . Warning ( $"Action '{link}' didn't finish download within {timeoutSeconds} seconds." );
2019-10-10 00:52:42 -04:00
}
else
{
2020-04-23 17:02:13 -04:00
executionContext . Warning ( $"Failed to download action '{link}'. Error: {ex.Message}" );
2019-10-10 00:52:42 -04:00
}
}
}
if ( String . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF" )))
{
var backOff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 10 ), TimeSpan . FromSeconds ( 30 ));
executionContext . Warning ( $"Back off {backOff.TotalSeconds} seconds before retry." );
await Task . Delay ( backOff );
}
}
ArgUtil . NotNullOrEmpty ( archiveFile , nameof ( archiveFile ));
2020-04-23 17:02:13 -04:00
executionContext . Debug ( $"Download '{link}' to '{archiveFile}'" );
2019-10-10 00:52:42 -04:00
var stagingDirectory = Path . Combine ( tempDirectory , "_staging" );
Directory . CreateDirectory ( stagingDirectory );
#if OS_WINDOWS
ZipFile . ExtractToDirectory ( archiveFile , stagingDirectory );
#else
string tar = WhichUtil . Which ( "tar" , require : true , trace : Trace );
// tar -xzf
using ( var processInvoker = HostContext . CreateService < IProcessInvoker >())
{
processInvoker . OutputDataReceived += new EventHandler < ProcessDataReceivedEventArgs >(( sender , args ) =>
{
if (! string . IsNullOrEmpty ( args . Data ))
{
Trace . Info ( args . Data );
}
});
processInvoker . ErrorDataReceived += new EventHandler < ProcessDataReceivedEventArgs >(( sender , args ) =>
{
if (! string . IsNullOrEmpty ( args . Data ))
{
Trace . Error ( args . Data );
}
});
int exitCode = await processInvoker . ExecuteAsync ( stagingDirectory , tar , $"-xzf \" { archiveFile } \ "" , null , executionContext . CancellationToken );
if ( exitCode != 0 )
{
throw new NotSupportedException ( $"Can't use 'tar -xzf' extract archive file: {archiveFile}. return code: {exitCode}." );
}
}
#endif
// repository archive from github always contains a nested folder
var subDirectories = new DirectoryInfo ( stagingDirectory ). GetDirectories ();
if ( subDirectories . Length != 1 )
{
throw new InvalidOperationException ( $"'{archiveFile}' contains '{subDirectories.Length}' directories" );
}
else
{
executionContext . Debug ( $"Unwrap '{subDirectories[0].Name}' to '{destDirectory}'" );
IOUtil . CopyDirectory ( subDirectories [ 0 ]. FullName , destDirectory , executionContext . CancellationToken );
}
Trace . Verbose ( "Create watermark file indicate action download succeed." );
2020-04-23 17:02:13 -04:00
string watermarkFile = GetWatermarkFilePath ( destDirectory );
2020-03-24 21:51:37 -04:00
File . WriteAllText ( watermarkFile , DateTime . UtcNow . ToString ());
2019-10-10 00:52:42 -04:00
executionContext . Debug ( $"Archive '{archiveFile}' has been unzipped into '{destDirectory}'." );
Trace . Info ( "Finished getting action repository." );
}
finally
{
try
{
//if the temp folder wasn't moved -> wipe it
if ( Directory . Exists ( tempDirectory ))
{
Trace . Verbose ( "Deleting action temp folder: {0}" , tempDirectory );
IOUtil . DeleteDirectory ( tempDirectory , CancellationToken . None ); // Don't cancel this cleanup and should be pretty fast.
}
}
catch ( Exception ex )
{
//it is not critical if we fail to delete the temp folder
Trace . Warning ( "Failed to delete temp folder '{0}'. Exception: {1}" , tempDirectory , ex );
}
}
}
2020-06-02 17:21:50 -04:00
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
2020-05-11 12:23:02 -04:00
private void ConfigureAuthorizationFromContext ( IExecutionContext executionContext , HttpClient httpClient )
{
var authToken = Environment . GetEnvironmentVariable ( "_GITHUB_ACTION_TOKEN" );
if ( string . IsNullOrEmpty ( authToken ))
{
// TODO: Deprecate the PREVIEW_ACTION_TOKEN
authToken = executionContext . Variables . Get ( "PREVIEW_ACTION_TOKEN" );
}
if (! string . IsNullOrEmpty ( authToken ))
{
HostContext . SecretMasker . AddValue ( authToken );
var base64EncodingToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"PAT:{authToken}" ));
httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Basic" , base64EncodingToken );
}
else
{
var accessToken = executionContext . GetGitHubContext ( "token" );
var base64EncodingToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"x-access-token:{accessToken}" ));
httpClient . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Basic" , base64EncodingToken );
}
}
2020-04-23 17:02:13 -04:00
private string GetWatermarkFilePath ( string directory ) => directory + ".completed" ;
2019-10-10 00:52:42 -04:00
private ActionContainer PrepareRepositoryActionAsync ( IExecutionContext executionContext , Pipelines . ActionStep repositoryAction )
{
var repositoryReference = repositoryAction . Reference as Pipelines . RepositoryPathReference ;
if ( string . Equals ( repositoryReference . RepositoryType , Pipelines . PipelineConstants . SelfAlias , StringComparison . OrdinalIgnoreCase ))
{
Trace . Info ( $"Repository action is in 'self' repository." );
return null ;
}
var setupInfo = new ActionContainer ();
string destDirectory = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Actions ), repositoryReference . Name . Replace ( Path . AltDirectorySeparatorChar , Path . DirectorySeparatorChar ), repositoryReference . Ref );
string actionEntryDirectory = destDirectory ;
string dockerFileRelativePath = repositoryReference . Name ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ));
if (! string . IsNullOrEmpty ( repositoryReference . Path ))
{
actionEntryDirectory = Path . Combine ( destDirectory , repositoryReference . Path );
dockerFileRelativePath = $"{dockerFileRelativePath}/{repositoryReference.Path}" ;
setupInfo . ActionRepository = $"{repositoryReference.Name}/{repositoryReference.Path}@{repositoryReference.Ref}" ;
}
else
{
setupInfo . ActionRepository = $"{repositoryReference.Name}@{repositoryReference.Ref}" ;
}
// find the docker file or action.yml file
var dockerFile = Path . Combine ( actionEntryDirectory , "Dockerfile" );
var dockerFileLowerCase = Path . Combine ( actionEntryDirectory , "dockerfile" );
2020-01-20 18:22:59 +01:00
var actionManifest = Path . Combine ( actionEntryDirectory , Constants . Path . ActionManifestYmlFile );
var actionManifestYaml = Path . Combine ( actionEntryDirectory , Constants . Path . ActionManifestYamlFile );
if ( File . Exists ( actionManifest ) || File . Exists ( actionManifestYaml ))
2019-10-10 00:52:42 -04:00
{
executionContext . Debug ( $"action.yml for action: '{actionManifest}'." );
var manifestManager = HostContext . GetService < IActionManifestManager >();
2020-01-20 18:22:59 +01:00
ActionDefinitionData actionDefinitionData = null ;
if ( File . Exists ( actionManifest ))
{
actionDefinitionData = manifestManager . Load ( executionContext , actionManifest );
}
else
{
actionDefinitionData = manifestManager . Load ( executionContext , actionManifestYaml );
}
2019-10-10 00:52:42 -04:00
if ( actionDefinitionData . Execution . ExecutionType == ActionExecutionType . Container )
{
var containerAction = actionDefinitionData . Execution as ContainerActionExecutionData ;
if ( containerAction . Image . EndsWith ( "Dockerfile" ) || containerAction . Image . EndsWith ( "dockerfile" ))
{
var dockerFileFullPath = Path . Combine ( actionEntryDirectory , containerAction . Image );
executionContext . Debug ( $"Dockerfile for action: '{dockerFileFullPath}'." );
setupInfo . Dockerfile = dockerFileFullPath ;
setupInfo . WorkingDirectory = destDirectory ;
return setupInfo ;
}
else if ( containerAction . Image . StartsWith ( "docker://" , StringComparison . OrdinalIgnoreCase ))
{
var actionImage = containerAction . Image . Substring ( "docker://" . Length );
executionContext . Debug ( $"Container image for action: '{actionImage}'." );
setupInfo . Image = actionImage ;
return setupInfo ;
}
else
{
throw new NotSupportedException ( $"'{containerAction.Image}' should be either '[path]/Dockerfile' or 'docker://image[:tag]'." );
}
}
else if ( actionDefinitionData . Execution . ExecutionType == ActionExecutionType . NodeJS )
{
Trace . Info ( $"Action node.js file: {(actionDefinitionData.Execution as NodeJSActionExecutionData).Script}, no more preparation." );
return null ;
}
else if ( actionDefinitionData . Execution . ExecutionType == ActionExecutionType . Plugin )
{
Trace . Info ( $"Action plugin: {(actionDefinitionData.Execution as PluginActionExecutionData).Plugin}, no more preparation." );
return null ;
}
else
{
throw new NotSupportedException ( actionDefinitionData . Execution . ExecutionType . ToString ());
}
}
else if ( File . Exists ( dockerFile ))
{
executionContext . Debug ( $"Dockerfile for action: '{dockerFile}'." );
setupInfo . Dockerfile = dockerFile ;
setupInfo . WorkingDirectory = destDirectory ;
return setupInfo ;
}
else if ( File . Exists ( dockerFileLowerCase ))
{
executionContext . Debug ( $"Dockerfile for action: '{dockerFileLowerCase}'." );
setupInfo . Dockerfile = dockerFileLowerCase ;
setupInfo . WorkingDirectory = destDirectory ;
return setupInfo ;
}
else
{
var fullPath = IOUtil . ResolvePath ( actionEntryDirectory , "." ); // resolve full path without access filesystem.
2020-01-20 18:22:59 +01:00
throw new InvalidOperationException ( $"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?" );
2019-10-10 00:52:42 -04:00
}
}
2020-05-11 12:23:02 -04:00
2020-06-02 17:21:50 -04:00
private static string GetDownloadInfoLookupKey ( Pipelines . ActionStep action )
{
if ( action . Reference . Type != Pipelines . ActionSourceType . Repository )
{
return null ;
}
var repositoryReference = action . Reference as Pipelines . RepositoryPathReference ;
ArgUtil . NotNull ( repositoryReference , nameof ( repositoryReference ));
if ( string . Equals ( repositoryReference . RepositoryType , Pipelines . PipelineConstants . SelfAlias , StringComparison . OrdinalIgnoreCase ))
{
return null ;
}
if (! string . Equals ( repositoryReference . RepositoryType , Pipelines . RepositoryTypes . GitHub , StringComparison . OrdinalIgnoreCase ))
{
throw new NotSupportedException ( repositoryReference . RepositoryType );
}
ArgUtil . NotNullOrEmpty ( repositoryReference . Name , nameof ( repositoryReference . Name ));
ArgUtil . NotNullOrEmpty ( repositoryReference . Ref , nameof ( repositoryReference . Ref ));
return $"{repositoryReference.Name}@{repositoryReference.Ref}" ;
}
2020-06-09 08:53:28 -04:00
private static string GetDownloadInfoLookupKey ( WebApi . ActionDownloadInfo info )
2020-06-02 17:21:50 -04:00
{
2020-06-09 08:53:28 -04:00
ArgUtil . NotNullOrEmpty ( info . NameWithOwner , nameof ( info . NameWithOwner ));
ArgUtil . NotNullOrEmpty ( info . Ref , nameof ( info . Ref ));
return $"{info.NameWithOwner}@{info.Ref}" ;
}
private AuthenticationHeaderValue CreateAuthHeader ( string token )
{
if ( string . IsNullOrEmpty ( token ))
2020-06-02 17:21:50 -04:00
{
return null ;
}
2020-06-09 08:53:28 -04:00
var base64EncodingToken = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"x-access-token:{token}" ));
HostContext . SecretMasker . AddValue ( base64EncodingToken );
return new AuthenticationHeaderValue ( "Basic" , base64EncodingToken );
2020-06-02 17:21:50 -04:00
}
// todo: Remove when feature flag DistributedTask.NewActionMetadata is removed
2020-05-11 12:23:02 -04:00
private class ActionDownloadDetails
{
public string ArchiveLink { get ; }
public Action < IExecutionContext , HttpClient > ConfigureAuthorization { get ; }
public ActionDownloadDetails ( string archiveLink , Action < IExecutionContext , HttpClient > configureAuthorization )
{
ArchiveLink = archiveLink ;
ConfigureAuthorization = configureAuthorization ;
}
}
2019-10-10 00:52:42 -04:00
}
public sealed class Definition
{
public ActionDefinitionData Data { get ; set ; }
public string Directory { get ; set ; }
}
public sealed class ActionDefinitionData
{
public string Name { get ; set ; }
public string Description { get ; set ; }
public MappingToken Inputs { get ; set ; }
public ActionExecutionData Execution { get ; set ; }
public Dictionary < String , String > Deprecated { get ; set ; }
}
public enum ActionExecutionType
{
Container ,
NodeJS ,
Plugin ,
Script ,
}
public sealed class ContainerActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . Container ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => ! string . IsNullOrEmpty ( Pre );
public override bool HasPost => ! string . IsNullOrEmpty ( Post );
2019-10-10 00:52:42 -04:00
public string Image { get ; set ; }
public string EntryPoint { get ; set ; }
public SequenceToken Arguments { get ; set ; }
public MappingToken Environment { get ; set ; }
2020-04-13 21:46:30 -04:00
public string Pre { get ; set ; }
public string Post { get ; set ; }
2019-10-10 00:52:42 -04:00
}
public sealed class NodeJSActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . NodeJS ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => ! string . IsNullOrEmpty ( Pre );
public override bool HasPost => ! string . IsNullOrEmpty ( Post );
2019-10-10 00:52:42 -04:00
public string Script { get ; set ; }
2020-04-13 21:46:30 -04:00
public string Pre { get ; set ; }
public string Post { get ; set ; }
2019-10-10 00:52:42 -04:00
}
public sealed class PluginActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . Plugin ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => false ;
public override bool HasPost => ! string . IsNullOrEmpty ( Post );
2019-10-10 00:52:42 -04:00
public string Plugin { get ; set ; }
2020-04-13 21:46:30 -04:00
public string Post { get ; set ; }
2019-10-10 00:52:42 -04:00
}
public sealed class ScriptActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType . Script ;
2020-04-13 21:46:30 -04:00
public override bool HasPre => false ;
public override bool HasPost => false ;
2019-10-10 00:52:42 -04:00
}
public abstract class ActionExecutionData
{
2020-04-13 21:46:30 -04:00
private string _initCondition = $"{Constants.Expressions.Always}()" ;
2019-10-10 00:52:42 -04:00
private string _cleanupCondition = $"{Constants.Expressions.Always}()" ;
public abstract ActionExecutionType ExecutionType { get ; }
2020-04-13 21:46:30 -04:00
public abstract bool HasPre { get ; }
public abstract bool HasPost { get ; }
2019-10-10 00:52:42 -04:00
public string CleanupCondition
{
get { return _cleanupCondition ; }
set { _cleanupCondition = value ; }
}
2020-04-13 21:46:30 -04:00
public string InitCondition
{
get { return _initCondition ; }
set { _initCondition = value ; }
}
2019-10-10 00:52:42 -04:00
}
public class ContainerSetupInfo
{
public ContainerSetupInfo ( List < Guid > ids , string image )
{
StepIds = ids ;
Container = new ActionContainer ()
{
Image = image
};
}
public ContainerSetupInfo ( List < Guid > ids , string dockerfile , string workingDirectory )
{
StepIds = ids ;
Container = new ActionContainer ()
{
Dockerfile = dockerfile ,
WorkingDirectory = workingDirectory
};
}
public List < Guid > StepIds { get ; set ; }
public ActionContainer Container { get ; set ; }
}
public class ActionContainer
{
public string Image { get ; set ; }
public string Dockerfile { get ; set ; }
public string WorkingDirectory { get ; set ; }
public string ActionRepository { get ; set ; }
}
}