diff --git a/CHANGELOG.md b/CHANGELOG.md index b615c2025c..3fbf1488b0 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 * Fix #548: Define property for skipping cluster autodetect/offline mode 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..f444ce9b0d 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 @@ -18,6 +18,8 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jkube.kit.common.KitLogger; import java.nio.charset.StandardCharsets; import java.util.Map; @@ -35,34 +37,43 @@ @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; - authEncoded = createAuthEncoded(); + this.identityToken = identityToken; } - public String toHeaderValue() { - return authEncoded; + public AuthConfig(String username, String password, String email, String auth) { + this(username, password, email, auth, null); } - private String createAuthEncoded() { + public String toHeaderValue(KitLogger logger) { JsonObject ret = new JsonObject(); - putNonNull(ret, "username", username); - putNonNull(ret, "password", password); - putNonNull(ret, "email", email); - putNonNull(ret, "auth", auth); + if(StringUtils.isNotBlank(identityToken)) { + putNonNull(ret, "identityToken", identityToken); + if (StringUtils.isNotBlank(username)) { + logger.warn("Using identityToken, found username not blank : " + username); + } + } else { + putNonNull(ret, "username", username); + putNonNull(ret, "password", password); + putNonNull(ret, "email", email); + putNonNull(ret, "auth", auth); + } + return encodeBase64ChunkedURLSafeString(ret.toString().getBytes(StandardCharsets.UTF_8)); } diff --git a/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/FromConfigRegistryAuthHandlerTest.java b/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/FromConfigRegistryAuthHandlerTest.java index 45b7cc090b..d10da186fd 100644 --- a/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/FromConfigRegistryAuthHandlerTest.java +++ b/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/FromConfigRegistryAuthHandlerTest.java @@ -88,7 +88,7 @@ public void testFromPluginConfigurationFailed() { } private void verifyAuthConfig(AuthConfig config, String username, String password, String email) { - JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue().getBytes())), JsonObject.class); + JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue(log).getBytes())), JsonObject.class); assertEquals(username, params.get("username").getAsString()); assertEquals(password, params.get("password").getAsString()); if (email != null) { diff --git a/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/OpenShiftRegistryAuthHandlerTest.java b/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/OpenShiftRegistryAuthHandlerTest.java index 6e6516ca34..f5dd5e829d 100644 --- a/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/OpenShiftRegistryAuthHandlerTest.java +++ b/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/OpenShiftRegistryAuthHandlerTest.java @@ -141,7 +141,7 @@ private void createOpenShiftConfig(File homeDir, String testConfig) { } private void verifyAuthConfig(AuthConfig config, String username, String password, String email) { - JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue().getBytes())), JsonObject.class); + JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue(log).getBytes())), JsonObject.class); assertEquals(username,params.get("username").getAsString()); assertEquals(password,params.get("password").getAsString()); if (email != null) { diff --git a/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/SystemPropertyRegistryAuthHandlerTest.java b/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/SystemPropertyRegistryAuthHandlerTest.java index f6ffc11285..2fb6666672 100644 --- a/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/SystemPropertyRegistryAuthHandlerTest.java +++ b/jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/auth/handler/SystemPropertyRegistryAuthHandlerTest.java @@ -100,7 +100,7 @@ private void checkException(String key) throws IOException { } private void verifyAuthConfig(AuthConfig config, String username, String password, String email) { - JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue().getBytes())), JsonObject.class); + JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue(log).getBytes())), JsonObject.class); assertEquals(username,params.get("username").getAsString()); assertEquals(password,params.get("password").getAsString()); if (email != null) { diff --git a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/access/hc/DockerAccessWithHcClient.java b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/access/hc/DockerAccessWithHcClient.java index 9f0fbac351..6168db124e 100644 --- a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/access/hc/DockerAccessWithHcClient.java +++ b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/access/hc/DockerAccessWithHcClient.java @@ -637,7 +637,7 @@ private Map createAuthHeader(AuthConfig authConfig) { if (authConfig == null) { authConfig = AuthConfig.EMPTY_AUTH_CONFIG; } - return Collections.singletonMap("X-Registry-Auth", authConfig.toHeaderValue()); + return Collections.singletonMap("X-Registry-Auth", authConfig.toHeaderValue(log)); } private boolean isRetryableErrorCode(int errorCode) { 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..83b490e704 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 final 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,108 @@ 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 AuthConfig.builder() + .username(user) + .password(password) + .email("none") + .auth(token) + .build(); + } + } + } + + 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; + } + + try { + return new URI(awsSdkHelper.getEcsMetadataEndpoint() + 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://www.eclipse.org/jkube/docs/kubernetes-maven-plugin#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 AuthConfig.builder() + .username(accessKeyId) + .password(secretAccessKey) + .email("none") + .auth(awsSdkHelper.getAwsSessionTokenEnvVar()) + .build(); + } + // ======================================================================================================= 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/CredentialHelperClient.java b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/CredentialHelperClient.java index 18ec63496e..26d23a0862 100644 --- a/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/CredentialHelperClient.java +++ b/jkube-kit/build/service/docker/src/main/java/org/eclipse/jkube/kit/build/service/docker/auth/CredentialHelperClient.java @@ -28,6 +28,7 @@ public class CredentialHelperClient { static final String SECRET_KEY = "Secret"; static final String USERNAME_KEY = "Username"; + static final String TOKEN_USERNAME = ""; private final String credentialHelperName; private final KitLogger log; @@ -61,12 +62,16 @@ public AuthConfig getAuthConfig(String registryToLookup) throws IOException { } } - private AuthConfig toAuthConfig(JsonObject credential){ + AuthConfig toAuthConfig(JsonObject credential){ if (credential == null) { return null; } String password = credential.get(CredentialHelperClient.SECRET_KEY).getAsString(); String userKey = credential.get(CredentialHelperClient.USERNAME_KEY).getAsString(); + if (TOKEN_USERNAME.equals(userKey)) { + // If userKey is , the password is actually a token + return new AuthConfig(null, null, null, null, password); + } return new AuthConfig(userKey,password, null,null); } 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..0bd17c6aea --- /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,64 @@ +/** + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +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 AuthConfig.builder() + .username(awsSdkHelper.getAWSAccessKeyIdFromCredentials(credentials)) + .password(awsSdkHelper.getAwsSecretKeyFromCredentials(credentials)) + .email("none") + .auth(awsSdkHelper.getSessionTokenFromCrendentials(credentials)) + .build(); + } 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..bcaed9d5c1 --- /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,83 @@ +/** + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.build.service.docker.auth.ecr; + +import java.lang.reflect.InvocationTargetException; + +public class AwsSdkHelper { + private static final String ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; + private static final String SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; + private static final String SESSION_TOKEN = "AWS_SESSION_TOKEN"; + private static final String CONTAINER_CREDENTIALS_RELATIVE_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; + private static final String METADATA_ENDPOINT = "ECS_METADATA_ENDPOINT"; + private static final String AWS_INSTANCE_LINK_LOCAL_ADDRESS = "http://169.254.170.2"; + private static final String DEFAULT_AWSCREDENTIALS_PROVIDER_CHAIN = "com.amazonaws.auth.DefaultAWSCredentialsProviderChain"; + private static final String AWS_SESSION_CREDENTIALS = "com.amazonaws.auth.AWSSessionCredentials"; + private static final String AWS_CREDENTIALS = "com.amazonaws.auth.AWSCredentials"; + + public boolean isDefaultAWSCredentialsProviderChainPresentInClassPath() { + try { + Class.forName(DEFAULT_AWSCREDENTIALS_PROVIDER_CHAIN); + 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() { + String endpoint = System.getenv(METADATA_ENDPOINT); + if (endpoint == null) { + return AWS_INSTANCE_LINK_LOCAL_ADDRESS; + } + return endpoint; + } + + public Object getCredentialsFromDefaultAWSCredentialsProviderChain() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + Class credentialsProviderChainClass = Class.forName(DEFAULT_AWSCREDENTIALS_PROVIDER_CHAIN); + 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(AWS_SESSION_CREDENTIALS); + 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(AWS_CREDENTIALS); + return (String) credentialsClass.getMethod("getAWSAccessKeyId").invoke(credentials); + } + + public String getAwsSecretKeyFromCredentials(Object credentials) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class credentialsClass = Class.forName(AWS_CREDENTIALS); + 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..920bc7fac7 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,181 @@ 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(log).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 AuthConfig.builder() + .username(accessKeyId) + .password(secretAccessKey) + .email(null) + .auth(null) + .identityToken(null) + .build(); + } + + } + + 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/CredentialHelperClientTest.java b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/CredentialHelperClientTest.java new file mode 100644 index 0000000000..b015ed91ac --- /dev/null +++ b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/CredentialHelperClientTest.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.kit.build.service.docker.auth; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class CredentialHelperClientTest { + private final Gson gson = new Gson(); + + @Mocked + private KitLogger logger; + + private CredentialHelperClient credentialHelperClient; + + private JsonObject jsonObject; + + private AuthConfig authConfig; + + @Before + public void givenCredentialHelperClient() { + this.credentialHelperClient = new CredentialHelperClient(logger, "desktop"); + } + + @Test + public void testUsernamePasswordAuthConfig() { + givenJson("{\"ServerURL\":\"registry.mycompany.com\",\"Username\":\"jane_doe\",\"Secret\":\"not-really\"}"); + + whenJsonObjectConvertedToAuthConfig(); + + assertEquals("username should match", "jane_doe", this.authConfig.getUsername()); + assertEquals("password should match", "not-really", this.authConfig.getPassword()); + assertNull("identityToken should not be set", this.authConfig.getIdentityToken()); + } + + @Test + public void testTokenAuthConfig() { + givenJson("{\"ServerURL\":\"registry.cloud-provider.com\",\"Username\":\"\",\"Secret\":\"gigantic-mess-of-jwt\"}"); + + whenJsonObjectConvertedToAuthConfig(); + + assertNull("username should not be set", this.authConfig.getUsername()); + assertNull("password should not be set", this.authConfig.getPassword()); + assertEquals("identity token should match", "gigantic-mess-of-jwt", this.authConfig.getIdentityToken()); + } + + private void givenJson(String json) { + this.jsonObject = this.gson.fromJson(json, JsonObject.class); + } + + private void whenJsonObjectConvertedToAuthConfig() { + this.authConfig = this.credentialHelperClient.toAuthConfig(this.jsonObject); + } +} 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..eef266e62d --- /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,97 @@ +/** + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +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/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/DockerRegistryAuthHandlerTest.java b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/DockerRegistryAuthHandlerTest.java index 269146f376..5e395d791f 100644 --- a/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/DockerRegistryAuthHandlerTest.java +++ b/jkube-kit/build/service/docker/src/test/java/org/eclipse/jkube/kit/build/service/docker/auth/ecr/DockerRegistryAuthHandlerTest.java @@ -207,7 +207,7 @@ private void addAuths(JsonObject config, String user, String password, String em } private void verifyAuthConfig(AuthConfig config, String username, String password, String email) { - JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue().getBytes())), JsonObject.class); + JsonObject params = new Gson().fromJson(new String(Base64.getDecoder().decode(config.toHeaderValue(log).getBytes())), JsonObject.class); assertEquals(username,params.get("username").getAsString()); assertEquals(password,params.get("password").getAsString()); if (email != null) { 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..de59b63e52 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 <> and <> 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