diff --git a/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java b/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java
index c3a72c6f77..ee3ba4087c 100644
--- a/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java
+++ b/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java
@@ -70,6 +70,9 @@ public class ClientBuilder {
private Duration readTimeout = Duration.ZERO;
// default health check is once a minute
private Duration pingInterval = Duration.ofMinutes(1);
+ // time to refresh exec based credentials
+ // TODO: Read the expiration from the credential itself
+ private Duration execCredentialRefreshPeriod = null;
/**
* Creates an {@link ApiClient} by calling {@link #standard()} and {@link #build()}.
@@ -272,6 +275,20 @@ protected ClientBuilder setBasePath(String host, String port) {
* @throws IOException if the files specified in the provided KubeConfig are not readable
*/
public static ClientBuilder kubeconfig(KubeConfig config) throws IOException {
+ return kubeconfig(config, null);
+ }
+
+ /**
+ * Creates a builder which is pre-configured from a {@link KubeConfig}.
+ *
+ *
To load a KubeConfig, see {@link KubeConfig#loadKubeConfig(Reader)}.
+ *
+ * @param config The {@link KubeConfig} to configure the builder from.
+ * @param tokenRefreshPeriod If the KubeConfig generates a bearer token, after this interval, it will be refreshed.
+ * @return ClientBuilder configured from the provided KubeConfig
+ * @throws IOException if the files specified in the provided KubeConfig are not readable
+ */
+ public static ClientBuilder kubeconfig(KubeConfig config, Duration tokenRefreshPeriod) throws IOException {
final ClientBuilder builder = new ClientBuilder();
String server = config.getServer();
@@ -295,7 +312,7 @@ public static ClientBuilder kubeconfig(KubeConfig config) throws IOException {
builder.setVerifyingSsl(config.verifySSL());
builder.setBasePath(server);
- builder.setAuthentication(new KubeconfigAuthentication(config));
+ builder.setAuthentication(new KubeconfigAuthentication(config, tokenRefreshPeriod));
return builder;
}
diff --git a/util/src/main/java/io/kubernetes/client/util/credentials/KubeconfigAuthentication.java b/util/src/main/java/io/kubernetes/client/util/credentials/KubeconfigAuthentication.java
index c2430565e5..236308fb46 100644
--- a/util/src/main/java/io/kubernetes/client/util/credentials/KubeconfigAuthentication.java
+++ b/util/src/main/java/io/kubernetes/client/util/credentials/KubeconfigAuthentication.java
@@ -16,6 +16,7 @@
import io.kubernetes.client.util.KubeConfig;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@@ -33,8 +34,9 @@
public class KubeconfigAuthentication implements Authentication {
private final Authentication delegateAuthentication;
+ private Duration tokenRefreshPeriod;
- public KubeconfigAuthentication(final KubeConfig config) throws IOException {
+ public KubeconfigAuthentication(final KubeConfig config, Duration tokenRefreshPeriod) throws IOException {
byte[] clientCert =
config.getDataOrFileRelative(
config.getClientCertificateData(), config.getClientCertificateFile());
@@ -60,7 +62,9 @@ public KubeconfigAuthentication(final KubeConfig config) throws IOException {
if (credentials != null) {
if (StringUtils.isNotEmpty(credentials.get(KubeConfig.CRED_TOKEN_KEY))) {
delegateAuthentication =
- new AccessTokenAuthentication(credentials.get(KubeConfig.CRED_TOKEN_KEY));
+ tokenRefreshPeriod == null ?
+ new AccessTokenAuthentication(credentials.get(KubeConfig.CRED_TOKEN_KEY)) :
+ new RefreshAuthentication(() -> config.getCredentials().get(KubeConfig.CRED_TOKEN_KEY), tokenRefreshPeriod);
return;
} else if (StringUtils.isNotEmpty(
credentials.get(KubeConfig.CRED_CLIENT_CERTIFICATE_DATA_KEY))
diff --git a/util/src/main/java/io/kubernetes/client/util/credentials/RefreshAuthentication.java b/util/src/main/java/io/kubernetes/client/util/credentials/RefreshAuthentication.java
new file mode 100644
index 0000000000..f03c610cb1
--- /dev/null
+++ b/util/src/main/java/io/kubernetes/client/util/credentials/RefreshAuthentication.java
@@ -0,0 +1,80 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+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 io.kubernetes.client.util.credentials;
+
+import io.kubernetes.client.openapi.ApiClient;
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.function.Supplier;
+
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+// TODO: prefer OpenAPI backed Auentication once it is available. see details in
+// https://github.com/OpenAPITools/openapi-generator/pull/6036. currently, the
+// workaround is to hijack the http request.
+// TODO: Merge this with TokenFileAuthentication.
+public class RefreshAuthentication implements Authentication, Interceptor {
+ private Instant expiry;
+ private Duration refreshPeriod;
+ private String token;
+ private Supplier tokenSupplier;
+ private Clock clock;
+
+ public RefreshAuthentication(Supplier tokenSupplier, Duration refreshPeriod) {
+ this(tokenSupplier, refreshPeriod, Clock.systemUTC());
+ }
+
+ public RefreshAuthentication(Supplier tokenSupplier, Duration refreshPeriod, Clock clock) {
+ this.expiry = Instant.MIN;
+ this.refreshPeriod = refreshPeriod;
+ this.token = tokenSupplier.get();
+ this.tokenSupplier = tokenSupplier;
+ this.clock = clock;
+ }
+
+ private String getToken() {
+ if (Instant.now(this.clock).isAfter(this.expiry)) {
+ this.token = tokenSupplier.get();
+ expiry = Instant.now(this.clock).plusSeconds(refreshPeriod.toSeconds());
+ }
+ return this.token;
+ }
+
+ public Duration getRefreshPeriod() {
+ return this.refreshPeriod;
+ }
+
+ public void setExpiry(Instant expiry) {
+ this.expiry = expiry;
+ }
+
+ @Override
+ public void provide(ApiClient client) {
+ OkHttpClient withInterceptor = client.getHttpClient().newBuilder().addInterceptor(this).build();
+ client.setHttpClient(withInterceptor);
+ }
+
+ @Override
+ public Response intercept(Interceptor.Chain chain) throws IOException {
+ Request request = chain.request();
+ Request newRequest;
+ newRequest = request.newBuilder().header("Authorization", "Bearer " + getToken()).build();
+ return chain.proceed(newRequest);
+ }
+}
diff --git a/util/src/test/java/io/kubernetes/client/util/credentials/KubeconfigAuthenticationTest.java b/util/src/test/java/io/kubernetes/client/util/credentials/KubeconfigAuthenticationTest.java
index 64c920e6b6..ce936bcb16 100644
--- a/util/src/test/java/io/kubernetes/client/util/credentials/KubeconfigAuthenticationTest.java
+++ b/util/src/test/java/io/kubernetes/client/util/credentials/KubeconfigAuthenticationTest.java
@@ -20,6 +20,7 @@
import io.kubernetes.client.util.KubeConfig;
import java.io.IOException;
+import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -39,7 +40,7 @@ void certificateAuthenticationFromExecCommand() throws IOException {
certCredentials.put(KubeConfig.CRED_CLIENT_KEY_DATA_KEY, "key");
when(kubeConfig.getCredentials()).thenReturn(certCredentials);
- KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
+ KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig, null);
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
.isInstanceOf(ClientCertificateAuthentication.class);
@@ -49,7 +50,7 @@ void certificateAuthenticationFromExecCommand() throws IOException {
void certificateAuthenticationFromKubeConfig() throws IOException {
when(kubeConfig.getDataOrFileRelative(any(), any())).thenReturn("data".getBytes());
- KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
+ KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig, null);
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
.isInstanceOf(ClientCertificateAuthentication.class);
@@ -61,7 +62,7 @@ void usernamePasswordAuthenticationFromKubeConfig() throws IOException {
when(kubeConfig.getUsername()).thenReturn("user");
when(kubeConfig.getPassword()).thenReturn("password");
- KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
+ KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig, null);
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
.isInstanceOf(UsernamePasswordAuthentication.class);
@@ -73,15 +74,31 @@ void accessTokenAuthenticationFromExecComand() throws IOException {
certCredentials.put(KubeConfig.CRED_TOKEN_KEY, "token");
when(kubeConfig.getCredentials()).thenReturn(certCredentials);
- KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
+ KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig, null);
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
.isInstanceOf(AccessTokenAuthentication.class);
}
+ @Test
+ void accessTokenAuthenticationFromExecComandWithRefresh() throws IOException {
+ Map certCredentials = new HashMap<>();
+ certCredentials.put(KubeConfig.CRED_TOKEN_KEY, "token");
+ when(kubeConfig.getCredentials()).thenReturn(certCredentials);
+
+ Duration period = Duration.ofSeconds(60);
+
+ KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig, period);
+
+ assertThat(kubeconfigAuthentication.getDelegateAuthentication())
+ .isInstanceOf(RefreshAuthentication.class);
+ RefreshAuthentication auth = (RefreshAuthentication) kubeconfigAuthentication.getDelegateAuthentication();
+ assertThat(auth.getRefreshPeriod()).isEqualTo(period);
+ }
+
@Test
void dummyAuthentication() throws IOException {
- KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig);
+ KubeconfigAuthentication kubeconfigAuthentication = new KubeconfigAuthentication(kubeConfig, null);
assertThat(kubeconfigAuthentication.getDelegateAuthentication())
.isInstanceOf(DummyAuthentication.class);
diff --git a/util/src/test/java/io/kubernetes/client/util/credentials/RefreshAuthenticationTest.java b/util/src/test/java/io/kubernetes/client/util/credentials/RefreshAuthenticationTest.java
new file mode 100644
index 0000000000..95d0b12d1e
--- /dev/null
+++ b/util/src/test/java/io/kubernetes/client/util/credentials/RefreshAuthenticationTest.java
@@ -0,0 +1,156 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+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 io.kubernetes.client.util.credentials;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.okForContentType;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+
+import io.kubernetes.client.Resources;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.Configuration;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.util.KubeConfig;
+
+import java.io.IOException;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class RefreshAuthenticationTest {
+ @RegisterExtension
+ static WireMockExtension apiServer = WireMockExtension.newInstance().options(options().dynamicPort()).build();
+
+ private int refreshCount;
+ private Instant instant;
+ private MockClock clock;
+
+ @BeforeEach
+ void setup() {
+ final ApiClient client = new ApiClient();
+ client.setBasePath("http://localhost:" + apiServer.getPort());
+ this.instant = Instant.now();
+ this.clock = new MockClock(instant);
+ RefreshAuthentication auth = new RefreshAuthentication(
+ () -> {
+ refreshCount++;
+ return "foo " + refreshCount;
+ }, Duration.ofSeconds(60),
+ this.clock);
+ auth.provide(client);
+ Configuration.setDefaultApiClient(client);
+
+ refreshCount = 0;
+ }
+
+ @Test
+ void tokenProvided() throws ApiException {
+ apiServer.stubFor(
+ get(urlPathEqualTo("/api/v1/pods")).willReturn(okForContentType("application/json",
+ "{\"items\":[]}")));
+ CoreV1Api api = new CoreV1Api();
+
+ api.listPodForAllNamespaces().execute();
+ apiServer.verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v1/pods"))
+ .withHeader("Authorization", equalTo("Bearer foo 1")));
+ assertThat(refreshCount).isEqualTo(1);
+ }
+
+ @Test
+ void tokenDoesntRefreshEarly() throws ApiException {
+ apiServer.stubFor(
+ get(urlPathEqualTo("/api/v1/pods")).willReturn(okForContentType("application/json",
+ "{\"items\":[]}")));
+ CoreV1Api api = new CoreV1Api();
+
+ api.listPodForAllNamespaces().execute();
+ api.listPodForAllNamespaces().execute();
+
+ apiServer.verify(
+ 2,
+ getRequestedFor(urlPathEqualTo("/api/v1/pods"))
+ .withHeader("Authorization", equalTo("Bearer foo 1")));
+ assertThat(refreshCount).isEqualTo(1);
+ }
+
+ @Test
+ void tokenRefreshes() throws ApiException {
+ apiServer.stubFor(
+ get(urlPathEqualTo("/api/v1/pods")).willReturn(okForContentType("application/json",
+ "{\"items\":[]}")));
+ CoreV1Api api = new CoreV1Api();
+
+ api.listPodForAllNamespaces().execute();
+ clock.setInstant(instant.plusSeconds(70));
+ api.listPodForAllNamespaces().execute();
+
+ apiServer.verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v1/pods"))
+ .withHeader("Authorization", equalTo("Bearer foo 1")));
+ apiServer.verify(
+ 1,
+ getRequestedFor(urlPathEqualTo("/api/v1/pods"))
+ .withHeader("Authorization", equalTo("Bearer foo 2")));
+ assertThat(refreshCount).isEqualTo(2);
+ }
+
+ static class MockClock extends Clock {
+ Instant now;
+
+ public MockClock(Instant start) {
+ this.now = start;
+ }
+
+ public void setInstant(Instant instant) {
+ this.now = instant;
+ }
+
+ @Override
+ public Instant instant() {
+ return now;
+ }
+
+ @Override
+ public ZoneId getZone() {
+ return ZoneOffset.UTC;
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}