diff --git a/modules/nextflow/src/test/groovy/nextflow/k8s/K8sDriverLauncherTest.groovy b/modules/nextflow/src/test/groovy/nextflow/k8s/K8sDriverLauncherTest.groovy index 8696f3a41a..8db49f6664 100644 --- a/modules/nextflow/src/test/groovy/nextflow/k8s/K8sDriverLauncherTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/k8s/K8sDriverLauncherTest.groovy @@ -138,7 +138,6 @@ class K8sDriverLauncherTest extends Specification { def pod = Mock(PodOptions) pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ] pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ] - pod.getAutomountServiceAccountToken() >> true def k8s = Mock(K8sConfig) k8s.getNextflowImageName() >> 'the-image' @@ -163,6 +162,7 @@ class K8sDriverLauncherTest extends Specification { kind: 'Pod', metadata: [name:'foo-boo', namespace:'foo', labels:[app:'nextflow', runName:'foo-boo']], spec: [ + automountServiceAccountToken: false, restartPolicy: 'Never', containers: [ [ @@ -197,7 +197,6 @@ class K8sDriverLauncherTest extends Specification { def pod = Mock(PodOptions) pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ] pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ] - pod.getAutomountServiceAccountToken() >> true def k8s = Mock(K8sConfig) k8s.getNextflowImageName() >> 'the-image' @@ -230,6 +229,7 @@ class K8sDriverLauncherTest extends Specification { template: [ metadata: metadata, spec: [ + automountServiceAccountToken: false, restartPolicy: 'Never', containers: [ [ @@ -265,7 +265,6 @@ class K8sDriverLauncherTest extends Specification { def pod = Mock(PodOptions) pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ] pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ] - pod.getAutomountServiceAccountToken() >> true def k8s = Mock(K8sConfig) k8s.getLaunchDir() >> '/the/user/dir' @@ -281,40 +280,11 @@ class K8sDriverLauncherTest extends Specification { driver.@headImage = 'foo/bar' when: - def spec = driver.makeLauncherSpec() + def result = driver.makeLauncherSpec() then: driver.getLaunchCli() >> 'nextflow run foo' - - spec == [ - apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo-boo', namespace:'foo', labels:[app:'nextflow', runName:'foo-boo']], - spec: [ - restartPolicy: 'Never', - containers: [ - [ - name: 'foo-boo', - image: 'foo/bar', - command: ['/bin/bash', '-c', "source /etc/nextflow/init.sh; nextflow run foo"], - env: [ - [name:'NXF_WORK', value:'/the/work/dir'], - [name:'NXF_ASSETS', value:'/the/project/dir'], - [name:'NXF_EXECUTOR', value:'k8s'], - [name:'NXF_ANSI_LOG', value: 'false'] - ], - volumeMounts: [ - [name:'vol-1', mountPath:'/mnt/path/data'], - [name:'vol-2', mountPath:'/mnt/path/cfg'] - ] - ] - ], - serviceAccountName: 'bar', - volumes: [ - [name:'vol-1', persistentVolumeClaim:[claimName:'pvc-1']], - [name:'vol-2', configMap:[name:'cfg-2'] ] - ] - ] - ] + and: + result.spec.containers[0].image == 'foo/bar' } @@ -324,9 +294,9 @@ class K8sDriverLauncherTest extends Specification { def pod = Mock(PodOptions) pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ] pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ] - pod.getAutomountServiceAccountToken() >> true def k8s = Mock(K8sConfig) + k8s.getNextflowImageName() >> 'the-image' k8s.getLaunchDir() >> '/the/user/dir' k8s.getWorkDir() >> '/the/work/dir' k8s.getProjectDir() >> '/the/project/dir' @@ -337,48 +307,17 @@ class K8sDriverLauncherTest extends Specification { driver.@runName = 'foo-boo' driver.@k8sClient = new K8sClient(new ClientConfig(namespace: 'foo', serviceAccount: 'bar')) driver.@k8sConfig = k8s - driver.@headImage = 'foo/bar' driver.@headCpus = 2 driver.@headMemory = '200Mi' when: - def spec = driver.makeLauncherSpec() + def result = driver.makeLauncherSpec() then: driver.getLaunchCli() >> 'nextflow run foo' - - spec == [ - apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo-boo', namespace:'foo', labels:[app:'nextflow', runName:'foo-boo']], - spec: [ - restartPolicy: 'Never', - containers: [ - [ - name: 'foo-boo', - image: 'foo/bar', - command: ['/bin/bash', '-c', "source /etc/nextflow/init.sh; nextflow run foo"], - env: [ - [name:'NXF_WORK', value:'/the/work/dir'], - [name:'NXF_ASSETS', value:'/the/project/dir'], - [name:'NXF_EXECUTOR', value:'k8s'], - [name:'NXF_ANSI_LOG', value: 'false'] - ], - resources: [ - requests: [cpu: 2, memory: '200Mi'], - limits: [memory: '200Mi'] - ], - volumeMounts: [ - [name:'vol-1', mountPath:'/mnt/path/data'], - [name:'vol-2', mountPath:'/mnt/path/cfg'] - ] - ] - ], - serviceAccountName: 'bar', - volumes: [ - [name:'vol-1', persistentVolumeClaim:[claimName:'pvc-1']], - [name:'vol-2', configMap:[name:'cfg-2'] ] - ] - ] + and: + result.spec.containers[0].resources == [ + requests: [cpu: 2, memory: '200Mi'], + limits: [memory: '200Mi'] ] } diff --git a/modules/nextflow/src/test/groovy/nextflow/k8s/K8sTaskHandlerTest.groovy b/modules/nextflow/src/test/groovy/nextflow/k8s/K8sTaskHandlerTest.groovy index 4d6f06af2f..1433ab5b3f 100644 --- a/modules/nextflow/src/test/groovy/nextflow/k8s/K8sTaskHandlerTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/k8s/K8sTaskHandlerTest.groovy @@ -52,7 +52,7 @@ class K8sTaskHandlerTest extends Specification { PodSpecBuilder.VOLUMES.set(0) } - def 'should return a new pod with args' () { + def 'should return a new pod request' () { given: def WORK_DIR = Paths.get('/some/work/dir') def config = Mock(TaskConfig) @@ -79,136 +79,24 @@ class K8sTaskHandlerTest extends Specification { 1 * config.getCpus() >> 0 1 * config.getMemory() >> null 1 * client.getConfig() >> new ClientConfig() - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [ - name:'nf-123', - namespace:'default' - ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-123', - image:'debian:latest', - args:['/bin/bash', '-ue','/some/work/dir/.command.run'] ] - ] - ] - ] - - when: - result = handler.newSubmitRequest(task) - then: - _ * handler.fusionEnabled() >> false - 1 * handler.entrypointOverride() >> true - 1 * handler.getSyntheticPodName(task) >> 'nf-foo' - 1 * handler.getLabels(task) >> [sessionId:'xxx'] - 1 * handler.getAnnotations() >> [evict: 'false'] - 1 * handler.getPodOptions() >> new PodOptions() - 1 * handler.getContainerMounts() >> [] - 1 * handler.fixOwnership() >> true - 1 * handler.getOwner() >> '501:502' - 1 * task.getContainer() >> 'debian:latest' - 1 * task.getWorkDir() >> WORK_DIR - 1 * task.getConfig() >> config - 1 * config.getCpus() >> 1 - 1 * config.getMemory() >> null - 1 * client.getConfig() >> new ClientConfig() - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-foo', namespace:'default', labels: [sessionId: 'xxx'], annotations: [evict: 'false']], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-foo', - image:'debian:latest', - command:['/bin/bash', '-ue','/some/work/dir/.command.run'], - resources:[ requests: [cpu:1] ], - env: [ [name:'NXF_OWNER', value:'501:502'] ] - ] - ] - ] - ] - - - when: - result = handler.newSubmitRequest(task) - then: - _ * handler.fusionEnabled() >> false - 1 * handler.fixOwnership() >> false - 1 * handler.entrypointOverride() >> true - 1 * handler.getSyntheticPodName(task) >> 'nf-abc' - 1 * handler.getLabels(task) >> [:] - 1 * handler.getAnnotations() >> [:] - 1 * handler.getPodOptions() >> new PodOptions() - 1 * handler.getContainerMounts() >> [] - 1 * task.getContainer() >> 'user/alpine:1.0' - 1 * task.getWorkDir() >> WORK_DIR - 1 * task.getConfig() >> config - 1 * config.getCpus() >> 4 - 1 * config.getMemory() >> MemoryUnit.of('16GB') - 1 * client.getConfig() >> new ClientConfig(namespace: 'namespace-x') - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-abc', namespace:'namespace-x' ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-abc', - image:'user/alpine:1.0', - command:['/bin/bash', '-ue', '/some/work/dir/.command.run'], - resources:[ requests: [cpu:4, memory:'16384Mi'], limits:[memory:'16384Mi'] ] - ] - ] - ] + and: + result == [ + apiVersion: 'v1', + kind: 'Pod', + metadata: [ + name:'nf-123', + namespace:'default' + ], + spec: [ + restartPolicy:'Never', + containers: [[ + name:'nf-123', + image:'debian:latest', + args:['/bin/bash', '-ue','/some/work/dir/.command.run'] + ]] + ] ] - } - - def 'should return a new pod request with no storage' () { - given: - def WORK_DIR = Paths.get('/some/work/dir') - def config = Mock(TaskConfig) - def task = Mock(TaskRun) - def client = Mock(K8sClient) - def builder = Mock(K8sWrapperBuilder) - def handler = Spy(new K8sTaskHandler(builder: builder, client:client)) - Map result - - when: - result = handler.newSubmitRequest(task) - then: - _ * handler.fusionEnabled() >> false - 1 * handler.fixOwnership() >> false - 1 * handler.entrypointOverride() >> true - 1 * handler.getPodOptions() >> new PodOptions() - 1 * handler.getSyntheticPodName(task) >> 'nf-123' - 1 * handler.getLabels(task) >> [foo: 'bar', hello: 'world'] - 1 * handler.getAnnotations() >> [fooz: 'barz', ciao: 'mondo'] - 1 * handler.getContainerMounts() >> [] - 1 * task.getContainer() >> 'debian:latest' - 1 * task.getWorkDir() >> WORK_DIR - 1 * task.getConfig() >> config - 1 * config.getCpus() >> 0 - 1 * config.getMemory() >> null - 1 * client.getConfig() >> new ClientConfig() - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [ - name:'nf-123', - namespace:'default', - labels:[ foo:'bar', hello: 'world'], - annotations:[ fooz:'barz', ciao: 'mondo'] - ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-123', - image:'debian:latest', - command:['/bin/bash', '-ue','/some/work/dir/.command.run'] ] - ] - ] - ] - when: result = handler.newSubmitRequest(task) then: @@ -227,21 +115,12 @@ class K8sTaskHandlerTest extends Specification { 1 * config.getCpus() >> 1 1 * config.getMemory() >> null 1 * client.getConfig() >> new ClientConfig() - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-foo', namespace:'default', labels: [sessionId: 'xxx'], annotations: [evict: 'false']], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-foo', - image:'debian:latest', - command:['/bin/bash', '-ue','/some/work/dir/.command.run'], - resources:[ requests: [cpu:1] ], - env: [ [name:'NXF_OWNER', value:'501:502'] ] - ] - ] - ] - ] + and: + result.metadata.labels == [sessionId: 'xxx'] + result.metadata.annotations == [evict: 'false'] + result.spec.containers[0].command == ['/bin/bash', '-ue', '/some/work/dir/.command.run'] + result.spec.containers[0].resources == [ requests: [cpu:1] ] + result.spec.containers[0].env == [ [name:'NXF_OWNER', value:'501:502'] ] when: result = handler.newSubmitRequest(task) @@ -260,20 +139,11 @@ class K8sTaskHandlerTest extends Specification { 1 * config.getCpus() >> 4 1 * config.getMemory() >> MemoryUnit.of('16GB') 1 * client.getConfig() >> new ClientConfig(namespace: 'namespace-x') - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-abc', namespace:'namespace-x' ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-abc', - image:'user/alpine:1.0', - command:['/bin/bash', '-ue', '/some/work/dir/.command.run'], - resources:[ requests: [cpu:4, memory:'16384Mi'], limits: [memory:'16384Mi'] ] - ] - ] - ] - ] + and: + result.metadata.namespace == 'namespace-x' + result.spec.containers[0].image == 'user/alpine:1.0' + result.spec.containers[0].command == ['/bin/bash', '-ue', '/some/work/dir/.command.run'] + result.spec.containers[0].resources == [ requests: [cpu:4, memory:'16384Mi'], limits: [memory:'16384Mi'] ] } @@ -306,22 +176,8 @@ class K8sTaskHandlerTest extends Specification { 1 * config.getCpus() >> 0 1 * config.getMemory() >> null 1 * client.getConfig() >> new ClientConfig() - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [ - name:'nf-123', - namespace:'default' - ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-123', - image:'debian:latest', - args:['/bin/bash', '-ue','/some/work/dir/.command.run'], - env:[[name:'NXF_DEBUG', value:'true']] ] - ] - ] - ] + and: + result.spec.containers[0].env == [[name:'NXF_DEBUG', value:'true']] cleanup: SysEnv.pop() @@ -355,22 +211,9 @@ class K8sTaskHandlerTest extends Specification { 1 * client.getConfig() >> config 1 * config.getNamespace() >> 'just-a-namespace' 1 * config.getServiceAccount() >> 'pedantic-kallisto' - - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-123', namespace:'just-a-namespace' ], - spec: [ - serviceAccountName: 'pedantic-kallisto', - restartPolicy:'Never', - containers:[ - [name:'nf-123', - image:'debian:latest', - command:['/bin/bash', '-ue','/some/work/dir/.command.run'], - resources:[requests:[cpu:1]] - ] - ] - ] - ] + and: + result.metadata.namespace == 'just-a-namespace' + result.spec.serviceAccountName == 'pedantic-kallisto' } @@ -385,7 +228,6 @@ class K8sTaskHandlerTest extends Specification { def handler = Spy(new K8sTaskHandler(builder:builder, client:client)) def podOptions = Mock(PodOptions) and: - podOptions.automountServiceAccountToken >> true Map result when: @@ -406,30 +248,15 @@ class K8sTaskHandlerTest extends Specification { 2 * podOptions.getEnvVars() >> [ PodEnv.value('FOO','bar') ] 2 * podOptions.getMountSecrets() >> [ new PodMountSecret('my-secret/key-z', '/data/secret.txt') ] 2 * podOptions.getMountConfigMaps() >> [ new PodMountConfig('my-data/key-x', '/etc/file.txt') ] - - result == [ - apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-123', namespace:'default' ], - spec: [ - restartPolicy: 'Never', - containers: [ - [ - name: 'nf-123', - image: 'debian:latest', - command: ['/bin/bash', '-ue','/some/work/dir/.command.run'], - env: [[name:'FOO', value:'bar']], - volumeMounts: [ - [name:'vol-1', mountPath:'/etc'], - [name:'vol-2', mountPath:'/data'] - ] - ] - ], - volumes:[ - [name:'vol-1', configMap:[name:'my-data', items:[[key:'key-x', path:'file.txt']]]], - [name:'vol-2', secret:[secretName:'my-secret', items:[[key:'key-z', path:'secret.txt']]]] - ] - ] + and: + result.spec.containers[0].env == [[name:'FOO', value:'bar']] + result.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/etc'], + [name:'vol-2', mountPath:'/data'] + ] + result.spec.volumes == [ + [name:'vol-1', configMap:[name:'my-data', items:[[key:'key-x', path:'file.txt']]]], + [name:'vol-2', secret:[secretName:'my-secret', items:[[key:'key-z', path:'secret.txt']]]] ] } @@ -443,13 +270,10 @@ class K8sTaskHandlerTest extends Specification { def client = Mock(K8sClient) def builder = Mock(K8sWrapperBuilder) def handler = Spy(new K8sTaskHandler(builder:builder, client:client)) + def podOptions = Mock(PodOptions) and: Map result - def podOptions = Mock(PodOptions) - def CLAIMS = [ new PodVolumeClaim('first','/work'), new PodVolumeClaim('second','/data') ] - podOptions.automountServiceAccountToken >> true - when: result = handler.newSubmitRequest(task) then: @@ -467,32 +291,19 @@ class K8sTaskHandlerTest extends Specification { 1 * config.getCpus() >> 0 1 * config.getMemory() >> null 1 * client.getConfig() >> new ClientConfig() - 2 * podOptions.getVolumeClaims() >> CLAIMS - - result == [ - apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-123', namespace:'default'], - spec: [ - restartPolicy: 'Never', - containers: [ - [ - name: 'nf-123', - image: 'debian:latest', - command: ['/bin/bash', '-ue', '/some/work/dir/.command.run'], - volumeMounts: [ - [name:'vol-1', mountPath:'/work'], - [name:'vol-2', mountPath:'/data'] - ] - ] - ], - volumes: [ - [name:'vol-1', persistentVolumeClaim:[claimName: 'first']], - [name:'vol-2', persistentVolumeClaim:[claimName: 'second']] - ] - ] + 2 * podOptions.getVolumeClaims() >> [ + new PodVolumeClaim('first','/work'), + new PodVolumeClaim('second','/data') + ] + and: + result.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/work'], + [name:'vol-2', mountPath:'/data'] + ] + result.spec.volumes == [ + [name:'vol-1', persistentVolumeClaim:[claimName: 'first']], + [name:'vol-2', persistentVolumeClaim:[claimName: 'second']] ] - when: result = handler.newSubmitRequest(task) @@ -511,29 +322,14 @@ class K8sTaskHandlerTest extends Specification { 1 * config.getCpus() >> 0 1 * config.getMemory() >> null 1 * client.getConfig() >> new ClientConfig() - - result == [ - apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'nf-123', namespace:'default'], - spec: [ - restartPolicy: 'Never', - containers: [ - [ - name: 'nf-123', - image: 'debian:latest', - command: ['/bin/bash', '-ue', '/some/work/dir/.command.run'], - volumeMounts: [ - [name:'vol-3', mountPath:'/tmp'], - [name:'vol-4', mountPath: '/data'] - ] - ] - ], - volumes: [ - [name:'vol-3', hostPath:[path:'/tmp']], - [name:'vol-4', hostPath:[path:'/data']] - ] - ] + and: + result.spec.containers[0].volumeMounts == [ + [name:'vol-3', mountPath:'/tmp'], + [name:'vol-4', mountPath: '/data'] + ] + result.spec.volumes == [ + [name:'vol-3', hostPath:[path:'/tmp']], + [name:'vol-4', hostPath:[path:'/data']] ] } @@ -584,7 +380,6 @@ class K8sTaskHandlerTest extends Specification { def handler = Spy(new K8sTaskHandler(builder: builder, client: client, executor: executor)) def podOptions = Mock(PodOptions) and: - podOptions.automountServiceAccountToken >> true Map result when: @@ -613,6 +408,7 @@ class K8sTaskHandlerTest extends Specification { template: [ metadata: [name: 'nf-123', namespace: 'default'], spec: [ + automountServiceAccountToken: false, restartPolicy: 'Never', containers: [[ name: 'nf-123', @@ -1123,23 +919,10 @@ class K8sTaskHandlerTest extends Specification { 1 * config.getCpus() >> 0 1 * config.getMemory() >> null 1 * client.getConfig() >> new ClientConfig() - result == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [ - name:'nf-123', - namespace:'default' - ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'nf-123', - image:'debian:latest', - args:['/usr/bin/fusion', 'bash', '/fusion/http/work/dir/.command.run'], - securityContext:[privileged:true], - env:[[name:'FUSION_BUCKETS', value:'this,that']]] - ] - ] - ] + and: + result.spec.containers[0].args == ['/usr/bin/fusion', 'bash', '/fusion/http/work/dir/.command.run'] + result.spec.containers[0].securityContext == [privileged:true] + result.spec.containers[0].env == [[name:'FUSION_BUCKETS', value:'this,that']] } def 'get fusion submit command' () { diff --git a/modules/nextflow/src/test/groovy/nextflow/k8s/model/PodSpecBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/k8s/model/PodSpecBuilderTest.groovy index d04cbc1849..e808d0c08e 100644 --- a/modules/nextflow/src/test/groovy/nextflow/k8s/model/PodSpecBuilderTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/k8s/model/PodSpecBuilderTest.groovy @@ -41,19 +41,19 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command:['echo', 'hello'], - workingDir:'/some/work/dir' - ] - ] - ] + spec == [ + apiVersion: 'v1', + kind: 'Pod', + metadata: [name:'foo', namespace:'default'], + spec: [ + restartPolicy:'Never', + containers:[[ + name:'foo', + image:'busybox', + command:['echo', 'hello'], + workingDir:'/some/work/dir' + ]] + ] ] } @@ -61,63 +61,35 @@ class PodSpecBuilderTest extends Specification { def 'should create pod spec with args' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/some/work/dir') .withArgs(['echo', 'hello']) .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - args:['echo', 'hello'], - workingDir:'/some/work/dir' - ] - ] - ] - ] + pod.spec.containers[0].args == ['echo', 'hello'] } def 'should create pod spec with args string' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/some/work/dir') .withArgs('echo foo') .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - args:['/bin/bash', '-c', 'echo foo'], - workingDir:'/some/work/dir' - ] - ] - ] - ] + pod.spec.containers[0].args == ['/bin/bash', '-c', 'echo foo'] } def 'should create pod spec with privileged' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') .withCommand('echo foo') @@ -125,27 +97,14 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command:['/bin/bash', '-c', 'echo foo'], - securityContext: [privileged: true] - ] - ] - ] - ] + pod.spec.containers[0].securityContext == [privileged: true] } def 'should create pod spec with device and capabilities' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') .withCommand('echo foo') @@ -154,31 +113,17 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command:['/bin/bash', '-c', 'echo foo'], - devices: ['/dev/fuse'], - securityContext: [capabilities: [add:['SYS_ADMIN']]] - ] - ] - ] - ] + pod.spec.containers[0].devices == ['/dev/fuse'] + pod.spec.containers[0].securityContext == [capabilities: [add:['SYS_ADMIN']]] } def 'should set namespace, labels and annotations' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/some/work/dir') .withCommand(['sh', '-c', 'echo hello']) .withNamespace('xyz') .withLabel('app','myApp') @@ -189,81 +134,39 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [ - name:'foo', - namespace:'xyz', - labels: [ - app: 'myApp', - runName: 'something', - version: '3.6.1' - ], - annotations: [ - anno1: "value1", - anno2: "value2", - anno3: "value3" - ] - ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command: ['sh', '-c', 'echo hello'], - workingDir:'/some/work/dir' - ] - ] - ] + pod.metadata.namespace == 'xyz' + pod.metadata.labels == [ + app: 'myApp', + runName: 'something', + version: '3.6.1' + ] + pod.metadata.annotations == [ + anno1: "value1", + anno2: "value2", + anno3: "value3" ] } def 'should truncate labels longer than 63 chars' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/some/work/dir') .withCommand(['sh', '-c', 'echo hello']) - .withNamespace('xyz') .withLabel('app','myApp') .withLabel('runName','something') .withLabel('tag','somethingreallylonggggggggggggggggggggggggggggggggggggggggggendEXTRABIT') .withLabels([tag2: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggendEXTRABIT', tag3: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggendEXTRABIT']) - .withAnnotation("anno1", "value1") - .withAnnotations([anno2: "value2", anno3: "value3"]) .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [ - name:'foo', - namespace:'xyz', - labels: [ - app: 'myApp', - runName: 'something', - tag: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend', - tag2: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend', - tag3: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend' - ], - annotations: [ - anno1: "value1", - anno2: "value2", - anno3: "value3" - ] - ], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command: ['sh', '-c', 'echo hello'], - workingDir:'/some/work/dir' - ] - ] - ] + pod.metadata.labels == [ + app: 'myApp', + runName: 'something', + tag: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend', + tag2: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend', + tag3: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend' ] } @@ -271,11 +174,10 @@ class PodSpecBuilderTest extends Specification { def 'should set resources and env' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') .withCommand('echo hello') - .withWorkDir('/some/work/dir') .withEnv(PodEnv.value('ALPHA','hello')) .withEnv(PodEnv.value('DELTA', 'world')) .withCpus(8) @@ -285,64 +187,37 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command:['/bin/bash', '-c', 'echo hello'], - workingDir:'/some/work/dir', - env: [ - [name:'ALPHA', value:'hello'], - [name:'DELTA', value:'world'] - ], - resources:[ - requests: ['foo.org/gpu':5, cpu:8, memory:'100Gi', 'ephemeral-storage':'10Gi'], - limits: ['foo.org/gpu':10, memory:'100Gi', 'ephemeral-storage':'10Gi'] - ] - ] - ] - ] + pod.spec.containers[0].env == [ + [name:'ALPHA', value:'hello'], + [name:'DELTA', value:'world'] + ] + pod.spec.containers[0].resources == [ + requests: ['foo.org/gpu':5, cpu:8, memory:'100Gi', 'ephemeral-storage':'10Gi'], + limits: ['foo.org/gpu':10, memory:'100Gi', 'ephemeral-storage':'10Gi'] ] } def 'should get storage spec for volume claims' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/path') .withCommand(['echo']) .withVolumeClaim(new PodVolumeClaim('first','/work')) .withVolumeClaim(new PodVolumeClaim('second', '/data', '/foo')) .withVolumeClaim(new PodVolumeClaim('third', '/things', null, true)) .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:'/work'], - [name:'vol-2', mountPath:'/data', subPath: '/foo'], - [name:'vol-3', mountPath:'/things', readOnly: true]] ] - ], - volumes:[ - [name:'vol-1', persistentVolumeClaim:[claimName:'first']], - [name:'vol-2', persistentVolumeClaim:[claimName:'second']], - [name:'vol-3', persistentVolumeClaim:[claimName:'third']] ] - ] - + pod.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/work'], + [name:'vol-2', mountPath:'/data', subPath: '/foo'], + [name:'vol-3', mountPath:'/things', readOnly: true] + ] + pod.spec.volumes == [ + [name:'vol-1', persistentVolumeClaim:[claimName:'first']], + [name:'vol-2', persistentVolumeClaim:[claimName:'second']], + [name:'vol-3', persistentVolumeClaim:[claimName:'third']] ] } @@ -350,10 +225,9 @@ class PodSpecBuilderTest extends Specification { def 'should only define one volume per persistentVolumeClaim' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/path') .withCommand(['echo']) .withVolumeClaim(new PodVolumeClaim('first','/work')) .withVolumeClaim(new PodVolumeClaim('first','/work2', '/bar')) @@ -361,27 +235,15 @@ class PodSpecBuilderTest extends Specification { .withVolumeClaim(new PodVolumeClaim('second', '/data2', '/fooz')) .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:'/work'], - [name:'vol-1', mountPath:'/work2', subPath: '/bar'], - [name:'vol-2', mountPath:'/data', subPath: '/foo'], - [name:'vol-2', mountPath:'/data2', subPath: '/fooz']]] - ], - volumes:[ - [name:'vol-1', persistentVolumeClaim:[claimName:'first']], - [name:'vol-2', persistentVolumeClaim:[claimName:'second']] ] - ] - + pod.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/work'], + [name:'vol-1', mountPath:'/work2', subPath: '/bar'], + [name:'vol-2', mountPath:'/data', subPath: '/foo'], + [name:'vol-2', mountPath:'/data2', subPath: '/fooz'] + ] + pod.spec.volumes == [ + [name:'vol-1', persistentVolumeClaim:[claimName:'first']], + [name:'vol-2', persistentVolumeClaim:[claimName:'second']] ] } @@ -389,34 +251,21 @@ class PodSpecBuilderTest extends Specification { def 'should get config map mounts' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/path') .withCommand(['echo']) .withConfigMap(new PodMountConfig(config: 'cfg1', mountPath: '/etc/config')) .withConfigMap(new PodMountConfig(config: 'data2', mountPath: '/data/path')) .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:'/etc/config'], - [name:'vol-2', mountPath:'/data/path']] ] - ], - volumes:[ - [name:'vol-1', configMap:[name:'cfg1']], - [name:'vol-2', configMap:[name:'data2']] ] - ] - + pod.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/etc/config'], + [name:'vol-2', mountPath:'/data/path'] + ] + pod.spec.volumes == [ + [name:'vol-1', configMap:[name:'cfg1']], + [name:'vol-2', configMap:[name:'data2']] ] } @@ -424,76 +273,46 @@ class PodSpecBuilderTest extends Specification { def 'should get csi ephemeral mounts' () { when: - def spec = new PodSpecBuilder() + def pod = 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]] - ] - ] + pod.spec.containers[0].volumeMounts == [ + [name: 'vol-1', mountPath: '/data', readOnly: true] + ] + pod.spec.volumes == [ + [name: 'vol-1', csi: [driver: 'inline.storage.kubernetes.io', readOnly: true]] ] } def 'should get empty dir mounts' () { when: - def spec = new PodSpecBuilder() + def pod = 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']] - ] - ] + pod.spec.containers[0].volumeMounts == [ + [name: 'vol-1', mountPath: '/scratch1'], + [name: 'vol-2', mountPath: '/scratch2'] + ] + pod.spec.volumes == [ + [name: 'vol-1', emptyDir: [medium: 'Disk']], + [name: 'vol-2', emptyDir: [medium: 'Memory']] ] } def 'should consume env secrets' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') .withCommand(['echo']) @@ -503,29 +322,17 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command: ['echo'], - env:[ [name: 'FOO', value: 'abc'], - [name:'VAR_X', valueFrom: [secretKeyRef: [name:'delta', key:'bar']]], - [name:'VAR_Y', valueFrom: [secretKeyRef: [name:'gamma', key:'VAR_Y']]] ] - ] - ] - ] - + pod.spec.containers[0].env == [ + [name: 'FOO', value: 'abc'], + [name: 'VAR_X', valueFrom: [secretKeyRef: [name:'delta', key:'bar']]], + [name: 'VAR_Y', valueFrom: [secretKeyRef: [name:'gamma', key:'VAR_Y']]] ] } def 'should consume env configMap' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') .withCommand(['echo']) @@ -535,29 +342,17 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command: ['echo'], - env:[ [name: 'FOO', value: 'abc'], - [name:'VAR_X', valueFrom: [configMapKeyRef: [name:'data', key:'VAR_X']]], - [name:'VAR_Y', valueFrom: [configMapKeyRef: [name:'omega', key:'bar-2']]] ] - ] - ] - ] - + pod.spec.containers[0].env == [ + [name: 'FOO', value: 'abc'], + [name: 'VAR_X', valueFrom: [configMapKeyRef: [name:'data', key:'VAR_X']]], + [name: 'VAR_Y', valueFrom: [configMapKeyRef: [name:'omega', key:'bar-2']]] ] } def 'should consume file secrets' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') .withCommand(['echo']) @@ -566,67 +361,40 @@ class PodSpecBuilderTest extends Specification { .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - containers:[ - [name:'foo', - image:'busybox', - command: ['echo'], - volumeMounts:[ - [name:'vol-1', mountPath:'/this/and/that'], - [name:'vol-2', mountPath:'/etc/mnt'] - ] - ] - ], - volumes:[ - [name:'vol-1', secret:[secretName: 'alpha']], - [name:'vol-2', secret:[ - secretName: 'delta', - items: [ - [ key: 'foo', path:'bar.txt' ] - ] - ]] - ] - ] - + pod.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/this/and/that'], + [name:'vol-2', mountPath:'/etc/mnt'] + ] + pod.spec.volumes == [ + [name:'vol-1', secret:[secretName: 'alpha']], + [name:'vol-2', secret:[ + secretName: 'delta', + items: [ + [ key: 'foo', path:'bar.txt' ] + ] + ]] ] } def 'should get host path mounts' () { when: - def spec = new PodSpecBuilder() + def pod = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') - .withWorkDir('/path') .withCommand(['echo']) .withHostMount('/tmp','/scratch') .withHostMount('/host/data','/mnt/container') .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:'/scratch'], - [name:'vol-2', mountPath:'/mnt/container']] ] - ], - volumes:[ - [name:'vol-1', hostPath: [path:'/tmp']], - [name:'vol-2', hostPath: [path:'/host/data']] ] - ] - + pod.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/scratch'], + [name:'vol-2', mountPath:'/mnt/container'] + ] + pod.spec.volumes == [ + [name:'vol-1', hostPath: [path:'/tmp']], + [name:'vol-2', hostPath: [path:'/host/data']] ] } @@ -711,18 +479,9 @@ class PodSpecBuilderTest extends Specification { } - def 'should create pod spec given pod options' () { + def 'should create pod spec with pod options' () { given: - def opts = Mock(PodOptions) - def builder = new PodSpecBuilder([ - podName: 'foo', - imageName: 'image', - command: ['echo'], - labels: [runName: 'crazy_john'], - annotations: [evict: 'false'] - ]) - def affinity = [ nodeAffinity: [ requiredDuringSchedulingIgnoredDuringExecution: [ @@ -732,15 +491,22 @@ class PodSpecBuilderTest extends Specification { ] ] ] - def tolerations = [[ key: 'example-key', operator: 'Exists', effect: 'NoSchedule' ]] + def opts = Mock(PodOptions) + and: + def builder = new PodSpecBuilder() + .withPodName('foo') + .withImageName('busybox') + .withCommand(['echo']) + .withLabel('runName', 'crazy_john') + .withAnnotation('evict', 'false') when: - def spec = builder.withPodOptions(opts).build() + def pod = builder.withPodOptions(opts).build() then: _ * opts.getAffinity() >> affinity _ * opts.getAnnotations() >> [OMEGA:'zzz', SIGMA:'www'] @@ -756,48 +522,51 @@ class PodSpecBuilderTest extends Specification { _ * opts.getPriorityClassName() >> 'high-priority' _ * opts.getSecurityContext() >> new PodSecurityContext(1000) _ * opts.getTolerations() >> tolerations - - spec == [ - apiVersion: 'v1', - kind: 'Pod', - metadata: [ + and: + pod.metadata == [ name:'foo', namespace:'default', labels:[runName:'crazy_john', ALPHA:'xxx', GAMMA:'yyy'], annotations: [evict: 'false', OMEGA:'zzz', SIGMA:'www'] - ], - spec: [ - affinity: affinity, - automountServiceAccountToken: false, - imagePullSecrets: [[ name: 'myPullSecret' ]], - nodeSelector: [gpu: 'true', queue: 'fast'], - priorityClassName: 'high-priority', - restartPolicy:'Never', - securityContext: [ runAsUser: 1000 ], - tolerations: tolerations, - - containers:[[ - name:'foo', - image:'image', - imagePullPolicy: 'always', - command:['echo'], - env:[[name:'HELLO', value:'WORLD']], - volumeMounts:[ - [name:'vol-1', mountPath:'/work'], - [name:'vol-2', mountPath:'/home/user'], - [name:'vol-3', mountPath:'/etc/secret.txt'] - ], - ]], - volumes:[ - [name:'vol-1', persistentVolumeClaim:[claimName:'pvc1']], - [name:'vol-2', configMap:[name:'data']], - [name:'vol-3', secret:[secretName:'blah']] - ] - ] + ] + and: + pod.spec.affinity == affinity + pod.spec.automountServiceAccountToken == false + pod.spec.imagePullSecrets == [[ name: 'myPullSecret' ]] + pod.spec.nodeSelector == [gpu: 'true', queue: 'fast'] + pod.spec.priorityClassName == 'high-priority' + pod.spec.securityContext == [ runAsUser: 1000 ] + pod.spec.tolerations == tolerations + pod.spec.containers[0].imagePullPolicy == 'always' + pod.spec.containers[0].env == [[name:'HELLO', value:'WORLD']] + pod.spec.containers[0].volumeMounts == [ + [name:'vol-1', mountPath:'/work'], + [name:'vol-2', mountPath:'/home/user'], + [name:'vol-3', mountPath:'/etc/secret.txt'] + ] + and: + pod.spec.volumes == [ + [name:'vol-1', persistentVolumeClaim:[claimName:'pvc1']], + [name:'vol-2', configMap:[name:'data']], + [name:'vol-3', secret:[secretName:'blah']] ] } + def 'should create pod spec with activeDeadlineSeconds' () { + + when: + def pod = new PodSpecBuilder() + .withPodName('foo') + .withImageName('busybox') + .withCommand(['echo', 'hello']) + .withActiveDeadline(100) + .build() + + then: + pod.spec.activeDeadlineSeconds == 100 + + } def 'should create image pull request map' () { given: @@ -1022,18 +791,18 @@ class PodSpecBuilderTest extends Specification { ] } - def 'should set labels and annotations for job' () { + def 'should create job spec with labels and annotations' () { when: - def spec = new PodSpecBuilder() + def job = new PodSpecBuilder() .withPodName('foo') .withImageName('busybox') .withCommand(['echo', 'hello']) .withLabel('app','someApp') .withLabel('runName','someName') .withLabel('version','3.8.1') - .withAnnotation("anno1", "val1") - .withAnnotations([anno2: "val2", anno3: "val3"]) + .withAnnotation('anno1', 'val1') + .withAnnotations([anno2: 'val2', anno3: 'val3']) .buildAsJob() def metadata = [ @@ -1045,62 +814,15 @@ class PodSpecBuilderTest extends Specification { version: '3.8.1' ], annotations: [ - anno1: "val1", - anno2: "val2", - anno3: "val3" - ] - ] - - then: - spec == [ - apiVersion: 'batch/v1', - kind: 'Job', - metadata: metadata, - spec: [ - backoffLimit: 0, - template: [ - metadata: metadata, - spec: [ - restartPolicy: 'Never', - containers: [[ - name: 'foo', - image: 'busybox', - command: ['echo', 'hello'], - ]] - ] - ] + anno1: 'val1', + anno2: 'val2', + anno3: 'val3' ] ] - } - - def 'should create pod spec with activeDeadlineSeconds' () { - - when: - def spec = new PodSpecBuilder() - .withPodName('foo') - .withImageName('busybox') - .withWorkDir('/some/work/dir') - .withCommand(['echo', 'hello']) - .withActiveDeadline(100) - .build() then: - spec == [ apiVersion: 'v1', - kind: 'Pod', - metadata: [name:'foo', namespace:'default'], - spec: [ - restartPolicy:'Never', - activeDeadlineSeconds: 100, - containers:[ - [name:'foo', - image:'busybox', - command:['echo', 'hello'], - workingDir:'/some/work/dir' - ] - ] - ] - ] - + job.metadata == metadata + job.spec.template.metadata == metadata } }