Skip to content

Commit b71fba4

Browse files
committed
AuthenticationRefresher wrapper solution
This commit introduces the AuthenticationRefresher as an implementation of Authentication. It's purpose is to warp another Authentication instance and refresh it very n seconds by calling `provide` on the wrapped Authentication object. This code is intedned to be used a solution to issue #2438. Any Authentication object that provides credentials, that must be refreshed, to the ApiClient do not have a way to do so. There are two ways to use the AuthenticationRefresher with ClientBuilder. The first option is to use the ClientBuilder.setAuthentication(...) method. An Authentication instance can be wrapped in an AuthenticationRefresher and passed into the setAuthentication method. For example, ``` ClientBuilder.standard().setAuthentication( //wrap an auth and refresh it every 15 minutes (900s) new Authentication(someDelegateAuthenticationInstace, 900) ); ``` This is integrated up into the ClientBuilder.build() method by adding a new authenticationRefreshSeconds field and getter/setter pair. If a refresh interval is set, the Authentication object used by ClientBuilder will be wrapped with an AuthenticationRefresher.
1 parent 9a6060e commit b71fba4

File tree

4 files changed

+174
-16
lines changed

4 files changed

+174
-16
lines changed

util/src/main/java/io/kubernetes/client/util/ClientBuilder.java

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,6 @@
2121
import static io.kubernetes.client.util.KubeConfig.KUBECONFIG;
2222
import static io.kubernetes.client.util.KubeConfig.KUBEDIR;
2323

24-
import io.kubernetes.client.openapi.ApiClient;
25-
import io.kubernetes.client.openapi.ApiException;
26-
import io.kubernetes.client.openapi.models.V1CertificateSigningRequest;
27-
import io.kubernetes.client.persister.FilePersister;
28-
import io.kubernetes.client.util.credentials.AccessTokenAuthentication;
29-
import io.kubernetes.client.util.credentials.Authentication;
30-
import io.kubernetes.client.util.credentials.ClientCertificateAuthentication;
31-
import io.kubernetes.client.util.credentials.KubeconfigAuthentication;
32-
import io.kubernetes.client.util.credentials.TokenFileAuthentication;
33-
import io.kubernetes.client.util.exception.CSRNotApprovedException;
3424
import java.io.BufferedReader;
3525
import java.io.ByteArrayInputStream;
3626
import java.io.File;
@@ -49,12 +39,25 @@
4939
import java.time.Duration;
5040
import java.util.Arrays;
5141
import java.util.List;
52-
import okhttp3.Protocol;
42+
5343
import org.apache.commons.compress.utils.IOUtils;
5444
import org.apache.commons.lang3.StringUtils;
5545
import org.slf4j.Logger;
5646
import org.slf4j.LoggerFactory;
5747

48+
import io.kubernetes.client.openapi.ApiClient;
49+
import io.kubernetes.client.openapi.ApiException;
50+
import io.kubernetes.client.openapi.models.V1CertificateSigningRequest;
51+
import io.kubernetes.client.persister.FilePersister;
52+
import io.kubernetes.client.util.credentials.AccessTokenAuthentication;
53+
import io.kubernetes.client.util.credentials.Authentication;
54+
import io.kubernetes.client.util.credentials.AuthenticationRefresher;
55+
import io.kubernetes.client.util.credentials.ClientCertificateAuthentication;
56+
import io.kubernetes.client.util.credentials.KubeconfigAuthentication;
57+
import io.kubernetes.client.util.credentials.TokenFileAuthentication;
58+
import io.kubernetes.client.util.exception.CSRNotApprovedException;
59+
import okhttp3.Protocol;
60+
5861
/** A Builder which allows the construction of {@link ApiClient}s in a fluent fashion. */
5962
public class ClientBuilder {
6063
private static final Logger log = LoggerFactory.getLogger(ClientBuilder.class);
@@ -71,6 +74,8 @@ public class ClientBuilder {
7174
// default health check is once a minute
7275
private Duration pingInterval = Duration.ofMinutes(1);
7376

77+
private Duration authenticationRefreshSeconds;
78+
7479
/**
7580
* Creates an {@link ApiClient} by calling {@link #standard()} and {@link #build()}.
7681
*
@@ -418,6 +423,15 @@ public ClientBuilder setKeyStorePassphrase(String keyStorePassphrase) {
418423
return this;
419424
}
420425

426+
public Duration getAuthenticationRefreshSeconds() {
427+
return authenticationRefreshSeconds;
428+
}
429+
430+
public ClientBuilder setAuthenticationRefreshSeconds(Duration authenticationRefreshSeconds) {
431+
this.authenticationRefreshSeconds = authenticationRefreshSeconds;
432+
return this;
433+
}
434+
421435
public ApiClient build() {
422436
final ApiClient client = new ApiClient();
423437

@@ -450,6 +464,15 @@ public ApiClient build() {
450464
}
451465
}
452466
}
467+
468+
// When authenticationRefreshSeconds is set, wrap the Authentication
469+
// object in an AuthenticationRefresher. It is important that we do this
470+
// after the passphrase code above is done munging around with the
471+
// internals of KubeConfigAuthentication
472+
if (authenticationRefreshSeconds != null) {
473+
authentication = new AuthenticationRefresher(authentication, authenticationRefreshSeconds.toSeconds());
474+
}
475+
453476
authentication.provide(client);
454477
}
455478

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.util.credentials;
14+
15+
import java.util.concurrent.Executors;
16+
import java.util.concurrent.ScheduledExecutorService;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import io.kubernetes.client.openapi.ApiClient;
22+
23+
/**
24+
* Wraps an existing {@link Authentication} and refreshes it every
25+
* expirationSeconds.
26+
*
27+
* Can be used with ClientBuilder as such:
28+
*
29+
* <pre>
30+
* ClientBuilder.standard()
31+
* .withAuthentication(new AuthenticationRefresher(new KubeconfigAuthentication(), 60))
32+
* .build();
33+
* </pre>
34+
*/
35+
public class AuthenticationRefresher implements Authentication {
36+
37+
private static final Logger log = LoggerFactory.getLogger(AuthenticationRefresher.class);
38+
39+
private final Authentication delegateAuthentication;
40+
private final Long expirationSeconds;
41+
42+
public AuthenticationRefresher(Authentication delegateAuthentication, long expirationSeconds) {
43+
this.delegateAuthentication = delegateAuthentication;
44+
this.expirationSeconds = expirationSeconds;
45+
log.debug("AuthenticationRefresher initialized with expirationSeconds: " + expirationSeconds);
46+
}
47+
48+
/**
49+
* Calls delegateAuthentication every expirationSeconds
50+
*/
51+
@Override
52+
public void provide(ApiClient client) {
53+
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
54+
executor.scheduleAtFixedRate(new Runnable() {
55+
@Override
56+
public void run() {
57+
log.debug("Refreshing authentication");
58+
delegateAuthentication.provide(client);
59+
}
60+
}, expirationSeconds, expirationSeconds, java.util.concurrent.TimeUnit.SECONDS);
61+
62+
// Run it now, synchronously.
63+
log.debug("Invoking authentication");
64+
delegateAuthentication.provide(client);
65+
}
66+
}

util/src/test/java/io/kubernetes/client/util/ClientBuilderTest.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,24 @@
1717
import static org.assertj.core.api.Assertions.assertThat;
1818
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1919
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.times;
2021
import static org.mockito.Mockito.verify;
2122

22-
import io.kubernetes.client.Resources;
23-
import io.kubernetes.client.openapi.ApiClient;
24-
import io.kubernetes.client.util.credentials.Authentication;
25-
import io.kubernetes.client.util.credentials.ClientCertificateAuthentication;
26-
import io.kubernetes.client.util.credentials.KubeconfigAuthentication;
2723
import java.io.File;
2824
import java.io.FileReader;
2925
import java.io.IOException;
3026
import java.nio.file.Files;
3127
import java.nio.file.Paths;
28+
import java.time.Duration;
29+
3230
import org.junit.jupiter.api.Test;
3331
import org.junit.jupiter.api.extension.ExtendWith;
32+
33+
import io.kubernetes.client.Resources;
34+
import io.kubernetes.client.openapi.ApiClient;
35+
import io.kubernetes.client.util.credentials.Authentication;
36+
import io.kubernetes.client.util.credentials.ClientCertificateAuthentication;
37+
import io.kubernetes.client.util.credentials.KubeconfigAuthentication;
3438
import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
3539
import uk.org.webcompere.systemstubs.jupiter.SystemStub;
3640
import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
@@ -218,6 +222,19 @@ void credentialProviderInvoked() throws IOException {
218222
verify(provider).provide(client);
219223
}
220224

225+
@Test
226+
void credentialProviderWrappedWithRefresher() throws IOException, InterruptedException {
227+
final Authentication provider = mock(Authentication.class);
228+
final ApiClient client = ClientBuilder.standard()
229+
.setAuthentication(provider)
230+
.setAuthenticationRefreshSeconds(Duration.ofSeconds(2))
231+
.build();
232+
233+
Thread.sleep(3000);
234+
235+
verify(provider, times(2)).provide(client);
236+
}
237+
221238
/**
222239
* We can't verify anything here because of how things are configured in swagger-codegen and
223240
* okhttp but combined with {@link #sslCertCaBad()} we have some certainty that it is being
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.util.credentials;
14+
15+
import static org.mockito.Mockito.mock;
16+
import static org.mockito.Mockito.times;
17+
import static org.mockito.Mockito.verify;
18+
19+
import java.io.IOException;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import io.kubernetes.client.openapi.ApiClient;
24+
import io.kubernetes.client.util.ClientBuilder;
25+
26+
public class AuthenticationRefresherTest {
27+
28+
@Test
29+
void credentialProviderWrapperInvoked() throws IOException {
30+
final Authentication provider = mock(Authentication.class);
31+
final Authentication wrapper = new AuthenticationRefresher(provider, 15);
32+
33+
final ApiClient client = ClientBuilder.standard().setAuthentication(wrapper).build();
34+
35+
verify(provider).provide(client);
36+
}
37+
38+
@Test
39+
void credentialProviderTimerInvoked() throws Exception {
40+
final Authentication provider = mock(Authentication.class);
41+
final Authentication wrapper = new AuthenticationRefresher(provider, 2);
42+
43+
final ApiClient client = ClientBuilder.standard().setAuthentication(wrapper).build();
44+
45+
Thread.sleep(3000);
46+
47+
// verify provider mock called 2 times
48+
verify(provider, times(2)).provide(client);
49+
50+
}
51+
52+
}

0 commit comments

Comments
 (0)