From ab95243ecb44941d58a1896f3450b97ced286afc Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 21 Aug 2024 19:18:33 +0200 Subject: [PATCH] use kubeconfigs listed in KUBECONFIG env var (#6240) Signed-off-by: Andre Dietisheim --- .../io/fabric8/kubernetes/client/Config.java | 106 ++++++++++++++--- .../client/internal/KubeConfigUtils.java | 27 +++++ .../client/utils/OpenIDConnectionUtils.java | 107 +++++++++++------- 3 files changed, 180 insertions(+), 60 deletions(-) diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java index 0d4e09637ba..c8002bf81a7 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java @@ -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 { @@ -232,7 +235,9 @@ public class Config { private Boolean autoConfigure; + @Deprecated private File file; + private List files = new ArrayList<>(); @JsonIgnore protected Map additionalProperties = new HashMap<>(); @@ -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 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 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 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 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)) { @@ -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. diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java index fbc77a3cfb7..f5793316ca4 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java @@ -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 @@ -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 void addTo(List source, Supplier> destinationSupplier, Consumer> destinationSetter) { + if (source == null) { + return; + } + + List list = destinationSupplier.get(); + if (list == null) { + list = new ArrayList<>(); + } + list.addAll(source); + destinationSetter.accept(list); + } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java index ef5fe66061c..c2ae79374a8 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java @@ -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; @@ -83,22 +84,22 @@ private OpenIDConnectionUtils() { * @return access token for interacting with Kubernetes API */ public static CompletableFuture resolveOIDCTokenFromAuthConfig( - Config currentConfig, Map currentAuthProviderConfig, HttpClient.Builder clientBuilder) { + Config currentConfig, Map 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 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; } @@ -127,9 +128,9 @@ static boolean isTokenRefreshSupported(Map currentAuthProviderCo * @return the OpenID Configuration as returned by the OpenID provider */ private static CompletableFuture getOpenIdConfiguration(HttpClient client, - Map authProviderConfig) { + Map 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) { @@ -150,13 +151,13 @@ private static CompletableFuture getOpenIdConfiguration(Htt * Issue Token Refresh HTTP Request to OIDC Provider */ private static CompletableFuture refreshOpenIdToken( - HttpClient httpClient, Map authProviderConfig, OpenIdConfiguration openIdConfiguration) { + HttpClient httpClient, Map 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) { @@ -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 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 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 authProviderConfig) { + Optional.of(config) + .map(AuthProviderConfig::getConfig) + .ifPresent(c -> c.putAll(authProviderConfig)); } /** @@ -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 authProviderConfig, String tokenRefreshUrl) { + HttpClient client, Map 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 requestBody = new LinkedHashMap<>(); @@ -282,8 +305,8 @@ public static boolean idTokenExpired(Config config) { Map 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; }