diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy index aa43a4395..38ee62c5a 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryAuth.groovy @@ -23,6 +23,7 @@ import java.util.regex.Pattern import groovy.transform.Canonical import groovy.transform.CompileStatic import groovy.transform.ToString +import io.seqera.wave.encoder.MoshiSerializable /** * Model container registry authentication meta-info @@ -32,7 +33,7 @@ import groovy.transform.ToString @Canonical @CompileStatic @ToString(includePackage = false, includeNames = true) -class RegistryAuth { +class RegistryAuth implements MoshiSerializable { private static final Pattern AUTH = ~/(?i)(?.+) realm="(?[^"]+)",service="(?[^"]+)"/ // some registries doesn't send the service diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryAuthStore.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryAuthStore.groovy deleted file mode 100644 index ba1c834c4..000000000 --- a/src/main/groovy/io/seqera/wave/auth/RegistryAuthStore.groovy +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Wave, containers provisioning service - * Copyright (c) 2023-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 . - */ - -package io.seqera.wave.auth - -import java.time.Duration - -import groovy.transform.CompileStatic -import groovy.util.logging.Slf4j -import io.micronaut.context.annotation.Value -import io.seqera.wave.encoder.MoshiEncodeStrategy -import io.seqera.wave.store.state.AbstractStateStore -import io.seqera.wave.store.state.impl.StateProvider -import jakarta.inject.Singleton -/** - * Implement a cache store for {@link RegistryAuth} object that - * can be distributed across wave replicas - * - * @author Paolo Di Tommaso - */ -@Slf4j -@Singleton -@CompileStatic -class RegistryAuthStore extends AbstractStateStore { - - private Duration duration - - RegistryAuthStore( - StateProvider provider, - @Value('${wave.registry-auth.cache.duration:`3h`}') Duration duration) - { - super(provider, new MoshiEncodeStrategy() {}) - this.duration = duration - log.info "Creating Registry Auth cache store ― duration=$duration" - } - - @Override - protected String getPrefix() { - return 'registry-auth/v1' - } - - @Override - protected Duration getDuration() { - return duration - } -} diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryLookupCache.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryLookupCache.groovy new file mode 100644 index 000000000..139e3c11d --- /dev/null +++ b/src/main/groovy/io/seqera/wave/auth/RegistryLookupCache.groovy @@ -0,0 +1,83 @@ +/* + * Wave, containers provisioning service + * Copyright (c) 2023-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 . + */ + +package io.seqera.wave.auth + +import java.time.Duration + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import io.micronaut.context.annotation.Value +import io.micronaut.core.annotation.Nullable +import io.seqera.wave.encoder.MoshiEncodeStrategy +import io.seqera.wave.encoder.MoshiSerializable +import io.seqera.wave.store.cache.AbstractTieredCache +import io.seqera.wave.store.cache.L2TieredCache +import jakarta.inject.Singleton +/** + * Implement a tiered cache for {@link RegistryLookupService} + * + * @author Munish Chouhan + */ +@Slf4j +@Singleton +@CompileStatic +class RegistryLookupCache extends AbstractTieredCache { + + @Value('${wave.registry-lookup.cache.duration:1h}') + private Duration duration + + @Value('${wave.registry-lookup.cache.max-size:10000}') + private int maxSize + + RegistryLookupCache(@Nullable L2TieredCache l2) { + super(l2, encoder()) + } + + @Override + int getMaxSize() { + return maxSize + } + + @Override + protected getName() { + return 'registry-lookup-cache' + } + + Duration getDuration() { + return duration + } + + @Override + protected String getPrefix() { + return 'registry-lookup-cache/v1' + } + + static MoshiEncodeStrategy encoder() { + new MoshiEncodeStrategy(factory()) {} + } + + static JsonAdapter.Factory factory() { + PolymorphicJsonAdapterFactory.of(MoshiSerializable.class, "@type") + .withSubtype(AbstractTieredCache.Entry.class, AbstractTieredCache.Entry.name) + .withSubtype(RegistryAuth.class, RegistryAuth.name) + } + +} diff --git a/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy b/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy index 13775d4a4..7dacc9595 100644 --- a/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy +++ b/src/main/groovy/io/seqera/wave/auth/RegistryLookupServiceImpl.groovy @@ -20,13 +20,8 @@ package io.seqera.wave.auth import java.net.http.HttpRequest import java.net.http.HttpResponse -import java.util.concurrent.CompletionException import java.util.concurrent.ExecutorService -import java.util.concurrent.TimeUnit -import com.github.benmanes.caffeine.cache.AsyncLoadingCache -import com.github.benmanes.caffeine.cache.CacheLoader -import com.github.benmanes.caffeine.cache.Caffeine import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import io.micronaut.scheduling.TaskExecutors @@ -34,7 +29,6 @@ import io.seqera.wave.configuration.HttpClientConfig import io.seqera.wave.exception.RegistryForwardException import io.seqera.wave.http.HttpClientFactory import io.seqera.wave.util.Retryable -import jakarta.annotation.PostConstruct import jakarta.inject.Inject import jakarta.inject.Named import jakarta.inject.Singleton @@ -56,43 +50,12 @@ class RegistryLookupServiceImpl implements RegistryLookupService { @Inject private HttpClientConfig httpConfig - @Inject - private RegistryAuthStore store - @Inject @Named(TaskExecutors.BLOCKING) private ExecutorService ioExecutor - private CacheLoader loader = new CacheLoader() { - @Override - RegistryAuth load(URI endpoint) throws Exception { - // check if there's a record in the store cache (redis) - def result = store.get(endpoint.toString()) - if( result ) { - log.debug "Authority lookup for endpoint: '$endpoint' => $result [from store]" - return result - } - // look-up using the corresponding API endpoint - result = lookup0(endpoint) - log.debug "Authority lookup for endpoint: '$endpoint' => $result" - // save it in the store cache (redis) - store.put(endpoint.toString(), result) - return result - } - } - - // FIXME https://github.com/seqeralabs/wave/issues/747 - private AsyncLoadingCache cache - - @PostConstruct - void init() { - cache = Caffeine - .newBuilder() - .maximumSize(10_000) - .expireAfterAccess(1, TimeUnit.HOURS) - .executor(ioExecutor) - .buildAsync(loader) - } + @Inject + private RegistryLookupCache cache protected RegistryAuth lookup0(URI endpoint) { final httpClient = HttpClientFactory.followRedirectsHttpClient() @@ -129,17 +92,9 @@ class RegistryLookupServiceImpl implements RegistryLookupService { */ @Override RegistryInfo lookup(String registry) { - try { - final endpoint = registryEndpoint(registry) - // FIXME https://github.com/seqeralabs/wave/issues/747 - final auth = cache.synchronous().get(endpoint) - return new RegistryInfo(registry, endpoint, auth) - } - catch (CompletionException e) { - // this catches the exception thrown in the cache loader lookup - // and throws the causing exception that should be `RegistryUnauthorizedAccessException` - throw e.cause - } + final endpoint = registryEndpoint(registry) + final auth = cache.getOrCompute(endpoint.toString(), (k) -> lookup0(endpoint), cache.duration) + return new RegistryInfo(registry, endpoint, auth) } /** diff --git a/src/main/groovy/io/seqera/wave/encoder/MoshiExchange.groovy b/src/main/groovy/io/seqera/wave/encoder/MoshiSerializable.groovy similarity index 91% rename from src/main/groovy/io/seqera/wave/encoder/MoshiExchange.groovy rename to src/main/groovy/io/seqera/wave/encoder/MoshiSerializable.groovy index c6d5fef54..78589b461 100644 --- a/src/main/groovy/io/seqera/wave/encoder/MoshiExchange.groovy +++ b/src/main/groovy/io/seqera/wave/encoder/MoshiSerializable.groovy @@ -19,9 +19,9 @@ package io.seqera.wave.encoder /** - * Marker interface for Moshi encoded exchange objects + * Marker interface for Moshi serializable objects * * @author Paolo Di Tommaso */ -interface MoshiExchange { +interface MoshiSerializable { } diff --git a/src/main/groovy/io/seqera/wave/proxy/DelegateResponse.groovy b/src/main/groovy/io/seqera/wave/proxy/DelegateResponse.groovy index 88c88afc3..3fa270be2 100644 --- a/src/main/groovy/io/seqera/wave/proxy/DelegateResponse.groovy +++ b/src/main/groovy/io/seqera/wave/proxy/DelegateResponse.groovy @@ -19,14 +19,14 @@ package io.seqera.wave.proxy import groovy.transform.EqualsAndHashCode -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable /** * Model a response object to be forwarded to the client * * @author Paolo Di Tommaso */ @EqualsAndHashCode -class DelegateResponse implements MoshiExchange { +class DelegateResponse implements MoshiSerializable { int statusCode Map> headers byte[] body diff --git a/src/main/groovy/io/seqera/wave/proxy/ProxyCache.groovy b/src/main/groovy/io/seqera/wave/proxy/ProxyCache.groovy index 9a8fe84fd..763d7ddbb 100644 --- a/src/main/groovy/io/seqera/wave/proxy/ProxyCache.groovy +++ b/src/main/groovy/io/seqera/wave/proxy/ProxyCache.groovy @@ -26,7 +26,7 @@ import groovy.util.logging.Slf4j import io.micronaut.core.annotation.Nullable import io.seqera.wave.configuration.ProxyCacheConfig import io.seqera.wave.encoder.MoshiEncodeStrategy -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable import io.seqera.wave.store.cache.AbstractTieredCache import io.seqera.wave.store.cache.L2TieredCache import jakarta.inject.Singleton @@ -38,7 +38,7 @@ import jakarta.inject.Singleton @Slf4j @Singleton @CompileStatic -class ProxyCache extends AbstractTieredCache { +class ProxyCache extends AbstractTieredCache { private ProxyCacheConfig config @@ -50,7 +50,7 @@ class ProxyCache extends AbstractTieredCache { static MoshiEncodeStrategy encoder() { // json adapter factory - final factory = PolymorphicJsonAdapterFactory.of(MoshiExchange.class, "@type") + final factory = PolymorphicJsonAdapterFactory.of(MoshiSerializable.class, "@type") .withSubtype(Entry.class, Entry.name) .withSubtype(DelegateResponse.class, DelegateResponse.simpleName) // the encoding strategy diff --git a/src/main/groovy/io/seqera/wave/service/aws/AwsEcrService.groovy b/src/main/groovy/io/seqera/wave/service/aws/AwsEcrService.groovy index 3db479f4b..5a4f30392 100644 --- a/src/main/groovy/io/seqera/wave/service/aws/AwsEcrService.groovy +++ b/src/main/groovy/io/seqera/wave/service/aws/AwsEcrService.groovy @@ -18,19 +18,19 @@ package io.seqera.wave.service.aws + import java.util.concurrent.ExecutorService -import java.util.concurrent.TimeUnit import java.util.regex.Pattern -import com.github.benmanes.caffeine.cache.AsyncLoadingCache -import com.github.benmanes.caffeine.cache.CacheLoader -import com.github.benmanes.caffeine.cache.Caffeine import groovy.transform.Canonical import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import io.micronaut.scheduling.TaskExecutors +import io.seqera.wave.service.aws.cache.AwsEcrAuthToken +import io.seqera.wave.service.aws.cache.AwsEcrCache +import io.seqera.wave.store.cache.TieredKey +import io.seqera.wave.util.RegHelper import io.seqera.wave.util.StringUtils -import jakarta.annotation.PostConstruct import jakarta.inject.Inject import jakarta.inject.Named import jakarta.inject.Singleton @@ -56,11 +56,16 @@ class AwsEcrService { static final private Pattern AWS_ECR_PUBLIC = ~/public\.ecr\.aws/ @Canonical - private static class AwsCreds { + private static class AwsCreds implements TieredKey { String accessKey String secretKey String region boolean ecrPublic + + @Override + String stableHash() { + RegHelper.sipHash(accessKey, secretKey, region, ecrPublic) + } } @Canonical @@ -69,31 +74,19 @@ class AwsEcrService { String region } - private CacheLoader loader = new CacheLoader() { - @Override - String load(AwsCreds creds) throws Exception { - return creds.ecrPublic - ? getLoginToken1(creds.accessKey, creds.secretKey, creds.region) - : getLoginToken0(creds.accessKey, creds.secretKey, creds.region) - } + AwsEcrAuthToken load(AwsCreds creds) throws Exception { + def token = creds.ecrPublic + ? getLoginToken1(creds.accessKey, creds.secretKey, creds.region) + : getLoginToken0(creds.accessKey, creds.secretKey, creds.region) + return new AwsEcrAuthToken(token) } @Inject @Named(TaskExecutors.BLOCKING) private ExecutorService ioExecutor - // FIXME https://github.com/seqeralabs/wave/issues/747 - private AsyncLoadingCache cache - - @PostConstruct - private void init() { - cache = Caffeine - .newBuilder() - .maximumSize(10_000) - .expireAfterWrite(3, TimeUnit.HOURS) - .executor(ioExecutor) - .buildAsync(loader) - } + @Inject + private AwsEcrCache cache private EcrClient ecrClient(String accessKey, String secretKey, String region) { EcrClient.builder() @@ -139,10 +132,8 @@ class AwsEcrService { assert region, "Missing AWS region argument" try { - // get the token from the cache, if missing the it's automatically - // fetch using the AWS ECR client - // FIXME https://github.com/seqeralabs/wave/issues/747 - return cache.synchronous().get(new AwsCreds(accessKey,secretKey,region,isPublic)) + final key = new AwsCreds(accessKey,secretKey,region,isPublic) + return cache.getOrCompute(key, (k) -> load(key), cache.duration).value } catch (Exception e) { final type = isPublic ? "ECR public" : "ECR" diff --git a/src/main/groovy/io/seqera/wave/service/aws/cache/AwsEcrAuthToken.groovy b/src/main/groovy/io/seqera/wave/service/aws/cache/AwsEcrAuthToken.groovy new file mode 100644 index 000000000..90663243e --- /dev/null +++ b/src/main/groovy/io/seqera/wave/service/aws/cache/AwsEcrAuthToken.groovy @@ -0,0 +1,39 @@ +/* + * 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 . + */ + +package io.seqera.wave.service.aws.cache + +import groovy.transform.CompileStatic +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import io.seqera.wave.encoder.MoshiSerializable +/** + * Model a tiered cache value for {@link AwsEcrCache} + * + * @author Munish Chouhan + */ +@CompileStatic +@EqualsAndHashCode +@ToString(includePackage = false, includeNames = true) +class AwsEcrAuthToken implements MoshiSerializable { + String value + + AwsEcrAuthToken(String value) { + this.value = value + } +} diff --git a/src/main/groovy/io/seqera/wave/service/aws/cache/AwsEcrCache.groovy b/src/main/groovy/io/seqera/wave/service/aws/cache/AwsEcrCache.groovy new file mode 100644 index 000000000..11c291a32 --- /dev/null +++ b/src/main/groovy/io/seqera/wave/service/aws/cache/AwsEcrCache.groovy @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +package io.seqera.wave.service.aws.cache + +import java.time.Duration + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import io.micronaut.context.annotation.Value +import io.micronaut.core.annotation.Nullable +import io.seqera.wave.encoder.MoshiEncodeStrategy +import io.seqera.wave.encoder.MoshiSerializable +import io.seqera.wave.store.cache.AbstractTieredCache +import io.seqera.wave.store.cache.L2TieredCache +import io.seqera.wave.store.cache.TieredKey +import jakarta.inject.Singleton +/** + * Implement a tiered cache for AWS ECR client + * + * @author Munish Chouhan + */ +@Slf4j +@Singleton +@CompileStatic +class AwsEcrCache extends AbstractTieredCache { + + @Value('${wave.aws.ecr.cache.duration:3h}') + private Duration duration + + @Value('${wave.aws.ecr.cache.max-size:10000}') + private int maxSize + + AwsEcrCache(@Nullable L2TieredCache l2) { + super(l2, encoder()) + } + + @Override + int getMaxSize() { + return maxSize + } + + @Override + protected getName() { + return 'aws-ecr-cache' + } + + @Override + protected String getPrefix() { + return 'aws-ecr-cache/v1' + } + + Duration getDuration() { + return duration + } + + static MoshiEncodeStrategy encoder() { + new MoshiEncodeStrategy(factory()) {} + } + + static JsonAdapter.Factory factory() { + PolymorphicJsonAdapterFactory.of(MoshiSerializable.class, "@type") + .withSubtype(AbstractTieredCache.Entry.class, AbstractTieredCache.Entry.name) + .withSubtype(AwsEcrAuthToken.class, AwsEcrAuthToken.simpleName) + } + +} diff --git a/src/main/groovy/io/seqera/wave/store/cache/AbstractTieredCache.groovy b/src/main/groovy/io/seqera/wave/store/cache/AbstractTieredCache.groovy index 724942e00..5b8a38d02 100644 --- a/src/main/groovy/io/seqera/wave/store/cache/AbstractTieredCache.groovy +++ b/src/main/groovy/io/seqera/wave/store/cache/AbstractTieredCache.groovy @@ -37,7 +37,7 @@ import groovy.transform.ToString import groovy.util.logging.Slf4j import io.seqera.wave.encoder.EncodingStrategy import io.seqera.wave.encoder.MoshiEncodeStrategy -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable import org.jetbrains.annotations.Nullable /** * Implement a tiered-cache mechanism using a local caffeine cache as 1st level access @@ -45,16 +45,22 @@ import org.jetbrains.annotations.Nullable * * This allow the use in distributed deployment. Note however strong consistently is not guaranteed. * + * @param + * The type of keys maintained by this cache. Note it must be either a + * subtype of {@link CharSequence} or an implementation of {@link TieredKey} interface. + * @param + * The type of values maintained by this cache, which must extend {@link MoshiSerializable}. + * * @author Paolo Di Tommaso */ @Slf4j @CompileStatic -abstract class AbstractTieredCache implements TieredCache { +abstract class AbstractTieredCache implements TieredCache { @Canonical @ToString(includePackage = false, includeNames = true) - static class Entry implements MoshiExchange { - MoshiExchange value + static class Entry implements MoshiSerializable { + MoshiSerializable value long expiresAt } @@ -121,6 +127,17 @@ abstract class AbstractTieredCache implements TieredCac } } + protected String k0(K key) { + if( key instanceof CharSequence ) + return key.toString() + if( key instanceof TieredKey ) + return key.stableHash() + if( key==null ) + throw new IllegalArgumentException("Tiered cache key cannot be null") + else + throw new IllegalArgumentException("Tiered cache key type - offending value: ${key}; type: ${key.getClass()}") + } + /** * Retrieve the value associated with the specified key * @@ -130,8 +147,8 @@ abstract class AbstractTieredCache implements TieredCac * The value associated with the specified key, or {@code null} otherwise */ @Override - V get(String key) { - getOrCompute0(key, null) + V get(K key) { + getOrCompute0(k0(key), null) } /** @@ -141,15 +158,17 @@ abstract class AbstractTieredCache implements TieredCac * The key of the value to be retrieved * @param loader * A function invoked to load the value the entry with the specified key is not available + * @param ttl + * time to live for the entry * @return * The value associated with the specified key, or {@code null} otherwise */ - V getOrCompute(String key, Function loader, Duration ttl) { + V getOrCompute(K key, Function loader, Duration ttl) { if( loader==null ) { - return getOrCompute0(key, null) + return getOrCompute0(k0(key), null) } - return getOrCompute0(key, (String k)-> { - V v = loader.apply(key) + return getOrCompute0(k0(key), (String k)-> { + V v = loader.apply(k) return v != null ? new Tuple2<>(v, ttl) : null }) } @@ -164,8 +183,8 @@ abstract class AbstractTieredCache implements TieredCac * @return * The value associated with the specified key, or #function result otherwise */ - V getOrCompute(String key, Function> loader) { - return getOrCompute0(key, loader) + V getOrCompute(K key, Function> loader) { + return getOrCompute0(k0(key), loader) } private V getOrCompute0(String key, Function> loader) { @@ -227,15 +246,15 @@ abstract class AbstractTieredCache implements TieredCac } @Override - void put(String key, V value, Duration ttl) { + void put(K key, V value, Duration ttl) { assert key!=null, "Cache key argument cannot be null" assert value!=null, "Cache value argument cannot be null" if( log.isTraceEnabled() ) log.trace "Cache '${name}' putting - key=$key; value=${value}" final exp = System.currentTimeMillis() + ttl.toMillis() final entry = new Entry(value, exp) - l1Put(key, entry) - l2Put(key, entry, ttl) + l1Put(k0(key), entry) + l2Put(k0(key), entry, ttl) } protected String key0(String k) { return getPrefix() + ':' + k } diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryAuthStoreTest.groovy b/src/main/groovy/io/seqera/wave/store/cache/TieredKey.groovy similarity index 64% rename from src/test/groovy/io/seqera/wave/auth/RegistryAuthStoreTest.groovy rename to src/main/groovy/io/seqera/wave/store/cache/TieredKey.groovy index c6fb81ffa..a00e178c5 100644 --- a/src/test/groovy/io/seqera/wave/auth/RegistryAuthStoreTest.groovy +++ b/src/main/groovy/io/seqera/wave/store/cache/TieredKey.groovy @@ -16,26 +16,15 @@ * along with this program. If not, see . */ -package io.seqera.wave.auth - -import spock.lang.Specification - -import io.micronaut.test.extensions.spock.annotation.MicronautTest -import jakarta.inject.Inject +package io.seqera.wave.store.cache /** + * Define the contract for key used by {@link TieredCache} caches * - * @author Paolo Di Tommaso + * @author Munish Chouhan */ -@MicronautTest -class RegistryAuthStoreTest extends Specification { - - @Inject RegistryAuthStore store - - def 'should return entry key' () { - expect: - store.key0('foo') == 'registry-auth/v1:foo' - } +interface TieredKey { + String stableHash() } diff --git a/src/main/groovy/io/seqera/wave/tower/User.groovy b/src/main/groovy/io/seqera/wave/tower/User.groovy index a604216e1..8100b5a89 100644 --- a/src/main/groovy/io/seqera/wave/tower/User.groovy +++ b/src/main/groovy/io/seqera/wave/tower/User.groovy @@ -21,7 +21,7 @@ package io.seqera.wave.tower import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable import jakarta.validation.constraints.NotNull import jakarta.validation.constraints.Size @@ -33,7 +33,7 @@ import jakarta.validation.constraints.Size @ToString(includeNames = true, includePackage = false, includes = 'id,userName,email') @EqualsAndHashCode @CompileStatic -class User implements MoshiExchange { +class User implements MoshiSerializable { Long id 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 9069f4f8f..b1a097574 100644 --- a/src/main/groovy/io/seqera/wave/tower/client/CredentialsDescription.groovy +++ b/src/main/groovy/io/seqera/wave/tower/client/CredentialsDescription.groovy @@ -23,12 +23,12 @@ import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import io.seqera.wave.WaveDefault -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable @EqualsAndHashCode @CompileStatic @ToString(includePackage = false, includeNames = true) -class CredentialsDescription implements MoshiExchange { +class CredentialsDescription implements MoshiSerializable { String id String provider diff --git a/src/main/groovy/io/seqera/wave/tower/client/GetCredentialsKeysResponse.groovy b/src/main/groovy/io/seqera/wave/tower/client/GetCredentialsKeysResponse.groovy index 9cd2b8eff..d0496f673 100644 --- a/src/main/groovy/io/seqera/wave/tower/client/GetCredentialsKeysResponse.groovy +++ b/src/main/groovy/io/seqera/wave/tower/client/GetCredentialsKeysResponse.groovy @@ -21,7 +21,7 @@ package io.seqera.wave.tower.client import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable /** * Models an encrypted credentials keys response @@ -31,7 +31,7 @@ import io.seqera.wave.encoder.MoshiExchange @ToString(includePackage = false, includeNames = true) @EqualsAndHashCode @CompileStatic -class GetCredentialsKeysResponse implements MoshiExchange { +class GetCredentialsKeysResponse implements MoshiSerializable { /** * Secret keys associated with the credentials diff --git a/src/main/groovy/io/seqera/wave/tower/client/GetUserInfoResponse.groovy b/src/main/groovy/io/seqera/wave/tower/client/GetUserInfoResponse.groovy index 7b9fc6f28..9dfa8654e 100644 --- a/src/main/groovy/io/seqera/wave/tower/client/GetUserInfoResponse.groovy +++ b/src/main/groovy/io/seqera/wave/tower/client/GetUserInfoResponse.groovy @@ -21,7 +21,7 @@ package io.seqera.wave.tower.client import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable import io.seqera.wave.tower.User /** * Model a Tower user-info response @@ -30,6 +30,6 @@ import io.seqera.wave.tower.User @EqualsAndHashCode @ToString(includePackage = false, includeNames = true) @CompileStatic -class GetUserInfoResponse implements MoshiExchange { +class GetUserInfoResponse implements MoshiSerializable { User user } diff --git a/src/main/groovy/io/seqera/wave/tower/client/ListCredentialsResponse.groovy b/src/main/groovy/io/seqera/wave/tower/client/ListCredentialsResponse.groovy index ef6969a32..77eadc3b1 100644 --- a/src/main/groovy/io/seqera/wave/tower/client/ListCredentialsResponse.groovy +++ b/src/main/groovy/io/seqera/wave/tower/client/ListCredentialsResponse.groovy @@ -21,12 +21,12 @@ package io.seqera.wave.tower.client import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable @EqualsAndHashCode @ToString(includePackage = false, includeNames = true) @CompileStatic -class ListCredentialsResponse implements MoshiExchange { +class ListCredentialsResponse implements MoshiSerializable { List credentials diff --git a/src/main/groovy/io/seqera/wave/tower/client/cache/ClientCache.groovy b/src/main/groovy/io/seqera/wave/tower/client/cache/ClientCache.groovy index 4b72f4703..5a8dbcd95 100644 --- a/src/main/groovy/io/seqera/wave/tower/client/cache/ClientCache.groovy +++ b/src/main/groovy/io/seqera/wave/tower/client/cache/ClientCache.groovy @@ -26,7 +26,7 @@ import groovy.util.logging.Slf4j import io.micronaut.context.annotation.Value import io.micronaut.core.annotation.Nullable import io.seqera.wave.encoder.MoshiEncodeStrategy -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable import io.seqera.wave.store.cache.AbstractTieredCache import io.seqera.wave.store.cache.L2TieredCache import io.seqera.wave.tower.User @@ -75,7 +75,7 @@ class ClientCache extends AbstractTieredCache { } static JsonAdapter.Factory factory() { - PolymorphicJsonAdapterFactory.of(MoshiExchange.class, "@type") + PolymorphicJsonAdapterFactory.of(MoshiSerializable.class, "@type") .withSubtype(AbstractTieredCache.Entry.class, AbstractTieredCache.Entry.name) // add all exchange classes used by the tower client .withSubtype(ComputeEnv.class, ComputeEnv.simpleName) diff --git a/src/main/groovy/io/seqera/wave/tower/compute/ComputeEnv.groovy b/src/main/groovy/io/seqera/wave/tower/compute/ComputeEnv.groovy index 453ecd1f4..af7febbee 100644 --- a/src/main/groovy/io/seqera/wave/tower/compute/ComputeEnv.groovy +++ b/src/main/groovy/io/seqera/wave/tower/compute/ComputeEnv.groovy @@ -21,7 +21,7 @@ package io.seqera.wave.tower.compute import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable /** * Model the response of compute environment from seqera platform @@ -31,7 +31,7 @@ import io.seqera.wave.encoder.MoshiExchange @CompileStatic @EqualsAndHashCode @ToString(includePackage = false, includeNames = true) -class ComputeEnv implements MoshiExchange { +class ComputeEnv implements MoshiSerializable { String id String platform String credentialsId diff --git a/src/main/groovy/io/seqera/wave/tower/compute/DescribeWorkflowLaunchResponse.groovy b/src/main/groovy/io/seqera/wave/tower/compute/DescribeWorkflowLaunchResponse.groovy index ac3653620..c1a9d05a8 100644 --- a/src/main/groovy/io/seqera/wave/tower/compute/DescribeWorkflowLaunchResponse.groovy +++ b/src/main/groovy/io/seqera/wave/tower/compute/DescribeWorkflowLaunchResponse.groovy @@ -21,7 +21,7 @@ package io.seqera.wave.tower.compute import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable /** * Model the response of workflow launch describe request @@ -31,7 +31,7 @@ import io.seqera.wave.encoder.MoshiExchange @CompileStatic @EqualsAndHashCode @ToString(includePackage = false, includeNames = true) -class DescribeWorkflowLaunchResponse implements MoshiExchange { +class DescribeWorkflowLaunchResponse implements MoshiSerializable { WorkflowLaunch launch diff --git a/src/main/groovy/io/seqera/wave/tower/compute/WorkflowLaunch.groovy b/src/main/groovy/io/seqera/wave/tower/compute/WorkflowLaunch.groovy index dd6cf0775..45b9ec249 100644 --- a/src/main/groovy/io/seqera/wave/tower/compute/WorkflowLaunch.groovy +++ b/src/main/groovy/io/seqera/wave/tower/compute/WorkflowLaunch.groovy @@ -21,7 +21,7 @@ package io.seqera.wave.tower.compute import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.ToString -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable /** * Model the response of workflow launch response from seqera platform @@ -31,6 +31,6 @@ import io.seqera.wave.encoder.MoshiExchange @CompileStatic @EqualsAndHashCode @ToString(includePackage = false, includeNames = true) -class WorkflowLaunch implements MoshiExchange { +class WorkflowLaunch implements MoshiSerializable { ComputeEnv computeEnv } diff --git a/src/test/groovy/io/seqera/wave/auth/RegistryLookupCacheTest.groovy b/src/test/groovy/io/seqera/wave/auth/RegistryLookupCacheTest.groovy new file mode 100644 index 000000000..3d87d51ef --- /dev/null +++ b/src/test/groovy/io/seqera/wave/auth/RegistryLookupCacheTest.groovy @@ -0,0 +1,66 @@ +/* + * Wave, containers provisioning service + * Copyright (c) 2023-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 . + */ + +package io.seqera.wave.auth + +import spock.lang.Shared +import spock.lang.Specification + +import java.time.Duration + +import io.micronaut.context.ApplicationContext +import io.seqera.wave.store.cache.RedisL2TieredCache +import io.seqera.wave.test.RedisTestContainer +/** + * + * @author Munish Chouhan + */ +class RegistryLookupCacheTest extends Specification implements RedisTestContainer { + + @Shared + ApplicationContext applicationContext + + def setup() { + applicationContext = ApplicationContext.run([ + REDIS_HOST : redisHostName, + REDIS_PORT : redisPort + ], 'test', 'redis') + sleep(500) // workaround to wait for Redis connection + } + + def cleanup() { + applicationContext.close() + } + + def 'should cache registry auth response' () { + given: + def TTL = Duration.ofSeconds(1) + def store = applicationContext.getBean(RedisL2TieredCache) + def cache1 = new RegistryLookupCache(store) + def cache2 = new RegistryLookupCache(store) + and: + def k = UUID.randomUUID().toString() + def resp = new RegistryAuth(URI.create('seqera.io/auth'), 'seqera', RegistryAuth.Type.Basic) + + when: + cache1.put(k, resp, TTL) + then: + cache2.get(k) == resp + } + +} diff --git a/src/test/groovy/io/seqera/wave/service/aws/cache/AwsEcrCacheTest.groovy b/src/test/groovy/io/seqera/wave/service/aws/cache/AwsEcrCacheTest.groovy new file mode 100644 index 000000000..fae7ab92e --- /dev/null +++ b/src/test/groovy/io/seqera/wave/service/aws/cache/AwsEcrCacheTest.groovy @@ -0,0 +1,64 @@ +/* + * 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 . + */ + +package io.seqera.wave.service.aws.cache + +import spock.lang.Shared +import spock.lang.Specification + +import java.time.Duration + +import io.micronaut.context.ApplicationContext +import io.seqera.wave.store.cache.RedisL2TieredCache +import io.seqera.wave.test.RedisTestContainer +/** + * + * @author Munish Chouhan + */ +class AwsEcrCacheTest extends Specification implements RedisTestContainer { + + @Shared + ApplicationContext applicationContext + + def setup() { + applicationContext = ApplicationContext.run([ + REDIS_HOST: redisHostName, + REDIS_PORT: redisPort + ], 'test', 'redis') + sleep(500) // workaround to wait for Redis connection + } + + def cleanup() { + applicationContext.close() + } + + def 'should cache ecr token response'() { + given: + def store = applicationContext.getBean(RedisL2TieredCache) + def cache1 = new AwsEcrCache(store) + def cache2 = new AwsEcrCache(store) + and: + def k = UUID.randomUUID().toString() + def token = new AwsEcrAuthToken('token') + + when: + cache1.put(k, token, Duration.ofSeconds(30)) + then: + cache2.get(k) == token + } +} diff --git a/src/test/groovy/io/seqera/wave/store/cache/AbstractTieredCacheTest.groovy b/src/test/groovy/io/seqera/wave/store/cache/AbstractTieredCacheTest.groovy index 81b302a5b..4b50416eb 100644 --- a/src/test/groovy/io/seqera/wave/store/cache/AbstractTieredCacheTest.groovy +++ b/src/test/groovy/io/seqera/wave/store/cache/AbstractTieredCacheTest.groovy @@ -26,7 +26,7 @@ import groovy.transform.Canonical import groovy.transform.Memoized import io.micronaut.context.ApplicationContext import io.seqera.wave.encoder.MoshiEncodeStrategy -import io.seqera.wave.encoder.MoshiExchange +import io.seqera.wave.encoder.MoshiSerializable import io.seqera.wave.test.RedisTestContainer import spock.lang.Shared import spock.lang.Specification @@ -34,21 +34,21 @@ import spock.lang.Specification class AbstractTieredCacheTest extends Specification implements RedisTestContainer { @Canonical - static class MyBean implements MoshiExchange { + static class MyBean implements MoshiSerializable { String foo String bar } @Memoized static MoshiEncodeStrategy encoder() { - JsonAdapter.Factory factory = PolymorphicJsonAdapterFactory.of(MoshiExchange.class, "@type") + JsonAdapter.Factory factory = PolymorphicJsonAdapterFactory.of(MoshiSerializable.class, "@type") .withSubtype(AbstractTieredCache.Entry.class, AbstractTieredCache.Entry.name) .withSubtype(MyBean.class, MyBean.name) return new MoshiEncodeStrategy(factory) {} } - static class MyCache extends AbstractTieredCache { + static class MyCache extends AbstractTieredCache { static String PREFIX = 'foo/v1'