move to standalone plugin
This commit is contained in:
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin
|
||||
|
||||
import nextflow.BuildInfo
|
||||
import nextflow.SysEnv
|
||||
import recreationaltech.plugin.client.ClientConfig
|
||||
import recreationaltech.plugin.model.PodEnv
|
||||
import recreationaltech.plugin.model.PodSecurityContext
|
||||
import recreationaltech.plugin.model.PodVolumeClaim
|
||||
import nextflow.util.Duration
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class K8sConfigTest extends Specification {
|
||||
|
||||
def 'should create config object' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getNamespace() == null
|
||||
cfg.getServiceAccount() == null
|
||||
!cfg.getDebug().getYaml()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( namespace:'foo', serviceAccount: 'bar', debug: [yaml: true] )
|
||||
then:
|
||||
cfg.getNamespace() == 'foo'
|
||||
cfg.getServiceAccount() == 'bar'
|
||||
cfg.getDebug().getYaml()
|
||||
cfg.debug.yaml
|
||||
|
||||
}
|
||||
|
||||
def 'should set cleanup' () {
|
||||
given:
|
||||
K8sConfig cfg
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig()
|
||||
then: 'it should return true when missing value'
|
||||
cfg.getCleanup()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig()
|
||||
then: 'it should return false specified as default'
|
||||
!cfg.getCleanup(false)
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(cleanup:false)
|
||||
then: 'it should return false'
|
||||
!cfg.getCleanup()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(cleanup:true)
|
||||
then: 'it should return true'
|
||||
cfg.getCleanup()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(cleanup:true)
|
||||
then: 'the default value should be ignored'
|
||||
cfg.getCleanup(false)
|
||||
}
|
||||
|
||||
def 'should create config with storage claims' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig(storageClaimName: 'pvc-1')
|
||||
then:
|
||||
cfg.getStorageClaimName() == 'pvc-1'
|
||||
cfg.getStorageMountPath() == '/workspace'
|
||||
cfg.getPodOptions().getVolumeClaims() == [ new PodVolumeClaim('pvc-1', '/workspace') ] as Set
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig([
|
||||
storageClaimName: 'pvc-2',
|
||||
storageMountPath: '/data',
|
||||
pod: [ [volumeClaim:'foo', mountPath: '/here'],
|
||||
[volumeClaim: 'bar', mountPath: '/there']] ])
|
||||
then:
|
||||
cfg.getStorageClaimName() == 'pvc-2'
|
||||
cfg.getStorageMountPath() == '/data'
|
||||
cfg.getPodOptions().getVolumeClaims() == [
|
||||
new PodVolumeClaim('pvc-2', '/data'),
|
||||
new PodVolumeClaim('foo', '/here'),
|
||||
new PodVolumeClaim('bar', '/there')
|
||||
] as Set
|
||||
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(storageClaimName: 'pvc-3', storageMountPath: '/some/path', storageSubPath: '/bar')
|
||||
then:
|
||||
cfg.getStorageClaimName() == 'pvc-3'
|
||||
cfg.getStorageMountPath() == '/some/path'
|
||||
cfg.getStorageSubPath() == '/bar'
|
||||
cfg.getPodOptions().getVolumeClaims() == [ new PodVolumeClaim('pvc-3', '/some/path', '/bar') ] as Set
|
||||
|
||||
}
|
||||
|
||||
def 'should set device plugin' () {
|
||||
when:
|
||||
def cfg = new K8sConfig([:])
|
||||
then:
|
||||
cfg.fuseDevicePlugin() == ['nextflow.io/fuse':1]
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig([fuseDevicePlugin:['foo/fuse':10]])
|
||||
then:
|
||||
cfg.fuseDevicePlugin() == ['foo/fuse':10]
|
||||
}
|
||||
|
||||
def 'should create client config' () {
|
||||
|
||||
given:
|
||||
def CONFIG = [namespace: 'this', serviceAccount: 'that', client: [server: 'http://foo']]
|
||||
|
||||
when:
|
||||
def config = new K8sConfig(CONFIG)
|
||||
def client = config.getClient()
|
||||
then:
|
||||
client.server == 'http://foo'
|
||||
client.namespace == 'this'
|
||||
client.serviceAccount == 'that'
|
||||
client.httpConnectTimeout == null // testing default null
|
||||
client.httpReadTimeout == null // testing default null
|
||||
client.retryConfig.maxAttempts == 4
|
||||
|
||||
}
|
||||
|
||||
def 'should create client config with http request timeouts' () {
|
||||
|
||||
given:
|
||||
def CONFIG = [
|
||||
namespace: 'this',
|
||||
serviceAccount: 'that',
|
||||
client: [server: 'http://foo'],
|
||||
httpReadTimeout: '20s',
|
||||
httpConnectTimeout: '25s' ]
|
||||
|
||||
when:
|
||||
def config = new K8sConfig(CONFIG)
|
||||
def client = config.getClient()
|
||||
then:
|
||||
client.server == 'http://foo'
|
||||
client.namespace == 'this'
|
||||
client.serviceAccount == 'that'
|
||||
client.httpConnectTimeout == Duration.of('25s')
|
||||
client.httpReadTimeout == Duration.of('20s')
|
||||
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should create client config with discovery' () {
|
||||
|
||||
given:
|
||||
def CONFIG = [context: CONTEXT, namespace: NAMESPACE, serviceAccount: SERVICE_ACCOUNT]
|
||||
K8sConfig config = Spy(K8sConfig, constructorArgs: [ CONFIG ])
|
||||
|
||||
when:
|
||||
def client = config.getClient()
|
||||
then:
|
||||
1 * config.clientDiscovery(CONTEXT, NAMESPACE, SERVICE_ACCOUNT) >> new ClientConfig(namespace: NAMESPACE, server: SERVER)
|
||||
and:
|
||||
client.server == SERVER
|
||||
client.namespace == NAMESPACE ?: 'default'
|
||||
client.serviceAccount == SERVICE_ACCOUNT ?: 'default'
|
||||
|
||||
where:
|
||||
CONTEXT | SERVER | NAMESPACE | SERVICE_ACCOUNT
|
||||
'foo' | 'host.com'| null | null
|
||||
'bar' | 'this.com'| 'ns1' | 'sa2'
|
||||
|
||||
}
|
||||
|
||||
def 'should get nextflow image name' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getNextflowImageName() == "nextflow/nextflow:${BuildInfo.version}"
|
||||
|
||||
}
|
||||
|
||||
def 'should get autoMountHostPaths' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
!cfg.getAutoMountHostPaths()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(autoMountHostPaths: true)
|
||||
then:
|
||||
cfg.getAutoMountHostPaths()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(autoMountHostPaths: false)
|
||||
then:
|
||||
!cfg.getAutoMountHostPaths()
|
||||
}
|
||||
|
||||
|
||||
def 'should get podOptions' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
def opts = cfg.getPodOptions()
|
||||
then:
|
||||
opts.envVars == [] as Set
|
||||
opts.mountSecrets == [] as Set
|
||||
opts.mountConfigMaps == [] as Set
|
||||
opts.volumeClaims == [] as Set
|
||||
|
||||
|
||||
when:
|
||||
opts = new K8sConfig(pod: [ [pullPolicy: 'Always'], [env: 'HELLO', value: 'WORLD'] ]).getPodOptions()
|
||||
then:
|
||||
opts.getImagePullPolicy() == 'Always'
|
||||
opts.getEnvVars() == [ PodEnv.value('HELLO','WORLD') ] as Set
|
||||
}
|
||||
|
||||
def 'should return user name' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getUserName() == System.properties.get('user.name')
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(userName: 'foo')
|
||||
then:
|
||||
cfg.getUserName() == 'foo'
|
||||
}
|
||||
|
||||
def 'should return user dir' () {
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getLaunchDir() == '/workspace/' + System.properties.get('user.name')
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(storageMountPath: '/this/path', userName: 'foo')
|
||||
then:
|
||||
cfg.getLaunchDir() == '/this/path/foo'
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(storageMountPath: '/this/path', userName: 'foo', launchDir: '/my/path')
|
||||
then:
|
||||
cfg.getLaunchDir() == '/my/path'
|
||||
|
||||
}
|
||||
|
||||
def 'should return work dir' () {
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getWorkDir() == "/workspace/${System.properties.get('user.name')}/work"
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(launchDir: '/my/dir')
|
||||
then:
|
||||
cfg.getWorkDir() == "/my/dir/work"
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(launchDir: '/my/dir', workDir: '/the/wor/dir')
|
||||
then:
|
||||
cfg.getWorkDir() == "/the/wor/dir"
|
||||
}
|
||||
|
||||
def 'should return project dir' () {
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getProjectDir() == '/workspace/projects'
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(storageMountPath: '/foo')
|
||||
then:
|
||||
cfg.getProjectDir() == '/foo/projects'
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(storageMountPath: '/foo', projectDir: '/my/project/dir')
|
||||
then:
|
||||
cfg.getProjectDir() == '/my/project/dir'
|
||||
}
|
||||
|
||||
def 'should return storage dir' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getStorageMountPath() == '/workspace'
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(storageMountPath: '/mnt/there')
|
||||
then:
|
||||
cfg.getStorageMountPath() == '/mnt/there'
|
||||
|
||||
}
|
||||
|
||||
def 'should return compute resource type' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
!cfg.useJobResource()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(computeResourceType: 'Job')
|
||||
then:
|
||||
cfg.useJobResource()
|
||||
|
||||
}
|
||||
|
||||
def 'should return storage claim name' () {
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.getStorageClaimName() == null
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(storageClaimName: 'xxx')
|
||||
then:
|
||||
cfg.getStorageClaimName() == 'xxx'
|
||||
}
|
||||
|
||||
def 'should create k8s config with one volume claim' () {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig( pod: [runAsUser: 1000] )
|
||||
then:
|
||||
cfg.getPodOptions().getSecurityContext() == new PodSecurityContext(1000)
|
||||
cfg.getPodOptions().getVolumeClaims().size() == 0
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( pod: [volumeClaim: 'nf-0001', mountPath: '/workspace'] )
|
||||
then:
|
||||
cfg.getPodOptions().getSecurityContext() == null
|
||||
cfg.getPodOptions().getVolumeClaims() == [new PodVolumeClaim('nf-0001', '/workspace')] as Set
|
||||
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( pod: [
|
||||
[runAsUser: 1000],
|
||||
[volumeClaim: 'nf-0001', mountPath: '/workspace'],
|
||||
[volumeClaim: 'nf-0002', mountPath: '/data', subPath: '/home']
|
||||
])
|
||||
then:
|
||||
cfg.getPodOptions().getSecurityContext() == new PodSecurityContext(1000)
|
||||
cfg.getPodOptions().getVolumeClaims() == [
|
||||
new PodVolumeClaim('nf-0001', '/workspace'),
|
||||
new PodVolumeClaim('nf-0002', '/data', '/home')
|
||||
] as Set
|
||||
|
||||
}
|
||||
|
||||
|
||||
def 'should set the sec context'( ) {
|
||||
|
||||
given:
|
||||
def ctx = [runAsUser: 500, fsGroup: 200, allowPrivilegeEscalation: true, seLinuxOptions: [level: "s0:c123,c456"]]
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig( runAsUser: 500 )
|
||||
then:
|
||||
cfg.getPodOptions().getSecurityContext() == new PodSecurityContext(500)
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( securityContext: ctx )
|
||||
then:
|
||||
cfg.getPodOptions().getSecurityContext() == new PodSecurityContext(ctx)
|
||||
|
||||
}
|
||||
|
||||
def 'should set env and sec context' () {
|
||||
given:
|
||||
def ctx = [
|
||||
[env: 'FUSION_BUCKETS', value: 's3://nextflow-ci'],
|
||||
[securityContext: [privileged: true]]]
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig( pod: ctx )
|
||||
then:
|
||||
cfg.getPodOptions().getEnvVars().first() == PodEnv.value('FUSION_BUCKETS', 's3://nextflow-ci')
|
||||
cfg.getPodOptions().getSecurityContext().toSpec() == [privileged:true]
|
||||
|
||||
}
|
||||
|
||||
def 'should set the image pull policy' () {
|
||||
when:
|
||||
def cfg = new K8sConfig( pullPolicy: 'always' )
|
||||
then:
|
||||
cfg.getPodOptions().getImagePullPolicy() == 'always'
|
||||
}
|
||||
|
||||
def 'should set preserve entrypoint setting'( ) {
|
||||
|
||||
when:
|
||||
def cfg = new K8sConfig([:])
|
||||
then:
|
||||
!cfg.entrypointOverride()
|
||||
|
||||
when:
|
||||
SysEnv.push(NXF_CONTAINER_ENTRYPOINT_OVERRIDE: 'true')
|
||||
cfg = new K8sConfig()
|
||||
def result = cfg.entrypointOverride()
|
||||
SysEnv.pop()
|
||||
then:
|
||||
result
|
||||
|
||||
}
|
||||
|
||||
def 'should set debug.yaml' () {
|
||||
when:
|
||||
def cfg = new K8sConfig( debug: [yaml: true] )
|
||||
then:
|
||||
cfg.getDebug().getYaml()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( debug: [yaml: false] )
|
||||
then:
|
||||
!cfg.getDebug().getYaml()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( debug: null )
|
||||
then:
|
||||
!cfg.getDebug().getYaml()
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( debug: [:] )
|
||||
then:
|
||||
!cfg.getDebug().getYaml()
|
||||
}
|
||||
|
||||
def 'should set fetchNodeName' () {
|
||||
when:
|
||||
def cfg = new K8sConfig( fetchNodeName: true )
|
||||
then:
|
||||
cfg.fetchNodeName() == true
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig( fetchNodeName: false )
|
||||
then:
|
||||
cfg.fetchNodeName() == false
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.fetchNodeName() == false
|
||||
}
|
||||
|
||||
def 'should set clientRefreshInterval' () {
|
||||
when:
|
||||
def cfg = new K8sConfig()
|
||||
then:
|
||||
cfg.clientRefreshInterval == Duration.of('50m')
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(clientRefreshInterval: '30m')
|
||||
then:
|
||||
cfg.clientRefreshInterval == Duration.of('30m')
|
||||
|
||||
when:
|
||||
cfg = new K8sConfig(clientRefreshInterval: '1h')
|
||||
then:
|
||||
cfg.clientRefreshInterval == Duration.of('1h')
|
||||
}
|
||||
|
||||
def 'should have nodeInit image' () {
|
||||
when:
|
||||
def cfg = new K8sConfig(
|
||||
nodeInit: [
|
||||
image: 'some-image:0'
|
||||
]
|
||||
)
|
||||
then:
|
||||
cfg.nodeInit.image == 'some-image:0'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
import nextflow.cli.CliOptions
|
||||
import nextflow.cli.CmdKubeRun
|
||||
import nextflow.cli.Launcher
|
||||
import recreationaltech.plugin.client.ClientConfig
|
||||
import recreationaltech.plugin.client.K8sClient
|
||||
import recreationaltech.plugin.model.PodMountConfig
|
||||
import recreationaltech.plugin.model.PodOptions
|
||||
import recreationaltech.plugin.model.PodSpecBuilder
|
||||
import recreationaltech.plugin.model.PodVolumeClaim
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class K8sDriverLauncherTest extends Specification {
|
||||
|
||||
def setup() {
|
||||
PodSpecBuilder.VOLUMES.set(0)
|
||||
}
|
||||
|
||||
def 'should execute run' () {
|
||||
given:
|
||||
def NAME = 'nxf-foo'
|
||||
def NF_CONFIG = [process:[executor:'k8s']]
|
||||
def K8S_CONFIG = Mock(K8sConfig)
|
||||
def K8S_CLIENT = Mock(K8sClient)
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
|
||||
when:
|
||||
driver.run(NAME, ['a','b','c'])
|
||||
then:
|
||||
1 * driver.makeConfig(NAME) >> NF_CONFIG
|
||||
1 * driver.makeK8sConfig(NF_CONFIG) >> K8S_CONFIG
|
||||
1 * driver.makeK8sClient(K8S_CONFIG) >> K8S_CLIENT
|
||||
1 * K8S_CONFIG.checkStorageAndPaths(K8S_CLIENT)
|
||||
1 * driver.createK8sConfigMap() >> null
|
||||
1 * driver.createK8sLauncherPod() >> null
|
||||
1 * driver.waitPodStart() >> null
|
||||
1 * driver.printK8sPodOutput() >> null
|
||||
|
||||
driver.pipelineName == NAME
|
||||
driver.interactive == false
|
||||
driver.config == NF_CONFIG
|
||||
driver.k8sConfig == K8S_CONFIG
|
||||
driver.k8sClient == K8S_CLIENT
|
||||
}
|
||||
|
||||
def 'should make k8s config' () {
|
||||
|
||||
given:
|
||||
K8sConfig k8sConfig
|
||||
K8sDriverLauncher driver = Spy(K8sDriverLauncher)
|
||||
|
||||
when:
|
||||
k8sConfig = driver.makeK8sConfig([:])
|
||||
then:
|
||||
k8sConfig != null
|
||||
|
||||
when:
|
||||
k8sConfig = driver.makeK8sConfig(k8s: [storageClaimName: 'foo', storageMountPath: '/mnt'])
|
||||
then:
|
||||
k8sConfig.getStorageClaimName() == 'foo'
|
||||
k8sConfig.getStorageMountPath() == '/mnt'
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Unroll
|
||||
def 'should get cmd cli' () {
|
||||
|
||||
given:
|
||||
def l = new K8sDriverLauncher(cmd: cmd, pipelineName: 'foo')
|
||||
|
||||
when:
|
||||
cmd.launcher = new Launcher(options: new CliOptions())
|
||||
then:
|
||||
l.getLaunchCli() == expected
|
||||
|
||||
where:
|
||||
cmd | expected
|
||||
new CmdKubeRun() | 'nextflow run foo'
|
||||
new CmdKubeRun(cacheable: false) | 'nextflow run foo -cache false'
|
||||
new CmdKubeRun(resume: true) | 'nextflow run foo -resume true'
|
||||
new CmdKubeRun(poolSize: 10) | 'nextflow run foo -ps 10'
|
||||
new CmdKubeRun(pollInterval: 5) | 'nextflow run foo -pi 5'
|
||||
new CmdKubeRun(queueSize: 9) | 'nextflow run foo -qs 9'
|
||||
new CmdKubeRun(revision: 'xyz') | 'nextflow run foo -r xyz'
|
||||
new CmdKubeRun(latest: true) | 'nextflow run foo -latest true'
|
||||
new CmdKubeRun(withTrace: true) | 'nextflow run foo -with-trace true'
|
||||
new CmdKubeRun(withTimeline: true) | 'nextflow run foo -with-timeline true'
|
||||
new CmdKubeRun(withDag: true) | 'nextflow run foo -with-dag true'
|
||||
new CmdKubeRun(dumpHashes: true) | 'nextflow run foo -dump-hashes true'
|
||||
new CmdKubeRun(dumpChannels: 'lala') | 'nextflow run foo -dump-channels lala'
|
||||
new CmdKubeRun(env: [XX:'hello', YY: 'world']) | 'nextflow run foo -e.XX hello -e.YY world'
|
||||
new CmdKubeRun(process: [mem: '100',cpus:'2']) | 'nextflow run foo -process.mem 100 -process.cpus 2'
|
||||
new CmdKubeRun(params: [alpha:'x', beta:'y']) | 'nextflow run foo --alpha x --beta y'
|
||||
new CmdKubeRun(params: [alpha: '/path/*.txt']) | 'nextflow run foo --alpha /path/\\*.txt'
|
||||
new CmdKubeRun(entryName: 'lala') | 'nextflow run foo -entry lala'
|
||||
}
|
||||
|
||||
def 'should set the run name' () {
|
||||
given:
|
||||
def cmd = new CmdKubeRun()
|
||||
cmd.launcher = new Launcher(options: new CliOptions())
|
||||
|
||||
when:
|
||||
def l = new K8sDriverLauncher(cmd: cmd, pipelineName: 'foo', runName: 'bar')
|
||||
then:
|
||||
l.getLaunchCli() == 'nextflow run foo -name bar'
|
||||
}
|
||||
|
||||
|
||||
|
||||
def 'should create launcher spec pod' () {
|
||||
|
||||
given:
|
||||
def pod = Mock(PodOptions)
|
||||
pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ]
|
||||
pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ]
|
||||
pod.automountServiceAccountToken = false
|
||||
|
||||
def k8s = Mock(K8sConfig)
|
||||
k8s.getNextflowImageName() >> 'the-image'
|
||||
k8s.getLaunchDir() >> '/the/user/dir'
|
||||
k8s.getWorkDir() >> '/the/work/dir'
|
||||
k8s.getProjectDir() >> '/the/project/dir'
|
||||
k8s.getPodOptions() >> pod
|
||||
|
||||
and:
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@runName = 'foo-boo'
|
||||
driver.@k8sClient = new K8sClient(new ClientConfig(namespace: 'foo', serviceAccount: 'bar'))
|
||||
driver.@k8sConfig = k8s
|
||||
|
||||
when:
|
||||
def spec = 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: 'the-image',
|
||||
command: ['/bin/bash', '-c', "source /etc/nextflow/init.sh; nextflow run foo; if [ -x /etc/nextflow/node-init-cleanup.sh ]; then /etc/nextflow/node-init-cleanup.sh || true; fi; exit \$status"],
|
||||
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',
|
||||
automountServiceAccountToken: false,
|
||||
volumes: [
|
||||
[name:'vol-1', persistentVolumeClaim:[claimName:'pvc-1']],
|
||||
[name:'vol-2', configMap:[name:'cfg-2']]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def 'should create launcher spec job' () {
|
||||
|
||||
given:
|
||||
def pod = Mock(PodOptions)
|
||||
pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ]
|
||||
pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ]
|
||||
|
||||
def k8s = Mock(K8sConfig)
|
||||
k8s.getNextflowImageName() >> 'the-image'
|
||||
k8s.getLaunchDir() >> '/the/user/dir'
|
||||
k8s.getWorkDir() >> '/the/work/dir'
|
||||
k8s.getProjectDir() >> '/the/project/dir'
|
||||
k8s.getPodOptions() >> pod
|
||||
k8s.useJobResource() >> true
|
||||
|
||||
and:
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@runName = 'foo-boo'
|
||||
driver.@k8sClient = new K8sClient(new ClientConfig(namespace: 'foo', serviceAccount: 'bar'))
|
||||
driver.@k8sConfig = k8s
|
||||
|
||||
and:
|
||||
def metadata = [name: 'foo-boo', namespace: 'foo', labels: [app: 'nextflow', runName: 'foo-boo']]
|
||||
|
||||
when:
|
||||
def spec = driver.makeLauncherSpec()
|
||||
then:
|
||||
driver.getLaunchCli() >> 'nextflow run foo'
|
||||
|
||||
spec == [
|
||||
apiVersion: 'batch/v1',
|
||||
kind: 'Job',
|
||||
metadata: metadata,
|
||||
spec: [
|
||||
backoffLimit: 0,
|
||||
template: [
|
||||
metadata: metadata,
|
||||
spec: [
|
||||
restartPolicy: 'Never',
|
||||
containers: [
|
||||
[
|
||||
name: 'foo-boo',
|
||||
image: 'the-image',
|
||||
command: ['/bin/bash', '-c', "source /etc/nextflow/init.sh; nextflow run foo; if [ -x /etc/nextflow/node-init-cleanup.sh ]; then /etc/nextflow/node-init-cleanup.sh || true; fi; exit \$status"],
|
||||
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',
|
||||
automountServiceAccountToken: false,
|
||||
volumes: [
|
||||
[name:'vol-1', persistentVolumeClaim:[claimName:'pvc-1']],
|
||||
[name:'vol-2', configMap:[name:'cfg-2']]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
def 'should use user provided pod image' () {
|
||||
|
||||
given:
|
||||
def pod = Mock(PodOptions)
|
||||
pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ]
|
||||
pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ]
|
||||
|
||||
def k8s = Mock(K8sConfig)
|
||||
k8s.getLaunchDir() >> '/the/user/dir'
|
||||
k8s.getWorkDir() >> '/the/work/dir'
|
||||
k8s.getProjectDir() >> '/the/project/dir'
|
||||
k8s.getPodOptions() >> pod
|
||||
|
||||
and:
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@runName = 'foo-boo'
|
||||
driver.@k8sClient = new K8sClient(new ClientConfig(namespace: 'foo', serviceAccount: 'bar'))
|
||||
driver.@k8sConfig = k8s
|
||||
driver.@headImage = 'foo/bar'
|
||||
|
||||
when:
|
||||
def result = driver.makeLauncherSpec()
|
||||
then:
|
||||
driver.getLaunchCli() >> 'nextflow run foo'
|
||||
and:
|
||||
result.spec.containers[0].image == 'foo/bar'
|
||||
|
||||
}
|
||||
|
||||
def 'should use user provided head-cpu and head-memory request' () {
|
||||
given:
|
||||
def pod = Mock(PodOptions)
|
||||
pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ]
|
||||
pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ]
|
||||
|
||||
def k8s = Mock(K8sConfig)
|
||||
k8s.getNextflowImageName() >> 'the-image'
|
||||
k8s.getLaunchDir() >> '/the/user/dir'
|
||||
k8s.getWorkDir() >> '/the/work/dir'
|
||||
k8s.getProjectDir() >> '/the/project/dir'
|
||||
k8s.getPodOptions() >> pod
|
||||
|
||||
and:
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@runName = 'foo-boo'
|
||||
driver.@k8sClient = new K8sClient(new ClientConfig(namespace: 'foo', serviceAccount: 'bar'))
|
||||
driver.@k8sConfig = k8s
|
||||
driver.@headCpus = 2
|
||||
driver.@headMemory = '200Mi'
|
||||
|
||||
when:
|
||||
def result = driver.makeLauncherSpec()
|
||||
then:
|
||||
driver.getLaunchCli() >> 'nextflow run foo'
|
||||
and:
|
||||
result.spec.containers[0].resources == [
|
||||
requests: [cpu: 2, memory: '200Mi'],
|
||||
limits: [memory: '200Mi']
|
||||
]
|
||||
}
|
||||
|
||||
def 'should use user provided head-cpu and head-memory limits' () {
|
||||
given:
|
||||
def pod = Mock(PodOptions)
|
||||
pod.getVolumeClaims() >> [ new PodVolumeClaim('pvc-1', '/mnt/path/data') ]
|
||||
pod.getMountConfigMaps() >> [ new PodMountConfig('cfg-2', '/mnt/path/cfg') ]
|
||||
|
||||
def k8s = Mock(K8sConfig)
|
||||
k8s.getNextflowImageName() >> 'the-image'
|
||||
k8s.getLaunchDir() >> '/the/user/dir'
|
||||
k8s.getWorkDir() >> '/the/work/dir'
|
||||
k8s.getProjectDir() >> '/the/project/dir'
|
||||
k8s.getPodOptions() >> pod
|
||||
k8s.cpuLimitsEnabled() >> true
|
||||
|
||||
and:
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@runName = 'foo-boo'
|
||||
driver.@k8sClient = new K8sClient(new ClientConfig(namespace: 'foo', serviceAccount: 'bar'))
|
||||
driver.@k8sConfig = k8s
|
||||
driver.@headCpus = 2
|
||||
driver.@headMemory = '200Mi'
|
||||
|
||||
when:
|
||||
def result = driver.makeLauncherSpec()
|
||||
then:
|
||||
driver.getLaunchCli() >> 'nextflow run foo'
|
||||
and:
|
||||
result.spec.containers[0].resources == [
|
||||
requests: [cpu: 2, memory: '200Mi'],
|
||||
limits: [cpu: 2, memory: '200Mi']
|
||||
]
|
||||
}
|
||||
|
||||
def 'should create config map' () {
|
||||
|
||||
given:
|
||||
def folder = Files.createTempDirectory('foo')
|
||||
|
||||
def params = folder.resolve('params.json')
|
||||
params.text = 'bla-bla'
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
def NXF_CONFIG = [foo: 'bar'].toConfigObject()
|
||||
|
||||
def SCM_FILE = folder.resolve('scm')
|
||||
SCM_FILE.text = "hello = 'world'\n"
|
||||
|
||||
|
||||
def EXPECTED = [:]
|
||||
EXPECTED['init.sh'] == ''
|
||||
|
||||
def POD_OPTIONS = new PodOptions()
|
||||
|
||||
def K8S_CONFIG = Mock(K8sConfig)
|
||||
K8S_CONFIG.getLaunchDir() >> '/launch/dir'
|
||||
K8S_CONFIG.getPodOptions() >> POD_OPTIONS
|
||||
|
||||
when:
|
||||
driver.@config = NXF_CONFIG
|
||||
driver.@k8sConfig = K8S_CONFIG
|
||||
driver.@cmd = new CmdKubeRun(paramsFile: params.toString())
|
||||
|
||||
driver.createK8sConfigMap()
|
||||
then:
|
||||
1 * driver.getScmFile() >> SCM_FILE
|
||||
1 * driver.makeConfigMapName(_ as Map) >> 'nf-config-123'
|
||||
1 * driver.tryCreateConfigMap('nf-config-123', _ as Map) >> { name, cfg ->
|
||||
assert cfg.'init.sh' == "mkdir -p '/launch/dir'; if [ -d '/launch/dir' ]; then cd '/launch/dir'; else echo 'Cannot create directory: /launch/dir'; exit 1; fi; [ -f /etc/nextflow/scm ] && ln -s /etc/nextflow/scm \$NXF_HOME/scm; [ -f /etc/nextflow/nextflow.config ] && cp /etc/nextflow/nextflow.config \$PWD/nextflow.config; "
|
||||
assert cfg.'nextflow.config' == "foo = 'bar'\n"
|
||||
assert cfg.'scm' == "hello = 'world'\n"
|
||||
assert cfg.'params.json' == 'bla-bla'
|
||||
return null
|
||||
}
|
||||
|
||||
POD_OPTIONS.getMountConfigMaps() == [ new PodMountConfig('nf-config-123', '/etc/nextflow') ] as Set
|
||||
|
||||
cleanup:
|
||||
folder?.deleteDir()
|
||||
}
|
||||
|
||||
def 'should create config map with pre-script' () {
|
||||
|
||||
given:
|
||||
def folder = Files.createTempDirectory('foo')
|
||||
|
||||
def params = folder.resolve('params.json')
|
||||
params.text = 'bla-bla'
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@headPreScript = '/bin/foo.sh'
|
||||
def NXF_CONFIG = [foo: 'bar'].toConfigObject()
|
||||
|
||||
def SCM_FILE = folder.resolve('scm')
|
||||
SCM_FILE.text = "hello = 'world'\n"
|
||||
|
||||
|
||||
def EXPECTED = [:]
|
||||
EXPECTED['init.sh'] == ''
|
||||
|
||||
def POD_OPTIONS = new PodOptions()
|
||||
|
||||
def K8S_CONFIG = Mock(K8sConfig)
|
||||
K8S_CONFIG.getLaunchDir() >> '/launch/dir'
|
||||
K8S_CONFIG.getPodOptions() >> POD_OPTIONS
|
||||
|
||||
when:
|
||||
driver.@config = NXF_CONFIG
|
||||
driver.@k8sConfig = K8S_CONFIG
|
||||
driver.@cmd = new CmdKubeRun(paramsFile: params.toString())
|
||||
|
||||
driver.createK8sConfigMap()
|
||||
then:
|
||||
1 * driver.getScmFile() >> SCM_FILE
|
||||
1 * driver.makeConfigMapName(_ as Map) >> 'nf-config-123'
|
||||
1 * driver.tryCreateConfigMap('nf-config-123', _ as Map) >> { name, cfg ->
|
||||
assert cfg.'init.sh' == "mkdir -p '/launch/dir'; if [ -d '/launch/dir' ]; then cd '/launch/dir'; else echo 'Cannot create directory: /launch/dir'; exit 1; fi; [ -f /etc/nextflow/scm ] && ln -s /etc/nextflow/scm \$NXF_HOME/scm; [ -f /etc/nextflow/nextflow.config ] && cp /etc/nextflow/nextflow.config \$PWD/nextflow.config; [ -f '/bin/foo.sh' ] && '/bin/foo.sh'; "
|
||||
assert cfg.'nextflow.config' == "foo = 'bar'\n"
|
||||
assert cfg.'scm' == "hello = 'world'\n"
|
||||
assert cfg.'params.json' == 'bla-bla'
|
||||
return null
|
||||
}
|
||||
|
||||
POD_OPTIONS.getMountConfigMaps() == [ new PodMountConfig('nf-config-123', '/etc/nextflow') ] as Set
|
||||
|
||||
cleanup:
|
||||
folder?.deleteDir()
|
||||
}
|
||||
|
||||
|
||||
def 'should make config' () {
|
||||
given:
|
||||
Map config
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
def NAME = 'somePipelineName'
|
||||
def CFG_EMPTY = new ConfigObject()
|
||||
def CFG_WITH_MOUNTS = new ConfigObject()
|
||||
CFG_WITH_MOUNTS.k8s.storageClaimName = 'pvc'
|
||||
CFG_WITH_MOUNTS.k8s.storageMountPath = '/foo'
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun()
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_EMPTY
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.pod == null
|
||||
config.k8s.storageMountPath == null
|
||||
config.k8s.storageClaimName == null
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun()
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_WITH_MOUNTS
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.storageClaimName == 'pvc'
|
||||
config.k8s.storageMountPath == '/foo'
|
||||
and:
|
||||
new K8sConfig(config.k8s).getStorageClaimName() == 'pvc'
|
||||
new K8sConfig(config.k8s).getStorageMountPath() == '/foo'
|
||||
new K8sConfig(config.k8s).getPodOptions() == new PodOptions([ [volumeClaim:'pvc', mountPath: '/foo'] ])
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun(volMounts: ['pvc-1:/this','pvc-2:/that'] )
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_EMPTY
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.storageClaimName == 'pvc-1'
|
||||
config.k8s.storageMountPath == '/this'
|
||||
config.k8s.pod == [ [volumeClaim: 'pvc-2', mountPath: '/that'] ]
|
||||
and:
|
||||
new K8sConfig(config.k8s).getStorageClaimName() == 'pvc-1'
|
||||
new K8sConfig(config.k8s).getStorageMountPath() == '/this'
|
||||
new K8sConfig(config.k8s).getPodOptions() == new PodOptions([
|
||||
[volumeClaim:'pvc-1', mountPath: '/this'],
|
||||
[volumeClaim:'pvc-2', mountPath: '/that']
|
||||
])
|
||||
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun(volMounts: ['xyz:/this'] )
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_WITH_MOUNTS
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.storageClaimName == 'xyz'
|
||||
config.k8s.storageMountPath == '/this'
|
||||
config.k8s.pod == null
|
||||
and:
|
||||
new K8sConfig(config.k8s).getStorageClaimName() == 'xyz'
|
||||
new K8sConfig(config.k8s).getStorageMountPath() == '/this'
|
||||
new K8sConfig(config.k8s).getPodOptions() == new PodOptions([
|
||||
[volumeClaim:'xyz', mountPath: '/this']
|
||||
])
|
||||
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun(volMounts: ['xyz', 'bar:/mnt/bar'] )
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_WITH_MOUNTS
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.storageClaimName == 'xyz'
|
||||
config.k8s.storageMountPath == null
|
||||
config.k8s.pod == [ [volumeClaim: 'bar', mountPath: '/mnt/bar'] ]
|
||||
and:
|
||||
new K8sConfig(config.k8s).getStorageClaimName() == 'xyz'
|
||||
new K8sConfig(config.k8s).getStorageMountPath() == '/workspace'
|
||||
new K8sConfig(config.k8s).getPodOptions() == new PodOptions([
|
||||
[volumeClaim:'xyz', mountPath: '/workspace'],
|
||||
[volumeClaim:'bar', mountPath: '/mnt/bar']
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
def 'should add the plugin into the config' () {
|
||||
given:
|
||||
def cmd = new CmdKubeRun()
|
||||
cmd.launcher = new Launcher(options: new CliOptions())
|
||||
|
||||
when:
|
||||
def l = new K8sDriverLauncher(cmd: cmd, plugins: 'nf-cws@1.0.0', runName: 'bar')
|
||||
then:
|
||||
l.makeConfig( "/bar").get('plugins') == [ 'nf-cws@1.0.0' ]
|
||||
}
|
||||
|
||||
def 'should make config - deprecated' () {
|
||||
|
||||
given:
|
||||
Map config
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
def NAME = 'somePipelineName'
|
||||
def CFG_EMPTY = new ConfigObject()
|
||||
def CFG_WITH_MOUNTS = new ConfigObject()
|
||||
CFG_WITH_MOUNTS.k8s.volumeClaims = [ pvc: [mountPath:'/foo'] ]
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun()
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_EMPTY
|
||||
config.process.executor == 'k8s'
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun()
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_WITH_MOUNTS
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.storageClaimName == 'pvc'
|
||||
config.k8s.storageMountPath == '/foo'
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun(volMounts: ['pvc-1:/this','pvc-2:/that'] )
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_EMPTY
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.storageClaimName == 'pvc-1'
|
||||
config.k8s.storageMountPath == '/this'
|
||||
config.k8s.pod == [ [volumeClaim: 'pvc-2', mountPath: '/that'] ]
|
||||
|
||||
|
||||
when:
|
||||
driver.@cmd = new CmdKubeRun(volMounts: ['xyz:/this'] )
|
||||
config = driver.makeConfig(NAME).toMap()
|
||||
then:
|
||||
1 * driver.loadConfig(NAME) >> CFG_WITH_MOUNTS
|
||||
config.process.executor == 'k8s'
|
||||
config.k8s.storageClaimName == 'xyz'
|
||||
config.k8s.storageMountPath == '/this'
|
||||
config.k8s.pod == null
|
||||
and:
|
||||
new K8sConfig(config.k8s).getStorageClaimName() == 'xyz'
|
||||
new K8sConfig(config.k8s).getStorageMountPath() == '/this'
|
||||
new K8sConfig(config.k8s).getPodOptions() == new PodOptions([
|
||||
[volumeClaim:'xyz', mountPath: '/this']
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
def 'should return pod exit status' () {
|
||||
given:
|
||||
def POD_NAME = 'pod-x'
|
||||
def client = Mock(K8sClient)
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@k8sClient = client
|
||||
driver.@runName = POD_NAME
|
||||
driver.@k8sConfig = Mock(K8sConfig)
|
||||
|
||||
when:
|
||||
def status = driver.waitPodTermination()
|
||||
then:
|
||||
1 * client.podState(POD_NAME) >> [terminated: [exitCode: 99]]
|
||||
1 * driver.k8sConfig.useJobResource() >> [:]
|
||||
then:
|
||||
status == 99
|
||||
|
||||
when:
|
||||
status = driver.waitPodTermination()
|
||||
then:
|
||||
1 * client.podState(POD_NAME) >> [:]
|
||||
then:
|
||||
1 * client.podState(POD_NAME) >> [terminated: [exitCode: 99]]
|
||||
then:
|
||||
status == 99
|
||||
|
||||
when:
|
||||
status = driver.waitPodTermination()
|
||||
then:
|
||||
1 * client.podState(POD_NAME) >> [:]
|
||||
1 * driver.isWaitTimedOut(_) >> true
|
||||
then:
|
||||
status == 127
|
||||
}
|
||||
|
||||
|
||||
def 'should delete configMap' () {
|
||||
given:
|
||||
def POD_NAME = 'pod-x'
|
||||
def config = Mock(K8sConfig)
|
||||
def driver = Spy(K8sDriverLauncher)
|
||||
driver.@k8sConfig = config
|
||||
driver.@runName = POD_NAME
|
||||
driver.@initDeployer = new K8sNodeInitDeployer(driver.k8sClient, config)
|
||||
|
||||
when:
|
||||
driver.shutdown()
|
||||
then:
|
||||
1 * driver.waitPodTermination() >> 0
|
||||
then:
|
||||
1 * config.getCleanup(true) >> true
|
||||
1 * driver.deleteConfigMap() >> null
|
||||
|
||||
when:
|
||||
driver.shutdown()
|
||||
then:
|
||||
1 * driver.waitPodTermination() >> 1
|
||||
then:
|
||||
1 * config.getCleanup(false) >> true
|
||||
1 * driver.deleteConfigMap() >> null
|
||||
|
||||
when:
|
||||
driver.shutdown()
|
||||
then:
|
||||
1 * driver.waitPodTermination() >> 1
|
||||
then:
|
||||
1 * config.getCleanup(false) >> false
|
||||
0 * driver.deleteConfigMap() >> null
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package recreationaltech.plugin
|
||||
|
||||
import nextflow.Session
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
* Implements a basic factory test
|
||||
*
|
||||
*/
|
||||
class K8sDvfsObserverTest extends Specification {
|
||||
|
||||
def 'should create the observer instance' () {
|
||||
given:
|
||||
def factory = new K8sDvfsFactory()
|
||||
when:
|
||||
def result = factory.create(Mock(Session))
|
||||
then:
|
||||
result.size() == 1
|
||||
result.first() instanceof K8sDvfsObserver
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import recreationaltech.plugin.client.ClientConfig
|
||||
import recreationaltech.plugin.client.K8sClient
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class K8sExecutorTest extends Specification {
|
||||
|
||||
def 'should cache k8s client and refresh after expiration' () {
|
||||
given:
|
||||
def CONFIG = new K8sConfig(
|
||||
client: [server: 'http://k8s-server'],
|
||||
namespace: 'test-ns',
|
||||
serviceAccount: 'test-sa',
|
||||
clientRefreshInterval: '100ms'
|
||||
)
|
||||
and:
|
||||
def executor = Spy(K8sExecutor)
|
||||
executor.getK8sConfig() >> CONFIG
|
||||
// use a short-lived cache for the test
|
||||
executor.@clientCache = CacheBuilder.newBuilder()
|
||||
.expireAfterWrite(100, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
when: 'first call to getClient'
|
||||
def client1 = executor.getClient()
|
||||
then: 'a new K8sClient is created'
|
||||
client1 instanceof K8sClient
|
||||
client1.config.server == 'http://k8s-server'
|
||||
|
||||
when: 'second call within cache interval'
|
||||
def client2 = executor.getClient()
|
||||
then: 'returns the same cached instance'
|
||||
client2.is(client1)
|
||||
|
||||
when: 'call after cache expiration'
|
||||
sleep(150)
|
||||
def client3 = executor.getClient()
|
||||
then: 'a new K8sClient instance is created'
|
||||
client3 instanceof K8sClient
|
||||
!client3.is(client1)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package recreationaltech.plugin
|
||||
|
||||
import recreationaltech.plugin.client.K8sClient
|
||||
import recreationaltech.plugin.model.PodSpecBuilder
|
||||
import spock.lang.Specification
|
||||
|
||||
class K8sNodeInitDeployerTest extends Specification {
|
||||
|
||||
def setup() {
|
||||
PodSpecBuilder.VOLUMES.set(0)
|
||||
}
|
||||
|
||||
def 'should not deploy pods when node init is disabled' () {
|
||||
given:
|
||||
def client = Mock(K8sClient)
|
||||
def config = new K8sConfig(nodeInit: [enabled: false])
|
||||
def deployer = new K8sNodeInitDeployer(client, config)
|
||||
|
||||
when:
|
||||
deployer.deploy()
|
||||
|
||||
then:
|
||||
0 * client.nodeList()
|
||||
0 * client.podCreate(_)
|
||||
}
|
||||
|
||||
def 'should deploy one init pod for each node' () {
|
||||
given:
|
||||
def client = Mock(K8sClient)
|
||||
def config = new K8sConfig(nodeInit: [
|
||||
enabled: true,
|
||||
image: 'ubuntu:latest',
|
||||
command: ['/bin/bash', '-c', 'echo init']
|
||||
])
|
||||
def deployer = new K8sNodeInitDeployer(client, config)
|
||||
|
||||
when:
|
||||
deployer.deploy()
|
||||
|
||||
then:
|
||||
1 * client.nodeList() >> [
|
||||
items: [
|
||||
[metadata: [name: 'node-a']],
|
||||
[metadata: [name: 'node-b']]
|
||||
]
|
||||
]
|
||||
|
||||
then:
|
||||
1 * client.podCreate({ Map spec ->
|
||||
spec.kind == 'Pod'
|
||||
spec.metadata.name == 'nf-init-node-a'
|
||||
spec.metadata.namespace == 'default'
|
||||
spec.spec.nodeName == 'node-a'
|
||||
spec.spec.restartPolicy == 'Never'
|
||||
|
||||
def container = spec.spec.containers[0]
|
||||
container.name == 'nf-init-node-a'
|
||||
container.image == 'ubuntu:latest'
|
||||
container.command == ['/bin/bash', '-c', 'echo init']
|
||||
container.securityContext.privileged == true
|
||||
|
||||
spec.spec.volumes*.hostPath*.path as Set == ['/sys', '/dev', '/lib/modules'] as Set
|
||||
container.volumeMounts*.mountPath as Set == ['/sys', '/dev', '/lib/modules'] as Set
|
||||
})
|
||||
|
||||
then:
|
||||
1 * client.podCreate({ Map spec ->
|
||||
spec.kind == 'Pod'
|
||||
spec.metadata.name == 'nf-init-node-b'
|
||||
spec.spec.nodeName == 'node-b'
|
||||
|
||||
def container = spec.spec.containers[0]
|
||||
container.name == 'nf-init-node-b'
|
||||
container.image == 'ubuntu:latest'
|
||||
container.command == ['/bin/bash', '-c', 'echo init']
|
||||
container.securityContext.privileged == true
|
||||
})
|
||||
|
||||
0 * client._
|
||||
}
|
||||
|
||||
def 'should lowercase and truncate generated pod names' () {
|
||||
given:
|
||||
def client = Mock(K8sClient)
|
||||
def config = new K8sConfig(nodeInit: [
|
||||
enabled: true,
|
||||
image: 'ubuntu:latest',
|
||||
command: ['true']
|
||||
])
|
||||
def deployer = new K8sNodeInitDeployer(client, config)
|
||||
|
||||
when:
|
||||
deployer.deploy()
|
||||
|
||||
then:
|
||||
1 * client.nodeList() >> [
|
||||
items: [
|
||||
[metadata: [name: 'NODE-WITH-A-VERY-LONG-NAME-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123']]
|
||||
]
|
||||
]
|
||||
|
||||
then:
|
||||
1 * client.podCreate({ Map spec ->
|
||||
spec.metadata.name == 'nf-init-node-with-a-very-long-name-abcdefghijklmnopqrstuvwxyz-0'
|
||||
spec.metadata.name.size() == 63
|
||||
spec.metadata.name == spec.metadata.name.toLowerCase()
|
||||
spec.spec.nodeName == 'NODE-WITH-A-VERY-LONG-NAME-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123'
|
||||
})
|
||||
|
||||
then:
|
||||
0 * client._
|
||||
}
|
||||
|
||||
def 'should not cleanup pods when node init is disabled' () {
|
||||
given:
|
||||
def client = Mock(K8sClient)
|
||||
def config = new K8sConfig(nodeInit: [enabled: false, cleanup: true])
|
||||
def deployer = new K8sNodeInitDeployer(client, config)
|
||||
|
||||
when:
|
||||
deployer.cleanup()
|
||||
|
||||
then:
|
||||
0 * client.nodeList()
|
||||
0 * client.podDelete(_)
|
||||
}
|
||||
|
||||
def 'should not cleanup pods when cleanup is disabled' () {
|
||||
given:
|
||||
def client = Mock(K8sClient)
|
||||
def config = new K8sConfig(nodeInit: [enabled: true, cleanup: false])
|
||||
def deployer = new K8sNodeInitDeployer(client, config)
|
||||
|
||||
when:
|
||||
deployer.cleanup()
|
||||
|
||||
then:
|
||||
0 * client.nodeList()
|
||||
0 * client.podDelete(_)
|
||||
}
|
||||
|
||||
def 'should cleanup one init pod for each node' () {
|
||||
given:
|
||||
def client = Mock(K8sClient)
|
||||
def config = new K8sConfig(nodeInit: [enabled: true, cleanup: true])
|
||||
def deployer = new K8sNodeInitDeployer(client, config)
|
||||
|
||||
when:
|
||||
deployer.cleanup()
|
||||
|
||||
then:
|
||||
1 * client.nodeList() >> [
|
||||
items: [
|
||||
[metadata: [name: 'node-a']],
|
||||
[metadata: [name: 'NODE-B']]
|
||||
]
|
||||
]
|
||||
|
||||
then:
|
||||
1 * client.podDelete('nf-init-node-a')
|
||||
1 * client.podDelete('nf-init-node-b')
|
||||
0 * client._
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package recreationaltech.plugin
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
class K8sRuntimeEstimatorTest extends Specification {
|
||||
|
||||
def 'does extract a function' () {
|
||||
given:
|
||||
def observations = [a: [new Tuple2(0, 0), new Tuple2(100, 100)]]
|
||||
def estimator = new K8sRuntimeEstimator(observations)
|
||||
|
||||
when:
|
||||
def estimation = estimator.estimate("a", 1234)
|
||||
|
||||
then:
|
||||
estimation != Double.POSITIVE_INFINITY
|
||||
estimation == 1234.0
|
||||
}
|
||||
|
||||
def 'can extract a function from single measurement' () {
|
||||
given:
|
||||
def observations = [a: [new Tuple2(100, 100)]]
|
||||
def estimator = new K8sRuntimeEstimator(observations)
|
||||
|
||||
when:
|
||||
def estimation = estimator.estimate("a", 1234)
|
||||
|
||||
then:
|
||||
estimation == 100.0 /* Single measurement implies constant runtime */
|
||||
}
|
||||
|
||||
def 'unknown function returns positive infinity' () {
|
||||
given:
|
||||
def observations = [] as HashMap<String, ArrayList>
|
||||
def estimator = new K8sRuntimeEstimator(observations)
|
||||
|
||||
when:
|
||||
def estimation = estimator.estimate("b", 1234)
|
||||
|
||||
then:
|
||||
estimation == Double.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
def 'extracts function from multiple measurements' () {
|
||||
given:
|
||||
def observations = [a: [new Tuple2(10, 15), new Tuple2(20, 34), new Tuple2(30, 46)]]
|
||||
def estimator = new K8sRuntimeEstimator(observations)
|
||||
|
||||
when:
|
||||
def estimation = estimator.estimate("a", 40)
|
||||
|
||||
then:
|
||||
estimation != Double.POSITIVE_INFINITY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package recreationaltech.plugin
|
||||
|
||||
import nextflow.processor.TaskRun
|
||||
import spock.lang.Specification
|
||||
|
||||
class K8sSchedulingRequestTest extends Specification {
|
||||
|
||||
def 'should create scheduling request from task handler' () {
|
||||
given:
|
||||
def task = Mock(TaskRun)
|
||||
def handler = Spy(K8sTaskHandler)
|
||||
handler.task = task
|
||||
|
||||
when:
|
||||
def request = new K8sSchedulingRequest(handler)
|
||||
|
||||
then:
|
||||
request.handler == handler
|
||||
request.task == task
|
||||
request.submitTimeMillis > 0
|
||||
request.submitTimeMillis <= System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
import nextflow.Session
|
||||
import nextflow.executor.Executor
|
||||
import nextflow.processor.TaskConfig
|
||||
import nextflow.processor.TaskProcessor
|
||||
import nextflow.processor.TaskRun
|
||||
import spock.lang.Specification
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class K8sWrapperBuilderTest extends Specification {
|
||||
|
||||
def 'should render launcher script' () {
|
||||
given:
|
||||
def folder = Files.createTempDirectory('test')
|
||||
and:
|
||||
def sess = Mock(Session)
|
||||
def exec = Mock(Executor)
|
||||
def proc = Mock(TaskProcessor) { getSession() >> sess; getExecutor() >> exec }
|
||||
def config = new TaskConfig()
|
||||
def task = Mock(TaskRun) {
|
||||
getName() >> 'foo'
|
||||
getConfig() >> config
|
||||
getProcessor() >> proc
|
||||
getWorkDir() >> folder
|
||||
getInputFilesMap() >> [:]
|
||||
getOutputFilesNames() >> []
|
||||
}
|
||||
|
||||
and:
|
||||
def builder = Spy(new K8sWrapperBuilder(task)) { getSecretsEnv() >> null; fixOwnership() >> false }
|
||||
|
||||
when:
|
||||
def binding = builder.makeBinding()
|
||||
|
||||
then:
|
||||
binding.header_script == "NXF_CHDIR=${folder}"
|
||||
|
||||
cleanup:
|
||||
folder?.deleteDir()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.client
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
import spock.lang.Specification
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class ClientConfigTest extends Specification {
|
||||
|
||||
def 'should stringify a config' () {
|
||||
|
||||
when:
|
||||
final CERT = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTRENDQVRDZ0F3SUJBZ0lJRlFsM1l2Y2k1TWN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T0RBeE1UTXlNREEyTlRaYUZ3MHhPVEF4TVRNeU1EQTJOVFphTURNeApGREFTQmdOVkJBb1RDMFJ2WTJ0bGNpQkpibU11TVJzd0dRWURWUVFERXhKa2IyTnJaWEl0Wm05eUxXUmxjMnQwCmIzQXdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBUEtYT0ZsV2t2THIzb29ETGNFOElyME0KTzNBMHZqQlVvUzZ0bUdBbFRYYTd0QWQwM3BTMXNJNit0WVRwVlU2YXR6ZU9vU0VrOWhmaWxBdVNYdG1hSHZCUAp1czFEcG1LZEZRMWI3OFRkSnQ4OGV3c3BRajFxYUwvQldHeitMUzUrRHUrNUJuUGtmZlhDS1UxQTdUc2tZamJyClhxeDhlN2FWZURWTmFjZXc0Z0RqQWdNQkFBR2pBakFBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBMXVtVlAKR29EZTVCRXJrb21qWXdITXhiTTd4UStibTYrUDE1T0pINUo0UGNQeU11d25ocC9ORVp1NnpsTTZSUUo3SUNKQgpHWTRBMnFKVmJsWUkwQkJzRkF1TXMreTAyazdVVVVoK0NRYVd0SXhBcFNmbkQ4dUVXQ0g5VE1ZNGdLbTZjTDhVCk1OVVl1RnpUQ2hmTS96RjdUMXVaZWxJYXNrYXFaWSt3a3hxa3YyRUQxQ2F5MDUxSXRWRXZVbDIvSVZyVHdrT20KZ25nL3Q4L2RkeDhpOUkzTFJrMTlTaERKdXlQZ1NrTTZRSWlSd09mRHk4V0ZFaURpd0hBS0ErSEZhTGhOOFJTMwpieDUvdEhEN01id0FpdnorNTU4YUFEQjNEd1ZpekthM2d5Wm4yUzRjUGFqZnNwODFqRkNIQS9QekdQdTU2MzJwCkxRN0gyRW1aYmJuUHFYTFgKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo='.decodeBase64()
|
||||
final KEY = 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDWGdJQkFBS0JnUUR5bHpoWlZwTHk2OTZLQXkzQlBDSzlERHR3Tkw0d1ZLRXVyWmhnSlUxMnU3UUhkTjZVCnRiQ092cldFNlZWT21yYzNqcUVoSlBZWDRwUUxrbDdabWg3d1Q3ck5RNlppblJVTlcrL0UzU2JmUEhzTEtVSTkKYW1pL3dWaHMvaTB1Zmc3dnVRWno1SDMxd2lsTlFPMDdKR0kyNjE2c2ZIdTJsWGcxVFduSHNPSUE0d0lEQVFBQgpBb0dBYWRUOCtVU2lvU1d6bFVRanZ1eHNQMHRKMXY2N2hqdzFnVGFzaGkxZjZRK2tUNmgxdml5eGxPU3dMZ2JaCmQ0eFpwL3dxWVZwTm5rZnp6RVNUNnB5cEo5WTEwdHY1cFpSWG9HbG1NT2tIZSswUW45N0c5ZDRzL2JCV3lmYXYKRzhRTC9tZFN6Vy85YUdrSkpiNWU0VDlsSURvRDNFVDgwYUFWbzl2V0NPVUxsdWtDUVFEK0hINU5ucVBuSTdnTApWOUJKZzlRRVBwUTVYa2traW8rejZ2YkRHQU5rR1VPV1dmRURKUHE2Q2JBb1dqeWh1Qy9KS1dYRWs4Rkt0M1Y2CkhVNllYeVpGQWtFQTlHVE9XOFM4KzVNNHE5R3lNeURxN1ZkVHA3M2daeSsvNjVQam5hNlpDUnhTZklxL2xKUVoKY2F6MkhGYVRzRFdLbkdhWGNxTmdBVXNEODNyWTlzM3hCd0pCQUt5Vjc1YUtPMm0rRWI3cWVsV2p5bmpEZytwZQp4akNpUnkxOFZQSjJPYjlmaFU3MWNVS2dlQVdvbE5NalRuREw1dkNxUkNzNTZ4cnk5VC9sN2I2QlNUMENRUURnCjRoV2xDZTdnQzhOZEQzTkxhdUhpRGJZenB4dmp0Mk9Ca2E4ai9ISmptTVVxUnI0dEtPNFUxUlFPVlhoRzc2MmgKWnlHNjRpeklZOCs1N3ZQUWZ3Wm5Ba0VBdW9RWW1lUi90UWhIakhRNFlhZGRHbkNBQ2hZZ29ObEFzSGhGTElxVQo1ZTZaMXN2Q3VKU285TDVVRCtrclFUYWlGU01pRHZwZlJyVE1ZKzZ5Q0tTajd3PT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K'.decodeBase64()
|
||||
|
||||
def config = new ClientConfig()
|
||||
config.clientCert = CERT
|
||||
config.clientKey = KEY
|
||||
config.sslCert = CERT
|
||||
println config.toString()
|
||||
|
||||
then:
|
||||
noExceptionThrown()
|
||||
|
||||
}
|
||||
|
||||
def 'should create a client config from a map' () {
|
||||
|
||||
given:
|
||||
def MAP = [
|
||||
server:'foo.com',
|
||||
token: 'blah-blah',
|
||||
namespace: 'my-namespace',
|
||||
verifySsl: true,
|
||||
sslCert: 'fizzbuzz'.bytes.encodeBase64().toString(),
|
||||
clientCert: 'hello'.bytes.encodeBase64().toString(),
|
||||
clientKey: 'world'.bytes.encodeBase64().toString() ]
|
||||
|
||||
when:
|
||||
def result = ClientConfig.fromNextflowConfig(MAP, null, null)
|
||||
|
||||
then:
|
||||
result.server == 'foo.com'
|
||||
result.token == 'blah-blah'
|
||||
result.namespace == 'my-namespace'
|
||||
result.serviceAccount == 'default'
|
||||
result.verifySsl
|
||||
result.clientCert == 'hello'.bytes
|
||||
result.clientKey == 'world'.bytes
|
||||
result.sslCert == 'fizzbuzz'.bytes
|
||||
|
||||
when:
|
||||
result = ClientConfig.fromNextflowConfig(MAP, 'ns1', 'sa2')
|
||||
then:
|
||||
result.server == 'foo.com'
|
||||
result.token == 'blah-blah'
|
||||
result.namespace == 'ns1'
|
||||
result.serviceAccount == 'sa2'
|
||||
result.verifySsl
|
||||
result.clientCert == 'hello'.bytes
|
||||
result.clientKey == 'world'.bytes
|
||||
result.sslCert == 'fizzbuzz'.bytes
|
||||
}
|
||||
|
||||
def 'should create a client config from a map with files' () {
|
||||
|
||||
given:
|
||||
def folder = Files.createTempDirectory('test')
|
||||
def file1 = folder.resolve('file1')
|
||||
def file2 = folder.resolve('file2')
|
||||
def file3 = folder.resolve('file3')
|
||||
file1.text = 'fizzbuzz'.bytes.encodeBase64().toString()
|
||||
file2.text = 'hello'.bytes.encodeBase64().toString()
|
||||
file3.text = 'world'.bytes.encodeBase64().toString()
|
||||
|
||||
def MAP = [
|
||||
server:'foo.com',
|
||||
token: 'blah-blah',
|
||||
namespace: 'my-namespace',
|
||||
verifySsl: false,
|
||||
sslCertFile: file1,
|
||||
clientCertFile: file2,
|
||||
clientKeyFile: file3 ]
|
||||
|
||||
when:
|
||||
def result = ClientConfig.fromNextflowConfig(MAP, null, null)
|
||||
|
||||
then:
|
||||
result.server == 'foo.com'
|
||||
result.token == 'blah-blah'
|
||||
result.namespace == 'my-namespace'
|
||||
result.serviceAccount == 'default'
|
||||
!result.verifySsl
|
||||
result.sslCert == file1.text.bytes
|
||||
result.clientCert == file2.text.bytes
|
||||
result.clientKey == file3.text.bytes
|
||||
|
||||
cleanup:
|
||||
folder?.deleteDir()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.client
|
||||
import javax.net.ssl.KeyManager
|
||||
import java.nio.file.Files
|
||||
|
||||
import spock.lang.Specification
|
||||
import test.TestHelper
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class ConfigDiscoveryTest extends Specification {
|
||||
|
||||
def 'should read config from file' () {
|
||||
|
||||
given:
|
||||
final CERT_DATA = "d29ybGQgaGVsbG8="
|
||||
final CLIENT_CERT = "aGVsbG8gd29ybGQ="
|
||||
final CLIENT_KEY = "Y2lhbyBtaWFv"
|
||||
|
||||
def CONFIG = TestHelper.createInMemTempFile('config')
|
||||
CONFIG.text = """
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://localhost:6443
|
||||
certificate-authority-data: $CERT_DATA
|
||||
name: docker-for-desktop-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: docker-for-desktop-cluster
|
||||
user: docker-for-desktop
|
||||
name: docker-for-desktop
|
||||
current-context: docker-for-desktop
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: docker-for-desktop
|
||||
user:
|
||||
client-certificate-data: $CLIENT_CERT
|
||||
client-key-data: $CLIENT_KEY
|
||||
"""
|
||||
.stripIndent()
|
||||
|
||||
def discovery = Spy(ConfigDiscovery)
|
||||
def KEY_MANAGERS = [] as KeyManager[]
|
||||
|
||||
when:
|
||||
def config = discovery.fromKubeConfig(CONFIG, null, null, null)
|
||||
then:
|
||||
0 * discovery.discoverAuthToken(_, 'default',null) >> 'secret-token'
|
||||
1 * discovery.createKeyManagers(CLIENT_CERT.decodeBase64(), CLIENT_KEY.decodeBase64()) >> KEY_MANAGERS
|
||||
config.server == 'https://localhost:6443'
|
||||
config.token == null
|
||||
config.namespace == 'default'
|
||||
config.serviceAccount == 'default'
|
||||
config.clientCert == CLIENT_CERT.decodeBase64()
|
||||
config.clientKey == CLIENT_KEY.decodeBase64()
|
||||
config.sslCert == CERT_DATA.decodeBase64()
|
||||
config.keyManagers.is( KEY_MANAGERS )
|
||||
!config.verifySsl
|
||||
!config.isFromCluster
|
||||
|
||||
}
|
||||
|
||||
def 'should read config from file with provided namespace' () {
|
||||
|
||||
given:
|
||||
final CERT_DATA = "d29ybGQgaGVsbG8="
|
||||
final CLIENT_CERT = "aGVsbG8gd29ybGQ="
|
||||
final CLIENT_KEY = "Y2lhbyBtaWFv"
|
||||
|
||||
def CONFIG = TestHelper.createInMemTempFile('config')
|
||||
CONFIG.text = """
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://localhost:6443
|
||||
certificate-authority-data: $CERT_DATA
|
||||
name: docker-for-desktop-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: docker-for-desktop-cluster
|
||||
user: docker-for-desktop
|
||||
name: docker-for-desktop
|
||||
current-context: docker-for-desktop
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: docker-for-desktop
|
||||
user:
|
||||
client-certificate-data: $CLIENT_CERT
|
||||
client-key-data: $CLIENT_KEY
|
||||
"""
|
||||
.stripIndent()
|
||||
|
||||
def discovery = Spy(ConfigDiscovery)
|
||||
def KEY_MANAGERS = [] as KeyManager[]
|
||||
|
||||
when:
|
||||
def config = discovery.fromKubeConfig(CONFIG, 'docker-for-desktop', 'ns1', 'sa2')
|
||||
then:
|
||||
0 * discovery.discoverAuthToken('docker-for-desktop','ns1','sa2') >> 'secret-token'
|
||||
1 * discovery.createKeyManagers(CLIENT_CERT.decodeBase64(), CLIENT_KEY.decodeBase64()) >> KEY_MANAGERS
|
||||
config.server == 'https://localhost:6443'
|
||||
config.token == null
|
||||
config.namespace == 'ns1'
|
||||
config.serviceAccount == 'sa2'
|
||||
config.clientCert == CLIENT_CERT.decodeBase64()
|
||||
config.clientKey == CLIENT_KEY.decodeBase64()
|
||||
config.sslCert == CERT_DATA.decodeBase64()
|
||||
config.keyManagers.is( KEY_MANAGERS )
|
||||
!config.verifySsl
|
||||
!config.isFromCluster
|
||||
|
||||
}
|
||||
|
||||
def 'should read config from file with cert files' () {
|
||||
|
||||
given:
|
||||
def folder = Files.createTempDirectory(null)
|
||||
def CA_FILE = folder.resolve('ca'); CA_FILE.text = 'ca-content'
|
||||
def CLIENT_CERT_FILE = folder.resolve('client-cert'); CLIENT_CERT_FILE.text = 'client-cert-content'
|
||||
def CLIENT_KEY_FILE = folder.resolve('client-key'); CLIENT_KEY_FILE.text = 'client-key-content'
|
||||
def CONFIG = folder.resolve('config')
|
||||
def KEY_MANAGERS = [] as KeyManager[]
|
||||
|
||||
CONFIG.text = """
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://localhost:6443
|
||||
certificate-authority: $CA_FILE
|
||||
name: docker-for-desktop-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: docker-for-desktop-cluster
|
||||
user: docker-for-desktop
|
||||
name: docker-for-desktop
|
||||
current-context: docker-for-desktop
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: docker-for-desktop
|
||||
user:
|
||||
client-certificate: $CLIENT_CERT_FILE
|
||||
client-key: $CLIENT_KEY_FILE
|
||||
"""
|
||||
.stripIndent()
|
||||
|
||||
def discovery = Spy(ConfigDiscovery)
|
||||
|
||||
when:
|
||||
def config = discovery.fromKubeConfig(CONFIG, null, null, null)
|
||||
then:
|
||||
1 * discovery.createKeyManagers( CLIENT_CERT_FILE.bytes, CLIENT_KEY_FILE.bytes ) >> KEY_MANAGERS
|
||||
config.server == 'https://localhost:6443'
|
||||
config.token == null
|
||||
config.namespace == 'default'
|
||||
config.serviceAccount == 'default'
|
||||
config.clientCert == CLIENT_CERT_FILE.bytes
|
||||
config.clientKey == CLIENT_KEY_FILE.bytes
|
||||
config.sslCert == CA_FILE.bytes
|
||||
config.keyManagers.is( KEY_MANAGERS )
|
||||
!config.verifySsl
|
||||
!config.isFromCluster
|
||||
|
||||
cleanup:
|
||||
folder?.deleteDir()
|
||||
}
|
||||
|
||||
def 'should read config and use token' () {
|
||||
|
||||
given:
|
||||
def folder = Files.createTempDirectory(null)
|
||||
def CONFIG = folder.resolve('config')
|
||||
|
||||
CONFIG.text = """
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://localhost:6443
|
||||
name: docker-for-desktop-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: docker-for-desktop-cluster
|
||||
user: docker-for-desktop
|
||||
name: docker-for-desktop
|
||||
current-context: docker-for-desktop
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: docker-for-desktop
|
||||
user:
|
||||
token: 90s090s98s7f8s
|
||||
"""
|
||||
.stripIndent()
|
||||
|
||||
def discovery = Spy(ConfigDiscovery)
|
||||
|
||||
when:
|
||||
def config = discovery.fromKubeConfig(CONFIG, null, null, null)
|
||||
then:
|
||||
0 * discovery.discoverAuthToken(_,_,_) >> 'secret-token'
|
||||
0 * discovery.createKeyManagers( _, _ ) >> null
|
||||
config.server == 'https://localhost:6443'
|
||||
config.token == '90s090s98s7f8s'
|
||||
config.namespace == 'default'
|
||||
config.serviceAccount == 'default'
|
||||
!config.verifySsl
|
||||
!config.isFromCluster
|
||||
|
||||
cleanup:
|
||||
folder?.deleteDir()
|
||||
}
|
||||
|
||||
def 'should read config and discover token' () {
|
||||
|
||||
given:
|
||||
def folder = Files.createTempDirectory(null)
|
||||
def CONFIG = folder.resolve('config')
|
||||
|
||||
CONFIG.text = """
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://localhost:6443
|
||||
name: docker-for-desktop-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: docker-for-desktop-cluster
|
||||
user: docker-for-desktop
|
||||
name: docker-for-desktop
|
||||
current-context: docker-for-desktop
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: docker-for-desktop
|
||||
user:
|
||||
foo: bar
|
||||
"""
|
||||
.stripIndent()
|
||||
|
||||
def discovery = Spy(ConfigDiscovery)
|
||||
|
||||
when:
|
||||
def config = discovery.fromKubeConfig(CONFIG, null, null, null)
|
||||
then:
|
||||
1 * discovery.discoverAuthToken(_, _, _) >> 'secret-token'
|
||||
0 * discovery.createKeyManagers( _, _ ) >> null
|
||||
config.server == 'https://localhost:6443'
|
||||
config.token == 'secret-token'
|
||||
config.namespace == 'default'
|
||||
config.serviceAccount == 'default'
|
||||
!config.verifySsl
|
||||
!config.isFromCluster
|
||||
|
||||
cleanup:
|
||||
folder?.deleteDir()
|
||||
}
|
||||
|
||||
def 'should read config from given context' () {
|
||||
|
||||
given:
|
||||
def folder = Files.createTempDirectory(null)
|
||||
folder.resolve('fake-cert-file').text = 'fake-cert-content'
|
||||
folder.resolve('fake-key-file').text = 'fake-key-content'
|
||||
folder.resolve('fake-ca-file').text = 'fake-ca-content'
|
||||
|
||||
def CONFIG = folder.resolve('config')
|
||||
CONFIG.text = '''
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority: fake-ca-file
|
||||
server: https://1.2.3.4
|
||||
name: development
|
||||
- cluster:
|
||||
insecure-skip-tls-verify: true
|
||||
server: https://5.6.7.8
|
||||
name: scratch
|
||||
contexts:
|
||||
- context:
|
||||
cluster: development
|
||||
namespace: frontend
|
||||
user: developer
|
||||
name: dev-frontend
|
||||
- context:
|
||||
cluster: development
|
||||
namespace: storage
|
||||
user: developer
|
||||
name: dev-storage
|
||||
- context:
|
||||
cluster: scratch
|
||||
namespace: default
|
||||
user: experimenter
|
||||
name: exp-scratch
|
||||
current-context: ""
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: developer
|
||||
user:
|
||||
client-certificate: fake-cert-file
|
||||
- name: experimenter
|
||||
user:
|
||||
password: some-password
|
||||
username: exp
|
||||
'''.stripIndent()
|
||||
|
||||
when:
|
||||
def cfg1 = new ConfigDiscovery().fromKubeConfig(CONFIG, 'dev-frontend', null, null)
|
||||
then:
|
||||
cfg1.server == 'https://1.2.3.4'
|
||||
cfg1.sslCert == 'fake-ca-content'.bytes
|
||||
cfg1.isVerifySsl()
|
||||
cfg1.namespace == 'frontend'
|
||||
cfg1.serviceAccount == 'default'
|
||||
cfg1.clientCert == 'fake-cert-content'.bytes
|
||||
|
||||
when:
|
||||
def cfg2 = new ConfigDiscovery().fromKubeConfig(CONFIG, 'dev-storage', null, null)
|
||||
then:
|
||||
cfg2.server == 'https://1.2.3.4'
|
||||
cfg2.sslCert == 'fake-ca-content'.bytes
|
||||
cfg2.isVerifySsl()
|
||||
cfg2.namespace == 'storage'
|
||||
cfg2.serviceAccount == 'default'
|
||||
cfg2.clientCert == 'fake-cert-content'.bytes
|
||||
|
||||
when:
|
||||
def cfg3 = new ConfigDiscovery().fromKubeConfig(CONFIG, 'exp-scratch', null, null)
|
||||
then:
|
||||
cfg3.server == 'https://5.6.7.8'
|
||||
cfg3.sslCert == null
|
||||
!cfg3.isVerifySsl()
|
||||
cfg3.namespace == 'default'
|
||||
cfg3.serviceAccount == 'default'
|
||||
|
||||
when:
|
||||
new ConfigDiscovery().fromKubeConfig(CONFIG, 'foo', null, null)
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
|
||||
true
|
||||
cleanup:
|
||||
folder.deleteDir()
|
||||
}
|
||||
|
||||
def 'should load from cluster env' () {
|
||||
given:
|
||||
def CERT_FILE = TestHelper.createInMemTempFile('ca'); CERT_FILE.text = 'ca-content'
|
||||
def TOKEN_FILE = TestHelper.createInMemTempFile('token'); TOKEN_FILE.text = 'my-token'
|
||||
def NAMESPACE_FILE = TestHelper.createInMemTempFile('namespace'); NAMESPACE_FILE.text = 'foo-namespace'
|
||||
|
||||
def discovery = Spy(ConfigDiscovery)
|
||||
|
||||
when:
|
||||
def env = [ KUBERNETES_SERVICE_HOST: 'foo.com', KUBERNETES_SERVICE_PORT: '4343' ]
|
||||
def config = discovery.fromCluster(env, null, null)
|
||||
then:
|
||||
1 * discovery.path('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt') >> CERT_FILE
|
||||
1 * discovery.path('/var/run/secrets/kubernetes.io/serviceaccount/token') >> TOKEN_FILE
|
||||
1 * discovery.path('/var/run/secrets/kubernetes.io/serviceaccount/namespace') >> NAMESPACE_FILE
|
||||
0 * discovery.createKeyManagers(_,_) >> null
|
||||
and:
|
||||
config.server == 'foo.com:4343'
|
||||
config.namespace == 'foo-namespace'
|
||||
config.token == 'my-token'
|
||||
config.sslCert == CERT_FILE.text.bytes
|
||||
config.isFromCluster
|
||||
|
||||
when:
|
||||
env = [ KUBERNETES_SERVICE_HOST: 'https://host.com' ]
|
||||
config = discovery.fromCluster(env, 'my-namespace', null)
|
||||
then:
|
||||
1 * discovery.path('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt') >> CERT_FILE
|
||||
1 * discovery.path('/var/run/secrets/kubernetes.io/serviceaccount/token') >> TOKEN_FILE
|
||||
1 * discovery.path('/var/run/secrets/kubernetes.io/serviceaccount/namespace') >> NAMESPACE_FILE
|
||||
and:
|
||||
config.server == 'https://host.com'
|
||||
config.namespace == 'my-namespace'
|
||||
}
|
||||
|
||||
def 'should create key managers' () {
|
||||
given:
|
||||
final CERT = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTRENDQVRDZ0F3SUJBZ0lJRlFsM1l2Y2k1TWN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T0RBeE1UTXlNREEyTlRaYUZ3MHhPVEF4TVRNeU1EQTJOVFphTURNeApGREFTQmdOVkJBb1RDMFJ2WTJ0bGNpQkpibU11TVJzd0dRWURWUVFERXhKa2IyTnJaWEl0Wm05eUxXUmxjMnQwCmIzQXdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBUEtYT0ZsV2t2THIzb29ETGNFOElyME0KTzNBMHZqQlVvUzZ0bUdBbFRYYTd0QWQwM3BTMXNJNit0WVRwVlU2YXR6ZU9vU0VrOWhmaWxBdVNYdG1hSHZCUAp1czFEcG1LZEZRMWI3OFRkSnQ4OGV3c3BRajFxYUwvQldHeitMUzUrRHUrNUJuUGtmZlhDS1UxQTdUc2tZamJyClhxeDhlN2FWZURWTmFjZXc0Z0RqQWdNQkFBR2pBakFBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBMXVtVlAKR29EZTVCRXJrb21qWXdITXhiTTd4UStibTYrUDE1T0pINUo0UGNQeU11d25ocC9ORVp1NnpsTTZSUUo3SUNKQgpHWTRBMnFKVmJsWUkwQkJzRkF1TXMreTAyazdVVVVoK0NRYVd0SXhBcFNmbkQ4dUVXQ0g5VE1ZNGdLbTZjTDhVCk1OVVl1RnpUQ2hmTS96RjdUMXVaZWxJYXNrYXFaWSt3a3hxa3YyRUQxQ2F5MDUxSXRWRXZVbDIvSVZyVHdrT20KZ25nL3Q4L2RkeDhpOUkzTFJrMTlTaERKdXlQZ1NrTTZRSWlSd09mRHk4V0ZFaURpd0hBS0ErSEZhTGhOOFJTMwpieDUvdEhEN01id0FpdnorNTU4YUFEQjNEd1ZpekthM2d5Wm4yUzRjUGFqZnNwODFqRkNIQS9QekdQdTU2MzJwCkxRN0gyRW1aYmJuUHFYTFgKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo='.decodeBase64()
|
||||
final KEY = 'LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDWGdJQkFBS0JnUUR5bHpoWlZwTHk2OTZLQXkzQlBDSzlERHR3Tkw0d1ZLRXVyWmhnSlUxMnU3UUhkTjZVCnRiQ092cldFNlZWT21yYzNqcUVoSlBZWDRwUUxrbDdabWg3d1Q3ck5RNlppblJVTlcrL0UzU2JmUEhzTEtVSTkKYW1pL3dWaHMvaTB1Zmc3dnVRWno1SDMxd2lsTlFPMDdKR0kyNjE2c2ZIdTJsWGcxVFduSHNPSUE0d0lEQVFBQgpBb0dBYWRUOCtVU2lvU1d6bFVRanZ1eHNQMHRKMXY2N2hqdzFnVGFzaGkxZjZRK2tUNmgxdml5eGxPU3dMZ2JaCmQ0eFpwL3dxWVZwTm5rZnp6RVNUNnB5cEo5WTEwdHY1cFpSWG9HbG1NT2tIZSswUW45N0c5ZDRzL2JCV3lmYXYKRzhRTC9tZFN6Vy85YUdrSkpiNWU0VDlsSURvRDNFVDgwYUFWbzl2V0NPVUxsdWtDUVFEK0hINU5ucVBuSTdnTApWOUJKZzlRRVBwUTVYa2traW8rejZ2YkRHQU5rR1VPV1dmRURKUHE2Q2JBb1dqeWh1Qy9KS1dYRWs4Rkt0M1Y2CkhVNllYeVpGQWtFQTlHVE9XOFM4KzVNNHE5R3lNeURxN1ZkVHA3M2daeSsvNjVQam5hNlpDUnhTZklxL2xKUVoKY2F6MkhGYVRzRFdLbkdhWGNxTmdBVXNEODNyWTlzM3hCd0pCQUt5Vjc1YUtPMm0rRWI3cWVsV2p5bmpEZytwZQp4akNpUnkxOFZQSjJPYjlmaFU3MWNVS2dlQVdvbE5NalRuREw1dkNxUkNzNTZ4cnk5VC9sN2I2QlNUMENRUURnCjRoV2xDZTdnQzhOZEQzTkxhdUhpRGJZenB4dmp0Mk9Ca2E4ai9ISmptTVVxUnI0dEtPNFUxUlFPVlhoRzc2MmgKWnlHNjRpeklZOCs1N3ZQUWZ3Wm5Ba0VBdW9RWW1lUi90UWhIakhRNFlhZGRHbkNBQ2hZZ29ObEFzSGhGTElxVQo1ZTZaMXN2Q3VKU285TDVVRCtrclFUYWlGU01pRHZwZlJyVE1ZKzZ5Q0tTajd3PT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K'.decodeBase64()
|
||||
final discovery = new ConfigDiscovery()
|
||||
when:
|
||||
def managers = discovery.createKeyManagers(CERT, KEY)
|
||||
then:
|
||||
managers.size()==1
|
||||
}
|
||||
|
||||
def 'should create key managers from an EC client key' () {
|
||||
given:
|
||||
final CERT = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTRENDQVRDZ0F3SUJBZ0lJRlFsM1l2Y2k1TWN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T0RBeE1UTXlNREEyTlRaYUZ3MHhPVEF4TVRNeU1EQTJOVFphTURNeApGREFTQmdOVkJBb1RDMFJ2WTJ0bGNpQkpibU11TVJzd0dRWURWUVFERXhKa2IyTnJaWEl0Wm05eUxXUmxjMnQwCmIzQXdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBUEtYT0ZsV2t2THIzb29ETGNFOElyME0KTzNBMHZqQlVvUzZ0bUdBbFRYYTd0QWQwM3BTMXNJNit0WVRwVlU2YXR6ZU9vU0VrOWhmaWxBdVNYdG1hSHZCUAp1czFEcG1LZEZRMWI3OFRkSnQ4OGV3c3BRajFxYUwvQldHeitMUzUrRHUrNUJuUGtmZlhDS1UxQTdUc2tZamJyClhxeDhlN2FWZURWTmFjZXc0Z0RqQWdNQkFBR2pBakFBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBMXVtVlAKR29EZTVCRXJrb21qWXdITXhiTTd4UStibTYrUDE1T0pINUo0UGNQeU11d25ocC9ORVp1NnpsTTZSUUo3SUNKQgpHWTRBMnFKVmJsWUkwQkJzRkF1TXMreTAyazdVVVVoK0NRYVd0SXhBcFNmbkQ4dUVXQ0g5VE1ZNGdLbTZjTDhVCk1OVVl1RnpUQ2hmTS96RjdUMXVaZWxJYXNrYXFaWSt3a3hxa3YyRUQxQ2F5MDUxSXRWRXZVbDIvSVZyVHdrT20KZ25nL3Q4L2RkeDhpOUkzTFJrMTlTaERKdXlQZ1NrTTZRSWlSd09mRHk4V0ZFaURpd0hBS0ErSEZhTGhOOFJTMwpieDUvdEhEN01id0FpdnorNTU4YUFEQjNEd1ZpekthM2d5Wm4yUzRjUGFqZnNwODFqRkNIQS9QekdQdTU2MzJwCkxRN0gyRW1aYmJuUHFYTFgKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo='.decodeBase64()
|
||||
final KEY = 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ21aZFZ3NmJRU0w1T1l5RjQKbzJ4V0hUQ05BSW1hRTkycGd2dGMzK2Z2UDVxaFJBTkNBQVJSd0RpUVptTUNqcWxvbFBzRTdiZjgwWjhrZkRXTworS2U4NUdVSll2MlBubWVxbDhkYjdwcmFlMHFPQUJaaXR2Mmh2SmJFeFdsUFR0MS9CYTNMK1B5NAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg=='.decodeBase64()
|
||||
final discovery = new ConfigDiscovery()
|
||||
when:
|
||||
def managers = discovery.createKeyManagers(CERT, KEY)
|
||||
then:
|
||||
managers.size()==1
|
||||
}
|
||||
|
||||
def 'should create key managers from an EC-encrypted client key' () {
|
||||
given:
|
||||
final CERT = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJSGw1Zmx0UmRTdDB3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekUwTnpRM09UVTNNQjRYRFRJME1EVXdNekUwTlRJek4xb1hEVEkxTURVdwpNekUwTlRJek4xb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJQN1Q5RHVvUlllLzBlUkwKUmNHV2RoYnl2Q3BucXlsSVIyaUwxdGkwc1hVdEpZZjUrVXhIOWFBMjdzY2FSYW1qbjdnTTFrKzZNaVk5cm15OApyRmdoWm1xalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCU0NhdXFoQVEvWEdoaFRtaFBoY21vRVdOeWluakFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlCZzRaNmlWeFV3Mk5uMHBQTG02VlovUGttQnVuTDEwZG50dEg3UVdIcklCd0loQU0vTDhVMGxQN0IyeFEyZwpsZjlhNHNhbzJ1bE5ONnQvQ0dibzlxTlo1QzZHCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTVRRM05EYzVOVGN3SGhjTk1qUXdOVEF6TVRRMU1qTTNXaGNOTXpRd05UQXhNVFExTWpNMwpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTVRRM05EYzVOVGN3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRZHFYVHdIQS9mVjRKZGdYa2FubXB1OVE0QStwUGRGaXZGdytiUmVhdEYKUXVOUTBKWndIbzlaa2ltb2lEUU5qb2h0TWdHckdtTVlsTTZuaXM4ZVFvM3RvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWdtcnFvUUVQMXhvWVU1b1Q0WEpxCkJGamNvcDR3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUlvb2ZmNzdvb1VYS2hmNVo3aVRzdExhOTVwU2VaRmUKRHZjMXdFQXVEa3NTQWlBNzJQajJxNnpBclhpYkpUa0s2RTBHTEtVODdhTHhHc3BmS29uVVJnalI2Zz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'.decodeBase64()
|
||||
final KEY = 'LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUNvQTNvRHkzN3NXdmszM3JGRGtRdlZ1Wkh1cCt1Uk40V3RqbUlPR1c4cHBvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFL3RQME82aEZoNy9SNUV0RndaWjJGdks4S21lcktVaEhhSXZXMkxTeGRTMGxoL241VEVmMQpvRGJ1eHhwRnFhT2Z1QXpXVDdveUpqMnViTHlzV0NGbWFnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo='.decodeBase64()
|
||||
final discovery = new ConfigDiscovery()
|
||||
when:
|
||||
def managers = discovery.createKeyManagers(CERT, KEY)
|
||||
then:
|
||||
managers.size()==1
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.client
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class K8sResponseExceptionTest extends Specification {
|
||||
|
||||
def 'should create response from valid json' () {
|
||||
|
||||
when:
|
||||
def resp = new K8sResponseException(
|
||||
'Request /this/that failed',
|
||||
new K8sResponseJson('{"foo":"one","bar":"two"}'))
|
||||
then:
|
||||
resp.getMessage() == '''
|
||||
Request /this/that failed
|
||||
|
||||
{
|
||||
"foo": "one",
|
||||
"bar": "two"
|
||||
}
|
||||
'''.stripIndent().leftTrim()
|
||||
}
|
||||
|
||||
|
||||
def 'should create response from error message' () {
|
||||
|
||||
when:
|
||||
def resp = new K8sResponseException(
|
||||
'Request /this/that failed',
|
||||
new K8sResponseJson('Oops.. it crashed badly'))
|
||||
then:
|
||||
resp.getMessage() == 'Request /this/that failed -- Oops.. it crashed badly'
|
||||
}
|
||||
|
||||
def 'should contain the response object passed to it' () {
|
||||
|
||||
given:
|
||||
def resp_json = new K8sResponseJson('{"error": "out of cheese error"}')
|
||||
|
||||
when:
|
||||
def resp = new K8sResponseException("Error occurred",resp_json)
|
||||
|
||||
then:
|
||||
resp.response == resp_json
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.client
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class K8sResponseJsonTest extends Specification {
|
||||
|
||||
def 'should create a response from a map' () {
|
||||
|
||||
given:
|
||||
def MAP = [foo: 'one', bar: 'two']
|
||||
|
||||
when:
|
||||
def resp = new K8sResponseJson(MAP)
|
||||
then:
|
||||
resp.foo == 'one'
|
||||
resp.bar == 'two'
|
||||
resp.toString() == '''
|
||||
{
|
||||
"foo": "one",
|
||||
"bar": "two"
|
||||
}
|
||||
'''.stripIndent().trim()
|
||||
|
||||
}
|
||||
|
||||
def 'should create a response from a json string' () {
|
||||
|
||||
when:
|
||||
def resp = new K8sResponseJson('{"foo":"one","bar":"two"}')
|
||||
then:
|
||||
resp.foo == 'one'
|
||||
resp.bar == 'two'
|
||||
resp.toString() == '''
|
||||
{
|
||||
"foo": "one",
|
||||
"bar": "two"
|
||||
}
|
||||
'''.stripIndent().trim()
|
||||
|
||||
}
|
||||
|
||||
def 'should create a response from an error message' () {
|
||||
when:
|
||||
def resp = new K8sResponseJson('Ooops .. this crashed')
|
||||
then:
|
||||
resp.toString() == 'Ooops .. this crashed'
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.model
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class PodEnvTest extends Specification {
|
||||
|
||||
def 'should return env spec' () {
|
||||
expect:
|
||||
PodEnv.value('ALPHA', 'aaa').toSpec() == [name:'ALPHA', value:'aaa']
|
||||
}
|
||||
|
||||
def 'should create env fieldPath spec' () {
|
||||
expect:
|
||||
PodEnv.fieldPath('ALPHA', 'aaa').toSpec() == [
|
||||
name:'ALPHA',
|
||||
valueFrom: [fieldRef:[fieldPath: 'aaa']]
|
||||
]
|
||||
}
|
||||
|
||||
def 'should create env secret spec' () {
|
||||
expect:
|
||||
PodEnv.secret('ALPHA', 'data/key-1').toSpec() == [
|
||||
name: 'ALPHA',
|
||||
valueFrom: [secretKeyRef:[name:'data', key:'key-1']]
|
||||
]
|
||||
|
||||
PodEnv.secret('ALPHA', 'data').toSpec() == [
|
||||
name: 'ALPHA',
|
||||
valueFrom: [secretKeyRef:[name:'data', key:'ALPHA']]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def 'should create env config spec' () {
|
||||
expect:
|
||||
PodEnv.config('ALPHA', 'data/key-1').toSpec() == [
|
||||
name: 'ALPHA',
|
||||
valueFrom: [configMapKeyRef:[name:'data', key:'key-1']]
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.model
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class PodMountConfigTest extends Specification {
|
||||
|
||||
def 'should create mount for configmap' () {
|
||||
|
||||
when:
|
||||
def opt = new PodMountConfig(mountPath: '/etc/some/name', config: 'here' )
|
||||
then:
|
||||
opt.mountPath == '/etc/some/name'
|
||||
opt.fileName == null
|
||||
opt.configName == 'here'
|
||||
opt.configKey == null
|
||||
|
||||
when:
|
||||
opt = new PodMountConfig(mountPath: '/etc/some/name', config: 'here/there.txt' )
|
||||
then:
|
||||
opt.mountPath == '/etc/some'
|
||||
opt.fileName == 'name'
|
||||
opt.configName == 'here'
|
||||
opt.configKey == 'there.txt'
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.model
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class PodMountSecretTest extends Specification {
|
||||
|
||||
def 'should create mount for configmap' () {
|
||||
|
||||
when:
|
||||
def opt = new PodMountSecret(mountPath: '/etc/some/name', secret: 'here' )
|
||||
then:
|
||||
opt.mountPath == '/etc/some/name'
|
||||
opt.fileName == null
|
||||
opt.secretName == 'here'
|
||||
opt.secretKey == null
|
||||
|
||||
when:
|
||||
opt = new PodMountSecret(mountPath: '/etc/some/name', secret: 'here/there.txt' )
|
||||
then:
|
||||
opt.mountPath == '/etc/some'
|
||||
opt.fileName == 'name'
|
||||
opt.secretName == 'here'
|
||||
opt.secretKey == 'there.txt'
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.model
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class PodNodeSelectorTest extends Specification {
|
||||
|
||||
def 'should create node selector' () {
|
||||
|
||||
expect:
|
||||
new PodNodeSelector(selector).toSpec() == spec
|
||||
|
||||
where:
|
||||
selector | spec
|
||||
'' | [:]
|
||||
'foo=1' | [foo:'1']
|
||||
'x=a,y=2,z=9' | [x:'a',y:'2',z:'9']
|
||||
'x= a , y=2 , z =9' | [x:'a',y:'2',z:'9']
|
||||
'gpu,intel' | [gpu:'true',intel: 'true']
|
||||
[foo:1, bar: 'two'] | [foo:'1', bar:'two']
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.model
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class PodOptionsTest extends Specification {
|
||||
|
||||
|
||||
def 'should create empty options' () {
|
||||
|
||||
when:
|
||||
def options = new PodOptions(null)
|
||||
then:
|
||||
options.getEnvVars() == [] as Set
|
||||
options.getMountConfigMaps() == [] as Set
|
||||
options.getMountCsiEphemerals() == [] as Set
|
||||
options.getMountEmptyDirs() == [] as Set
|
||||
options.getMountSecrets() == [] as Set
|
||||
options.getAutomountServiceAccountToken() == true
|
||||
}
|
||||
|
||||
def 'should set pullPolicy' () {
|
||||
|
||||
when:
|
||||
def options = new PodOptions()
|
||||
then:
|
||||
options.getImagePullPolicy() == null
|
||||
|
||||
when:
|
||||
options = new PodOptions([ [pullPolicy:'Always'] ])
|
||||
then:
|
||||
options.getImagePullPolicy() == 'Always'
|
||||
|
||||
when:
|
||||
options = new PodOptions([ [imagePullPolicy:'latest'] ])
|
||||
then:
|
||||
options.getImagePullPolicy() == 'latest'
|
||||
}
|
||||
|
||||
def 'should set imagePullSecret' () {
|
||||
|
||||
when:
|
||||
def options = new PodOptions()
|
||||
then:
|
||||
options.imagePullSecret == null
|
||||
|
||||
when:
|
||||
options = new PodOptions([ [imagePullSecret:'foo'] ])
|
||||
then:
|
||||
options.imagePullSecret == 'foo'
|
||||
|
||||
when:
|
||||
options = new PodOptions([ [imagePullSecrets:'bar'] ])
|
||||
then:
|
||||
options.imagePullSecret == 'bar'
|
||||
}
|
||||
|
||||
def 'should return config mounts' () {
|
||||
|
||||
given:
|
||||
def options = [
|
||||
[mountPath: '/this/path1.txt', config: 'name/key1'],
|
||||
[mountPath: '/this/path2.txt', config: 'name/key2'],
|
||||
[mountPath: '/this/path2.txt', config: 'name/key2'], // <-- identical entry are ignored
|
||||
[mountPath: '/this/path2.txt', secret: 'name/secret'],
|
||||
[env: 'FOO', config: '/name/foo']
|
||||
]
|
||||
|
||||
when:
|
||||
def configs = new PodOptions(options).getMountConfigMaps()
|
||||
then:
|
||||
configs.size() == 2
|
||||
configs == [
|
||||
new PodMountConfig(mountPath: '/this/path1.txt', config: 'name/key1'),
|
||||
new PodMountConfig(mountPath: '/this/path2.txt', config: 'name/key2')
|
||||
] as Set
|
||||
}
|
||||
|
||||
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 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:
|
||||
def options = [
|
||||
[mountPath: '/this/path1.txt', config: 'name/key1'],
|
||||
[mountPath: '/this/alpha.txt', secret: 'name/secret1'],
|
||||
[mountPath: '/this/beta.txt', secret: 'name/secret2'],
|
||||
[mountPath: '/this/beta.txt', secret: 'name/secret2'],
|
||||
[env: 'FOO', config: '/name/foo']
|
||||
]
|
||||
|
||||
when:
|
||||
def secrets = new PodOptions(options).getMountSecrets()
|
||||
then:
|
||||
secrets.size() == 2
|
||||
secrets == [
|
||||
new PodMountSecret(mountPath: '/this/alpha.txt', secret: 'name/secret1'),
|
||||
new PodMountSecret(mountPath: '/this/beta.txt', secret: 'name/secret2'),
|
||||
] as Set
|
||||
}
|
||||
|
||||
|
||||
def 'should return env definitions' () {
|
||||
|
||||
given:
|
||||
def options = [
|
||||
[mountPath: '/this/path1.txt', config: 'name/key1'],
|
||||
[mountPath: '/this/alpha.txt', secret: 'name/secret1'],
|
||||
[mountPath: '/this/beta.txt', secret: 'name/secret2'],
|
||||
[env: 'FOO', config: '/name/foo'],
|
||||
[env: 'FOO', config: '/name/foo'],
|
||||
[env: 'BAR', config: '/name/BAR'],
|
||||
[env: 'ALPHA', value: 'aaa'],
|
||||
[env: 'ALPHA', value: 'aaa'],
|
||||
[env: 'BETA', value: 'bbb'],
|
||||
[env: 'PASSWORD', secret:'name/key'],
|
||||
[env: 'PASSWORD', secret:'name/key'],
|
||||
]
|
||||
|
||||
when:
|
||||
def env = new PodOptions(options).getEnvVars()
|
||||
then:
|
||||
env.size() == 5
|
||||
env == [
|
||||
PodEnv.config('FOO', '/name/foo'),
|
||||
PodEnv.config('BAR', '/name/BAR'),
|
||||
PodEnv.value('ALPHA', 'aaa'),
|
||||
PodEnv.value('BETA', 'bbb'),
|
||||
PodEnv.secret('PASSWORD', 'name/key'),
|
||||
] as Set
|
||||
|
||||
}
|
||||
|
||||
def 'should create persistent volume claims' () {
|
||||
given:
|
||||
def options = [
|
||||
[volumeClaim:'pvc1', mountPath: '/this/path'],
|
||||
[volumeClaim:'pvc2', mountPath: '/that/path'],
|
||||
[volumeClaim:'pvc3', mountPath: '/some/data', subPath: '/foo']
|
||||
]
|
||||
|
||||
when:
|
||||
def claims = new PodOptions(options).getVolumeClaims()
|
||||
then:
|
||||
claims.size() == 3
|
||||
claims == [
|
||||
new PodVolumeClaim('pvc1', '/this/path'),
|
||||
new PodVolumeClaim('pvc2', '/that/path'),
|
||||
new PodVolumeClaim('pvc3', '/some/data', '/foo')
|
||||
] as Set
|
||||
|
||||
}
|
||||
|
||||
def 'should create host path' () {
|
||||
given:
|
||||
def options = [
|
||||
[hostPath: '/host/one', mountPath: '/pod/1'],
|
||||
[hostPath: '/host/two', mountPath: '/pod/2']
|
||||
]
|
||||
when:
|
||||
def mounts = new PodOptions(options).getMountHostPaths()
|
||||
|
||||
then:
|
||||
mounts == [
|
||||
new PodHostMount('/host/one', '/pod/1'),
|
||||
new PodHostMount('/host/two', '/pod/2')
|
||||
] as Set
|
||||
|
||||
}
|
||||
|
||||
def 'should not create env' () {
|
||||
when:
|
||||
new PodOptions([ [env:'FOO'] ])
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
|
||||
when:
|
||||
new PodOptions([ [secret:'FOO'] ])
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
|
||||
when:
|
||||
new PodOptions([ [config:'FOO'] ])
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
|
||||
when:
|
||||
new PodOptions([ [volumeClaim:'FOO'] ])
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
}
|
||||
|
||||
def 'should merge podOptions' () {
|
||||
|
||||
given:
|
||||
def list1 = [
|
||||
[env: 'HELLO', value: 'WORLD'],
|
||||
[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'],
|
||||
[config: 'bar/key', mountPath: '/b/bb'],
|
||||
[secret: 'foo/key', mountPath: '/a/aa'],
|
||||
[volumeClaim: 'cvp', mountPath: '/c/cc'],
|
||||
|
||||
[env: 'DELTA', value: 'LAMBDA'],
|
||||
[config: 'y', mountPath: '/y'],
|
||||
[secret: 'x', mountPath: '/x'],
|
||||
[volumeClaim: 'z', mountPath: '/z'],
|
||||
]
|
||||
|
||||
def list3 = [
|
||||
[env: 'HELLO', value: 'WORLD'],
|
||||
[config: 'data/key', mountPath: '/data/file.txt'],
|
||||
[secret: 'secret/key', mountPath: '/etc/secret'],
|
||||
[volumeClaim: 'pvc', mountPath: '/mnt/claim'],
|
||||
|
||||
[env: 'DELTA', value: 'LAMBDA'],
|
||||
[config: 'y', mountPath: '/y'],
|
||||
[secret: 'x', mountPath: '/x'],
|
||||
[volumeClaim: 'z', mountPath: '/z'],
|
||||
|
||||
[csi: [driver: 'inline.storage.kubernetes.io'], mountPath: '/data'],
|
||||
[emptyDir: [:], mountPath: '/scratch1'],
|
||||
[securityContext: [runAsUser: 1000, fsGroup: 200, allowPrivilegeEscalation: true]],
|
||||
[nodeSelector: 'foo=X, bar=Y'],
|
||||
[automountServiceAccountToken: false],
|
||||
[priorityClassName: 'high-priority']
|
||||
]
|
||||
|
||||
PodOptions opts
|
||||
|
||||
when:
|
||||
opts = new PodOptions() + new PodOptions()
|
||||
then:
|
||||
opts == new PodOptions()
|
||||
|
||||
when:
|
||||
opts = new PodOptions(list1) + new PodOptions()
|
||||
then:
|
||||
opts == new PodOptions(list1)
|
||||
opts.securityContext.toSpec() == [runAsUser:500]
|
||||
|
||||
when:
|
||||
opts = new PodOptions() + new PodOptions(list1)
|
||||
then:
|
||||
opts == new PodOptions(list1)
|
||||
opts.securityContext.toSpec() == [runAsUser:500]
|
||||
|
||||
when:
|
||||
opts = new PodOptions(list1) + new PodOptions(list1)
|
||||
then:
|
||||
opts == new PodOptions(list1)
|
||||
opts.securityContext.toSpec() == [runAsUser:500]
|
||||
|
||||
when:
|
||||
opts = new PodOptions(list1) + new PodOptions(list2)
|
||||
then:
|
||||
opts == new PodOptions(list1 + list2)
|
||||
opts.securityContext.toSpec() == [runAsUser:500]
|
||||
|
||||
when:
|
||||
opts = new PodOptions(list1) + new PodOptions(list3)
|
||||
then:
|
||||
opts.getEnvVars() == [
|
||||
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.getMountEmptyDirs() == [
|
||||
new PodMountEmptyDir([:], '/scratch1'),
|
||||
] as Set
|
||||
|
||||
opts.getMountSecrets() == [
|
||||
new PodMountSecret('secret/key', '/etc/secret'),
|
||||
new PodMountSecret('x', '/x')
|
||||
] as Set
|
||||
|
||||
opts.getVolumeClaims() == [
|
||||
new PodVolumeClaim('pvc','/mnt/claim'),
|
||||
new PodVolumeClaim('z','/z'),
|
||||
] as Set
|
||||
|
||||
opts.securityContext.toSpec() == [runAsUser: 1000, fsGroup: 200, allowPrivilegeEscalation: true]
|
||||
|
||||
opts.nodeSelector.toSpec() == [foo: 'X', bar: "Y"]
|
||||
|
||||
opts.getAutomountServiceAccountToken() == false
|
||||
|
||||
opts.getPriorityClassName() == 'high-priority'
|
||||
}
|
||||
|
||||
def 'should copy image pull policy' (){
|
||||
given:
|
||||
def data = [
|
||||
[imagePullPolicy : 'FOO']
|
||||
]
|
||||
|
||||
when:
|
||||
def opts = new PodOptions() + new PodOptions(data)
|
||||
then:
|
||||
opts.imagePullPolicy == 'FOO'
|
||||
|
||||
when:
|
||||
opts = new PodOptions(data) + new PodOptions()
|
||||
then:
|
||||
opts.imagePullPolicy == 'FOO'
|
||||
}
|
||||
|
||||
def 'should copy image pull secret' (){
|
||||
given:
|
||||
def data = [
|
||||
[imagePullSecret : 'BAR']
|
||||
]
|
||||
|
||||
when:
|
||||
def opts = new PodOptions() + new PodOptions(data)
|
||||
then:
|
||||
opts.imagePullSecret == 'BAR'
|
||||
|
||||
when:
|
||||
opts = new PodOptions(data) + new PodOptions()
|
||||
then:
|
||||
opts.imagePullSecret == 'BAR'
|
||||
}
|
||||
|
||||
def 'should copy pod labels' (){
|
||||
given:
|
||||
def data = [
|
||||
[label: "LABEL", value: 'VALUE']
|
||||
]
|
||||
|
||||
when:
|
||||
def opts = new PodOptions() + new PodOptions(data)
|
||||
then:
|
||||
opts.labels == ["LABEL": "VALUE"]
|
||||
|
||||
when:
|
||||
opts = new PodOptions(data) + new PodOptions()
|
||||
then:
|
||||
opts.labels == ["LABEL": "VALUE"]
|
||||
|
||||
when:
|
||||
opts = new PodOptions([[label:"FOO", value:'one']]) + new PodOptions([[label:"BAR", value:'two']])
|
||||
then:
|
||||
opts.labels == [FOO: 'one', BAR: 'two']
|
||||
}
|
||||
|
||||
def 'should copy host paths' (){
|
||||
given:
|
||||
def data = [
|
||||
[hostPath: "/foo", mountPath: '/one']
|
||||
]
|
||||
|
||||
when:
|
||||
def opts = new PodOptions() + new PodOptions(data)
|
||||
then:
|
||||
opts.getMountHostPaths() == [new PodHostMount('/foo', '/one')] as Set
|
||||
|
||||
when:
|
||||
opts = new PodOptions(data) + new PodOptions()
|
||||
then:
|
||||
opts.getMountHostPaths() == [new PodHostMount('/foo', '/one')] as Set
|
||||
|
||||
when:
|
||||
opts = new PodOptions([[hostPath:"/foo", mountPath: '/one']]) + new PodOptions([[hostPath:"/bar", mountPath: '/two']])
|
||||
then:
|
||||
opts.getMountHostPaths() == [
|
||||
new PodHostMount('/foo','/one'),
|
||||
new PodHostMount('/bar','/two')
|
||||
] as Set
|
||||
}
|
||||
|
||||
def 'should create pod labels' () {
|
||||
|
||||
given:
|
||||
def options = [
|
||||
[label: 'ALPHA', value: 'aaa'],
|
||||
[label: 'DELTA', value: 'bbb'],
|
||||
[label: 'DELTA', value: 'ddd']
|
||||
]
|
||||
|
||||
when:
|
||||
def opts = new PodOptions(options)
|
||||
then:
|
||||
opts.labels.size() == 2
|
||||
opts.labels == [ALPHA: 'aaa', DELTA: 'ddd']
|
||||
|
||||
}
|
||||
|
||||
def 'should copy pod annotations' (){
|
||||
given:
|
||||
def data = [
|
||||
[annotation: "ANNOTATION", value: 'VALUE']
|
||||
]
|
||||
|
||||
when:
|
||||
def opts = new PodOptions() + new PodOptions(data)
|
||||
then:
|
||||
opts.annotations == ["ANNOTATION": "VALUE"]
|
||||
|
||||
when:
|
||||
opts = new PodOptions(data) + new PodOptions()
|
||||
then:
|
||||
opts.annotations == ["ANNOTATION": "VALUE"]
|
||||
|
||||
when:
|
||||
opts = new PodOptions([[annotation:"FOO", value:'one']]) + new PodOptions([[annotation:"BAR", value:'two']])
|
||||
then:
|
||||
opts.annotations == [FOO: 'one', BAR: 'two']
|
||||
}
|
||||
|
||||
def 'should create pod annotations' () {
|
||||
|
||||
given:
|
||||
def options = [
|
||||
[annotation: 'ALPHA', value: 'aaa'],
|
||||
[annotation: 'DELTA', value: 'bbb'],
|
||||
[annotation: 'DELTA', value: 'ddd']
|
||||
]
|
||||
|
||||
when:
|
||||
def opts = new PodOptions(options)
|
||||
then:
|
||||
opts.annotations.size() == 2
|
||||
opts.annotations == [ALPHA: 'aaa', DELTA: 'ddd']
|
||||
|
||||
}
|
||||
|
||||
def 'should create user security context' () {
|
||||
when:
|
||||
def opts = new PodOptions([ [runAsUser: 1000] ])
|
||||
then:
|
||||
opts.getSecurityContext() == new PodSecurityContext(1000)
|
||||
|
||||
when:
|
||||
opts = new PodOptions([ [runAsUser: 'foo'] ])
|
||||
then:
|
||||
opts.getSecurityContext() == new PodSecurityContext('foo')
|
||||
|
||||
when:
|
||||
opts = new PodOptions([ [runAsUser: 'foo'] ])
|
||||
then:
|
||||
opts.getSecurityContext() != new PodSecurityContext('bar')
|
||||
|
||||
when:
|
||||
def ctx = [runAsUser: 500, fsGroup: 200, allowPrivilegeEscalation: true, seLinuxOptions: [level: "s0:c123,c456"]]
|
||||
def expected = new PodSecurityContext(ctx)
|
||||
opts = new PodOptions([ [securityContext: ctx] ])
|
||||
then:
|
||||
opts.getSecurityContext() == expected
|
||||
opts.getSecurityContext().toSpec() == ctx
|
||||
}
|
||||
|
||||
def 'should create pod node selector' () {
|
||||
when:
|
||||
def opts = new PodOptions([ [nodeSelector: 'foo=1, bar=true, baz=Z'] ])
|
||||
then:
|
||||
opts.nodeSelector.toSpec() == [foo: '1', bar: 'true', baz: 'Z']
|
||||
|
||||
}
|
||||
|
||||
def 'should set pod automount service token' () {
|
||||
when:
|
||||
def opts = new PodOptions([[automountServiceAccountToken: false]])
|
||||
then:
|
||||
opts.getAutomountServiceAccountToken() == false
|
||||
}
|
||||
|
||||
def 'should set pod priority class name' () {
|
||||
when:
|
||||
def opts = new PodOptions([[priorityClassName: 'high-priority']])
|
||||
then:
|
||||
opts.getPriorityClassName() == 'high-priority'
|
||||
}
|
||||
|
||||
def 'should set pod privileged' () {
|
||||
when:
|
||||
def opts = new PodOptions([:])
|
||||
then:
|
||||
!opts.getPrivileged()
|
||||
|
||||
when:
|
||||
opts = new PodOptions([[privileged: true]])
|
||||
then:
|
||||
opts.getPrivileged()
|
||||
}
|
||||
|
||||
def 'should set pod schedulerName' () {
|
||||
when:
|
||||
def opts = new PodOptions()
|
||||
then:
|
||||
opts.getSchedulerName() == null
|
||||
|
||||
when:
|
||||
opts = new PodOptions([ [schedulerName:'my-scheduler'] ])
|
||||
then:
|
||||
opts.getSchedulerName() == 'my-scheduler'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.model
|
||||
|
||||
import nextflow.executor.res.AcceleratorResource
|
||||
import nextflow.util.MemoryUnit
|
||||
import spock.lang.Specification
|
||||
import spock.lang.Unroll
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class PodSpecBuilderTest extends Specification {
|
||||
|
||||
def setup() {
|
||||
PodSpecBuilder.VOLUMES.set(0)
|
||||
}
|
||||
|
||||
|
||||
def 'should create pod spec' () {
|
||||
|
||||
when:
|
||||
def spec = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withWorkDir('/some/work/dir')
|
||||
.withCommand(['echo', 'hello'])
|
||||
.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'
|
||||
]]
|
||||
]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def 'should create pod spec with args' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withArgs(['echo', 'hello'])
|
||||
.build()
|
||||
|
||||
then:
|
||||
pod.spec.containers[0].args == ['echo', 'hello']
|
||||
|
||||
}
|
||||
|
||||
def 'should create pod spec with args string' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withArgs('echo foo')
|
||||
.build()
|
||||
|
||||
then:
|
||||
pod.spec.containers[0].args == ['/bin/bash', '-c', 'echo foo']
|
||||
|
||||
}
|
||||
|
||||
def 'should create pod spec with privileged' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand('echo foo')
|
||||
.withPrivileged(true)
|
||||
.build()
|
||||
|
||||
then:
|
||||
pod.spec.containers[0].securityContext == [privileged: true]
|
||||
|
||||
}
|
||||
|
||||
def 'should create pod with resources limits' () {
|
||||
when:
|
||||
def pod1 = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand('echo foo')
|
||||
.withResourcesLimits('nextflow.io/fuse': 1)
|
||||
.build()
|
||||
|
||||
then:
|
||||
pod1.spec.containers[0].resources == [limits:['nextflow.io/fuse':1]]
|
||||
|
||||
|
||||
when:
|
||||
def pod2 = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand('echo foo')
|
||||
.withCpus(8)
|
||||
.withCpuLimits(true)
|
||||
.withMemory(MemoryUnit.of('10GB'))
|
||||
.withResourcesLimits('nextflow.io/fuse': 1)
|
||||
.build()
|
||||
|
||||
then:
|
||||
pod2.spec.containers[0].resources == [
|
||||
requests: ['cpu':8, 'memory':'10240Mi'],
|
||||
limits: ['cpu':8, 'memory':'10240Mi', 'nextflow.io/fuse':1] ]
|
||||
}
|
||||
|
||||
def 'should set namespace, labels and annotations' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['sh', '-c', 'echo hello'])
|
||||
.withNamespace('xyz')
|
||||
.withLabel('app','myApp')
|
||||
.withLabel('runName','something')
|
||||
.withLabel('version','3.6.1')
|
||||
.withAnnotation("anno1", "value1")
|
||||
.withAnnotations([anno2: "value2", anno3: "value3"])
|
||||
.build()
|
||||
|
||||
then:
|
||||
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 pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['sh', '-c', 'echo hello'])
|
||||
.withLabel('app','myApp')
|
||||
.withLabel('runName','something')
|
||||
.withLabel('tag','somethingreallylonggggggggggggggggggggggggggggggggggggggggggendEXTRABIT')
|
||||
.withLabels([tag2: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggendEXTRABIT', tag3: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggendEXTRABIT'])
|
||||
.build()
|
||||
|
||||
then:
|
||||
pod.metadata.labels == [
|
||||
app: 'myApp',
|
||||
runName: 'something',
|
||||
tag: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend',
|
||||
tag2: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend',
|
||||
tag3: 'somethingreallylonggggggggggggggggggggggggggggggggggggggggggend'
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def 'should set resources and env' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand('echo hello')
|
||||
.withEnv(PodEnv.value('ALPHA','hello'))
|
||||
.withEnv(PodEnv.value('DELTA', 'world'))
|
||||
.withCpus(8)
|
||||
.withAccelerator( new AcceleratorResource(request: 5, limit:10, type: 'foo.org') )
|
||||
.withMemory('100Gi')
|
||||
.withDisk('10Gi')
|
||||
.build()
|
||||
|
||||
then:
|
||||
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 pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withVolumeClaim(new PodVolumeClaim('first','/work'))
|
||||
.withVolumeClaim(new PodVolumeClaim('second', '/data', '/foo'))
|
||||
.withVolumeClaim(new PodVolumeClaim('third', '/things', null, true))
|
||||
.build()
|
||||
then:
|
||||
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']]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def 'should only define one volume per persistentVolumeClaim' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withVolumeClaim(new PodVolumeClaim('first','/work'))
|
||||
.withVolumeClaim(new PodVolumeClaim('first','/work2', '/bar'))
|
||||
.withVolumeClaim(new PodVolumeClaim('second', '/data', '/foo'))
|
||||
.withVolumeClaim(new PodVolumeClaim('second', '/data2', '/fooz'))
|
||||
.build()
|
||||
then:
|
||||
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']]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def 'should get config map mounts' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withConfigMap(new PodMountConfig(config: 'cfg1', mountPath: '/etc/config'))
|
||||
.withConfigMap(new PodMountConfig(config: 'data2', mountPath: '/data/path'))
|
||||
.build()
|
||||
then:
|
||||
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']]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def 'should get csi ephemeral mounts' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withCsiEphemeral(new PodMountCsiEphemeral(csi: [driver: 'inline.storage.kubernetes.io', readOnly: true], mountPath: '/data'))
|
||||
.build()
|
||||
then:
|
||||
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 pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withEmptyDir(new PodMountEmptyDir(mountPath: '/scratch1', emptyDir: [medium: 'Disk']))
|
||||
.withEmptyDir(new PodMountEmptyDir(mountPath: '/scratch2', emptyDir: [medium: 'Memory']))
|
||||
.build()
|
||||
then:
|
||||
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 pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withEnv( PodEnv.value('FOO','abc'))
|
||||
.withEnv( PodEnv.secret('VAR_X', 'delta/bar'))
|
||||
.withEnv( PodEnv.secret('VAR_Y', 'gamma'))
|
||||
.build()
|
||||
|
||||
then:
|
||||
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 pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withEnv( PodEnv.value('FOO','abc'))
|
||||
.withEnv( PodEnv.config('VAR_X', 'data'))
|
||||
.withEnv( PodEnv.config('VAR_Y', 'omega/bar-2'))
|
||||
.build()
|
||||
|
||||
then:
|
||||
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 pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withSecret(new PodMountSecret(secret: 'alpha', mountPath: '/this/and/that'))
|
||||
.withSecret(new PodMountSecret(secret: 'delta/foo', mountPath: '/etc/mnt/bar.txt'))
|
||||
.build()
|
||||
|
||||
then:
|
||||
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 pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo'])
|
||||
.withHostMount('/tmp','/scratch')
|
||||
.withHostMount('/host/data','/mnt/container')
|
||||
.build()
|
||||
|
||||
then:
|
||||
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']]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
|
||||
def 'should return secret file volume and mounts' () {
|
||||
|
||||
given:
|
||||
List mounts
|
||||
List volumes
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
when:
|
||||
def secret1 = new PodMountSecret(secret:'foo', mountPath: '/etc/conf')
|
||||
builder.secretToSpec( 'vol1', secret1, mounts=[], volumes=[] )
|
||||
|
||||
then:
|
||||
mounts == [
|
||||
[ name: 'vol1', mountPath: '/etc/conf']
|
||||
]
|
||||
|
||||
volumes == [
|
||||
[ name: 'vol1', secret: [secretName: 'foo']]
|
||||
]
|
||||
|
||||
|
||||
when:
|
||||
def secret2 = new PodMountSecret(secret:'bar/hello.txt', mountPath: '/etc/conf/world.txt')
|
||||
builder.secretToSpec( 'vol2', secret2, mounts=[], volumes=[] )
|
||||
|
||||
then:
|
||||
mounts == [
|
||||
[ name: 'vol2', mountPath: '/etc/conf']
|
||||
]
|
||||
|
||||
volumes == [
|
||||
[ name: 'vol2', secret: [
|
||||
secretName: 'bar',
|
||||
items: [ [key: 'hello.txt', path:'world.txt'] ]
|
||||
]]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def 'should return configmap file volume and mounts' () {
|
||||
|
||||
given:
|
||||
List mounts
|
||||
List volumes
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
when:
|
||||
def config1 = new PodMountConfig(config:'foo', mountPath: '/etc/conf')
|
||||
builder.configMapToSpec( 'vol1', config1, mounts=[], volumes=[] )
|
||||
|
||||
then:
|
||||
mounts == [
|
||||
[ name: 'vol1', mountPath: '/etc/conf']
|
||||
]
|
||||
|
||||
volumes == [
|
||||
[ name: 'vol1', configMap: [name: 'foo']]
|
||||
]
|
||||
|
||||
|
||||
when:
|
||||
def config2 = new PodMountConfig(config:'bar/hello.txt', mountPath: '/etc/conf/world.txt')
|
||||
builder.configMapToSpec( 'vol2', config2, mounts=[], volumes=[] )
|
||||
|
||||
then:
|
||||
mounts == [
|
||||
[ name: 'vol2', mountPath: '/etc/conf']
|
||||
]
|
||||
|
||||
volumes == [
|
||||
[ name: 'vol2', configMap: [
|
||||
name: 'bar',
|
||||
items: [ [key: 'hello.txt', path:'world.txt'] ]
|
||||
]]
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
|
||||
def 'should create pod spec with pod options' () {
|
||||
|
||||
given:
|
||||
def affinity = [
|
||||
nodeAffinity: [
|
||||
requiredDuringSchedulingIgnoredDuringExecution: [
|
||||
nodeSelectorTerms: [
|
||||
[key: 'foo', operator: 'In', values: ['bar', 'baz']]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
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 pod = builder.withPodOptions(opts).build()
|
||||
then:
|
||||
_ * opts.getAffinity() >> affinity
|
||||
_ * opts.getAnnotations() >> [OMEGA:'zzz', SIGMA:'www']
|
||||
_ * opts.getAutomountServiceAccountToken() >> false
|
||||
2 * opts.getEnvVars() >> [ PodEnv.value('HELLO','WORLD') ]
|
||||
2 * opts.getImagePullPolicy() >> 'always'
|
||||
2 * opts.getImagePullSecret() >> 'myPullSecret'
|
||||
_ * opts.getLabels() >> [ALPHA: 'xxx', GAMMA: 'yyy']
|
||||
2 * opts.getVolumeClaims() >> [ new PodVolumeClaim('pvc1', '/work') ]
|
||||
2 * opts.getMountConfigMaps() >> [ new PodMountConfig('data', '/home/user') ]
|
||||
2 * opts.getMountSecrets() >> [ new PodMountSecret('blah', '/etc/secret.txt') ]
|
||||
_ * opts.getNodeSelector() >> new PodNodeSelector(gpu:true, queue: 'fast')
|
||||
_ * opts.getPriorityClassName() >> 'high-priority'
|
||||
_ * opts.getSecurityContext() >> new PodSecurityContext(1000)
|
||||
_ * opts.getTolerations() >> tolerations
|
||||
and:
|
||||
pod.metadata == [
|
||||
name:'foo',
|
||||
namespace:'default',
|
||||
labels:[runName:'crazy_john', ALPHA:'xxx', GAMMA:'yyy'],
|
||||
annotations: [evict: 'false', OMEGA:'zzz', SIGMA:'www']
|
||||
]
|
||||
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 pod spec with schedulerName' () {
|
||||
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo', 'hello'])
|
||||
.withPodOptions(new PodOptions(schedulerName: 'my-scheduler'))
|
||||
.build()
|
||||
|
||||
then:
|
||||
pod.spec.schedulerName == 'my-scheduler'
|
||||
|
||||
}
|
||||
|
||||
def 'should create image pull request map' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder(imagePullSecret: 'MySecret')
|
||||
when:
|
||||
def result = builder.createPullSecret()
|
||||
then:
|
||||
result.size() == 1
|
||||
result.get(0).name == 'MySecret'
|
||||
}
|
||||
|
||||
|
||||
def 'should return the resources map' () {
|
||||
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
when:
|
||||
def res = builder.addAcceleratorResources(new AcceleratorResource(request:2, limit: 5), null)
|
||||
then:
|
||||
res.requests == ['nvidia.com/gpu': 2]
|
||||
res.limits == ['nvidia.com/gpu': 5]
|
||||
|
||||
when:
|
||||
res = builder.addAcceleratorResources(new AcceleratorResource(limit: 5, type:'foo'), null)
|
||||
then:
|
||||
res.requests == ['foo.com/gpu': 5]
|
||||
res.limits == ['foo.com/gpu': 5]
|
||||
|
||||
when:
|
||||
res = builder.addAcceleratorResources(new AcceleratorResource(request: 5, type:'foo.org'), null)
|
||||
then:
|
||||
res.requests == ['foo.org/gpu': 5]
|
||||
res.limits == null
|
||||
|
||||
when:
|
||||
res = builder.addAcceleratorResources(new AcceleratorResource(request: 5, type: 'foo.org'), [requests: [cpu: 2]])
|
||||
then:
|
||||
res.requests == [cpu: 2, 'foo.org/gpu': 5]
|
||||
res.limits == null
|
||||
|
||||
when:
|
||||
res = builder.addAcceleratorResources(new AcceleratorResource(request: 5, limit: 10, type: 'foo.org'), [requests: [cpu: 2]])
|
||||
then:
|
||||
res.requests == [cpu: 2, 'foo.org/gpu': 5]
|
||||
res.limits == ['foo.org/gpu': 10]
|
||||
|
||||
when:
|
||||
res = builder.addAcceleratorResources(new AcceleratorResource(request: 5, type:'example.com/fpga'), null)
|
||||
then:
|
||||
res.requests == ['example.com/fpga': 5]
|
||||
res.limits == null
|
||||
|
||||
when:
|
||||
res = builder.addAcceleratorResources(new AcceleratorResource(request: 5, limit: 10, type: 'example.com/fpga'), [requests: [cpu: 2]])
|
||||
then:
|
||||
res.requests == [cpu: 2, 'example.com/fpga': 5]
|
||||
res.limits == ['example.com/fpga': 10]
|
||||
}
|
||||
|
||||
def 'should add resources limits' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
Map resources
|
||||
|
||||
when:
|
||||
resources = builder.addResourcesLimits(['foo':1], null)
|
||||
then:
|
||||
resources == [limits:[foo:1]]
|
||||
|
||||
when:
|
||||
resources = builder.addResourcesLimits(['foo':1], [requests: ['x':1], limits: ['y': 2]])
|
||||
then:
|
||||
resources == [requests:[x:1], limits:[y:2, foo:1]]
|
||||
}
|
||||
|
||||
|
||||
@Unroll
|
||||
def 'should sanitize k8s label value: #label' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
expect:
|
||||
builder.sanitizeValue(label, PodSpecBuilder.MetaType.LABEL, PodSpecBuilder.SegmentType.VALUE) == str
|
||||
|
||||
where:
|
||||
label | str
|
||||
null | 'null'
|
||||
'hello' | 'hello'
|
||||
'hello world' | 'hello_world'
|
||||
'hello world' | 'hello_world'
|
||||
'hello.world' | 'hello.world'
|
||||
'hello-world' | 'hello-world'
|
||||
'hello_world' | 'hello_world'
|
||||
'hello_world-' | 'hello_world'
|
||||
'hello_world_' | 'hello_world'
|
||||
'hello_world.' | 'hello_world'
|
||||
'hello_123' | 'hello_123'
|
||||
'HELLO 123' | 'HELLO_123'
|
||||
'123hello' | '123hello'
|
||||
'x2345678901234567890123456789012345678901234567890123456789012345' | 'x23456789012345678901234567890123456789012345678901234567890123'
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should sanitize k8s label key: #label_key' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
expect:
|
||||
builder.sanitizeKey(label_key, PodSpecBuilder.MetaType.LABEL) == str
|
||||
|
||||
where:
|
||||
label_key | str
|
||||
'foo' | 'foo'
|
||||
'key 1' | 'key_1'
|
||||
'foo.bar/key 2' | 'foo.bar/key_2'
|
||||
'foo.bar/' | 'foo.bar'
|
||||
'/foo.bar' | 'foo.bar'
|
||||
'x2345678901234567890123456789012345678901234567890123456789012345' | 'x23456789012345678901234567890123456789012345678901234567890123'
|
||||
'x23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345/key 2' | 'x234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123/key_2'
|
||||
'foo.bar/x2345678901234567890123456789012345678901234567890123456789012345' | 'foo.bar/x23456789012345678901234567890123456789012345678901234567890123'
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should report error if sanitizing k8s label with more than one slash character: #label_key' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
when:
|
||||
builder.sanitizeKey(label_key, PodSpecBuilder.MetaType.LABEL)
|
||||
|
||||
then:
|
||||
def error = thrown(expectedException)
|
||||
error.message == expectedMessage
|
||||
|
||||
where:
|
||||
label_key | expectedException | expectedMessage
|
||||
'foo.bar/key 2/key 3' | IllegalArgumentException | "Invalid key in pod label -- Key can only contain exactly one '/' character"
|
||||
'foo.bar/foo/bar/bar' | IllegalArgumentException | "Invalid key in pod label -- Key can only contain exactly one '/' character"
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should sanitize k8s label map' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
expect:
|
||||
builder.sanitize(KEY_VALUE, PodSpecBuilder.MetaType.LABEL) == EXPECTED
|
||||
|
||||
where:
|
||||
KEY_VALUE | EXPECTED
|
||||
[foo:'bar'] | [foo:'bar']
|
||||
['key 1':'value 2'] | [key_1:'value_2']
|
||||
['foo.bar/key 2':'value 3'] | ['foo.bar/key_2':'value_3']
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should sanitize k8s annotation key' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
expect:
|
||||
builder.sanitize(KEY_VALUE, PodSpecBuilder.MetaType.ANNOTATION) == EXPECTED
|
||||
|
||||
where:
|
||||
KEY_VALUE | EXPECTED
|
||||
[foo:'bar'] | [foo:'bar']
|
||||
['key 1':'value 2'] | [key_1:'value 2']
|
||||
['foo.bar/key 2':'value 3'] | ['foo.bar/key_2':'value 3']
|
||||
['x2345678901234567890123456789012345678901234567890123456789012345':'value 5'] | ['x23456789012345678901234567890123456789012345678901234567890123':'value 5']
|
||||
['x23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345/key 4':'value 6'] | ['x234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123/key_4':'value 6']
|
||||
['foo.bar/x2345678901234567890123456789012345678901234567890123456789012345':'value 7'] | ['foo.bar/x23456789012345678901234567890123456789012345678901234567890123':'value 7']
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should report error if sanitizing k8s annotation key with more than one slash character: #annotation_key' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
when:
|
||||
builder.sanitizeKey(annotation_key, PodSpecBuilder.MetaType.ANNOTATION)
|
||||
|
||||
then:
|
||||
def error = thrown(expectedException)
|
||||
error.message == expectedMessage
|
||||
|
||||
where:
|
||||
annotation_key | expectedException | expectedMessage
|
||||
'foo.bar/key 2/key 3' | IllegalArgumentException | "Invalid key in pod annotation -- Key can only contain exactly one '/' character"
|
||||
'foo.bar/foo/bar/bar' | IllegalArgumentException | "Invalid key in pod annotation -- Key can only contain exactly one '/' character"
|
||||
}
|
||||
|
||||
@Unroll
|
||||
def 'should not sanitize k8s annotation value' () {
|
||||
given:
|
||||
def builder = new PodSpecBuilder()
|
||||
|
||||
expect:
|
||||
builder.sanitize(ANNOTATION, PodSpecBuilder.MetaType.ANNOTATION) == EXPECTED
|
||||
|
||||
where:
|
||||
ANNOTATION | EXPECTED
|
||||
['foo':'value 1'] | ['foo':'value 1']
|
||||
['foo':'foo.bar / *'] | ['foo':'foo.bar / *']
|
||||
['foo':'value 2 \n value 3'] | ['foo':'value 2 \n value 3']
|
||||
['foo':'value 3'] | ['foo':'value 3']
|
||||
['foo':'x2345678901234567890123456789012345678901234567890123456789012345'] | ['foo':'x2345678901234567890123456789012345678901234567890123456789012345']
|
||||
}
|
||||
|
||||
def 'should create job spec' () {
|
||||
|
||||
when:
|
||||
def spec = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo', 'hello'])
|
||||
.buildAsJob()
|
||||
|
||||
then:
|
||||
spec == [
|
||||
apiVersion: 'batch/v1',
|
||||
kind: 'Job',
|
||||
metadata: [name: 'foo', namespace: 'default'],
|
||||
spec: [
|
||||
backoffLimit: 0,
|
||||
template: [
|
||||
metadata: [name: 'foo', namespace: 'default'],
|
||||
spec: [
|
||||
restartPolicy: 'Never',
|
||||
containers: [[
|
||||
name: 'foo',
|
||||
image: 'busybox',
|
||||
command: ['echo', 'hello'],
|
||||
]]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
def 'should create job spec with labels and annotations' () {
|
||||
|
||||
when:
|
||||
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'])
|
||||
.buildAsJob()
|
||||
|
||||
def metadata = [
|
||||
name: 'foo',
|
||||
namespace: 'default',
|
||||
labels: [
|
||||
app: 'someApp',
|
||||
runName: 'someName',
|
||||
version: '3.8.1'
|
||||
],
|
||||
annotations: [
|
||||
anno1: 'val1',
|
||||
anno2: 'val2',
|
||||
anno3: 'val3'
|
||||
]
|
||||
]
|
||||
|
||||
then:
|
||||
job.metadata == metadata
|
||||
job.spec.template.metadata == metadata
|
||||
}
|
||||
|
||||
def 'should create job spec with ttl seconds' () {
|
||||
when:
|
||||
def job = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo', 'hello'])
|
||||
.buildAsJob()
|
||||
then:
|
||||
!job.spec.ttlSecondsAfterFinished
|
||||
|
||||
when:
|
||||
job = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo', 'hello'])
|
||||
.withPodOptions( new PodOptions(ttlSecondsAfterFinished: 60) )
|
||||
.buildAsJob()
|
||||
then:
|
||||
job.spec.ttlSecondsAfterFinished == 60
|
||||
}
|
||||
|
||||
def 'should create pod spec with runtimeClassName' () {
|
||||
when:
|
||||
def pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo', 'hello'])
|
||||
.build()
|
||||
then:
|
||||
!pod.spec.runtimeClassName
|
||||
|
||||
when:
|
||||
pod = new PodSpecBuilder()
|
||||
.withPodName('foo')
|
||||
.withImageName('busybox')
|
||||
.withCommand(['echo', 'hello'])
|
||||
.withPodOptions(new PodOptions(runtimeClassName: 'val1'))
|
||||
.build()
|
||||
then:
|
||||
pod.spec.runtimeClassName == 'val1'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2013-2026, Seqera Labs
|
||||
*
|
||||
* 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 recreationaltech.plugin.model
|
||||
|
||||
import spock.lang.Specification
|
||||
/**
|
||||
*
|
||||
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
|
||||
*/
|
||||
class PodVolumeClaimTest extends Specification {
|
||||
|
||||
|
||||
def 'should create a pod volume claim' () {
|
||||
when:
|
||||
def vol1 = new PodVolumeClaim('foo', '/bar')
|
||||
|
||||
then:
|
||||
vol1.claimName == 'foo'
|
||||
vol1.mountPath == '/bar'
|
||||
vol1.readOnly == false
|
||||
|
||||
when:
|
||||
def vol2 = new PodVolumeClaim(volumeClaim: 'alpha', mountPath: '/gamma')
|
||||
then:
|
||||
vol2.claimName == 'alpha'
|
||||
vol2.mountPath == '/gamma'
|
||||
vol2.readOnly == false
|
||||
|
||||
when:
|
||||
def vol3 = new PodVolumeClaim('aaa', '/bbb', null, true)
|
||||
|
||||
then:
|
||||
vol3.claimName == 'aaa'
|
||||
vol3.mountPath == '/bbb'
|
||||
vol3.readOnly == true
|
||||
|
||||
when:
|
||||
def vol4 = new PodVolumeClaim(volumeClaim: 'ccc', mountPath: '/ddd', readOnly: true)
|
||||
|
||||
then:
|
||||
vol4.claimName == 'ccc'
|
||||
vol4.mountPath == '/ddd'
|
||||
vol4.readOnly == true
|
||||
|
||||
}
|
||||
|
||||
def 'should sanitize paths' () {
|
||||
|
||||
expect :
|
||||
new PodVolumeClaim('foo','/data/work//').mountPath == '/data/work'
|
||||
new PodVolumeClaim('foo','//').mountPath == '/'
|
||||
new PodVolumeClaim('foo','/data').mountPath == '/data'
|
||||
|
||||
when:
|
||||
new PodVolumeClaim('foo','data')
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user