diff --git a/CHANGELOG.md b/CHANGELOG.md index 4248b8faa8..bb4c47a907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Usage: * Fix #705: JIB assembly works on Windows * Fix #714: feat: Helm support for Golang expressions * Port fabric8io/docker-maven-plugin#1318: Update ECR autorization token URL +* Port fabric8io/docker-maven-plugin#1311: Use AWS SDK to fetch AWS credentials * Fix #710: Support DockerImage as output for Openshift builds ### 1.3.0 diff --git a/jkube-kit/build/api/src/main/java/org/eclipse/jkube/kit/build/api/auth/AuthConfig.java b/jkube-kit/build/api/src/main/java/org/eclipse/jkube/kit/build/api/auth/AuthConfig.java index 024c0619bc..e6eccd1ead 100644 --- a/jkube-kit/build/api/src/main/java/org/eclipse/jkube/kit/build/api/auth/AuthConfig.java +++ b/jkube-kit/build/api/src/main/java/org/eclipse/jkube/kit/build/api/auth/AuthConfig.java @@ -19,6 +19,7 @@ import lombok.Getter; import org.apache.commons.codec.binary.Base64; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; @@ -35,35 +36,50 @@ @EqualsAndHashCode public class AuthConfig { - public static final AuthConfig EMPTY_AUTH_CONFIG = new AuthConfig("", "", "", ""); + public static final AuthConfig EMPTY_AUTH_CONFIG = new AuthConfig("", "", "", "", ""); private final String username; private final String password; private final String email; private final String auth; + private final String identityToken; private String authEncoded; @Builder - public AuthConfig(String username, String password, String email, String auth) { + public AuthConfig(String username, String password, String email, String auth, String identityToken) { this.username = username; this.password = password; this.email = email; this.auth = auth; + this.identityToken = identityToken; authEncoded = createAuthEncoded(); } + public AuthConfig(String username, String password, String email, String auth) { + this(username, password, email, auth, null); + } + public String toHeaderValue() { return authEncoded; } private String createAuthEncoded() { JsonObject ret = new JsonObject(); - putNonNull(ret, "username", username); - putNonNull(ret, "password", password); - putNonNull(ret, "email", email); - putNonNull(ret, "auth", auth); - return encodeBase64ChunkedURLSafeString(ret.toString().getBytes(StandardCharsets.UTF_8)); + if(identityToken != null) { + putNonNull(ret, "identityToken", identityToken); + } else { + putNonNull(ret, "username", username); + putNonNull(ret, "password", password); + putNonNull(ret, "email", email); + putNonNull(ret, "auth", auth); + } + + try { + return encodeBase64ChunkedURLSafeString(ret.toString().getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + return encodeBase64ChunkedURLSafeString(ret.toString().getBytes()); + } } public static AuthConfig fromMap(Map params) { diff --git a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactory.java b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactory.java index 8d0c5a24a7..fc625b53f2 100644 --- a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactory.java +++ b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactory.java @@ -17,6 +17,8 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jkube.kit.build.service.docker.auth.ecr.AwsSdkAuthConfigFactory; +import org.eclipse.jkube.kit.build.service.docker.auth.ecr.AwsSdkHelper; import org.eclipse.jkube.kit.common.RegistryServerConfiguration; import org.eclipse.jkube.kit.build.api.helper.DockerFileUtil; import org.eclipse.jkube.kit.build.api.auth.AuthConfig; @@ -35,11 +37,12 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -63,12 +66,18 @@ public class AuthConfigFactory { private static final String DOCKER_LOGIN_DEFAULT_REGISTRY = "https://index.docker.io/v1/"; private final KitLogger log; + private AwsSdkHelper awsSdkHelper; private static final String[] DEFAULT_REGISTRIES = new String[]{ "docker.io", "index.docker.io", "registry.hub.docker.com" }; public AuthConfigFactory(KitLogger log) { + this(log, new AwsSdkHelper()); + } + + AuthConfigFactory(KitLogger log, AwsSdkHelper awsSdkHelper) { this.log = log; + this.awsSdkHelper = awsSdkHelper; } /** @@ -111,7 +120,7 @@ public AuthConfigFactory(KitLogger log) { public AuthConfig createAuthConfig(boolean isPush, boolean skipExtendedAuth, Map authConfig, List settings, String user, String registry, UnaryOperator passwordDecryptionMethod) throws IOException { - AuthConfig ret = createStandardAuthConfig(isPush, authConfig, settings, user, registry, passwordDecryptionMethod, log); + AuthConfig ret = createStandardAuthConfig(isPush, authConfig, settings, user, registry, passwordDecryptionMethod, log, awsSdkHelper); if (ret != null) { if (registry == null || skipExtendedAuth) { return ret; @@ -185,7 +194,7 @@ private AuthConfig extendedAuthentication(AuthConfig standardAuthConfig, String * * @throws IOException any exception in case of fetching authConfig */ - private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfigMap, List settings, String user, String registry, UnaryOperator passwordDecryptionMethod, KitLogger log) + private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfigMap, List settings, String user, String registry, UnaryOperator passwordDecryptionMethod, KitLogger log, AwsSdkHelper awsSdkHelper) throws IOException { AuthConfig ret; @@ -225,6 +234,18 @@ private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfi // check EC2 instance role if registry is ECR if (EcrExtendedAuth.isAwsRegistry(registry)) { + ret = getAuthConfigViaAwsSdk(awsSdkHelper, log); + if (ret != null) { + log.debug("AuthConfig: AWS credentials from AWS SDK"); + return ret; + } + + ret = getAuthConfigFromAwsEnvironmentVariables(awsSdkHelper, log); + if (ret != null) { + log.debug("AuthConfig: AWS credentials from ENV variables"); + return ret; + } + try { ret = getAuthConfigFromEC2InstanceRole(log); } catch (ConnectTimeoutException ex) { @@ -238,6 +259,18 @@ private static AuthConfig createStandardAuthConfig(boolean isPush, Map authConfi log.debug("AuthConfig: credentials from EC2 instance role"); return ret; } + try { + ret = getAuthConfigFromTaskRole(awsSdkHelper, log); + } catch (ConnectTimeoutException ex) { + log.debug("Connection timeout while retrieving ECS meta-data, likely not an ECS instance (%s)", + ex.getMessage()); + } catch (IOException ex) { + log.warn("Error while retrieving ECS Task role credentials: %s", ex.getMessage()); + } + if (ret != null) { + log.debug("AuthConfig: credentials from ECS Task role"); + return ret; + } } // No authentication found @@ -433,6 +466,103 @@ private static JsonObject getCredentialsNode(JsonObject auths,String registryToL return null; } + // if the local credentials don't contain user and password & is not a EC2 instance, + // use ECS|Fargate Task instance role credentials + private static AuthConfig getAuthConfigFromTaskRole(AwsSdkHelper awsSdkHelper, KitLogger log) throws IOException { + log.debug("No user and password set for ECR, checking ECS Task role"); + URI uri = getMetadataEndpointForCredentials(awsSdkHelper, log); + if (uri == null) { + return null; + } + // get temporary credentials + log.debug("Getting temporary security credentials from: %s", uri); + try (CloseableHttpClient client = HttpClients.custom().useSystemProperties().build()) { + RequestConfig conf = + RequestConfig.custom().setConnectionRequestTimeout(1000).setConnectTimeout(1000) + .setSocketTimeout(1000).build(); + HttpGet request = new HttpGet(uri); + request.setConfig(conf); + return readAwsCredentials(client, request, log); + } + } + + + private static AuthConfig readAwsCredentials(CloseableHttpClient client, HttpGet request, KitLogger log) throws IOException { + try (CloseableHttpResponse response = client.execute(request)) { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + log.debug("No security credential found, return code was %d", + response.getStatusLine().getStatusCode()); + // no instance role found + return null; + } + + // read instance role + try (Reader r = new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)) { + JsonObject securityCredentials = new Gson().fromJson(r, JsonObject.class); + + String user = securityCredentials.getAsJsonPrimitive("AccessKeyId").getAsString(); + String password = securityCredentials.getAsJsonPrimitive("SecretAccessKey").getAsString(); + String token = securityCredentials.getAsJsonPrimitive("Token").getAsString(); + + log.debug("Received temporary access key %s...", user.substring(0, 8)); + return new AuthConfig(user, password, "none", token); + } + } + } + + private static URI getMetadataEndpointForCredentials(AwsSdkHelper awsSdkHelper, KitLogger log) { + // get ECS task role - if available + String awsContainerCredentialsUri = awsSdkHelper.getAwsContainerCredentialsRelativeUri(); + if (awsContainerCredentialsUri == null) { + log.debug("System environment not set for variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI, no task role found"); + return null; + } + if (awsContainerCredentialsUri.charAt(0) != '/') { + awsContainerCredentialsUri = "/" + awsContainerCredentialsUri; + } + + String ecsMetadataEndpoint = awsSdkHelper.getEcsMetadataEndpoint(); + if (ecsMetadataEndpoint == null) { + ecsMetadataEndpoint = "http://169.254.170.2"; + } + + try { + return new URI(ecsMetadataEndpoint + awsContainerCredentialsUri); + } catch (URISyntaxException e) { + log.warn("Failed to construct path to ECS metadata endpoint for credentials", e); + return null; + } + } + + private static AuthConfig getAuthConfigViaAwsSdk(AwsSdkHelper awsSdkHelper, KitLogger log) { + boolean credProviderPresent = awsSdkHelper.isDefaultAWSCredentialsProviderChainPresentInClassPath(); + if (!credProviderPresent) { + log.info("It appears that you're using AWS ECR." + + " Consider integrating the AWS SDK in order to make use of common AWS authentication mechanisms," + + " see https://dmp.fabric8.io/#extended-authentication"); + return null; + } + return new AwsSdkAuthConfigFactory(log, awsSdkHelper).createAuthConfig(); + } + + /** + * Try using the AWS credentials provided via ENV variables. + * See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html + */ + private static AuthConfig getAuthConfigFromAwsEnvironmentVariables(AwsSdkHelper awsSdkHelper, KitLogger log) { + String accessKeyId = awsSdkHelper.getAwsAccessKeyIdEnvVar(); + if (accessKeyId == null) { + log.debug("System environment not set for variable AWS_ACCESS_KEY_ID, no AWS credentials found"); + return null; + } + String secretAccessKey = awsSdkHelper.getAwsSecretAccessKeyEnvVar(); + if (secretAccessKey == null) { + log.warn("System environment set for variable AWS_ACCESS_KEY_ID, but NOT for variable AWS_SECRET_ACCESS_KEY!"); + return null; + } + return new AuthConfig(accessKeyId, secretAccessKey, "none", awsSdkHelper.getAwsSessionTokenEnvVar()); + } + // ======================================================================================================= private static Map getAuthConfigMapToCheck(LookupMode lookupMode, Map authConfigMap) { diff --git a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkAuthConfigFactory.java b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkAuthConfigFactory.java new file mode 100644 index 0000000000..729611cb8e --- /dev/null +++ b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkAuthConfigFactory.java @@ -0,0 +1,51 @@ +package org.eclipse.jkube.kit.build.service.docker.auth.ecr; + +import org.eclipse.jkube.kit.build.api.auth.AuthConfig; +import org.eclipse.jkube.kit.common.KitLogger; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class AwsSdkAuthConfigFactory { + + private final KitLogger log; + private AwsSdkHelper awsSdkHelper; + + public AwsSdkAuthConfigFactory(KitLogger log, AwsSdkHelper awsSdkHelper) { + this.log = log; + this.awsSdkHelper = awsSdkHelper; + } + + public AuthConfig createAuthConfig() { + try { + Object credentials = awsSdkHelper.getCredentialsFromDefaultAWSCredentialsProviderChain(); + if (credentials == null) { + return null; + } + + return new AuthConfig( + awsSdkHelper.getAWSAccessKeyIdFromCredentials(credentials), + awsSdkHelper.getAwsSecretKeyFromCredentials(credentials), + "none", + awsSdkHelper.getSessionTokenFromCrendentials(credentials) + ); + } catch (Exception t) { + String issueTitle = null; + try { + issueTitle = URLEncoder.encode("Failed calling AWS SDK: " + t.getMessage(), UTF_8.name()); + } catch (UnsupportedEncodingException ignore) { + } + log.warn("Failed to fetch AWS credentials: %s", t.getMessage()); + if (t.getCause() != null) { + log.warn("Caused by: %s", t.getCause().getMessage()); + } + log.warn("Please report a bug at https://github.com/eclipse/jkube/issues/new?%s", + issueTitle == null ? "" : "title=?" + issueTitle); + log.warn("%s", t); + return null; + } + } + +} diff --git a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkHelper.java b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkHelper.java new file mode 100644 index 0000000000..d04ca15d98 --- /dev/null +++ b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkHelper.java @@ -0,0 +1,62 @@ +package org.eclipse.jkube.kit.build.service.docker.auth.ecr; + +import java.lang.reflect.InvocationTargetException; + +public class AwsSdkHelper { + public static final String ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; + public static final String SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; + public static final String SESSION_TOKEN = "AWS_SESSION_TOKEN"; + public static final String CONTAINER_CREDENTIALS_RELATIVE_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; + public static final String METADATA_ENDPOINT = "ECS_METADATA_ENDPOINT"; + + public boolean isDefaultAWSCredentialsProviderChainPresentInClassPath() { + try { + Class.forName("com.amazonaws.auth.DefaultAWSCredentialsProviderChain"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + public String getAwsAccessKeyIdEnvVar() { + return System.getenv(ACCESS_KEY_ID); + } + + public String getAwsSecretAccessKeyEnvVar() { + return System.getenv(SECRET_ACCESS_KEY); + } + + public String getAwsSessionTokenEnvVar() { + return System.getenv(SESSION_TOKEN); + } + + public String getAwsContainerCredentialsRelativeUri() { + return System.getenv(CONTAINER_CREDENTIALS_RELATIVE_URI); + } + + public String getEcsMetadataEndpoint() { + return System.getenv(METADATA_ENDPOINT); + } + + public Object getCredentialsFromDefaultAWSCredentialsProviderChain() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Class credentialsProviderChainClass = Class.forName("com.amazonaws.auth.DefaultAWSCredentialsProviderChain"); + Object credentialsProviderChain = credentialsProviderChainClass.getDeclaredConstructor().newInstance(); + return credentialsProviderChainClass.getMethod("getCredentials").invoke(credentialsProviderChain); + } + + public String getSessionTokenFromCrendentials(Object credentials) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class sessionCredentialsClass = Class.forName("com.amazonaws.auth.AWSSessionCredentials"); + return sessionCredentialsClass.isInstance(credentials) + ? (String) sessionCredentialsClass.getMethod("getSessionToken").invoke(credentials) : null; + } + + public String getAWSAccessKeyIdFromCredentials(Object credentials) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class credentialsClass = Class.forName("com.amazonaws.auth.AWSCredentials"); + return (String) credentialsClass.getMethod("getAWSAccessKeyId").invoke(credentials); + } + + public String getAwsSecretKeyFromCredentials(Object credentials) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class credentialsClass = Class.forName("com.amazonaws.auth.AWSCredentials"); + return (String) credentialsClass.getMethod("getAWSSecretKey").invoke(credentials); + } +} diff --git a/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactoryTest.java b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactoryTest.java index a9b1fd617f..b5b49d0dd0 100644 --- a/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactoryTest.java +++ b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/AuthConfigFactoryTest.java @@ -13,28 +13,69 @@ */ package org.eclipse.jkube.kit.build.service.docker.auth; +import com.google.common.collect.ImmutableMap; +import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; +import mockit.Expectations; import mockit.Mock; import mockit.MockUp; import mockit.Mocked; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.bootstrap.HttpServer; +import org.apache.http.impl.bootstrap.ServerBootstrap; import org.eclipse.jkube.kit.build.api.auth.AuthConfig; import org.eclipse.jkube.kit.build.api.helper.DockerFileUtil; +import org.eclipse.jkube.kit.build.service.docker.auth.ecr.AwsSdkAuthConfigFactory; +import org.eclipse.jkube.kit.build.service.docker.auth.ecr.AwsSdkHelper; import org.eclipse.jkube.kit.common.KitLogger; import org.eclipse.jkube.kit.common.RegistryServerConfiguration; import org.eclipse.jkube.kit.common.SystemMock; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import static java.util.UUID.randomUUID; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; public class AuthConfigFactoryTest { + public static final String ECR_NAME = "123456789012.dkr.ecr.bla.amazonaws.com"; + private AuthConfigFactory factory; + private GsonBuilder gsonBuilder; + + @Mocked + private KitLogger log; + + @Mocked + private AwsSdkHelper awsSdkHelper; + + private HttpServer httpServer; + + @Before + public void containerSetup() { + factory = new AuthConfigFactory(log, awsSdkHelper); + gsonBuilder = new GsonBuilder(); + } + + @After + public void shutdownHttpServer() { + if (httpServer != null) { + httpServer.stop(); + httpServer = null; + } + } + @Test public void testGetAuthConfigFromSystemProperties() throws IOException { // Given @@ -197,6 +238,175 @@ public void testGetStandardAuthConfigFromMavenSettings(@Mocked KitLogger logger) assertAuthConfig(authConfig, "testuser", "testpass"); } + @Test + public void getAuthConfigViaAwsSdk() throws IOException { + new Expectations() {{ + awsSdkHelper.isDefaultAWSCredentialsProviderChainPresentInClassPath(); + result = true; + }}; + String accessKeyId = randomUUID().toString(); + String secretAccessKey = randomUUID().toString(); + new MockedAwsSdkAuthConfigFactory(accessKeyId, secretAccessKey); + + AuthConfig authConfig = factory.createAuthConfig(false, true, null, Collections.emptyList(), "user", ECR_NAME, s -> s); + + verifyAuthConfig(authConfig, accessKeyId, secretAccessKey, null, null); + } + + @Test + public void ecsTaskRole() throws IOException { + givenAwsSdkIsDisabled(); + String containerCredentialsUri = "/v2/credentials/" + randomUUID().toString(); + String accessKeyId = randomUUID().toString(); + String secretAccessKey = randomUUID().toString(); + String sessionToken = randomUUID().toString(); + givenEcsMetadataService(containerCredentialsUri, accessKeyId, secretAccessKey, sessionToken); + setupEcsMetadataConfiguration(httpServer, containerCredentialsUri); + + AuthConfig authConfig = factory.createAuthConfig(false, true, null, Collections.emptyList(), "user", ECR_NAME, s -> s); + + verifyAuthConfig(authConfig, accessKeyId, secretAccessKey, null, sessionToken); + } + + @Test + public void fargateTaskRole() throws IOException { + givenAwsSdkIsDisabled(); + String containerCredentialsUri = "v2/credentials/" + randomUUID(); + String accessKeyId = randomUUID().toString(); + String secretAccessKey = randomUUID().toString(); + String sessionToken = randomUUID().toString(); + givenEcsMetadataService("/" + containerCredentialsUri, accessKeyId, secretAccessKey, sessionToken); + setupEcsMetadataConfiguration(httpServer, containerCredentialsUri); + + AuthConfig authConfig = factory.createAuthConfig(false, true, null, Collections.emptyList(), "user", ECR_NAME, s -> s); + + verifyAuthConfig(authConfig, accessKeyId, secretAccessKey, null, sessionToken); + } + + @Test + public void awsTemporaryCredentialsArePickedUpFromEnvironment() throws IOException { + givenAwsSdkIsDisabled(); + String accessKeyId = randomUUID().toString(); + String secretAccessKey = randomUUID().toString(); + String sessionToken = randomUUID().toString(); + new Expectations() {{ + awsSdkHelper.getAwsAccessKeyIdEnvVar(); + result = accessKeyId; + awsSdkHelper.getAwsSecretAccessKeyEnvVar(); + result = secretAccessKey; + awsSdkHelper.getAwsSessionTokenEnvVar(); + result = sessionToken; + }}; + + AuthConfig authConfig = factory.createAuthConfig(false, true, null, Collections.emptyList(), "user", ECR_NAME, s -> s); + + verifyAuthConfig(authConfig, accessKeyId, secretAccessKey, null, sessionToken); + } + + @Test + public void awsStaticCredentialsArePickedUpFromEnvironment() throws IOException { + givenAwsSdkIsDisabled(); + String accessKeyId = randomUUID().toString(); + String secretAccessKey = randomUUID().toString(); + new Expectations() {{ + awsSdkHelper.getAwsAccessKeyIdEnvVar(); + result = accessKeyId; + awsSdkHelper.getAwsSecretAccessKeyEnvVar(); + result = secretAccessKey; + }}; + + AuthConfig authConfig = factory.createAuthConfig(false, true, null, Collections.emptyList(), "user", ECR_NAME, s -> s); + + verifyAuthConfig(authConfig, accessKeyId, secretAccessKey, null, null); + } + + @Test + public void incompleteAwsCredentialsAreIgnored() throws IOException { + givenAwsSdkIsDisabled(); + System.setProperty("AWS_ACCESS_KEY_ID", randomUUID().toString()); + + AuthConfig authConfig = factory.createAuthConfig(false, true, null, Collections.emptyList(), "user", ECR_NAME, s -> s); + + assertNull(authConfig); + } + + private void givenEcsMetadataService(String containerCredentialsUri, String accessKeyId, String secretAccessKey, String sessionToken) throws IOException { + httpServer = + ServerBootstrap.bootstrap() + .setLocalAddress(InetAddress.getLoopbackAddress()) + .registerHandler("*", (request, response, context) -> { + System.out.println("REQUEST: " + request.getRequestLine()); + if (containerCredentialsUri.matches(request.getRequestLine().getUri())) { + response.setEntity(new StringEntity(gsonBuilder.create().toJson(ImmutableMap.of( + "AccessKeyId", accessKeyId, + "SecretAccessKey", secretAccessKey, + "Token", sessionToken + )))); + } else { + response.setStatusCode(SC_NOT_FOUND); + } + }) + .create(); + httpServer.start(); + } + + private void setupEcsMetadataConfiguration(HttpServer httpServer, String containerCredentialsUri) { + new Expectations() {{ + awsSdkHelper.getEcsMetadataEndpoint(); + result = "http://" + + httpServer.getInetAddress().getHostAddress()+":" + httpServer.getLocalPort(); + + awsSdkHelper.getAwsContainerCredentialsRelativeUri(); + result = containerCredentialsUri; + }}; + } + + private static void givenAwsSdkIsDisabled() { + new DisableAwsSdkAuthConfigFactory(); + } + + private void verifyAuthConfig(AuthConfig config, String username, String password, String email, String auth) { + assertNotNull(config); + JsonObject params = gsonBuilder.create().fromJson(new String(Base64.decodeBase64(config.toHeaderValue().getBytes())), JsonObject.class); + assertEquals(username, params.get("username").getAsString()); + assertEquals(password, params.get("password").getAsString()); + if (email != null) { + assertEquals(email, params.get("email").getAsString()); + } + if (auth != null) { + assertEquals(auth, params.get("auth").getAsString()); + } + } + + private static class MockedAwsSdkAuthConfigFactory extends MockUp { + private final String accessKeyId; + private final String secretAccessKey; + + public MockedAwsSdkAuthConfigFactory(String accessKeyId, String secretAccessKey) { + this.accessKeyId = accessKeyId; + this.secretAccessKey = secretAccessKey; + } + + @Mock + public void $init(KitLogger log) { } + + @Mock + public AuthConfig createAuthConfig() { + return new AuthConfig(accessKeyId, secretAccessKey, null,null, null); + } + + } + + private static class DisableAwsSdkAuthConfigFactory extends MockUp { + @Mock + public void $init(KitLogger log) { } + + @Mock + public AuthConfig createAuthConfig() { + return null; + } + } + private void assertAuthConfig(AuthConfig authConfig, String username, String password) { assertNotNull(authConfig); assertEquals(username, authConfig.getUsername()); diff --git a/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkAuthConfigFactoryTest.java b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkAuthConfigFactoryTest.java new file mode 100644 index 0000000000..ad605fe7b2 --- /dev/null +++ b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/AwsSdkAuthConfigFactoryTest.java @@ -0,0 +1,84 @@ +package org.eclipse.jkube.kit.build.service.docker.auth.ecr; + +import mockit.Expectations; +import mockit.Mocked; +import org.eclipse.jkube.kit.build.api.auth.AuthConfig; +import org.eclipse.jkube.kit.common.KitLogger; +import org.junit.Before; +import org.junit.Test; + +import static java.util.UUID.randomUUID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class AwsSdkAuthConfigFactoryTest { + @Mocked + private KitLogger log; + + @Mocked + private AwsSdkHelper awsSdkHelper; + + private AwsSdkAuthConfigFactory objectUnderTest; + + @Before + public void setup() { + objectUnderTest = new AwsSdkAuthConfigFactory(log, awsSdkHelper); + } + + @Test + public void nullValueIsPassedOn() { + AuthConfig authConfig = objectUnderTest.createAuthConfig(); + + assertNull(authConfig); + } + + @Test + public void reflectionWorksForBasicCredentials() throws Exception { + String accessKey = randomUUID().toString(); + String secretKey = randomUUID().toString(); + Object credentials = new Object(); + new Expectations() {{ + awsSdkHelper.getCredentialsFromDefaultAWSCredentialsProviderChain(); + result = credentials; + awsSdkHelper.getAWSAccessKeyIdFromCredentials(any); + result = accessKey; + awsSdkHelper.getAwsSecretKeyFromCredentials(any); + result = secretKey; + }}; + + AuthConfig authConfig = objectUnderTest.createAuthConfig(); + + assertNotNull(authConfig); + assertEquals(accessKey, authConfig.getUsername()); + assertEquals(secretKey, authConfig.getPassword()); + assertNull(authConfig.getAuth()); + assertNull(authConfig.getIdentityToken()); + } + + @Test + public void reflectionWorksForSessionCredentials() throws Exception { + String accessKey = randomUUID().toString(); + String secretKey = randomUUID().toString(); + String sessionToken = randomUUID().toString(); + Object credentials = new Object(); + new Expectations() {{ + awsSdkHelper.getCredentialsFromDefaultAWSCredentialsProviderChain(); + result = credentials; + awsSdkHelper.getAWSAccessKeyIdFromCredentials(any); + result = accessKey; + awsSdkHelper.getAwsSecretKeyFromCredentials(any); + result = secretKey; + awsSdkHelper.getSessionTokenFromCrendentials(any); + result = sessionToken; + }}; + AuthConfig authConfig = objectUnderTest.createAuthConfig(); + + assertNotNull(authConfig); + assertEquals(accessKey, authConfig.getUsername()); + assertEquals(secretKey, authConfig.getPassword()); + assertEquals(sessionToken, authConfig.getAuth()); + assertNull(authConfig.getIdentityToken()); + } + +} \ No newline at end of file diff --git a/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/_authentication.adoc b/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/_authentication.adoc index 1dcbef9dd6..5a5378f5d0 100644 --- a/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/_authentication.adoc +++ b/kubernetes-maven-plugin/doc/src/main/asciidoc/inc/_authentication.adoc @@ -176,7 +176,7 @@ password. Some docker registries require additional steps to authenticate. link:https://docs.aws.amazon.com/AmazonECR/latest/userguide/ECR_GetStarted.html[Amazon ECR] requires using an IAM access key to obtain temporary docker login credentials. -The <> and <> goals automatically execute this exchange for any registry of the form +The {goal-prefix}:push and {goal-prefix}:build goals automatically execute this exchange for any registry of the form __ *.dkr.ecr.* __ *.amazonaws.com*, unless the `skipExtendedAuth` configuration (`jkube.docker.skip.extendedAuth` property) is set true. @@ -185,10 +185,38 @@ Note that for an ECR repository with URI `123456789012.dkr.ecr.eu-west-1.amazona You can use any IAM access key with the necessary permissions in any of the locations mentioned above except `~/.docker/config.json`. Use the IAM *Access key ID* as the username and the *Secret access key* as the password. In case you're using temporary security credentials provided by the AWS Security Token Service (AWS STS), you have to provide the *security token* as well. -To do so, either specify the `docker.authToken` system property or provide an `` element alongside username & password in the `authConfig`. +To do so, either specify the an `` element alongside username & password in the `authConfig`. -In case you are running on an EC2 instance that has an appropriate IAM role assigned -(e.g. a role that grants the AWS built-in policy _AmazonEC2ContainerRegistryPowerUser_) -authentication information doesn't need to be provided at all. Instead the instance -meta-data service is queried for temporary access credentials supplied by the -assigned role. +Plugin will attempt to read AWS credentials from some well-known spots in case there is no explicit configuration: + +* it will pick up ENV variables link:https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html[as documented for the AWS CLI] + +* it will pick up temporary credentials of link:https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html[the IAM role of an EC2 instance] + +* it will pick up temporary credentials of link:https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html[the IAM role of a fargate task (OR ECS with EC2 with ECS_AWSVPC_BLOCK_IMDS as "true")] + +If any of these authentication information is accessible, it will be used. + +[NOTE] +For a more complete, robust and reliable authentication experience, you can add the AWS SDK for Java as a dependency. + +[source,xml] +---- + + + org.eclipse.jkube + kubernetes-maven-plugin + + + com.amazonaws + aws-java-sdk-core + 1.11.707 + + + + +---- + +This extra dependency allows the usage of all link:https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html[options] that the AWS default credential provider chain provides. + +If the AWS SDK is found in the classpath, it takes precedence over the custom AWS credentials lookup mechanisms listed above. \ No newline at end of file