Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Charliecloud: registry, docs #5018

Merged
merged 10 commits into from
May 22, 2024
12 changes: 11 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ The following settings are available:
### Scope `charliecloud`

The `charliecloud` scope controls how [Charliecloud](https://hpc.github.io/charliecloud/) containers are executed by Nextflow.
If `charliecloud.writeFake` is unset / `false`, charliecloud will create a copy of the container in the process working directory.

The following settings are available:

Expand All @@ -497,6 +498,15 @@ The following settings are available:
`charliecloud.temp`
: Mounts a path of your choice as the `/tmp` directory in the container. Use the special value `auto` to create a temporary directory each time a container is created.

`charliecloud.registry`
: The registry from where images are pulled. It should be only used to specify a private registry server. It should NOT include the protocol prefix i.e. `http://`.

`charliecloud.writeFake`
: Enable `writeFake` with charliecloud. This allows to run containers from storage in writeable mode, using overlayfs, see [charliecloud documentation](https://hpc.github.io/charliecloud/ch-run.html#ch-run-overlay) for details

`charliecloud.useSquash`
: Create a temporary squashFS container image in the process work directory instead of a folder.

Read the {ref}`container-charliecloud` page to learn more about how to use Charliecloud containers with Nextflow.

(config-conda)=
Expand Down Expand Up @@ -2126,4 +2136,4 @@ Some features can be enabled using the `nextflow.enable` and `nextflow.preview`

: *Experimental: may change in a future release.*

: When `true`, enables {ref}`topic channels <channel-topic>` feature.
: When `true`, enables {ref}`topic channels <channel-topic>` feature.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import groovy.util.logging.Slf4j
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
* @author Patrick Hüther <patrick.huether@gmail.com>
* @author Laurent Modolo <laurent.modolo@ens-lyon.fr>
* @author Niklas Schandry <niklas@bio.lmu.de>
*/
@CompileStatic
@Slf4j
Expand Down Expand Up @@ -165,4 +166,4 @@ class CharliecloudBuilder extends ContainerBuilder<CharliecloudBuilder> {

return result
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import nextflow.util.Duration
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
* @author Patrick Hüther <patrick.huether@gmail.com>
* @author Niklas Schandry <niklas@bio.lmu.de>
*/
@Slf4j
@CompileStatic
Expand Down Expand Up @@ -79,11 +80,15 @@ class CharliecloudCache {
String simpleName(String imageUrl) {
def p = imageUrl.indexOf('://')
def name = p != -1 ? imageUrl.substring(p+3) : imageUrl

// add registry
if( registry )
if( registry ) {
if( !registry.endsWith('/') ) {
registry += '/'
}
name = registry + name

}
pditommaso marked this conversation as resolved.
Show resolved Hide resolved

name = name.replace(':','+').replace('/','%')
return name
}
Expand Down Expand Up @@ -207,8 +212,9 @@ class CharliecloudCache {
if( missingCacheDir )
log.warn1 "Charliecloud cache directory has not been defined -- Remote image will be stored in the path: $targetPath.parent.parent -- Use the charliecloud.cacheDir config option or set the NXF_CHARLIECLOUD_CACHEDIR variable to specify a different location"


log.info "Charliecloud pulling image $imageUrl [cache $targetPath]"

String cmd = "ch-image pull -s $targetPath.parent.parent $imageUrl > /dev/null"
try {
runCommand( cmd, targetPath )
Expand Down Expand Up @@ -296,4 +302,4 @@ class CharliecloudCache {
return result
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,15 @@ class ContainerHandler {
return Escape.path(result)
}
if( engine == 'charliecloud' ) {
final normalizedImageName = normalizeCharliecloudImageName(imageName)
if( !config.isEnabled() || !normalizedImageName )
return normalizedImageName
// if the imagename starts with '/' it's an absolute path
// otherwise we assume it's in a remote registry and pull it from there
final requiresCaching = !imageName.startsWith('/')
if( ContainerInspectMode.active() && requiresCaching )
return imageName
final result = requiresCaching ? createCharliecloudCache(this.config, imageName) : imageName
final result = requiresCaching ? createCharliecloudCache(this.config, normalizedImageName) : normalizedImageName
return Escape.path(result)
}
// fallback to docker
Expand Down Expand Up @@ -271,4 +274,38 @@ class ContainerHandler {
// prefix it with the `docker://` pseudo protocol used by apptainer to download it
return "docker://${normalizeDockerImageName(img)}"
}
}

/**
* Normalize charliecloud image name resolving the absolute path
*
* @param imageName The container image name
* @return Image name in canonical format
*/
@PackageScope
String normalizeCharliecloudImageName(String img) {
pditommaso marked this conversation as resolved.
Show resolved Hide resolved
if( !img )
return null

// when starts with `/` it's an absolute image file path, just return it
if( img.startsWith("/") ) {
return img
}
// remove docker:// if present
if( img.startsWith("docker://") ) {
img = img.minus("docker://")
}
// if no tag, add :latest
if( !img.contains(':') ) {
img += ':latest'
}

// if it's the path of an existing image file return it
def imagePath = baseDir.resolve(img)
if( imagePath.exists() ) {
return imagePath.toString()
}

// in all other case it's supposed to be the name of an image
return "${normalizeDockerImageName(img)}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import spock.lang.Unroll
* @author Patrick Hüther <patrick.huether@gmail.com>
*/
class CharliecloudCacheTest extends Specification {

@Unroll
def 'should return a simple name given an image url'() {

Expand All @@ -48,6 +47,21 @@ class CharliecloudCacheTest extends Specification {
'foo:bar' | 'foo+bar'
}

@Unroll
def 'should return a path with registry'() {

given:
def helper = new CharliecloudCache([registry: registry])

expect:
helper.simpleName(url) == expected

where:
url | registry | expected
'foo:2.0' | 'my.reg' | 'my.reg%foo+2.0'
'foo:2.0' | 'my.reg/' | 'my.reg%foo+2.0'
}

def 'should return the cache dir from the config file' () {

given:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,35 @@ class ContainerHandlerTest extends Specification {

}

@Unroll
pditommaso marked this conversation as resolved.
Show resolved Hide resolved
def 'test normalize method for charliecloud' () {

given:
def n = new ContainerHandler([registry: registry])

expect:
n.normalizeCharliecloudImageName(image) == expected

where:
image | registry | expected
null | null | null
'' | null | null
'/abs/path/bar.img' | null | '/abs/path/bar.img'
'docker://library/busybox' | null | 'library/busybox:latest'
'shub://busybox' | null | 'shub://busybox'
'foo://busybox' | null | 'foo://busybox'
'foo' | null | 'foo:latest'
'foo:2.0' | null | 'foo:2.0'
'foo.img' | null | 'foo.img:latest'
'quay.io/busybox' | null | 'quay.io/busybox:latest'
'http://reg.io/v1/alpine:latest' | null | 'http://reg.io/v1/alpine:latest'
'https://reg.io/v1/alpine:latest' | null | 'https://reg.io/v1/alpine:latest'
and:
'/abs/path/bar.img' | 'my.reg' | '/abs/path/bar.img'
'busybox' | 'my.reg' | 'my.reg/busybox:latest'
'foo:2.0' | 'my.reg' | 'my.reg/foo:2.0'
}

@Unroll
def 'test normalize method for singularity' () {
given:
Expand Down