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 c061c30e5..d41efdd1f 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/BuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/BuildStrategy.groovy @@ -36,7 +36,9 @@ abstract class BuildStrategy { private BuildConfig buildConfig abstract void build(String jobName, BuildRequest req) - + + abstract InputStream getLogs(String buildId) + static final public String BUILDKIT_ENTRYPOINT = 'buildctl-daemonless.sh' List launchCmd(BuildRequest req) { 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 5a20d6e2b..bd534e793 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/DockerBuildStrategy.groovy @@ -156,4 +156,16 @@ class DockerBuildStrategy extends BuildStrategy { wrapper.add(buildConfig.singularityImage(platform)) return wrapper } + + + @Override + InputStream getLogs(String jobName) { + def logCmd = ['docker', 'logs'] + jobName + log.info("Get build logs: ${logCmd.join(' ')}") + final proc = new ProcessBuilder() + .command(logCmd) + .redirectErrorStream(true) + .start() + return proc.inputStream + } } 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 f6ba6e041..084cb0074 100644 --- a/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy +++ b/src/main/groovy/io/seqera/wave/service/builder/KubeBuildStrategy.groovy @@ -98,6 +98,12 @@ class KubeBuildStrategy extends BuildStrategy { } } + @Override + InputStream getLogs(String jobName) { + final pod = k8sService.getLatestPodForJob(jobName) + return k8sService.getCurrentLogsPod(pod.spec.containers.first().name) + } + protected String getBuildImage(BuildRequest buildRequest){ if( buildRequest.formatDocker() ) { return buildConfig.buildkitImage 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 ccaedec07..e72c45e0f 100644 --- a/src/main/groovy/io/seqera/wave/service/k8s/K8sService.groovy +++ b/src/main/groovy/io/seqera/wave/service/k8s/K8sService.groovy @@ -53,6 +53,8 @@ interface K8sService { V1Job launchMirrorJob(String name, String containerImage, List args, Path workDir, Path creds, MirrorConfig config) + InputStream getCurrentLogsPod(String name) + V1Pod getLatestPodForJob(String jobName) } 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 199c788ac..58fbde52b 100644 --- a/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/k8s/K8sServiceImpl.groovy @@ -347,6 +347,29 @@ 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 + InputStream getCurrentLogsPod(String name) { + try { + def logs = k8sClient.coreV1Api() + .readNamespacedPodLog(name, namespace) + .container(name) + .follow(false) + .execute() + logs = logs ? logs.replaceAll("\u001B\\[[;\\d]*m", "") : null // strip ansi escape codes + return new ByteArrayInputStream(logs.getBytes()) + } 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/BuildLogServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy index c93d3139a..ea4e820b3 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 java.util.concurrent.ExecutorService @@ -27,6 +28,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 @@ -35,6 +37,7 @@ import io.micronaut.runtime.event.annotation.EventListener import io.micronaut.scheduling.TaskExecutors 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.persistence.PersistenceService import jakarta.annotation.PostConstruct import jakarta.inject.Inject @@ -64,6 +67,9 @@ class BuildLogServiceImpl implements BuildLogService { @Inject private PersistenceService persistenceService + @Inject + private BuildStrategy buildStrategy + @Nullable @Value('${wave.build.logs.prefix}') private String prefix @@ -129,7 +135,14 @@ 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) { + def logStream = buildStrategy.getLogs(buildId) + if( !logStream ) + return null + return logStream ? new StreamedFile(logStream, MediaType.APPLICATION_OCTET_STREAM_TYPE) : null } @Override diff --git a/src/main/resources/io/seqera/wave/build-view.hbs b/src/main/resources/io/seqera/wave/build-view.hbs index 509546fc0..5aca55a12 100644 --- a/src/main/resources/io/seqera/wave/build-view.hbs +++ b/src/main/resources/io/seqera/wave/build-view.hbs @@ -74,6 +74,21 @@ Container build failed + {{else build_in_progress}} +
+

+ Container build in progress +
+

+ Container build completed successfully! +

+
+ {{else build_failed}} +
+

+ Container build failed +

+
{{else build_in_progress}} {{! build is not completed, show a spinning icon }}