Skip to content

Commit

Permalink
Add support for binary cache for Spack builds (#249)
Browse files Browse the repository at this point in the history

Signed-off-by: Paolo Di Tommaso <[email protected]>
  • Loading branch information
pditommaso authored Jun 14, 2023
1 parent bebd78f commit b2c66af
Show file tree
Hide file tree
Showing 22 changed files with 829 additions and 32 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.33.4
0.34.0-RC6
93 changes: 93 additions & 0 deletions src/main/groovy/io/seqera/wave/configuration/SpackConfig.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.seqera.wave.configuration

import java.nio.file.Path

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import io.micronaut.context.annotation.Value
import io.micronaut.core.annotation.Nullable
import jakarta.inject.Singleton
/**
* Model Spack configuration
*
* @author Paolo Di Tommaso <[email protected]>
*/
@ToString
@EqualsAndHashCode
@Singleton
@CompileStatic
class SpackConfig {

/**
* The host path where where Spack cached binaries are stored
*/
@Nullable
@Value('${wave.build.spack.cacheDirectory}')
private String cacheDirectory

/**
* The container mount path where Spack cached binaries are stored
*/
@Nullable
@Value('${wave.build.spack.cacheMountPath}')
private String cacheMountPath

/**
* The host path where the GPG key required by the Spack "buildcache" is located
*/
@Nullable
@Value('${wave.build.spack.secretKeyFile}')
private String secretKeyFile

/**
* The container path where the GPG key required by the Spack "buildcache" is located
*/
@Nullable
@Value('${wave.build.spack.secretMountPath}')
private String secretMountPath

/**
* The container image used for Spack builds
*/
@Value('${wave.build.spack.builderImage:`spack/ubuntu-jammy:v0.20.0`}')
private String builderImage

/**
* The container image used for Spack container
*/
@Value('${wave.build.spack.runnerImage:`ubuntu:22.04`}')
private String runnerImage

Path getCacheDirectory() {
if( !cacheDirectory )
throw new IllegalStateException("Missing Spack cacheDirectory configuration setting")
return Path.of(cacheDirectory).toAbsolutePath().normalize()
}

String getCacheMountPath() {
if( !cacheMountPath )
throw new IllegalStateException("Missing Spack cacheMountPath configuration setting")
return cacheMountPath
}

Path getSecretKeyFile() {
if( !secretKeyFile )
throw new IllegalStateException("Missing Spack secretKeyFile configuration setting")
return Path.of(secretKeyFile).toAbsolutePath().normalize()
}

String getSecretMountPath() {
if( !secretMountPath )
throw new IllegalStateException("Missing Spack secretMountPath configuration setting")
return secretMountPath
}

String getBuilderImage() {
return builderImage
}

String getRunnerImage() {
return runnerImage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ import io.seqera.wave.tower.User
import io.seqera.wave.tower.auth.JwtAuthStore
import io.seqera.wave.util.DataTimeUtils
import jakarta.inject.Inject

import static io.seqera.wave.WaveDefault.TOWER

import static io.seqera.wave.util.SpackHelper.prependBuilderTemplate
/**
* Implement a controller to receive container token requests
*
Expand Down Expand Up @@ -177,7 +176,7 @@ class ContainerTokenController {
final offset = DataTimeUtils.offsetId(req.timestamp)
// create a unique digest to identify the request
return new BuildRequest(
dockerContent,
(spackContent ? prependBuilderTemplate(dockerContent) : dockerContent),
Path.of(workspace),
build,
condaContent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class BuildRequest {
*/
final String offsetId

final boolean isSpackBuild

BuildRequest(String containerFile, Path workspace, String repo, String condaFile, String spackFile, User user, ContainerPlatform platform, String configJson, String cacheRepo, String ip, String offsetId = null) {
this.id = computeDigest(containerFile, condaFile, spackFile, platform, repo)
this.dockerFile = containerFile
Expand All @@ -106,6 +108,7 @@ class BuildRequest {
this.startTime = Instant.now()
this.job = "${id}-${startTime.toEpochMilli().toString().md5()[-5..-1]}"
this.ip = ip
this.isSpackBuild = spackFile
}

static private String computeDigest(String containerFile, String condaFile, String spackFile, ContainerPlatform platform, String repository) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import io.micronaut.context.annotation.Value
import io.micronaut.context.event.ApplicationEventPublisher
import io.seqera.wave.auth.RegistryCredentialsProvider
import io.seqera.wave.auth.RegistryLookupService
import io.seqera.wave.configuration.SpackConfig
import io.seqera.wave.ratelimit.AcquireRequest
import io.seqera.wave.ratelimit.RateLimiterService
import io.seqera.wave.util.SpackHelper
import io.seqera.wave.util.TemplateRenderer
import io.seqera.wave.util.ThreadPoolBuilder
import jakarta.inject.Inject
import jakarta.inject.Singleton
Expand Down Expand Up @@ -77,6 +80,9 @@ class ContainerBuildServiceImpl implements ContainerBuildService {
@Inject
private MeterRegistry meterRegistry

@Inject
private SpackConfig spackConfig

@PostConstruct
void init() {
executor = ThreadPoolBuilder.io(10, 10, 100, 'wave-builder')
Expand Down Expand Up @@ -111,15 +117,29 @@ class ContainerBuildServiceImpl implements ContainerBuildService {
.awaitBuild(targetImage)
}

protected String dockerFile0(BuildRequest req, SpackConfig config) {
if( req.isSpackBuild ) {
final binding = new HashMap(2)
binding.spack_builder_image = config.builderImage
binding.spack_runner_image = config.runnerImage
binding.spack_arch = SpackHelper.toSpackArch(req.getPlatform())
binding.spack_cache_dir = config.cacheMountPath
binding.spack_key_file = config.secretMountPath
return new TemplateRenderer().render(req.dockerFile, binding)
}
else
return req.dockerFile
}

protected BuildResult launch(BuildRequest req) {
// launch an external process to build the container
BuildResult resp=null
try {
// create the workdir path
Files.createDirectories(req.workDir)
// save the dockerfile
final dockerfile = req.workDir.resolve('Dockerfile')
Files.write(dockerfile, req.dockerFile.bytes, CREATE, WRITE, TRUNCATE_EXISTING)
final dockerFile = req.workDir.resolve('Dockerfile')
Files.write(dockerFile, dockerFile0(req,spackConfig).bytes, CREATE, WRITE, TRUNCATE_EXISTING)
// save the conda file
if( req.condaFile ) {
final condaFile = req.workDir.resolve('conda.yml')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import groovy.json.JsonOutput
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Value
import io.seqera.wave.configuration.SpackConfig
import io.seqera.wave.core.ContainerPlatform
import jakarta.inject.Inject
import jakarta.inject.Singleton

import static java.nio.file.StandardOpenOption.CREATE
Expand All @@ -35,6 +37,9 @@ class DockerBuildStrategy extends BuildStrategy {
@Value('${wave.debug}')
Boolean debug

@Inject
SpackConfig spackConfig

@Override
BuildResult build(BuildRequest req) {

Expand All @@ -44,7 +49,7 @@ class DockerBuildStrategy extends BuildStrategy {
Files.write(configFile, JsonOutput.prettyPrint(req.configJson).bytes, CREATE, WRITE, TRUNCATE_EXISTING)
}

// comand the docker build command
// command the docker build command
final buildCmd= buildCmd(req, configFile)
log.debug "Build run command: ${buildCmd.join(' ')}"
// save docker cli for debugging purpose
Expand All @@ -66,11 +71,15 @@ class DockerBuildStrategy extends BuildStrategy {
}

protected List<String> buildCmd(BuildRequest req, Path credsFile) {
final dockerCmd = dockerWrapper(req.workDir, credsFile, req.platform)
final dockerCmd = dockerWrapper(
req.workDir,
credsFile,
req.isSpackBuild ? spackConfig : null,
req.platform)
return dockerCmd + launchCmd(req)
}

protected List<String> dockerWrapper(Path workDir, Path credsFile, ContainerPlatform platform) {
protected List<String> dockerWrapper(Path workDir, Path credsFile, SpackConfig spackConfig, ContainerPlatform platform ) {
final wrapper = ['docker',
'run',
'--rm',
Expand All @@ -82,6 +91,15 @@ class DockerBuildStrategy extends BuildStrategy {
wrapper.add("$credsFile:/kaniko/.docker/config.json:ro".toString())
}

if( spackConfig ) {
// secret file
wrapper.add('-v')
wrapper.add("${spackConfig.secretKeyFile}:${spackConfig.secretMountPath}:ro".toString())
// cache directory
wrapper.add('-v')
wrapper.add("${spackConfig.cacheDirectory}:${spackConfig.cacheMountPath}:rw".toString())
}

if( platform ) {
wrapper.add('--platform')
wrapper.add(platform.toString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.micronaut.context.annotation.Primary
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import io.seqera.wave.configuration.SpackConfig
import io.seqera.wave.core.ContainerPlatform
import io.seqera.wave.exception.BadRequestException
import io.seqera.wave.service.k8s.K8sService
Expand Down Expand Up @@ -46,6 +47,9 @@ class KubeBuildStrategy extends BuildStrategy {
@Nullable
private Map<String, String> nodeSelectorMap

@Inject
private SpackConfig spackConfig

private String podName(BuildRequest req) {
return "build-${req.job}"
}
Expand All @@ -62,7 +66,8 @@ class KubeBuildStrategy extends BuildStrategy {
final buildCmd = launchCmd(req)
final name = podName(req)
final selector= getSelectorLabel(req.platform, nodeSelectorMap)
final pod = k8sService.buildContainer(name, buildImage, buildCmd, req.workDir, configFile, selector)
final spackCfg0 = req.isSpackBuild ? spackConfig : null
final pod = k8sService.buildContainer(name, buildImage, buildCmd, req.workDir, configFile, spackCfg0, selector)
final terminated = k8sService.waitPod(pod, buildTimeout.toMillis())
final stdout = k8sService.logsPod(name)
if( terminated ) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/groovy/io/seqera/wave/service/k8s/K8sService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.nio.file.Path
import io.kubernetes.client.openapi.models.V1ContainerStateTerminated
import io.kubernetes.client.openapi.models.V1Job
import io.kubernetes.client.openapi.models.V1Pod
import io.seqera.wave.configuration.SpackConfig

/**
* Defines Kubernetes operations
Expand All @@ -27,7 +28,7 @@ interface K8sService {

void deletePod(String name)

V1Pod buildContainer(String name, String containerImage, List<String> args, Path workDir, Path creds, Map<String,String> nodeSelector)
V1Pod buildContainer(String name, String containerImage, List<String> args, Path workDir, Path creds, SpackConfig spackConfig, Map<String,String> nodeSelector)

V1ContainerStateTerminated waitPod(V1Pod pod, long timeout)
}
37 changes: 33 additions & 4 deletions src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.kubernetes.client.openapi.models.V1VolumeMount
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import io.seqera.wave.configuration.SpackConfig
import io.seqera.wave.core.ContainerPlatform
import jakarta.inject.Inject
import jakarta.inject.Singleton
Expand Down Expand Up @@ -78,6 +79,9 @@ class K8sServiceImpl implements K8sService {
@Nullable
private String requestsMemory

@Inject
private SpackConfig spackConfig

@Inject
private K8sClient k8sClient

Expand Down Expand Up @@ -251,6 +255,27 @@ class K8sServiceImpl implements K8sService {
.readOnly(true)
}

protected V1VolumeMount mountSpackCacheDir(Path spackCacheDir, @Nullable String storageMountPath, String containerPath) {
final rel = Path.of(storageMountPath).relativize(spackCacheDir).toString()
if( !rel || rel.startsWith('../') )
throw new IllegalArgumentException("Spack cacheDirectory '$spackCacheDir' must be a sub-directory of storage path '$storageMountPath'")
return new V1VolumeMount()
.name('build-data')
.mountPath(containerPath)
.subPath(rel)
}

protected V1VolumeMount mountSpackSecretFile(Path secretFile, @Nullable String storageMountPath, String containerPath) {
final rel = Path.of(storageMountPath).relativize(secretFile).toString()
if( !rel || rel.startsWith('../') )
throw new IllegalArgumentException("Spack secretKeyFile '$secretFile' must be a sub-directory of storage path '$storageMountPath'")
return new V1VolumeMount()
.name('build-data')
.readOnly(true)
.mountPath(containerPath)
.subPath(rel)
}

/**
* Create a container for container image building via Kaniko
*
Expand All @@ -269,15 +294,14 @@ class K8sServiceImpl implements K8sService {
*/
@Override
@CompileDynamic
V1Pod buildContainer(String name, String containerImage, List<String> args, Path workDir, Path creds, Map<String,String> nodeSelector) {
final spec = buildSpec(name, containerImage, args, workDir, creds, nodeSelector)
V1Pod buildContainer(String name, String containerImage, List<String> args, Path workDir, Path creds, SpackConfig spackConfig, Map<String,String> nodeSelector) {
final spec = buildSpec(name, containerImage, args, workDir, creds, spackConfig, nodeSelector)
return k8sClient
.coreV1Api()
.createNamespacedPod(namespace, spec, null, null, null)
}

@CompileDynamic
V1Pod buildSpec(String name, String containerImage, List<String> args, Path workDir, Path creds, Map<String,String> nodeSelector) {
V1Pod buildSpec(String name, String containerImage, List<String> args, Path workDir, Path creds, SpackConfig spackConfig, Map<String,String> nodeSelector) {

// required volumes
final mounts = new ArrayList<V1VolumeMount>(5)
Expand All @@ -290,6 +314,11 @@ class K8sServiceImpl implements K8sService {
mounts.add(0, mountDockerConfig(workDir, storageMountPath))
}

if( spackConfig ) {
mounts.add(mountSpackCacheDir(spackConfig.cacheDirectory, storageMountPath, spackConfig.cacheMountPath))
mounts.add(mountSpackSecretFile(spackConfig.secretKeyFile, storageMountPath, spackConfig.secretMountPath))
}

V1PodBuilder builder = new V1PodBuilder()

//metadata section
Expand Down
Loading

0 comments on commit b2c66af

Please sign in to comment.