Skip to content

Commit

Permalink
use kubeconfigs 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 Sep 12, 2024
1 parent 32b3473 commit 8b8fee9
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

import static io.fabric8.kubernetes.client.internal.KubeConfigUtils.addTo;

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(allowGetters = true, allowSetters = true)
public class Config {
Expand Down Expand Up @@ -232,7 +235,9 @@ public class Config {

private Boolean autoConfigure;

@Deprecated
private File file;
private List<File> files = new ArrayList<>();

@JsonIgnore
protected Map<String, Object> additionalProperties = new HashMap<>();
Expand Down Expand Up @@ -856,37 +861,94 @@ private static boolean tryKubeConfig(Config config, String context) {
if (!Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, true)) {
return false;
}
File kubeConfigFile = new File(getKubeconfigFilename());
if (!kubeConfigFile.isFile()) {
LOGGER.debug("Did not find Kubernetes config at: [{}]. Ignoring.", kubeConfigFile.getPath());
List<String> kubeConfigFilenames = Arrays.asList(getKubeconfigFilenames());
if (kubeConfigFilenames.isEmpty()) {
return false;
}
LOGGER.debug("Found for Kubernetes config at: [{}].", kubeConfigFile.getPath());
String kubeconfigContents = getKubeconfigContents(kubeConfigFile);
if (kubeconfigContents == null) {
List<File> allKubeConfigFiles = kubeConfigFilenames.stream()
.map(File::new)
.collect(Collectors.toList());
File mainKubeConfig = allKubeConfigFiles.get(0);
io.fabric8.kubernetes.api.model.Config kubeConfig = createKubeconfig(mainKubeConfig);
if (kubeConfig == null) {
return false;
}
config.file = new File(kubeConfigFile.getPath());
return loadFromKubeconfig(config, context, kubeconfigContents);
config.file = mainKubeConfig;
config.files = allKubeConfigFiles;

List<File> additionalConfigs = config.files.subList(1, allKubeConfigFiles.size());
addAdditionalConfigs(kubeConfig, additionalConfigs);

return loadFromKubeconfig(config, context, mainKubeConfig);
}

public static String getKubeconfigFilename() {
String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILE,
new File(getHomeDir(), ".kube" + File.separator + "config").toString());
private static void addAdditionalConfigs(io.fabric8.kubernetes.api.model.Config kubeConfig, List<File> files) {
if (files == null
|| files.isEmpty()) {
return;
}
files.stream()
.map(Config::createKubeconfig)
.filter(Objects::nonNull)
.forEach(additionalConfig -> {
addTo(additionalConfig.getContexts(), kubeConfig::getContexts, kubeConfig::setContexts);
addTo(additionalConfig.getClusters(), kubeConfig::getClusters, kubeConfig::setClusters);
addTo(additionalConfig.getUsers(), kubeConfig::getUsers, kubeConfig::setUsers);
});
}

// if system property/env var contains multiple files take the first one based on the environment
// we are running in (eg. : for Linux, ; for Windows)
String[] fileNames = fileName.split(File.pathSeparator);
private static io.fabric8.kubernetes.api.model.Config createKubeconfig(File file) {
if (file == null) {
return null;
}
if (!file.isFile()) {
LOGGER.debug("Did not find Kubernetes config at: [{}]. Ignoring.", file.getPath());
return null;
}
io.fabric8.kubernetes.api.model.Config kubeConfig = null;
LOGGER.debug("Found for Kubernetes config at: [{}].", file.getPath());
try {
String content = getKubeconfigContents(file);
if (content != null
&& !content.isEmpty()) {
kubeConfig = KubeConfigUtils.parseConfigFromString(content);
}
} catch (KubernetesClientException e) {
LOGGER.error("Could not load Kubernetes config [{}].", file.getPath(), e);
}

if (fileNames.length > 1) {
LOGGER.warn(
"Found multiple Kubernetes config files [{}], using the first one: [{}]. If not desired file, please change it by doing `export KUBECONFIG=/path/to/kubeconfig` on Unix systems or `$Env:KUBECONFIG=/path/to/kubeconfig` on Windows.",
fileNames, fileNames[0]);
return kubeConfig;
}

/**
* @deprecated use {@link #getKubeconfigFilenames()} instead
*/
@Deprecated
public static String getKubeconfigFilename() {
String fileName = null;
String[] fileNames = getKubeconfigFilenames();
if (Utils.isNotNullOrEmpty(fileNames)) {
// if system property/env var contains multiple files take the first one based on the environment
fileName = fileNames[0];
if (fileNames.length > 1) {
LOGGER.info("Found multiple Kubernetes config files [{}], returning the first one. Use #getKubeconfigFilenames instead",
fileNames[0]);
}
}
return fileName;
}

public static String[] getKubeconfigFilenames() {
String[] fileNames = null;
String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILE);

fileNames = fileName.split(File.pathSeparator);
if (fileNames.length == 0) {
fileNames = new String[] { new File(getHomeDir(), ".kube" + File.separator + "config").toString() };
}
return fileNames;
}

private static String getKubeconfigContents(File kubeConfigFile) {
String kubeconfigContents = null;
try (FileReader reader = new FileReader(kubeConfigFile)) {
Expand All @@ -898,6 +960,14 @@ private static String getKubeconfigContents(File kubeConfigFile) {
return kubeconfigContents;
}

private static boolean loadFromKubeconfig(Config config, String context, File kubeConfigFile) {
String contents = getKubeconfigContents(kubeConfigFile);
if (contents == null) {
return false;
}
return loadFromKubeconfig(config, context, contents);
}

// Note: kubeconfigPath is optional
// It is only used to rewrite relative tls asset paths inside kubeconfig when a file is passed, and in the case that
// the kubeconfig references some assets via relative paths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
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 @@ -161,4 +164,28 @@ public static void persistKubeConfigIntoFile(Config kubeConfig, String kubeConfi
writer.write(Serialization.asYaml(kubeConfig));
}
}

/**
* 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;
}

List<T> list = destinationSupplier.get();
if (list == null) {
list = new ArrayList<>();
}
list.addAll(source);
destinationSetter.accept(list);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.fabric8.kubernetes.api.model.AuthProviderConfig;
import io.fabric8.kubernetes.api.model.NamedAuthInfo;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.Config.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 @@ -83,22 +84,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 @@ -127,9 +128,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 @@ -150,13 +151,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 @@ -190,38 +191,60 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut
if (oAuthToken != null) {
authProviderConfig.put(ID_TOKEN_KUBECONFIG, oAuthToken.idToken);
authProviderConfig.put(REFRESH_TOKEN_KUBECONFIG, oAuthToken.refreshToken);
// Persist in memory
Optional.of(currentConfig).map(Config::getAuthProvider).map(AuthProviderConfig::getConfig)
.ifPresent(c -> c.putAll(authProviderConfig));
persistOAuthTokenToFile(currentConfig.getAuthProvider(), authProviderConfig);
}
// Persist in file
persistOAuthTokenToFile(currentConfig, token, authProviderConfig);

return oAuthToken;
}

private static void persistOAuthTokenToFile(Config currentConfig, String token, Map<String, String> authProviderConfig) {
if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) {
try {
final io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils.parseConfig(currentConfig.getFile());
final String userName = currentConfig.getCurrentContext().getContext().getUser();
NamedAuthInfo namedAuthInfo = kubeConfig.getUsers().stream().filter(n -> n.getName().equals(userName)).findFirst()
.orElseGet(() -> {
NamedAuthInfo result = new NamedAuthInfo(userName, new AuthInfo());
kubeConfig.getUsers().add(result);
return result;
});
if (namedAuthInfo.getUser() == null) {
namedAuthInfo.setUser(new AuthInfo());
}
if (namedAuthInfo.getUser().getAuthProvider() == null) {
namedAuthInfo.getUser().setAuthProvider(new AuthProviderConfig());
KubeConfigFile kubeConfigFile = currentConfig.getFile(userName);
if (kubeConfigFile == null) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG: file for user {} not found", userName);
return;
}
namedAuthInfo.getUser().getAuthProvider().getConfig().putAll(authProviderConfig);
if (Utils.isNotNullOrEmpty(token)) {
namedAuthInfo.getUser().setToken(token);
}
KubeConfigUtils.persistKubeConfigIntoFile(kubeConfig, currentConfig.getFile().getAbsolutePath());
final NamedAuthInfo namedAuthInfo = getOrCreateNamedAuthInfo(userName, kubeConfigFile.getConfig());
setAuthProviderAndToken(token, authProviderConfig, namedAuthInfo);

KubeConfigUtils.persistKubeConfigIntoFile(kubeConfigFile.getConfig(), kubeConfigFile.getFile().getAbsolutePath());
} catch (IOException ex) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG", ex);
}
}
}

private static void setAuthProviderAndToken(String token, Map<String, String> authProviderConfig, NamedAuthInfo namedAuthInfo) {
if (namedAuthInfo.getUser() == null) {
namedAuthInfo.setUser(new AuthInfo());
}
if (namedAuthInfo.getUser().getAuthProvider() == null) {
namedAuthInfo.getUser().setAuthProvider(new AuthProviderConfig());
}
namedAuthInfo.getUser().getAuthProvider().getConfig().putAll(authProviderConfig);
if (Utils.isNotNullOrEmpty(token)) {
namedAuthInfo.getUser().setToken(token);
}
}

return oAuthToken;
private static NamedAuthInfo getOrCreateNamedAuthInfo(String userName, 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;
});
}

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

/**
Expand All @@ -245,19 +268,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 @@ -282,8 +305,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

0 comments on commit 8b8fee9

Please sign in to comment.