From 03101a6ec5b3fce58a92a734797bb1aa20c31137 Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Thu, 7 Nov 2024 17:05:58 +0100 Subject: [PATCH] feat(config): support for scattered kubeconfig files Signed-off-by: Marc Nuri --- .../io/fabric8/kubernetes/client/Config.java | 30 ++++- .../client/internal/KubeConfigUtils.java | 108 +++++++++++++----- .../client/utils/OpenIDConnectionUtils.java | 7 +- .../client/ConfigAutoConfigureTest.java | 37 +++++- .../fabric8/kubernetes/client/ConfigTest.java | 9 +- .../internal/KubeConfigUtilsMergeTest.java | 38 +++++- .../client/internal/KubeConfigUtilsTest.java | 62 +++++++--- .../OpenIDConnectionUtilsBehaviorTest.java | 51 +++++++++ .../scattered-cluster.yaml | 20 ++++ .../scattered-context.yaml | 21 ++++ .../config-auto-configure/scattered-user.yaml | 20 ++++ .../config-auto-configure/scattered.yaml | 17 +++ 12 files changed, 353 insertions(+), 67 deletions(-) create mode 100644 kubernetes-client-api/src/test/resources/config-auto-configure/scattered-cluster.yaml create mode 100644 kubernetes-client-api/src/test/resources/config-auto-configure/scattered-context.yaml create mode 100644 kubernetes-client-api/src/test/resources/config-auto-configure/scattered-user.yaml create mode 100644 kubernetes-client-api/src/test/resources/config-auto-configure/scattered.yaml 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 d4f670367f5..b7d0f04c17d 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 @@ -799,10 +799,12 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S if (Utils.isNullOrEmpty(kubeconfigContents)) { throw new KubernetesClientException("Could not create Config from kubeconfig"); } - final var kubeconfig = KubeConfigUtils.parseConfigFromString(kubeconfigContents); + final io.fabric8.kubernetes.api.model.Config kubeconfig; if (kubeconfigPath != null) { // TODO: temp workaround until the method is removed (marked for removal in 7.0.0) - kubeconfig.setAdditionalProperty("KUBERNETES_CONFIG_FILE_KEY", new File(kubeconfigPath)); + kubeconfig = KubeConfigUtils.parseConfig(new File(kubeconfigPath)); + } else { + kubeconfig = KubeConfigUtils.parseConfigFromString(kubeconfigContents); } KubeConfigUtils.merge(config, context, kubeconfig); if (!disableAutoConfig()) { @@ -1463,7 +1465,29 @@ public void setCurrentContext(NamedContext context) { * @return the path to the kubeconfig file. */ public File getFile() { - return KubeConfigUtils.getFileFromContext(getCurrentContext()); + return KubeConfigUtils.getFileWithNamedContext(getCurrentContext()); + } + + /** + * Returns the path to the file that contains the cluster information from which this configuration was loaded from. + *

+ * Returns {@code null} if no file was used. + * + * @return the path to the kubeconfig file. + */ + public File getFileWithCluster() { + return KubeConfigUtils.getFileWithNamedCluster(getCurrentContext()); + } + + /** + * Returns the path to the file that contains the user information from which this configuration was loaded from. + *

+ * Returns {@code null} if no file was used. + * + * @return the path to the kubeconfig file. + */ + public File getFileWithAuthInfo() { + return KubeConfigUtils.getFileWithNamedAuthInfo(getCurrentContext()); } @JsonIgnore 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 3cb58d5f635..50810e6c2f7 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 @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Supplier; import java.util.stream.Collectors; import static io.fabric8.kubernetes.client.Config.HTTPS_PROTOCOL_PREFIX; @@ -55,7 +56,9 @@ public class KubeConfigUtils { private static final Logger logger = LoggerFactory.getLogger(io.fabric8.kubernetes.client.Config.class); - private static final String KUBERNETES_CONFIG_FILE_KEY = "KUBERNETES_CONFIG_FILE_KEY"; + private static final String KUBERNETES_CONFIG_CONTEXT_FILE_KEY = "KUBERNETES_CONFIG_CONTEXT_FILE_KEY"; + private static final String KUBERNETES_CONFIG_CLUSTER_FILE_KEY = "KUBERNETES_CONFIG_CLUSTER_FILE_KEY"; + private static final String KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY = "KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY"; private static final String ACCESS_TOKEN = "access-token"; private static final String ID_TOKEN = "id-token"; @@ -68,7 +71,16 @@ public static Config parseConfig(File kubeconfig) { } try (var fis = Files.newInputStream(kubeconfig.toPath())) { final var ret = Serialization.unmarshal(fis, Config.class); - ret.setAdditionalProperty(KUBERNETES_CONFIG_FILE_KEY, kubeconfig); + if (ret.getContexts() != null) { + ret.getContexts().forEach(ctx -> ctx.getAdditionalProperties().put(KUBERNETES_CONFIG_CONTEXT_FILE_KEY, kubeconfig)); + } + if (ret.getClusters() != null) { + ret.getClusters() + .forEach(cluster -> cluster.getAdditionalProperties().put(KUBERNETES_CONFIG_CLUSTER_FILE_KEY, kubeconfig)); + } + if (ret.getUsers() != null) { + ret.getUsers().forEach(user -> user.getAdditionalProperties().put(KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY, kubeconfig)); + } return ret; } catch (Exception e) { throw KubernetesClientException.launderThrowable(kubeconfig + " (File) is not a parseable Kubernetes Config", e); @@ -87,34 +99,60 @@ public static Config parseConfigFromString(String contents) { * @throws IOException in case of failure while writing to file. */ public static void persistKubeConfigIntoFile(Config kubeconfig, File kubeConfigPath) throws IOException { - if (kubeconfig.getAdditionalProperties() != null) { - kubeconfig.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY); - } if (kubeconfig.getContexts() != null) { - kubeconfig.getContexts().stream() - .filter(ctx -> ctx.getAdditionalProperties() != null) - .forEach(ctx -> ctx.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY)); + kubeconfig.getContexts().forEach(c -> removeAdditionalProperties(c::getAdditionalProperties)); + } + if (kubeconfig.getClusters() != null) { + kubeconfig.getClusters().forEach(c -> removeAdditionalProperties(c::getAdditionalProperties)); } if (kubeconfig.getUsers() != null) { - kubeconfig.getUsers().stream() - .filter(u -> u.getAdditionalProperties() != null) - .forEach(u -> u.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY)); + kubeconfig.getUsers().forEach(c -> removeAdditionalProperties(c::getAdditionalProperties)); } Files.writeString(kubeConfigPath.toPath(), Serialization.asYaml(kubeconfig)); } - public static File getFileFromContext(NamedContext namedContext) { - return namedContext != null && namedContext.getAdditionalProperties() != null - && namedContext.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) instanceof File - ? (File) namedContext.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) - : null; + /** + * Returns the file containing the context information if it was loaded using KubeConfigUtils#parseConfig. + * + * @param namedContext the context to get the file from. + * @return the file containing the context information if it was loaded using KubeConfigUtils#parseConfig or null. + */ + public static File getFileWithNamedContext(NamedContext namedContext) { + return getFile(namedContext != null ? namedContext::getAdditionalProperties : null, KUBERNETES_CONFIG_CONTEXT_FILE_KEY); + } + + /** + * Returns the file containing the cluster information if it was loaded using KubeConfigUtils#parseConfig. + * + * @param namedContext the context to get the file from. + * @return the file containing the cluster information if it was loaded using KubeConfigUtils#parseConfig or null. + */ + public static File getFileWithNamedCluster(NamedContext namedContext) { + return getFile(namedContext != null ? namedContext::getAdditionalProperties : null, KUBERNETES_CONFIG_CLUSTER_FILE_KEY); + } + + /** + * Returns the file containing the auth info information if it was loaded using KubeConfigUtils#parseConfig. + * + * @param namedContext the context to get the file from. + * @return the file containing the auth info information if it was loaded using KubeConfigUtils#parseConfig or null. + */ + public static File getFileWithNamedAuthInfo(NamedContext namedContext) { + return getFile(namedContext != null ? namedContext::getAdditionalProperties : null, KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY); + } + + private static File getFileWithNamedCluster(NamedCluster namedCluster) { + return getFile(namedCluster != null ? namedCluster::getAdditionalProperties : null, KUBERNETES_CONFIG_CLUSTER_FILE_KEY); } - public static File getFileFromAuthInfo(NamedAuthInfo namedAuthInfo) { - return namedAuthInfo != null && namedAuthInfo.getAdditionalProperties() != null - && namedAuthInfo.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) instanceof File - ? (File) namedAuthInfo.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) - : null; + private static File getFileWithNamedAuthInfo(NamedAuthInfo namedAuthInfo) { + return getFile(namedAuthInfo != null ? namedAuthInfo::getAdditionalProperties : null, KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY); + } + + private static File getFile(Supplier> provider, String key) { + return provider != null && provider.get() != null && provider.get().get(key) instanceof File + ? (File) provider.get().get(key) + : null; } /** @@ -144,10 +182,12 @@ public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Strin clientConfig.setCurrentContext(currentContext); clientConfig.setNamespace(currentContext.getContext().getNamespace()); final var mergedClusters = mergeClusters(kubeconfigs); - if (mergedClusters.containsKey(currentContext.getContext().getCluster())) { + final var currentNamedCluster = mergedClusters.get(currentContext.getContext().getCluster()); + if (currentNamedCluster != null) { // If config was loaded using KubeConfigUtils#parseConfig, then the file is available in the additional properties - final File configFile = getFileFromContext(currentContext); - final var currentCluster = mergedClusters.get(currentContext.getContext().getCluster()).getCluster(); + final var configFile = getFileWithNamedCluster(currentNamedCluster); + currentContext.setAdditionalProperty(KUBERNETES_CONFIG_CLUSTER_FILE_KEY, configFile); + final var currentCluster = currentNamedCluster.getCluster(); clientConfig.setMasterUrl(currentCluster.getServer()); clientConfig.setTrustCerts(Objects.equals(currentCluster.getInsecureSkipTlsVerify(), true)); clientConfig.setDisableHostnameVerification(Objects.equals(currentCluster.getInsecureSkipTlsVerify(), true)); @@ -169,10 +209,11 @@ public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Strin } } final var mergedUsers = mergeUsers(kubeconfigs); - if (mergedUsers.containsKey(currentContext.getContext().getUser())) { - final var currentNamedAuthInfo = mergedUsers.get(currentContext.getContext().getUser()); + final var currentNamedAuthInfo = mergedUsers.get(currentContext.getContext().getUser()); + if (currentNamedAuthInfo != null) { // If config was loaded using KubeConfigUtils#parseConfig, then the file is available in the additional properties - final File configFile = getFileFromAuthInfo(currentNamedAuthInfo); + final var configFile = getFileWithNamedAuthInfo(currentNamedAuthInfo); + currentContext.setAdditionalProperty(KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY, configFile); final var currentAuthInfo = currentNamedAuthInfo.getUser(); String clientCertFile = currentAuthInfo.getClientCertificate(); String clientKeyFile = currentAuthInfo.getClientKey(); @@ -203,8 +244,6 @@ private static Map mergeContexts(io.fabric8.kubernetes.cli if (kubeconfigs[i].getContexts() != null) { for (NamedContext ctx : kubeconfigs[i].getContexts()) { if (ctx.getContext() != null) { - // Contains KUBERNETES_CONFIG_FILE_KEY if config was parsed using KubeConfigUtils#parseConfig - ctx.getAdditionalProperties().putAll(kubeconfigs[i].getAdditionalProperties()); mergedContexts.put(ctx.getName(), ctx); } } @@ -240,8 +279,6 @@ private static Map mergeUsers(Config... kubeconfigs) { if (kubeconfigs[i].getUsers() != null) { for (NamedAuthInfo user : kubeconfigs[i].getUsers()) { if (user.getUser() != null) { - // Contains KUBERNETES_CONFIG_FILE_KEY if config was parsed using KubeConfigUtils#parseConfig - user.getAdditionalProperties().putAll(kubeconfigs[i].getAdditionalProperties()); mergedUsers.put(user.getName(), user); } } @@ -387,4 +424,13 @@ private static String absolutify(File relativeTo, String filename) { } return new File(relativeTo.getParentFile(), filename).getAbsolutePath(); } + + private static void removeAdditionalProperties(Supplier> provider) { + if (provider == null) { + return; + } + provider.get().remove(KUBERNETES_CONFIG_CONTEXT_FILE_KEY); + provider.get().remove(KUBERNETES_CONFIG_CLUSTER_FILE_KEY); + provider.get().remove(KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY); + } } 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 e45f01397d9..10b326fb5fc 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 @@ -195,9 +195,10 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut .ifPresent(c -> c.putAll(authProviderConfig)); } // Persist in file - if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) { + if (currentConfig.getFileWithAuthInfo() != null && currentConfig.getCurrentContext() != null) { try { - final io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils.parseConfig(currentConfig.getFile()); + final io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils + .parseConfig(currentConfig.getFileWithAuthInfo()); final String userName = currentConfig.getCurrentContext().getContext().getUser(); NamedAuthInfo namedAuthInfo = kubeConfig.getUsers().stream().filter(n -> n.getName().equals(userName)).findFirst() .orElseGet(() -> { @@ -215,7 +216,7 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut if (Utils.isNotNullOrEmpty(token)) { namedAuthInfo.getUser().setToken(token); } - KubeConfigUtils.persistKubeConfigIntoFile(kubeConfig, currentConfig.getFile()); + KubeConfigUtils.persistKubeConfigIntoFile(kubeConfig, currentConfig.getFileWithAuthInfo()); } catch (Exception ex) { LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG", ex); } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigAutoConfigureTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigAutoConfigureTest.java index 4b42177ca08..bed02af2374 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigAutoConfigureTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigAutoConfigureTest.java @@ -46,7 +46,9 @@ void withNoKubeConfigFiles() { final var result = new ConfigBuilder().withAutoConfigure().build(); assertThat(result) .hasFieldOrPropertyWithValue("autoConfigure", true) - .returns(null, Config::getFile); + .returns(null, Config::getFile) + .returns(null, Config::getFileWithCluster) + .returns(null, Config::getFileWithAuthInfo); } @Test @@ -55,7 +57,9 @@ void withNonExistentConfigFile() { final var result = new ConfigBuilder().withAutoConfigure().build(); assertThat(result) .hasFieldOrPropertyWithValue("autoConfigure", true) - .returns(null, Config::getFile); + .returns(null, Config::getFile) + .returns(null, Config::getFileWithCluster) + .returns(null, Config::getFileWithAuthInfo); } @Test @@ -65,7 +69,9 @@ void withEmptyConfigFile() throws IOException { final var result = new ConfigBuilder().withAutoConfigure().build(); assertThat(result) .hasFieldOrPropertyWithValue("autoConfigure", true) - .returns(null, Config::getFile); + .returns(null, Config::getFile) + .returns(null, Config::getFileWithCluster) + .returns(null, Config::getFileWithAuthInfo); } @Test @@ -75,6 +81,8 @@ void withSingleConfigFile() { assertThat(result) .hasFieldOrPropertyWithValue("autoConfigure", true) .returns(resolveFile("/config-auto-configure/config-2.yaml"), Config::getFile) + .returns(resolveFile("/config-auto-configure/config-2.yaml"), Config::getFileWithCluster) + .returns(resolveFile("/config-auto-configure/config-2.yaml"), Config::getFileWithAuthInfo) .hasFieldOrPropertyWithValue("masterUrl", "https://config-2.example.com/") .hasFieldOrPropertyWithValue("currentContext.name", "context-in-all-configs"); @@ -90,6 +98,8 @@ void withMultipleConfigFiles() { assertThat(result) .hasFieldOrPropertyWithValue("autoConfigure", true) .returns(resolveFile("/config-auto-configure/config-1.yaml"), Config::getFile) + .returns(resolveFile("/config-auto-configure/config-1.yaml"), Config::getFileWithCluster) + .returns(resolveFile("/config-auto-configure/config-1.yaml"), Config::getFileWithAuthInfo) .hasFieldOrPropertyWithValue("masterUrl", "https://config-1.example.com/") .hasFieldOrPropertyWithValue("currentContext.name", "context-in-all-configs"); @@ -106,12 +116,29 @@ void withMultipleConfigFilesAndContext() { assertThat(result) .hasFieldOrPropertyWithValue("autoConfigure", true) .returns(resolveFile("/config-auto-configure/config-3.yaml"), Config::getFile) + .returns(resolveFile("/config-auto-configure/config-3.yaml"), Config::getFileWithCluster) + .returns(resolveFile("/config-auto-configure/config-3.yaml"), Config::getFileWithAuthInfo) .hasFieldOrPropertyWithValue("masterUrl", "https://config-3-special-cluster.example.com/") .hasFieldOrPropertyWithValue("currentContext.name", "context-in-config-3"); - } - // TODO: What if the user info is in a different file + @Test + void withMultipleConfigFilesAndScattered() { + System.setProperty("kubeconfig", + resolveFile("/config-auto-configure/scattered.yaml").getAbsolutePath() + File.pathSeparator + + resolveFile("/config-auto-configure/scattered-context.yaml").getAbsolutePath() + File.pathSeparator + + resolveFile("/config-auto-configure/scattered-cluster.yaml").getAbsolutePath() + File.pathSeparator + + resolveFile("/config-auto-configure/scattered-user.yaml").getAbsolutePath() + File.pathSeparator); + final var result = new ConfigBuilder().withAutoConfigure().build(); + assertThat(result) + .hasFieldOrPropertyWithValue("autoConfigure", true) + .returns(resolveFile("/config-auto-configure/scattered-context.yaml"), Config::getFile) + .returns(resolveFile("/config-auto-configure/scattered-cluster.yaml"), Config::getFileWithCluster) + .returns(resolveFile("/config-auto-configure/scattered-user.yaml"), Config::getFileWithAuthInfo) + .hasFieldOrPropertyWithValue("masterUrl", "https://scattered-cluster.example.com/") + .hasFieldOrPropertyWithValue("currentContext.name", "scattered-context"); + + } } private static File resolveFile(String path) { 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 3e0c72d9f48..c5aa0f5eef4 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 @@ -985,6 +985,11 @@ void setUp() { System.setProperty("os.name", "Windows"); } + @AfterEach + void tearDown() { + System.setProperty("os.name", osNamePropToRestore); + } + @Test void shouldUseHomeDriveHomePathOnWindows_WhenHomeEnvVariableIsNotSet() { // Given @@ -1021,10 +1026,6 @@ void shouldUseHomeEnvVariableOnWindows_WhenHomeEnvVariableIsSet() { .isEqualTo("C:\\Users\\user\\workspace\\myworkspace\\tools\\cygwin\\"); } - @AfterEach - void tearDown() { - System.setProperty("os.name", osNamePropToRestore); - } } @Test diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsMergeTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsMergeTest.java index 8b6374aeaec..fb805b90e2e 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsMergeTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsMergeTest.java @@ -91,11 +91,27 @@ void contextsContainsAllValidConfigContexts() { } @Test - void contextsContainInformationFromFile() { + void contextsContainInformationFromUsedFile() { // Parser adds additional properties to context to be able to retrieve the file where it was loaded from assertThat(result.getContexts()) .allMatch(ctx -> ctx.getAdditionalProperties() != null) - .allMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_FILE_KEY") != null); + .allMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_CONTEXT_FILE_KEY") != null) + // This information is only available for the applicable/current context + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_CLUSTER_FILE_KEY") == null) + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY") != null) + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_CLUSTER_FILE_KEY") == null) + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY") != null); + } + + @Test + void currentContextContainsInformationFromUsedFiles() { + assertThat(result.getCurrentContext()) + .extracting("additionalProperties") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class)) + .containsKeys( + "KUBERNETES_CONFIG_CONTEXT_FILE_KEY", + "KUBERNETES_CONFIG_CLUSTER_FILE_KEY", + "KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY"); } @Test @@ -150,7 +166,23 @@ void contextsContainInformationFromFile() { // Parser adds additional properties to context to be able to retrieve the file where it was loaded from assertThat(result.getContexts()) .allMatch(ctx -> ctx.getAdditionalProperties() != null) - .allMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_FILE_KEY") != null); + .allMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_CONTEXT_FILE_KEY") != null) + // This information is only available for the applicable/current context + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_CLUSTER_FILE_KEY") == null) + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY") != null) + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_CLUSTER_FILE_KEY") == null) + .anyMatch(ctx -> ctx.getAdditionalProperties().get("KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY") != null); + } + + @Test + void currentContextContainsInformationFromUsedFiles() { + assertThat(result.getCurrentContext()) + .extracting("additionalProperties") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, Object.class)) + .containsKeys( + "KUBERNETES_CONFIG_CONTEXT_FILE_KEY", + "KUBERNETES_CONFIG_CLUSTER_FILE_KEY", + "KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY"); } @Test diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsTest.java index 9a3703ae505..0bcf7e6df1a 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsTest.java @@ -64,12 +64,33 @@ void throwsExceptionIfInvalidFile() throws IOException { } @Test - void addsAdditionalPropertyWithFileLocation() { + void addsNamedContextAdditionalPropertyWithFileLocation() { final var file = new File(Objects .requireNonNull(KubeConfigUtilsTest.class.getResource("/internal/kube-config-utils-parse/config-1.yaml")).getPath()); final var config = KubeConfigUtils.parseConfig(file); - assertThat(config) - .hasFieldOrPropertyWithValue("additionalProperties.KUBERNETES_CONFIG_FILE_KEY", file); + assertThat(config.getContexts()) + .singleElement() + .hasFieldOrPropertyWithValue("additionalProperties.KUBERNETES_CONFIG_CONTEXT_FILE_KEY", file); + } + + @Test + void addsNamedClusterAdditionalPropertyWithFileLocation() { + final var file = new File(Objects + .requireNonNull(KubeConfigUtilsTest.class.getResource("/internal/kube-config-utils-parse/config-1.yaml")).getPath()); + final var config = KubeConfigUtils.parseConfig(file); + assertThat(config.getClusters()) + .singleElement() + .hasFieldOrPropertyWithValue("additionalProperties.KUBERNETES_CONFIG_CLUSTER_FILE_KEY", file); + } + + @Test + void addsNamedUserAdditionalPropertyWithFileLocation() { + final var file = new File(Objects + .requireNonNull(KubeConfigUtilsTest.class.getResource("/internal/kube-config-utils-parse/config-1.yaml")).getPath()); + final var config = KubeConfigUtils.parseConfig(file); + assertThat(config.getUsers()) + .singleElement() + .hasFieldOrPropertyWithValue("additionalProperties.KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY", file); } @Test @@ -111,61 +132,66 @@ void doesntPersistAdditionalProperties() throws IOException { final var file = new File(Objects .requireNonNull(KubeConfigUtilsTest.class.getResource("/internal/kube-config-utils-parse/config-1.yaml")).getPath()); final var config = KubeConfigUtils.parseConfig(file); - config.getAdditionalProperties().put("KUBERNETES_CONFIG_FILE_KEY", file); + // Should already be set by the parser, but just to test reassurance config.getContexts().iterator().next().getAdditionalProperties() - .put("KUBERNETES_CONFIG_FILE_KEY", file); + .put("KUBERNETES_CONFIG_CONTEXT_FILE_KEY", file); + config.getClusters().iterator().next().getAdditionalProperties() + .put("KUBERNETES_CONFIG_CLUSTER_FILE_KEY", file); config.getUsers().iterator().next().getAdditionalProperties() - .put("KUBERNETES_CONFIG_FILE_KEY", file); + .put("KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY", file); // When KubeConfigUtils.persistKubeConfigIntoFile(config, file); // Then assertThat(file) .content() - .doesNotContain("KUBERNETES_CONFIG_FILE_KEY"); + .doesNotContain("KUBERNETES_CONFIG_CONTEXT_FILE_KEY") + .doesNotContain("KUBERNETES_CONFIG_CLUSTER_FILE_KEY") + .doesNotContain("KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY") + .doesNotContain("KUBERNETES_CONFIG"); } } @Nested - @DisplayName("getFileFromContext") - class GetFileFromContext { + @DisplayName("getFileWithNamedContextInfo") + class GetFileWithNamedContextInfo { @Test void withNullNamedContext() { - assertThat(KubeConfigUtils.getFileFromContext(null)).isNull(); + assertThat(KubeConfigUtils.getFileWithNamedContext(null)).isNull(); } @Test void withNullAdditionalProperties() { final var context = new NamedContext(); context.setAdditionalProperties(null); - assertThat(KubeConfigUtils.getFileFromContext(context)).isNull(); + assertThat(KubeConfigUtils.getFileWithNamedContext(context)).isNull(); } @Test void withEmptyAdditionalProperties() { final var context = new NamedContext(); - assertThat(KubeConfigUtils.getFileFromContext(context)).isNull(); + assertThat(KubeConfigUtils.getFileWithNamedContext(context)).isNull(); } @Test void withNullValue() { final var context = new NamedContext(); - context.setAdditionalProperty("KUBERNETES_CONFIG_FILE_KEY", null); - assertThat(KubeConfigUtils.getFileFromContext(context)).isNull(); + context.setAdditionalProperty("KUBERNETES_CONFIG_CONTEXT_FILE_KEY", null); + assertThat(KubeConfigUtils.getFileWithNamedContext(context)).isNull(); } @Test void withInvalidValue() { final var context = new NamedContext(); - context.setAdditionalProperty("KUBERNETES_CONFIG_FILE_KEY", "not-file"); - assertThat(KubeConfigUtils.getFileFromContext(context)).isNull(); + context.setAdditionalProperty("KUBERNETES_CONFIG_CONTEXT_FILE_KEY", "not-file"); + assertThat(KubeConfigUtils.getFileWithNamedContext(context)).isNull(); } @Test void withValidValue() { final var context = new NamedContext(); - context.setAdditionalProperty("KUBERNETES_CONFIG_FILE_KEY", new File(".")); - assertThat(KubeConfigUtils.getFileFromContext(context)).isEqualTo(new File(".")); + context.setAdditionalProperty("KUBERNETES_CONFIG_CONTEXT_FILE_KEY", new File(".")); + assertThat(KubeConfigUtils.getFileWithNamedContext(context)).isEqualTo(new File(".")); } } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsBehaviorTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsBehaviorTest.java index 996f4e9071c..e6630dde04b 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsBehaviorTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsBehaviorTest.java @@ -52,6 +52,7 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Base64; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -547,5 +548,55 @@ void persistsOAuthTokenInFile() { } } + @Nested + @DisplayName("With valid kube config") + class WithScatteredKubeConfig { + + private File userConfig; + + @BeforeEach + void setUp() throws IOException { + kubeConfig = tempDir.resolve("main-config").toFile(); + userConfig = tempDir.resolve("user-config").toFile(); + Files.write(kubeConfig.toPath(), ("---\n" + + "clusters:\n" + + "- name: cluster\n" + + " cluster:\n" + + " server: https://cluster.example.com\n" + + "contexts:\n" + + "- name: context\n" + + " context:\n" + + " cluster: cluster\n" + + " user: user\n" + + "current-context: context\n").getBytes(StandardCharsets.UTF_8)); + Files.write(userConfig.toPath(), ("---\n" + + "users:\n" + + "- name: user\n" + + " user:\n" + + " token: original-token\n").getBytes(StandardCharsets.UTF_8)); + System.setProperty("kubeconfig", kubeConfig.getAbsolutePath() + File.pathSeparator + userConfig.getAbsolutePath()); + originalConfig = new ConfigBuilder().withAutoConfigure().build(); + persistOAuthToken(originalConfig, oAuthTokenResponse, "updated-token"); + } + + @AfterEach + void tearDown() { + System.clearProperty("kubeconfig"); + } + + @Test + void persistsTokenInUserFile() { + assertThat(KubeConfigUtils.parseConfig(userConfig)) + .returns("updated-token", c -> c.getUsers().iterator().next().getUser().getToken()); + } + + @Test + void leavesOtherConfigUntouched() { + assertThat(KubeConfigUtils.parseConfig(kubeConfig)) + .returns(Collections.emptyList(), io.fabric8.kubernetes.api.model.Config::getUsers); + } + + } + } } diff --git a/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-cluster.yaml b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-cluster.yaml new file mode 100644 index 00000000000..5556b0f8e71 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-cluster.yaml @@ -0,0 +1,20 @@ +# +# 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. +# + +clusters: + - cluster: + server: https://scattered-cluster.example.com + name: scattered-cluster diff --git a/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-context.yaml b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-context.yaml new file mode 100644 index 00000000000..17963f40b93 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-context.yaml @@ -0,0 +1,21 @@ +# +# 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. +# + +contexts: + - context: + cluster: scattered-cluster + user: scattered-user + name: scattered-context diff --git a/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-user.yaml b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-user.yaml new file mode 100644 index 00000000000..6d81260a209 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered-user.yaml @@ -0,0 +1,20 @@ +# +# 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. +# + +users: + - name: scattered-user + user: + token: scattered-user-token diff --git a/kubernetes-client-api/src/test/resources/config-auto-configure/scattered.yaml b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered.yaml new file mode 100644 index 00000000000..09beb0020df --- /dev/null +++ b/kubernetes-client-api/src/test/resources/config-auto-configure/scattered.yaml @@ -0,0 +1,17 @@ +# +# 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. +# + +current-context: scattered-context