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..db3cdf830 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,18 @@ class BuildLogServiceImpl implements BuildLogService { .replaceAll(/#\d+ \d+\.\d+\s*/, '') } + String fetchValidCondaLock(String buildId) { + log.info "Container Image is already cached, uploading previously successful build's condalock file for buildId: $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 + } + }