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

Use ephemeral-storage for disk directive and k8s executor #2998

Merged
merged 13 commits into from
Nov 19, 2022
1 change: 1 addition & 0 deletions docs/executor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Resource requests and other job characteristics can be controlled via the follow

* :ref:`process-accelerator`
* :ref:`process-cpus`
* :ref:`process-disk`
* :ref:`process-memory`
* :ref:`process-pod`
* :ref:`process-time`
Expand Down
1 change: 1 addition & 0 deletions docs/process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1898,6 +1898,7 @@ The ``pod`` directive allows the definition of the following options:
``env: <E>, config: <C/K>`` Defines an environment variable with name ``E`` and whose value is given by the entry associated to the key with name ``K`` in the `ConfigMap <https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/>`_ with name ``C``.
``env: <E>, secret: <S/K>`` Defines an environment variable with name ``E`` and whose value is given by the entry associated to the key with name ``K`` in the `Secret <https://kubernetes.io/docs/concepts/configuration/secret/>`_ with name ``S``.
``config: <C/K>, mountPath: </absolute/path>`` The content of the `ConfigMap <https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/>`_ with name ``C`` with key ``K`` is made available to the path ``/absolute/path``. When the key component is omitted the path is interpreted as a directory and all the ``ConfigMap`` entries are exposed in that path.
``emptyDir: <V>, mountPath: </absolute/path>`` Mounts an `emptyDir <https://kubernetes.io/docs/concepts/storage/volumes/#emptydir>`_ with configuration ``V`` to the path ``/absolute/path``.
``secret: <S/K>, mountPath: </absolute/path>`` The content of the `Secret <https://kubernetes.io/docs/concepts/configuration/secret/>`_ with name ``S`` with key ``K`` is made available to the path ``/absolute/path``. When the key component is omitted the path is interpreted as a directory and all the ``Secret`` entries are exposed in that path.
``volumeClaim: <V>, mountPath: </absolute/path>`` Mounts a `Persistent volume claim <https://kubernetes.io/docs/concepts/storage/persistent-volumes/>`_ with name ``V`` to the specified path location. Use the optional ``subPath`` parameter to mount a directory inside the referenced volume instead of its root. The volume may be mounted with `readOnly: true`, but is read/write by default.
``imagePullPolicy: <V>`` Specifies the strategy to be used to pull the container image e.g. ``imagePullPolicy: 'Always'``.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,14 @@ class K8sTaskHandler extends TaskHandler {
// add computing resources
final cpus = taskCfg.getCpus()
final mem = taskCfg.getMemory()
final disk = taskCfg.getDisk()
final acc = taskCfg.getAccelerator()
if( cpus )
builder.withCpus(cpus)
if( mem )
builder.withMemory(mem)
if( disk )
builder.withDisk(disk)
if( acc )
builder.withAccelerator(acc)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2020-2022, Seqera Labs
* Copyright 2013-2019, Centre for Genomic Regulation (CRG)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.k8s.model

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

/**
* Model a K8s pod emptyDir mount
*
* See also https://kubernetes.io/docs/concepts/storage/volumes/#emptydir
*
* @author Ben Sherman <[email protected]>
*/
@CompileStatic
@ToString(includeNames = true)
@EqualsAndHashCode
class PodMountEmptyDir {

String mountPath

Map emptyDir

PodMountEmptyDir( Map emptyDir, String mountPath ) {
assert emptyDir
assert mountPath

this.emptyDir = emptyDir
this.mountPath = mountPath
}

PodMountEmptyDir( Map entry ) {
this(entry.emptyDir as Map, entry.mountPath as String)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class PodOptions {

private Collection<PodMountConfig> mountConfigMaps

private Collection<PodMountEmptyDir> mountEmptyDirs

private Collection<PodMountSecret> mountSecrets

private Collection<PodVolumeClaim> mountClaims
Expand All @@ -66,8 +68,9 @@ class PodOptions {
PodOptions( List<Map> options=null ) {
int size = options ? options.size() : 0
envVars = new HashSet<>(size)
mountSecrets = new HashSet<>(size)
mountConfigMaps = new HashSet<>(size)
mountEmptyDirs = new HashSet<>(size)
mountSecrets = new HashSet<>(size)
mountClaims = new HashSet<>(size)
automountServiceAccountToken = true
tolerations = new ArrayList<Map>(size)
Expand All @@ -94,12 +97,15 @@ class PodOptions {
else if( entry.env && entry.config ) {
envVars << PodEnv.config(entry.env, entry.config)
}
else if( entry.mountPath && entry.secret ) {
mountSecrets << new PodMountSecret(entry)
}
else if( entry.mountPath && entry.config ) {
mountConfigMaps << new PodMountConfig(entry)
}
else if( entry.mountPath && entry.emptyDir ) {
mountEmptyDirs << new PodMountEmptyDir(entry)
}
else if( entry.mountPath && entry.secret ) {
mountSecrets << new PodMountSecret(entry)
}
else if( entry.mountPath && entry.volumeClaim ) {
mountClaims << new PodVolumeClaim(entry)
}
Expand Down Expand Up @@ -148,6 +154,8 @@ class PodOptions {

Collection<PodMountConfig> getMountConfigMaps() { mountConfigMaps }

Collection<PodMountEmptyDir> getMountEmptyDirs() { mountEmptyDirs }

Collection<PodMountSecret> getMountSecrets() { mountSecrets }

Collection<PodVolumeClaim> getVolumeClaims() { mountClaims }
Expand Down Expand Up @@ -210,6 +218,10 @@ class PodOptions {
result.mountConfigMaps.addAll( mountConfigMaps )
result.mountConfigMaps.addAll( other.mountConfigMaps )

// empty dirs
result.mountEmptyDirs.addAll( mountEmptyDirs )
result.mountEmptyDirs.addAll( other.mountEmptyDirs )

// secrets
result.mountSecrets.addAll( mountSecrets )
result.mountSecrets.addAll( other.mountSecrets )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,20 @@ class PodSpecBuilder {

String memory

String disk

String serviceAccount

boolean automountServiceAccountToken = true

AcceleratorResource accelerator

Collection<PodMountSecret> secrets = []

Collection<PodMountConfig> configMaps = []

Collection<PodMountEmptyDir> emptyDirs = []

Collection<PodMountSecret> secrets = []

Collection<PodHostMount> hostMounts = []

Collection<PodVolumeClaim> volumeClaims = []
Expand Down Expand Up @@ -167,6 +171,16 @@ class PodSpecBuilder {
return this
}

PodSpecBuilder withDisk(String disk) {
this.disk = disk
return this
}

PodSpecBuilder withDisk(MemoryUnit disk) {
this.disk = "${disk.mega}Mi".toString()
return this
}

PodSpecBuilder withAccelerator(AcceleratorResource acc) {
this.accelerator = acc
return this
Expand Down Expand Up @@ -223,6 +237,16 @@ class PodSpecBuilder {
return this
}

PodSpecBuilder withEmptyDirs( Collection<PodMountEmptyDir> emptyDirs ) {
this.emptyDirs.addAll(emptyDirs)
return this
}

PodSpecBuilder withEmptyDir( PodMountEmptyDir emptyDir ) {
this.emptyDirs.add(emptyDir)
return this
}

PodSpecBuilder withSecrets( Collection<PodMountSecret> secrets ) {
this.secrets.addAll(secrets)
return this
Expand Down Expand Up @@ -262,12 +286,15 @@ class PodSpecBuilder {
// -- env vars
if( opts.getEnvVars() )
envVars.addAll( opts.getEnvVars() )
// -- secrets
if( opts.getMountSecrets() )
secrets.addAll( opts.getMountSecrets() )
// -- configMaps
if( opts.getMountConfigMaps() )
configMaps.addAll( opts.getMountConfigMaps() )
// -- emptyDirs
if( opts.getMountEmptyDirs() )
emptyDirs.addAll( opts.getMountEmptyDirs() )
// -- secrets
if( opts.getMountSecrets() )
secrets.addAll( opts.getMountSecrets() )
// -- volume claims
if( opts.getVolumeClaims() )
volumeClaims.addAll( opts.getVolumeClaims() )
Expand Down Expand Up @@ -334,6 +361,8 @@ class PodSpecBuilder {
res.cpu = this.cpus
if( this.memory )
res.memory = this.memory
if( this.disk )
res['ephemeral-storage'] = this.disk

final container = [ name: this.podName, image: this.imageName ]
if( this.command )
Expand Down Expand Up @@ -445,6 +474,13 @@ class PodSpecBuilder {
configMapToSpec(name, entry, mounts, volumes)
}

// -- emptyDir volumes
for( PodMountEmptyDir entry : emptyDirs ) {
final name = nextVolName()
mounts << [name: name, mountPath: entry.mountPath]
volumes << [name: name, emptyDir: entry.emptyDir]
}

// host mounts
for( PodHostMount entry : hostMounts ) {
final name = nextVolName()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ class PodOptionsTest extends Specification {
def options = new PodOptions(null)
then:
options.getEnvVars() == [] as Set
options.getMountSecrets() == [] as Set
options.getMountConfigMaps() == [] as Set
options.getMountEmptyDirs() == [] as Set
options.getMountSecrets() == [] as Set
options.getAutomountServiceAccountToken() == true
}

Expand Down Expand Up @@ -95,6 +96,25 @@ class PodOptionsTest extends Specification {
}


def 'should return emptyDir mounts' () {

given:
def options = [
[mountPath: '/scratch1', emptyDir: [medium: 'Memory']],
[mountPath: '/scratch2', emptyDir: [medium: 'Disk']]
]

when:
def emptyDirs = new PodOptions(options).getMountEmptyDirs()
then:
emptyDirs.size() == 2
emptyDirs == [
new PodMountEmptyDir(options[0]),
new PodMountEmptyDir(options[1])
] as Set
}


def 'should return secret mounts' () {

given:
Expand Down Expand Up @@ -225,6 +245,7 @@ class PodOptionsTest extends Specification {
[config: 'y', mountPath: '/y'],
[volumeClaim: 'z', mountPath: '/z'],

[emptyDir: [:], mountPath: 'scratch1'],
bentsherman marked this conversation as resolved.
Show resolved Hide resolved
[securityContext: [runAsUser: 1000, fsGroup: 200, allowPrivilegeEscalation: true]],
[nodeSelector: 'foo=X, bar=Y'],
[automountServiceAccountToken: false],
Expand Down Expand Up @@ -282,6 +303,10 @@ class PodOptionsTest extends Specification {
new PodMountConfig('y', '/y'),
] as Set

opts.getMountEmptyDirs() == [
new PodMountEmptyDir([:], '/scratch1'),
] as Set

opts.getVolumeClaims() == [
new PodVolumeClaim('pvc','/mnt/claim'),
new PodVolumeClaim('z','/z'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ class PodSpecBuilderTest extends Specification {
.withCpus(8)
.withAccelerator( new AcceleratorResource(request: 5, limit:10, type: 'foo.org') )
.withMemory('100Gi')
.withDisk('10Gi')
.build()

then:
Expand All @@ -270,8 +271,8 @@ class PodSpecBuilderTest extends Specification {
[name:'DELTA', value:'world']
],
resources:[
requests: ['foo.org/gpu':5, cpu:8, memory:'100Gi'],
limits:['foo.org/gpu':10, cpu:8, memory:'100Gi'] ]
requests: ['foo.org/gpu':5, cpu:8, memory:'100Gi', 'ephemeral-storage':'10Gi'],
limits:['foo.org/gpu':10, cpu:8, memory:'100Gi', 'ephemeral-storage':'10Gi'] ]
]
]
]
Expand Down Expand Up @@ -390,6 +391,42 @@ class PodSpecBuilderTest extends Specification {

}

def 'should get empty dir mounts' () {

when:
def spec = new PodSpecBuilder()
.withPodName('foo')
.withImageName('busybox')
.withWorkDir('/path')
.withCommand(['echo'])
.withEmptyDir(new PodMountEmptyDir(mountPath: '/scratch1', emptyDir: [medium: 'Disk']))
.withEmptyDir(new PodMountEmptyDir(mountPath: '/scratch2', emptyDir: [medium: 'Memory']))
.build()
then:
spec == [
apiVersion: 'v1',
kind: 'Pod',
metadata: [name: 'foo', namespace: 'default'],
spec: [
restartPolicy: 'Never',
containers: [[
name: 'foo',
image: 'busybox',
command: ['echo'],
workingDir: '/path',
volumeMounts: [
[name: 'vol-1', mountPath: '/scratch1'],
[name: 'vol-2', mountPath: '/scratch2']
]
]],
volumes: [
[name: 'vol-1', emptyDir: [medium: 'Disk']],
[name: 'vol-2', emptyDir: [medium: 'Memory']]
]
]
]
}

def 'should consume env secrets' () {

when:
Expand Down