From 13a59373e73affca74ff8f4a3ab47268cb886cef Mon Sep 17 00:00:00 2001 From: Munish Chouhan Date: Sun, 15 Dec 2024 17:30:36 +0100 Subject: [PATCH] Fix Condalock fetching for cached build layers (#727) Signed-off-by: munishchouhan Signed-off-by: Paolo Di Tommaso Co-authored-by: Paolo Di Tommaso --- .../service/logs/BuildLogServiceImpl.groovy | 40 +++++++++- .../service/logs/BuildLogsServiceTest.groovy | 76 +++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) 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 5fadd46c9..c93d3139a 100644 --- a/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/service/logs/BuildLogServiceImpl.groovy @@ -152,7 +152,17 @@ class BuildLogServiceImpl implements BuildLogService { if( !logs ) return try { String condaLock = extractCondaLockFile(logs) - if (condaLock){ + /* When a container image is cached, dockerfile does not get executed. + In that case condalock file will contain "cat environment.lock" because its not been executed. + So wave will check the previous builds of that container image + and render the condalock file from latest successful build + and replace with the current build's condalock file. + */ + if( condaLock && condaLock.contains('cat environment.lock') ){ + condaLock = fetchValidCondaLock(buildId) + } + + if ( condaLock ){ log.debug "Storing conda lock for buildId: $buildId" final uploadRequest = UploadRequest.fromBytes(condaLock.bytes, condaLockKey(buildId)) objectStorageOperations.upload(uploadRequest) @@ -198,4 +208,32 @@ class BuildLogServiceImpl implements BuildLogService { .replaceAll(/#\d+ \d+\.\d+\s*/, '') } + String fetchValidCondaLock(String buildId) { + try { + final result = fetchValidCondaLock0(buildId) + if( result ) + log.debug "Container Image is already cached for buildId: $buildId - uploading build's condalock file from buildId: $result" + else + log.warn "Container Image is already cached for buildId: $buildId - Unable to find condalock file from previous build" + return result + } + catch (Throwable t) { + log.error "Unable to determine condalock content for buildId: ${buildId} - cause: ${t.message}", t + return null + } + } + + private String fetchValidCondaLock0(String buildId) { + def builds = persistenceService.allBuilds(buildId.split('-')[1].split('_')[0]) + for (def build : builds) { + if ( build.succeeded() ){ + def curCondaLock = fetchCondaLockString(build.buildId) + if( curCondaLock && !curCondaLock.contains('cat environment.lock') ){ + return curCondaLock + } + } + } + return null + } + } diff --git a/src/test/groovy/io/seqera/wave/service/logs/BuildLogsServiceTest.groovy b/src/test/groovy/io/seqera/wave/service/logs/BuildLogsServiceTest.groovy index 0aa1ee3e5..cd7d2bd18 100644 --- a/src/test/groovy/io/seqera/wave/service/logs/BuildLogsServiceTest.groovy +++ b/src/test/groovy/io/seqera/wave/service/logs/BuildLogsServiceTest.groovy @@ -22,13 +22,19 @@ import spock.lang.Specification import spock.lang.Unroll import io.micronaut.objectstorage.InputStreamMapper +import io.micronaut.objectstorage.ObjectStorageOperations import io.micronaut.objectstorage.aws.AwsS3Configuration +import io.micronaut.objectstorage.aws.AwsS3ObjectStorageEntry import io.micronaut.objectstorage.aws.AwsS3Operations +import io.seqera.wave.service.persistence.PersistenceService +import io.seqera.wave.service.persistence.WaveBuildRecord import io.seqera.wave.test.AwsS3TestContainer import software.amazon.awssdk.auth.credentials.AwsBasicCredentials import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.ResponseInputStream import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.GetObjectResponse /** * @@ -167,4 +173,74 @@ class BuildLogsServiceTest extends Specification implements AwsS3TestContainer { noExceptionThrown() } + def 'should return valid conda lock from previous successful build'() { + given: + def persistenceService = Mock(PersistenceService) + def objectStorageOperations = Mock(ObjectStorageOperations) + def service = new BuildLogServiceImpl(persistenceService: persistenceService, objectStorageOperations: objectStorageOperations) + def build1 = Mock(WaveBuildRecord) { + succeeded() >> false + buildId >> 'bd-abc_1' + } + def build2 = Mock(WaveBuildRecord) { + succeeded() >> true + buildId >> 'bd-abc_2' + } + def build3 = Mock(WaveBuildRecord) { + succeeded() >> true + buildId >> 'bd-abc_3' + } + def responseMetadata = GetObjectResponse.builder() + .contentLength(1024L) + .contentType("text/plain") + .build() + def contentStream = new ByteArrayInputStream("valid conda lock".bytes); + def responseInputStream = new ResponseInputStream<>(responseMetadata, contentStream); + persistenceService.allBuilds(_) >> [build1, build2] + objectStorageOperations.retrieve(service.condaLockKey('bd-abc_2')) >> Optional.of(new AwsS3ObjectStorageEntry('bd-abc_2', responseInputStream)) + + expect: + service.fetchValidCondaLock('bd-abc_3') == 'valid conda lock' + } + + def 'should return null when no successful build has valid conda lock'() { + given: + def persistenceService = Mock(PersistenceService) + def objectStorageOperations = Mock(ObjectStorageOperations) + def service = new BuildLogServiceImpl(persistenceService: persistenceService, objectStorageOperations: objectStorageOperations) + def build1 = Mock(WaveBuildRecord) { + succeeded() >> false + buildId >> 'bd-abc_1' + } + def build2 = Mock(WaveBuildRecord) { + succeeded() >> true + buildId >> 'bd-abc_2' + } + def build3 = Mock(WaveBuildRecord) { + succeeded() >> true + buildId >> 'bd-abc_3' + } + def responseMetadata = GetObjectResponse.builder() + .contentLength(1024L) + .contentType("text/plain") + .build() + def contentStream = new ByteArrayInputStream("cat environment.lock".bytes); + def responseInputStream = new ResponseInputStream<>(responseMetadata, contentStream); + persistenceService.allBuilds(_) >> [build1, build2] + objectStorageOperations.retrieve(service.condaLockKey('bd-abc_2')) >> Optional.of(new AwsS3ObjectStorageEntry('bd-abc_2', responseInputStream)) + + expect: + service.fetchValidCondaLock('bd-abc_3') == null + } + + def 'should return null when no builds are available'() { + given: + def persistenceService = Mock(PersistenceService) + def service = new BuildLogServiceImpl(persistenceService: persistenceService) + persistenceService.allBuilds(_) >> [] + + expect: + service.fetchValidCondaLock('bd-abc_1') == null + } + }