Skip to content

Commit

Permalink
update token in file listed in KUBECONFIG env var (fabric8io#6240)
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Dietisheim <[email protected]>
  • Loading branch information
adietish committed Oct 21, 2024
1 parent aca647f commit e5cde95
Show file tree
Hide file tree
Showing 18 changed files with 569 additions and 191 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
* Fix #6281: use GitHub binary repo for Kube API Tests
* Fix #6282: Allow annotated types with Pattern, Min, and Max with Lists and Maps and CRD generation
* Fix #5480: Move `io.fabric8:zjsonpatch` to KubernetesClient project
* Fix #6354: Prevent deadlock in okhttp AsyncBody.cancel
* Fix #6240: Use kubeconfig files listed in the KUBECONFIG env var

#### Dependency Upgrade
* Fix #6052: Removed dependency on no longer maintained com.github.mifmif:generex
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@ public Config build() {
fluent.getOauthTokenProvider(), fluent.getCustomHeaders(), fluent.getRequestRetryBackoffLimit(),
fluent.getRequestRetryBackoffInterval(), fluent.getUploadRequestTimeout(), fluent.getOnlyHttpWatches(),
fluent.getCurrentContext(), fluent.getContexts(),
Optional.ofNullable(fluent.getAutoConfigure()).orElse(!disableAutoConfig()), true);
Optional.ofNullable(fluent.getAutoConfigure()).orElse(!disableAutoConfig()), true, fluent.getFiles());
buildable.setAuthProvider(fluent.getAuthProvider());
buildable.setFile(fluent.getFile());
return buildable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void copyInstance(Config instance) {
this.withContexts(instance.getContexts());
this.withAutoConfigure(instance.getAutoConfigure());
this.withAuthProvider(instance.getAuthProvider());
this.withFile(instance.getFile());
this.withFiles(instance.getFiles());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2015 Red Hat, Inc.
*
* 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.fabric8.kubernetes.client;

import java.io.File;
import io.fabric8.kubernetes.api.model.Config;
import lombok.Getter;

@Getter
public class KubeConfigFile {
private final File file;
private final Config config;

/** for testing purposes **/
public KubeConfigFile(File file, Config config) {
this.file = file;
this.config = config;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.fabric8.kubernetes.client.http.TlsVersion;
import io.sundr.builder.annotations.Buildable;

import java.io.File;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -50,14 +51,14 @@ public SundrioConfig(String masterUrl, String apiVersion, String namespace, Bool
String impersonateUsername, String[] impersonateGroups, Map<String, List<String>> impersonateExtras,
OAuthTokenProvider oauthTokenProvider, Map<String, String> customHeaders, Integer requestRetryBackoffLimit,
Integer requestRetryBackoffInterval, Integer uploadRequestTimeout, Boolean onlyHttpWatches, NamedContext currentContext,
List<NamedContext> contexts, Boolean autoConfigure) {
List<NamedContext> contexts, Boolean autoConfigure, List<File> files) {
super(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData,
clientCertFile, clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username,
password, oauthToken, autoOAuthToken, watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout,
scaleTimeout, loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, http2Disable,
httpProxy, httpsProxy, noProxy, userAgent, tlsVersions, websocketPingInterval, proxyUsername, proxyPassword,
trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups,
impersonateExtras, oauthTokenProvider, customHeaders, requestRetryBackoffLimit, requestRetryBackoffInterval,
uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true);
uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true, files);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@
import io.fabric8.kubernetes.api.model.AuthInfo;
import io.fabric8.kubernetes.api.model.Cluster;
import io.fabric8.kubernetes.api.model.Config;
import io.fabric8.kubernetes.api.model.ConfigBuilder;
import io.fabric8.kubernetes.api.model.Context;
import io.fabric8.kubernetes.api.model.NamedAuthInfo;
import io.fabric8.kubernetes.api.model.NamedCluster;
import io.fabric8.kubernetes.api.model.NamedContext;
import io.fabric8.kubernetes.api.model.NamedExtension;
import io.fabric8.kubernetes.api.model.PreferencesBuilder;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.kubernetes.client.utils.Utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
* Helper class for working with the YAML config file thats located in
Expand Down Expand Up @@ -165,27 +166,40 @@ public static void persistKubeConfigIntoFile(Config kubeConfig, String kubeConfi
}
}

/**
* Adds the given source list to the destination list that's provided by the given supplier
* and then set to the destination by the given setter.
* Creates the list if it doesn't exist yet (supplier returns {@code null}.
* Does not copy if the given list is {@code null}.
*
* @param source the source list to add to the destination
* @param destinationSupplier supplies the list that the source shall be added to
* @param destinationSetter sets the list, once the source was added to it
*/
public static <T> void addTo(List<T> source, Supplier<List<T>> destinationSupplier, Consumer<List<T>> destinationSetter) {
if (source == null) {
return;
public static Config merge(Config thisConfig, Config thatConfig) {
if (thisConfig == null) {
return thatConfig;
}

List<T> list = destinationSupplier.get();
if (list == null) {
list = new ArrayList<>();
ConfigBuilder builder = new ConfigBuilder(thatConfig);
if (thisConfig.getClusters() != null) {
builder.addAllToClusters(thisConfig.getClusters());
}
if (thisConfig.getContexts() != null) {
builder.addAllToContexts(thisConfig.getContexts());
}
if (thisConfig.getUsers() != null) {
builder.addAllToUsers(thisConfig.getUsers());
}
list.addAll(source);
destinationSetter.accept(list);
if (thisConfig.getExtensions() != null) {
builder.addAllToExtensions(thisConfig.getExtensions());
}
if (!builder.hasCurrentContext()
&& Utils.isNotNullOrEmpty(thisConfig.getCurrentContext())) {
builder.withCurrentContext(thisConfig.getCurrentContext());
}
Config merged = builder.build();
mergePreferences(thisConfig, merged);
return merged;
}

public static void mergePreferences(io.fabric8.kubernetes.api.model.Config source,
io.fabric8.kubernetes.api.model.Config destination) {
if (source.getPreferences() != null) {
PreferencesBuilder builder = new PreferencesBuilder(destination.getPreferences());
if (source.getPreferences() != null) {
builder.addToExtensions(source.getExtensions().toArray(new NamedExtension[] {}));
}
destination.setPreferences(builder.build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
import io.fabric8.kubernetes.api.model.AuthInfo;
import io.fabric8.kubernetes.api.model.AuthProviderConfig;
import io.fabric8.kubernetes.api.model.NamedAuthInfo;
import io.fabric8.kubernetes.api.model.NamedAuthInfoBuilder;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.Config.KubeConfigFile;
import io.fabric8.kubernetes.client.KubeConfigFile;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.http.HttpClient;
import io.fabric8.kubernetes.client.http.HttpRequest;
Expand Down Expand Up @@ -84,22 +85,22 @@ private OpenIDConnectionUtils() {
* @return access token for interacting with Kubernetes API
*/
public static CompletableFuture<String> resolveOIDCTokenFromAuthConfig(
Config currentConfig, Map<String, String> currentAuthProviderConfig, HttpClient.Builder clientBuilder) {
Config currentConfig, Map<String, String> currentAuthProviderConfig, HttpClient.Builder clientBuilder) {
String originalToken = currentAuthProviderConfig.get(ID_TOKEN_KUBECONFIG);
String idpCert = currentAuthProviderConfig.getOrDefault(IDP_CERT_DATA, getClientCertDataFromConfig(currentConfig));
if (isTokenRefreshSupported(currentAuthProviderConfig)) {
final HttpClient httpClient = initHttpClientWithPemCert(idpCert, clientBuilder);
final CompletableFuture<String> result = getOpenIdConfiguration(httpClient, currentAuthProviderConfig)
.thenCompose(openIdConfiguration -> refreshOpenIdToken(httpClient, currentAuthProviderConfig, openIdConfiguration))
.thenApply(oAuthToken -> persistOAuthToken(currentConfig, oAuthToken, null))
.thenApply(oAuthToken -> {
if (oAuthToken == null || Utils.isNullOrEmpty(oAuthToken.idToken)) {
LOGGER.warn("token response did not contain an id_token, either the scope \\\"openid\\\" wasn't " +
"requested upon login, or the provider doesn't support id_tokens as part of the refresh response.");
return originalToken;
}
return oAuthToken.idToken;
});
.thenCompose(openIdConfiguration -> refreshOpenIdToken(httpClient, currentAuthProviderConfig, openIdConfiguration))
.thenApply(oAuthToken -> persistOAuthToken(currentConfig, oAuthToken, null))
.thenApply(oAuthToken -> {
if (oAuthToken == null || Utils.isNullOrEmpty(oAuthToken.idToken)) {
LOGGER.warn("token response did not contain an id_token, either the scope \\\"openid\\\" wasn't " +
"requested upon login, or the provider doesn't support id_tokens as part of the refresh response.");
return originalToken;
}
return oAuthToken.idToken;
});
result.whenComplete((s, t) -> httpClient.close());
return result;
}
Expand Down Expand Up @@ -128,9 +129,9 @@ static boolean isTokenRefreshSupported(Map<String, String> currentAuthProviderCo
* @return the OpenID Configuration as returned by the OpenID provider
*/
private static CompletableFuture<OpenIdConfiguration> getOpenIdConfiguration(HttpClient client,
Map<String, String> authProviderConfig) {
Map<String, String> authProviderConfig) {
final HttpRequest request = client.newHttpRequestBuilder()
.uri(resolveWellKnownUrlForOpenIDIssuer(authProviderConfig)).build();
.uri(resolveWellKnownUrlForOpenIDIssuer(authProviderConfig)).build();
return client.sendAsync(request, String.class).thenApply(response -> {
try {
if (response.isSuccessful() && response.body() != null) {
Expand All @@ -151,13 +152,13 @@ private static CompletableFuture<OpenIdConfiguration> getOpenIdConfiguration(Htt
* Issue Token Refresh HTTP Request to OIDC Provider
*/
private static CompletableFuture<OAuthToken> refreshOpenIdToken(
HttpClient httpClient, Map<String, String> authProviderConfig, OpenIdConfiguration openIdConfiguration) {
HttpClient httpClient, Map<String, String> authProviderConfig, OpenIdConfiguration openIdConfiguration) {
if (openIdConfiguration == null || Utils.isNullOrEmpty(openIdConfiguration.tokenEndpoint)) {
LOGGER.warn("oidc: discovery object doesn't contain a valid token endpoint: {}", openIdConfiguration);
return CompletableFuture.completedFuture(null);
}
final HttpRequest request = initTokenRefreshHttpRequest(httpClient, authProviderConfig,
openIdConfiguration.tokenEndpoint);
openIdConfiguration.tokenEndpoint);
return httpClient.sendAsync(request, String.class).thenApply(r -> {
String body = r.body();
if (body != null) {
Expand Down Expand Up @@ -199,10 +200,11 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut
}

private static void persistOAuthTokenToFile(Config currentConfig, String token, Map<String, String> authProviderConfig) {
if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) {
if (currentConfig.getCurrentContext() != null
&& currentConfig.getCurrentContext().getContext() != null) {
try {
final String userName = currentConfig.getCurrentContext().getContext().getUser();
KubeConfigFile kubeConfigFile = currentConfig.getFile(userName);
KubeConfigFile kubeConfigFile = currentConfig.getFileWithAuthInfo(userName);
if (kubeConfigFile == null) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG: file for user {} not found", userName);
return;
Expand All @@ -217,7 +219,8 @@ private static void persistOAuthTokenToFile(Config currentConfig, String token,
}
}

private static void setAuthProviderAndToken(String token, Map<String, String> authProviderConfig, NamedAuthInfo namedAuthInfo) {
private static void setAuthProviderAndToken(String token, Map<String, String> authProviderConfig,
NamedAuthInfo namedAuthInfo) {
if (namedAuthInfo.getUser() == null) {
namedAuthInfo.setUser(new AuthInfo());
}
Expand All @@ -230,21 +233,28 @@ private static void setAuthProviderAndToken(String token, Map<String, String> au
}
}

private static NamedAuthInfo getOrCreateNamedAuthInfo(String userName, io.fabric8.kubernetes.api.model.Config kubeConfig) {
private static NamedAuthInfo getOrCreateNamedAuthInfo(String name, io.fabric8.kubernetes.api.model.Config kubeConfig) {
return kubeConfig.getUsers().stream()
.filter(n -> n.getName().equals(userName))
.findFirst()
.orElseGet(() -> {
NamedAuthInfo result = new NamedAuthInfo(userName, new AuthInfo());
kubeConfig.getUsers().add(result);
return result;
});
.filter(n -> n.getName().equals(name))
.findFirst()
.orElseGet(() -> {
NamedAuthInfo authInfo = new NamedAuthInfoBuilder()
.withName(name)
.withNewUser()
.endUser()
.build();
kubeConfig.getUsers().add(authInfo);
return authInfo;
});
}

private static void persistOAuthTokenToFile(AuthProviderConfig config, Map<String, String> authProviderConfig) {
if (config == null) {
return;
}
Optional.of(config)
.map(AuthProviderConfig::getConfig)
.ifPresent(c -> c.putAll(authProviderConfig));
.map(AuthProviderConfig::getConfig)
.ifPresent(c -> c.putAll(authProviderConfig));
}

/**
Expand All @@ -268,19 +278,19 @@ private static HttpClient initHttpClientWithPemCert(String idpCert, HttpClient.B
clientBuilder.sslContext(keyManagers, trustManagers);
return clientBuilder.build();
} catch (KeyStoreException | InvalidKeySpecException | NoSuchAlgorithmException | IOException | UnrecoverableKeyException
| CertificateException e) {
| CertificateException e) {
throw KubernetesClientException.launderThrowable("Could not import idp certificate", e);
}
}

private static HttpRequest initTokenRefreshHttpRequest(
HttpClient client, Map<String, String> authProviderConfig, String tokenRefreshUrl) {
HttpClient client, Map<String, String> authProviderConfig, String tokenRefreshUrl) {

final String clientId = authProviderConfig.get(CLIENT_ID_KUBECONFIG);
final String clientSecret = authProviderConfig.getOrDefault(CLIENT_SECRET_KUBECONFIG, "");
final HttpRequest.Builder httpRequestBuilder = client.newHttpRequestBuilder().uri(tokenRefreshUrl);
final String credentials = java.util.Base64.getEncoder().encodeToString((clientId + ':' + clientSecret)
.getBytes(StandardCharsets.UTF_8));
.getBytes(StandardCharsets.UTF_8));
httpRequestBuilder.header("Authorization", "Basic " + credentials);

final Map<String, String> requestBody = new LinkedHashMap<>();
Expand All @@ -305,8 +315,8 @@ public static boolean idTokenExpired(Config config) {
Map<String, Object> jwtPayloadMap = Serialization.unmarshal(jwtPayloadDecoded, Map.class);
int expiryTimestampInSeconds = (Integer) jwtPayloadMap.get(JWT_TOKEN_EXPIRY_TIMESTAMP_KEY);
return Instant.ofEpochSecond(expiryTimestampInSeconds)
.minusSeconds(TOKEN_EXPIRY_DELTA)
.isBefore(Instant.now());
.minusSeconds(TOKEN_EXPIRY_DELTA)
.isBefore(Instant.now());
} catch (Exception e) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -291,16 +292,32 @@ public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}

public static boolean isNotNullOrEmpty(Map map) {
return !(map == null || map.isEmpty());
public static boolean isNotNullOrEmpty(Map<?, ?> map) {
return !isNullOrEmpty(map);
}

public static boolean isNullOrEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}

public static boolean isNotNullOrEmpty(Collection<?> collection) {
return !isNullOrEmpty(collection);
}

public static boolean isNullOrEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}

public static boolean isNotNullOrEmpty(String str) {
return !isNullOrEmpty(str);
}

public static boolean isNotNullOrEmpty(String[] array) {
return !(array == null || array.length == 0);
return !isNullOrEmpty(array);
}

public static boolean isNullOrEmpty(String[] array) {
return array == null || array.length == 0;
}

public static <T> boolean isNotNull(T... refList) {
Expand Down
Loading

0 comments on commit e5cde95

Please sign in to comment.