Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API to search build details #544

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
14 changes: 14 additions & 0 deletions src/main/groovy/io/seqera/wave/controller/BuildController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import io.micronaut.http.annotation.QueryValue
import io.micronaut.http.server.types.files.StreamedFile
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import io.seqera.wave.api.BuildStatusResponse
import io.seqera.wave.exception.BadRequestException
import io.seqera.wave.service.builder.ContainerBuildService
import io.seqera.wave.service.builder.model.BuildsResponse
import io.seqera.wave.service.logs.BuildLogService
import io.seqera.wave.service.persistence.WaveBuildRecord
import jakarta.inject.Inject
Expand Down Expand Up @@ -79,4 +82,15 @@ class BuildController {
: HttpResponse.<BuildStatusResponse>notFound()
}

@Get("/v1alpha1/builds")
HttpResponse<BuildsResponse> getBuildRecords(@Nullable @QueryValue String imageName,
@Nullable @QueryValue String user){
if( !imageName && !user )
throw new BadRequestException('Either imageName or user must be specified')

final record = buildService.getBuildRecords(imageName, user)
return record != null
? HttpResponse.ok(new BuildsResponse(record))
: HttpResponse.<BuildsResponse>notFound()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,13 @@ interface ContainerBuildService {
* @return The {@link WaveBuildRecord} associated with the corresponding Id, or {@code null} if it cannot be found
*/
WaveBuildRecord getBuildRecord(String buildId)

/**
* Retrieve the build records for the specified parameters.
*
* @param imageName, The name of the image to be retrieved
* @param user, The user name or email or id
* @return The {@link WaveBuildRecord} associated with the corresponding Id, or {@code null} if it cannot be found
*/
List<WaveBuildRecord> getBuildRecords(String imageName, String user)
}
Original file line number Diff line number Diff line change
Expand Up @@ -375,4 +375,8 @@ class ContainerBuildServiceImpl implements ContainerBuildService {
return buildRecordStore.getBuildRecord(buildId) ?: persistenceService.loadBuild(buildId)
}

@Override
List<WaveBuildRecord> getBuildRecords(String imageName, String user) {
return persistenceService.loadBuilds(imageName, user)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

package io.seqera.wave.service.builder.model

import io.seqera.wave.service.persistence.WaveBuildRecord
/**
* Model a Builds response
*
* @author Munish Chouhan <[email protected]>
*/
class BuildsResponse {

List<WaveBuildRecord> builds

//for testing
BuildsResponse() {}

BuildsResponse(List<WaveBuildRecord> builds) {
this.builds = builds
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,12 @@ interface PersistenceService {
scanRecord.vulnerabilities )
}

/**
* Retrieve a {@link List<WaveBuildRecord>} List of objects matching the specified image name and user
*
* @param imageName The container image name
* @param user The user name or email
* @return The corresponding {@link WaveBuildRecord} object object
*/
List<WaveBuildRecord> loadBuilds(String imageName, String user)
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,9 @@ class LocalPersistenceService implements PersistenceService {
scanStore.get(scanId)
}

@Override
List<WaveBuildRecord> loadBuilds(String imageName, String user) {
return buildStore.findAll { k, v -> v.targetImage == imageName
|| (user!=null && (v.userName == user || v.userEmail == user))}.values().toList().sort((a, b)-> a.buildId <=> b.buildId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,13 @@ class SurrealPersistenceService implements PersistenceService {
return result
}

@Override
List<WaveBuildRecord> loadBuilds(String imageName, String user) {
final query = "select * from wave_build where targetImage = '$imageName' or userEmail = '$user' or userName = '$user' order by buildId"
final json = surrealDb.sqlAsString(getAuthorization(), query)
final type = new TypeReference<ArrayList<SurrealResult<WaveBuildRecord>>>() {}
final data= json ? JacksonHelper.fromJson(json, type) : null
final result = data && data[0].result ? data[0].result.toList() : null
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ import io.seqera.wave.service.builder.BuildEvent
import io.seqera.wave.service.builder.BuildFormat
import io.seqera.wave.service.builder.BuildRequest
import io.seqera.wave.service.builder.BuildResult
import io.seqera.wave.service.builder.model.BuildsResponse
import io.seqera.wave.service.logs.BuildLogService
import io.seqera.wave.service.logs.BuildLogServiceImpl
import io.seqera.wave.service.persistence.PersistenceService
import io.seqera.wave.service.persistence.WaveBuildRecord
import io.seqera.wave.tower.PlatformId
import io.seqera.wave.tower.User
import io.seqera.wave.api.BuildStatusResponse
import io.seqera.wave.util.ContainerHelper
import jakarta.inject.Inject
/**
Expand Down Expand Up @@ -153,4 +156,100 @@ class BuildControllerTest extends Specification {
e.status == HttpStatus.NOT_FOUND
}

def 'should get container build records' () {
given:
final containerFile1 = 'FROM foo:latest'
final format1 = BuildFormat.DOCKER
final platform1 = ContainerPlatform.of('amd64')
final build1 = new BuildRequest(
'containerId1',
containerFile1,
null,
null,
Path.of("/some/path"),
'docker.io/my/repo:container1',
new PlatformId(new User(id: 1, userName: 'foo', email: '[email protected]'), 100),
platform1,
'cacherepo',
"1.2.3.4",
'{"config":"json"}',
null,
null,
'scan12345',
null,
format1,
Duration.ofMinutes(1))
.withBuildId('1')
final result1 = new BuildResult(build1.buildId, -1, "ok", Instant.now(), Duration.ofSeconds(3), null)
final event1 = new BuildEvent(build1, result1)
final entry1 = WaveBuildRecord.fromEvent(event1)

final containerFile2 = 'FROM bar:latest'
final format2 = BuildFormat.DOCKER
final platform2 = ContainerPlatform.of('amd64')
final build2 = new BuildRequest(
'containerId2',
containerFile2,
null,
null,
Path.of("/some/path"),
'docker.io/my/repo:container2',
new PlatformId(new User(id: 1, userName: 'foo', email: '[email protected]'), 100),
platform2,
'cacherepo',
"1.2.3.4",
'{"config":"json"}',
null,
null,
'scan12345',
null,
format2,
Duration.ofMinutes(1))
.withBuildId('2')
final result2 = new BuildResult(build2.buildId, -1, "ok", Instant.now(), Duration.ofSeconds(3), null)
final event2 = new BuildEvent(build2, result2)
final entry2 = WaveBuildRecord.fromEvent(event2)
and:
persistenceService.saveBuild(entry1)
persistenceService.saveBuild(entry2)

when:
def req = HttpRequest.GET("/v1alpha1/builds?imageName=docker.io/my/repo:container2&user=foo")
def res = client.toBlocking().exchange(req, BuildsResponse)

then:
res.body().builds[0].buildId == entry1.buildId
res.body().builds[1].buildId == entry2.buildId

when:
req = HttpRequest.GET("/v1alpha1/builds?imageName=docker.io/my/repo:container1")
res = client.toBlocking().exchange(req, BuildsResponse)

then:
res.body().builds[0].buildId == entry1.buildId

when:
req = HttpRequest.GET("/v1alpha1/builds?imageName=docker.io/my/repo:container2")
res = client.toBlocking().exchange(req, BuildsResponse)

then:
res.body().builds[0].buildId == entry2.buildId

when:
req = HttpRequest.GET("/v1alpha1/builds?user=foo")
res = client.toBlocking().exchange(req, BuildsResponse)

then:
res.body().builds[0].buildId == entry1.buildId
res.body().builds[1].buildId == entry2.buildId

when:
req = HttpRequest.GET("/v1alpha1/[email protected]")
res = client.toBlocking().exchange(req, BuildsResponse)

then:
res.body().builds[0].buildId == entry1.buildId
res.body().builds[1].buildId == entry2.buildId

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import io.seqera.wave.service.persistence.WaveBuildRecord
import io.seqera.wave.test.RedisTestContainer
import io.seqera.wave.test.SurrealDBTestContainer
import io.seqera.wave.tower.PlatformId
import io.seqera.wave.tower.User
import io.seqera.wave.util.Packer
import io.seqera.wave.util.SpackHelper
import io.seqera.wave.util.TemplateRenderer
Expand All @@ -59,7 +60,6 @@ import io.seqera.wave.util.ContainerHelper
*/
@Slf4j
@MicronautTest

class ContainerBuildServiceTest extends Specification implements RedisTestContainer, SurrealDBTestContainer{

@Inject ContainerBuildServiceImpl service
Expand Down Expand Up @@ -717,6 +717,90 @@ class ContainerBuildServiceTest extends Specification implements RedisTestContai
record2.digest == 'abc123'
}

void "should load builds from database"() {
given:
final request1 = new BuildRequest(
'container1',
'test',
'test',
'test',
Path.of("."),
'docker.io/my/repo:container1',
new PlatformId(new User(id: 1, userName: 'foo', email: '[email protected]'), 100),
ContainerPlatform.of('amd64'),
'docker.io/my/cache',
'127.0.0.1',
'{"config":"json"}',
null,
null,
'scan12345',
null,
BuildFormat.DOCKER,
Duration.ofMinutes(1)
).withBuildId('1')

final request2 = new BuildRequest(
'container2',
'test',
'test',
'test',
Path.of("."),
'docker.io/my/repo:container2',
new PlatformId(new User(id: 1, userName: 'foo', email: '[email protected]'), 100),
ContainerPlatform.of('amd64'),
'docker.io/my/cache',
'127.0.0.1',
'{"config":"json"}',
null,
null,
'scan12345',
null,
BuildFormat.DOCKER,
Duration.ofMinutes(1)
).withBuildId('2')

and:
def result1 = new BuildResult(request1.buildId, 0, "content", Instant.now(), Duration.ofSeconds(1), 'abc123')
def event1 = new BuildEvent(request1, result1)
def result2 = new BuildResult(request2.buildId, 0, "content", Instant.now(), Duration.ofSeconds(1), 'abc1234')
def event2 = new BuildEvent(request2, result2)
and:
service.saveBuildRecord(event1)
service.saveBuildRecord(event2)

when:
def record = service.getBuildRecords('docker.io/my/repo:container1', null)

then:
record[0].buildId == request1.buildId
record[0].digest == 'abc123'

when:
record = service.getBuildRecords('docker.io/my/repo:container2', null)

then:
record[0].buildId == request2.buildId
record[0].digest == 'abc1234'

when:
record = service.getBuildRecords('docker.io/my/repo:container1', 'foo')

then:
record[0].buildId == request1.buildId
record[0].digest == 'abc123'
record[1].buildId == request2.buildId
record[1].digest == 'abc1234'

when:
record = service.getBuildRecords(null, '[email protected]')

then:
record[0].buildId == request1.buildId
record[0].digest == 'abc123'
record[1].buildId == request2.buildId
record[1].digest == 'abc1234'
}

def 'should return only the host name' () {
expect:
ContainerInspectServiceImpl.host0(CONTAINER) == EXPECTED
Expand Down
Loading