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 d5cc658efb1..4abd800c4f1 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 @@ -281,7 +281,7 @@ private static Config autoConfigure(Config config, String context) { final var kubeConfigFile = findKubeConfigFile(); if (kubeConfigFile != null) { config.file = kubeConfigFile; - KubeConfigUtils.merge(config, KubeConfigUtils.parseConfig(kubeConfigFile), context); + KubeConfigUtils.merge(config, context, KubeConfigUtils.parseConfig(kubeConfigFile)); } else { tryServiceAccount(config); tryNamespaceFromPath(config); @@ -791,9 +791,9 @@ public static Config fromKubeconfig(String context, File kubeconfigFile) { return ret; } - private static Config fromKubeconfig(String context, io.fabric8.kubernetes.api.model.Config kubeconfig) { + private static Config fromKubeconfig(String context, io.fabric8.kubernetes.api.model.Config... kubeconfigs) { final Config ret = Config.empty(); - KubeConfigUtils.merge(ret, kubeconfig, context); + KubeConfigUtils.merge(ret, context, kubeconfigs); return ret; } @@ -811,7 +811,7 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S if (Utils.isNullOrEmpty(kubeconfigContents)) { throw new KubernetesClientException("Could not create Config from kubeconfig"); } - KubeConfigUtils.merge(config, KubeConfigUtils.parseConfigFromString(kubeconfigContents), context); + KubeConfigUtils.merge(config, context, KubeConfigUtils.parseConfigFromString(kubeconfigContents)); if (!disableAutoConfig()) { postAutoConfigure(config); } 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 e380f95738c..4442ee83a27 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 @@ -16,9 +16,7 @@ package io.fabric8.kubernetes.client.internal; 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.Context; import io.fabric8.kubernetes.api.model.ExecConfig; import io.fabric8.kubernetes.api.model.ExecEnvVar; import io.fabric8.kubernetes.api.model.NamedAuthInfo; @@ -37,6 +35,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -78,126 +77,45 @@ public static Config parseConfigFromString(String contents) { } /** - * Returns the current context in the given config + * Persist KUBECONFIG file from the provided {@link io.fabric8.kubernetes.api.model.Config} object. * - * @param config Config object - * @return returns context in config if found, otherwise null + * @param kubeconfig modified {@link io.fabric8.kubernetes.api.model.Config} object. + * @param kubeConfigPath path to KUBECONFIG. + * @throws IOException in case of failure while writing to file. */ - public static NamedContext getCurrentContext(Config config) { - final String currentContext = config.getCurrentContext(); - if (currentContext != null && config.getContexts() != null) { - for (NamedContext context : config.getContexts()) { - if (Objects.equals(currentContext, context.getName())) { - return context; - } - } - } - return null; - } - - public static NamedContext findContext(List contexts, String context) { - if (contexts != null && Utils.isNotNullOrEmpty(context)) { - for (var ctx : contexts) { - if (Objects.equals(ctx.getName(), context)) { - return ctx; - } - } - } - return null; - } - - /** - * Returns the current user token for the config and current context - * - * @param config Config object - * @param context Context object - * @return returns current user based upon provided parameters. - */ - public static String getUserToken(Config config, Context context) { - AuthInfo authInfo = getUserAuthInfo(config, context); - if (authInfo != null) { - return authInfo.getToken(); - } - return null; - } - - /** - * Returns the current {@link AuthInfo} for the current context and user - * - * @param config Config object - * @param context Context object - * @return {@link AuthInfo} for current context - */ - public static AuthInfo getUserAuthInfo(Config config, Context context) { - if (config != null && config.getUsers() != null && context != null && context.getUser() != null) { - return config.getUsers().stream() - .filter(u -> Objects.equals(u.getName(), context.getUser())) - .findAny() - .map(NamedAuthInfo::getUser) - .orElse(null); + public static void persistKubeConfigIntoFile(Config kubeconfig, File kubeConfigPath) throws IOException { + if (kubeconfig.getAdditionalProperties() != null) { + kubeconfig.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY); } - return null; - } - - /** - * Returns the current {@link Cluster} for the current context - * - * @param config {@link Config} config object - * @param context {@link Context} context object - * @return current {@link Cluster} for current context - */ - public static Cluster getCluster(Config config, Context context) { - if (config != null && config.getClusters() != null && context != null && context.getCluster() != null) { - return config.getClusters().stream() - .filter(c -> Objects.equals(c.getName(), context.getCluster())) - .findAny() - .map(NamedCluster::getCluster) - .orElse(null); + if (kubeconfig.getContexts() != null) { + kubeconfig.getContexts().stream() + .filter(ctx -> ctx.getAdditionalProperties() != null) + .forEach(ctx -> ctx.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY)); } - return null; + Files.writeString(kubeConfigPath.toPath(), Serialization.asYaml(kubeconfig)); } /** - * Get User index from Config object - * - * @param config {@link io.fabric8.kubernetes.api.model.Config} Kube Config - * @param userName username inside Config - * @return index of user in users array + * Merges the provided {@link Config} objects into the provided {@link io.fabric8.kubernetes.client.Config} object. + *

+ * The following precedence is followed: + *

    + *
  1. Incomplete Contexts, Clusters, and Users are ignored
  2. + *
  3. Context argument provided by the user is used if provided and exists
  4. + *
  5. The first Config object to set a value wins
  6. + *
*/ - public static int getNamedUserIndexFromConfig(Config config, String userName) { - for (int i = 0; i < config.getUsers().size(); i++) { - if (config.getUsers().get(i).getName().equals(userName)) { - return i; + public static void merge(io.fabric8.kubernetes.client.Config clientConfig, String context, Config... kubeconfigs) { + final var mergedContexts = mergeContexts(clientConfig, kubeconfigs); + clientConfig.setContexts(new ArrayList<>(mergedContexts.values())); + // Try to load the context requested by the user, otherwise fallback to the one selected in the first .kube/config + NamedContext currentContext = null; + for (String contextName : contextPreference(context, kubeconfigs)) { + if (mergedContexts.containsKey(contextName)) { + currentContext = mergedContexts.get(contextName); + break; } } - return -1; - } - - /** - * Modify KUBECONFIG file - * - * @param kubeConfig modified {@link io.fabric8.kubernetes.api.model.Config} object - * @param kubeConfigPath path to KUBECONFIG - * @throws IOException in case of failure while writing to file - */ - public static void persistKubeConfigIntoFile(Config kubeConfig, File kubeConfigPath) throws IOException { - Files.writeString(kubeConfigPath.toPath(), Serialization.asYaml(kubeConfig)); - } - - public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Config kubeConfig, String context) { - if (clientConfig.getContexts() == null) { - clientConfig.setContexts(new ArrayList<>()); - } - if (kubeConfig.getContexts() != null) { - clientConfig.getContexts().addAll(kubeConfig.getContexts()); - } - // Try to load the context requested by the user, otherwise fallback to the one selected in the .kube/config - final NamedContext currentContext; - if (findContext(clientConfig.getContexts(), context) != null) { - currentContext = findContext(clientConfig.getContexts(), context); - } else { - currentContext = findContext(kubeConfig.getContexts(), kubeConfig.getCurrentContext()); - } if (currentContext == null || currentContext.getContext() == null) { return; } @@ -205,13 +123,14 @@ public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Confi clientConfig.setNamespace(currentContext.getContext().getNamespace()); // If config was loaded using KubeConfigUtils#parseConfig, then the file is available in the additional properties final File configFile; - if (kubeConfig.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) instanceof File) { - configFile = (File) kubeConfig.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY); + if (currentContext.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) instanceof File) { + configFile = (File) currentContext.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY); } else { configFile = null; } - final var currentCluster = KubeConfigUtils.getCluster(kubeConfig, currentContext.getContext()); - if (currentCluster != null) { + final var mergedClusters = mergeClusters(kubeconfigs); + if (mergedClusters.containsKey(currentContext.getContext().getCluster())) { + final var currentCluster = mergedClusters.get(currentContext.getContext().getCluster()).getCluster(); clientConfig.setMasterUrl(currentCluster.getServer()); clientConfig.setTrustCerts(Objects.equals(currentCluster.getInsecureSkipTlsVerify(), true)); clientConfig.setDisableHostnameVerification(Objects.equals(currentCluster.getInsecureSkipTlsVerify(), true)); @@ -232,8 +151,9 @@ public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Confi } } } - final var currentAuthInfo = KubeConfigUtils.getUserAuthInfo(kubeConfig, currentContext.getContext()); - if (currentAuthInfo != null) { + final var mergedUsers = mergeUsers(kubeconfigs); + if (mergedUsers.containsKey(currentContext.getContext().getUser())) { + final var currentAuthInfo = mergedUsers.get(currentContext.getContext().getUser()).getUser(); String clientCertFile = currentAuthInfo.getClientCertificate(); String clientKeyFile = currentAuthInfo.getClientKey(); if (configFile != null) { @@ -256,6 +176,71 @@ public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Confi } } + private static Map mergeContexts(io.fabric8.kubernetes.client.Config config, Config... kubeconfigs) { + final Map mergedContexts = new HashMap<>(); + // process kubeconfigs in inverse order, so that the first kubeconfig has precedence + for (int i = kubeconfigs.length - 1; i >= 0; i--) { + 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); + } + } + } + } + if (config.getContexts() != null) { + for (NamedContext ctx : config.getContexts()) { + mergedContexts.put(ctx.getName(), ctx); + } + } + return mergedContexts; + } + + private static Map mergeClusters(Config... kubeconfigs) { + final Map mergedClusters = new HashMap<>(); + // process kubeconfigs in inverse order, so that the first kubeconfig has precedence + for (int i = kubeconfigs.length - 1; i >= 0; i--) { + if (kubeconfigs[i].getClusters() != null) { + for (NamedCluster cluster : kubeconfigs[i].getClusters()) { + if (cluster.getCluster() != null) { + mergedClusters.put(cluster.getName(), cluster); + } + } + } + } + return mergedClusters; + } + + private static Map mergeUsers(Config... kubeconfigs) { + final Map mergedUsers = new HashMap<>(); + // process kubeconfigs in inverse order, so that the first kubeconfig has precedence + for (int i = kubeconfigs.length - 1; i >= 0; i--) { + if (kubeconfigs[i].getUsers() != null) { + for (NamedAuthInfo user : kubeconfigs[i].getUsers()) { + if (user.getUser() != null) { + mergedUsers.put(user.getName(), user); + } + } + } + } + return mergedUsers; + } + + private static List contextPreference(String context, Config... kubeconfigs) { + final List contextPreference = new ArrayList<>(); + if (Utils.isNotNullOrEmpty(context)) { + contextPreference.add(context); + } + for (Config kubeconfig : kubeconfigs) { + if (Utils.isNotNullOrEmpty(kubeconfig.getCurrentContext())) { + contextPreference.add(kubeconfig.getCurrentContext()); + } + } + return contextPreference; + } + private static void mergeKubeConfigAuthProviderConfig(io.fabric8.kubernetes.client.Config config, AuthInfo currentAuthInfo) { if (currentAuthInfo.getAuthProvider().getConfig() != null) { config.setAuthProvider(currentAuthInfo.getAuthProvider()); 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 new file mode 100644 index 00000000000..1b23f29e478 --- /dev/null +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubeConfigUtilsMergeTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes.client.internal; + +import io.fabric8.kubernetes.api.model.NamedContext; +import io.fabric8.kubernetes.api.model.NamedContextBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collections; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("KubeConfigUtils.merge") +class KubeConfigUtilsMergeTest { + + private Config result; + + @Test + void noContextProvidedLeavesConfigUnchanged() { + result = Config.empty(); + KubeConfigUtils.merge(result, null, + parseConfig("/internal/kube-config-utils-merge/config-empty.yaml")); + assertThat(result) + .hasFieldOrPropertyWithValue("currentContext", null) + .hasFieldOrPropertyWithValue("contexts", Collections.emptyList()); + } + + @Test + void incompleteContextProvidedLeavesConfigUnchanged() { + result = new ConfigBuilder(Config.empty()) + .addToContexts(new NamedContextBuilder() + .withName("incomplete-context") + .build()) + .build(); + KubeConfigUtils.merge(result, "incomplete-context", + parseConfig("/internal/kube-config-utils-merge/config-1.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-2.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-3.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-4.yaml")); + assertThat(result) + .hasFieldOrPropertyWithValue("currentContext", null) + .extracting("contexts") + .asInstanceOf(InstanceOfAssertFactories.list(NamedContext.class)) + // Contains the contexts from the config files too + .filteredOn(ctx -> "incomplete-context".equals(ctx.getName())) + .singleElement() + .hasFieldOrPropertyWithValue("name", "incomplete-context") + .hasFieldOrPropertyWithValue("context", null); + } + + @Nested + @DisplayName("When merging multiple Configs with null context, use context from first Config") + class NullContextArgument { + + @BeforeEach + void setUp() { + result = Config.empty(); + KubeConfigUtils.merge(result, null, + parseConfig("/internal/kube-config-utils-merge/config-1.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-2.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-3.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-4.yaml")); + } + + @Test + void contextsContainsAllValidConfigContexts() { + // Contexts that don't contain a valid nested Context object are ignored + assertThat(result.getContexts()) + .allMatch(ctx -> ctx.getContext() != null); + } + + @Test + 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); + } + + @Test + void currentContextLoadedFromFirstConfig() { + assertThat(result.getCurrentContext()) + .hasFieldOrPropertyWithValue("name", "context-in-all-configs") + .hasFieldOrPropertyWithValue("context.cluster", "config-1-cluster") + .hasFieldOrPropertyWithValue("context.namespace", "config-1-namespace") + .hasFieldOrPropertyWithValue("context.user", "config-1-user"); + } + + @Test + void clusterInfoFromFirstConfig() { + assertThat(result) + .hasFieldOrPropertyWithValue("masterUrl", "https://config-1.example.com/") + .hasFieldOrPropertyWithValue("httpsProxy", "socks5://proxy.config-1.example.com"); + } + + @Test + void userInfoFromFirstConfig() { + assertThat(result) + .hasFieldOrPropertyWithValue("username", "config-1-user-username") + .hasFieldOrPropertyWithValue("password", "config-1-user-pa33word") + .hasFieldOrPropertyWithValue("autoOAuthToken", "config-1-user-token"); + } + } + + @Nested + @DisplayName("When merging multiple Configs with context argument, use context from Config that matches context argument") + class MatchingContextArgument { + + @BeforeEach + void setUp() { + result = Config.empty(); + KubeConfigUtils.merge(result, "context-in-config-3", + parseConfig("/internal/kube-config-utils-merge/config-1.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-2.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-3.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-4.yaml")); + } + + @Test + void currentContextLoadedFromConfig3() { + assertThat(result.getCurrentContext()) + .hasFieldOrPropertyWithValue("name", "context-in-config-3") + .hasFieldOrPropertyWithValue("context.cluster", "config-3-special-cluster") + .hasFieldOrPropertyWithValue("context.user", "config-3-special-user"); + } + + @Test + void clusterInfoFromConfig3() { + assertThat(result) + .hasFieldOrPropertyWithValue("masterUrl", "https://config-3-special-cluster.example.com/") + .hasFieldOrPropertyWithValue("httpsProxy", "socks5://proxy.config-3-special-cluster.example.com"); + } + + @Test + void userInfoFromConfig3() { + assertThat(result) + .hasFieldOrPropertyWithValue("username", null) + .hasFieldOrPropertyWithValue("password", null) + .hasFieldOrPropertyWithValue("autoOAuthToken", "config-3-special-user-token"); + } + } + + @Nested + @DisplayName("When merging multiple Configs with context argument, use context from FIRST Config that matches context argument") + class MatchingDuplicateContextArgument { + + @BeforeEach + void setUp() { + result = Config.empty(); + KubeConfigUtils.merge(result, "duplicate-context", + parseConfig("/internal/kube-config-utils-merge/config-1.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-2.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-3.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-4.yaml")); + } + + @Test + void currentContextLoadedFromFirstMatchingConfig() { + assertThat(result.getCurrentContext()) + .hasFieldOrPropertyWithValue("name", "duplicate-context") + .hasFieldOrPropertyWithValue("context.cluster", "duplicate-cluster") + .hasFieldOrPropertyWithValue("context.user", "duplicate-user"); + } + + @Test + void clusterInfoFromFirstMatchingConfig() { + assertThat(result) + .hasFieldOrPropertyWithValue("masterUrl", "https://duplicate-cluster-in-1.example.com/") + .hasFieldOrPropertyWithValue("httpsProxy", null); + } + + @Test + void userInfoFromFirstMatchingConfig() { + assertThat(result) + .hasFieldOrPropertyWithValue("username", null) + .hasFieldOrPropertyWithValue("password", null) + .hasFieldOrPropertyWithValue("autoOAuthToken", "duplicate-user-1-token"); + } + } + + @Nested + @DisplayName("When merging multiple Configs with context argument, use context from ORIGINAL Config that matches context argument") + class ContextArgumentInOriginalConfig { + + @BeforeEach + void setUp() { + result = new ConfigBuilder() + .addToContexts(new NamedContextBuilder() + .withName("context-in-original-config") + .withNewContext() + .withCluster("original-cluster") + .withUser("original-user") + .endContext() + .build()) + .withMasterUrl("https://original-cluster.example.com/") + .withHttpsProxy("socks5://proxy.original-cluster.example.com") + .withUsername("original-username") + .withPassword("original-password") + .build(); + KubeConfigUtils.merge(result, "context-in-original-config", + parseConfig("/internal/kube-config-utils-merge/config-1.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-2.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-3.yaml"), + parseConfig("/internal/kube-config-utils-merge/config-4.yaml")); + } + + @Test + void currentContextPreservedFromOriginalConfig() { + assertThat(result.getCurrentContext()) + .hasFieldOrPropertyWithValue("name", "context-in-original-config") + .hasFieldOrPropertyWithValue("context.cluster", "original-cluster") + .hasFieldOrPropertyWithValue("context.user", "original-user"); + } + + @Test + void clusterInfoPreservedFromOriginalConfig() { + assertThat(result) + .hasFieldOrPropertyWithValue("masterUrl", "https://original-cluster.example.com/") + .hasFieldOrPropertyWithValue("httpsProxy", "socks5://proxy.original-cluster.example.com"); + } + + @Test + void userInfoPreservedFromOriginalConfig() { + assertThat(result) + .hasFieldOrPropertyWithValue("username", "original-username") + .hasFieldOrPropertyWithValue("password", "original-password") + .hasFieldOrPropertyWithValue("autoOAuthToken", null); + } + } + + private static io.fabric8.kubernetes.api.model.Config parseConfig(String path) { + final var file = new File(Objects.requireNonNull(KubeConfigUtilsMergeTest.class.getResource(path)).getFile()); + return KubeConfigUtils.parseConfig(file); + } +} 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 7ff5f9776f4..c4edf170518 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 @@ -15,67 +15,38 @@ */ package io.fabric8.kubernetes.client.internal; -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.ExecConfig; import io.fabric8.kubernetes.api.model.ExecConfigBuilder; -import io.fabric8.kubernetes.api.model.NamedContext; import io.fabric8.kubernetes.client.utils.Utils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; class KubeConfigUtilsTest { - @Test - void testGetNamedUserIndexFromConfig() { - // Given - Config config = getTestKubeConfig(); - - // When - int index = KubeConfigUtils.getNamedUserIndexFromConfig(config, "test/test-cluster:443"); - - // Then - assertEquals(2, index); - } - - @Test - void testGetCurrentContext() { - // Given - Config config = getTestKubeConfig(); - // When - NamedContext namedContext = KubeConfigUtils.getCurrentContext(config); - - // Then - assertNotNull(namedContext); - assertEquals("test-context", namedContext.getName()); - assertEquals("ns1", namedContext.getContext().getNamespace()); - assertEquals("system:admin/api-testing:6334", namedContext.getContext().getUser()); - assertEquals("api-testing:6334", namedContext.getContext().getCluster()); - } + @TempDir + private Path tempDir; @Test void testParseConfig() { // Given File configFile = new File(getClass().getResource("/test-kubeconfig").getPath()); - // When Config config = KubeConfigUtils.parseConfig(configFile); - // Then assertNotNull(config); assertEquals(1, config.getClusters().size()); @@ -83,54 +54,12 @@ void testParseConfig() { assertEquals(3, config.getUsers().size()); } - @Test - void testGetUserToken() { - // Given - Config config = getTestKubeConfig(); - Context context = Objects.requireNonNull(KubeConfigUtils.getCurrentContext(config)).getContext(); - - // When - String token = KubeConfigUtils.getUserToken(config, context); - - // Then - assertEquals("test-token-2", token); - } - - @Test - void testGetCluster() { - // Given - Config config = getTestKubeConfig(); - Context context = Objects.requireNonNull(KubeConfigUtils.getCurrentContext(config)).getContext(); - - // When - Cluster cluster = KubeConfigUtils.getCluster(config, context); - - // Then - assertNotNull(cluster); - } - - @Test - void testGetUserAuthInfo() { - // Given - Config config = getTestKubeConfig(); - Context context = config.getContexts().get(0).getContext(); - - // When - AuthInfo authInfo = KubeConfigUtils.getUserAuthInfo(config, context); - - // Then - assertNotNull(authInfo); - assertEquals("test-token-2", authInfo.getToken()); - } - @Test @DisplayName("should create expected authenticator command for aws") void getAuthenticatorCommandFromExecConfig_whenAwsCommandUsed_thenUseCommandLineArgsInExecCommand() throws IOException { // Given - File commandFolder = Files.createTempDirectory("test").toFile(); - File commandFile = new File(commandFolder, "aws"); - Files.createFile(commandFile.toPath()); - String systemPathValue = getTestPathValue(commandFolder); + Path commandFile = Files.createFile(tempDir.resolve("aws")); + String systemPathValue = getTestPathValue(tempDir.toFile()); ExecConfig execConfig = new ExecConfigBuilder() .withApiVersion("client.authentication.k8s.io/v1alpha1") .addToArgs("--region", "us-west2", "eks", "get-token", "--cluster-name", "api-eks.example.com") @@ -149,7 +78,7 @@ void getAuthenticatorCommandFromExecConfig_whenAwsCommandUsed_thenUseCommandLine assertPlatformPrefixes(processBuilderArgs); List commandParts = Arrays.asList(processBuilderArgs.get(2).split(" ")); assertThat(commandParts) - .containsExactly(commandFile.getAbsolutePath(), "--region", "us-west2", "eks", + .containsExactly(commandFile.toFile().getAbsolutePath(), "--region", "us-west2", "eks", "get-token", "--cluster-name", "api-eks.example.com"); } @@ -157,12 +86,11 @@ void getAuthenticatorCommandFromExecConfig_whenAwsCommandUsed_thenUseCommandLine @DisplayName("should generate expected authenticator command for gke-gcloud-auth-plugin") void getAuthenticatorCommandFromExecConfig_whenGkeAuthPluginCommandProvided_thenUseCommandLineArgs() throws IOException { // Given - File commandFolder = Files.createTempDirectory("test").toFile(); - File commandFile = new File(commandFolder, "gke-gcloud-auth-plugin"); - String systemPathValue = getTestPathValue(commandFolder); + Path commandFile = Files.createFile(tempDir.resolve("gke-gcloud-auth-plugin")); + String systemPathValue = getTestPathValue(tempDir.toFile()); ExecConfig execConfigNoArgs = new ExecConfigBuilder() .withApiVersion("client.authentication.k8s.io/v1alpha1") - .withCommand(commandFile.getPath()) + .withCommand(commandFile.toFile().getPath()) .build(); // Simulate "user.exec.args: null" like e.g. in the configuration for the gke-gcloud-auth-plugin. execConfigNoArgs.setArgs(null); @@ -175,7 +103,7 @@ void getAuthenticatorCommandFromExecConfig_whenGkeAuthPluginCommandProvided_then assertThat(processBuilderArgs) .isNotNull() .hasSize(3) - .satisfies(pb -> assertThat(pb.get(2)).isEqualTo(commandFile.getPath())); + .satisfies(pb -> assertThat(pb.get(2)).isEqualTo(commandFile.toFile().getPath())); assertPlatformPrefixes(processBuilderArgs); } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java index 43023fadca6..1c022cfa1fb 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtilsTest.java @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.api.model.AuthProviderConfig; import io.fabric8.kubernetes.api.model.AuthProviderConfigBuilder; +import io.fabric8.kubernetes.api.model.NamedAuthInfo; import io.fabric8.kubernetes.api.model.NamedContext; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; @@ -47,7 +48,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; @@ -85,11 +85,17 @@ void persistOAuthTokenWithUpdatedToken(@TempDir Path tempDir) throws IOException // Then io.fabric8.kubernetes.api.model.Config config = KubeConfigUtils.parseConfig(kubeConfig.toFile()); assertNotNull(config); - NamedContext currentNamedContext = KubeConfigUtils.getCurrentContext(config); + NamedContext currentNamedContext = config.getContexts().stream() + .filter(ctx -> ctx.getName().equals(config.getCurrentContext())) + .findFirst() + .orElse(null); assertNotNull(currentNamedContext); - int currentUserIndex = KubeConfigUtils.getNamedUserIndexFromConfig(config, currentNamedContext.getContext().getUser()); - assertTrue(currentUserIndex > 0); - Map authProviderConfigInFile = config.getUsers().get(currentUserIndex).getUser().getAuthProvider() + NamedAuthInfo currentUser = config.getUsers().stream() + .filter(user -> user.getName().equals(currentNamedContext.getContext().getUser())) + .findFirst() + .orElse(null); + assertNotNull(currentUser); + Map authProviderConfigInFile = currentUser.getUser().getAuthProvider() .getConfig(); assertFalse(authProviderConfigInFile.isEmpty()); Map authProviderConfigInMemory = originalConfig.getAuthProvider().getConfig(); diff --git a/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-1.yaml b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-1.yaml new file mode 100644 index 00000000000..3cdc3e3717d --- /dev/null +++ b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-1.yaml @@ -0,0 +1,55 @@ +# +# 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://config-1.example.com + proxy-url: socks5://proxy.config-1.example.com + name: config-1-cluster + - cluster: + server: https://config-1-2.example.com + proxy-url: socks5://proxy.config-1-2.example.com + name: config-1-cluster-2 + - cluster: + server: https://duplicate-cluster-in-1.example.com + name: duplicate-cluster +contexts: + - context: + cluster: config-1-cluster + namespace: config-1-namespace + user: config-1-user + name: context-in-all-configs + - context: + cluster: duplicate-cluster + user: duplicate-user + name: duplicate-context + - context: + cluster: config-1-cluster-2 + user: config-1-user-2 + name: config-1-context-2 +users: + - name: config-1-user + user: + token: config-1-user-token + username: config-1-user-username + password: config-1-user-pa33word # notsecret + - name: config-1-user-2 + user: + token: config-1-user-2-token + - name: duplicate-user + user: + token: duplicate-user-1-token +current-context: context-in-all-configs diff --git a/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-2.yaml b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-2.yaml new file mode 100644 index 00000000000..f93eca5204a --- /dev/null +++ b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-2.yaml @@ -0,0 +1,50 @@ +# +# 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://config-2.example.com + proxy-url: socks5://proxy.config-2.example.com + name: config-2-cluster + - cluster: + server: https://duplicate-cluster-in-2.example.com + name: duplicate-cluster + - name: incomplete-cluster +contexts: + - context: + cluster: config-2-cluster + namespace: config-2-namespace + user: config-2-user + name: context-in-all-configs + - context: + cluster: duplicate-cluster + user: duplicate-user + name: duplicate-context + - name: incomplete-context +users: + - name: config-2-user + user: + token: config-2-user-token + username: config-2-user-username + password: config-2-user-pa33word # notsecret + - name: config-2-user-2 + user: + token: config-2-user-2-token + - name: duplicate-user + user: + token: duplicate-user-2-token + - name: incomplete-user +current-context: context-in-all-configs diff --git a/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-3.yaml b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-3.yaml new file mode 100644 index 00000000000..71657c44572 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-3.yaml @@ -0,0 +1,48 @@ +# +# 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://config-3.example.com + proxy-url: socks5://proxy.config-3.example.com + name: config-3-cluster + - cluster: + server: https://config-3-special-cluster.example.com + proxy-url: socks5://proxy.config-3-special-cluster.example.com + name: config-3-special-cluster + - cluster: + name: config-3-cluster-without-cluster-info +contexts: + - context: + cluster: config-3-cluster + namespace: config-3-namespace + user: config-3-user + name: context-in-all-configs + - context: + cluster: config-3-special-cluster + namespace: config-3-namespace + user: config-3-special-user + name: context-in-config-3 +users: + - name: config-3-user + user: + token: config-3-user-token + username: config-3-user-username + password: config-3-user-pa33word # notsecret + - name: config-3-special-user + user: + token: config-3-special-user-token +current-context: context-in-all-configs diff --git a/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-4.yaml b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-4.yaml new file mode 100644 index 00000000000..0f4584f3507 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-4.yaml @@ -0,0 +1,26 @@ +# +# 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: missing-entries + namespace: missing-entries + user: missing-entries + name: context-in-all-configs + - context: + cluster: missing-entries + user: missing-entries + name: missing-entries diff --git a/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-empty.yaml b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-empty.yaml new file mode 100644 index 00000000000..1ad423c7253 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/internal/kube-config-utils-merge/config-empty.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. +# + +contexts: