diff --git a/pom.xml b/pom.xml index a3bb92c2d..cac9b05a8 100644 --- a/pom.xml +++ b/pom.xml @@ -129,12 +129,12 @@ org.apache.httpcomponents httpclient - 4.5 + 4.5.5 org.apache.httpcomponents httpcore - 4.4.5 + 4.4.9 com.github.jnr @@ -169,6 +169,7 @@ 3.0.1 provided + com.google.auth @@ -183,6 +184,20 @@ + + + com.amazonaws + aws-java-sdk-ecr + 1.11.313 + true + + + software.amazon.ion + ion-java + + + + junit diff --git a/src/main/java/com/spotify/docker/client/auth/ecr/Authenticator.java b/src/main/java/com/spotify/docker/client/auth/ecr/Authenticator.java new file mode 100644 index 000000000..e23a5eea3 --- /dev/null +++ b/src/main/java/com/spotify/docker/client/auth/ecr/Authenticator.java @@ -0,0 +1,95 @@ +/*- + * -\-\- + * docker-client + * -- + * Copyright (C) 2016 - 2017 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +package com.spotify.docker.client.auth.ecr; + +import com.spotify.docker.client.exceptions.DockerException; +import com.spotify.docker.client.messages.RegistryAuth; + +import java.util.Objects; + +public interface Authenticator extends AutoCloseable { + public static interface Factory { + public Authenticator getAuthenticator(); + } + + public static class Authentication { + public final String username; + public final String password; + public final String registry; + public final long expiration; + public final boolean def; + + public Authentication( + String username, + String password, + String registry, + long expiration, + boolean def) { + this.username = username; + this.password = password; + this.registry = registry; + this.expiration = expiration; + this.def = def; + } + + public RegistryAuth toRegistryAuth() { + return RegistryAuth.builder() + .username(username) + .password(password) + .serverAddress(registry) + .build(); + } + + public int hashCode() { + return Objects.hash( + username, + password, + registry, + expiration, + def); + } + + public boolean equals(Object other) { + boolean result; + if (this == other) { + result = true; + } else + if (other == null) { + result = false; + } else + if (other instanceof Authentication) { + Authentication that = (Authentication) other; + result = Objects.equals(username, that.username) + && Objects.equals(password, password) + && Objects.equals(registry, that.registry) + && expiration == that.expiration + && def == that.def; + } else { + result = false; + } + return result; + } + } + + public Authentication authenticate(String registry) throws DockerException; + + public void close() throws DockerException; +} diff --git a/src/main/java/com/spotify/docker/client/auth/ecr/EcrAuthenticator.java b/src/main/java/com/spotify/docker/client/auth/ecr/EcrAuthenticator.java new file mode 100644 index 000000000..efa527ece --- /dev/null +++ b/src/main/java/com/spotify/docker/client/auth/ecr/EcrAuthenticator.java @@ -0,0 +1,92 @@ +/*- + * -\-\- + * docker-client + * -- + * Copyright (C) 2016 - 2017 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +package com.spotify.docker.client.auth.ecr; + +import com.amazonaws.services.ecr.AmazonECR; +import com.amazonaws.services.ecr.model.AuthorizationData; +import com.amazonaws.services.ecr.model.GetAuthorizationTokenRequest; +import com.amazonaws.services.ecr.model.GetAuthorizationTokenResult; +import com.amazonaws.util.Base64; +import com.spotify.docker.client.exceptions.DockerException; + +import java.nio.charset.StandardCharsets; + +public class EcrAuthenticator implements Authenticator { + private final AmazonECR client; + + public EcrAuthenticator(AmazonECR client) { + this.client = client; + } + + @Override + public Authentication authenticate(String registry) throws DockerException { + GetAuthorizationTokenRequest request = new GetAuthorizationTokenRequest(); + if (registry != null) { + request = request.withRegistryIds(registry); + } + + AuthorizationData authorization; + try { + GetAuthorizationTokenResult response = client.getAuthorizationToken(request); + authorization = response.getAuthorizationData().get(0); + } catch (Exception e) { + throw new DockerException("Failed to retrieve ECR credentials", e); + } + + String auth = new String( + Base64.decode(authorization.getAuthorizationToken()), + StandardCharsets.UTF_8); + String[] authParts = auth.split(":", 2); + if (authParts.length < 2) { + // Never put credentials -- even encoded credentials -- in an Exception message. + throw new DockerException("Failed to parse ECR credentials"); + } + + String username = authParts[0]; + String password = authParts[1]; + + String newregistry; + String endpoint = authorization.getProxyEndpoint(); + if (endpoint.startsWith("https://")) { + newregistry = endpoint.substring("https://".length(), endpoint.length()); + } else { + throw new DockerException("Failed to parse ECR endpoint: " + endpoint); + } + + long expiration; + if (authorization.getExpiresAt() != null) { + expiration = authorization.getExpiresAt().getTime(); + } else { + expiration = -1L; + } + + return new Authentication(username, password, newregistry, expiration, registry == null); + } + + @Override + public void close() throws DockerException { + getClient().shutdown(); + } + + public AmazonECR getClient() { + return client; + } +} diff --git a/src/main/java/com/spotify/docker/client/auth/ecr/EcrAuthenticatorFactory.java b/src/main/java/com/spotify/docker/client/auth/ecr/EcrAuthenticatorFactory.java new file mode 100644 index 000000000..058ee41c5 --- /dev/null +++ b/src/main/java/com/spotify/docker/client/auth/ecr/EcrAuthenticatorFactory.java @@ -0,0 +1,43 @@ +/*- + * -\-\- + * docker-client + * -- + * Copyright (C) 2016 - 2017 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +package com.spotify.docker.client.auth.ecr; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.ecr.AmazonECRClient; + +public class EcrAuthenticatorFactory implements Authenticator.Factory { + private final AWSCredentialsProvider provider; + + public EcrAuthenticatorFactory(AWSCredentialsProvider provider) { + this.provider = provider; + } + + @Override + public Authenticator getAuthenticator() { + return new EcrAuthenticator(AmazonECRClient.builder() + .withCredentials(getProvider()) + .build()); + } + + private AWSCredentialsProvider getProvider() { + return provider; + } +} diff --git a/src/main/java/com/spotify/docker/client/auth/ecr/ElasticContainerRegistryAuthSupplier.java b/src/main/java/com/spotify/docker/client/auth/ecr/ElasticContainerRegistryAuthSupplier.java new file mode 100644 index 000000000..b9ba95bf3 --- /dev/null +++ b/src/main/java/com/spotify/docker/client/auth/ecr/ElasticContainerRegistryAuthSupplier.java @@ -0,0 +1,332 @@ +/*- + * -\-\- + * docker-client + * -- + * Copyright (C) 2016 - 2017 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +package com.spotify.docker.client.auth.ecr; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.auth.EnvironmentVariableCredentialsProvider; +import com.amazonaws.auth.SystemPropertiesCredentialsProvider; +import com.amazonaws.auth.profile.ProfileCredentialsProvider; + +import com.google.api.client.util.Clock; +import com.google.common.annotations.VisibleForTesting; + +import com.spotify.docker.client.auth.RegistryAuthSupplier; +import com.spotify.docker.client.exceptions.DockerException; +import com.spotify.docker.client.messages.RegistryAuth; +import com.spotify.docker.client.messages.RegistryConfigs; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + *

+ * A RegistryAuthSupplier for getting access tokens from a Amazon Web Services service or user + * account. This implementation uses the aos-java-sdk-core library to get an access + * token given an account's credentials, and will refresh the token if it expires within a + * configurable amount of time. + *

+ * + *

+ * To construct a new instance, use any of the static methods that return a {@link Builder}. + *

+ */ +public class ElasticContainerRegistryAuthSupplier implements RegistryAuthSupplier { + private static final Logger log = LoggerFactory + .getLogger(ElasticContainerRegistryAuthSupplier.class); + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using the default provider chain. + * + * @see DefaultAWSCredentialsProviderChain + * @see Builder + */ + public static Builder getApplicationDefaultDefault() throws IOException { + return forProvider(new DefaultAWSCredentialsProviderChain()); + } + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using data from the program + * environment variables AWS_ACCESS_KEY and AWS_SECRET_ACCESS_KEY. + * + * @see EnvironmentVariableCredentialsProvider + * @see Builder + */ + public static Builder forEnvironmentVariables() throws IOException { + return forProvider(new EnvironmentVariableCredentialsProvider()); + } + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using data from the program + * environment variables AWS_ACCESS_KEY and AWS_SECRET_ACCESS_KEY. + * + * @see SystemPropertiesCredentialsProvider + * @see Builder + */ + public static Builder forSystemProperties() throws IOException { + return forProvider(new SystemPropertiesCredentialsProvider()); + } + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using the default profile + * from the system default profile config file. + * + * @see ProfileCredentialsProvider + * @see Builder + */ + public static Builder forProfile() throws IOException { + return forProfile("default"); + } + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using the given profile + * from the system default profile config file. + * + * @see ProfileCredentialsProvider + * @see Builder + */ + public static Builder forProfile(String profile) throws IOException { + return forProvider(new ProfileCredentialsProvider(profile)); + } + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using the given credentials. + * + * @see ProfileCredentialsProvider + * @see Builder + */ + public static Builder forCredentials( + String awsTokenId, + String awsTokenSecret) throws IOException { + return forCredentials(new BasicAWSCredentials(awsTokenId, awsTokenSecret)); + } + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using the given credentials. + * + * @see ProfileCredentialsProvider + * @see Builder + */ + public static Builder forCredentials(AWSCredentials credentials) throws IOException { + return forProvider(new AWSStaticCredentialsProvider(credentials)); + } + + /** + * Constructs an ElasticContainerRegistryAuthSupplier using the given credentials. + * + * @see ProfileCredentialsProvider + * @see Builder + */ + public static Builder forProvider(AWSCredentialsProvider provider) throws IOException { + return new Builder(provider); + } + + /** + * A Builder of ContainerRegistryAuthSupplier. + *

+ * The access tokens returned by the ContainerRegistryAuthSupplier are scoped to + * devstorage.read_write by default, these can be customized with {@link #withScopes(Collection)}. + *

+ *

+ * The default value for the minimum expiry time of an access token is one minute. When the + * ContainerRegistryAuthSupplier is asked for a RegistryAuth, it will check if the existing + * AccessToken for the GoogleCredentials expires within this amount of time. If it does, then the + * AccessToken is refreshed before being returned.

+ */ + public static class Builder { + private final AWSCredentialsProvider provider; + private long minimumExpiryMillis; + + public Builder(final AWSCredentialsProvider provider) { + this.provider = provider; + this.minimumExpiryMillis = TimeUnit.MINUTES.toMillis(1L); + } + + public AWSCredentialsProvider getProvider() { + return provider; + } + + public long getMinimumExpiryMillis() { + return minimumExpiryMillis; + } + + public Builder withMinimumExpiryMillis(long amount, TimeUnit unit) { + this.minimumExpiryMillis = TimeUnit.MILLISECONDS.convert(amount, unit); + return this; + } + + public ElasticContainerRegistryAuthSupplier build() { + // log some sort of identifier for the credentials, which requires looking at the + // instance type + log.info("Loading credentials from " + provider.getClass().getName()); + + return new ElasticContainerRegistryAuthSupplier(getMinimumExpiryMillis(), getProvider()); + } + } + + private final Clock clock; + private final long minimumExpiryMillis; + private final Authenticator.Factory authenticatorFactory; + + /** + * This is the "current" token. In ECR, most of the activity involves doing + * pushes and pulls from your "default" registry, which is scoped to your + * account. As a result, we only cache the most recent token. It would not be + * difficult to expand this to a Guava Cache if needed. + */ + private Authenticator.Authentication authentication; + + @VisibleForTesting + ElasticContainerRegistryAuthSupplier( + final long minimumExpiryMillis, + final AWSCredentialsProvider provider) { + this(Clock.SYSTEM, minimumExpiryMillis, new EcrAuthenticatorFactory(provider)); + } + + + @VisibleForTesting + ElasticContainerRegistryAuthSupplier( + final Clock clock, + final long minimumExpiryMillis, + final Authenticator.Factory authenticatorFactory) { + this.clock = clock; + this.minimumExpiryMillis = minimumExpiryMillis; + this.authenticatorFactory = authenticatorFactory; + } + + /** + * @param registry ECR registry, e.g. 123456789.dkr.ecr.us-east-1.amazonaws.com. If + * null, this method returns a token for the user's default registry. + */ + protected RegistryAuth authForRegistry(final String registry) throws DockerException { + if (registry != null) { + final String[] registryParts = registry.split("\\.", 6); + if (registryParts.length < 6) { + // not an ECR registry + return null; + } + + // TODO: Validate account ID, region + // String accountId = registryParts[0]; + String dkr = registryParts[1]; + String ecr = registryParts[2]; + // String region = registryParts[3]; + String amazonaws = registryParts[4]; + String com = registryParts[5]; + if (!dkr.equals("dkr") || !ecr.equals("ecr") + || !amazonaws.equals("amazonaws") || !com.equals("com")) { + // not an image on ECR + return null; + } + } + + Authenticator.Authentication result; + if (authentication != null && covers(authentication, registry) && !expired(authentication)) { + result = authentication; + } else { + result = authentication = authenticate(registry); + } + + return result != null ? result.toRegistryAuth() : null; + } + + @Override + public RegistryAuth authFor(final String imageName) throws DockerException { + final String[] imageParts = imageName.split("/", 2); + if (imageParts.length < 2) { + // not an image on ECR + return null; + } + + // Example: 123456789.dkr.ecr.us-east-1.amazonaws.com + final String registry = imageParts[0]; + + return authForRegistry(registry); + } + + @Override + public RegistryAuth authForSwarm() throws DockerException { + return authForRegistry(null); + } + + @Override + public RegistryConfigs authForBuild() throws DockerException { + Authenticator.Authentication result; + if (authentication != null && covers(authentication, null) && !expired(authentication)) { + result = authentication; + } else { + result = authentication = authenticate(null); + } + return RegistryConfigs.create( + Collections.singletonMap(result.registry, result.toRegistryAuth())); + } + + protected Authenticator.Authentication authenticate(String registry) throws DockerException { + Authenticator.Authentication result; + try (Authenticator authenticator = getAuthenticatorFactory().getAuthenticator()) { + result = authenticator.authenticate(registry); + } + return result; + } + + protected boolean expired(Authenticator.Authentication authentication) { + boolean result; + + if (authentication.expiration == -1) { + result = false; + } else { + long now = clock.currentTimeMillis(); + long then = now + minimumExpiryMillis; + if (then > authentication.expiration) { + result = true; + } else { + result = false; + } + } + + return result; + } + + private Authenticator.Factory getAuthenticatorFactory() { + return authenticatorFactory; + } + + private static boolean covers(Authenticator.Authentication authentication, String registry) { + boolean result; + + if (authentication.def) { + result = registry == null || registry.equals(authentication.registry); + } else { + result = Objects.equals(registry, authentication.registry); + } + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/spotify/docker/client/auth/ecr/package-info.java b/src/main/java/com/spotify/docker/client/auth/ecr/package-info.java new file mode 100644 index 000000000..22b4d8fd5 --- /dev/null +++ b/src/main/java/com/spotify/docker/client/auth/ecr/package-info.java @@ -0,0 +1,24 @@ +/*- + * -\-\- + * docker-client + * -- + * Copyright (C) 2016 - 2017 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +/** + * Support for authenticating with Amazon Elastic Container Registry. + */ +package com.spotify.docker.client.auth.ecr; diff --git a/src/test/java/com/spotify/docker/client/auth/ecr/ElasticContainerRegistryAuthSupplierTest.java b/src/test/java/com/spotify/docker/client/auth/ecr/ElasticContainerRegistryAuthSupplierTest.java new file mode 100644 index 000000000..e36acc22d --- /dev/null +++ b/src/test/java/com/spotify/docker/client/auth/ecr/ElasticContainerRegistryAuthSupplierTest.java @@ -0,0 +1,420 @@ +/*- + * -\-\- + * docker-client + * -- + * Copyright (C) 2016 - 2017 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ + +package com.spotify.docker.client.auth.ecr; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.everyItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.client.util.Clock; + +import com.spotify.docker.client.exceptions.DockerException; +import com.spotify.docker.client.messages.RegistryAuth; +import com.spotify.docker.client.messages.RegistryConfigs; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.joda.time.DateTime; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class ElasticContainerRegistryAuthSupplierTest { + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + private final int minimumExpirationSecs = 30; + private final DateTime now = new DateTime(2000, 1, 1, 12, 0, 0); + private final DateTime expiration = now.plusSeconds(2 * minimumExpirationSecs); + + private final Clock clock = mock(Clock.class); + + private final Authenticator.Factory authenticatorFactory = mock(Authenticator.Factory.class); + + private final ElasticContainerRegistryAuthSupplier supplier = + new ElasticContainerRegistryAuthSupplier( + clock, + TimeUnit.SECONDS.toMillis(minimumExpirationSecs), + authenticatorFactory); + + public static final String REGISTRY1 = + "123456789012.dkr.ecr.us-east-1.amazonaws.com"; + + public static final String IMAGE1 = + "123456789012.dkr.ecr.us-east-1.amazonaws.com/foo/barticus:latest"; + + @Test + public void testAuthForImage_NoRefresh() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + expiration.getMillis(), + false); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(REGISTRY1)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(now.plusSeconds(minimumExpirationSecs - 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + verify(authenticator, times(1)).authenticate(REGISTRY1); + } + + @Test + public void testAuthForImage_RefreshNeeded() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + expiration.getMillis(), + false); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(REGISTRY1)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(now.plusSeconds(minimumExpirationSecs + 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + verify(authenticator, times(2)).authenticate(REGISTRY1); + } + + @Test + public void testAuthForImage_TokenExpired() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + expiration.getMillis(), + false); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(REGISTRY1)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(expiration.plusSeconds(minimumExpirationSecs + 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + verify(authenticator, times(2)).authenticate(REGISTRY1); + } + + @Test + public void testAuthForImage_NonEcrImage() throws Exception { + when(clock.currentTimeMillis()) + .thenReturn(expiration.minusSeconds(minimumExpirationSecs + 1).getMillis()); + + assertThat(supplier.authFor("foobar"), is(nullValue())); + } + + @Test + public void testAuthForImage_ExceptionOnRefresh() throws Exception { + final DockerException ex = new DockerException("failure!!"); + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(REGISTRY1)) + .thenThrow(ex); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + // the exception should propagate up + exception.expect(DockerException.class); + + supplier.authFor(IMAGE1); + } + + @Test + public void testAuthForImage_TokenWithoutExpirationDoesNotCauseRefresh() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + -1L, + false); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(REGISTRY1)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(expiration.plusSeconds(minimumExpirationSecs + 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + assertThat(supplier.authFor(IMAGE1), is(auth)); + + verify(authenticator, times(1)).authenticate(REGISTRY1); + } + + @Test + public void testAuthForSwarm_NoRefresh() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + expiration.getMillis(), + true); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(now.plusSeconds(minimumExpirationSecs - 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + assertThat(supplier.authForSwarm(), is(auth)); + + assertThat(supplier.authForSwarm(), is(auth)); + + verify(authenticator, times(1)).authenticate(null); + } + + @Test + public void testAuthForSwarm_RefreshNeeded() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + expiration.getMillis(), + true); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(now.plusSeconds(minimumExpirationSecs + 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + assertThat(supplier.authForSwarm(), is(auth)); + + assertThat(supplier.authForSwarm(), is(auth)); + + verify(authenticator, times(2)).authenticate(null); + } + + @Test + public void testAuthForSwarm_ExceptionOnRefresh() throws Exception { + final DockerException ex = new DockerException("failure!!"); + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenThrow(ex); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + // the exception should propagate up + exception.expect(DockerException.class); + + supplier.authForSwarm(); + } + + @Test + public void testAuthForSwarm_TokenWithoutExpirationDoesNotCauseRefresh() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + -1L, + true); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(expiration.plusSeconds(minimumExpirationSecs + 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + assertThat(supplier.authForSwarm(), is(auth)); + + assertThat(supplier.authForSwarm(), is(auth)); + + verify(authenticator, times(1)).authenticate(null); + } + + @Test + public void testAuthForBuild_NoRefresh() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + expiration.getMillis(), + true); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(now.plusSeconds(minimumExpirationSecs - 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + RegistryConfigs configs1 = supplier.authForBuild(); + assertThat(configs1.configs().values(), is(not(empty()))); + assertThat(configs1.configs().values(), everyItem(is(auth))); + + RegistryConfigs configs2 = supplier.authForBuild(); + assertThat(configs2.configs().values(), is(not(empty()))); + assertThat(configs2.configs().values(), everyItem(is(auth))); + + verify(authenticator, times(1)).authenticate(null); + } + + @Test + public void testAuthForBuild_RefreshNeeded() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + expiration.getMillis(), + true); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(now.plusSeconds(minimumExpirationSecs + 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + RegistryConfigs configs1 = supplier.authForBuild(); + assertThat(configs1.configs().values(), is(not(empty()))); + assertThat(configs1.configs().values(), everyItem(is(auth))); + + RegistryConfigs configs2 = supplier.authForBuild(); + assertThat(configs2.configs().values(), is(not(empty()))); + assertThat(configs2.configs().values(), everyItem(is(auth))); + + verify(authenticator, times(2)).authenticate(null); + } + + @Test + public void testAuthForBuild_ExceptionOnRefresh() throws Exception { + final DockerException ex = new DockerException("failure!!"); + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenThrow(ex); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + // the exception should propagate up + exception.expect(DockerException.class); + + supplier.authForBuild(); + } + + @Test + public void testAuthForBuild_TokenWithoutExpirationDoesNotCauseRefresh() throws Exception { + Authenticator.Authentication authentication = new Authenticator.Authentication( + UUID.randomUUID().toString(), + UUID.randomUUID().toString(), + REGISTRY1, + -1L, + true); + + Authenticator authenticator = mock(Authenticator.class); + when(authenticator.authenticate(null)) + .thenReturn(authentication); + + when(authenticatorFactory.getAuthenticator()) + .thenReturn(authenticator); + + when(clock.currentTimeMillis()) + .thenReturn(expiration.plusSeconds(minimumExpirationSecs + 1).getMillis()); + + RegistryAuth auth = authentication.toRegistryAuth(); + + RegistryConfigs configs1 = supplier.authForBuild(); + assertThat(configs1.configs().values(), is(not(empty()))); + assertThat(configs1.configs().values(), everyItem(is(auth))); + + RegistryConfigs configs2 = supplier.authForBuild(); + assertThat(configs2.configs().values(), is(not(empty()))); + assertThat(configs2.configs().values(), everyItem(is(auth))); + + verify(authenticator, times(1)).authenticate(null); + } +}