From 817a54e09ef4acdff3b999675973d8bef0f6a550 Mon Sep 17 00:00:00 2001 From: jhbae200 Date: Fri, 26 Nov 2021 18:37:36 +0900 Subject: [PATCH] GCPAuthenticator support without gcloud Signed-off-by: jhbae200 --- pom.xml | 6 +++ util/pom.xml | 5 ++ .../util/authenticators/GCPAuthenticator.java | 49 +++++++++++++++++-- .../client/util/KubeConfigTest.java | 43 +++++++++++++++- .../authenticators/GCPAuthenticatorTest.java | 30 ++++++++++-- 5 files changed, 123 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index c3f20ef11d..87f458fc7b 100644 --- a/pom.xml +++ b/pom.xml @@ -228,6 +228,12 @@ jsr305 ${jsr305.version} + + com.google.auth + google-auth-library-oauth2-http + 1.3.0 + true + diff --git a/util/pom.xml b/util/pom.xml index 002d42dfcc..2e4c7bdeaf 100644 --- a/util/pom.xml +++ b/util/pom.xml @@ -84,6 +84,11 @@ org.bitbucket.b_c jose4j + + com.google.auth + google-auth-library-oauth2-http + true + junit diff --git a/util/src/main/java/io/kubernetes/client/util/authenticators/GCPAuthenticator.java b/util/src/main/java/io/kubernetes/client/util/authenticators/GCPAuthenticator.java index 14438e0bed..5f5599f578 100644 --- a/util/src/main/java/io/kubernetes/client/util/authenticators/GCPAuthenticator.java +++ b/util/src/main/java/io/kubernetes/client/util/authenticators/GCPAuthenticator.java @@ -12,6 +12,8 @@ */ package io.kubernetes.client.util.authenticators; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import io.kubernetes.client.util.KubeConfig; @@ -40,17 +42,25 @@ public class GCPAuthenticator implements Authenticator { static final String EXPIRY = "expiry"; static final String CMD_ARGS = "cmd-args"; static final String CMD_PATH = "cmd-path"; + static final String SCOPES = "scopes"; + static final String[] DEFAULT_SCOPES = + new String[] { + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email" + }; private static final Logger log = LoggerFactory.getLogger(GCPAuthenticator.class); private final ProcessBuilder pb; + private GoogleCredentials gc; public GCPAuthenticator() { - this(new ProcessBuilder()); + this(new ProcessBuilder(), null); } - public GCPAuthenticator(ProcessBuilder pb) { + public GCPAuthenticator(ProcessBuilder pb, GoogleCredentials gc) { this.pb = pb; + this.gc = gc; } @Override @@ -81,8 +91,39 @@ public boolean isExpired(Map config) { @Override public Map refresh(Map config) { - if (!config.containsKey(CMD_ARGS) || !config.containsKey(CMD_PATH)) - throw new RuntimeException("Could not refresh token"); + if (isCmd(config)) { + return refreshCmd(config); + } + // Google Application Credentials-based refresh + // https://cloud.google.com/kubernetes-engine/docs/how-to/api-server-authentication#environments-without-gcloud + String[] scopes = parseScopes(config); + try { + if (this.gc == null) this.gc = GoogleCredentials.getApplicationDefault().createScoped(scopes); + AccessToken accessToken = gc.getAccessToken(); + config.put(ACCESS_TOKEN, accessToken.getTokenValue()); + config.put(EXPIRY, accessToken.getExpirationTime()); + return config; + } catch (IOException e) { + throw new RuntimeException("The Application Default Credentials are not available.", e); + } + } + + public String[] parseScopes(Map config) { + String scopes = (String) config.get(SCOPES); + if (scopes == null) { + return DEFAULT_SCOPES; + } + if (scopes.isEmpty()) { + return new String[] {}; + } + return scopes.split(","); + } + + private boolean isCmd(Map config) { + return config.containsKey(CMD_ARGS) && config.containsKey(CMD_PATH); + } + + private Map refreshCmd(Map config) { String cmdPath = (String) config.get(CMD_PATH); String cmdArgs = (String) config.get(CMD_ARGS); List fullCmd = diff --git a/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java b/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java index 1dbf180bd8..861baa6551 100644 --- a/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java +++ b/util/src/test/java/io/kubernetes/client/util/KubeConfigTest.java @@ -14,6 +14,8 @@ import static org.junit.Assert.*; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import io.kubernetes.client.util.authenticators.Authenticator; import io.kubernetes.client.util.authenticators.AzureActiveDirectoryAuthenticator; import io.kubernetes.client.util.authenticators.GCPAuthenticator; @@ -25,6 +27,8 @@ import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.sql.Date; +import java.time.Instant; import java.util.Map; import org.junit.Rule; import org.junit.Test; @@ -151,7 +155,7 @@ public void testGCPAuthProviderStringDate() { } @Test - public void testGCPAuthProviderExpiredToken() { + public void testGCPAuthProviderExpiredTokenWithinGCloud() { String gcpConfigExpiredToken = "apiVersion: v1\n" + "contexts:\n" @@ -191,7 +195,7 @@ public void testGCPAuthProviderExpiredToken() { fail("Unexpected exception: " + ex); } - KubeConfig.registerAuthenticator(new GCPAuthenticator(mockPB)); + KubeConfig.registerAuthenticator(new GCPAuthenticator(mockPB, null)); try { KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(gcpConfigExpiredToken)); assertEquals("new-fake-token", kc.getAccessToken()); @@ -201,6 +205,41 @@ public void testGCPAuthProviderExpiredToken() { } } + @Test + public void testGCPAuthProviderExpiredTokenWithoutGCloud() { + String gcpConfigExpiredToken = + "apiVersion: v1\n" + + "contexts:\n" + + "- context:\n" + + " user: gke-cluster\n" + + " name: foo-context\n" + + "current-context: foo-context\n" + + "users:\n" + + "- name: gke-cluster\n" + + " user:\n" + + " auth-provider:\n" + + " config:\n" + + " access-token: fake-token\n" + + " expiry: 1970-01-01T00:00:00Z\n" + + " name: gcp"; + + String fakeToken = "new-fake-token"; + String fakeTokenExpiry = "2121-08-05T02:30:24Z"; + + GoogleCredentials mockGC = Mockito.mock(GoogleCredentials.class); + Mockito.when(mockGC.getAccessToken()) + .thenReturn(new AccessToken(fakeToken, Date.from(Instant.parse(fakeTokenExpiry)))); + + KubeConfig.registerAuthenticator(new GCPAuthenticator(null, mockGC)); + try { + KubeConfig kc = KubeConfig.loadKubeConfig(new StringReader(gcpConfigExpiredToken)); + assertEquals(fakeToken, kc.getAccessToken()); + } catch (Exception ex) { + ex.printStackTrace(); + fail("Unexpected exception: " + ex); + } + } + @Test public void testAzureAuthProvider() { KubeConfig.registerAuthenticator(new AzureActiveDirectoryAuthenticator()); diff --git a/util/src/test/java/io/kubernetes/client/util/authenticators/GCPAuthenticatorTest.java b/util/src/test/java/io/kubernetes/client/util/authenticators/GCPAuthenticatorTest.java index b29467f865..eabdd771e3 100644 --- a/util/src/test/java/io/kubernetes/client/util/authenticators/GCPAuthenticatorTest.java +++ b/util/src/test/java/io/kubernetes/client/util/authenticators/GCPAuthenticatorTest.java @@ -14,12 +14,17 @@ import static org.assertj.core.api.Fail.fail; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,18 +48,24 @@ public class GCPAuthenticatorTest { add(cmdArgsSplit[2]); } }); - + private static final String fakeToken = "new-fake-token"; + private static final String fakeTokenExpiry = "2121-08-05T02:30:24Z"; private static final String fakeExecResult = "{\n" + " \"credential\": {\n" - + " \"access_token\": \"new-fake-token\",\n" + + " \"access_token\": \"" + + fakeToken + + "\",\n" + " \"id_token\": \"id-fake-token\",\n" - + " \"token_expiry\": \"2121-08-05T02:30:24Z\"\n" + + " \"token_expiry\": \"" + + fakeTokenExpiry + + "\"\n" + " }\n" + "}"; private final ProcessBuilder mockPB = Mockito.mock(ProcessBuilder.class); - private final GCPAuthenticator gcpAuthenticator = new GCPAuthenticator(mockPB); + private final GoogleCredentials mockGC = Mockito.mock(GoogleCredentials.class); + private final GCPAuthenticator gcpAuthenticator = new GCPAuthenticator(mockPB, mockGC); @Before public void setup() { @@ -183,4 +194,15 @@ public void testRefreshLeadingWhitespaceInPathAndArgs() { List executedCommand = mockPB.command(); MatcherAssert.assertThat(executedCommand, is(expectedCommand)); } + + @Test + public void testRefreshApplicationDefaultCredentials() { + Date fakeTokenExpiryDate = Date.from(Instant.parse(fakeTokenExpiry)); + Mockito.when(mockGC.getAccessToken()) + .thenReturn(new AccessToken(fakeToken, fakeTokenExpiryDate)); + final Map config = new HashMap() {}; + final Map result = gcpAuthenticator.refresh(config); + assertEquals(fakeToken, result.get(GCPAuthenticator.ACCESS_TOKEN)); + assertEquals(fakeTokenExpiryDate, result.get(GCPAuthenticator.EXPIRY)); + } }