Skip to content

Commit

Permalink
feat(docker registry): endpoint to get tags sorted by date created
Browse files Browse the repository at this point in the history
http://localhost:8081/dockerRegistry/images/tags?account=testregistry&repository=busybox
  • Loading branch information
tomaslin committed Apr 7, 2017
1 parent 82e9aa1 commit 8d8db39
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 17 deletions.
Expand Up @@ -16,6 +16,7 @@

package com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBearerToken
import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBearerTokenService
Expand All @@ -35,6 +36,7 @@ import retrofit.http.*

import java.net.*
import java.io.*
import java.time.Instant
import java.util.concurrent.TimeUnit

@Slf4j
Expand Down Expand Up @@ -184,21 +186,35 @@ class DockerRegistryClient {
}

public String getDigest(String name, String tag) {
def response = request({
registryService.getManifest(name, tag, tokenService.basicAuthHeader, clouddriverUserAgentApplicationName)
}, { token ->
registryService.getManifest(name, tag, token, clouddriverUserAgentApplicationName)
}, name)

def response = getManifest(name, tag)
def headers = response.headers

def digest = headers?.find {
it.name == "Docker-Content-Digest"
}

return digest?.value
}

private Map tagDateCache = [:]

public Instant getCreationDate(String name, String tag) {
String key = "${name}:${tag}"
if(tagDateCache.containsKey(key) && tag !='latest'){
return tagDateCache[key]
}
Map manifest = converter.fromBody(getManifest(name, tag).body, Map)
Instant dateCreated = Instant.parse(new Gson().fromJson(manifest.history[0].v1Compatibility, Map).created)
tagDateCache[key] = dateCreated
dateCreated
}

private getManifest(String name, String tag) {
request({
registryService.getManifest(name, tag, tokenService.basicAuthHeader, clouddriverUserAgentApplicationName)
}, { token ->
registryService.getManifest(name, tag, token, clouddriverUserAgentApplicationName)
}, name)
}

private static String parseLink(retrofit.client.Header header) {
if (!header.name.equalsIgnoreCase("link")) {
return null
Expand Down
Expand Up @@ -45,6 +45,8 @@ class DockerRegistryConfigurationProperties {
int paginateSize
// Track digest changes. This is _not_ recommended as it consumes a high QPM, and most registries are flaky.
boolean trackDigests
// Sort tags by creation date.
boolean sortTagsByDate
// List of all repositories to index. Can be of the form <user>/<repo>,
// or <library> for repositories like 'ubuntu'.
List<String> repositories
Expand Down
Expand Up @@ -24,8 +24,11 @@ import com.netflix.spinnaker.clouddriver.docker.registry.provider.DockerRegistry
import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials
import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
Expand All @@ -37,6 +40,12 @@ class DockerRegistryImageLookupController {
@Autowired
AccountCredentialsProvider accountCredentialsProvider

@RequestMapping(value = "/tags", method = RequestMethod.GET)
List<String> getTags(@RequestParam('account') String account, @RequestParam('repository') String repository) {
def credentials = (DockerRegistryNamedAccountCredentials) accountCredentialsProvider.getCredentials(account)
credentials?.getTags(repository)
}

@RequestMapping(value = '/find', method = RequestMethod.GET)
List<Map> find(LookupOptions lookupOptions) {
def account = lookupOptions.account ?: ""
Expand Down Expand Up @@ -83,11 +92,11 @@ class DockerRegistryImageLookupController {
} else {
def parse = Keys.parse(it.id)
return [
repository: (String) parse.repository,
tag : (String) parse.tag,
account : it.attributes.account,
registry : credentials.registry,
digest : it.attributes.digest,
repository: (String) parse.repository,
tag : (String) parse.tag,
account : it.attributes.account,
registry : credentials.registry,
digest : it.attributes.digest,
]
}
}
Expand Down
Expand Up @@ -23,9 +23,10 @@ class DockerRegistryCredentials {
private List<String> repositories
private final boolean reloadRepositories
private final boolean trackDigests
private final boolean sortTagsByDate
private List<String> skip

DockerRegistryCredentials(DockerRegistryClient client, List<String> repositories, boolean trackDigests, List<String> skip) {
DockerRegistryCredentials(DockerRegistryClient client, List<String> repositories, boolean trackDigests, List<String> skip, boolean sortTagsByDate) {
this.client = client
this.trackDigests = trackDigests
this.skip = skip
Expand All @@ -36,6 +37,7 @@ class DockerRegistryCredentials {
this.reloadRepositories = false
this.repositories = repositories
}
this.sortTagsByDate = sortTagsByDate
}

DockerRegistryClient getClient() {
Expand Down
Expand Up @@ -75,6 +75,7 @@ class DockerRegistryCredentialsInitializer implements CredentialsInitializerSync
.clientTimeoutMillis(managedAccount.clientTimeoutMillis)
.paginateSize(managedAccount.paginateSize)
.trackDigests(managedAccount.trackDigests)
.sortTagsByDate(managedAccount.sortTagsByDate)
.repositories(managedAccount.repositories)
.skip(managedAccount.skip)
.build()
Expand Down
Expand Up @@ -39,6 +39,7 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
long clientTimeoutMillis
int paginateSize
boolean trackDigests
boolean sortTagsByDate
List<String> repositories
List<String> skip

Expand Down Expand Up @@ -119,6 +120,11 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
return this
}

Builder sortTagsByDate(boolean sortTagsByDate) {
this.sortTagsByDate = sortTagsByDate
return this
}

Builder repositories(List<String> repositories) {
this.repositories = repositories
return this
Expand All @@ -144,7 +150,8 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
cacheThreads,
clientTimeoutMillis,
paginateSize,
trackDigests)
trackDigests,
sortTagsByDate)
}
}

Expand All @@ -162,7 +169,8 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
int cacheThreads,
long clientTimeoutMillis,
int paginateSize,
boolean trackDigests) {
boolean trackDigests,
boolean sortTagsByDate) {
this(accountName,
environment,
accountType,
Expand All @@ -178,6 +186,7 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
clientTimeoutMillis,
paginateSize,
trackDigests,
sortTagsByDate,
null)
}

Expand All @@ -196,6 +205,7 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
long clientTimeoutMillis,
int paginateSize,
boolean trackDigests,
boolean sortTagsByDate,
List<String> requiredGroupMembership) {
if (!accountName) {
throw new IllegalArgumentException("Docker Registry account must be provided with a name.")
Expand Down Expand Up @@ -234,6 +244,7 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
this.password = password
this.email = email
this.trackDigests = trackDigests
this.sortTagsByDate = sortTagsByDate
this.skip = skip ?: []
this.requiredGroupMembership = requiredGroupMembership == null ? Collections.emptyList() : Collections.unmodifiableList(requiredGroupMembership)
this.credentials = buildCredentials(repositories)
Expand All @@ -254,6 +265,17 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
return this.credentials?.client?.basicAuth ?: ""
}

@JsonIgnore
List<String> getTags(String repository) {
def tags = credentials.client.getTags(repository).tags
if(sortTagsByDate){
tags = tags.sort{ tag ->
credentials.client.getCreationDate(repository, tag)
}.reverse()
}
tags
}

String getV2Endpoint() {
return "$address/v2"
}
Expand All @@ -275,7 +297,7 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
.paginateSize(paginateSize)
.build()

return new DockerRegistryCredentials(client, repositories, trackDigests, skip)
return new DockerRegistryCredentials(client, repositories, trackDigests, skip, sortTagsByDate)
} catch (RetrofitError e) {
if (e.response?.status == 404) {
throw new DockerRegistryConfigException("No repositories specified for ${name}, and the provided endpoint ${address} does not support /_catalog.")
Expand All @@ -298,6 +320,7 @@ class DockerRegistryNamedAccountCredentials implements AccountCredentials<Docker
final File passwordFile
final String email
final boolean trackDigests
final boolean sortTagsByDate
final int cacheThreads
final long clientTimeoutMillis
final int paginateSize
Expand Down

0 comments on commit 8d8db39

Please sign in to comment.