From ff70a5287854f607f1e892f61e887800bb67bf6e Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Wed, 17 Apr 2024 09:27:53 +0200 Subject: [PATCH 01/25] add getlogs method --- .../wave/service/builder/BuildStrategy.groovy | 2 ++ .../service/builder/DockerBuildStrategy.groovy | 18 +++++++++++++++++- .../service/builder/KubeBuildStrategy.groovy | 13 +++++++++---- .../wave/service/k8s/K8sServiceImpl.groovy | 2 +- .../service/logs/BuildLogServiceImpl.groovy | 14 +++++++++++++- .../FutureContainerBuildServiceTest.groovy | 5 +++++ .../builder/KubeBuildStrategyTest.groovy | 2 +- 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/builder/BuildStrategy.groovy b/src/main/groovy/io/seqera/wave/service/builder/BuildStrategy.groovy index 0d8c7dc5b9..4514a3dafd 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/BuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/BuildStrategy.groovy @@ -37,6 +37,8 @@ abstract class BuildStrategy { abstract BuildResult build(BuildRequest req) + abstract String getLogs(String buildId) + void cleanup(BuildRequest req) { req.workDir?.deleteDir() } diff --git a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy index a4ed1e7455..f40980f3c4 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy @@ -113,7 +113,7 @@ class DockerBuildStrategy extends BuildStrategy { ? cmdForKaniko( req.workDir, credsFile, spack, req.platform) : cmdForSingularity( req.workDir, credsFile, spack, req.platform) - return dockerCmd + launchCmd(req) + return dockerCmd + builderName(req.buildId) +launchCmd(req) } protected List cmdForKaniko(Path workDir, Path credsFile, SpackConfig spackConfig, ContainerPlatform platform ) { @@ -173,4 +173,20 @@ class DockerBuildStrategy extends BuildStrategy { wrapper.add(buildConfig.singularityImage(platform)) return wrapper } + + protected String builderName(String buildId) { + def name = "build-${buildId}".toString().replace('_', '-') + + return ['--name', name] + } + + @Override + String getLogs(String buildId) { + def logCmd = ['docker', 'logs', builderName(buildId)] + final proc = new ProcessBuilder() + .command(logCmd) + .redirectErrorStream(true) + .start() + return proc.inputStream.text + } } diff --git a/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy b/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy index be7fdc1787..7efd842bf4 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy @@ -71,8 +71,8 @@ class KubeBuildStrategy extends BuildStrategy { @Inject private RegistryProxyService proxyService - protected String podName(BuildRequest req) { - return "build-${req.buildId}".toString().replace('_', '-') + protected String podName(String buildId) { + return "build-${buildId}".toString().replace('_', '-') } @Override @@ -96,7 +96,7 @@ class KubeBuildStrategy extends BuildStrategy { try { final buildImage = getBuildImage(req) final buildCmd = launchCmd(req) - final name = podName(req) + final name = podName(req.buildId) final selector= getSelectorLabel(req.platform, nodeSelectorMap) final spackCfg0 = req.isSpackBuild ? spackConfig : null final pod = k8sService.buildContainer(name, buildImage, buildCmd, req.workDir, configFile, spackCfg0, selector) @@ -130,7 +130,7 @@ class KubeBuildStrategy extends BuildStrategy { @Override void cleanup(BuildRequest req) { super.cleanup(req) - final name = podName(req) + final name = podName(req.buildId) try { k8sService.deletePod(name) } @@ -139,4 +139,9 @@ class KubeBuildStrategy extends BuildStrategy { } } + @Override + String getLogs(String buildId) { + return k8sService.logsPod(podName(buildId)) + } + } diff --git a/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy index df64fc33bb..8a16015a3b 100644 --- a/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy @@ -442,7 +442,7 @@ class K8sServiceImpl implements K8sService { } } - /** +/** * Fetch the logs of a pod * * @param name The pod name diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy index f8e0782ad1..f72de7438f 100644 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy @@ -32,6 +32,9 @@ import io.micronaut.objectstorage.request.UploadRequest import io.micronaut.runtime.event.annotation.EventListener import io.seqera.wave.service.builder.BuildEvent import io.seqera.wave.service.builder.BuildRequest +import io.seqera.wave.service.builder.BuildStrategy +import io.seqera.wave.service.builder.KubeBuildStrategy +import io.seqera.wave.service.k8s.K8sService import io.seqera.wave.service.persistence.PersistenceService import jakarta.annotation.PostConstruct import jakarta.inject.Inject @@ -56,6 +59,9 @@ class BuildLogServiceImpl implements BuildLogService { @Inject private PersistenceService persistenceService + @Inject + private BuildStrategy buildStrategy + @Nullable @Value('${wave.build.logs.prefix}') private String prefix @@ -107,7 +113,13 @@ class BuildLogServiceImpl implements BuildLogService { private StreamedFile fetchLogStream0(String buildId) { if( !buildId ) return null final Optional> result = objectStorageOperations.retrieve(logKey(buildId)) - return result.isPresent() ? result.get().toStreamedFile() : null + return result.isPresent() ? result.get().toStreamedFile() : fetchLogStream1(buildId) + } + + private StreamedFile fetchLogStream1(String buildId) { + StreamedFile result = null + buildStrategy.getLogs(buildId) + return result } @Override diff --git a/src/test/groovy/io/seqera/wave/service/builder/FutureContainerBuildServiceTest.groovy b/src/test/groovy/io/seqera/wave/service/builder/FutureContainerBuildServiceTest.groovy index f2a1a40f8e..f685a53302 100644 --- a/src/test/groovy/io/seqera/wave/service/builder/FutureContainerBuildServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/builder/FutureContainerBuildServiceTest.groovy @@ -53,6 +53,11 @@ class FutureContainerBuildServiceTest extends Specification { BuildResult build(BuildRequest req) { new BuildResult("", exitCode, "a fake build result in a test", Instant.now(), Duration.ofSeconds(3), 'abc') } + + @Override + String getLogs(String buildId) { + return "fake build logs" + } } } diff --git a/src/test/groovy/io/seqera/wave/service/builder/KubeBuildStrategyTest.groovy b/src/test/groovy/io/seqera/wave/service/builder/KubeBuildStrategyTest.groovy index 588b2f773a..a9cfbf0d43 100644 --- a/src/test/groovy/io/seqera/wave/service/builder/KubeBuildStrategyTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/builder/KubeBuildStrategyTest.groovy @@ -122,7 +122,7 @@ class KubeBuildStrategyTest extends Specification { req = req.withBuildId('1') when: - def podName = strategy.podName(req) + def podName = strategy.podName(req.buildId) then: req.buildId == '143ee73bcdac45b1_1' From 6f3cd8f8376326c26b7d8a798b764839bb0449e8 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Wed, 17 Apr 2024 21:55:26 +0200 Subject: [PATCH 02/25] fixed builderName --- .../seqera/wave/service/builder/DockerBuildStrategy.groovy | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy index f40980f3c4..296e0b3c6a 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy @@ -174,15 +174,14 @@ class DockerBuildStrategy extends BuildStrategy { return wrapper } - protected String builderName(String buildId) { + protected List builderName(String buildId) { def name = "build-${buildId}".toString().replace('_', '-') - - return ['--name', name] + return List.of('--name', name) } @Override String getLogs(String buildId) { - def logCmd = ['docker', 'logs', builderName(buildId)] + def logCmd = ['docker', 'logs'] + builderName(buildId) final proc = new ProcessBuilder() .command(logCmd) .redirectErrorStream(true) From 8af97ef34b79cabed31eb6d13d66bf4627d87f9d Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Thu, 18 Apr 2024 12:56:26 +0200 Subject: [PATCH 03/25] fixed DockerBuildStrategyTest --- .../builder/DockerBuildStrategy.groovy | 19 +++++++++++++------ .../builder/DockerBuildStrategyTest.groovy | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy index 296e0b3c6a..6f7b7d3154 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy @@ -110,13 +110,13 @@ class DockerBuildStrategy extends BuildStrategy { final spack = req.isSpackBuild ? spackConfig : null final dockerCmd = req.formatDocker() - ? cmdForKaniko( req.workDir, credsFile, spack, req.platform) - : cmdForSingularity( req.workDir, credsFile, spack, req.platform) + ? cmdForKaniko( req.workDir, credsFile, spack, req.platform, req.buildId) + : cmdForSingularity( req.workDir, credsFile, spack, req.platform, req.buildId) - return dockerCmd + builderName(req.buildId) +launchCmd(req) + return dockerCmd + launchCmd(req) } - protected List cmdForKaniko(Path workDir, Path credsFile, SpackConfig spackConfig, ContainerPlatform platform ) { + protected List cmdForKaniko(Path workDir, Path credsFile, SpackConfig spackConfig, ContainerPlatform platform, String buildId) { final wrapper = ['docker', 'run', '--rm', @@ -137,13 +137,17 @@ class DockerBuildStrategy extends BuildStrategy { wrapper.add('--platform') wrapper.add(platform.toString()) } + + //add builder name + wrapper.addAll(builderName(buildId)) + // the container image to be used t wrapper.add( buildConfig.kanikoImage ) // return it return wrapper } - protected List cmdForSingularity(Path workDir, Path credsFile, SpackConfig spackConfig, ContainerPlatform platform) { + protected List cmdForSingularity(Path workDir, Path credsFile, SpackConfig spackConfig, ContainerPlatform platform, String buildId) { final wrapper = ['docker', 'run', '--rm', @@ -170,11 +174,14 @@ class DockerBuildStrategy extends BuildStrategy { wrapper.add(platform.toString()) } + //add builder name + wrapper.addAll(builderName(buildId)) + wrapper.add(buildConfig.singularityImage(platform)) return wrapper } - protected List builderName(String buildId) { + protected static List builderName(String buildId) { def name = "build-${buildId}".toString().replace('_', '-') return List.of('--name', name) } diff --git a/src/test/groovy/io/seqera/wave/service/builder/DockerBuildStrategyTest.groovy b/src/test/groovy/io/seqera/wave/service/builder/DockerBuildStrategyTest.groovy index d9140bd103..4409862ba8 100644 --- a/src/test/groovy/io/seqera/wave/service/builder/DockerBuildStrategyTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/builder/DockerBuildStrategyTest.groovy @@ -45,16 +45,17 @@ class DockerBuildStrategyTest extends Specification { and: def work = Path.of('/work/foo') when: - def cmd = service.cmdForKaniko(work, null, null, null) + def cmd = service.cmdForKaniko(work, null, null, null, '1234') then: cmd == ['docker', 'run', '--rm', '-v', '/work/foo:/work/foo', + '--name', 'build-1234', 'gcr.io/kaniko-project/executor:v1.19.2'] when: - cmd = service.cmdForKaniko(work, Path.of('/foo/creds.json'), null, ContainerPlatform.of('arm64')) + cmd = service.cmdForKaniko(work, Path.of('/foo/creds.json'), null, ContainerPlatform.of('arm64'), '1234') then: cmd == ['docker', 'run', @@ -62,10 +63,11 @@ class DockerBuildStrategyTest extends Specification { '-v', '/work/foo:/work/foo', '-v', '/foo/creds.json:/kaniko/.docker/config.json:ro', '--platform', 'linux/arm64', + '--name', 'build-1234', 'gcr.io/kaniko-project/executor:v1.19.2'] when: - cmd = service.cmdForKaniko(work, Path.of('/foo/creds.json'), spackConfig, null) + cmd = service.cmdForKaniko(work, Path.of('/foo/creds.json'), spackConfig, null, '1234') then: cmd == ['docker', 'run', @@ -73,6 +75,7 @@ class DockerBuildStrategyTest extends Specification { '-v', '/work/foo:/work/foo', '-v', '/foo/creds.json:/kaniko/.docker/config.json:ro', '-v', '/host/spack/key:/opt/spack/key:ro', + '--name', 'build-1234', 'gcr.io/kaniko-project/executor:v1.19.2'] cleanup: @@ -87,6 +90,7 @@ class DockerBuildStrategyTest extends Specification { def creds = Path.of('/work/creds.json') and: def req = new BuildRequest( + buildId: '1234', workDir: Path.of('/work/foo/89fb83ce6ec8627b'), platform: ContainerPlatform.of('linux/amd64'), targetImage: 'repo:89fb83ce6ec8627b', @@ -100,6 +104,7 @@ class DockerBuildStrategyTest extends Specification { '-v', '/work/foo/89fb83ce6ec8627b:/work/foo/89fb83ce6ec8627b', '-v', '/work/creds.json:/kaniko/.docker/config.json:ro', '--platform', 'linux/amd64', + '--name', 'build-1234', 'gcr.io/kaniko-project/executor:v1.19.2', '--dockerfile', '/work/foo/89fb83ce6ec8627b/Containerfile', '--context', '/work/foo/89fb83ce6ec8627b/context', @@ -118,6 +123,7 @@ class DockerBuildStrategyTest extends Specification { def service = ctx.getBean(DockerBuildStrategy) and: def req = new BuildRequest( + buildId: '1234', workDir: Path.of('/work/foo/89fb83ce6ec8627b'), platform: ContainerPlatform.of('linux/amd64'), targetImage: 'repo:89fb83ce6ec8627b', @@ -151,6 +157,7 @@ class DockerBuildStrategyTest extends Specification { def creds = Path.of('/work/creds.json') and: def req = new BuildRequest( + buildId: '1234', workDir: Path.of('/work/foo/d4869cc39b8d7d55'), platform: ContainerPlatform.of('linux/amd64'), targetImage: 'oras://repo:d4869cc39b8d7d55', @@ -170,6 +177,7 @@ class DockerBuildStrategyTest extends Specification { '-v', '/work/singularity-remote.yaml:/root/.singularity/remote.yaml:ro', '-v', '/host/spack/key:/opt/spack/key:ro', '--platform', 'linux/amd64', + '--name', 'build-1234', 'quay.io/singularity/singularity:v3.11.4-slim', 'sh', '-c', @@ -193,6 +201,7 @@ class DockerBuildStrategyTest extends Specification { def creds = Path.of('/work/creds.json') and: def req = new BuildRequest( + buildId: '1234', workDir: Path.of('/work/foo/9c68af894bb2419c'), platform: ContainerPlatform.of('linux/arm64'), targetImage: 'oras://repo:9c68af894bb2419c', @@ -212,6 +221,7 @@ class DockerBuildStrategyTest extends Specification { '-v', '/work/singularity-remote.yaml:/root/.singularity/remote.yaml:ro', '-v', '/host/spack/key:/opt/spack/key:ro', '--platform', 'linux/arm64', + '--name', 'build-1234', 'quay.io/singularity/singularity:v3.11.4-slim-arm64', 'sh', '-c', From d228dd98dd0300ae7acbd004de92b40ac027327c Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Thu, 18 Apr 2024 14:47:49 +0200 Subject: [PATCH 04/25] added BuildLogLocalServiceImpl --- .../builder/DockerBuildStrategy.groovy | 12 +-- .../logs/BuildLogLocalServiceImpl.groovy | 76 +++++++++++++++++++ .../service/logs/BuildLogServiceImpl.groovy | 9 ++- 3 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy diff --git a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy index 6f7b7d3154..94dad11efe 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy @@ -139,7 +139,8 @@ class DockerBuildStrategy extends BuildStrategy { } //add builder name - wrapper.addAll(builderName(buildId)) + wrapper.add('--name') + wrapper.add(builderName(buildId)) // the container image to be used t wrapper.add( buildConfig.kanikoImage ) @@ -175,20 +176,21 @@ class DockerBuildStrategy extends BuildStrategy { } //add builder name - wrapper.addAll(builderName(buildId)) + wrapper.add('--name') + wrapper.add(builderName(buildId)) wrapper.add(buildConfig.singularityImage(platform)) return wrapper } - protected static List builderName(String buildId) { - def name = "build-${buildId}".toString().replace('_', '-') - return List.of('--name', name) + protected static String builderName(String buildId) { + return "build-${buildId}".toString().replace('_', '-') } @Override String getLogs(String buildId) { def logCmd = ['docker', 'logs'] + builderName(buildId) + log.info("Get build logs: ${logCmd.join(' ')}") final proc = new ProcessBuilder() .command(logCmd) .redirectErrorStream(true) diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy new file mode 100644 index 0000000000..f829cd7104 --- /dev/null +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy @@ -0,0 +1,76 @@ +/* + * Wave, containers provisioning service + * Copyright (c) 2024, Seqera Labs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.seqera.wave.service.logs + +import java.nio.charset.StandardCharsets + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import io.micronaut.context.annotation.Requires +import io.micronaut.context.annotation.Value +import io.micronaut.http.MediaType +import io.micronaut.http.server.types.files.StreamedFile +import io.seqera.wave.service.builder.BuildStrategy +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.apache.commons.io.input.BoundedInputStream + +/** + * Implements Service to manage logs from local storage + * + * @author Munish Chouhan + */ +@Slf4j +@Singleton +@CompileStatic +@Requires(missingProperty = 'wave.build.logs.bucket') +class BuildLogLocalServiceImpl implements BuildLogService { + + @Inject + private BuildStrategy buildStrategy + + @Value('${wave.build.logs.maxLength:100000}') + private long maxLength + + Map logStore = new HashMap() + + @Override + void storeLog(String buildId, String log) { + logStore.put(buildId, log) + } + + @Override + StreamedFile fetchLogStream(String buildId) { + String log = logStore.get(buildId) ?: buildStrategy.getLogs(buildId) + if( !log ) + return null + def inputStream = new ByteArrayInputStream(log.getBytes(StandardCharsets.UTF_8)) + def result = inputStream ? new StreamedFile(inputStream, MediaType.TEXT_PLAIN_TYPE) : null + return result + } + + @Override + BuildLog fetchLogString(String buildId) { + final result = fetchLogStream(buildId) + if( !result ) + return null + final logs = new BoundedInputStream(result.getInputStream(), maxLength).getText() + return new BuildLog(logs, logs.length()>=maxLength) + } +} diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy index f72de7438f..a657754f79 100644 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy @@ -18,6 +18,7 @@ package io.seqera.wave.service.logs +import java.nio.charset.StandardCharsets import java.util.concurrent.CompletableFuture import io.micronaut.core.annotation.Nullable @@ -25,6 +26,7 @@ import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import io.micronaut.context.annotation.Requires import io.micronaut.context.annotation.Value +import io.micronaut.http.MediaType import io.micronaut.http.server.types.files.StreamedFile import io.micronaut.objectstorage.ObjectStorageEntry import io.micronaut.objectstorage.ObjectStorageOperations @@ -33,8 +35,6 @@ import io.micronaut.runtime.event.annotation.EventListener import io.seqera.wave.service.builder.BuildEvent import io.seqera.wave.service.builder.BuildRequest import io.seqera.wave.service.builder.BuildStrategy -import io.seqera.wave.service.builder.KubeBuildStrategy -import io.seqera.wave.service.k8s.K8sService import io.seqera.wave.service.persistence.PersistenceService import jakarta.annotation.PostConstruct import jakarta.inject.Inject @@ -117,8 +117,9 @@ class BuildLogServiceImpl implements BuildLogService { } private StreamedFile fetchLogStream1(String buildId) { - StreamedFile result = null - buildStrategy.getLogs(buildId) + final log = buildStrategy.getLogs(buildId) + def inputStream = new ByteArrayInputStream(log.getBytes(StandardCharsets.UTF_8)) + def result = inputStream ? new StreamedFile(inputStream, MediaType.TEXT_PLAIN_TYPE) : null return result } From a5ba7d9da3678c1404cbf907d6be273ab8647d80 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Thu, 18 Apr 2024 17:11:39 +0200 Subject: [PATCH 05/25] added getCurrentLogsPod(String name) --- .../service/builder/KubeBuildStrategy.groovy | 2 +- .../seqera/wave/service/k8s/K8sService.groovy | 2 ++ .../wave/service/k8s/K8sServiceImpl.groovy | 21 ++++++++++++++++++- .../logs/BuildLogLocalServiceImpl.groovy | 13 +++++++++++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy b/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy index 7efd842bf4..c84ffc0c1b 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy @@ -141,7 +141,7 @@ class KubeBuildStrategy extends BuildStrategy { @Override String getLogs(String buildId) { - return k8sService.logsPod(podName(buildId)) + return k8sService.getCurrentLogsPod(podName(buildId)) } } diff --git a/src/main/groovy/io/seqera/wave/service/k8s/K8sService.groovy b/src/main/groovy/io/seqera/wave/service/k8s/K8sService.groovy index cbea2605f3..16285b49e4 100644 --- a/src/main/groovy/io/seqera/wave/service/k8s/K8sService.groovy +++ b/src/main/groovy/io/seqera/wave/service/k8s/K8sService.groovy @@ -54,4 +54,6 @@ interface K8sService { V1Pod transferContainer(String name, String containerImage, List args, BlobCacheConfig blobConfig) V1ContainerStateTerminated waitPod(V1Pod pod, long timeout) + + String getCurrentLogsPod(String name) } diff --git a/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy index 8a16015a3b..5a3299a2b3 100644 --- a/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy @@ -19,6 +19,7 @@ package io.seqera.wave.service.k8s import java.nio.file.Path +import java.util.stream.Collectors import javax.annotation.PostConstruct import groovy.transform.CompileDynamic @@ -35,6 +36,7 @@ import io.kubernetes.client.openapi.models.V1JobBuilder import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimVolumeSource import io.kubernetes.client.openapi.models.V1Pod import io.kubernetes.client.openapi.models.V1PodBuilder +import io.kubernetes.client.openapi.models.V1PodStatus import io.kubernetes.client.openapi.models.V1ResourceRequirements import io.kubernetes.client.openapi.models.V1Volume import io.kubernetes.client.openapi.models.V1VolumeMount @@ -442,7 +444,7 @@ class K8sServiceImpl implements K8sService { } } -/** + /** * Fetch the logs of a pod * * @param name The pod name @@ -460,6 +462,23 @@ class K8sServiceImpl implements K8sService { } } + /** + * Fetch current available logs of a running pod + * + * @param name The pod name + * @return The logs as a string or when logs are not available or cannot be accessed + */ + @Override + String getCurrentLogsPod(String name) { + try { + return k8sClient.coreV1Api().readNamespacedPodLog(name, namespace, null, null, null, null, null, null, null, null, null) + } catch (Exception e) { + // logging trace here because errors are expected when the pod is not running + log.trace "Unable to fetch logs for pod: $name", e + return null + } + } + /** * Delete a pod * diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy index f829cd7104..19f7cba499 100644 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy @@ -19,6 +19,8 @@ package io.seqera.wave.service.logs import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -26,6 +28,8 @@ import io.micronaut.context.annotation.Requires import io.micronaut.context.annotation.Value import io.micronaut.http.MediaType import io.micronaut.http.server.types.files.StreamedFile +import io.micronaut.runtime.event.annotation.EventListener +import io.seqera.wave.service.builder.BuildEvent import io.seqera.wave.service.builder.BuildStrategy import jakarta.inject.Inject import jakarta.inject.Singleton @@ -48,7 +52,14 @@ class BuildLogLocalServiceImpl implements BuildLogService { @Value('${wave.build.logs.maxLength:100000}') private long maxLength - Map logStore = new HashMap() + Map logStore = new ConcurrentHashMap() + + @EventListener + void onBuildEvent(BuildEvent event) { + if(event.result.logs) { + CompletableFuture.supplyAsync(() -> storeLog(event.result.id, event.result.logs)) + } + } @Override void storeLog(String buildId, String log) { From 6cb5e0e725935b03fba54d63b683021a25ed8972 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Thu, 18 Apr 2024 19:37:46 +0200 Subject: [PATCH 06/25] fixed logs color issue --- .../service/logs/BuildLogLocalServiceImpl.groovy | 13 ++++++------- .../wave/service/logs/BuildLogServiceImpl.groovy | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy index 19f7cba499..2b2c9d9a72 100644 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy @@ -34,7 +34,6 @@ import io.seqera.wave.service.builder.BuildStrategy import jakarta.inject.Inject import jakarta.inject.Singleton import org.apache.commons.io.input.BoundedInputStream - /** * Implements Service to manage logs from local storage * @@ -52,7 +51,7 @@ class BuildLogLocalServiceImpl implements BuildLogService { @Value('${wave.build.logs.maxLength:100000}') private long maxLength - Map logStore = new ConcurrentHashMap() + Map logStore = new ConcurrentHashMap() @EventListener void onBuildEvent(BuildEvent event) { @@ -68,12 +67,12 @@ class BuildLogLocalServiceImpl implements BuildLogService { @Override StreamedFile fetchLogStream(String buildId) { - String log = logStore.get(buildId) ?: buildStrategy.getLogs(buildId) - if( !log ) + //replace all regex is removing color from log otherwise it will not be displayed correctly in browser + final logs = logStore.get(buildId) ?: buildStrategy.getLogs(buildId).replaceAll("\u001B\\[[;\\d]*m", "") + if( !logs ) return null - def inputStream = new ByteArrayInputStream(log.getBytes(StandardCharsets.UTF_8)) - def result = inputStream ? new StreamedFile(inputStream, MediaType.TEXT_PLAIN_TYPE) : null - return result + def inputStream = new ByteArrayInputStream(logs.getBytes(StandardCharsets.UTF_8)) + return inputStream ? new StreamedFile(inputStream, MediaType.TEXT_HTML_TYPE) : null } @Override diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy index a657754f79..ae0e23f6a9 100644 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy @@ -117,10 +117,10 @@ class BuildLogServiceImpl implements BuildLogService { } private StreamedFile fetchLogStream1(String buildId) { - final log = buildStrategy.getLogs(buildId) - def inputStream = new ByteArrayInputStream(log.getBytes(StandardCharsets.UTF_8)) - def result = inputStream ? new StreamedFile(inputStream, MediaType.TEXT_PLAIN_TYPE) : null - return result + //replace all regex is removing color from log otherwise it will not be displayed correctly in browser + final logs = buildStrategy.getLogs(buildId).replaceAll("\u001B\\[[;\\d]*m", "") + def inputStream = new ByteArrayInputStream(logs.getBytes(StandardCharsets.UTF_8)) + return inputStream ? new StreamedFile(inputStream, MediaType.TEXT_HTML_TYPE) : null } @Override From eafcb949926bf3580d60fce49c0e537b180ecd38 Mon Sep 17 00:00:00 2001 From: munishchouhan Date: Thu, 18 Apr 2024 22:45:03 +0200 Subject: [PATCH 07/25] added build in progress phase and dynamic logs loading --- .../wave/controller/ViewController.groovy | 2 + .../logs/BuildLogLocalServiceImpl.groovy | 86 ------------------- .../service/logs/BuildLogServiceImpl.groovy | 6 +- .../resources/io/seqera/wave/build-view.hbs | 48 ++++++++++- 4 files changed, 53 insertions(+), 89 deletions(-) delete mode 100644 src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy diff --git a/src/main/groovy/io/seqera/wave/controller/ViewController.groovy b/src/main/groovy/io/seqera/wave/controller/ViewController.groovy index 49aac310da..327e646d1a 100644 --- a/src/main/groovy/io/seqera/wave/controller/ViewController.groovy +++ b/src/main/groovy/io/seqera/wave/controller/ViewController.groovy @@ -71,6 +71,8 @@ class ViewController { final binding = new HashMap(20) binding.build_id = result.buildId binding.build_success = result.succeeded() + binding.build_failed = result.exitStatus && result.exitStatus != 0 + binding.build_in_progress = result.exitStatus == null binding.build_exit_status = result.exitStatus binding.build_user = (result.userName ?: '-') + " (ip: ${result.requestIp})" binding.build_time = formatTimestamp(result.startTime, result.offsetId) ?: '-' diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy deleted file mode 100644 index 2b2c9d9a72..0000000000 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogLocalServiceImpl.groovy +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Wave, containers provisioning service - * Copyright (c) 2024, Seqera Labs - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package io.seqera.wave.service.logs - -import java.nio.charset.StandardCharsets -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap - -import groovy.transform.CompileStatic -import groovy.util.logging.Slf4j -import io.micronaut.context.annotation.Requires -import io.micronaut.context.annotation.Value -import io.micronaut.http.MediaType -import io.micronaut.http.server.types.files.StreamedFile -import io.micronaut.runtime.event.annotation.EventListener -import io.seqera.wave.service.builder.BuildEvent -import io.seqera.wave.service.builder.BuildStrategy -import jakarta.inject.Inject -import jakarta.inject.Singleton -import org.apache.commons.io.input.BoundedInputStream -/** - * Implements Service to manage logs from local storage - * - * @author Munish Chouhan - */ -@Slf4j -@Singleton -@CompileStatic -@Requires(missingProperty = 'wave.build.logs.bucket') -class BuildLogLocalServiceImpl implements BuildLogService { - - @Inject - private BuildStrategy buildStrategy - - @Value('${wave.build.logs.maxLength:100000}') - private long maxLength - - Map logStore = new ConcurrentHashMap() - - @EventListener - void onBuildEvent(BuildEvent event) { - if(event.result.logs) { - CompletableFuture.supplyAsync(() -> storeLog(event.result.id, event.result.logs)) - } - } - - @Override - void storeLog(String buildId, String log) { - logStore.put(buildId, log) - } - - @Override - StreamedFile fetchLogStream(String buildId) { - //replace all regex is removing color from log otherwise it will not be displayed correctly in browser - final logs = logStore.get(buildId) ?: buildStrategy.getLogs(buildId).replaceAll("\u001B\\[[;\\d]*m", "") - if( !logs ) - return null - def inputStream = new ByteArrayInputStream(logs.getBytes(StandardCharsets.UTF_8)) - return inputStream ? new StreamedFile(inputStream, MediaType.TEXT_HTML_TYPE) : null - } - - @Override - BuildLog fetchLogString(String buildId) { - final result = fetchLogStream(buildId) - if( !result ) - return null - final logs = new BoundedInputStream(result.getInputStream(), maxLength).getText() - return new BuildLog(logs, logs.length()>=maxLength) - } -} diff --git a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy index ae0e23f6a9..a1b02f7c4e 100644 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy @@ -118,7 +118,11 @@ class BuildLogServiceImpl implements BuildLogService { private StreamedFile fetchLogStream1(String buildId) { //replace all regex is removing color from log otherwise it will not be displayed correctly in browser - final logs = buildStrategy.getLogs(buildId).replaceAll("\u001B\\[[;\\d]*m", "") + def logs = buildStrategy.getLogs(buildId).replaceAll("\u001B\\[[;\\d]*m", "") + if( !logs ) + return null + else + logs = logs.replaceAll("\u001B\\[[;\\d]*m", "") def inputStream = new ByteArrayInputStream(logs.getBytes(StandardCharsets.UTF_8)) return inputStream ? new StreamedFile(inputStream, MediaType.TEXT_HTML_TYPE) : null } diff --git a/src/main/resources/io/seqera/wave/build-view.hbs b/src/main/resources/io/seqera/wave/build-view.hbs index 5cb33235f9..962433ca6d 100644 --- a/src/main/resources/io/seqera/wave/build-view.hbs +++ b/src/main/resources/io/seqera/wave/build-view.hbs @@ -21,12 +21,18 @@ Container build completed successfully! - {{else}} + {{else build_failed}}

Container build failed

+ {{else build_in_progress}} +
+

+ Container build in progress +

+
{{/if}}

Summary

@@ -98,12 +104,50 @@ {{#if build_log_data}}

Build logs

-
{{build_log_data}}
+
{{build_log_data}}
{{#if build_log_truncated}} Click here to download the complete build log {{/if}} {{/if}} +