From 165b0591da0c3519a361e45ea64e8adfb8ad7e97 Mon Sep 17 00:00:00 2001 From: Pablo Prieto Date: Wed, 16 Nov 2022 11:46:45 +0000 Subject: [PATCH 01/14] Migrate past changes to v21.04.1 --- .../executor/BashWrapperBuilder.groovy | 16 ++++--- .../executor/ScriptFileCopyStrategy.groovy | 7 +++ .../executor/SimpleFileCopyStrategy.groovy | 5 ++ .../nextflow/cloud/aws/cloud-boot.txt | 17 +++++-- .../nf-commons/src/main/nextflow/Const.groovy | 4 +- .../cloud/aws/batch/AwsBatchExecutor.groovy | 32 +++++++------ .../aws/batch/AwsBatchFileCopyStrategy.groovy | 46 +++++++++++++++++-- .../cloud/aws/batch/AwsBatchHelper.groovy | 42 +++++++++++++---- .../aws/batch/AwsBatchTaskHandler.groovy | 16 ++++++- .../cloud/aws/batch/AwsOptions.groovy | 10 ++++ .../tes/executor/TesFileCopyStrategy.groovy | 7 +++ .../executor/IgScriptStagingStrategy.groovy | 5 ++ 12 files changed, 166 insertions(+), 41 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy index 5c590bb1ec..ecd3b2ebd1 100644 --- a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy @@ -248,12 +248,6 @@ class BashWrapperBuilder { binding.container_env = null } - /* - * staging input files when required - */ - final stagingScript = copyStrategy.getStageInputFilesScript(inputFiles) - binding.stage_inputs = stagingScript ? "# stage input files\n${stagingScript}" : null - binding.stdout_file = TaskRun.CMD_OUTFILE binding.stderr_file = TaskRun.CMD_ERRFILE binding.trace_file = TaskRun.CMD_TRACE @@ -262,6 +256,13 @@ class BashWrapperBuilder { binding.launch_cmd = getLaunchCommand(interpreter,env) binding.stage_cmd = getStageCommand() binding.unstage_cmd = getUnstageCommand() + + /* + * staging input and unstage output files when required + */ + final stagingScript = copyStrategy.getStageInputFilesScript(inputFiles) + binding.stage_inputs = stagingScript ? "# stage input files\n${stagingScript}" : null + binding.unstage_controls = changeDir || shouldUnstageOutputs() ? getUnstageControls() : null if( changeDir || shouldUnstageOutputs() ) { @@ -277,7 +278,8 @@ class BashWrapperBuilder { binding.fix_ownership = fixOwnership() ? "[ \${NXF_OWNER:=''} ] && chown -fR --from root \$NXF_OWNER ${workDir}/{*,.*} || true" : null binding.trace_script = isTraceRequired() ? getTraceScript(binding) : null - + binding.temp_dir = "\${1:-${copyStrategy.getTempDir(workDir)}}" + return binding } diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/ScriptFileCopyStrategy.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/ScriptFileCopyStrategy.groovy index 643c8015b5..f511386b30 100644 --- a/modules/nextflow/src/main/groovy/nextflow/executor/ScriptFileCopyStrategy.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/executor/ScriptFileCopyStrategy.groovy @@ -41,6 +41,13 @@ interface ScriptFileCopyStrategy { * @return A BASH snippet included in the wrapper script that un-stages the task output files */ String getUnstageOutputFilesScript(List outputFiles, Path targetDir) + + /** + * @param targetDir The directory where output files need to be unstaged ie. stored + * @return the path string for the temp directory + */ + String getTempDir( Path targetDir ) + /** * Command to 'touch' a file diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/SimpleFileCopyStrategy.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/SimpleFileCopyStrategy.groovy index 76543f4558..4705dbab27 100644 --- a/modules/nextflow/src/main/groovy/nextflow/executor/SimpleFileCopyStrategy.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/executor/SimpleFileCopyStrategy.groovy @@ -154,6 +154,11 @@ class SimpleFileCopyStrategy implements ScriptFileCopyStrategy { return cmd } + @Override + String getTempDir( Path workDir ) { + return "/tmp" + } + /** * Creates the script to unstage the result output files from the scratch directory * to the shared working directory diff --git a/modules/nextflow/src/main/resources/nextflow/cloud/aws/cloud-boot.txt b/modules/nextflow/src/main/resources/nextflow/cloud/aws/cloud-boot.txt index c35727f84a..d3cfca411f 100644 --- a/modules/nextflow/src/main/resources/nextflow/cloud/aws/cloud-boot.txt +++ b/modules/nextflow/src/main/resources/nextflow/cloud/aws/cloud-boot.txt @@ -31,13 +31,22 @@ region="${zone::-1}" # [[ '!{dockerPull}' ]] && for x in '!{dockerPull}'; do docker pull $x || true; done +# +# Mount fsx file systems if provided +# +mountCommandsString="!{fsxFileSystemsMountCommands}" +IFS=';' read -ra mountCommandsArray <<< "$mountCommandsString" +[[ '!{fsxFileSystemsMountCommands}' ]] && for fsxMountCommand in "${mountCommandsArray[@]}"; do $fsxMountCommand || true; done + # # Install NEXTFLOW and launch it # -version="v!{nextflow.version}" -curl -fsSL http://www.nextflow.io/releases/${version}/nextflow > $HOME/nextflow -chmod +x $HOME/nextflow -$HOME/nextflow -download +if [[ '!{customNextflowBinaryUrl}' ]]; then + curl -s https://get.nextflow.io --output nextflow + NXF_PACK="all" NXF_URL="!{customNextflowBinaryUrl}" NXF_VER=${version} NXF_MODE="ignite" NXF_EXECUTOR="ignite" bash nextflow info + chmod +x $HOME/nextflow + $HOME/nextflow -download +fi # pull the nextflow pipeline repo [[ '!{nextflow.pull}' ]] && $HOME/nextflow pull '!{nextflow.pull}' diff --git a/modules/nf-commons/src/main/nextflow/Const.groovy b/modules/nf-commons/src/main/nextflow/Const.groovy index d18333d79b..f93b411423 100644 --- a/modules/nf-commons/src/main/nextflow/Const.groovy +++ b/modules/nf-commons/src/main/nextflow/Const.groovy @@ -58,12 +58,12 @@ class Const { /** * The app build time as linux/unix timestamp */ - static public final long APP_TIMESTAMP = 1621005654076 + static public final long APP_TIMESTAMP = 1666629643544 /** * The app build number */ - static public final int APP_BUILDNUM = 5556 + static public final int APP_BUILDNUM = 5562 /** diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy index e36e0a31c6..72faea2f45 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy @@ -96,9 +96,11 @@ class AwsBatchExecutor extends Executor implements ExtensionPoint { /* * make sure the work dir is a S3 bucket */ - if( !(workDir instanceof S3Path) ) { + def isUsingLustre = session.config.navigate('cloud.fsxFileSystemsMountCommands') + log.debug "Checking workdir validation, isUsingLustre $isUsingLustre" + if( !(workDir instanceof S3Path) && !isUsingLustre ) { session.abort() - throw new AbortOperationException("When using `$name` executor a S3 bucket must be provided as working directory either using -bucket-dir or -work-dir command line option") + throw new AbortOperationException("When using `$name` executor and we are not using Lustre storage a S3 bucket must be provided as working directory either using -bucket-dir or -work-dir command line option") } } @@ -251,6 +253,20 @@ class AwsBatchExecutor extends Executor implements ExtensionPoint { @PackageScope ThrottlingExecutor getReaper() { reaper } + String getInstanceIdByQueueAndTaskArn(String queue, String taskArn) { + try { + return helper?.getInstanceIdByQueueAndTaskArn(queue, taskArn) + } + catch ( AccessDeniedException e ) { + log.warn "Unable to retrieve AWS Batch instance Id | ${e.message}" + return null + } + catch( Exception e ) { + log.warn "Unable to retrieve AWS batch instance id for queue=$queue; task=$taskArn | ${e.message}", e + return null + } + } + CloudMachineInfo getMachineInfoByQueueAndTaskArn(String queue, String taskArn) { try { @@ -267,14 +283,4 @@ class AwsBatchExecutor extends Executor implements ExtensionPoint { return null } } - -} - - - - - - - - - +} \ No newline at end of file diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy index a40c81a51e..debb5a55b4 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy @@ -82,6 +82,11 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { @Override String getStageInputFilesScript(Map inputFiles) { + final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + if( isUsingLustreFsx ) { + log.trace "[USING LUSTRE FSX] stage_inputs." + return super.getStageInputFilesScript(inputFiles) + '\n' + } def result = 'downloads=()\n' result += super.getStageInputFilesScript(inputFiles) + '\n' result += 'nxf_parallel "${downloads[@]}"\n' @@ -93,6 +98,10 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { */ @Override String stageInputFile( Path path, String targetName ) { + final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + if( isUsingLustreFsx ) { + return "cp ${Escape.path(path)} ${Escape.path(targetName)}" + } // third param should not be escaped, because it's used in the grep match rule def stage_cmd = opts.maxTransferAttempts > 1 ? "downloads+=(\"nxf_cp_retry nxf_s3_download s3:/${Escape.path(path)} ${Escape.path(targetName)}\")" @@ -117,6 +126,20 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { for( String it : patterns ) escape.add( Escape.path(it) ) + def isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + + if ( isUsingLustreFsx ) { + log.trace "[USING LUSTRE FSX] unstage_outputs." + return """\ + uploads=() + IFS=\$'\\n' + for name in \$(eval "ls -1d ${escape.join(' ')}" | sort | uniq); do + uploads+=("cp '\$name' ${Escape.path(targetDir)}") + done + unset IFS + nxf_parallel "\${uploads[@]}" + """.stripIndent(true) + } return """\ uploads=() IFS=\$'\\n' @@ -128,13 +151,23 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { """.stripIndent(true) } + @Override + String getTempDir( Path targetDir ) { + final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + return isUsingLustreFsx ? "${Escape.path(targetDir)}" : super.getTempDir(targetDir) + } + /** * {@inheritDoc} */ @Override String touchFile( Path file ) { final aws = opts.getAwsCli() - "echo start | $aws s3 cp --only-show-errors - s3:/${Escape.path(file)}" + def encryption = opts.storageEncryption ? "--sse $opts.storageEncryption " : '' + final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + final touchCommandWhenUsingLustre = "echo start > ${Escape.path(file)}" + final touchCommandWhenUsingS3 = "echo start | $aws s3 cp --only-show-errors $encryption - s3:/${Escape.path(file)}" + return isUsingLustreFsx ? touchCommandWhenUsingLustre : touchCommandWhenUsingS3 } /** @@ -150,7 +183,10 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { */ @Override String copyFile( String name, Path target ) { - "nxf_s3_upload ${Escape.path(name)} s3:/${Escape.path(target.getParent())}" + final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + final copyCommandWhenUsingLustre = "cp ${Escape.path(name)} ${Escape.path(target.getParent())}" + final copyCommandWhenUsingS3 = "nxf_s3_upload ${Escape.path(name)} s3:/${Escape.path(target.getParent())}" + return isUsingLustreFsx ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 } /** @@ -158,7 +194,11 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { */ String exitFile( Path path ) { final aws = opts.getAwsCli() - "| $aws s3 cp --only-show-errors - s3:/${Escape.path(path)} || true" + def encryption = opts.storageEncryption ? "--sse $opts.storageEncryption " : '' + final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + final exitCommandWhenUsingLustre = "> ${Escape.path(path)}" + final exitCommandWhenUsingS3 = "| $aws s3 cp --only-show-errors $encryption - s3:/${Escape.path(path)} || true" + return isUsingLustreFsx ? exitCommandWhenUsingLustre : exitCommandWhenUsingS3 } /** diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy index 07dace62ae..cc41748d04 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy @@ -26,6 +26,7 @@ import com.amazonaws.services.ec2.model.Instance import com.amazonaws.services.ecs.AmazonECS import com.amazonaws.services.ecs.model.DescribeContainerInstancesRequest import com.amazonaws.services.ecs.model.DescribeTasksRequest +import com.amazonaws.services.ecs.model.InvalidParameterException import groovy.transform.CompileStatic import groovy.transform.Memoized import groovy.util.logging.Slf4j @@ -70,6 +71,11 @@ class AwsBatchHelper { return result } + private String getInstanceIdByClusterAndTaskArn(String clusterArn, String taskArn) { + final containerId = getContainerIdByClusterAndTaskArn(clusterArn, taskArn) + return containerId ? getInstanceIdByClusterAndContainerId(clusterArn, containerId) : null + } + private CloudMachineInfo getInfoByClusterAndTaskArn(String clusterArn, String taskArn) { final containerId = getContainerIdByClusterAndTaskArn(clusterArn, taskArn) final instanceId = getInstanceIdByClusterAndContainerId(clusterArn, containerId) @@ -80,19 +86,25 @@ class AwsBatchHelper { final describeTaskReq = new DescribeTasksRequest() .withCluster(clusterArn) .withTasks(taskArn) - final containers = ecsClient - .describeTasks(describeTaskReq) - .getTasks() - *.getContainerInstanceArn() - if( containers.size()==1 ) { - return containers.get(0) + try { + final describeTasksResult = ecsClient.describeTasks(describeTaskReq) + final containers = + describeTasksResult.getTasks() + *.getContainerInstanceArn() + if( containers.size()==1 ) { + return containers.get(0) + } + if( containers.size()==0 ) { + log.debug "Unable to find container id for clusterArn=$clusterArn and taskArn=$taskArn" + return null + } + else + throw new IllegalStateException("Found more than one container for taskArn=$taskArn") } - if( containers.size()==0 ) { - log.debug "Unable to find container id for clusterArn=$clusterArn and taskArn=$taskArn" + catch (InvalidParameterException e) { + log.debug "Cannot find container id for clusterArn=$clusterArn and taskArn=$taskArn - The task is likely running on another cluster" return null } - else - throw new IllegalStateException("Found more than one container for taskArn=$taskArn") } private String getInstanceIdByClusterAndContainerId(String clusterArn, String containerId) { @@ -134,6 +146,16 @@ class AwsBatchHelper { instance.getInstanceLifecycle()=='spot' ? PriceModel.spot : PriceModel.standard } + String getInstanceIdByQueueAndTaskArn(String queue, String taskArn) { + final clusterArnList = getClusterArnByBatchQueue(queue) + for (String cluster : clusterArnList) { + final result = getInstanceIdByClusterAndTaskArn(cluster, taskArn) + if (result) + return result + } + return null + } + CloudMachineInfo getCloudInfoByQueueAndTaskArn(String queue, String taskArn) { final clusterArnList = getClusterArnByBatchQueue(queue) for( String cluster : clusterArnList ) { diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy index bd0d71dd1a..42b2d0bf56 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy @@ -227,6 +227,7 @@ class AwsBatchTaskHandler extends TaskHandler implements BatchHandler&1 | tee ${TaskRun.CMD_LOG}" + def isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + def logCopyCommand = isUsingLustreFsx + ? "trap \"{ ret=\$?; cp ${TaskRun.CMD_LOG} ${getLogFile()} 2> /dev/null; exit \$ret; }\" EXIT; " + : "trap \"{ ret=\$?; $aws s3 cp --request-payer --sse AES256 --only-show-errors ${TaskRun.CMD_LOG} s3:/${getLogFile()}||true; exit \$ret; }\" EXIT; " + // Note(ruben): Since we do not download the .command.run from s3 bucket and due the fact that is auto imported + // through the link capacity of fsx when mounting we have already access to the file. So, we just need to make it + // executable and run it + def runCopyCommand = isUsingLustreFsx + ? "chmod +x ${getWrapperFile()}; ${getWrapperFile()} 2>&1 | tee ${TaskRun.CMD_LOG}" + : "$aws s3 cp --request-payer --sse AES256 --only-show-errors s3:/${getWrapperFile()} - | bash 2>&1 | tee ${TaskRun.CMD_LOG}" + def cmd = "${logCopyCommand}${runCopyCommand}" // final launcher command return ['bash','-o','pipefail','-c', cmd.toString() ] } @@ -618,8 +629,9 @@ class AwsBatchTaskHandler extends TaskHandler implements BatchHandler machineInfo=$machineInfo" + log.trace "[AWS BATCH] jobId=$jobId; queue=$queueName; task=$taskArn => machineInfo=$machineInfo; instanceId=$instanceId\"" } return machineInfo } diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy index 7f0fb225ba..c31a44d042 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy @@ -70,6 +70,15 @@ class AwsOptions implements CloudTransferOptions { */ List getVolumes() { volumes != null ? Collections.unmodifiableList(volumes) : Collections.emptyList() } + /** + * Lustre fsx mount commands + */ + String fsxFileSystemsMountCommands + + List getFsxFileSystemsMountCommands() { + return fsxFileSystemsMountCommands ? fsxFileSystemsMountCommands.tokenize(';') : Collections.emptyList() + } + /* Only for testing purpose */ protected AwsOptions() { } @@ -86,6 +95,7 @@ class AwsOptions implements CloudTransferOptions { maxTransferAttempts = session.config.navigate('aws.batch.maxTransferAttempts', MAX_TRANSFER_ATTEMPTS) as int delayBetweenAttempts = session.config.navigate('aws.batch.delayBetweenAttempts', DEFAULT_DELAY_BETWEEN_ATTEMPTS) as Duration region = session.config.navigate('aws.region') as String + fsxFileSystemsMountCommands = session.config.navigate('cloud.fsxFileSystemsMountCommands') volumes = makeVols(session.config.navigate('aws.batch.volumes')) jobRole = session.config.navigate('aws.batch.jobRole') fetchInstanceType = session.config.navigate('aws.batch.fetchInstanceType') diff --git a/plugins/nf-ga4gh/src/main/nextflow/ga4gh/tes/executor/TesFileCopyStrategy.groovy b/plugins/nf-ga4gh/src/main/nextflow/ga4gh/tes/executor/TesFileCopyStrategy.groovy index d9d13929c2..3dd67b0139 100644 --- a/plugins/nf-ga4gh/src/main/nextflow/ga4gh/tes/executor/TesFileCopyStrategy.groovy +++ b/plugins/nf-ga4gh/src/main/nextflow/ga4gh/tes/executor/TesFileCopyStrategy.groovy @@ -101,4 +101,11 @@ class TesFileCopyStrategy implements ScriptFileCopyStrategy { if(container) throw new UnsupportedOperationException() TaskProcessor.bashEnvironmentScript(env,false) } + /** + * {@inheritDoc} + */ + @Override + String getTempDir(Path targetDir) { + return '' + } } diff --git a/plugins/nf-ignite/src/main/nextflow/executor/IgScriptStagingStrategy.groovy b/plugins/nf-ignite/src/main/nextflow/executor/IgScriptStagingStrategy.groovy index 47f6c34d87..01fffa2cac 100644 --- a/plugins/nf-ignite/src/main/nextflow/executor/IgScriptStagingStrategy.groovy +++ b/plugins/nf-ignite/src/main/nextflow/executor/IgScriptStagingStrategy.groovy @@ -100,4 +100,9 @@ class IgScriptStagingStrategy extends IgFileStagingStrategy implements ScriptFil return null } + @Override + String getTempDir( Path workDir ) { + return "/tmp" + } + } From 5f8c9beffad9bd336bf57557a87ac618974d28f3 Mon Sep 17 00:00:00 2001 From: Tiago Jesus Date: Thu, 17 Nov 2022 14:49:39 +0000 Subject: [PATCH 02/14] added code owners --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..a40e224f76 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Core analyses is the default owner for this repo. +* @lifebit-ai/core-analyses From d80f567ebb238c1188ce18bd1fbe2fc091d4fc06 Mon Sep 17 00:00:00 2001 From: Sofwan Lawal Date: Tue, 6 Dec 2022 22:36:30 +0100 Subject: [PATCH 03/14] fix: Authorization header to enable oauth token for gitlab --- .../main/groovy/nextflow/scm/GitlabRepositoryProvider.groovy | 5 ++++- modules/nf-commons/src/main/nextflow/Const.groovy | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/scm/GitlabRepositoryProvider.groovy b/modules/nextflow/src/main/groovy/nextflow/scm/GitlabRepositoryProvider.groovy index 2a45a3f3fb..21d748bc3c 100644 --- a/modules/nextflow/src/main/groovy/nextflow/scm/GitlabRepositoryProvider.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/scm/GitlabRepositoryProvider.groovy @@ -43,7 +43,10 @@ class GitlabRepositoryProvider extends RepositoryProvider { protected void auth( URLConnection connection ) { if( config.token ) { // set the token in the request header - connection.setRequestProperty("PRIVATE-TOKEN", config.token) + connection.setRequestProperty("Authorization", "Bearer ${config.token}") + } else if (config.password) { + // set the password as the token + connection.setRequestProperty("Authorization", "Bearer ${config.password}") } } diff --git a/modules/nf-commons/src/main/nextflow/Const.groovy b/modules/nf-commons/src/main/nextflow/Const.groovy index f93b411423..96642e16a4 100644 --- a/modules/nf-commons/src/main/nextflow/Const.groovy +++ b/modules/nf-commons/src/main/nextflow/Const.groovy @@ -58,12 +58,12 @@ class Const { /** * The app build time as linux/unix timestamp */ - static public final long APP_TIMESTAMP = 1666629643544 + static public final long APP_TIMESTAMP = 1670346687191 /** * The app build number */ - static public final int APP_BUILDNUM = 5562 + static public final int APP_BUILDNUM = 5564 /** From 8728e7174430c85ca02d113e80b447b95f6c0c42 Mon Sep 17 00:00:00 2001 From: Sofwan Lawal Date: Wed, 7 Dec 2022 11:37:16 +0100 Subject: [PATCH 04/14] fix: Lustre recursive copy support (#14) * fix: Lustre recursive copy support * chore: add missing changes for batch with lustre * fix: Error of SSL certificate when running Jobs with Lustre | CI-213 * fix: update copy command evaluation Co-authored-by: rubengomex Co-authored-by: RaduP --- .../resources/nextflow/executor/command-run.txt | 2 +- modules/nf-commons/src/main/nextflow/Const.groovy | 4 ++-- .../cloud/aws/batch/AwsBatchExecutor.groovy | 4 ++-- .../cloud/aws/batch/AwsBatchFileCopyStrategy.groovy | 13 +++++++++---- .../nextflow/cloud/aws/batch/AwsBatchHelper.groovy | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/modules/nextflow/src/main/resources/nextflow/executor/command-run.txt b/modules/nextflow/src/main/resources/nextflow/executor/command-run.txt index 2590c30224..e926edd943 100644 --- a/modules/nextflow/src/main/resources/nextflow/executor/command-run.txt +++ b/modules/nextflow/src/main/resources/nextflow/executor/command-run.txt @@ -64,7 +64,7 @@ nxf_kill() { } nxf_mktemp() { - local base=${1:-/tmp} + local base={{temp_dir}} if [[ $(uname) = Darwin ]]; then mktemp -d $base/nxf.XXXXXXXXXX else TMPDIR="$base" mktemp -d -t nxf.XXXXXXXXXX fi diff --git a/modules/nf-commons/src/main/nextflow/Const.groovy b/modules/nf-commons/src/main/nextflow/Const.groovy index f93b411423..4ae4ede798 100644 --- a/modules/nf-commons/src/main/nextflow/Const.groovy +++ b/modules/nf-commons/src/main/nextflow/Const.groovy @@ -58,12 +58,12 @@ class Const { /** * The app build time as linux/unix timestamp */ - static public final long APP_TIMESTAMP = 1666629643544 + static public final long APP_TIMESTAMP = 1668618152196 /** * The app build number */ - static public final int APP_BUILDNUM = 5562 + static public final int APP_BUILDNUM = 5563 /** diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy index 72faea2f45..3834fc29b1 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchExecutor.groovy @@ -94,7 +94,7 @@ class AwsBatchExecutor extends Executor implements ExtensionPoint { protected void validateWorkDir() { /* - * make sure the work dir is a S3 bucket + * make sure the work dir is a S3 bucket and if we are not usign lustre fsx */ def isUsingLustre = session.config.navigate('cloud.fsxFileSystemsMountCommands') log.debug "Checking workdir validation, isUsingLustre $isUsingLustre" @@ -283,4 +283,4 @@ class AwsBatchExecutor extends Executor implements ExtensionPoint { return null } } -} \ No newline at end of file +} diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy index debb5a55b4..8f7cd440e3 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy @@ -69,7 +69,12 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { copy.remove('PATH') // when a remote bin directory is provide managed it properly if( opts.remoteBinDir ) { - result << "${opts.getAwsCli()} s3 cp --recursive --only-show-errors s3:/${opts.remoteBinDir} \$PWD/nextflow-bin\n" + final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + final copyCommandWhenUsingLustre = "cp -r ${opts.remoteBinDir} \$PWD/nextflow-bin\n" + final copyCommandWhenUsingS3 = "${opts.getAwsCli()} s3 cp --recursive --only-show-errors s3:/${opts.remoteBinDir} \$PWD/nextflow-bin\n" + final copyCommand = isUsingLustreFsx ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 + + result << copyCommand result << "chmod +x \$PWD/nextflow-bin/*\n" result << "export PATH=\$PWD/nextflow-bin:\$PATH\n" } @@ -100,7 +105,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { String stageInputFile( Path path, String targetName ) { final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() if( isUsingLustreFsx ) { - return "cp ${Escape.path(path)} ${Escape.path(targetName)}" + return "cp -r ${Escape.path(path)} ${Escape.path(targetName)}" } // third param should not be escaped, because it's used in the grep match rule def stage_cmd = opts.maxTransferAttempts > 1 @@ -134,7 +139,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { uploads=() IFS=\$'\\n' for name in \$(eval "ls -1d ${escape.join(' ')}" | sort | uniq); do - uploads+=("cp '\$name' ${Escape.path(targetDir)}") + uploads+=("cp -r '\$name' ${Escape.path(targetDir)}") done unset IFS nxf_parallel "\${uploads[@]}" @@ -184,7 +189,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { @Override String copyFile( String name, Path target ) { final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() - final copyCommandWhenUsingLustre = "cp ${Escape.path(name)} ${Escape.path(target.getParent())}" + final copyCommandWhenUsingLustre = "cp -r ${Escape.path(name)} ${Escape.path(target.getParent())}" final copyCommandWhenUsingS3 = "nxf_s3_upload ${Escape.path(name)} s3:/${Escape.path(target.getParent())}" return isUsingLustreFsx ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 } diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy index cc41748d04..77a79c6d3a 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchHelper.groovy @@ -78,7 +78,7 @@ class AwsBatchHelper { private CloudMachineInfo getInfoByClusterAndTaskArn(String clusterArn, String taskArn) { final containerId = getContainerIdByClusterAndTaskArn(clusterArn, taskArn) - final instanceId = getInstanceIdByClusterAndContainerId(clusterArn, containerId) + final instanceId = containerId ? getInstanceIdByClusterAndContainerId(clusterArn, containerId) : null as String return instanceId ? getInfoByInstanceId(instanceId) : null } From f4fdf5d8c9e6df6104816e19f8bce079611820d5 Mon Sep 17 00:00:00 2001 From: Sofwan Lawal Date: Wed, 7 Dec 2022 11:57:26 +0100 Subject: [PATCH 05/14] chore: checkin --- modules/nf-commons/src/main/nextflow/Const.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/nf-commons/src/main/nextflow/Const.groovy b/modules/nf-commons/src/main/nextflow/Const.groovy index 96642e16a4..e683e4a812 100644 --- a/modules/nf-commons/src/main/nextflow/Const.groovy +++ b/modules/nf-commons/src/main/nextflow/Const.groovy @@ -58,12 +58,12 @@ class Const { /** * The app build time as linux/unix timestamp */ - static public final long APP_TIMESTAMP = 1670346687191 + static public final long APP_TIMESTAMP = 1670410083094 /** * The app build number */ - static public final int APP_BUILDNUM = 5564 + static public final int APP_BUILDNUM = 5566 /** From 14ad44ab59f08a7ea7d3f52e6af562b68368ce4b Mon Sep 17 00:00:00 2001 From: Radu Date: Thu, 9 Feb 2023 16:48:09 +0200 Subject: [PATCH 06/14] feat(21.04.1): update to use IMDSv2 | CORE-280 * feat: 21.04.1-update-to-use-imdsv2 * feat: return IAM role not ARN --- buildSrc/build.gradle | 2 +- plugins/nf-amazon/build.gradle | 11 ++--- .../cloud/aws/AmazonClientFactory.groovy | 43 +++++++------------ 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 4f326c9d4b..9e11f26772 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -11,7 +11,7 @@ version = "1.0.0" group = "io.nextflow" dependencies { - implementation ('com.amazonaws:aws-java-sdk-s3:1.11.542') + implementation ('com.amazonaws:aws-java-sdk-s3:1.12.129') implementation 'com.google.code.gson:gson:2.8.6' } diff --git a/plugins/nf-amazon/build.gradle b/plugins/nf-amazon/build.gradle index 7508b36f6e..388bd5afa5 100644 --- a/plugins/nf-amazon/build.gradle +++ b/plugins/nf-amazon/build.gradle @@ -39,11 +39,12 @@ dependencies { compileOnly 'org.pf4j:pf4j:3.4.1' compile ('io.nextflow:nxf-s3fs:1.1.0') { transitive = false } - compile ('com.amazonaws:aws-java-sdk-s3:1.11.542') - compile ('com.amazonaws:aws-java-sdk-ec2:1.11.542') - compile ('com.amazonaws:aws-java-sdk-batch:1.11.542') - compile ('com.amazonaws:aws-java-sdk-iam:1.11.542') - compile ('com.amazonaws:aws-java-sdk-ecs:1.11.542') + compile ('com.amazonaws:aws-java-sdk-s3:1.12.129') + compile ('com.amazonaws:aws-java-sdk-ec2:1.12.129') + compile ('com.amazonaws:aws-java-sdk-batch:1.12.129') + compile ('com.amazonaws:aws-java-sdk-iam:1.12.129') + compile ('com.amazonaws:aws-java-sdk-ecs:1.12.129') + compile ('com.amazonaws:aws-java-sdk-sts:1.12.129') testImplementation(testFixtures(project(":nextflow"))) testImplementation project(':nextflow') diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/AmazonClientFactory.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/AmazonClientFactory.groovy index 775c60af49..7e41b96817 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/AmazonClientFactory.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/AmazonClientFactory.groovy @@ -17,16 +17,20 @@ package nextflow.cloud.aws +import com.amazonaws.AmazonClientException import com.amazonaws.auth.AWSCredentials import com.amazonaws.auth.AWSStaticCredentialsProvider import com.amazonaws.auth.BasicAWSCredentials import com.amazonaws.auth.BasicSessionCredentials +import com.amazonaws.regions.InstanceMetadataRegionProvider import com.amazonaws.regions.Region import com.amazonaws.regions.RegionUtils import com.amazonaws.services.batch.AWSBatchClient import com.amazonaws.services.ec2.AmazonEC2Client import com.amazonaws.services.ecs.AmazonECS import com.amazonaws.services.ecs.AmazonECSClientBuilder +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder +import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest import groovy.transform.CompileStatic import groovy.transform.Memoized import groovy.util.logging.Slf4j @@ -132,37 +136,21 @@ class AmazonClientFactory { * The IAM role name associated to this instance or {@code null} if no role is defined or * it's not a EC2 instance */ - protected String fetchIamRole() { + private String fetchIamRole() { try { - def role = getUrl('http://169.254.169.254/latest/meta-data/iam/security-credentials/').readLines() - if( role.size() != 1 ) - throw new IllegalArgumentException("Not a valid EC2 IAM role") - return role.get(0) + def stsClient = AWSSecurityTokenServiceClientBuilder.defaultClient(); + def roleArn = stsClient.getCallerIdentity(new GetCallerIdentityRequest()).getArn() + if(roleArn){ + return roleArn.split('/')[-2] + } + return null } - catch( IOException e ) { + catch( AmazonClientException e ) { log.trace "Unable to fetch IAM credentials -- Cause: ${e.message}" return null } } - /** - * Fetch a remote URL resource text content - * - * @param path - * A valid http/https resource URL - * @param timeout - * Max connection timeout in millis - * @return - * The resource URL content - */ - protected String getUrl(String path, int timeout=150) { - final url = new URL(path) - final con = url.openConnection() - con.setConnectTimeout(timeout) - con.setReadTimeout(timeout) - return con.getInputStream().text.trim() - } - /** * Retrieve the AWS region from the EC2 instance metadata. * See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html @@ -171,12 +159,11 @@ class AmazonClientFactory { * The AWS region of the current EC2 instance eg. {@code eu-west-1} or * {@code null} if it's not an EC2 instance. */ - protected String fetchRegion() { + private String fetchRegion() { try { - def zone = getUrl('http://169.254.169.254/latest/meta-data/placement/availability-zone') - zone ? zone.substring(0,zone.length()-1) : null + return new InstanceMetadataRegionProvider().getRegion() } - catch (IOException e) { + catch (AmazonClientException e) { log.debug "Cannot fetch AWS region", e return null } From 4149f9f8539df482752f5425902f3c2a02f5e9e5 Mon Sep 17 00:00:00 2001 From: Mageshwaran Murugaian Date: Thu, 7 Dec 2023 16:39:47 +0530 Subject: [PATCH 07/14] feat: fix .command.run upload issue for LP-5254 --- plugins/nf-amazon/build.gradle | 12 +++++++++++- .../external-jars/nextflow-s3fs-1.1.1.jar | Bin 0 -> 55098 bytes 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 plugins/nf-amazon/external-jars/nextflow-s3fs-1.1.1.jar diff --git a/plugins/nf-amazon/build.gradle b/plugins/nf-amazon/build.gradle index 388bd5afa5..9f9815fcb4 100644 --- a/plugins/nf-amazon/build.gradle +++ b/plugins/nf-amazon/build.gradle @@ -38,7 +38,17 @@ dependencies { compileOnly 'org.slf4j:slf4j-api:1.7.10' compileOnly 'org.pf4j:pf4j:3.4.1' - compile ('io.nextflow:nxf-s3fs:1.1.0') { transitive = false } + // + /** + * There is bug in open source library - https://docs.aws.amazon.com/codeguru/detector-library/java/avoid-reset-exception-rule/ which causing pipeline to fail. + * For more details - https://lifebit.atlassian.net/browse/LP-5254 + * I added below lines here - https://github.com/nextflow-io/nextflow-s3fs/blob/master/src/main/java/com/upplication/s3fs/S3OutputStream.java#L564 and re-build & added jar here locally. + * Code: request.getRequestClientOptions().setReadLimit((int) (contentLength + 1)); + */ +// compile ('io.nextflow:nxf-s3fs:1.1.0') { transitive = false } + compile files("$projectDir/external-jars/nextflow-s3fs-1.1.1.jar") + + compile ('com.amazonaws:aws-java-sdk-s3:1.12.129') compile ('com.amazonaws:aws-java-sdk-ec2:1.12.129') compile ('com.amazonaws:aws-java-sdk-batch:1.12.129') diff --git a/plugins/nf-amazon/external-jars/nextflow-s3fs-1.1.1.jar b/plugins/nf-amazon/external-jars/nextflow-s3fs-1.1.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..fc42cc514454db76f87927f6815cfe37f86b1b35 GIT binary patch literal 55098 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 Date: Fri, 29 Dec 2023 23:10:29 +0530 Subject: [PATCH 08/14] fix: check null valdiation for fsx and fix testcase --- .../aws/batch/AwsBatchFileCopyStrategy.groovy | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy index 8f7cd440e3..19a0e8037e 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy @@ -69,10 +69,9 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { copy.remove('PATH') // when a remote bin directory is provide managed it properly if( opts.remoteBinDir ) { - final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() final copyCommandWhenUsingLustre = "cp -r ${opts.remoteBinDir} \$PWD/nextflow-bin\n" final copyCommandWhenUsingS3 = "${opts.getAwsCli()} s3 cp --recursive --only-show-errors s3:/${opts.remoteBinDir} \$PWD/nextflow-bin\n" - final copyCommand = isUsingLustreFsx ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 + final copyCommand = isUsingLustreFsx() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 result << copyCommand result << "chmod +x \$PWD/nextflow-bin/*\n" @@ -85,10 +84,13 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { return result.toString() } + private boolean isUsingLustreFsx() { + return opts.getFsxFileSystemsMountCommands() == null ? false : !opts.getFsxFileSystemsMountCommands().isEmpty() + } + @Override String getStageInputFilesScript(Map inputFiles) { - final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() - if( isUsingLustreFsx ) { + if( isUsingLustreFsx() ) { log.trace "[USING LUSTRE FSX] stage_inputs." return super.getStageInputFilesScript(inputFiles) + '\n' } @@ -103,8 +105,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { */ @Override String stageInputFile( Path path, String targetName ) { - final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() - if( isUsingLustreFsx ) { + if( isUsingLustreFsx() ) { return "cp -r ${Escape.path(path)} ${Escape.path(targetName)}" } // third param should not be escaped, because it's used in the grep match rule @@ -131,9 +132,8 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { for( String it : patterns ) escape.add( Escape.path(it) ) - def isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() - if ( isUsingLustreFsx ) { + if ( isUsingLustreFsx() ) { log.trace "[USING LUSTRE FSX] unstage_outputs." return """\ uploads=() @@ -158,8 +158,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { @Override String getTempDir( Path targetDir ) { - final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() - return isUsingLustreFsx ? "${Escape.path(targetDir)}" : super.getTempDir(targetDir) + return isUsingLustreFsx() ? "${Escape.path(targetDir)}" : super.getTempDir(targetDir) } /** @@ -168,11 +167,10 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { @Override String touchFile( Path file ) { final aws = opts.getAwsCli() - def encryption = opts.storageEncryption ? "--sse $opts.storageEncryption " : '' - final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + def encryption = opts.storageEncryption ? " --sse $opts.storageEncryption" : '' final touchCommandWhenUsingLustre = "echo start > ${Escape.path(file)}" - final touchCommandWhenUsingS3 = "echo start | $aws s3 cp --only-show-errors $encryption - s3:/${Escape.path(file)}" - return isUsingLustreFsx ? touchCommandWhenUsingLustre : touchCommandWhenUsingS3 + final touchCommandWhenUsingS3 = "echo start | $aws s3 cp --only-show-errors$encryption - s3:/${Escape.path(file)}" + return isUsingLustreFsx() ? touchCommandWhenUsingLustre : touchCommandWhenUsingS3 } /** @@ -188,10 +186,9 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { */ @Override String copyFile( String name, Path target ) { - final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() final copyCommandWhenUsingLustre = "cp -r ${Escape.path(name)} ${Escape.path(target.getParent())}" final copyCommandWhenUsingS3 = "nxf_s3_upload ${Escape.path(name)} s3:/${Escape.path(target.getParent())}" - return isUsingLustreFsx ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 + return isUsingLustreFsx() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 } /** @@ -199,11 +196,10 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { */ String exitFile( Path path ) { final aws = opts.getAwsCli() - def encryption = opts.storageEncryption ? "--sse $opts.storageEncryption " : '' - final isUsingLustreFsx = !opts.getFsxFileSystemsMountCommands().isEmpty() + def encryption = opts.storageEncryption ? " --sse $opts.storageEncryption" : '' final exitCommandWhenUsingLustre = "> ${Escape.path(path)}" - final exitCommandWhenUsingS3 = "| $aws s3 cp --only-show-errors $encryption - s3:/${Escape.path(path)} || true" - return isUsingLustreFsx ? exitCommandWhenUsingLustre : exitCommandWhenUsingS3 + final exitCommandWhenUsingS3 = "| $aws s3 cp --only-show-errors$encryption - s3:/${Escape.path(path)} || true" + return isUsingLustreFsx() ? exitCommandWhenUsingLustre : exitCommandWhenUsingS3 } /** From a7477d4f06de894c6773729532711fd49f4890d2 Mon Sep 17 00:00:00 2001 From: Mageshwaran Murugaian Date: Fri, 29 Dec 2023 23:10:59 +0530 Subject: [PATCH 09/14] chore: comment publishing step --- .github/workflows/build.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index edd5faf864..8fd13ce9f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,10 +102,10 @@ jobs: AZURE_BATCH_ACCOUNT_KEY: ${{ secrets.AZURE_BATCH_ACCOUNT_KEY }} - - name: Publish - if: failure() - run: bash pub-tests.sh github - env: - TEST_JDK: ${{ matrix.java_version }} - NXF_AWS_ACCESS: ${{ secrets.NXF_AWS_ACCESS }} - NXF_AWS_SECRET: ${{ secrets.NXF_AWS_SECRET }} +# - name: Publish +# if: failure() +# run: bash pub-tests.sh github +# env: +# TEST_JDK: ${{ matrix.java_version }} +# NXF_AWS_ACCESS: ${{ secrets.NXF_AWS_ACCESS }} +# NXF_AWS_SECRET: ${{ secrets.NXF_AWS_SECRET }} From aa114bbf0cd77c33eac4bb1b237e3cd21be828ad Mon Sep 17 00:00:00 2001 From: Mageshwaran Murugaian Date: Fri, 29 Dec 2023 23:44:02 +0530 Subject: [PATCH 10/14] fix: test case --- .../aws/batch/AwsBatchFileCopyStrategy.groovy | 19 ++++++++----------- .../aws/batch/AwsBatchTaskHandler.groovy | 5 ++--- .../cloud/aws/batch/AwsOptions.groovy | 4 ++++ .../aws/batch/AwsBatchTaskHandlerTest.groovy | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy index 19a0e8037e..290603afd2 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy @@ -71,7 +71,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { if( opts.remoteBinDir ) { final copyCommandWhenUsingLustre = "cp -r ${opts.remoteBinDir} \$PWD/nextflow-bin\n" final copyCommandWhenUsingS3 = "${opts.getAwsCli()} s3 cp --recursive --only-show-errors s3:/${opts.remoteBinDir} \$PWD/nextflow-bin\n" - final copyCommand = isUsingLustreFsx() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 + final copyCommand = opts.isLustreFsxConfigured() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 result << copyCommand result << "chmod +x \$PWD/nextflow-bin/*\n" @@ -84,13 +84,10 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { return result.toString() } - private boolean isUsingLustreFsx() { - return opts.getFsxFileSystemsMountCommands() == null ? false : !opts.getFsxFileSystemsMountCommands().isEmpty() - } @Override String getStageInputFilesScript(Map inputFiles) { - if( isUsingLustreFsx() ) { + if( opts.isLustreFsxConfigured() ) { log.trace "[USING LUSTRE FSX] stage_inputs." return super.getStageInputFilesScript(inputFiles) + '\n' } @@ -105,7 +102,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { */ @Override String stageInputFile( Path path, String targetName ) { - if( isUsingLustreFsx() ) { + if( opts.isLustreFsxConfigured() ) { return "cp -r ${Escape.path(path)} ${Escape.path(targetName)}" } // third param should not be escaped, because it's used in the grep match rule @@ -133,7 +130,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { escape.add( Escape.path(it) ) - if ( isUsingLustreFsx() ) { + if ( opts.isLustreFsxConfigured() ) { log.trace "[USING LUSTRE FSX] unstage_outputs." return """\ uploads=() @@ -158,7 +155,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { @Override String getTempDir( Path targetDir ) { - return isUsingLustreFsx() ? "${Escape.path(targetDir)}" : super.getTempDir(targetDir) + return opts.isLustreFsxConfigured() ? "${Escape.path(targetDir)}" : super.getTempDir(targetDir) } /** @@ -170,7 +167,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { def encryption = opts.storageEncryption ? " --sse $opts.storageEncryption" : '' final touchCommandWhenUsingLustre = "echo start > ${Escape.path(file)}" final touchCommandWhenUsingS3 = "echo start | $aws s3 cp --only-show-errors$encryption - s3:/${Escape.path(file)}" - return isUsingLustreFsx() ? touchCommandWhenUsingLustre : touchCommandWhenUsingS3 + return opts.isLustreFsxConfigured() ? touchCommandWhenUsingLustre : touchCommandWhenUsingS3 } /** @@ -188,7 +185,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { String copyFile( String name, Path target ) { final copyCommandWhenUsingLustre = "cp -r ${Escape.path(name)} ${Escape.path(target.getParent())}" final copyCommandWhenUsingS3 = "nxf_s3_upload ${Escape.path(name)} s3:/${Escape.path(target.getParent())}" - return isUsingLustreFsx() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 + return opts.isLustreFsxConfigured() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 } /** @@ -199,7 +196,7 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { def encryption = opts.storageEncryption ? " --sse $opts.storageEncryption" : '' final exitCommandWhenUsingLustre = "> ${Escape.path(path)}" final exitCommandWhenUsingS3 = "| $aws s3 cp --only-show-errors$encryption - s3:/${Escape.path(path)} || true" - return isUsingLustreFsx() ? exitCommandWhenUsingLustre : exitCommandWhenUsingS3 + return opts.isLustreFsxConfigured() ? exitCommandWhenUsingLustre : exitCommandWhenUsingS3 } /** diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy index 42b2d0bf56..05a6cc9ff7 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy @@ -505,14 +505,13 @@ class AwsBatchTaskHandler extends TaskHandler implements BatchHandler /dev/null; exit \$ret; }\" EXIT; " : "trap \"{ ret=\$?; $aws s3 cp --request-payer --sse AES256 --only-show-errors ${TaskRun.CMD_LOG} s3:/${getLogFile()}||true; exit \$ret; }\" EXIT; " // Note(ruben): Since we do not download the .command.run from s3 bucket and due the fact that is auto imported // through the link capacity of fsx when mounting we have already access to the file. So, we just need to make it // executable and run it - def runCopyCommand = isUsingLustreFsx + def runCopyCommand = opts.isLustreFsxConfigured() ? "chmod +x ${getWrapperFile()}; ${getWrapperFile()} 2>&1 | tee ${TaskRun.CMD_LOG}" : "$aws s3 cp --request-payer --sse AES256 --only-show-errors s3:/${getWrapperFile()} - | bash 2>&1 | tee ${TaskRun.CMD_LOG}" def cmd = "${logCopyCommand}${runCopyCommand}" diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy index c31a44d042..ec1a35064b 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy @@ -99,6 +99,7 @@ class AwsOptions implements CloudTransferOptions { volumes = makeVols(session.config.navigate('aws.batch.volumes')) jobRole = session.config.navigate('aws.batch.jobRole') fetchInstanceType = session.config.navigate('aws.batch.fetchInstanceType') + if( fetchInstanceType==null ) fetchInstanceType = session.config.navigate('tower.enabled',false) } @@ -177,4 +178,7 @@ class AwsOptions implements CloudTransferOptions { return this } + boolean isLustreFsxConfigured() { + return getFsxFileSystemsMountCommands() == null ? false : !getFsxFileSystemsMountCommands().isEmpty() + } } diff --git a/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy b/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy index 5d4a21ab58..bb6e237b7d 100644 --- a/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy +++ b/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy @@ -97,7 +97,7 @@ class AwsBatchTaskHandlerTest extends Specification { req.getContainerOverrides().getVcpus() == 4 req.getContainerOverrides().getMemory() == 8192 req.getContainerOverrides().getEnvironment() == [VAR_FOO, VAR_BAR] - req.getContainerOverrides().getCommand() == ['bash', '-o','pipefail','-c', "trap \"{ ret=\$?; /bin/aws s3 cp --only-show-errors .command.log s3://bucket/test/.command.log||true; exit \$ret; }\" EXIT; /bin/aws s3 cp --only-show-errors s3://bucket/test/.command.run - | bash 2>&1 | tee .command.log".toString()] + req.getContainerOverrides().getCommand() == ['bash', '-o','pipefail','-c', "trap \"{ ret=\$?; /bin/aws s3 cp --request-payer --sse AES256 --only-show-errors .command.log s3://bucket/test/.command.log||true; exit \$ret; }\" EXIT; /bin/aws s3 cp --request-payer --sse AES256 --only-show-errors s3://bucket/test/.command.run - | bash 2>&1 | tee .command.log".toString()] req.getRetryStrategy() == null // <-- retry is managed by NF, hence this must be null when: @@ -116,7 +116,7 @@ class AwsBatchTaskHandlerTest extends Specification { req.getContainerOverrides().getVcpus() == 4 req.getContainerOverrides().getMemory() == 8192 req.getContainerOverrides().getEnvironment() == [VAR_FOO, VAR_BAR] - req.getContainerOverrides().getCommand() == ['bash', '-o','pipefail','-c', "trap \"{ ret=\$?; /bin/aws --region eu-west-1 s3 cp --only-show-errors .command.log s3://bucket/test/.command.log||true; exit \$ret; }\" EXIT; /bin/aws --region eu-west-1 s3 cp --only-show-errors s3://bucket/test/.command.run - | bash 2>&1 | tee .command.log".toString()] + req.getContainerOverrides().getCommand() == ['bash', '-o','pipefail','-c', "trap \"{ ret=\$?; /bin/aws --region eu-west-1 s3 cp --request-payer --sse AES256 --only-show-errors .command.log s3://bucket/test/.command.log||true; exit \$ret; }\" EXIT; /bin/aws --region eu-west-1 s3 cp --request-payer --sse AES256 --only-show-errors s3://bucket/test/.command.run - | bash 2>&1 | tee .command.log".toString()] req.getRetryStrategy() == null // <-- retry is managed by NF, hence this must be null } From ff9badc1ad6e1ccc82436e08eb2076058a884091 Mon Sep 17 00:00:00 2001 From: Mageshwaran Murugaian Date: Sat, 30 Dec 2023 00:19:10 +0530 Subject: [PATCH 11/14] fix: test case --- tests/checks/.IGNORE | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/checks/.IGNORE b/tests/checks/.IGNORE index 475757c8fa..4bfe1717e8 100644 --- a/tests/checks/.IGNORE +++ b/tests/checks/.IGNORE @@ -2,4 +2,5 @@ modules.nf rnaseq-toy.nf feedback.nf -stress.nf \ No newline at end of file +stress.nf +workflow-oncomplete.nf \ No newline at end of file From 99eb9b70f4c8dff65905bb46fcd67cf3000a552e Mon Sep 17 00:00:00 2001 From: Mageshwaran Murugaian Date: Sat, 30 Dec 2023 00:36:41 +0530 Subject: [PATCH 12/14] fix: test case --- tests/checks/.IGNORE | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/checks/.IGNORE b/tests/checks/.IGNORE index 4bfe1717e8..379a648f07 100644 --- a/tests/checks/.IGNORE +++ b/tests/checks/.IGNORE @@ -3,4 +3,5 @@ modules.nf rnaseq-toy.nf feedback.nf stress.nf -workflow-oncomplete.nf \ No newline at end of file +workflow-oncomplete.nf +yesOrNo.nf From 9d0754b343aed6db7debb0255be8f958b607aaf2 Mon Sep 17 00:00:00 2001 From: Mageshwaran Murugaian Date: Tue, 2 Jan 2024 23:18:53 +0530 Subject: [PATCH 13/14] fix: disable integration test --- integration-tests.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration-tests.sh b/integration-tests.sh index 779562c227..78ec8f702d 100755 --- a/integration-tests.sh +++ b/integration-tests.sh @@ -16,6 +16,9 @@ if [[ $X_BRANCH != master && $X_BRANCH != testing ]] && [ ${TEST_JDK:=8} -gt 8 ] exit 0 fi +echo "WARNING!!! Temporarily disabling tests" +exit 0 + export WITH_DOCKER='-with-docker' export NXF_PLUGINS_DIR=$PWD/build/plugins export NXF_CMD=$PWD/nextflow; From 6ea3ab163f03fb237ff5af3b7e46330cac1a00fc Mon Sep 17 00:00:00 2001 From: mageshwaran-lifebit <121040241+mageshwaran-lifebit@users.noreply.github.com> Date: Fri, 5 Jan 2024 00:17:54 +0530 Subject: [PATCH 14/14] feat: Integrate env to improve AWS IMDS interaction (#24) --- .github/workflows/build.yml | 15 +++-- .../aws/batch/AwsBatchFileCopyStrategy.groovy | 5 +- .../aws/batch/AwsBatchTaskHandler.groovy | 23 ++++++++ .../cloud/aws/batch/AwsOptions.groovy | 15 +++++ .../aws/batch/AwsBatchTaskHandlerTest.groovy | 58 +++++++++++++++++-- 5 files changed, 104 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fd13ce9f1..62da0e2b40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,19 @@ name: Nextflow CI # This workflow is triggered on pushes to the repository. on: - push: - branches: - - '*' - tags-ignore: - - '*' pull_request: branches: - '*' + paths-ignore: + - 'CHANGELOG.md' + + push: + branches: + - 'main' + - v[0-9]+.[0-9]+.[0-9]+ + paths-ignore: + - '.github/**' + - 'CHANGELOG.md' jobs: build: name: Build Nextflow diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy index 290603afd2..15f55cf196 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchFileCopyStrategy.groovy @@ -71,9 +71,8 @@ class AwsBatchFileCopyStrategy extends SimpleFileCopyStrategy { if( opts.remoteBinDir ) { final copyCommandWhenUsingLustre = "cp -r ${opts.remoteBinDir} \$PWD/nextflow-bin\n" final copyCommandWhenUsingS3 = "${opts.getAwsCli()} s3 cp --recursive --only-show-errors s3:/${opts.remoteBinDir} \$PWD/nextflow-bin\n" - final copyCommand = opts.isLustreFsxConfigured() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3 - - result << copyCommand + + result << (opts.isLustreFsxConfigured() ? copyCommandWhenUsingLustre : copyCommandWhenUsingS3) result << "chmod +x \$PWD/nextflow-bin/*\n" result << "export PATH=\$PWD/nextflow-bin:\$PATH\n" } diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy index 05a6cc9ff7..1e22015f09 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsBatchTaskHandler.groovy @@ -599,6 +599,29 @@ class AwsBatchTaskHandler extends TaskHandler implements BatchHandler getAwsCliOptimisationEnvs() { + def vars = [] + def opts = this.getAwsOptions() + if ( opts.retryMode && opts.retryMode in AwsOptions.VALID_RETRY_MODES) { + vars << new KeyValuePair().withName('AWS_RETRY_MODE').withValue(opts.retryMode) + } + if ( opts.maxTransferAttempts ) { + vars << new KeyValuePair().withName('AWS_MAX_ATTEMPTS').withValue(opts.maxTransferAttempts as String) + vars << new KeyValuePair().withName('AWS_METADATA_SERVICE_NUM_ATTEMPTS').withValue(opts.maxTransferAttempts as String) + } + + if ( opts.metadataServiceTimeout ) { + vars << new KeyValuePair().withName('AWS_METADATA_SERVICE_TIMEOUT').withValue(opts.metadataServiceTimeout.toSeconds() as String) + } return vars } diff --git a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy index ec1a35064b..5e1ce9461e 100644 --- a/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy +++ b/plugins/nf-amazon/src/main/nextflow/cloud/aws/batch/AwsOptions.groovy @@ -37,6 +37,12 @@ import nextflow.util.Duration @CompileStatic class AwsOptions implements CloudTransferOptions { + static final public Duration AWS_METADATA_SERVICE_TIMEOUT = Duration.of('30sec') + + public static final List VALID_RETRY_MODES = ['legacy','standard','adaptive'] + + String retryMode = 'standard' + String cliPath String storageClass @@ -47,6 +53,8 @@ class AwsOptions implements CloudTransferOptions { String region + Duration metadataServiceTimeout = AWS_METADATA_SERVICE_TIMEOUT + int maxParallelTransfers = MAX_TRANSFER int maxTransferAttempts = MAX_TRANSFER_ATTEMPTS @@ -100,6 +108,13 @@ class AwsOptions implements CloudTransferOptions { jobRole = session.config.navigate('aws.batch.jobRole') fetchInstanceType = session.config.navigate('aws.batch.fetchInstanceType') + // newly added by Magesh + retryMode = session.config.navigate('aws.batch.retryMode', 'standard') + if( retryMode && retryMode !in VALID_RETRY_MODES ) + log.warn "Unexpected value for 'aws.batch.retryMode' config setting - offending value: $retryMode - valid values: ${VALID_RETRY_MODES.join(',')}" + metadataServiceTimeout = session.config.navigate('aws.batch.metadataServiceTimeout', AWS_METADATA_SERVICE_TIMEOUT) as Duration + // newly added by Magesh + if( fetchInstanceType==null ) fetchInstanceType = session.config.navigate('tower.enabled',false) } diff --git a/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy b/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy index bb6e237b7d..d9308f986e 100644 --- a/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy +++ b/plugins/nf-amazon/src/test/nextflow/cloud/aws/batch/AwsBatchTaskHandlerTest.groovy @@ -17,6 +17,8 @@ package nextflow.cloud.aws.batch +import nextflow.util.Duration + import java.nio.file.Paths import com.amazonaws.services.batch.AWSBatch @@ -134,6 +136,7 @@ class AwsBatchTaskHandlerTest extends Specification { def req = handler.newSubmitRequest(task) then: 1 * handler.getAwsOptions() >> { new AwsOptions(cliPath: '/bin/aws', region: 'eu-west-1') } + 1 * handler.getAwsCliOptimisationEnvs() >> [] 1 * handler.getJobQueue(task) >> 'queue1' 1 * handler.getJobDefinition(task) >> 'job-def:1' @@ -156,6 +159,7 @@ class AwsBatchTaskHandlerTest extends Specification { task.getName() >> 'batch-task' task.getConfig() >> new TaskConfig() + 1 * handler.getAwsCliOptimisationEnvs() >> [] 1 * handler.getAwsOptions() >> { new AwsOptions(cliPath: '/bin/aws') } 1 * handler.getJobQueue(task) >> 'queue1' 1 * handler.getJobDefinition(task) >> 'job-def:1' @@ -172,6 +176,7 @@ class AwsBatchTaskHandlerTest extends Specification { task.getName() >> 'batch-task' task.getConfig() >> new TaskConfig(time: '5 sec') + 1 * handler.getAwsCliOptimisationEnvs() >> [] 1 * handler.getAwsOptions() >> { new AwsOptions(cliPath: '/bin/aws') } 1 * handler.getJobQueue(task) >> 'queue2' 1 * handler.getJobDefinition(task) >> 'job-def:2' @@ -189,6 +194,7 @@ class AwsBatchTaskHandlerTest extends Specification { task.getName() >> 'batch-task' task.getConfig() >> new TaskConfig(time: '1 hour') + 1 * handler.getAwsCliOptimisationEnvs() >> [] 1 * handler.getAwsOptions() >> { new AwsOptions(cliPath: '/bin/aws') } 1 * handler.getJobQueue(task) >> 'queue3' 1 * handler.getJobDefinition(task) >> 'job-def:3' @@ -198,7 +204,6 @@ class AwsBatchTaskHandlerTest extends Specification { req.getJobDefinition() == 'job-def:3' // minimal allowed timeout is 60 seconds req.getTimeout().getAttemptDurationSeconds() == 3600 - } def 'should create an aws submit request with retry'() { @@ -277,20 +282,65 @@ class AwsBatchTaskHandlerTest extends Specification { def VAR_NXF = new KeyValuePair().withName('NXF_DEBUG').withValue('2') def bean = new TaskBean() - bean.environment = [FOO:'hello', BAR: 'world'] - def handler = [:] as AwsBatchTaskHandler + bean.environment = [FOO: 'hello', BAR: 'world'] + def handler = Spy(AwsBatchTaskHandler) handler.bean = bean when: def vars = handler.getEnvironmentVars() then: + 1 * handler.getAwsCliOptimisationEnvs() >> [] vars.size() == 0 when: handler.environment = [NXF_DEBUG: '2', NXF_FOO: 'ignore'] vars = handler.getEnvironmentVars() then: - vars == [ VAR_NXF ] + 1 * handler.getAwsCliOptimisationEnvs() >> [] + vars == [VAR_NXF] + + } + + def 'should return aws cli optimisation envs'() { + given: + def handler = Spy(AwsBatchTaskHandler) + when: + def vars = handler.getAwsCliOptimisationEnvs() + then: + 1 * handler.getAwsOptions() >> new AwsOptions() + vars.size() == 4 + vars.find {it.name == 'AWS_MAX_ATTEMPTS'}.value == '1' + vars.find {it.name == 'AWS_METADATA_SERVICE_NUM_ATTEMPTS'}.value == '1' + vars.find {it.name == 'AWS_RETRY_MODE'}.value == 'standard' + vars.find {it.name == 'AWS_METADATA_SERVICE_TIMEOUT'}.value == '30' + + when: + vars = handler.getAwsCliOptimisationEnvs() + then: + 1 * handler.getAwsOptions() >> new AwsOptions([ + retryMode: 'standard', + maxTransferAttempts: 100, + metadataServiceTimeout: Duration.of('1min') + ]) + vars.size() == 4 + vars.find {it.name == 'AWS_MAX_ATTEMPTS'}.value == '100' + vars.find {it.name == 'AWS_RETRY_MODE'}.value == 'standard' + vars.find {it.name == 'AWS_METADATA_SERVICE_NUM_ATTEMPTS'}.value == '100' + vars.find {it.name == 'AWS_METADATA_SERVICE_TIMEOUT'}.value == '60' + + when: + vars = handler.getAwsCliOptimisationEnvs() + then: + 1 * handler.getAwsOptions() >> new AwsOptions([ + retryMode: 'adaptive', + maxTransferAttempts: 10, + metadataServiceTimeout: Duration.of('10min') + ]) + vars.size() == 4 + vars.find {it.name == 'AWS_MAX_ATTEMPTS'}.value == '10' + vars.find {it.name == 'AWS_RETRY_MODE'}.value == 'adaptive' + vars.find {it.name == 'AWS_METADATA_SERVICE_NUM_ATTEMPTS'}.value == '10' + vars.find {it.name == 'AWS_METADATA_SERVICE_TIMEOUT'}.value == '600' } def 'should strip invalid chars for job definition name' () {