Skip to content

Commit

Permalink
✨ : add resources to find repositories and tags from docker
Browse files Browse the repository at this point in the history
  • Loading branch information
cdubuisson committed Feb 1, 2020
1 parent fff1259 commit b0e486e
Show file tree
Hide file tree
Showing 9 changed files with 510 additions and 0 deletions.
41 changes: 41 additions & 0 deletions src/main/java/io/codeka/gaia/modules/api/DockerRegistryApi.kt
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 src/main/java/io/codeka/gaia/modules/api/DockerRegistryResponse.kt
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()
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")

}

1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ gaia.dockerDaemonUrl=unix:///var/run/docker.sock

terraform.releases.url=https://releases.hashicorp.com/terraform/
terraform.releases.version.min=0.11.13
docker.registry.api.url=https://registry.hub.docker.com/v2
159 changes: 159 additions & 0 deletions src/test/java/io/codeka/gaia/modules/api/DockerRegistryApiTest.kt
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()
}

}
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()
}

}

0 comments on commit b0e486e

Please sign in to comment.