diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy
index aa43a4395..67cc240e8 100644
--- a/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy
+++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy
@@ -48,13 +48,18 @@ class RegistryAuth {
return type==Type.Bearer
}
- URI getEndpoint() {
+ URI getEndpoint(String account=null,String repository=null) {
if( !realm )
return null
final uri = realm.toString()
if( uri?.endsWith('.amazonaws.com/') )
return new URI(uri + "v2/")
- return new URI(service ? "$uri?service=${service}".toString() : uri)
+ def query = "service=${service}"
+ if( account )
+ query += "&account=$account"
+ if( repository )
+ query += "&scope=${URLEncoder.encode("repository:$repository:pull",'UTF-8')}"
+ return new URI(service ? "$uri?$query".toString() : uri)
}
static RegistryAuth parse(String auth) {
diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy
index eca481b02..abb5e388b 100644
--- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy
+++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthService.groovy
@@ -28,7 +28,9 @@ interface RegistryAuthService {
/**
* Perform a registry login
*
- * @param registry The registry to login against which e.g. {@code docker.io}
+ * @param registry
+ * The registry to login against which e.g. {@code docker.io} or a container
+ * repository e.g. {@code docker.io/library/ubuntu}
* @param user The registry username
* @param password The registry password or PAT
* @return {@code true} if the login was successful or {@code false} otherwise
@@ -38,7 +40,9 @@ interface RegistryAuthService {
/**
* Check if the provided credentials are valid
*
- * @param registry The registry to check the credentials which e.g. {@code docker.io}
+ * @param registry
+ * The registry to check the credentials which e.g. {@code docker.io} or a container
+ * repository e.g. {@code docker.io/library/ubuntu}
* @param user The registry username
* @param password The registry password or PAT
* @return {@code true} if the login was successful or {@code false} otherwise
diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy
index bdc7fe6da..af142f7cc 100644
--- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy
+++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuthServiceImpl.groovy
@@ -36,6 +36,7 @@ import groovy.transform.ToString
import groovy.util.logging.Slf4j
import io.seqera.wave.configuration.HttpClientConfig
import io.seqera.wave.http.HttpClientFactory
+import io.seqera.wave.model.ContainerCoordinates
import io.seqera.wave.util.RegHelper
import io.seqera.wave.util.Retryable
import io.seqera.wave.util.StringUtils
@@ -114,7 +115,8 @@ class RegistryAuthServiceImpl implements RegistryAuthService {
/**
* Implements container registry login
*
- * @param registryName The registry name e.g. docker.io or quay.io
+ * @param registryName
+ * The registry name e.g. docker.io or quay.io or a repository name
* @param username The registry username
* @param password The registry password
* @return {@code true} if the login was successful or {@code false} otherwise
@@ -125,18 +127,20 @@ class RegistryAuthServiceImpl implements RegistryAuthService {
if( !registryName )
registryName = DOCKER_IO
+ final target = TargetInfo.parse(registryName)
+
// 1. look up the registry authorisation info for the given registry name
- final registry = lookupService.lookup(registryName)
- log.debug "Registry '$registryName' => auth: $registry"
+ final registry = lookupService.lookup(target.registry)
+ log.debug "Registry '$target.registry' => auth: $registry"
// 2. get the registry credentials
// this is needed because some services e.g. AWS ECR requires the use of temporary tokens
- final creds = credentialsFactory.create(registryName, username, password)
+ final creds = credentialsFactory.create(target.registry, username, password)
// 3. make a request against the authorization "realm" service using basic
// credentials to get the login token
final basic = "${creds.username}:${creds.password}".bytes.encodeBase64()
- final endpoint = registry.auth.endpoint
+ final endpoint = registry.auth.getEndpoint()
HttpRequest request = HttpRequest.newBuilder()
.uri(endpoint)
.GET()
@@ -285,10 +289,28 @@ class RegistryAuthServiceImpl implements RegistryAuthService {
tokenStore.remove(getStableKey(key))
}
+ @Canonical
+ static class TargetInfo {
+ String registry
+ String repository
+
+ static TargetInfo parse(String registryOrRepository) {
+ assert registryOrRepository, "Missing 'registryOrRepository' argument"
+ if( registryOrRepository.contains('/') ) {
+ final coords = ContainerCoordinates.parse(registryOrRepository)
+ return new TargetInfo(coords.getRegistry(), coords.getImage())
+ }
+ else {
+ return new TargetInfo(registryOrRepository)
+ }
+ }
+ }
+
/**
* Invalidate all cached authorization tokens
*/
private static String getStableKey(CacheKey key) {
return "key-" + key.stableKey()
}
+
}
diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy
index e422eceed..fecfe0c31 100644
--- a/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy
+++ b/src/main/groovy/io/seqera/wave/auth/RegistryCredentialsProviderImpl.groovy
@@ -21,6 +21,7 @@ package io.seqera.wave.auth
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
+
import io.micronaut.core.annotation.Nullable
import io.seqera.wave.configuration.BuildConfig
import io.seqera.wave.core.ContainerPath
@@ -118,14 +119,13 @@ class RegistryCredentialsProviderImpl implements RegistryCredentialsProvider {
// for a repo having the same registry host
if( container.sameRegistry(buildConfig.defaultBuildRepository) || container.sameRegistry(buildConfig.defaultCacheRepository) || container.sameRegistry(buildConfig.defaultPublicRepository) )
return getDefaultCredentials(container)
-
- return getUserCredentials0(container.registry, identity)
+ return getUserCredentials0(container, identity)
}
- protected RegistryCredentials getUserCredentials0(String registry, PlatformId identity) {
- final keys = credentialsService.findRegistryCreds(registry, identity)
+ protected RegistryCredentials getUserCredentials0(ContainerPath container, PlatformId identity) {
+ final keys = credentialsService.findRegistryCreds(container, identity)
final result = keys
- ? credentialsFactory.create(registry, keys.userName, keys.password)
+ ? credentialsFactory.create(container.registry, keys.userName, keys.password)
// create a missing credentials class with a unique key (the access token) because even when
// no credentials are provided a registry auth token token can be associated to this user
: new MissingCredentials(identity.accessToken)
diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy
index d03a0d39c..1be2a3cc7 100644
--- a/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy
+++ b/src/main/groovy/io/seqera/wave/auth/RegistryLookupService.groovy
@@ -30,7 +30,7 @@ interface RegistryLookupService {
* auth endpoint
*
* @param registry
- * The registry name e.g. {@code docker.io} or {@code quay.io}
+ * The registry name e.g. {@code docker.io} or {@code quay.io}
* @return The corresponding {@link RegistryAuth} object holding the realm URI and service info,
* or {@code null} if nothing is found
*/
diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy
index 02781c8ca..0b20ffb99 100644
--- a/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy
+++ b/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy
@@ -130,7 +130,7 @@ class RegistryLookupServiceImpl implements RegistryLookupService {
* @return the corresponding registry endpoint uri
*/
protected URI registryEndpoint(String registry) {
- def result = registry ?: DOCKER_IO
+ String result = registry ?: DOCKER_IO
if( result==DOCKER_IO )
result = DOCKER_REGISTRY_1
if( !result.startsWith('http://') && !result.startsWith('https://') )
diff --git a/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy b/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy
index 631d0bcc8..adad44f8c 100644
--- a/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy
+++ b/src/main/groovy/io/seqera/wave/controller/ValidateController.groovy
@@ -25,6 +25,7 @@ import io.micronaut.http.annotation.Post
import io.micronaut.scheduling.TaskExecutors
import io.micronaut.scheduling.annotation.ExecuteOn
import io.seqera.wave.auth.RegistryAuthService
+import io.seqera.wave.exchange.ValidateRegistryCredsRequest
import jakarta.inject.Inject
import reactor.core.publisher.Mono
diff --git a/src/main/groovy/io/seqera/wave/controller/ValidateRegistryCredsRequest.groovy b/src/main/groovy/io/seqera/wave/exchange/ValidateRegistryCredsRequest.groovy
similarity index 73%
rename from src/main/groovy/io/seqera/wave/controller/ValidateRegistryCredsRequest.groovy
rename to src/main/groovy/io/seqera/wave/exchange/ValidateRegistryCredsRequest.groovy
index db465ef0a..132d37612 100644
--- a/src/main/groovy/io/seqera/wave/controller/ValidateRegistryCredsRequest.groovy
+++ b/src/main/groovy/io/seqera/wave/exchange/ValidateRegistryCredsRequest.groovy
@@ -16,19 +16,34 @@
* along with this program. If not, see .
*/
-package io.seqera.wave.controller
+package io.seqera.wave.exchange
-import io.micronaut.core.annotation.Nullable
import javax.validation.constraints.NotBlank
import io.micronaut.core.annotation.Introspected
+/**
+ * Request object to valide a container registry credentials
+ */
@Introspected
class ValidateRegistryCredsRequest {
+
+ /**
+ * The registry user name
+ */
@NotBlank
String userName
+
+ /**
+ * The registry password
+ */
@NotBlank
String password
+
+ /**
+ * The registry name e.g. @{code docker.io} or a registry including the repository
+ * name e.g. {@code docker.io/user/repository} (without tag extension)
+ */
@NotBlank
String registry
}
diff --git a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy
index 8af268b0a..351428b6d 100644
--- a/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy
+++ b/src/main/groovy/io/seqera/wave/service/CredentialServiceImpl.groovy
@@ -22,8 +22,10 @@ import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.seqera.tower.crypto.AsymmetricCipher
import io.seqera.tower.crypto.EncryptedPacket
+import io.seqera.wave.core.ContainerPath
import io.seqera.wave.service.aws.AwsEcrService
import io.seqera.wave.service.pairing.PairingService
+import io.seqera.wave.tower.client.CredentialsDescription
import io.seqera.wave.tower.PlatformId
import io.seqera.wave.tower.auth.JwtAuth
import io.seqera.wave.tower.client.CredentialsDescription
@@ -31,6 +33,7 @@ import io.seqera.wave.tower.client.TowerClient
import jakarta.inject.Inject
import jakarta.inject.Singleton
import static io.seqera.wave.WaveDefault.DOCKER_IO
+
/**
* Define operations to access container registry credentials from Tower
*
@@ -48,7 +51,7 @@ class CredentialServiceImpl implements CredentialsService {
private PairingService keyService
@Override
- ContainerRegistryKeys findRegistryCreds(String registryName, PlatformId identity) {
+ ContainerRegistryKeys findRegistryCreds(ContainerPath container, PlatformId identity) {
if (!identity.userId)
throw new IllegalArgumentException("Missing userId parameter")
if (!identity.accessToken)
@@ -77,20 +80,18 @@ class CredentialServiceImpl implements CredentialsService {
// the ones associated with docker.io.
// This cannot be implemented at the moment since, in tower, container registry
// credentials are associated to the whole registry
- final matchingRegistryName = registryName ?: DOCKER_IO
- def creds = all.find {
- it.provider == 'container-reg' && (it.registry ?: DOCKER_IO) == matchingRegistryName
- }
- if (!creds && identity.workflowId && AwsEcrService.isEcrHost(registryName) ) {
+ final repo = container.repository ?: DOCKER_IO
+ def creds = findBestMatchingCreds(repo, all)
+ if (!creds && identity.workflowId && AwsEcrService.isEcrHost(container.registry) ) {
creds = findComputeCreds(identity)
}
if (!creds) {
- log.debug "No credentials matching criteria registryName=$registryName; userId=$identity.userId; workspaceId=$identity.workspaceId; workflowId=${identity.workflowId}; endpoint=$identity.towerEndpoint"
+ log.debug "No credentials matching criteria registryName=$container.registry; userId=$identity.userId; workspaceId=$identity.workspaceId; workflowId=${identity.workflowId}; endpoint=$identity.towerEndpoint"
return null
}
// log for debugging purposes
- log.debug "Credentials matching criteria registryName=$registryName; userId=$identity.userId; workspaceId=$identity.workspaceId; endpoint=$identity.towerEndpoint => $creds"
+ log.debug "Credentials matching criteria registryName=$container.registry; userId=$identity.userId; workspaceId=$identity.workspaceId; endpoint=$identity.towerEndpoint => $creds"
// now fetch the encrypted key
final encryptedCredentials = towerClient.fetchEncryptedCredentials(identity.towerEndpoint, JwtAuth.of(identity), creds.id, pairing.pairingId, identity.workspaceId).get()
final privateKey = pairing.privateKey
@@ -98,6 +99,54 @@ class CredentialServiceImpl implements CredentialsService {
return parsePayload(credentials)
}
+ protected CredentialsDescription findBestMatchingCreds(String target, List all) {
+ assert target, "Missing 'target' container repository"
+ // take all container registry credentials
+ final creds = all
+ .findAll(it-> it.provider=='container-reg' )
+
+ // try to find an exact match
+ final match = creds.find(it-> it.registry==target )
+ if( match )
+ return match
+
+ // find the longest matching repository
+ creds.inject((CredentialsDescription)null) { best, it-> matchingLongest(target,best,it)}
+ }
+
+ protected CredentialsDescription matchingLongest(String target, CredentialsDescription best, CredentialsDescription candidate) {
+ final a = best ? matchingScore(target, best.registry) : 0
+ final b = matchingScore(target, candidate.registry)
+ return a >= b ? best : candidate
+ }
+
+ /**
+ * Return the longest matching path length of two container repositories
+ *
+ * @param target The target repository to be authenticated
+ * @param authority The authority repository against which the target repository should be authenticated
+ * @return An integer greater or equals to zero representing the long the path in the two repositories
+ */
+ protected int matchingScore(String target, String authority) {
+ if( !authority )
+ return 0
+ if( !authority.contains('/') && !authority.endsWith('/*') )
+ authority += '/*'
+ if( authority.endsWith('/*') ) {
+ final len = authority.length()-2
+ if( target.startsWith(authority.substring(0,len)) )
+ return len
+ }
+ else if( target==authority ) {
+ return target.length()
+ }
+ return 0
+ }
+
+ protected int repoLen(String repo) {
+ repo ? repo.tokenize('/').size() : 0
+ }
+
CredentialsDescription findComputeCreds(PlatformId identity) {
try {
return findComputeCreds0(identity)
diff --git a/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy b/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy
index 7712c98fc..2515989fa 100644
--- a/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy
+++ b/src/main/groovy/io/seqera/wave/service/CredentialsService.groovy
@@ -18,8 +18,8 @@
package io.seqera.wave.service
+import io.seqera.wave.core.ContainerPath
import io.seqera.wave.tower.PlatformId
-
/**
* Declare operations to access container registry credentials from Tower
*
@@ -27,6 +27,21 @@ import io.seqera.wave.tower.PlatformId
*/
interface CredentialsService {
- ContainerRegistryKeys findRegistryCreds(String registryName, PlatformId identity)
-
+ /**
+ *
+ * @param container
+ * The container for which the registry credentials should be retrieved
+ * @param userId
+ * The unique ID of the Tower user
+ * @param workspaceId
+ * The unique ID of the Tower workspace
+ * @param towerToken
+ * The Tower access token
+ * @param towerEndpoint
+ * The Tower endpoint
+ * @return
+ * The container registry credentials to be used to authenticate the specified container registry or repository
+ * or {@code null} if no match is found
+ */
+ ContainerRegistryKeys findRegistryCreds(ContainerPath container, PlatformId identity)
}
diff --git a/src/main/groovy/io/seqera/wave/tower/client/CredentialsDescription.groovy b/src/main/groovy/io/seqera/wave/tower/client/CredentialsDescription.groovy
index 175ed645c..3f866512f 100644
--- a/src/main/groovy/io/seqera/wave/tower/client/CredentialsDescription.groovy
+++ b/src/main/groovy/io/seqera/wave/tower/client/CredentialsDescription.groovy
@@ -27,8 +27,21 @@ import io.seqera.wave.WaveDefault
@ToString(includePackage = false, includeNames = true)
class CredentialsDescription {
+ /**
+ * The credential record unique ID
+ */
String id
+
+ /**
+ * The credentials provider as defined in tower. This must be {@code container-reg} for container registry
+ * credentials
+ */
String provider
+
+ /**
+ * The target container registry e.g. {@code docker.io}, {@code quay.io}. Note this can also specify a full or
+ * partial container repository name e.g. {@code docker.io/user/foo}
+ */
String registry
@JsonProperty("keys")
diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy
index 7cbb097e7..9b166f1b1 100644
--- a/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy
+++ b/src/test/groovy/io/seqera/wave/auth/RegistryAuthServiceTest.groovy
@@ -77,67 +77,72 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis
void 'test valid login'() {
given:
- String uri = getTestRegistryUrl(REGISTRY_URL)
+ String registry = getTestRegistryName(REGISTRY)
when:
- boolean logged = loginService.login(uri, USER, PWD)
+ boolean logged = loginService.login(registry, USER, PWD)
then:
logged == VALID
where:
- USER | PWD | REGISTRY_URL | VALID
- 'test' | 'test' | 'localhost' | true
- 'nope' | 'yepes' | 'localhost' | false
- dockerUsername | dockerPassword | "https://registry-1.docker.io" | true
- 'nope' | 'yepes' | "https://registry-1.docker.io" | false
- quayUsername | quayPassword | "https://quay.io" | true
- 'nope' | 'yepes' | "https://quay.io" | false
+ USER | PWD | REGISTRY | VALID
+ 'test' | 'test' | 'localhost' | true
+ 'nope' | 'yepes' | 'localhost' | false
+ dockerUsername | dockerPassword | "docker.io" | true
+ 'nope' | 'yepes' | "docker.io" | false
+ quayUsername | quayPassword | "quay.io" | true
+ 'nope' | 'yepes' | "quay.io" | false
}
@IgnoreIf({!System.getenv('AZURECR_USER')})
void 'test valid azure login'() {
given:
- def REGISTRY_URL = 'seqeralabs.azurecr.io'
+ def registry = 'seqeralabs.azurecr.io'
expect:
- loginService.login(REGISTRY_URL, azureUsername, azurePassword)
+ loginService.login(registry, azureUsername, azurePassword)
}
@IgnoreIf({!System.getenv('AWS_ACCESS_KEY_ID')})
void 'test valid aws ecr private'() {
given:
- String REGISTRY_URL = '195996028523.dkr.ecr.eu-west-1.amazonaws.com'
+ String registry = '195996028523.dkr.ecr.eu-west-1.amazonaws.com'
expect:
- loginService.login(REGISTRY_URL, awsEcrUsername, awsEcrPassword)
+ loginService.login(registry, awsEcrUsername, awsEcrPassword)
}
@IgnoreIf({!System.getenv('AWS_ACCESS_KEY_ID')})
void 'test valid aws ecr public'() {
given:
- String REGISTRY_URL = 'public.ecr.aws'
+ String registry = 'public.ecr.aws'
expect:
- loginService.login(REGISTRY_URL, awsEcrUsername, awsEcrPassword)
+ loginService.login(registry, awsEcrUsername, awsEcrPassword)
}
void 'test containerService valid login'() {
given:
- String uri = getTestRegistryUrl(REGISTRY_URL)
+ String registry = getTestRegistryName(REGISTRY)
when:
- boolean logged = loginService.validateUser(uri, USER, PWD)
+ boolean logged = loginService.validateUser(registry, USER, PWD)
then:
logged == VALID
where:
- USER | PWD | REGISTRY_URL | VALID
- 'test' | 'test' | 'localhost' | true
- 'nope' | 'yepes' | 'localhost' | false
- dockerUsername | dockerPassword | "https://registry-1.docker.io" | true
- 'nope' | 'yepes' | "https://registry-1.docker.io" | false
- quayUsername | quayPassword | "https://quay.io" | true
- 'nope' | 'yepes' | "https://quay.io" | false
+ USER | PWD | REGISTRY | VALID
+ 'test' | 'test' | 'localhost' | true
+ 'nope' | 'yepes' | 'localhost' | false
+ dockerUsername | dockerPassword | "docker.io" | true
+ 'nope' | 'yepes' | "docker.io" | false
+ quayUsername | quayPassword | "quay.io" | true
+ 'nope' | 'yepes' | "quay.io" | false
+ and:
+ dockerUsername | dockerPassword | "docker.io/pditommaso/wave-tests" | true
+ dockerUsername | dockerPassword | "docker.io/pditommaso" | true
+ // the following should fail
+ //dockerUsername | dockerPassword | "registry-1.docker.io/unknown/repo" | false
}
@Ignore
@@ -199,7 +204,7 @@ class RegistryAuthServiceTest extends Specification implements SecureDockerRegis
def c5 = new RegistryAuthServiceImpl.CacheKey(i1, a1, k3)
expect:
- c1.stableKey() == '23476a51c7b6216a'
+ c1.stableKey() == 'c234dc4c210c6612'
c1.stableKey() == c2.stableKey()
c1.stableKey() == c3.stableKey()
and:
diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy
index 2c34a9ce8..4a165c9de 100644
--- a/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy
+++ b/src/test/groovy/io/seqera/wave/auth/RegistryCredentialsProviderTest.groovy
@@ -25,6 +25,7 @@ import spock.lang.Specification
import io.micronaut.context.annotation.Value
import io.micronaut.core.annotation.Nullable
import io.micronaut.test.extensions.spock.annotation.MicronautTest
+import io.seqera.wave.model.ContainerCoordinates
import io.seqera.wave.service.ContainerRegistryKeys
import io.seqera.wave.service.CredentialsService
import io.seqera.wave.service.aws.AwsEcrService
@@ -101,7 +102,7 @@ class RegistryCredentialsProviderTest extends Specification {
def 'should get credentials from user' () {
given:
- def REGISTRY = 'foo'
+ def CONTAINER = ContainerCoordinates.parse('docker.io/foo')
def USER_ID = 100
def WORKSPACE_ID = 200
def TOWER_TOKEN = "token"
@@ -114,9 +115,10 @@ class RegistryCredentialsProviderTest extends Specification {
and:
def identity = new PlatformId(new User(id:USER_ID), WORKSPACE_ID, TOWER_TOKEN, TOWER_ENDPOINT)
when:
- def result = provider.getUserCredentials0(REGISTRY, identity)
+ def result = provider.getUserCredentials0(CONTAINER, identity)
+
then:
- 1 * credentialService.findRegistryCreds(REGISTRY, identity) >> new ContainerRegistryKeys(userName:'usr1',password:'pwd2',registry:REGISTRY)
+ 1 * credentialService.findRegistryCreds(CONTAINER, identity) >> new ContainerRegistryKeys(userName:'usr1',password:'pwd2',registry:CONTAINER)
and:
result.getUsername() == 'usr1'
result.getPassword() == 'pwd2'
diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryLookupServiceTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryLookupServiceTest.groovy
index 65f4525da..3805c8c93 100644
--- a/src/test/groovy/io/seqera/wave/auth/RegistryLookupServiceTest.groovy
+++ b/src/test/groovy/io/seqera/wave/auth/RegistryLookupServiceTest.groovy
@@ -73,6 +73,8 @@ class RegistryLookupServiceTest extends Specification {
'http://foo.com' | 'http://foo.com/v2/'
'http://foo.com/v2' | 'http://foo.com/v2/'
'http://foo.com/v2/'| 'http://foo.com/v2/'
+ 'localhost' | 'http://localhost/v2/'
+ 'localhost:8000' | 'http://localhost:8000/v2/'
}
}
diff --git a/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy b/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy
index 5f3cad31f..85d28088a 100644
--- a/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy
+++ b/src/test/groovy/io/seqera/wave/controller/ValidateCredsControllerTest.groovy
@@ -90,7 +90,7 @@ class ValidateCredsControllerTest extends Specification implements SecureDockerR
def req = [
userName:'test',
password:'test',
- registry: getTestRegistryUrl('test') ]
+ registry: getTestRegistryName('test') ]
and:
HttpRequest request = HttpRequest.POST("/validate-creds", req)
when:
@@ -106,7 +106,7 @@ class ValidateCredsControllerTest extends Specification implements SecureDockerR
def req = [
userName: USER,
password: PWD,
- registry: getTestRegistryUrl(REGISTRY_URL)
+ registry: getTestRegistryName(REGISTRY)
]
HttpRequest request = HttpRequest.POST("/validate-creds", req)
when:
@@ -118,13 +118,13 @@ class ValidateCredsControllerTest extends Specification implements SecureDockerR
response.body() == VALID
where:
- USER | PWD | REGISTRY_URL | VALID
- 'test' | 'test' | 'test' | true
- 'nope' | 'yepes' | 'test' | false
- dockerUsername | dockerPassword | "https://registry-1.docker.io" | true
- 'nope' | 'yepes' | "https://registry-1.docker.io" | false
- quayUsername | quayPassword | "https://quay.io" | true
- 'nope' | 'yepes' | "https://quay.io" | false
- 'test' | 'test' | 'test' | true
+ USER | PWD | REGISTRY | VALID
+ 'test' | 'test' | 'test' | true
+ 'nope' | 'yepes' | 'test' | false
+ dockerUsername | dockerPassword | "registry-1.docker.io" | true
+ 'nope' | 'yepes' | "registry-1.docker.io" | false
+ quayUsername | quayPassword | "quay.io" | true
+ 'nope' | 'yepes' | "quay.io" | false
+ 'test' | 'test' | 'test' | true
}
}
diff --git a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy
index a2babb87d..e3ddba736 100644
--- a/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy
+++ b/src/test/groovy/io/seqera/wave/service/CredentialsServiceTest.groovy
@@ -19,6 +19,7 @@
package io.seqera.wave.service
import spock.lang.Specification
+import spock.lang.Unroll
import java.security.PublicKey
import java.time.Duration
@@ -28,6 +29,8 @@ import java.util.concurrent.CompletableFuture
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import io.seqera.tower.crypto.AsymmetricCipher
+import io.seqera.wave.core.ContainerPath
+import io.seqera.wave.model.ContainerCoordinates
import io.seqera.wave.service.pairing.PairingRecord
import io.seqera.wave.service.pairing.PairingService
import io.seqera.wave.tower.PlatformId
@@ -101,7 +104,8 @@ class CredentialsServiceTest extends Specification {
def auth = JwtAuth.of(identity)
when: 'look those registry credentials from tower'
- def credentials = credentialsService.findRegistryCreds("quay.io",identity)
+ def container = ContainerCoordinates.parse("quay.io/foo")
+ def credentials = credentialsService.findRegistryCreds(container, identity)
then: 'the registered key is fetched correctly from the security service'
1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, towerEndpoint) >> keyRecord
@@ -124,9 +128,10 @@ class CredentialsServiceTest extends Specification {
def 'should fail if keys where not registered for the tower endpoint'() {
given:
+ def container = ContainerCoordinates.parse('quay.io/foo')
def identity = new PlatformId(new User(id:10), 10,"token",'endpoint')
when:
- credentialsService.findRegistryCreds('quay.io',identity)
+ credentialsService.findRegistryCreds(container,identity)
then: 'the security service does not have the key for the hostname'
1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE,'endpoint') >> null
@@ -137,10 +142,12 @@ class CredentialsServiceTest extends Specification {
def 'should return no registry credentials if the user has no credentials in tower' () {
given:
+ def container = ContainerCoordinates.parse('quay.io/foo')
def identity = new PlatformId(new User(id:10), 10,"token",'tower.io')
def auth = JwtAuth.of(identity)
when:
- def credentials = credentialsService.findRegistryCreds('quay.io', identity)
+ def credentials = credentialsService.findRegistryCreds(container, identity)
+
then: 'a key is found'
1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, 'tower.io') >> new PairingRecord(
pairingId: 'a-key-id',
@@ -173,7 +180,8 @@ class CredentialsServiceTest extends Specification {
def auth = JwtAuth.of(identity)
when:
- def credentials = credentialsService.findRegistryCreds('quay.io', identity)
+ def container = ContainerCoordinates.parse('quay.io/foo')
+ def credentials = credentialsService.findRegistryCreds(container, identity)
then: 'a key is found'
1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, 'tower.io') >> new PairingRecord(
@@ -208,6 +216,48 @@ class CredentialsServiceTest extends Specification {
keys.password == 'you'
}
+ def'should find the best registry match with exact match'(){
+ given:
+ def svc = new CredentialServiceImpl()
+ and:
+ def target = "host.com/foo/bar"
+ def choices = [
+ new CredentialsDescription(registry:"host.com", provider: 'container-reg'),
+ new CredentialsDescription(registry:"host.com/foo",provider: 'container-reg'),
+ new CredentialsDescription(registry:"host.com/foo/bar", provider:'container-reg'),
+ new CredentialsDescription(registry:"host.com/foo/bar", provider:'something-else'),
+ new CredentialsDescription(registry:"host.com/foo/bar/baz",provider: 'container-reg') ]
+
+ when:
+ def match = svc.findBestMatchingCreds(target, choices)
+
+ then:
+ match.registry == "host.com/foo/bar"
+ match.provider == "container-reg"
+ }
+
+ def'should find the best registry match with partial match'(){
+ given:
+ def svc = new CredentialServiceImpl()
+ def target = TARGET
+ def choices = CHOICES.tokenize(' ').collect(it-> new CredentialsDescription(registry: it, provider: 'container-reg') )
+
+ when:
+ def match = svc.findBestMatchingCreds(target, choices)
+
+ then:
+ match.registry == EXPECTED
+
+ where:
+ TARGET | EXPECTED | CHOICES
+ "host.com" | 'host.com' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*'
+ "host.com/fo" | 'host.com' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*'
+ "host.com/bar" | 'host.com' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*'
+ "host.com/foo" | 'host.com/foo' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*'
+ "host.com/foo/bar" | 'host.com/foo/*' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*'
+ "host.com/foo/bar/baz" | 'host.com/foo/bar/baz/*' | 'host.com host.com/foo host.com/foo/* host.com/foo/* host.com/fooo/* host.com/foo/bar/baz/*'
+ }
+
def 'should parse aws keys payload' () {
given:
def svc = new CredentialServiceImpl()
@@ -264,9 +314,10 @@ class CredentialsServiceTest extends Specification {
and:
def identity = new PlatformId(new User(id:userId), workspaceId,token,towerEndpoint,workflowId)
def auth = JwtAuth.of(identity)
+ def containerPath = ContainerCoordinates.parse("$registryName/foo")
when: 'look those registry credentials from tower'
- def containerCredentials = credentialsService.findRegistryCreds(registryName,identity)
+ def containerCredentials = credentialsService.findRegistryCreds(containerPath, identity)
then: 'the registered key is fetched correctly from the security service'
1 * securityService.getPairingRecord(PairingService.TOWER_SERVICE, towerEndpoint) >> keyRecord
@@ -294,4 +345,51 @@ class CredentialsServiceTest extends Specification {
return null
}
+
+ @Unroll
+ def 'should get the repository score' () {
+ given:
+ def svc = new CredentialServiceImpl()
+
+ expect:
+ svc.matchingScore(TARGET, PATTERN) == EXPECTED
+
+ where:
+ TARGET | PATTERN | EXPECTED
+ null | null | 0
+ 'quay.io' | null | 0
+ 'quay.io' | 'docker.io' | 0
+ and:
+ 'quay.io' | 'quay.io' | 'quay.io'.length()
+ 'quay.io/foo' | 'quay.io' | 'quay.io'.length()
+ 'quay.io/foo/bar' | 'quay.io' | 'quay.io'.length()
+ and:
+ 'quay.io/foo/bar' | 'quay.io/fo' | 0
+ 'quay.io/foo/bar' | 'quay.io/fooo' | 0
+ 'quay.io/foo/bar' | 'quay.io/*' | 'quay.io'.length()
+ and:
+ 'quay.io/foo' | 'quay.io/foo/*' | 'quay.io/foo'.length()
+ 'quay.io/foo/bar' | 'quay.io/foo/*' | 'quay.io/foo'.length()
+ and:
+ // should should return 0 because the "authority" repository has
+ // a longer name of the target one. Therefore it cannot be used
+ // to authenticate the target
+ 'quay.io' | 'quay.io/foo/*' | 0
+ 'quay.io/fo/bar' | 'quay.io/foo/*' | 0
+ }
+
+
+ def 'should return the longest matching repository' () {
+ given:
+ def svc = new CredentialServiceImpl()
+
+ expect:
+ svc.matchingLongest(TARGET, new CredentialsDescription(registry: R1), new CredentialsDescription(registry: R2)).registry == EXPECTED
+ where:
+ TARGET | R1 | R2 | EXPECTED
+ 'docker.io' | 'docker.io' | 'quay.io' | 'docker.io'
+ 'docker.io/foo' | 'docker.io/foo' | 'docker.io' | 'docker.io/foo'
+ 'docker.io/foo/bar' | 'docker.io/foo/*' | 'docker.io' | 'docker.io/foo/*'
+ 'docker.io/foo/bar' | 'docker.io/foo' | 'docker.io' | 'docker.io'
+ }
}
diff --git a/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy b/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy
index 40ba378b8..2019ac706 100644
--- a/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy
+++ b/src/test/groovy/io/seqera/wave/test/BaseTestContainerRegistry.groovy
@@ -31,15 +31,19 @@ trait BaseTestContainerRegistry {
abstract GenericContainer getTestcontainers()
- String getTestRegistryUrl(String registry=null) {
+ String getTestRegistryName(String registry=null) {
if( !registry || registry=='test' || registry=='localhost' ) {
int port = testcontainers.firstMappedPort
- return "http://$testcontainers.containerIpAddress:$port"
+ return "${testcontainers.getHost()}:$port"
}
else
return registry
}
+ String getTestRegistryUrl(String registry=null) {
+ return "http://" + getTestRegistryName(registry)
+ }
+
RegistryInfo getLocalTestRegistryInfo() {
final uri = new URI(getTestRegistryUrl())
new RegistryInfo('test', uri, new RegistryAuth(uri, null, RegistryAuth.Type.Basic))