Skip to content

Commit

Permalink
Add CSI ephemeral volume (#2988) [ci fast]
Browse files Browse the repository at this point in the history
Signed-off-by: Ben Sherman <[email protected]>
Signed-off-by: Paolo Di Tommaso <[email protected]>
Co-authored-by: Paolo Di Tommaso <[email protected]>
  • Loading branch information
bentsherman and pditommaso authored Nov 14, 2022
1 parent 6b496bb commit f18f6e8
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 32 deletions.
5 changes: 3 additions & 2 deletions docs/process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1903,8 +1903,9 @@ The ``pod`` directive allows the definition of the following options:
``env: <E>, fieldPath: <V>`` Defines an environment variable with name ``E`` and whose value is given by the ``V`` `field path <https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/>`_.
``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.
``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.
``config: <C/K>, mountPath: </absolute/path>`` Mounts a `ConfigMap <https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/>`_ with name ``C`` with key ``K``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.
``csi: <V>, mountPath: </absolute/path>`` Mounts a `CSI ephemeral volume <https://kubernetes.io/docs/concepts/storage/ephemeral-volumes/#csi-ephemeral-volumes>`_ with config ``V``to the path ``/absolute/path``.
``secret: <S/K>, mountPath: </absolute/path>`` Mounts a `Secret <https://kubernetes.io/docs/concepts/configuration/secret/>`_ with name ``S`` with key ``K``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'``.
``imagePullSecret: <V>`` Specifies the secret name to access a private container image registry. See `Kubernetes documentation <https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod>`_ for details.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 java.nio.file.Paths

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

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

String mountPath

Map csi

PodMountCsiEphemeral( Map csi, String mountPath ) {
assert csi
assert mountPath

this.csi = csi
this.mountPath = mountPath
}

PodMountCsiEphemeral( Map entry ) {
this(entry.csi 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<PodMountCsiEphemeral> mountCsiEphemerals

private Collection<PodMountSecret> mountSecrets

private Collection<PodVolumeClaim> mountClaims
Expand All @@ -66,6 +68,7 @@ class PodOptions {
PodOptions( List<Map> options=null ) {
int size = options ? options.size() : 0
envVars = new HashSet<>(size)
mountCsiEphemerals = new HashSet<>(size)
mountSecrets = new HashSet<>(size)
mountConfigMaps = new HashSet<>(size)
mountClaims = new HashSet<>(size)
Expand Down Expand Up @@ -95,11 +98,14 @@ class PodOptions {
envVars << PodEnv.config(entry.env, entry.config)
}
else if( entry.mountPath && entry.secret ) {
mountSecrets << new PodMountSecret(entry)
mountSecrets << new PodMountSecret(entry)
}
else if( entry.mountPath && entry.config ) {
mountConfigMaps << new PodMountConfig(entry)
}
else if( entry.mountPath && entry.csi ) {
mountCsiEphemerals << new PodMountCsiEphemeral(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<PodMountCsiEphemeral> getMountCsiEphemerals() { mountCsiEphemerals }

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 )

// csi ephemeral volumes
result.mountCsiEphemerals.addAll( mountCsiEphemerals )
result.mountCsiEphemerals.addAll( other.mountCsiEphemerals )

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

AcceleratorResource accelerator

Collection<PodMountSecret> secrets = []

Collection<PodMountConfig> configMaps = []

Collection<PodMountCsiEphemeral> csiEphemerals = []

Collection<PodMountSecret> secrets = []

Collection<PodHostMount> hostMounts = []

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

PodSpecBuilder withCsiEphemerals( Collection<PodMountCsiEphemeral> csiEphemerals ) {
this.csiEphemerals.addAll(csiEphemerals)
return this
}

PodSpecBuilder withCsiEphemeral( PodMountCsiEphemeral csiEphemeral ) {
this.csiEphemerals.add(csiEphemeral)
return this
}

PodSpecBuilder withSecrets( Collection<PodMountSecret> secrets ) {
this.secrets.addAll(secrets)
return this
Expand Down Expand Up @@ -273,12 +285,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() )
// -- csi ephemeral volumes
if( opts.getMountCsiEphemerals() )
csiEphemerals.addAll( opts.getMountCsiEphemerals() )
// -- secrets
if( opts.getMountSecrets() )
secrets.addAll( opts.getMountSecrets() )
// -- volume claims
if( opts.getVolumeClaims() )
volumeClaims.addAll( opts.getVolumeClaims() )
Expand Down Expand Up @@ -438,7 +453,7 @@ class PodSpecBuilder {
volumes << [name: volName, persistentVolumeClaim: [claimName: claimName]]
}

// -- volume claims
// -- persistent volume claims
for( PodVolumeClaim entry : volumeClaims ) {
//check if we already have a volume for the pvc
final name = namesMap.get(entry.claimName)
Expand All @@ -456,19 +471,26 @@ class PodSpecBuilder {
configMapToSpec(name, entry, mounts, volumes)
}

// host mounts
for( PodHostMount entry : hostMounts ) {
// -- csi ephemeral volumes
for( PodMountCsiEphemeral entry : csiEphemerals ) {
final name = nextVolName()
mounts << [name: name, mountPath: entry.mountPath]
volumes << [name: name, hostPath: [path: entry.hostPath]]
mounts << [name: name, mountPath: entry.mountPath, readOnly: entry.csi.readOnly ?: false]
volumes << [name: name, csi: entry.csi]
}

// secret volumes
// -- secret volumes
for( PodMountSecret entry : secrets ) {
final name = nextVolName()
secretToSpec(name, entry, mounts, volumes)
}

// -- host path volumes
for( PodHostMount entry : hostMounts ) {
final name = nextVolName()
mounts << [name: name, mountPath: entry.mountPath]
volumes << [name: name, hostPath: [path: entry.hostPath]]
}


if( volumes )
spec.volumes = volumes
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.getMountCsiEphemerals() == [] as Set
options.getMountSecrets() == [] as Set
options.getAutomountServiceAccountToken() == true
}

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


def 'should return csi ephemeral mounts' () {

given:
def options = [
[
mountPath: '/data',
csi: [
driver: 'inline.storage.kubernetes.io',
volumeAttributes: [foo: 'bar']
]
]
]

when:
def csiEphemerals = new PodOptions(options).getMountCsiEphemerals()
then:
csiEphemerals == [
new PodMountCsiEphemeral(mountPath: '/data', csi: options[0].csi)
] as Set
}


def 'should return secret mounts' () {

given:
Expand Down Expand Up @@ -148,7 +171,7 @@ class PodOptionsTest extends Specification {

}

def 'should create volume claims' () {
def 'should create persistent volume claims' () {
given:
def options = [
[volumeClaim:'pvc1', mountPath: '/this/path'],
Expand Down Expand Up @@ -196,35 +219,36 @@ class PodOptionsTest extends Specification {
given:
def list1 = [
[env: 'HELLO', value: 'WORLD'],
[secret: 'secret/key', mountPath: '/etc/secret'],
[config: 'data/key', mountPath: '/data/file.txt'],
[secret: 'secret/key', mountPath: '/etc/secret'],
[volumeClaim: 'pvc', mountPath: '/mnt/claim'],
[runAsUser: 500]
]

def list2 = [
[env: 'ALPHA', value: 'GAMMA'],
[secret: 'foo/key', mountPath: '/a/aa'],
[config: 'bar/key', mountPath: '/b/bb'],
[secret: 'foo/key', mountPath: '/a/aa'],
[volumeClaim: 'cvp', mountPath: '/c/cc'],

[env: 'DELTA', value: 'LAMBDA'],
[secret: 'x', mountPath: '/x'],
[config: 'y', mountPath: '/y'],
[secret: 'x', mountPath: '/x'],
[volumeClaim: 'z', mountPath: '/z'],
]

def list3 = [
[env: 'HELLO', value: 'WORLD'],
[secret: 'secret/key', mountPath: '/etc/secret'],
[config: 'data/key', mountPath: '/data/file.txt'],
[secret: 'secret/key', mountPath: '/etc/secret'],
[volumeClaim: 'pvc', mountPath: '/mnt/claim'],

[env: 'DELTA', value: 'LAMBDA'],
[secret: 'x', mountPath: '/x'],
[config: 'y', mountPath: '/y'],
[secret: 'x', mountPath: '/x'],
[volumeClaim: 'z', mountPath: '/z'],

[csi: [driver: 'inline.storage.kubernetes.io'], mountPath: '/data'],
[securityContext: [runAsUser: 1000, fsGroup: 200, allowPrivilegeEscalation: true]],
[nodeSelector: 'foo=X, bar=Y'],
[automountServiceAccountToken: false],
Expand All @@ -239,7 +263,6 @@ class PodOptionsTest extends Specification {
opts == new PodOptions()

when:

opts = new PodOptions(list1) + new PodOptions()
then:
opts == new PodOptions(list1)
Expand All @@ -257,7 +280,6 @@ class PodOptionsTest extends Specification {
opts == new PodOptions(list1)
opts.securityContext.toSpec() == [runAsUser:500]


when:
opts = new PodOptions(list1) + new PodOptions(list2)
then:
Expand All @@ -268,23 +290,27 @@ class PodOptionsTest extends Specification {
opts = new PodOptions(list1) + new PodOptions(list3)
then:
opts.getEnvVars() == [
PodEnv.value('HELLO','WORLD'),
PodEnv.value('DELTA','LAMBDA')
PodEnv.value('HELLO','WORLD'),
PodEnv.value('DELTA','LAMBDA')
] as Set

opts.getMountConfigMaps() == [
new PodMountConfig('data/key', '/data/file.txt'),
new PodMountConfig('y', '/y'),
] as Set

opts.getMountCsiEphemerals() == [
new PodMountCsiEphemeral([driver: 'inline.storage.kubernetes.io'], '/data')
] as Set

opts.getMountSecrets() == [
new PodMountSecret('secret/key', '/etc/secret'),
new PodMountSecret('x', '/x')
] as Set

opts.getMountConfigMaps() == [
new PodMountConfig('data/key', '/data/file.txt'),
new PodMountConfig('y', '/y'),
] as Set

opts.getVolumeClaims() == [
new PodVolumeClaim('pvc','/mnt/claim'),
new PodVolumeClaim('z','/z'),
new PodVolumeClaim('pvc','/mnt/claim'),
new PodVolumeClaim('z','/z'),
] as Set

opts.securityContext.toSpec() == [runAsUser: 1000, fsGroup: 200, allowPrivilegeEscalation: true]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,39 @@ class PodSpecBuilderTest extends Specification {

}

def 'should get csi ephemeral mounts' () {

when:
def spec = new PodSpecBuilder()
.withPodName('foo')
.withImageName('busybox')
.withWorkDir('/path')
.withCommand(['echo'])
.withCsiEphemeral(new PodMountCsiEphemeral(csi: [driver: 'inline.storage.kubernetes.io', readOnly: true], mountPath: '/data'))
.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: '/data', readOnly: true]
]
]],
volumes: [
[name: 'vol-1', csi: [driver: 'inline.storage.kubernetes.io', readOnly: true]]
]
]
]
}

def 'should consume env secrets' () {

when:
Expand Down

0 comments on commit f18f6e8

Please sign in to comment.