From 8d0a22b8a6721a42267cc578096101b5b592b030 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 21 Aug 2024 19:18:33 +0200 Subject: [PATCH] use kubeconfigs listed in KUBECONFIG env var (#6240) * update token in file listed in KUBECONFIG env var (#6240) * only parse configs once (#6240) * update file with auth info when merging authinfos * parametrized KubeConfigUtilsTest for #hasAuthInfoNamed * expose Config#getFileWithCurrentContext & #getFileWithContext to consumers Signed-off-by: Andre Dietisheim --- CHANGELOG.md | 1 + .../io/fabric8/kubernetes/client/Config.java | 333 ++++++++++++------ .../kubernetes/client/ConfigBuilder.java | 4 +- .../kubernetes/client/ConfigFluent.java | 18 +- .../kubernetes/client/KubeConfigFile.java | 87 +++++ .../kubernetes/client/SundrioConfig.java | 5 +- .../client/internal/KubeConfigUtils.java | 120 +++++-- .../client/utils/OpenIDConnectionUtils.java | 80 +++-- .../kubernetes/client/utils/Utils.java | 23 +- .../client/ConfigConstructorTest.java | 24 +- .../fabric8/kubernetes/client/ConfigTest.java | 226 +++++++++++- .../kubernetes/client/KubeConfigFileTest.java | 113 ++++++ .../client/internal/KubeConfigUtilsTest.java | 59 +++- .../OpenIDConnectionUtilsBehaviorTest.java | 28 +- .../utils/OpenIDConnectionUtilsTest.java | 9 +- .../test/resources/test-ec-kubeconfig-mangled | 19 + .../src/test/resources/test-kubeconfig-empty | 7 + ...ctxt.yml => test-kubeconfig-nocurrentctxt} | 0 .../resources/test-kubeconfig-onlycurrentctx | 7 + 19 files changed, 982 insertions(+), 181 deletions(-) create mode 100644 kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java create mode 100644 kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/KubeConfigFileTest.java create mode 100644 kubernetes-client-api/src/test/resources/test-ec-kubeconfig-mangled create mode 100644 kubernetes-client-api/src/test/resources/test-kubeconfig-empty rename kubernetes-client-api/src/test/resources/{test-kubeconfig-nocurrentctxt.yml => test-kubeconfig-nocurrentctxt} (100%) create mode 100644 kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentctx diff --git a/CHANGELOG.md b/CHANGELOG.md index bff3fd27dfc..233f471fad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Fix #6281: use GitHub binary repo for Kube API Tests * Fix #6282: Allow annotated types with Pattern, Min, and Max with Lists and Maps and CRD generation * Fix #5480: Move `io.fabric8:zjsonpatch` to KubernetesClient project +* Fix #6240: Use kubeconfig files listed in the KUBECONFIG env var #### Dependency Upgrade * Fix #2632: Bumped OkHttp from 3.12.12 to 4.12.0 diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/Config.java index 0d4e09637ba..0b3cddf53f5 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 @@ -28,6 +28,7 @@ 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; import io.fabric8.kubernetes.api.model.NamedContext; import io.fabric8.kubernetes.client.http.TlsVersion; import io.fabric8.kubernetes.client.internal.CertUtils; @@ -37,13 +38,13 @@ import io.fabric8.kubernetes.client.utils.IOHelpers; import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Utils; +import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -116,7 +117,12 @@ 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 use {@link #KUBERNETES_KUBECONFIG_FILES} instead. + */ + @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"; @@ -148,6 +154,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; @@ -189,6 +197,8 @@ public class Config { private List contexts; private NamedContext currentContext = null; + @Getter + private List kubeConfigFiles = new ArrayList<>(); /** * fields not used but needed for builder generation. @@ -232,8 +242,6 @@ public class Config { private Boolean autoConfigure; - private File file; - @JsonIgnore protected Map additionalProperties = new HashMap<>(); @@ -263,7 +271,7 @@ private Config(boolean autoConfigure) { null, null, null, null, null, null, null, null, null, null, - null, autoConfigure, true); + null, autoConfigure, true, null); } /** @@ -371,7 +379,7 @@ public Config(String masterUrl, String apiVersion, String namespace, Boolean tru httpProxy, httpsProxy, noProxy, userAgent, tlsVersions, websocketPingInterval, proxyUsername, proxyPassword, trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups, impersonateExtras, oauthTokenProvider, customHeaders, requestRetryBackoffLimit, requestRetryBackoffInterval, - uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, false, true); + uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, false, true, null); } public Config(String masterUrl, String apiVersion, String namespace, Boolean trustCerts, Boolean disableHostnameVerification, @@ -395,7 +403,7 @@ public Config(String masterUrl, String apiVersion, String namespace, Boolean tru httpProxy, httpsProxy, noProxy, userAgent, tlsVersions, websocketPingInterval, proxyUsername, proxyPassword, trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups, impersonateExtras, oauthTokenProvider, customHeaders, requestRetryBackoffLimit, requestRetryBackoffInterval, - uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true); + uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true, null); } /* @@ -414,7 +422,7 @@ public Config(String masterUrl, String apiVersion, String namespace, Boolean tru String impersonateUsername, String[] impersonateGroups, Map> impersonateExtras, OAuthTokenProvider oauthTokenProvider, Map customHeaders, Integer requestRetryBackoffLimit, Integer requestRetryBackoffInterval, Integer uploadRequestTimeout, Boolean onlyHttpWatches, NamedContext currentContext, - List contexts, Boolean autoConfigure, Boolean shouldSetDefaultValues) { + List contexts, Boolean autoConfigure, Boolean shouldSetDefaultValues, List files) { if (Boolean.TRUE.equals(shouldSetDefaultValues)) { this.masterUrl = DEFAULT_MASTER_URL; this.apiVersion = "v1"; @@ -582,7 +590,7 @@ public Config(String masterUrl, String apiVersion, String namespace, Boolean tru if (Utils.isNotNullOrEmpty(autoOAuthToken)) { this.autoOAuthToken = autoOAuthToken; } - if (contexts != null && !contexts.isEmpty()) { + if (Utils.isNotNullOrEmpty(contexts)) { this.contexts = contexts; } if (Utils.isNotNull(currentContext)) { @@ -596,6 +604,9 @@ public Config(String masterUrl, String apiVersion, String namespace, Boolean tru this.oauthTokenProvider = oauthTokenProvider; this.customHeaders = customHeaders; this.onlyHttpWatches = onlyHttpWatches; + if (Utils.isNotNullOrEmpty(files)) { + this.kubeConfigFiles = files; + } } public static void configFromSysPropsOrEnvVars(Config config) { @@ -718,7 +729,7 @@ public static void configFromSysPropsOrEnvVars(Config config) { } String tlsVersionsVar = Utils.getSystemPropertyOrEnvVar(KUBERNETES_TLS_VERSIONS); - if (tlsVersionsVar != null && !tlsVersionsVar.isEmpty()) { + if (Utils.isNotNullOrEmpty(tlsVersionsVar)) { String[] tlsVersionsSplit = tlsVersionsVar.split(","); TlsVersion[] tlsVersions = new TlsVersion[tlsVersionsSplit.length]; for (int i = 0; i < tlsVersionsSplit.length; i++) { @@ -815,7 +826,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.setFile(new File(kubeconfigPath)); } if (!loadFromKubeconfig(config, context, kubeconfigContents)) { throw new KubernetesClientException("Could not create Config from kubeconfig"); @@ -834,20 +845,19 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S */ public Config refresh() { final String currentContextName = this.getCurrentContext() != null ? this.getCurrentContext().getName() : null; - if (this.oauthToken != null && !this.oauthToken.isEmpty()) { + if (Utils.isNotNullOrEmpty(this.oauthToken)) { return this; } 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 + // if files is null there's nothing to refresh - the kubeconfigs were directly supplied + if (!Utils.isNullOrEmpty(kubeConfigFiles)) { + io.fabric8.kubernetes.api.model.Config mergedConfig = mergeKubeConfigs(kubeConfigFiles); + if (mergedConfig != null) { + loadFromKubeconfig(this, mergedConfig.getCurrentContext(), mergedConfig); } - return Config.fromKubeconfig(currentContextName, kubeconfigContents, this.file.getPath()); } - // nothing to refresh - the kubeconfig was directly supplied return this; } @@ -856,55 +866,89 @@ private static boolean tryKubeConfig(Config config, String context) { if (!Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, true)) { return false; } - File kubeConfigFile = new File(getKubeconfigFilename()); - if (!kubeConfigFile.isFile()) { - LOGGER.debug("Did not find Kubernetes config at: [{}]. Ignoring.", kubeConfigFile.getPath()); + String[] kubeConfigFilenames = getKubeconfigFilenames(); + if (Utils.isNullOrEmpty(kubeConfigFilenames)) { return false; } - LOGGER.debug("Found for Kubernetes config at: [{}].", kubeConfigFile.getPath()); - String kubeconfigContents = getKubeconfigContents(kubeConfigFile); - if (kubeconfigContents == null) { - return false; + + List files = Arrays.stream(kubeConfigFilenames) + .map(KubeConfigFile::new) + .collect(Collectors.toList()); + config.setKubeConfigFiles(files); + io.fabric8.kubernetes.api.model.Config mergedConfig = mergeKubeConfigs(files); + return loadFromKubeconfig(config, context, mergedConfig); + } + + private static io.fabric8.kubernetes.api.model.Config mergeKubeConfigs(List files) { + if (Utils.isNullOrEmpty(files)) { + return null; } - config.file = new File(kubeConfigFile.getPath()); - return loadFromKubeconfig(config, context, kubeconfigContents); + return files.stream() + .map(KubeConfigFile::getConfig) + .reduce(null, (merged, additionalConfig) -> { + if (additionalConfig == null) { + return merged; + } else { + return KubeConfigUtils.merge(additionalConfig, merged); + } + }); } + /** + * Returns the first filename of all the filenames that are used in this Config. + * + * @return the first config filename that is used in this config. + * + * @deprecated use {@link #getKubeconfigFilenames()} instead + */ + @Deprecated public static String getKubeconfigFilename() { - String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILE, - new File(getHomeDir(), ".kube" + File.separator + "config").toString()); - - // if system property/env var contains multiple files take the first one based on the environment - // we are running in (eg. : for Linux, ; for Windows) - String[] fileNames = fileName.split(File.pathSeparator); - - if (fileNames.length > 1) { - LOGGER.warn( - "Found multiple Kubernetes config files [{}], using the first one: [{}]. If not desired file, please change it by doing `export KUBECONFIG=/path/to/kubeconfig` on Unix systems or `$Env:KUBECONFIG=/path/to/kubeconfig` on Windows.", - fileNames, fileNames[0]); + String fileName = null; + String[] fileNames = getKubeconfigFilenames(); + if (Utils.isNotNullOrEmpty(fileNames)) { + // if system property/env var contains multiple files take the first one based on the environment fileName = fileNames[0]; + if (fileNames.length > 1) { + LOGGER.info("Found multiple Kubernetes config files [{}], returning the first one. Use #getKubeconfigFilenames instead", + fileNames[0]); + } } return fileName; } - private static String getKubeconfigContents(File kubeConfigFile) { - String kubeconfigContents = null; - try (FileReader reader = new FileReader(kubeConfigFile)) { - kubeconfigContents = IOHelpers.readFully(reader); - } catch (IOException e) { - LOGGER.error("Could not load Kubernetes config file from {}", kubeConfigFile.getPath(), e); - return null; + /** + * Returns all the filenames that are used in this Config. + * Several config files can be used by setting the {@link Config#KUBERNETES_KUBECONFIG_FILES} env variable. + * Returns the default file at {@link Config#DEFAULT_KUBECONFIG_FILE} otherwise + * + * @return all the config files that are used in this Config + */ + public static String[] getKubeconfigFilenames() { + String[] fileNames = null; + String fileName = Utils.getSystemPropertyOrEnvVar(KUBERNETES_KUBECONFIG_FILES); + if (Utils.isNotNullOrEmpty(fileName)) { + fileNames = fileName.split(File.pathSeparator); + } + if (Utils.isNullOrEmpty(fileNames)) { + fileNames = new String[] { DEFAULT_KUBECONFIG_FILE.toString() }; } - return kubeconfigContents; + return fileNames; } // 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 (Utils.isNotNullOrEmpty(kubeconfigContents)) { + 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; } @@ -929,52 +973,70 @@ private static void mergeKubeConfigContents(Config config, String context, io.fa config.setDisableHostnameVerification( currentCluster.getInsecureSkipTlsVerify() != null && currentCluster.getInsecureSkipTlsVerify()); config.setCaCertData(currentCluster.getCertificateAuthorityData()); - AuthInfo currentAuthInfo = KubeConfigUtils.getUserAuthInfo(kubeConfig, currentContext); - if (currentAuthInfo != null) { + if (currentContext != null) { + NamedAuthInfo currentAuthInfo = KubeConfigUtils.getAuthInfo(kubeConfig, currentContext.getUser()); mergeKubeConfigAuthInfo(config, currentCluster, currentAuthInfo); - } - String proxyUrl = currentCluster.getProxyUrl(); - if (Utils.isNotNullOrEmpty(proxyUrl)) { - if (proxyUrl.startsWith(SOCKS5_PROTOCOL_PREFIX) && config.getMasterUrl().startsWith(HTTPS_PROTOCOL_PREFIX)) { - config.setHttpsProxy(proxyUrl); - } else if (proxyUrl.startsWith(SOCKS5_PROTOCOL_PREFIX)) { - config.setHttpProxy(proxyUrl); - } else if (proxyUrl.startsWith(HTTP_PROTOCOL_PREFIX)) { - config.setHttpProxy(proxyUrl); - } else if (proxyUrl.startsWith(HTTPS_PROTOCOL_PREFIX)) { - config.setHttpsProxy(proxyUrl); - } + mergeProxyUrl(config, currentCluster.getProxyUrl()); } } } - private static void mergeKubeConfigAuthInfo(Config config, Cluster currentCluster, AuthInfo currentAuthInfo) + private static void mergeProxyUrl(Config config, String proxyUrl) { + if (Utils.isNullOrEmpty(proxyUrl)) { + return; + } + if (proxyUrl.startsWith(SOCKS5_PROTOCOL_PREFIX) && config.getMasterUrl().startsWith(HTTPS_PROTOCOL_PREFIX)) { + config.setHttpsProxy(proxyUrl); + } else if (proxyUrl.startsWith(SOCKS5_PROTOCOL_PREFIX)) { + config.setHttpProxy(proxyUrl); + } else if (proxyUrl.startsWith(HTTP_PROTOCOL_PREFIX)) { + config.setHttpProxy(proxyUrl); + } else if (proxyUrl.startsWith(HTTPS_PROTOCOL_PREFIX)) { + config.setHttpsProxy(proxyUrl); + } + } + + private static void mergeKubeConfigAuthInfo(Config config, Cluster currentCluster, NamedAuthInfo currentAuthInfo) throws IOException { - // rewrite tls asset paths if needed + if (currentAuthInfo == null) { + return; + } + AuthInfo user = currentAuthInfo.getUser(); + KubeConfigFile kubeConfigFile = config.getFileWithAuthInfo(currentAuthInfo.getName()); + File file = (kubeConfigFile != null) ? kubeConfigFile.getFile() : null; + mergeCertFiles(config, file, currentCluster, user); + config.setClientCertData(user.getClientCertificateData()); + config.setClientKeyData(user.getClientKeyData()); + config.setClientKeyAlgo(getKeyAlgorithm(config.getClientKeyFile(), config.getClientKeyData())); + config.setAutoOAuthToken(user.getToken()); + config.setUsername(user.getUsername()); + config.setPassword(user.getPassword()); + + if (Utils.isNullOrEmpty(config.getAutoOAuthToken()) && user.getAuthProvider() != null) { + mergeKubeConfigAuthProviderConfig(config, user); + } else if (config.getOauthTokenProvider() == null) { // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins + mergeKubeConfigExecCredential(config, user.getExec(), file); + } + } + + private static void mergeCertFiles(Config config, File file, Cluster currentCluster, AuthInfo currentAuthInfo) { + if (config == null + || currentCluster == null + || currentAuthInfo == null) { + return; + } String caCertFile = currentCluster.getCertificateAuthority(); String clientCertFile = currentAuthInfo.getClientCertificate(); String clientKeyFile = currentAuthInfo.getClientKey(); - File configFile = config.file; - if (configFile != null) { - caCertFile = absolutify(configFile, currentCluster.getCertificateAuthority()); - clientCertFile = absolutify(configFile, currentAuthInfo.getClientCertificate()); - clientKeyFile = absolutify(configFile, currentAuthInfo.getClientKey()); + if (file != null) { + // rewrite tls asset paths if needed + caCertFile = absolutify(file, currentCluster.getCertificateAuthority()); + clientCertFile = absolutify(file, currentAuthInfo.getClientCertificate()); + clientKeyFile = absolutify(file, currentAuthInfo.getClientKey()); } config.setCaCertFile(caCertFile); config.setClientCertFile(clientCertFile); - config.setClientCertData(currentAuthInfo.getClientCertificateData()); config.setClientKeyFile(clientKeyFile); - config.setClientKeyData(currentAuthInfo.getClientKeyData()); - config.setClientKeyAlgo(getKeyAlgorithm(config.getClientKeyFile(), config.getClientKeyData())); - config.setAutoOAuthToken(currentAuthInfo.getToken()); - config.setUsername(currentAuthInfo.getUsername()); - config.setPassword(currentAuthInfo.getPassword()); - - if (Utils.isNullOrEmpty(config.getAutoOAuthToken()) && currentAuthInfo.getAuthProvider() != null) { - mergeKubeConfigAuthProviderConfig(config, currentAuthInfo); - } else if (config.getOauthTokenProvider() == null) { // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins - mergeKubeConfigExecCredential(config, currentAuthInfo.getExec(), configFile); - } } private static void mergeKubeConfigAuthProviderConfig(Config config, AuthInfo currentAuthInfo) { @@ -1059,7 +1121,7 @@ protected static List getAuthenticatorCommandFromExecConfig(ExecConfig e command = shellQuote(command); List args = exec.getArgs(); - if (args != null && !args.isEmpty()) { + if (Utils.isNotNullOrEmpty(args)) { command += " " + args .stream() .map(Config::shellQuote) @@ -1092,6 +1154,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; @@ -1144,22 +1207,9 @@ private static boolean tryNamespaceFromPath(Config config) { return false; } - private static String getHomeDir() { - return getHomeDir(Config::isDirectoryAndExists, Config::getSystemEnvVariable); - } - - private static boolean isDirectoryAndExists(String filePath) { - File f = new File(filePath); - return f.exists() && f.isDirectory(); - } - - private static String getSystemEnvVariable(String envVariableName) { - return System.getenv(envVariableName); - } - protected static String getHomeDir(Predicate directoryExists, UnaryOperator getEnvVar) { String home = getEnvVar.apply("HOME"); - if (home != null && !home.isEmpty() && directoryExists.test(home)) { + if (Utils.isNotNullOrEmpty(home) && directoryExists.test(home)) { return home; } String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); @@ -1173,7 +1223,7 @@ protected static String getHomeDir(Predicate directoryExists, UnaryOpera } } String userProfile = getEnvVar.apply("USERPROFILE"); - if (userProfile != null && !userProfile.isEmpty() && directoryExists.test(userProfile)) { + if (Utils.isNotNullOrEmpty(userProfile) && directoryExists.test(userProfile)) { return userProfile; } } @@ -1686,11 +1736,44 @@ 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. - * - * @return the path to the kubeConfig file + * + * @return the kubeConfig file */ public File getFile() { - return file; + if (Utils.isNotNullOrEmpty(kubeConfigFiles)) { + return kubeConfigFiles.get(0).getFile(); + } else { + return null; + } + } + + public KubeConfigFile getFileWithAuthInfo(String name) { + if (Utils.isNullOrEmpty(name)) { + return null; + } + return getFirstKubeConfigFileMatching(config -> KubeConfigUtils.hasAuthInfoNamed(config, name)); + } + + public KubeConfigFile getFileWithContext(String name) { + if (Utils.isNullOrEmpty(name)) { + return null; + } + return getFirstKubeConfigFileMatching(config -> KubeConfigUtils.getContext(config, name) != null); + } + + public KubeConfigFile getFileWithCurrentContext() { + return getFirstKubeConfigFileMatching(config -> Utils.isNotNullOrEmpty(config.getCurrentContext())); + } + + private KubeConfigFile getFirstKubeConfigFileMatching(Predicate predicate) { + if (Utils.isNullOrEmpty(kubeConfigFiles)) { + return null; + } + return kubeConfigFiles.stream() + .filter(KubeConfigFile::isReadable) + .filter(entry -> predicate.test(entry.getConfig())) + .findFirst() + .orElse(null); } @JsonIgnore @@ -1716,8 +1799,58 @@ public void setAdditionalProperty(String name, Object value) { this.additionalProperties.put(name, value); } + public void setKubeConfigFiles(List files) { + if (Utils.isNullOrEmpty(files)) { + this.kubeConfigFiles = Collections.emptyList(); + } else { + this.kubeConfigFiles = files; + } + } + + /** + * 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 the files and Configs that configure this client + */ + public List getKubeConfigFiles() { + if (this.kubeConfigFiles == null) { + return Collections.emptyList(); + } else { + return new ArrayList<>(kubeConfigFiles); + } + } + public void setFile(File file) { - this.file = file; + setFiles(Collections.singletonList(file)); + } + + public void setFiles(List files) { + if (Utils.isNullOrEmpty(files)) { + setKubeConfigFiles(Collections.emptyList()); + } else { + setKubeConfigFiles(files.stream() + .map(KubeConfigFile::new) + .collect(Collectors.toList())); + } + } + + /** + * 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 the files that configure this client + */ + public List getFiles() { + if (this.kubeConfigFiles == null) { + return Collections.emptyList(); + } else { + return kubeConfigFiles.stream() + .map(KubeConfigFile::getFile) + .collect(Collectors.toList()); + } } public void setAutoConfigure(boolean autoConfigure) { diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigBuilder.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigBuilder.java index a829b7ac6ed..c5c99bc529b 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigBuilder.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigBuilder.java @@ -57,9 +57,9 @@ public Config build() { fluent.getOauthTokenProvider(), fluent.getCustomHeaders(), fluent.getRequestRetryBackoffLimit(), fluent.getRequestRetryBackoffInterval(), fluent.getUploadRequestTimeout(), fluent.getOnlyHttpWatches(), fluent.getCurrentContext(), fluent.getContexts(), - Optional.ofNullable(fluent.getAutoConfigure()).orElse(!disableAutoConfig()), true); + Optional.ofNullable(fluent.getAutoConfigure()).orElse(!disableAutoConfig()), true, fluent.getKubeConfigFiles()); buildable.setAuthProvider(fluent.getAuthProvider()); - buildable.setFile(fluent.getFile()); return buildable; } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigFluent.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigFluent.java index b2d81528893..ae6696d5950 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigFluent.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/ConfigFluent.java @@ -15,6 +15,11 @@ */ package io.fabric8.kubernetes.client; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + public class ConfigFluent> extends SundrioConfigFluent { public ConfigFluent() { super(); @@ -78,7 +83,7 @@ public void copyInstance(Config instance) { this.withContexts(instance.getContexts()); this.withAutoConfigure(instance.getAutoConfigure()); this.withAuthProvider(instance.getAuthProvider()); - this.withFile(instance.getFile()); + this.withKubeConfigFiles(instance.getKubeConfigFiles()); } } @@ -148,4 +153,15 @@ public A withAutoConfigure(boolean autoConfigure) { return this.withAutoConfigure(Boolean.valueOf(autoConfigure)); } + public A withFiles(File... files) { + if (files != null + && files.length > 0) { + List configFiles = Arrays.stream(files) + .map(KubeConfigFile::new) + .collect(Collectors.toList()); + withKubeConfigFiles(configFiles); + } + return (A) this; + } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java new file mode 100644 index 00000000000..bce3f2ba61c --- /dev/null +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java @@ -0,0 +1,87 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.fabric8.kubernetes.api.model.Config; +import io.fabric8.kubernetes.client.internal.KubeConfigUtils; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +public class KubeConfigFile { + + private static final Logger LOGGER = LoggerFactory.getLogger(KubeConfigFile.class); + + @Getter + private final File file; + private boolean parsed = false; + private Config config; + + @JsonCreator + public KubeConfigFile(@JsonProperty("file") String file) { + this(new File(file), null); + } + + public KubeConfigFile(File file) { + this(file, null); + } + + private KubeConfigFile(File file, Config config) { + this.file = file; + this.config = config; + } + + public Config getConfig() { + if (!parsed) { + this.config = createConfig(file); + this.parsed = true; + } + return config; + } + + private Config createConfig(File file) { + Config created = null; + try { + if (isReadable(file)) { + LOGGER.debug("Found for Kubernetes config at: [{}].", file.getPath()); + created = KubeConfigUtils.parseConfig(file); + } + } catch (IOException e) { + LOGGER.debug("Kubernetes file at [{}] is not a valid config. Ignoring.", file.getPath(), e); + } + return created; + } + + @JsonIgnore + public boolean isReadable() { + return isReadable(file); + } + + private boolean isReadable(File file) { + try { + return file != null + && file.isFile(); + } catch (SecurityException e) { + return false; + } + } +} \ No newline at end of file diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/SundrioConfig.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/SundrioConfig.java index d6c027f3b3a..91c8afee732 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/SundrioConfig.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/SundrioConfig.java @@ -50,7 +50,7 @@ public SundrioConfig(String masterUrl, String apiVersion, String namespace, Bool String impersonateUsername, String[] impersonateGroups, Map> impersonateExtras, OAuthTokenProvider oauthTokenProvider, Map customHeaders, Integer requestRetryBackoffLimit, Integer requestRetryBackoffInterval, Integer uploadRequestTimeout, Boolean onlyHttpWatches, NamedContext currentContext, - List contexts, Boolean autoConfigure) { + List contexts, Boolean autoConfigure, List kubeConfigFiles) { super(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData, clientCertFile, clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, password, oauthToken, autoOAuthToken, watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout, @@ -58,6 +58,7 @@ public SundrioConfig(String masterUrl, String apiVersion, String namespace, Bool httpProxy, httpsProxy, noProxy, userAgent, tlsVersions, websocketPingInterval, proxyUsername, proxyPassword, trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups, impersonateExtras, oauthTokenProvider, customHeaders, requestRetryBackoffLimit, requestRetryBackoffInterval, - uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true); + uploadRequestTimeout, onlyHttpWatches, currentContext, contexts, autoConfigure, true, kubeConfigFiles); } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java index fbc77a3cfb7..db06894fd07 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,16 +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 io.fabric8.kubernetes.client.utils.Utils; import java.io.File; -import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; import java.util.List; /** @@ -40,7 +44,7 @@ private KubeConfigUtils() { } public static Config parseConfig(File file) throws IOException { - return Serialization.unmarshal(new FileInputStream(file), Config.class); + return Serialization.unmarshal(Files.newInputStream(file.toPath()), Config.class); } public static Config parseConfigFromString(String contents) { @@ -56,16 +60,31 @@ public static Config parseConfigFromString(String contents) { public static NamedContext getCurrentContext(Config config) { String contextName = config.getCurrentContext(); if (contextName != null) { + return getContext(config, contextName); + } + return null; + } + + /** + * Returns the {@link NamedContext} with the given name. + * Returns {@code null} otherwise + * + * @param config the config to search + * @param name the context name to match + * @return the context with the the given name + */ + public static NamedContext getContext(Config config, String name) { + NamedContext context = null; + if (config != null && name != null) { List contexts = config.getContexts(); if (contexts != null) { - for (NamedContext context : contexts) { - if (contextName.equals(context.getName())) { - return context; - } - } + context = contexts.stream() + .filter(toInspect -> name.equals(toInspect.getName())) + .findAny() + .orElse(null); } } - return null; + return context; } /** @@ -91,23 +110,49 @@ public static String getUserToken(Config config, Context context) { * @return {@link AuthInfo} for current context */ public static AuthInfo getUserAuthInfo(Config config, Context context) { - AuthInfo authInfo = null; - if (config != null && context != null) { - String user = context.getUser(); - if (user != null) { - List users = config.getUsers(); - if (users != null) { - authInfo = users.stream() - .filter(u -> u.getName().equals(user)) - .findAny() - .map(NamedAuthInfo::getUser) - .orElse(null); - } + NamedAuthInfo namedAuthInfo = getAuthInfo(config, context.getUser()); + return (namedAuthInfo != null) ? namedAuthInfo.getUser() : null; + } + + /** + * Returns the {@link NamedAuthInfo} with the given name. + * Returns {@code null} otherwise + * + * @param config the config to search + * @param name + * @return + */ + public static NamedAuthInfo getAuthInfo(Config config, String name) { + NamedAuthInfo authInfo = null; + if (config != null && name != null) { + List users = config.getUsers(); + if (users != null) { + authInfo = users.stream() + .filter(toInspect -> name.equals(toInspect.getName())) + .findAny() + .orElse(null); } } return authInfo; } + /** + * Returns {@code true} if the given {@link Config} has a {@link NamedAuthInfo} with the given name. + * Returns {@code false} otherwise. + * + * @param name the name of the NamedAuthInfo that we are looking for + * @param config the Config to search + * @return true if it contains a NamedAuthInfo with the given name + */ + public static boolean hasAuthInfoNamed(Config config, String name) { + if (Utils.isNullOrEmpty(name) + || config == null + || config.getUsers() == null) { + return false; + } + return getAuthInfo(config, name) != null; + } + /** * Returns the current {@link Cluster} for the current context * @@ -161,4 +206,39 @@ public static void persistKubeConfigIntoFile(Config kubeConfig, String kubeConfi writer.write(Serialization.asYaml(kubeConfig)); } } + + public static Config merge(Config thisConfig, Config thatConfig) { + if (thisConfig == null) { + return thatConfig; + } + 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()); + } + if (thisConfig.getExtensions() != null) { + builder.addAllToExtensions(thisConfig.getExtensions()); + } + if (!builder.hasCurrentContext() + && Utils.isNotNullOrEmpty(thisConfig.getCurrentContext())) { + 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()); + 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 ef5fe66061c..6cc77d74d62 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 @@ -20,7 +20,9 @@ import io.fabric8.kubernetes.api.model.AuthInfo; import io.fabric8.kubernetes.api.model.AuthProviderConfig; import io.fabric8.kubernetes.api.model.NamedAuthInfo; +import io.fabric8.kubernetes.api.model.NamedAuthInfoBuilder; import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubeConfigFile; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.http.HttpClient; import io.fabric8.kubernetes.client.http.HttpRequest; @@ -190,38 +192,70 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut if (oAuthToken != null) { authProviderConfig.put(ID_TOKEN_KUBECONFIG, oAuthToken.idToken); authProviderConfig.put(REFRESH_TOKEN_KUBECONFIG, oAuthToken.refreshToken); - // Persist in memory - Optional.of(currentConfig).map(Config::getAuthProvider).map(AuthProviderConfig::getConfig) - .ifPresent(c -> c.putAll(authProviderConfig)); + persistOAuthTokenToFile(currentConfig.getAuthProvider(), authProviderConfig); } - // Persist in file - if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) { + persistOAuthTokenToFile(currentConfig, token, authProviderConfig); + + return oAuthToken; + } + + private static void persistOAuthTokenToFile(Config currentConfig, String token, Map authProviderConfig) { + if (currentConfig.getCurrentContext() != null + && currentConfig.getCurrentContext().getContext() != null) { try { - final io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils.parseConfig(currentConfig.getFile()); final String userName = currentConfig.getCurrentContext().getContext().getUser(); - NamedAuthInfo namedAuthInfo = kubeConfig.getUsers().stream().filter(n -> n.getName().equals(userName)).findFirst() - .orElseGet(() -> { - NamedAuthInfo result = new NamedAuthInfo(userName, new AuthInfo()); - kubeConfig.getUsers().add(result); - return result; - }); - if (namedAuthInfo.getUser() == null) { - namedAuthInfo.setUser(new AuthInfo()); - } - if (namedAuthInfo.getUser().getAuthProvider() == null) { - namedAuthInfo.getUser().setAuthProvider(new AuthProviderConfig()); + KubeConfigFile kubeConfigFile = currentConfig.getFileWithAuthInfo(userName); + if (kubeConfigFile == null + || kubeConfigFile.getConfig() == null) { + LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG: file for user {} not found", userName); + return; } - namedAuthInfo.getUser().getAuthProvider().getConfig().putAll(authProviderConfig); - if (Utils.isNotNullOrEmpty(token)) { - namedAuthInfo.getUser().setToken(token); - } - KubeConfigUtils.persistKubeConfigIntoFile(kubeConfig, currentConfig.getFile().getAbsolutePath()); + final NamedAuthInfo namedAuthInfo = getOrCreateNamedAuthInfo(userName, kubeConfigFile.getConfig()); + setAuthProviderAndToken(token, authProviderConfig, namedAuthInfo); + + KubeConfigUtils.persistKubeConfigIntoFile(kubeConfigFile.getConfig(), kubeConfigFile.getFile().getAbsolutePath()); } catch (IOException ex) { LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG", ex); } } + } - return oAuthToken; + private static void setAuthProviderAndToken(String token, Map authProviderConfig, + NamedAuthInfo namedAuthInfo) { + if (namedAuthInfo.getUser() == null) { + namedAuthInfo.setUser(new AuthInfo()); + } + if (namedAuthInfo.getUser().getAuthProvider() == null) { + namedAuthInfo.getUser().setAuthProvider(new AuthProviderConfig()); + } + namedAuthInfo.getUser().getAuthProvider().getConfig().putAll(authProviderConfig); + if (Utils.isNotNullOrEmpty(token)) { + namedAuthInfo.getUser().setToken(token); + } + } + + private static NamedAuthInfo getOrCreateNamedAuthInfo(String name, io.fabric8.kubernetes.api.model.Config kubeConfig) { + return kubeConfig.getUsers().stream() + .filter(n -> n.getName().equals(name)) + .findFirst() + .orElseGet(() -> { + NamedAuthInfo authInfo = new NamedAuthInfoBuilder() + .withName(name) + .withNewUser() + .endUser() + .build(); + kubeConfig.getUsers().add(authInfo); + return authInfo; + }); + } + + private static void persistOAuthTokenToFile(AuthProviderConfig config, Map authProviderConfig) { + if (config == null) { + return; + } + Optional.of(config) + .map(AuthProviderConfig::getConfig) + .ifPresent(c -> c.putAll(authProviderConfig)); } /** diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java index 8c491c1158e..8db382b5c66 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java @@ -36,6 +36,7 @@ import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -291,8 +292,20 @@ public static boolean isNullOrEmpty(String str) { return str == null || str.isEmpty(); } - public static boolean isNotNullOrEmpty(Map map) { - return !(map == null || map.isEmpty()); + public static boolean isNotNullOrEmpty(Map map) { + return !isNullOrEmpty(map); + } + + public static boolean isNullOrEmpty(Map map) { + return map == null || map.isEmpty(); + } + + public static boolean isNotNullOrEmpty(Collection collection) { + return !isNullOrEmpty(collection); + } + + public static boolean isNullOrEmpty(Collection collection) { + return collection == null || collection.isEmpty(); } public static boolean isNotNullOrEmpty(String str) { @@ -300,7 +313,11 @@ public static boolean isNotNullOrEmpty(String str) { } public static boolean isNotNullOrEmpty(String[] array) { - return !(array == null || array.length == 0); + return !isNullOrEmpty(array); + } + + public static boolean isNullOrEmpty(String[] array) { + return array == null || array.length == 0; } public static boolean isNotNull(T... refList) { diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigConstructorTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigConstructorTest.java index 9caaafed939..98fca170fd3 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigConstructorTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/ConfigConstructorTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import java.io.File; +import java.util.Collections; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; @@ -47,7 +48,7 @@ void emptyConfiguration() { null, null, null, null, null, null, null, null, null, null, - null, null, false); + null, null, false, null); // Then assertThat(config) @@ -190,7 +191,7 @@ void whenAutoConfigureEnabled_thenUseBothDefaultAndAutoConfiguredValues() { null, null, null, null, null, null, null, null, null, null, - null, true, true); + null, true, true, null); // Then assertThat(config) @@ -237,7 +238,8 @@ void whenAutoConfigureEnabled_thenUseBothDefaultAndAutoConfiguredValues() { .hasFieldOrPropertyWithValue("proxyPassword", "autoconfigured-proxyPassword") .hasFieldOrPropertyWithValue("noProxy", new String[] { "autoconfigured-no-proxy-url1.io", "autoconfigured-no-proxy-url2.io" }) - .hasFieldOrPropertyWithValue("autoOAuthToken", "autoconfigured-token"); + .hasFieldOrPropertyWithValue("autoOAuthToken", "autoconfigured-token") + .hasFieldOrPropertyWithValue("files", Collections.singletonList(Config.DEFAULT_KUBECONFIG_FILE)); } @Test @@ -256,7 +258,7 @@ void whenAutoConfigureDisabled_thenOnlyUseDefaultValues() { null, null, null, null, null, null, null, null, null, null, - null, false, true); + null, false, true, null); assertThat(config) .isNotNull() @@ -299,7 +301,8 @@ void whenAutoConfigureDisabled_thenOnlyUseDefaultValues() { .hasFieldOrPropertyWithValue("proxyUsername", null) .hasFieldOrPropertyWithValue("proxyPassword", null) .hasFieldOrPropertyWithValue("noProxy", null) - .hasFieldOrPropertyWithValue("autoOAuthToken", null); + .hasFieldOrPropertyWithValue("autoOAuthToken", null) + .hasFieldOrPropertyWithValue("files", Collections. emptyList()); } } @@ -364,7 +367,7 @@ void configLoadedViaSystemProperties() { null, null, null, null, null, null, null, null, null, null, - null, true, true); + null, true, true, null); // Then assertThat(config) @@ -411,7 +414,8 @@ void configLoadedViaSystemProperties() { .hasFieldOrPropertyWithValue("proxyPassword", "autoconfigured-proxyPassword") .hasFieldOrPropertyWithValue("noProxy", new String[] { "autoconfigured-no-proxy-url1.io", "autoconfigured-no-proxy-url2.io" }) - .hasFieldOrPropertyWithValue("autoOAuthToken", "autoconfigured-token"); + .hasFieldOrPropertyWithValue("autoOAuthToken", "autoconfigured-token") + .hasFieldOrPropertyWithValue("files", Collections.singletonList(Config.DEFAULT_KUBECONFIG_FILE)); } finally { System.clearProperty("kubernetes.master"); System.clearProperty("kubernetes.namespace"); @@ -472,7 +476,7 @@ void configLoadedViaKubeConfig() { null, null, null, null, null, null, null, null, null, null, - null, true, true); + null, true, true, null); // Then assertThat(config) @@ -513,7 +517,7 @@ void configLoadedViaServiceAccount() { null, null, null, null, null, null, null, null, null, null, - null, true, true); + null, true, true, null); // Then assertThat(config) @@ -563,7 +567,7 @@ void throwsException() { null, null, null, null, null, null, null, null, null, null, - null, true, false)); + null, true, false, null)); } finally { System.clearProperty("kubeconfig"); } 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..98d8710b0fd 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 @@ -17,8 +17,11 @@ import io.fabric8.kubernetes.api.model.ExecConfig; import io.fabric8.kubernetes.api.model.ExecConfigBuilder; +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.client.http.TlsVersion; +import io.fabric8.kubernetes.client.internal.KubeConfigUtils; import io.fabric8.kubernetes.client.lib.FileSystem; import io.fabric8.kubernetes.client.utils.Utils; import org.assertj.core.api.InstanceOfAssertFactories; @@ -79,7 +82,7 @@ class ConfigTest { .filePath(ConfigTest.class.getResource("/test-kubeconfig-exec-args-with-spaces")); private static final String TEST_KUBECONFIG_NO_CURRENT_CONTEXT_FILE = Utils - .filePath(ConfigTest.class.getResource("/test-kubeconfig-nocurrentctxt.yml")); + .filePath(ConfigTest.class.getResource("/test-kubeconfig-nocurrentctxt")); private static final String TEST_KUBECONFIG_EXEC_FILE_CERT_AUTH = Utils .filePath(ConfigTest.class.getResource("/test-kubeconfig-exec-cert-auth")); @@ -426,7 +429,7 @@ void autoConfigure_whenKubernetesServiceEnvironmentVariablesPresent_thenComputeM // Then assertThat(config) .hasFieldOrPropertyWithValue("masterUrl", "https://10.0.0.1:443/") - .hasFieldOrPropertyWithValue("file", null); + .hasFieldOrPropertyWithValue("files", Collections.singletonList(new File("/dev/null"))); } finally { System.clearProperty("KUBERNETES_SERVICE_HOST"); System.clearProperty("KUBERNETES_SERVICE_PORT"); @@ -438,14 +441,14 @@ void autoConfigure_whenKubernetesServiceEnvironmentVariablesPresent_thenComputeM void refresh_whenInvoked_shouldCreateNewInstance() { Config config = Config.autoConfigure(null); assertThat(config) - .hasFieldOrPropertyWithValue("file", null) + .hasFieldOrPropertyWithValue("files", Collections.singletonList(new File("/dev/null"))) .hasFieldOrPropertyWithValue("autoConfigure", true); // ensure that refresh creates a new instance Config refresh = config.refresh(); assertThat(refresh) .isNotSameAs(config) - .hasFieldOrPropertyWithValue("file", null) + .hasFieldOrPropertyWithValue("files", Collections.singletonList(new File("/dev/null"))) .hasFieldOrPropertyWithValue("autoConfigure", true); } @@ -461,7 +464,7 @@ void autoConfigure_whenKubernetesServiceEnvironmentVariablesPresentWithIPv6_then // Then assertThat(config) .hasFieldOrPropertyWithValue("masterUrl", "https://[2001:db8:1f70::999:de8:7648:6e8]:443/") - .hasFieldOrPropertyWithValue("file", null); + .hasFieldOrPropertyWithValue("files", Collections.singletonList(new File("/dev/null"))); } finally { System.clearProperty("KUBERNETES_SERVICE_HOST"); System.clearProperty("KUBERNETES_SERVICE_PORT"); @@ -538,7 +541,7 @@ void noArgConstructor_shouldAutoConfigureFromKubeConfig() { .hasFieldOrPropertyWithValue("autoOAuthToken", "token") .satisfies(c -> assertThat(c.getCaCertFile()).endsWith("testns/ca.pem".replace("/", File.separator))) .satisfies(c -> assertThat(new File(c.getCaCertFile())).isAbsolute()) - .hasFieldOrPropertyWithValue("file", new File(TEST_KUBECONFIG_FILE)); + .hasFieldOrPropertyWithValue("files", Collections.singletonList(new File(TEST_KUBECONFIG_FILE))); } @Test @@ -572,7 +575,7 @@ void fromKubeConfigContent() throws IOException { assertThat(config) .hasFieldOrPropertyWithValue("masterUrl", "https://172.28.128.4:8443/") .hasFieldOrPropertyWithValue("autoConfigure", false) - .hasFieldOrPropertyWithValue("file", null) + .hasFieldOrPropertyWithValue("files", Collections.emptyList()) .isSameAs(config.refresh()); } @@ -803,6 +806,26 @@ void honorClientAuthenticatorCommands() throws Exception { } } + @Test + @DisabledOnOs(OS.WINDOWS) + void autoconfigure_givenAuthenticationCommandIn2ndFile_then_honorClientAuthenticatorCommands() throws Exception { + try { + // Given + Files.setPosixFilePermissions(Paths.get(TEST_TOKEN_GENERATOR_FILE), PosixFilePermissions.fromString("rwxrwxr-x")); + System.setProperty("kubeconfig", + TEST_KUBECONFIG_FILE + File.pathSeparator + + TEST_KUBECONFIG_EXEC_FILE); + // When + Config config = Config.autoConfigure("test"); // context in 2nd file + // Then + assertThat(config) + .isNotNull() + .hasFieldOrPropertyWithValue("autoOAuthToken", "HELLO WORLD"); + } finally { + System.clearProperty("kubeconfig"); + } + } + @Test void should_accept_client_authentication_commands_with_null_args() throws Exception { try { @@ -1215,11 +1238,11 @@ void refresh_whenOAuthTokenSourceSetToUser_thenConfigUnchanged() { } @Test - void givenEmptyKubeConfig_whenConfigCreated_thenShouldNotProduceNPE() throws URISyntaxException { + void builder_given_emptyKubeConfig_then_shouldNotProduceNPE() 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 +1253,189 @@ void givenEmptyKubeConfig_whenConfigCreated_thenShouldNotProduceNPE() throws URI System.clearProperty("kubeconfig"); } } + + @Test + void builder_given_severalKubeConfigsAndCurrentContextInFirstFile_then_shouldUseCurrentContextInFirstFile() + throws URISyntaxException { + try { + // Given + System.setProperty("kubeconfig", + getResourceAbsolutePath("/test-kubeconfig-onlycurrentctx") + File.pathSeparator + + 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("kubeconfig"); + } + } + + @Test + void builder_given_severalKubeConfigsAndCurrentContextInSecondFile_then_shouldUseCurrentContextInSecondFile() + throws URISyntaxException { + try { + // Given + System.setProperty("kubeconfig", + getResourceAbsolutePath("/test-kubeconfig-empty") + File.pathSeparator + + 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("kubeconfig"); + } + } + + @Test + void builder_given_severalKubeConfigsWithSameCluster_then_shouldUseFirstCluster() + throws URISyntaxException, IOException { + try { + // Given + String clusterName = "172-28-128-4:8443"; + + String withSecureCluster = getResourceAbsolutePath("/test-ec-kubeconfig-mangled"); + NamedCluster secureCluster = getCluster(withSecureCluster, clusterName); + assertThat(secureCluster).isNotNull(); + assertThat(secureCluster.getCluster().getServer()).isEqualTo("https://bogus.com"); + + String withInsecureCluster = getResourceAbsolutePath("/test-ec-kubeconfig"); + NamedCluster insecureCluster = getCluster(withInsecureCluster, clusterName); + assertThat(insecureCluster).isNotNull(); + assertThat(insecureCluster.getCluster().getServer()).isEqualTo("https://172.28.128.4:8443"); + + System.setProperty("kubeconfig", + withSecureCluster + File.pathSeparator + + withInsecureCluster); + + // When + Config config = new ConfigBuilder().build(); + + // Then + // cluster in 1st file is not overriden by identically named cluster in 2nd file + assertThat(config.getMasterUrl()).startsWith("https://bogus.com"); + } finally { + System.clearProperty("kubeconfig"); + } + } + + private static NamedCluster getCluster(String filePath, String clusterName) throws IOException { + io.fabric8.kubernetes.api.model.Config config = KubeConfigUtils.parseConfig(new File(filePath)); + return config.getClusters().stream() + .filter(cluster -> clusterName.equals(cluster.getName())) + .findFirst() + .orElse(null); + } + + @Test + void builder_given_severalKubeConfigsWithSameUser_then_shouldUseFirstUser() + throws URISyntaxException, IOException { + try { + // Given + String userName = "user/172-28-128-4:8443"; + + String withUserWithToken = getResourceAbsolutePath("/test-ec-kubeconfig-mangled"); + NamedAuthInfo userWithToken = getUser(withUserWithToken, userName); + assertThat(userWithToken).isNotNull(); + assertThat(userWithToken.getUser().getToken()).isEqualTo("token"); + + String withUserWithoutToken = getResourceAbsolutePath("/test-ec-kubeconfig"); + NamedAuthInfo userWithoutToken = getUser(withUserWithoutToken, userName); + assertThat(userWithoutToken).isNotNull(); + assertThat(userWithoutToken.getUser().getToken()).isNull(); + + System.setProperty("kubeconfig", + withUserWithToken + File.pathSeparator + + withUserWithoutToken); + + // When + Config config = new ConfigBuilder().build(); + + // Then + // user in 1st file is not overriden by identically named user in 2nd file + assertThat(config.getAutoOAuthToken()).isEqualTo("token"); + } finally { + System.clearProperty("kubeconfig"); + } + } + + private static NamedAuthInfo getUser(String filePath, String userName) throws IOException { + io.fabric8.kubernetes.api.model.Config config = KubeConfigUtils.parseConfig(new File(filePath)); + return config.getUsers().stream() + .filter(user -> userName.equals(user.getName())) + .findFirst() + .orElse(null); + } + + @Test + void getKubeconfigFilenames_given_severalFilenamesDefinedInKUBECONFIG_then_returnsAllFilenames() throws URISyntaxException { + try { + // Given + String file1 = getResourceAbsolutePath("/test-kubeconfig-empty"); + // has all contexts, clusters, users + String file2 = getResourceAbsolutePath("/test-kubeconfig"); + System.setProperty("kubeconfig", file1 + File.pathSeparator + file2); + + // When + String[] filenames = Config.getKubeconfigFilenames(); + + // Then + assertThat(filenames) + .isNotNull() + .hasSize(2) + .containsExactly(file1, file2); + } finally { + System.clearProperty("kubeconfig"); + } + } + + @Test + void getKubeconfigFilenames_given_KUBECONFNotSet_then_returnsDefault() { + // Given + System.clearProperty("kubeconfig"); + + // When + String[] filenames = Config.getKubeconfigFilenames(); + + // Then + assertThat(filenames) + .isNotNull() + .containsExactly(Config.DEFAULT_KUBECONFIG_FILE.getAbsolutePath()); + } + + @Test + void getFileWithAuthInfo_given_2ConfigsExists_then_returnsFileWithUser() throws URISyntaxException { + try { + // Given + String fileWithToken = getResourceAbsolutePath("/test-kubeconfig-oidc"); + System.setProperty("kubeconfig", + fileWithToken + File.pathSeparator + + Config.DEFAULT_KUBECONFIG_FILE.getAbsolutePath()); + Config config = new ConfigBuilder().build(); + + // When + KubeConfigFile found = config.getFileWithAuthInfo("mmosley"); + + // Then + assertThat(found) + .isNotNull() + .returns(fileWithToken, kubeConfigFile -> kubeConfigFile.getFile().getAbsolutePath()); + } finally { + System.clearProperty("kubeconfig"); + } + } + + 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/java/io/fabric8/kubernetes/client/KubeConfigFileTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/KubeConfigFileTest.java new file mode 100644 index 00000000000..d231716808e --- /dev/null +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/KubeConfigFileTest.java @@ -0,0 +1,113 @@ +/* + * 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; + +import io.fabric8.kubernetes.api.model.Config; +import io.fabric8.kubernetes.client.internal.KubeConfigUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; + +class KubeConfigFileTest { + + private KubeConfigFile kubeConfig; + private KubeConfigFile noFileConfig; + + @BeforeEach + void setUp() throws URISyntaxException { + Path path = Paths.get(Objects.requireNonNull(ConfigTest.class.getResource("/test-kubeconfig")).toURI()); + this.kubeConfig = new KubeConfigFile(path.toFile()); + this.noFileConfig = new KubeConfigFile(new File("bogus")); + } + + @Test + void isReadable_whenFileDoesNotExist_returnsFalse() { + // given + // when + boolean readable = noFileConfig.isReadable(); + // then + assertThat(readable).isFalse(); + } + + @Test + void isReadable_whenFileIsDirectory_returnsFalse() { + // given + KubeConfigFile config = new KubeConfigFile(new File(System.getProperty("user.dir"))); + // when + boolean readable = config.isReadable(); + // then + assertThat(readable).isFalse(); + } + + @Test + void isReadable_whenFileExist_returnsTrue() { + // given + boolean readable = kubeConfig.isReadable(); + // then + assertThat(readable).isTrue(); + } + + @Test + void getConfig_whenFileExists_returnsConfig() { + // given + Config config = kubeConfig.getConfig(); + // then + assertThat(config).isNotNull(); + } + + @Test + void getConfig_whenGettingConfig_isParsingLazily() { + // given + try (MockedStatic utilsMock = Mockito.mockStatic(KubeConfigUtils.class)) { + utilsMock.verify(() -> KubeConfigUtils.parseConfig(any()), Mockito.never()); + // when + kubeConfig.getConfig(); + // then + utilsMock.verify(() -> KubeConfigUtils.parseConfig(kubeConfig.getFile()), Mockito.times(1)); + } + } + + @Test + void getConfig_whenGettingConfig_isNotParsingASecondTime() { + // given + try (MockedStatic utilsMock = Mockito.mockStatic(KubeConfigUtils.class)) { + kubeConfig.getConfig(); + // when + kubeConfig.getConfig(); + // then + utilsMock.verify(() -> KubeConfigUtils.parseConfig(kubeConfig.getFile()), Mockito.times(1)); + } + } + + @Test + void getConfig_whenFileDoesntExist_returnsNull() { + // given + Config config = noFileConfig.getConfig(); + // then + assertThat(config).isNull(); + } + +} 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 e14e91ae501..109480d0c38 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 @@ -20,16 +20,24 @@ 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.NamedContext; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Stream; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Named.named; class KubeConfigUtilsTest { @Test @@ -115,7 +123,56 @@ void testGetUserAuthInfo() { assertEquals("test-token-2", authInfo.getToken()); } - private Config getTestKubeConfig() { + @Test + void getAuthInfo_when_authInfoExists_returnsAuthInfo() { + // given + Config config = getTestKubeConfig(); + // when + NamedAuthInfo found = KubeConfigUtils.getAuthInfo(config, "test/api-test-com:443"); + // then + assertThat(found).isNotNull(); + } + + @Test + void getAuthInfo_when_authInfoDoesntExist_returnsNull() { + // given + Config config = getTestKubeConfig(); + // when + NamedAuthInfo found = KubeConfigUtils.getAuthInfo(config, "bogus"); + // then + assertThat(found).isNull(); + } + + @ParameterizedTest + @MethodSource("hasAuthInfoNamed_arguments") + void hasAuthInfoNamed(Config config, String authInfoName, Consumer consumer) { + // given + // when + boolean hasIt = KubeConfigUtils.hasAuthInfoNamed(config, authInfoName); + // then + consumer.accept(hasIt); + } + + static Stream hasAuthInfoNamed_arguments() { + return Stream.of( + // given config with authInfo, when getAuthInfoName with existing name, then should return true + Arguments.of( + named("given config with authInfo", getTestKubeConfig()), + named("given existing name", "test/api-test-com:443"), + named("then return true", (Consumer) (hasIt -> assertThat(hasIt).isTrue()))), + // given config with authInfo, when getAuthInfoName with missing name, then should return false + Arguments.of( + named("given config with authInfo", getTestKubeConfig()), + named("given missing authInfo name", "bogus"), + named("then return false", (Consumer) (hasIt -> assertThat(hasIt).isFalse()))), + // given config without authInfo, when getAuthInfoName with missing name, then should return false + Arguments.of( + named("given config without authInfo", new ConfigBuilder().build()), + named("given missing authInfo name", "test/api-test-com:443"), + named("then return false", (Consumer) (hasIt -> assertThat(hasIt).isFalse())))); + } + + private static Config getTestKubeConfig() { return new ConfigBuilder() .withCurrentContext("test-context") .addNewCluster() 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 1870b8dd910..5ab97fe52d1 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 @@ -19,6 +19,7 @@ import io.fabric8.kubernetes.api.model.NamedAuthInfo; import io.fabric8.kubernetes.api.model.NamedAuthInfoBuilder; import io.fabric8.kubernetes.api.model.NamedClusterBuilder; +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; @@ -112,7 +113,7 @@ void setUp() throws Exception { .build(); Files.write(kubeConfigFile, Serialization.asYaml(kubeConfig).getBytes(StandardCharsets.UTF_8)); originalConfig = new ConfigBuilder(Config.empty()) - .withFile(tempDir.resolve("kube-config").toFile()) + .withFiles(tempDir.resolve("kube-config").toFile()) .build() .refresh(); // Auth provider configuration (minimal) @@ -493,11 +494,12 @@ void skipsInFileWhenOriginalConfigHasNoCurrentContext() { void logsWarningIfReferencedFileIsMissing() { originalConfig.setFile(kubeConfig); originalConfig = new ConfigBuilder(originalConfig) - .withCurrentContext(new NamedContextBuilder().withName("context").build()).build(); + .withCurrentContext(createNamedContext("context", "default-user")) + .build(); persistOAuthToken(originalConfig, oAuthTokenResponse, "fake.token"); assertThat(systemErr.toString()) .contains("oidc: failure while persisting new tokens into KUBECONFIG") - .contains("FileNotFoundException"); + .contains("file for user default-user not found"); } @Nested @@ -505,18 +507,19 @@ void logsWarningIfReferencedFileIsMissing() { class WithValidKubeConfig { @BeforeEach void setUp() throws IOException { - Files.write(kubeConfig.toPath(), ("---" + + Files.write(kubeConfig.toPath(), ("---\n" + "users:\n" + - "- name: user\n").getBytes(StandardCharsets.UTF_8)); + "- name: user\n" + + "contexts:\n" + + "- context:\n" + + " name: context\n").getBytes(StandardCharsets.UTF_8)); } @Test void persistsTokenInFile() throws IOException { originalConfig.setFile(kubeConfig); originalConfig = new ConfigBuilder(originalConfig) - .withCurrentContext(new NamedContextBuilder() - .withName("context") - .withNewContext().withUser("user").endContext().build()) + .withCurrentContext(createNamedContext("context", "user")) .build(); persistOAuthToken(originalConfig, oAuthTokenResponse, "fake.token"); assertThat(KubeConfigUtils.parseConfig(kubeConfig)) @@ -555,4 +558,13 @@ void persistsOAuthTokenInFile() throws IOException { } } + + private static NamedContext createNamedContext(String name, String user) { + return new NamedContextBuilder() + .withName(name) + .withNewContext() + .withUser(user) + .endContext() + .build(); + } } 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 e5a7886a061..ce61dedfe40 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 @@ -40,6 +40,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import static io.fabric8.kubernetes.client.http.TestStandardHttpClientFactory.Mode.SINGLETON; import static io.fabric8.kubernetes.client.utils.OpenIDConnectionUtils.CLIENT_ID_KUBECONFIG; @@ -77,9 +78,13 @@ void persistOAuthTokenWithUpdatedToken(@TempDir Path tempDir) throws IOException oAuthTokenResponse.setIdToken("id-token-updated"); oAuthTokenResponse.setRefreshToken("refresh-token-updated"); Path kubeConfig = Files.createTempFile(tempDir, "test", "kubeconfig"); - Files.copy(OpenIDConnectionUtilsTest.class.getResourceAsStream("/test-kubeconfig-oidc"), kubeConfig, + Files.copy( + Objects.requireNonNull(OpenIDConnectionUtilsTest.class.getResourceAsStream("/test-kubeconfig-oidc")), + kubeConfig, StandardCopyOption.REPLACE_EXISTING); - Config originalConfig = Config.fromKubeconfig(null, new String(Files.readAllBytes(kubeConfig), StandardCharsets.UTF_8), + Config originalConfig = Config.fromKubeconfig( + null, + new String(Files.readAllBytes(kubeConfig), StandardCharsets.UTF_8), kubeConfig.toFile().getAbsolutePath()); // When diff --git a/kubernetes-client-api/src/test/resources/test-ec-kubeconfig-mangled b/kubernetes-client-api/src/test/resources/test-ec-kubeconfig-mangled new file mode 100644 index 00000000000..d0b63ee8bd2 --- /dev/null +++ b/kubernetes-client-api/src/test/resources/test-ec-kubeconfig-mangled @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority: testns/ca.pem + server: https://bogus.com + name: 172-28-128-4:8443 +contexts: +- context: + cluster: 172-28-128-4:8443 + namespace: default + user: user/172-28-128-4:8443 + name: default/172-28-128-4:8443/user +current-context: default/172-28-128-4:8443/user +kind: Config +preferences: {} +users: +- name: user/172-28-128-4:8443 + user: + token: token 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-nocurrentctxt.yml b/kubernetes-client-api/src/test/resources/test-kubeconfig-nocurrentctxt similarity index 100% rename from kubernetes-client-api/src/test/resources/test-kubeconfig-nocurrentctxt.yml rename to kubernetes-client-api/src/test/resources/test-kubeconfig-nocurrentctxt diff --git a/kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentctx b/kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentctx new file mode 100644 index 00000000000..dc9a9504c8b --- /dev/null +++ b/kubernetes-client-api/src/test/resources/test-kubeconfig-onlycurrentctx @@ -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