-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ : add resources to find repositories and tags from docker
- Loading branch information
1 parent
fff1259
commit b0e486e
Showing
9 changed files
with
510 additions
and
0 deletions.
There are no files selected for viewing
41 changes: 41 additions & 0 deletions
41
src/main/java/io/codeka/gaia/modules/api/DockerRegistryApi.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package io.codeka.gaia.modules.api | ||
|
||
import org.springframework.beans.factory.annotation.Value | ||
import org.springframework.core.ParameterizedTypeReference | ||
import org.springframework.http.HttpEntity | ||
import org.springframework.http.HttpHeaders | ||
import org.springframework.http.HttpMethod | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.stereotype.Repository | ||
import org.springframework.web.client.RestTemplate | ||
|
||
inline fun <reified T : Any> typeRef(): ParameterizedTypeReference<T> = object : ParameterizedTypeReference<T>() {} | ||
|
||
@Repository | ||
class DockerRegistryApi constructor( | ||
@Value("\${docker.registry.api.url}") private val dockerRegistryApiUrl: String, | ||
private val restTemplate: RestTemplate) { | ||
|
||
fun findRepositoriesByName(name: String, pageNum: Int = 1, pageSize: Int = 10): List<DockerRegistryRepository> { | ||
val response = restTemplate.exchange( | ||
"$dockerRegistryApiUrl/search/repositories?query=$name&page=$pageNum&page_size=$pageSize", | ||
HttpMethod.GET, | ||
HttpEntity<Any>(HttpHeaders()), | ||
typeRef<DockerRegistryResponse<DockerRegistryRepository>>()) | ||
return if (HttpStatus.OK == response.statusCode && null != response.body) { | ||
response.body!!.results | ||
} else listOf() | ||
} | ||
|
||
fun findTagsByName(name: String, repository: String, pageNum: Int = 1, pageSize: Int = 10): List<DockerRegistryRepositoryTag> { | ||
val response = restTemplate.exchange( | ||
"$dockerRegistryApiUrl/repositories/$repository/tags?name=$name&page=$pageNum&page_size=$pageSize", | ||
HttpMethod.GET, | ||
HttpEntity<Any>(HttpHeaders()), | ||
typeRef<DockerRegistryResponse<DockerRegistryRepositoryTag>>()) | ||
return if (HttpStatus.OK == response.statusCode && null != response.body) { | ||
response.body!!.results | ||
} else listOf() | ||
} | ||
|
||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/io/codeka/gaia/modules/api/DockerRegistryResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package io.codeka.gaia.modules.api | ||
|
||
import com.fasterxml.jackson.annotation.JsonAlias | ||
|
||
sealed class DockerRegistryResponseResult | ||
|
||
data class DockerRegistryResponse<DockerHubResponseResult>(val results: List<DockerHubResponseResult>) | ||
|
||
data class DockerRegistryRepository( | ||
@JsonAlias("repo_name") val name: String, | ||
@JsonAlias("short_description") val description: String) : DockerRegistryResponseResult() | ||
|
||
data class DockerRegistryRepositoryTag(@JsonAlias("name") val name: String) : DockerRegistryResponseResult() |
25 changes: 25 additions & 0 deletions
25
src/main/java/io/codeka/gaia/modules/controller/DockerRegistryRestController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package io.codeka.gaia.modules.controller | ||
|
||
import io.codeka.gaia.modules.api.DockerRegistryApi | ||
import org.springframework.security.access.annotation.Secured | ||
import org.springframework.web.bind.annotation.* | ||
|
||
@RestController | ||
@RequestMapping("/api/docker") | ||
@Secured | ||
class DockerRegistryRestController(private val dockerRegistryApi: DockerRegistryApi) { | ||
|
||
@GetMapping("/repositories") | ||
fun listRepositoriesByName(@RequestParam name: String) = this.dockerRegistryApi.findRepositoriesByName(name) | ||
|
||
@GetMapping( | ||
"/repositories/{repository}/tags", | ||
"/repositories/{owner}/{repository}/tags") | ||
fun listTagsByName( | ||
@PathVariable repository: String, | ||
@PathVariable(required = false) owner: String?, | ||
@RequestParam name: String | ||
) = this.dockerRegistryApi.findTagsByName(name, "${owner ?: "library"}/$repository") | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
159 changes: 159 additions & 0 deletions
159
src/test/java/io/codeka/gaia/modules/api/DockerRegistryApiTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package io.codeka.gaia.modules.api | ||
|
||
import io.codeka.gaia.registries.controller.whenever | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.BeforeEach | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.extension.ExtendWith | ||
import org.mockito.ArgumentMatchers.* | ||
import org.mockito.Mock | ||
import org.mockito.Mockito.times | ||
import org.mockito.Mockito.verify | ||
import org.mockito.junit.jupiter.MockitoExtension | ||
import org.springframework.http.HttpEntity | ||
import org.springframework.http.HttpMethod | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.http.ResponseEntity | ||
import org.springframework.web.client.RestTemplate | ||
|
||
@ExtendWith(MockitoExtension::class) | ||
class DockerRegistryApiTest { | ||
|
||
lateinit var api: DockerRegistryApi | ||
|
||
@Mock | ||
lateinit var restTemplate: RestTemplate | ||
|
||
@BeforeEach | ||
fun setup() { | ||
api = DockerRegistryApi("test_url", restTemplate) | ||
} | ||
|
||
@Test | ||
fun `findRepositoriesByName() should return a list of repositories matching a name`() { | ||
// given | ||
val repositories = listOf( | ||
DockerRegistryRepository("solo/spices", "drugs"), | ||
DockerRegistryRepository("solo/a280cfe", "blaster rifles")) | ||
val response = ResponseEntity.ok(DockerRegistryResponse(repositories)) | ||
|
||
// when | ||
whenever(restTemplate.exchange( | ||
anyString(), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepository>>()))) | ||
.thenReturn(response) | ||
val result = api.findRepositoriesByName("solo") | ||
|
||
// then | ||
assertThat(result).isNotNull.isNotEmpty.hasSize(2) | ||
verify(restTemplate, times(1)).exchange( | ||
eq("test_url/search/repositories?query=solo&page=1&page_size=10"), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepository>>())) | ||
} | ||
|
||
@Test | ||
fun `findRepositoriesByName() should return an empty list when no match`() { | ||
// given | ||
val response = ResponseEntity.ok<DockerRegistryResponse<DockerRegistryRepository>>(null) | ||
|
||
// when | ||
whenever(restTemplate.exchange( | ||
anyString(), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepository>>()))) | ||
.thenReturn(response) | ||
val result = api.findRepositoriesByName("solo") | ||
|
||
// then | ||
assertThat(result).isNotNull.isEmpty() | ||
} | ||
|
||
@Test | ||
fun `findRepositoriesByName() should return an empty list when response is not ok`() { | ||
// given | ||
val repositories = emptyList<DockerRegistryRepository>() | ||
val response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(DockerRegistryResponse(repositories)) | ||
|
||
// when | ||
whenever(restTemplate.exchange( | ||
anyString(), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepository>>()))) | ||
.thenReturn(response) | ||
val result = api.findRepositoriesByName("solo") | ||
|
||
// then | ||
assertThat(result).isNotNull.isEmpty() | ||
} | ||
|
||
@Test | ||
fun `findTagsByName() should return a list of tags for a repository`() { | ||
// given | ||
val tags = listOf( | ||
DockerRegistryRepositoryTag("sw-4"), | ||
DockerRegistryRepositoryTag("sw-5"), | ||
DockerRegistryRepositoryTag("sw-6")) | ||
val response = ResponseEntity.ok(DockerRegistryResponse(tags)) | ||
|
||
// when | ||
whenever(restTemplate.exchange( | ||
anyString(), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepositoryTag>>()))) | ||
.thenReturn(response) | ||
val result = api.findTagsByName("sw-5", "lucas/original-movies") | ||
|
||
// then | ||
assertThat(result).isNotNull.isNotEmpty.hasSize(3) | ||
verify(restTemplate, times(1)).exchange( | ||
eq("test_url/repositories/lucas/original-movies/tags?name=sw-5&page=1&page_size=10"), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepositoryTag>>())) | ||
} | ||
|
||
@Test | ||
fun `findTagsByName() should return an empty list when no match`() { | ||
// given | ||
val response = ResponseEntity.ok<DockerRegistryResponse<DockerRegistryRepositoryTag>>(null) | ||
|
||
// when | ||
whenever(restTemplate.exchange( | ||
anyString(), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepositoryTag>>()))) | ||
.thenReturn(response) | ||
val result = api.findTagsByName("sw-10", "lucas/original-movies") | ||
|
||
// then | ||
assertThat(result).isNotNull.isEmpty() | ||
} | ||
|
||
@Test | ||
fun `findTagsByName() should return an empty list when response is not ok`() { | ||
// given | ||
val tags = emptyList<DockerRegistryRepositoryTag>() | ||
val response = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(DockerRegistryResponse(tags)) | ||
|
||
// when | ||
whenever(restTemplate.exchange( | ||
anyString(), | ||
eq(HttpMethod.GET), | ||
any<HttpEntity<Any>>(), | ||
eq(typeRef<DockerRegistryResponse<DockerRegistryRepositoryTag>>()))) | ||
.thenReturn(response) | ||
val result = api.findTagsByName("sw-1", "lucas/original-movies") | ||
|
||
// then | ||
assertThat(result).isNotNull.isEmpty() | ||
} | ||
|
||
} |
106 changes: 106 additions & 0 deletions
106
src/test/java/io/codeka/gaia/modules/controller/DockerRegistryRestControllerIT.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package io.codeka.gaia.modules.controller; | ||
|
||
import io.codeka.gaia.test.MongoContainer | ||
import org.hamcrest.Matchers.* | ||
import org.junit.jupiter.api.Test | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient | ||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc | ||
import org.springframework.boot.test.context.SpringBootTest | ||
import org.springframework.core.io.ClassPathResource | ||
import org.springframework.http.MediaType | ||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user | ||
import org.springframework.test.annotation.DirtiesContext | ||
import org.springframework.test.web.client.MockRestServiceServer.bindTo | ||
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo | ||
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess | ||
import org.springframework.test.web.servlet.MockMvc | ||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get | ||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath | ||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status | ||
import org.springframework.web.client.RestTemplate | ||
import org.testcontainers.junit.jupiter.Container | ||
import org.testcontainers.junit.jupiter.Testcontainers | ||
|
||
@SpringBootTest | ||
@DirtiesContext | ||
@Testcontainers | ||
@AutoConfigureWebClient | ||
@AutoConfigureMockMvc | ||
class DockerRegistryRestControllerIT { | ||
|
||
@Autowired | ||
lateinit var mockMvc: MockMvc | ||
|
||
@Autowired | ||
lateinit var restTemplate: RestTemplate | ||
|
||
companion object { | ||
@Container | ||
val mongoContainer = MongoContainer().withScript("src/test/resources/db/10_user.js") | ||
} | ||
|
||
@Test | ||
fun `resource repositories should return the repositories matching the query param name`() { | ||
// given | ||
val server = bindTo(restTemplate).build() | ||
val urlToCall = "https://registry.hub.docker.com/v2/search/repositories?query=terraform&page=1&page_size=10" | ||
server.expect(requestTo(urlToCall)).andRespond(withSuccess( | ||
ClassPathResource("/rest/docker-hub/terraform-repositories.json"), MediaType.APPLICATION_JSON)) | ||
|
||
// when | ||
mockMvc.perform(get("/api/docker/repositories") | ||
.queryParam("name", "terraform") | ||
.with(user("Mary J"))) | ||
.andExpect(status().isOk) | ||
.andExpect(jsonPath("$", hasSize<Any>(3))) | ||
.andExpect(jsonPath("$[0]name", equalTo("hashicorp/terraform"))) | ||
.andExpect(jsonPath("$[0]description", equalTo("official one"))) | ||
.andExpect(jsonPath("$[1]name", equalTo("rogue/terraform"))) | ||
.andExpect(jsonPath("$[1]description", equalTo("rebels one"))) | ||
.andExpect(jsonPath("$[2]name", equalTo("empire/terraform"))) | ||
.andExpect(jsonPath("$[2]description", equalTo("empire one"))) | ||
|
||
// then | ||
server.verify() | ||
} | ||
|
||
@Test | ||
fun `resource tags should return the tags for the repository`() { | ||
// given | ||
val server = bindTo(restTemplate).build() | ||
val urlToCall = "https://registry.hub.docker.com/v2/repositories/hashicorp/terraform/tags?name=latest&page=1&page_size=10" | ||
server.expect(requestTo(urlToCall)).andRespond(withSuccess( | ||
ClassPathResource("/rest/docker-hub/terraform-tags.json"), MediaType.APPLICATION_JSON)) | ||
|
||
// when | ||
mockMvc.perform(get("/api/docker/repositories/hashicorp/terraform/tags?name=latest") | ||
.with(user("Mary J"))) | ||
.andExpect(status().isOk) | ||
.andExpect(jsonPath("$", hasSize<Any>(3))) | ||
.andExpect(jsonPath("$[0]name", equalTo("latest"))) | ||
.andExpect(jsonPath("$[1]name", equalTo("light"))) | ||
.andExpect(jsonPath("$[2]name", equalTo("full"))) | ||
|
||
// then | ||
server.verify() | ||
} | ||
|
||
@Test | ||
fun `resource tags should return the tags for the repository when default owner`() { | ||
// given | ||
val server = bindTo(restTemplate).build() | ||
val urlToCall = "https://registry.hub.docker.com/v2/repositories/library/terraform/tags?name=unknown&page=1&page_size=10" | ||
server.expect(requestTo(urlToCall)).andRespond(withSuccess()) | ||
|
||
// when | ||
mockMvc.perform(get("/api/docker/repositories/terraform/tags?name=unknown") | ||
.with(user("Mary J"))) | ||
.andExpect(status().isOk) | ||
.andExpect(jsonPath("$", empty<Any>())) | ||
|
||
// then | ||
server.verify() | ||
} | ||
|
||
} |
Oops, something went wrong.