From 3960a3c41f0bb464aecbdf1440769e50fa92f21d Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 21 Aug 2024 19:54:20 +0200 Subject: [PATCH] update token in file listed in KUBECONFIG env var (#6240) Signed-off-by: Andre Dietisheim --- .../io/fabric8/kubernetes/client/Config.java | 209 ++++++++++++------ .../client/internal/KubeConfigUtils.java | 58 +++-- .../client/utils/OpenIDConnectionUtils.java | 64 +++--- .../fabric8/kubernetes/client/ConfigTest.java | 118 +++++++++- .../src/test/resources/test-kubeconfig-empty | 7 + .../test-kubeconfig-onlycurrentcontext | 7 + 6 files changed, 340 insertions(+), 123 deletions(-) create mode 100644 kubernetes-client-api/src/test/resources/test-kubeconfig-empty create mode 100644 kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentcontext 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 10b1c09f633..9277ec76b27 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,14 +56,11 @@ 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 { @@ -119,7 +116,9 @@ public class Config { public static final String KUBERNETES_NAMESPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; public static final String KUBERNETES_NAMESPACE_FILE = "kubenamespace"; public static final String KUBERNETES_NAMESPACE_SYSTEM_PROPERTY = "kubernetes.namespace"; + @Deprecated public static final String KUBERNETES_KUBECONFIG_FILE = "kubeconfig"; + public static final String KUBERNETES_KUBECONFIG_FILES = "kubeconfig"; public static final String KUBERNETES_SERVICE_HOST_PROPERTY = "KUBERNETES_SERVICE_HOST"; public static final String KUBERNETES_SERVICE_PORT_PROPERTY = "KUBERNETES_SERVICE_PORT"; public static final String KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"; @@ -151,6 +150,8 @@ public class Config { public static final String HTTP_PROTOCOL_PREFIX = "http://"; public static final String HTTPS_PROTOCOL_PREFIX = "https://"; + public static final File DEFAULT_KUBECONFIG_FILE = Paths.get(System.getProperty("user.home"), ".kube", "config").toFile(); + private static final String ACCESS_TOKEN = "access-token"; private static final String ID_TOKEN = "id-token"; private static final int DEFAULT_WATCH_RECONNECT_INTERVAL = 1000; @@ -235,7 +236,6 @@ public class Config { private Boolean autoConfigure; @Deprecated - private File file; private List files = new ArrayList<>(); @JsonIgnore @@ -813,7 +813,7 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S // we allow passing context along here, since downstream accepts it Config config = new Config(false); if (kubeconfigPath != null) { - config.file = new File(kubeconfigPath); + config.files = Arrays.asList(new File(kubeconfigPath)); } if (!loadFromKubeconfig(config, context, kubeconfigContents)) { throw new KubernetesClientException("Could not create Config from kubeconfig"); @@ -838,14 +838,15 @@ public Config refresh() { if (this.autoConfigure) { return Config.autoConfigure(currentContextName); } - if (this.file != null) { - String kubeconfigContents = getKubeconfigContents(this.file); - if (kubeconfigContents == null) { - return this; // getKubeconfigContents will have logged an exception + File file = getFile(); + // if file is null there's nothing to refresh - the kubeconfig was directly supplied + if (file != null) { + String kubeconfigContents = getKubeconfigContents(file); + // getKubeconfigContents will have logged an exception if content is null + if (kubeconfigContents != null) { + return Config.fromKubeconfig(currentContextName, kubeconfigContents, file.getPath()); } - return Config.fromKubeconfig(currentContextName, kubeconfigContents, this.file.getPath()); } - // nothing to refresh - the kubeconfig was directly supplied return this; } @@ -854,56 +855,42 @@ private static boolean tryKubeConfig(Config config, String context) { if (!Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, true)) { return false; } - List kubeConfigFilenames = Arrays.asList(getKubeconfigFilenames()); - if (kubeConfigFilenames.isEmpty()) { - return false; - } - 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) { + String[] kubeConfigFilenames = getKubeconfigFilenames(); + if (kubeConfigFilenames == null + || kubeConfigFilenames.length == 0) { return false; } - config.file = mainKubeConfig; - config.files = allKubeConfigFiles; - - List additionalConfigs = config.files.subList(1, allKubeConfigFiles.size()); - addAdditionalConfigs(kubeConfig, additionalConfigs); - - return loadFromKubeconfig(config, context, mainKubeConfig); + List allFiles = Arrays.stream(kubeConfigFilenames) + .map(File::new) + .collect(Collectors.toList()); + config.files = allFiles; + io.fabric8.kubernetes.api.model.Config mergedConfig = mergeKubeConfigs(allFiles); + return loadFromKubeconfig(config, context, mergedConfig); } - private static void addAdditionalConfigs(io.fabric8.kubernetes.api.model.Config kubeConfig, List files) { + private static io.fabric8.kubernetes.api.model.Config mergeKubeConfigs(List files) { if (files == null - || files.isEmpty()) { - return; + || files.isEmpty()) { + return null; } - 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); - }); + return files.stream() + .map(Config::createKubeconfig) + .reduce(null, (merged, additionalConfig) -> { + if (additionalConfig != null) { + return KubeConfigUtils.merge(additionalConfig, merged); + } else { + return merged; + } + }); } 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()) { + && !content.isEmpty()) { kubeConfig = KubeConfigUtils.parseConfigFromString(content); } } catch (KubernetesClientException e) { @@ -926,7 +913,7 @@ public static String getKubeconfigFilename() { fileName = fileNames[0]; if (fileNames.length > 1) { LOGGER.info("Found multiple Kubernetes config files [{}], returning the first one. Use #getKubeconfigFilenames instead", - fileNames[0]); + fileNames[0]); } } return fileName; @@ -934,16 +921,36 @@ public static String getKubeconfigFilename() { public static String[] getKubeconfigFilenames() { String[] fileNames = null; - String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILE); + String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILES); + if (fileName != null + && !fileName.isEmpty()) { + fileNames = fileName.split(File.pathSeparator); + } + if (fileNames == null + || fileNames.length == 0) { + fileNames = new String[] { DEFAULT_KUBECONFIG_FILE.toString() }; + } + return Arrays.stream(fileNames) + .filter(filename -> + isReadableKubeconfFile(new File(filename))) + .toArray(String[]::new); + } - fileNames = fileName.split(File.pathSeparator); - if (fileNames.length == 0) { - fileNames = new String[] { new File(getHomeDir(), ".kube" + File.separator + "config").toString() }; + private static boolean isReadableKubeconfFile(File file) { + if (file == null) { + return false; + } + if (!file.isFile()) { + LOGGER.debug("Did not find Kubernetes config at: [{}]. Ignoring.", file.getPath()); + return false; } - return fileNames; + return true; } private static String getKubeconfigContents(File kubeConfigFile) { + if (kubeConfigFile == null) { + return null; + } String kubeconfigContents = null; try (FileReader reader = new FileReader(kubeConfigFile)) { kubeconfigContents = IOHelpers.readFully(reader); @@ -954,21 +961,20 @@ 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. private static boolean loadFromKubeconfig(Config config, String context, String kubeconfigContents) { + if (kubeconfigContents != null && !kubeconfigContents.isEmpty()) { + return loadFromKubeconfig(config, context, KubeConfigUtils.parseConfigFromString(kubeconfigContents)); + } else { + return false; + } + } + + private static boolean loadFromKubeconfig(Config config, String context, io.fabric8.kubernetes.api.model.Config kubeConfig) { try { - if (kubeconfigContents != null && !kubeconfigContents.isEmpty()) { - io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils.parseConfigFromString(kubeconfigContents); + if (kubeConfig != null) { mergeKubeConfigContents(config, context, kubeConfig); return true; } @@ -1006,7 +1012,7 @@ private static void mergeKubeConfigAuthInfo(Config config, Cluster currentCluste String caCertFile = currentCluster.getCertificateAuthority(); String clientCertFile = currentAuthInfo.getClientCertificate(); String clientKeyFile = currentAuthInfo.getClientKey(); - File configFile = config.file; + File configFile = config.getFile(); if (configFile != null) { caCertFile = absolutify(configFile, currentCluster.getCertificateAuthority()); clientCertFile = absolutify(configFile, currentAuthInfo.getClientCertificate()); @@ -1144,6 +1150,7 @@ protected static String getCommandWithFullyQualifiedPath(String command, String private static Context setCurrentContext(String context, Config config, io.fabric8.kubernetes.api.model.Config kubeConfig) { if (context != null) { + // override existing current-context kubeConfig.setCurrentContext(context); } Context currentContext = null; @@ -1738,11 +1745,74 @@ public void setCurrentContext(NamedContext context) { /** * * Returns the path to the file that this configuration was loaded from. Returns {@code null} if no file was used. + * + * @deprecated use {@link #getFiles} instead. * - * @return the path to the kubeConfig file + * @return the kubeConfig file */ + @Deprecated public File getFile() { - return file; + if (files != null + && !files.isEmpty()) { + return files.get(0); + } else { + return null; + } + } + + /** + * Returns the kube config files that are used to configure this client. + * Returns the files that are listed in the KUBERNETES_KUBECONFIG_FILES env or system variables. + * Returns the default kube config file if it's not set'. + * + * @return + */ + public List getFiles() { + return files; + } + + public KubeConfigFile getFile(String username) { + if (username == null + || username.isEmpty()) { + return null; + } + return Arrays.stream(getKubeconfigFilenames()) + .map(File::new) + .map(file -> { + try { + return new KubeConfigFile(file, KubeConfigUtils.parseConfig(file)); + } catch (IOException e) { + return null; + } + }) + .filter(entry -> entry != null + && entry.getConfig() != null + && hasAuthInfo(username, entry.getConfig())) + .findFirst() + .orElse(null); + } + + private boolean hasAuthInfo(String username, io.fabric8.kubernetes.api.model.Config kubeConfig) { + return kubeConfig.getUsers().stream() + .anyMatch(namedAuthInfo -> username.equals(namedAuthInfo.getUser().getUsername())); + } + + public static class KubeConfigFile { + private final File file; + private final io.fabric8.kubernetes.api.model.Config config; + + private KubeConfigFile(File file, io.fabric8.kubernetes.api.model.Config config) { + this.file = file; + this.config = config; + } + + public File getFile() { + return file; + } + + public io.fabric8.kubernetes.api.model.Config getConfig() { + return config; + } } @JsonIgnore @@ -1768,8 +1838,13 @@ public void setAdditionalProperty(String name, Object value) { this.additionalProperties.put(name, value); } + @Deprecated public void setFile(File file) { - this.file = file; + setFiles(Collections.singletonList(file)); + } + + public void setFiles(List files) { + this.files = files; } public void setAutoConfigure(boolean autoConfigure) { 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 f5793316ca4..8f293d4f8e3 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 @@ -18,20 +18,20 @@ 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 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 @@ -165,27 +165,41 @@ 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 void addTo(List source, Supplier> destinationSupplier, Consumer> destinationSetter) { - if (source == null) { - return; + public static Config merge(Config thisConfig, Config thatConfig) { + if (thisConfig == null) { + return thatConfig; } - - List 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() + && thisConfig.getCurrentContext() != null + && !thisConfig.getCurrentContext().isEmpty()) { + 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()); + } + } } 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 c2ae79374a8..60915905c31 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,7 +21,6 @@ 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; @@ -84,22 +83,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; } @@ -128,9 +127,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) { @@ -151,13 +150,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) { @@ -202,7 +201,7 @@ private static void persistOAuthTokenToFile(Config currentConfig, String token, if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) { try { final String userName = currentConfig.getCurrentContext().getContext().getUser(); - KubeConfigFile kubeConfigFile = currentConfig.getFile(userName); + Config.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; @@ -217,7 +216,8 @@ private static void persistOAuthTokenToFile(Config currentConfig, String token, } } - private static void setAuthProviderAndToken(String token, Map authProviderConfig, NamedAuthInfo namedAuthInfo) { + private static void setAuthProviderAndToken(String token, Map authProviderConfig, + NamedAuthInfo namedAuthInfo) { if (namedAuthInfo.getUser() == null) { namedAuthInfo.setUser(new AuthInfo()); } @@ -232,19 +232,19 @@ private static void setAuthProviderAndToken(String token, Map au 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; - }); + .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)); + .map(AuthProviderConfig::getConfig) + .ifPresent(c -> c.putAll(authProviderConfig)); } /** @@ -268,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<>(); @@ -305,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; } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java index ad069d389de..50733cbb2b8 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigTest.java @@ -1215,11 +1215,11 @@ void refresh_whenOAuthTokenSourceSetToUser_thenConfigUnchanged() { } @Test - void givenEmptyKubeConfig_whenConfigCreated_thenShouldNotProduceNPE() throws URISyntaxException { + void build_givenEmptyKubeConfig_whenConfigCreated_thenShouldNotProduceNPE() throws URISyntaxException { try { // Given System.setProperty("kubeconfig", - new File(Objects.requireNonNull(getClass().getResource("/test-empty-kubeconfig")).toURI()).getAbsolutePath()); + getResourceAbsolutePath("/test-empty-kubeconfig")); // When Config config = new ConfigBuilder().build(); @@ -1230,4 +1230,118 @@ void givenEmptyKubeConfig_whenConfigCreated_thenShouldNotProduceNPE() throws URI System.clearProperty("kubeconfig"); } } + + @Test + void build_givenSeveralKubeConfigs_whenAutoconfigured_and_currentContextInFirstFile_then_shouldUseCurrentContextInFirstFile() + throws URISyntaxException { + try { + // Given + System.setProperty(Config.KUBERNETES_KUBECONFIG_FILES, + // only has current-context + getResourceAbsolutePath("/test-kubeconfig-onlycurrentcontext") + File.pathSeparator + + // has all contexts, clusters, users + getResourceAbsolutePath("/test-kubeconfig") + ); + + // When + Config config = new ConfigBuilder().build(); + + // Then + assertThat(config.getCurrentContext()).isNotNull(); + // current-context set by 1st file, current context in 2nd file is ignored + assertThat(config.getCurrentContext().getName()).isEqualTo("production/172-28-128-4:8443/root"); + } finally { + System.clearProperty(Config.KUBERNETES_KUBECONFIG_FILES); + } + } + + @Test + void build_givenSeveralKubeConfigs_whenAutoconfigured_and_currentContextInSecondFile_then_shouldUseCurrentContextInSecondFile() + throws URISyntaxException { + try { + // Given + System.setProperty(Config.KUBERNETES_KUBECONFIG_FILES, + // only has current-context + getResourceAbsolutePath("/test-kubeconfig-empty") + File.pathSeparator + + // has all contexts, clusters, users + getResourceAbsolutePath("/test-kubeconfig")); + + // When + Config config = new ConfigBuilder().build(); + + // Then + assertThat(config.getCurrentContext()).isNotNull(); + // current-context set by 1st file, current context in 2nd file is ignored + assertThat(config.getCurrentContext().getName()).isEqualTo("testns/172-28-128-4:8443/user"); + } finally { + System.clearProperty(Config.KUBERNETES_KUBECONFIG_FILES); + } + } + + @Test + void getKubeconfigFilenames_given_filenames_defined_in_KUBECONFIG_then_returns_all_filenames() throws URISyntaxException { + try { + // Given + // only has current-context + String file1 = getResourceAbsolutePath("/test-kubeconfig-empty"); + // has all contexts, clusters, users + String file2 = getResourceAbsolutePath("/test-kubeconfig"); + System.setProperty(Config.KUBERNETES_KUBECONFIG_FILES, file1 + File.pathSeparator + file2); + + // When + String[] filenames = Config.getKubeconfigFilenames(); + + // Then + assertThat(filenames) + .isNotNull() + .hasSize(2) + .containsExactly(file1, file2); + } finally { + System.clearProperty(Config.KUBERNETES_KUBECONFIG_FILES); + } + } + + @Test + void getKubeconfigFilenames_given_directory_defined_in_KUBECONFIG_then_does_not_return_it() throws URISyntaxException { + try { + // Given + // only has current-context + String file1 = new File("/dev/null").getAbsolutePath(); + // has all contexts, clusters, users + String file2 = getResourceAbsolutePath("/test-kubeconfig"); + System.setProperty(Config.KUBERNETES_KUBECONFIG_FILES, file1 + File.pathSeparator + file2); + + // When + String[] filenames = Config.getKubeconfigFilenames(); + + // Then + assertThat(filenames) + .isNotNull() + .hasSize(1) + .containsExactly(file2); + } finally { + System.clearProperty(Config.KUBERNETES_KUBECONFIG_FILES); + } + } + + @Test + void getKubeconfigFilenames_given_KUBECONF_not_set_then_returns_default() { + // Given + // only has current-context + System.clearProperty(Config.KUBERNETES_KUBECONFIG_FILES); + + // When + String[] filenames = Config.getKubeconfigFilenames(); + + // Then + assertThat(filenames) + .isNotNull() + .hasSize(1) + .containsExactly(Config.DEFAULT_KUBECONFIG_FILE.getAbsolutePath()); + } + + private String getResourceAbsolutePath(String filename) throws URISyntaxException { + return new File(Objects.requireNonNull(getClass().getResource(filename)).toURI()).getAbsolutePath(); + } + } diff --git a/kubernetes-client-api/src/test/resources/test-kubeconfig-empty b/kubernetes-client-api/src/test/resources/test-kubeconfig-empty new file mode 100644 index 00000000000..81265e28f6e --- /dev/null +++ b/kubernetes-client-api/src/test/resources/test-kubeconfig-empty @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Config +clusters: null +contexts: null +current-context: null +preferences: {} +users: null \ No newline at end of file diff --git a/kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentcontext b/kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentcontext new file mode 100644 index 00000000000..dc9a9504c8b --- /dev/null +++ b/kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentcontext @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Config +clusters: null +contexts: null +current-context: production/172-28-128-4:8443/root +preferences: {} +users: null \ No newline at end of file