From d5972d2a7068894c4ac06348e8be923e6c9d7d4a Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 14 Aug 2024 11:32:43 +0200 Subject: [PATCH] fix: allow multiple configs listed in KUBECONFIG (#779) * save kube conf files when current ctx or ns changes * watch all config files (#779) Signed-off-by: Andre Dietisheim --- build.gradle | 11 + gradle.properties | 2 +- .../io/fabric8/kubernetes/client/Config.java | 1902 +++++++++++++++++ .../kubernetes/client/KubeConfigFile.java | 84 + .../client/internal/KubeConfigUtils.java | 244 +++ .../client/utils/OpenIDConnectionUtils.java | 362 ++++ .../kubernetes/client/utils/Utils.java | 565 +++++ .../intellij/kubernetes/model/AllContexts.kt | 37 +- .../kubernetes/model/client/ClientConfig.kt | 105 +- .../model/client/KubeConfigAdapter.kt | 42 - .../model/context/IActiveContext.kt | 3 + .../kubernetes/model/AllContextsTest.kt | 127 +- .../model/client/ClientConfigTest.kt | 583 +++-- .../kubernetes/model/mocks/ClientMocks.kt | 18 +- 14 files changed, 3653 insertions(+), 432 deletions(-) create mode 100644 src/main/java/io/fabric8/kubernetes/client/Config.java create mode 100644 src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java create mode 100644 src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java create mode 100644 src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java create mode 100644 src/main/java/io/fabric8/kubernetes/client/utils/Utils.java delete mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/KubeConfigAdapter.kt diff --git a/build.gradle b/build.gradle index bac794d74..d51269f1b 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,17 @@ sourceSets { compileClasspath += sourceSets.main.output + configurations.runtimeClasspath runtimeClasspath += output + compileClasspath } + main { + java.srcDirs("src/main/java") + kotlin.srcDirs("src/main/kotlin") + } + test { + java.srcDirs("src/test/java") + kotlin.srcDirs("src/test/kotlin") + // #779: unit tests need to see kubernetes-client classes in src/main/java + compileClasspath += sourceSets.main.output + configurations.runtimeClasspath + runtimeClasspath += output + compileClasspath + } } task integrationTest(type: Test) { diff --git a/gradle.properties b/gradle.properties index 2cc897239..1b0da083b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ jetBrainsToken=invalid jetBrainsChannel=stable intellijPluginVersion=1.16.1 kotlinJvmPluginVersion=1.8.0 -intellijCommonVersion=1.9.6 +intellijCommonVersion=1.9.7-SNAPSHOT telemetryPluginVersion=1.1.0.52 kotlin.stdlib.default.dependency = false kotlinVersion=1.6.21 diff --git a/src/main/java/io/fabric8/kubernetes/client/Config.java b/src/main/java/io/fabric8/kubernetes/client/Config.java new file mode 100644 index 000000000..f36a301d2 --- /dev/null +++ b/src/main/java/io/fabric8/kubernetes/client/Config.java @@ -0,0 +1,1902 @@ +/* + * 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.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.fabric8.kubernetes.api.model.AuthInfo; +import io.fabric8.kubernetes.api.model.AuthProviderConfig; +import io.fabric8.kubernetes.api.model.Cluster; +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.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; +import io.fabric8.kubernetes.client.internal.KubeConfigUtils; +import io.fabric8.kubernetes.client.internal.SSLUtils; +import io.fabric8.kubernetes.client.readiness.Readiness; +import io.fabric8.kubernetes.client.utils.IOHelpers; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.fabric8.kubernetes.client.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(allowGetters = true, allowSetters = true) +public class Config { + + private static final Logger LOGGER = LoggerFactory.getLogger(Config.class); + + /** + * Disables auto-configuration based on opinionated defaults in a {@link Config} object in the all arguments constructor + */ + public static final String KUBERNETES_DISABLE_AUTO_CONFIG_SYSTEM_PROPERTY = "kubernetes.disable.autoConfig"; + public static final String KUBERNETES_MASTER_SYSTEM_PROPERTY = "kubernetes.master"; + public static final String KUBERNETES_API_VERSION_SYSTEM_PROPERTY = "kubernetes.api.version"; + public static final String KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY = "kubernetes.trust.certificates"; + public static final String KUBERNETES_DISABLE_HOSTNAME_VERIFICATION_SYSTEM_PROPERTY = "kubernetes.disable.hostname.verification"; + public static final String KUBERNETES_CA_CERTIFICATE_FILE_SYSTEM_PROPERTY = "kubernetes.certs.ca.file"; + public static final String KUBERNETES_CA_CERTIFICATE_DATA_SYSTEM_PROPERTY = "kubernetes.certs.ca.data"; + public static final String KUBERNETES_CLIENT_CERTIFICATE_FILE_SYSTEM_PROPERTY = "kubernetes.certs.client.file"; + public static final String KUBERNETES_CLIENT_CERTIFICATE_DATA_SYSTEM_PROPERTY = "kubernetes.certs.client.data"; + public static final String KUBERNETES_CLIENT_KEY_FILE_SYSTEM_PROPERTY = "kubernetes.certs.client.key.file"; + public static final String KUBERNETES_CLIENT_KEY_DATA_SYSTEM_PROPERTY = "kubernetes.certs.client.key.data"; + public static final String KUBERNETES_CLIENT_KEY_ALGO_SYSTEM_PROPERTY = "kubernetes.certs.client.key.algo"; + public static final String KUBERNETES_CLIENT_KEY_PASSPHRASE_SYSTEM_PROPERTY = "kubernetes.certs.client.key.passphrase"; + public static final String KUBERNETES_AUTH_BASIC_USERNAME_SYSTEM_PROPERTY = "kubernetes.auth.basic.username"; + public static final String KUBERNETES_AUTH_BASIC_PASSWORD_SYSTEM_PROPERTY = "kubernetes.auth.basic.password"; + public static final String KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY = "kubernetes.auth.tryKubeConfig"; + public static final String KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY = "kubernetes.auth.tryServiceAccount"; + public static final String KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY = "kubernetes.auth.serviceAccount.token"; + public static final String KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY = "kubernetes.auth.token"; + public static final String KUBERNETES_WATCH_RECONNECT_INTERVAL_SYSTEM_PROPERTY = "kubernetes.watch.reconnectInterval"; + public static final String KUBERNETES_WATCH_RECONNECT_LIMIT_SYSTEM_PROPERTY = "kubernetes.watch.reconnectLimit"; + public static final String KUBERNETES_CONNECTION_TIMEOUT_SYSTEM_PROPERTY = "kubernetes.connection.timeout"; + public static final String KUBERNETES_UPLOAD_REQUEST_TIMEOUT_SYSTEM_PROPERTY = "kubernetes.upload.request.timeout"; + public static final String KUBERNETES_REQUEST_TIMEOUT_SYSTEM_PROPERTY = "kubernetes.request.timeout"; + public static final String KUBERNETES_REQUEST_RETRY_BACKOFFLIMIT_SYSTEM_PROPERTY = "kubernetes.request.retry.backoffLimit"; + public static final String KUBERNETES_REQUEST_RETRY_BACKOFFINTERVAL_SYSTEM_PROPERTY = "kubernetes.request.retry.backoffInterval"; + public static final String KUBERNETES_LOGGING_INTERVAL_SYSTEM_PROPERTY = "kubernetes.logging.interval"; + public static final String KUBERNETES_SCALE_TIMEOUT_SYSTEM_PROPERTY = "kubernetes.scale.timeout"; + public static final String KUBERNETES_WEBSOCKET_PING_INTERVAL_SYSTEM_PROPERTY = "kubernetes.websocket.ping.interval"; + public static final String KUBERNETES_MAX_CONCURRENT_REQUESTS = "kubernetes.max.concurrent.requests"; + public static final String KUBERNETES_MAX_CONCURRENT_REQUESTS_PER_HOST = "kubernetes.max.concurrent.requests.per.host"; + + public static final String KUBERNETES_IMPERSONATE_USERNAME = "kubernetes.impersonate.username"; + public static final String KUBERNETES_IMPERSONATE_GROUP = "kubernetes.impersonate.group"; + + public static final String KUBERNETES_TRUSTSTORE_PASSPHRASE_PROPERTY = "kubernetes.truststore.passphrase"; + public static final String KUBERNETES_TRUSTSTORE_FILE_PROPERTY = "kubernetes.truststore.file"; + public static final String KUBERNETES_KEYSTORE_PASSPHRASE_PROPERTY = "kubernetes.keystore.passphrase"; + public static final String KUBERNETES_KEYSTORE_FILE_PROPERTY = "kubernetes.keystore.file"; + + public static final String KUBERNETES_TLS_VERSIONS = "kubernetes.tls.versions"; + + public static final String KUBERNETES_TRYNAMESPACE_PATH_SYSTEM_PROPERTY = "kubernetes.tryNamespacePath"; + 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"; + public static final String KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; + public static final String KUBERNETES_HTTP2_DISABLE = "http2.disable"; + public static final String KUBERNETES_HTTP_PROXY = "http.proxy"; + public static final String KUBERNETES_HTTPS_PROXY = "https.proxy"; + public static final String KUBERNETES_ALL_PROXY = "all.proxy"; + public static final String KUBERNETES_NO_PROXY = "no.proxy"; + public static final String KUBERNETES_PROXY_USERNAME = "proxy.username"; + public static final String KUBERNETES_PROXY_PASSWORD = "proxy.password"; + + public static final String KUBERNETES_USER_AGENT = "kubernetes.user.agent"; + + public static final String DEFAULT_MASTER_URL = "https://kubernetes.default.svc"; + public static final Long DEFAULT_SCALE_TIMEOUT = 10 * 60 * 1000L; + public static final int DEFAULT_REQUEST_TIMEOUT = 10 * 1000; + public static final int DEFAULT_LOGGING_INTERVAL = 20 * 1000; + public static final Long DEFAULT_WEBSOCKET_PING_INTERVAL = 30 * 1000L; + + public static final Integer DEFAULT_MAX_CONCURRENT_REQUESTS = 64; + public static final Integer DEFAULT_MAX_CONCURRENT_REQUESTS_PER_HOST = 5; + + public static final Integer DEFAULT_REQUEST_RETRY_BACKOFFLIMIT = 10; + public static final Integer DEFAULT_REQUEST_RETRY_BACKOFFINTERVAL = 100; + + public static final int DEFAULT_UPLOAD_REQUEST_TIMEOUT = 120 * 1000; + + 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; + private static final int DEFAULT_CONNECTION_TIMEOUT = 10 * 1000; + private static final String DEFAULT_CLIENT_KEY_PASSPHRASE = "changeit"; + private static final String SOCKS5_PROTOCOL_PREFIX = "socks5://"; + + private Boolean trustCerts; + private Boolean disableHostnameVerification; + private String masterUrl; + private String apiVersion; + private String namespace; + private Boolean defaultNamespace; + private String caCertFile; + private String caCertData; + private String clientCertFile; + private String clientCertData; + private String clientKeyFile; + private String clientKeyData; + private String clientKeyAlgo; + private String clientKeyPassphrase; + private String trustStoreFile; + private String trustStorePassphrase; + private String keyStoreFile; + private String keyStorePassphrase; + private AuthProviderConfig authProvider; + private String username; + private String password; + private volatile String oauthToken; + @JsonIgnore + private volatile String autoOAuthToken; + private OAuthTokenProvider oauthTokenProvider; + private Long websocketPingInterval; + private Integer connectionTimeout; + private Integer maxConcurrentRequests; + private Integer maxConcurrentRequestsPerHost; + + private final RequestConfig requestConfig; + + private List contexts; + private NamedContext currentContext = null; + private List kubeConfigFiles = new ArrayList<>(); + + /** + * fields not used but needed for builder generation. + */ + private Integer watchReconnectInterval; + private Integer watchReconnectLimit; + private Integer uploadRequestTimeout; + private Integer requestRetryBackoffLimit; + private Integer requestRetryBackoffInterval; + private Integer requestTimeout; + private Long scaleTimeout; + private Integer loggingInterval; + private String impersonateUsername; + + /** + * @deprecated use impersonateGroups instead + */ + @Deprecated + private String impersonateGroup; + private String[] impersonateGroups; + private Map> impersonateExtras; + /** + * end of fields not used but needed for builder generation. + */ + + private Boolean http2Disable; + private String httpProxy; + private String httpsProxy; + private String proxyUsername; + private String proxyPassword; + private String[] noProxy; + private String userAgent; + private TlsVersion[] tlsVersions; + + private Boolean onlyHttpWatches; + + @Deprecated + private Map errorMessages = new HashMap<>(); + + /** + * custom headers + */ + private Map customHeaders = null; + + private Boolean autoConfigure; + + @JsonIgnore + protected Map additionalProperties = new HashMap<>(); + + /** + * @deprecated use {@link #autoConfigure(String)} or {@link ConfigBuilder} instead + */ + @Deprecated + public Config() { + this(!disableAutoConfig()); + } + + static boolean disableAutoConfig() { + return Utils.getSystemPropertyOrEnvVar(KUBERNETES_DISABLE_AUTO_CONFIG_SYSTEM_PROPERTY, false); + } + + private Config(boolean autoConfigure) { + this(null, null, null, null, null, + null, null, null, null, null, + null, null, null, null, null, + null, null, null, null, null, + null, null, null, null, + null, + null, null, null, null, + null, null, null, + null, + null, null, null, null, null, + null, null, null, + null, null, null, + null, null, null, null, + null, autoConfigure, true, null); + } + + /** + * Create an empty {@link Config} class without any automatic configuration + * (i.e. reading system properties/environment variables to load defaults.) + * You can also reuse this object to build your own {@link Config} object + * without any auto configuration like this: + * + * + *
{@code
+   * Config configFromBuilder = new ConfigBuilder(Config.empty())
+   *                                // ...
+   *                               .build();
+   * }
+ * + * @return a Config object without any automatic configuration + */ + public static Config empty() { + return new Config(false); + } + + /** + * Does auto detection with some opinionated defaults. + * + * @param context if null will use current-context + * @return Config object + */ + public static Config autoConfigure(String context) { + Config config = new Config(false); + return autoConfigure(config, context); + } + + private static Config autoConfigure(Config config, String context) { + if (!tryKubeConfig(config, context)) { + tryServiceAccount(config); + tryNamespaceFromPath(config); + } + postAutoConfigure(config); + config.autoConfigure = true; + return config; + } + + private static void postAutoConfigure(Config config) { + configFromSysPropsOrEnvVars(config); + + config.masterUrl = ensureHttps(config.masterUrl, config); + config.masterUrl = ensureEndsWithSlash(config.masterUrl); + } + + private static String ensureEndsWithSlash(String masterUrl) { + if (!masterUrl.endsWith("/")) { + masterUrl = masterUrl + "/"; + } + return masterUrl; + } + + private static String ensureHttps(String masterUrl, Config config) { + if (!masterUrl.toLowerCase(Locale.ROOT).startsWith(HTTP_PROTOCOL_PREFIX) + && !masterUrl.toLowerCase(Locale.ROOT).startsWith(HTTPS_PROTOCOL_PREFIX)) { + masterUrl = (SSLUtils.isHttpsAvailable(config) ? HTTPS_PROTOCOL_PREFIX : HTTP_PROTOCOL_PREFIX) + masterUrl; + } + return masterUrl; + } + + @Deprecated + public Config(String masterUrl, String apiVersion, String namespace, Boolean trustCerts, boolean disableHostnameVerification, + String caCertFile, String caCertData, String clientCertFile, String clientCertData, String clientKeyFile, + String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, String username, String password, + String oauthToken, String autoOAuthToken, int watchReconnectInterval, int watchReconnectLimit, int connectionTimeout, + int requestTimeout, + long rollingTimeout, long scaleTimeout, int loggingInterval, int maxConcurrentRequests, int maxConcurrentRequestsPerHost, + String httpProxy, String httpsProxy, String[] noProxy, Map errorMessages, String userAgent, + TlsVersion[] tlsVersions, long websocketPingInterval, String proxyUsername, String proxyPassword, + String trustStoreFile, String trustStorePassphrase, String keyStoreFile, String keyStorePassphrase, + String impersonateUsername, String[] impersonateGroups, Map> impersonateExtras) { + this(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData, clientCertFile, + clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, password, oauthToken, + autoOAuthToken, + watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout, scaleTimeout, + loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, false, httpProxy, httpsProxy, noProxy, + userAgent, tlsVersions, websocketPingInterval, proxyUsername, proxyPassword, + trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, impersonateUsername, impersonateGroups, + impersonateExtras, null, null, DEFAULT_REQUEST_RETRY_BACKOFFLIMIT, DEFAULT_REQUEST_RETRY_BACKOFFINTERVAL, + DEFAULT_UPLOAD_REQUEST_TIMEOUT, false, null, Collections.emptyList()); + } + + @Deprecated + public Config(String masterUrl, String apiVersion, String namespace, Boolean trustCerts, boolean disableHostnameVerification, + String caCertFile, String caCertData, String clientCertFile, String clientCertData, String clientKeyFile, + String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, String username, String password, + String oauthToken, String autoOAuthToken, int watchReconnectInterval, int watchReconnectLimit, int connectionTimeout, + int requestTimeout, + long scaleTimeout, int loggingInterval, int maxConcurrentRequests, int maxConcurrentRequestsPerHost, + boolean http2Disable, String httpProxy, String httpsProxy, String[] noProxy, + String userAgent, TlsVersion[] tlsVersions, long websocketPingInterval, String proxyUsername, + String proxyPassword, String trustStoreFile, String trustStorePassphrase, String keyStoreFile, String keyStorePassphrase, + String impersonateUsername, String[] impersonateGroups, Map> impersonateExtras, + OAuthTokenProvider oauthTokenProvider, Map customHeaders, int requestRetryBackoffLimit, + int requestRetryBackoffInterval, int uploadRequestTimeout, boolean onlyHttpWatches, NamedContext currentContext, + List contexts) { + this(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData, + clientCertFile, clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, + password, oauthToken, autoOAuthToken, watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout, + scaleTimeout, loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, http2Disable, + 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, null); + } + + public Config(String masterUrl, String apiVersion, String namespace, Boolean trustCerts, Boolean disableHostnameVerification, + String caCertFile, String caCertData, String clientCertFile, String clientCertData, String clientKeyFile, + String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, String username, String password, + String oauthToken, String autoOAuthToken, Integer watchReconnectInterval, Integer watchReconnectLimit, + Integer connectionTimeout, + Integer requestTimeout, + Long scaleTimeout, Integer loggingInterval, Integer maxConcurrentRequests, Integer maxConcurrentRequestsPerHost, + Boolean http2Disable, String httpProxy, String httpsProxy, String[] noProxy, + String userAgent, TlsVersion[] tlsVersions, Long websocketPingInterval, String proxyUsername, + String proxyPassword, String trustStoreFile, String trustStorePassphrase, String keyStoreFile, String keyStorePassphrase, + String impersonateUsername, String[] impersonateGroups, Map> impersonateExtras, + OAuthTokenProvider oauthTokenProvider, Map customHeaders, Integer requestRetryBackoffLimit, + Integer requestRetryBackoffInterval, Integer uploadRequestTimeout, Boolean onlyHttpWatches, NamedContext currentContext, + List contexts, Boolean autoConfigure) { + this(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, caCertFile, caCertData, + clientCertFile, clientCertData, clientKeyFile, clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, + password, oauthToken, autoOAuthToken, watchReconnectInterval, watchReconnectLimit, connectionTimeout, requestTimeout, + scaleTimeout, loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, http2Disable, + 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, null); + } + + /** + * introduced for backward compatibility with {@link io.fabric8.openshift.client.OpenShiftConfig} 6.12.0 + **/ + protected Config(String masterUrl, String apiVersion, String namespace, boolean trustCerts, boolean disableHostnameVerification, + String caCertFile, String caCertData, String clientCertFile, String clientCertData, String clientKeyFile, + String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, String username, String password, + String oauthToken, String autoOAuthToken, int watchReconnectInterval, int watchReconnectLimit, int connectionTimeout, + int requestTimeout, + long scaleTimeout, int loggingInterval, int maxConcurrentRequests, int maxConcurrentRequestsPerHost, + boolean http2Disable, String httpProxy, String httpsProxy, String[] noProxy, Map errorMessages, + String userAgent, TlsVersion[] tlsVersions, long websocketPingInterval, String proxyUsername, + String proxyPassword, String trustStoreFile, String trustStorePassphrase, String keyStoreFile, String keyStorePassphrase, + String impersonateUsername, String[] impersonateGroups, Map> impersonateExtras, + OAuthTokenProvider oauthTokenProvider, Map customHeaders, int requestRetryBackoffLimit, + int requestRetryBackoffInterval, int uploadRequestTimeout, boolean onlyHttpWatches) { + this(masterUrl, apiVersion, namespace, trustCerts, disableHostnameVerification, + caCertFile, caCertData, clientCertFile, clientCertData, clientKeyFile, + clientKeyData, clientKeyAlgo, clientKeyPassphrase, username, password, + oauthToken, autoOAuthToken, watchReconnectInterval, watchReconnectLimit, + connectionTimeout, + requestTimeout, + scaleTimeout, loggingInterval, maxConcurrentRequests, maxConcurrentRequestsPerHost, + http2Disable, httpProxy, httpsProxy, noProxy, + // errorMessages, + userAgent, tlsVersions, websocketPingInterval, proxyUsername, + proxyPassword, trustStoreFile, trustStorePassphrase, keyStoreFile, keyStorePassphrase, + impersonateUsername, impersonateGroups, impersonateExtras, + oauthTokenProvider, customHeaders, requestRetryBackoffLimit, + requestRetryBackoffInterval, uploadRequestTimeout, onlyHttpWatches, null, + null, false, true, null); + } + + /* + * The Builder is generated in SundrioConfig, if new fields need to be added here, please make sure to add them there too. + */ + Config(String masterUrl, String apiVersion, String namespace, Boolean trustCerts, Boolean disableHostnameVerification, + String caCertFile, String caCertData, String clientCertFile, String clientCertData, String clientKeyFile, + String clientKeyData, String clientKeyAlgo, String clientKeyPassphrase, String username, String password, + String oauthToken, String autoOAuthToken, Integer watchReconnectInterval, Integer watchReconnectLimit, + Integer connectionTimeout, + Integer requestTimeout, + Long scaleTimeout, Integer loggingInterval, Integer maxConcurrentRequests, Integer maxConcurrentRequestsPerHost, + Boolean http2Disable, String httpProxy, String httpsProxy, String[] noProxy, + String userAgent, TlsVersion[] tlsVersions, Long websocketPingInterval, String proxyUsername, + String proxyPassword, String trustStoreFile, String trustStorePassphrase, String keyStoreFile, String keyStorePassphrase, + 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 files) { + if (Boolean.TRUE.equals(shouldSetDefaultValues)) { + this.masterUrl = DEFAULT_MASTER_URL; + this.apiVersion = "v1"; + this.defaultNamespace = true; + this.trustCerts = false; + this.disableHostnameVerification = false; + this.onlyHttpWatches = false; + this.http2Disable = false; + this.clientKeyAlgo = "RSA"; + this.clientKeyPassphrase = DEFAULT_CLIENT_KEY_PASSPHRASE; + this.websocketPingInterval = DEFAULT_WEBSOCKET_PING_INTERVAL; + this.connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; + this.maxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS; + this.maxConcurrentRequestsPerHost = DEFAULT_MAX_CONCURRENT_REQUESTS_PER_HOST; + this.contexts = new ArrayList<>(); + this.watchReconnectInterval = DEFAULT_WATCH_RECONNECT_INTERVAL; + this.watchReconnectLimit = -1; + this.uploadRequestTimeout = DEFAULT_UPLOAD_REQUEST_TIMEOUT; + this.requestRetryBackoffInterval = DEFAULT_REQUEST_RETRY_BACKOFFINTERVAL; + this.requestRetryBackoffLimit = DEFAULT_REQUEST_RETRY_BACKOFFLIMIT; + this.requestTimeout = DEFAULT_REQUEST_TIMEOUT; + this.scaleTimeout = DEFAULT_SCALE_TIMEOUT; + this.loggingInterval = DEFAULT_LOGGING_INTERVAL; + this.userAgent = "fabric8-kubernetes-client/" + Version.clientVersion(); + this.tlsVersions = new TlsVersion[] { TlsVersion.TLS_1_3, TlsVersion.TLS_1_2 }; + this.requestConfig = new RequestConfig(this.watchReconnectLimit, this.watchReconnectInterval, + this.requestTimeout, this.scaleTimeout, this.loggingInterval, + this.requestRetryBackoffLimit, this.requestRetryBackoffInterval, this.uploadRequestTimeout); + } else { + this.requestConfig = new RequestConfig(watchReconnectLimit, watchReconnectInterval, requestTimeout, scaleTimeout, loggingInterval, requestRetryBackoffLimit, requestRetryBackoffInterval, uploadRequestTimeout); + } + + if (Boolean.TRUE.equals(autoConfigure)) { + autoConfigure(this, null); + } + if (Utils.isNotNullOrEmpty(apiVersion)) { + this.apiVersion = apiVersion; + } + if (Utils.isNotNullOrEmpty(masterUrl)) { + this.masterUrl = masterUrl; + } + if (Utils.isNotNullOrEmpty(namespace)) { + this.namespace = namespace; + } + if (Boolean.TRUE.equals(trustCerts)) { + this.trustCerts = true; + } + if (Boolean.TRUE.equals(disableHostnameVerification)) { + this.disableHostnameVerification = true; + } + if (Utils.isNotNullOrEmpty(caCertFile)) { + this.caCertFile = caCertFile; + } + if (Utils.isNotNullOrEmpty(caCertData)) { + this.caCertData = caCertData; + } + if (Utils.isNotNullOrEmpty(clientCertFile)) { + this.clientCertFile = clientCertFile; + } + if (Utils.isNotNullOrEmpty(clientCertData)) { + this.clientCertData = clientCertData; + } + if (Utils.isNotNullOrEmpty(clientKeyFile)) { + this.clientKeyFile = clientKeyFile; + } + if (Utils.isNotNullOrEmpty(clientKeyData)) { + this.clientKeyData = clientKeyData; + } + if (Utils.isNotNullOrEmpty(clientKeyAlgo)) { + this.clientKeyAlgo = clientKeyAlgo; + } + if (Utils.isNotNullOrEmpty(clientKeyPassphrase)) { + this.clientKeyPassphrase = clientKeyPassphrase; + } + if (Utils.isNotNullOrEmpty(username)) { + this.username = username; + } + if (Utils.isNotNullOrEmpty(password)) { + this.password = password; + } + if (Utils.isNotNullOrEmpty(oauthToken)) { + this.oauthToken = oauthToken; + } + if (websocketPingInterval != null) { + this.websocketPingInterval = websocketPingInterval; + } + if (connectionTimeout != null) { + this.connectionTimeout = connectionTimeout; + } + if (watchReconnectLimit != null) { + setWatchReconnectLimit(watchReconnectLimit); + } + if (watchReconnectInterval != null) { + setWatchReconnectInterval(watchReconnectInterval); + } + if (requestTimeout != null) { + setRequestTimeout(requestTimeout); + } + if (scaleTimeout != null) { + setScaleTimeout(scaleTimeout); + } + if (loggingInterval != null) { + setLoggingInterval(loggingInterval); + } + if (requestRetryBackoffLimit != null) { + setRequestRetryBackoffLimit(requestRetryBackoffLimit); + } + if (requestRetryBackoffInterval != null) { + setRequestRetryBackoffInterval(requestRetryBackoffInterval); + } + if (uploadRequestTimeout != null) { + setUploadRequestTimeout(uploadRequestTimeout); + } + if (Utils.isNotNullOrEmpty(impersonateUsername)) { + setImpersonateUsername(impersonateUsername); + } + if (Utils.isNotNullOrEmpty(impersonateGroups)) { + setImpersonateGroups(impersonateGroups); + } + if (Utils.isNotNullOrEmpty(impersonateExtras)) { + setImpersonateExtras(impersonateExtras); + } + if (http2Disable != null) { + this.http2Disable = http2Disable; + } + if (Utils.isNotNullOrEmpty(httpProxy)) { + this.httpProxy = httpProxy; + } + if (Utils.isNotNullOrEmpty(httpsProxy)) { + this.httpsProxy = httpsProxy; + } + if (Utils.isNotNullOrEmpty(noProxy)) { + this.noProxy = noProxy; + } + if (Utils.isNotNullOrEmpty(proxyUsername)) { + this.proxyUsername = proxyUsername; + } + if (Utils.isNotNullOrEmpty(proxyPassword)) { + this.proxyPassword = proxyPassword; + } + if (Utils.isNotNullOrEmpty(userAgent)) { + this.userAgent = userAgent; + } + if (tlsVersions != null && tlsVersions.length > 0) { + this.tlsVersions = tlsVersions; + } + if (Utils.isNotNullOrEmpty(trustStoreFile)) { + this.trustStoreFile = trustStoreFile; + } + if (Utils.isNotNullOrEmpty(trustStorePassphrase)) { + this.trustStorePassphrase = trustStorePassphrase; + } + if (Utils.isNotNullOrEmpty(keyStoreFile)) { + this.keyStoreFile = keyStoreFile; + } + if (Utils.isNotNullOrEmpty(keyStorePassphrase)) { + this.keyStorePassphrase = keyStorePassphrase; + } + if (maxConcurrentRequests != null) { + this.maxConcurrentRequests = maxConcurrentRequests; + } + if (maxConcurrentRequestsPerHost != null) { + this.maxConcurrentRequestsPerHost = maxConcurrentRequestsPerHost; + } + if (Utils.isNotNullOrEmpty(autoOAuthToken)) { + this.autoOAuthToken = autoOAuthToken; + } + if (Utils.isNotNullOrEmpty(contexts)) { + this.contexts = contexts; + } + if (Utils.isNotNull(currentContext)) { + this.currentContext = currentContext; + } + + if (Utils.isNotNullOrEmpty(this.masterUrl)) { + this.masterUrl = ensureEndsWithSlash(ensureHttps(this.masterUrl, this)); + } + this.autoConfigure = autoConfigure; + this.oauthTokenProvider = oauthTokenProvider; + this.customHeaders = customHeaders; + this.onlyHttpWatches = onlyHttpWatches; + if (Utils.isNotNullOrEmpty(files)) { + this.kubeConfigFiles = files; + } + } + + public static void configFromSysPropsOrEnvVars(Config config) { + config.setTrustCerts(Utils.getSystemPropertyOrEnvVar(KUBERNETES_TRUST_CERT_SYSTEM_PROPERTY, config.isTrustCerts())); + config.setDisableHostnameVerification(Utils.getSystemPropertyOrEnvVar( + KUBERNETES_DISABLE_HOSTNAME_VERIFICATION_SYSTEM_PROPERTY, config.isDisableHostnameVerification())); + config.setMasterUrl(Utils.getSystemPropertyOrEnvVar(KUBERNETES_MASTER_SYSTEM_PROPERTY, config.getMasterUrl())); + config.setApiVersion(Utils.getSystemPropertyOrEnvVar(KUBERNETES_API_VERSION_SYSTEM_PROPERTY, config.getApiVersion())); + config.setNamespace(Utils.getSystemPropertyOrEnvVar(KUBERNETES_NAMESPACE_SYSTEM_PROPERTY, config.getNamespace())); + config + .setCaCertFile(Utils.getSystemPropertyOrEnvVar(KUBERNETES_CA_CERTIFICATE_FILE_SYSTEM_PROPERTY, config.getCaCertFile())); + config + .setCaCertData(Utils.getSystemPropertyOrEnvVar(KUBERNETES_CA_CERTIFICATE_DATA_SYSTEM_PROPERTY, config.getCaCertData())); + config.setClientCertFile( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_CLIENT_CERTIFICATE_FILE_SYSTEM_PROPERTY, config.getClientCertFile())); + config.setClientCertData( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_CLIENT_CERTIFICATE_DATA_SYSTEM_PROPERTY, config.getClientCertData())); + config.setClientKeyFile( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_CLIENT_KEY_FILE_SYSTEM_PROPERTY, config.getClientKeyFile())); + config.setClientKeyData( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_CLIENT_KEY_DATA_SYSTEM_PROPERTY, config.getClientKeyData())); + config.setClientKeyAlgo(getKeyAlgorithm(config.getClientKeyFile(), config.getClientKeyData())); + config.setClientKeyPassphrase(Utils.getSystemPropertyOrEnvVar(KUBERNETES_CLIENT_KEY_PASSPHRASE_SYSTEM_PROPERTY, + new String(config.getClientKeyPassphrase()))); + config.setUserAgent(Utils.getSystemPropertyOrEnvVar(KUBERNETES_USER_AGENT, config.getUserAgent())); + + config.setTrustStorePassphrase( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_TRUSTSTORE_PASSPHRASE_PROPERTY, config.getTrustStorePassphrase())); + config.setTrustStoreFile(Utils.getSystemPropertyOrEnvVar(KUBERNETES_TRUSTSTORE_FILE_PROPERTY, config.getTrustStoreFile())); + config.setKeyStorePassphrase( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_KEYSTORE_PASSPHRASE_PROPERTY, config.getKeyStorePassphrase())); + config.setKeyStoreFile(Utils.getSystemPropertyOrEnvVar(KUBERNETES_KEYSTORE_FILE_PROPERTY, config.getKeyStoreFile())); + + config + .setAutoOAuthToken(Utils.getSystemPropertyOrEnvVar(KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY, config.getAutoOAuthToken())); + config.setUsername(Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_BASIC_USERNAME_SYSTEM_PROPERTY, config.getUsername())); + config.setPassword(Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_BASIC_PASSWORD_SYSTEM_PROPERTY, config.getPassword())); + + config.setImpersonateUsername( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_IMPERSONATE_USERNAME, config.getImpersonateUsername())); + + String configuredImpersonateGroups = Utils.getSystemPropertyOrEnvVar(KUBERNETES_IMPERSONATE_GROUP, Arrays + .stream(Optional.ofNullable(config.getImpersonateGroups()).orElse(new String[0])).collect(Collectors.joining(","))); + if (Utils.isNotNullOrEmpty(configuredImpersonateGroups)) { + config.setImpersonateGroups(configuredImpersonateGroups.split(",")); + } + + String configuredWatchReconnectInterval = Utils + .getSystemPropertyOrEnvVar(KUBERNETES_WATCH_RECONNECT_INTERVAL_SYSTEM_PROPERTY); + if (configuredWatchReconnectInterval != null) { + config.setWatchReconnectInterval(Integer.parseInt(configuredWatchReconnectInterval)); + } + + String configuredWatchReconnectLimit = Utils.getSystemPropertyOrEnvVar(KUBERNETES_WATCH_RECONNECT_LIMIT_SYSTEM_PROPERTY); + if (configuredWatchReconnectLimit != null) { + config.setWatchReconnectLimit(Integer.parseInt(configuredWatchReconnectLimit)); + } + + String configuredScaleTimeout = Utils.getSystemPropertyOrEnvVar(KUBERNETES_SCALE_TIMEOUT_SYSTEM_PROPERTY, + String.valueOf(DEFAULT_SCALE_TIMEOUT)); + if (configuredScaleTimeout != null) { + config.setScaleTimeout(Long.parseLong(configuredScaleTimeout)); + } + + String configuredLoggingInterval = Utils.getSystemPropertyOrEnvVar(KUBERNETES_LOGGING_INTERVAL_SYSTEM_PROPERTY, + String.valueOf(DEFAULT_LOGGING_INTERVAL)); + if (configuredLoggingInterval != null) { + config.setLoggingInterval(Integer.parseInt(configuredLoggingInterval)); + } + + config.setConnectionTimeout( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_CONNECTION_TIMEOUT_SYSTEM_PROPERTY, config.getConnectionTimeout())); + config.setUploadRequestTimeout( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_UPLOAD_REQUEST_TIMEOUT_SYSTEM_PROPERTY, config.getUploadRequestTimeout())); + config.setRequestTimeout( + Utils.getSystemPropertyOrEnvVar(KUBERNETES_REQUEST_TIMEOUT_SYSTEM_PROPERTY, config.getRequestTimeout())); + config.setRequestRetryBackoffLimit(Utils.getSystemPropertyOrEnvVar(KUBERNETES_REQUEST_RETRY_BACKOFFLIMIT_SYSTEM_PROPERTY, + config.getRequestRetryBackoffLimit())); + config.setRequestRetryBackoffInterval(Utils.getSystemPropertyOrEnvVar( + KUBERNETES_REQUEST_RETRY_BACKOFFINTERVAL_SYSTEM_PROPERTY, config.getRequestRetryBackoffInterval())); + + String configuredWebsocketPingInterval = Utils.getSystemPropertyOrEnvVar(KUBERNETES_WEBSOCKET_PING_INTERVAL_SYSTEM_PROPERTY, + String.valueOf(config.getWebsocketPingInterval())); + if (configuredWebsocketPingInterval != null) { + config.setWebsocketPingInterval(Long.parseLong(configuredWebsocketPingInterval)); + } + + String configuredMaxConcurrentRequests = Utils.getSystemPropertyOrEnvVar(KUBERNETES_MAX_CONCURRENT_REQUESTS, + String.valueOf(config.getMaxConcurrentRequests())); + if (configuredMaxConcurrentRequests != null) { + config.setMaxConcurrentRequests(Integer.parseInt(configuredMaxConcurrentRequests)); + } + + String configuredMaxConcurrentReqeustsPerHost = Utils.getSystemPropertyOrEnvVar(KUBERNETES_MAX_CONCURRENT_REQUESTS_PER_HOST, + String.valueOf(config.getMaxConcurrentRequestsPerHost())); + if (configuredMaxConcurrentReqeustsPerHost != null) { + config.setMaxConcurrentRequestsPerHost(Integer.parseInt(configuredMaxConcurrentReqeustsPerHost)); + } + + config.setHttp2Disable(Utils.getSystemPropertyOrEnvVar(KUBERNETES_HTTP2_DISABLE, config.isHttp2Disable())); + + // Only set http(s) proxy fields if they're not set. This is done in order to align behavior of + // KubernetesClient with kubectl / client-go . Please see https://github.com/fabric8io/kubernetes-client/issues/6150 + // Precedence is given to proxy-url read from kubeconfig . + if (Utils.isNullOrEmpty(config.getHttpProxy())) { + config.setHttpProxy(Utils.getSystemPropertyOrEnvVar(KUBERNETES_ALL_PROXY, config.getHttpProxy())); + config.setHttpProxy(Utils.getSystemPropertyOrEnvVar(KUBERNETES_HTTP_PROXY, config.getHttpProxy())); + } + if (Utils.isNullOrEmpty(config.getHttpsProxy())) { + config.setHttpsProxy(Utils.getSystemPropertyOrEnvVar(KUBERNETES_ALL_PROXY, config.getHttpsProxy())); + config.setHttpsProxy(Utils.getSystemPropertyOrEnvVar(KUBERNETES_HTTPS_PROXY, config.getHttpsProxy())); + } + + config.setProxyUsername(Utils.getSystemPropertyOrEnvVar(KUBERNETES_PROXY_USERNAME, config.getProxyUsername())); + config.setProxyPassword(Utils.getSystemPropertyOrEnvVar(KUBERNETES_PROXY_PASSWORD, config.getProxyPassword())); + + String noProxyVar = Utils.getSystemPropertyOrEnvVar(KUBERNETES_NO_PROXY); + if (noProxyVar != null) { + config.setNoProxy(noProxyVar.split(",")); + } + + String tlsVersionsVar = Utils.getSystemPropertyOrEnvVar(KUBERNETES_TLS_VERSIONS); + if (Utils.isNotNullOrEmpty(tlsVersionsVar)) { + String[] tlsVersionsSplit = tlsVersionsVar.split(","); + TlsVersion[] tlsVersions = new TlsVersion[tlsVersionsSplit.length]; + for (int i = 0; i < tlsVersionsSplit.length; i++) { + tlsVersions[i] = TlsVersion.forJavaName(tlsVersionsSplit[i]); + } + config.setTlsVersions(tlsVersions); + } + } + + private static boolean tryServiceAccount(Config config) { + LOGGER.debug("Trying to configure client from service account..."); + String masterHost = Utils.getSystemPropertyOrEnvVar(KUBERNETES_SERVICE_HOST_PROPERTY, (String) null); + String masterPort = Utils.getSystemPropertyOrEnvVar(KUBERNETES_SERVICE_PORT_PROPERTY, (String) null); + String caCertPath = Utils.getSystemPropertyOrEnvVar(KUBERNETES_CA_CERTIFICATE_FILE_SYSTEM_PROPERTY, + KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH); + + if (masterHost != null && masterPort != null) { + String hostPort = joinHostPort(masterHost, masterPort); + LOGGER.debug("Found service account host and port: {}", hostPort); + config.setMasterUrl("https://" + hostPort); + } + if (Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, true)) { + boolean serviceAccountCaCertExists = Files.isRegularFile(new File(caCertPath).toPath()); + if (serviceAccountCaCertExists) { + LOGGER.debug("Found service account ca cert at: [{}}].", caCertPath); + config.setCaCertFile(caCertPath); + } else { + LOGGER.debug("Did not find service account ca cert at: [{}}].", caCertPath); + } + + File saTokenPathFile = findServiceAccountTokenFile(); + if (saTokenPathFile != null) { + String saTokenPathLocation = saTokenPathFile.getAbsolutePath(); + try { + String serviceTokenCandidate = new String(Files.readAllBytes(saTokenPathFile.toPath())); + LOGGER.debug("Found service account token at: [{}].", saTokenPathLocation); + config.setAutoOAuthToken(serviceTokenCandidate); + return true; + } catch (IOException e) { + // No service account token available... + LOGGER.warn("Error reading service account token from: [{}]. Ignoring.", saTokenPathLocation); + } + } + } + return false; + } + + private static File findServiceAccountTokenFile() { + File saTokenPathFile; + String saTokenPath = Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY); + if (saTokenPath != null) { + // if user has set the service account token system property, we use it. + saTokenPathFile = new File(saTokenPath); + } else { + // otherwise, let's try the default location iif the location exists. + saTokenPathFile = new File(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH); + if (!saTokenPathFile.exists()) { + saTokenPathFile = null; + LOGGER.debug("Could not find the service account token at the default location: [{}]. Ignoring.", + KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH); + } + } + + return saTokenPathFile; + } + + private static String joinHostPort(String host, String port) { + if (host.indexOf(':') >= 0) { + // Host is an IPv6 + return "[" + host + "]:" + port; + } + return host + ":" + port; + } + + private static String absolutify(File relativeTo, String filename) { + if (filename == null) { + return null; + } + + File file = new File(filename); + if (file.isAbsolute()) { + return file.getAbsolutePath(); + } + + return new File(relativeTo.getParentFile(), filename).getAbsolutePath(); + } + + public static Config fromKubeconfig(String kubeconfigContents) { + return fromKubeconfig(null, kubeconfigContents, null); + } + + // Note: kubeconfigPath is optional (see note on loadFromKubeConfig) + public static Config fromKubeconfig(String context, String kubeconfigContents, String kubeconfigPath) { + // we allow passing context along here, since downstream accepts it + Config config = new Config(false); + if (kubeconfigPath != null) { + config.setFile(new File(kubeconfigPath)); + } + if (!loadFromKubeconfig(config, context, kubeconfigContents)) { + throw new KubernetesClientException("Could not create Config from kubeconfig"); + } + if (!disableAutoConfig()) { + postAutoConfigure(config); + } + return config; + } + + /** + * Refresh the config from file / env sources. + * Any values that the user have programmatically set will be lost. + * + * @return + */ + public Config refresh() { + final String currentContextName = this.getCurrentContext() != null ? this.getCurrentContext().getName() : null; + if (Utils.isNotNullOrEmpty(this.oauthToken)) { + return this; + } + if (this.autoConfigure) { + return Config.autoConfigure(currentContextName); + } + // 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 this; + } + + private static boolean tryKubeConfig(Config config, String context) { + LOGGER.debug("Trying to configure client from Kubernetes config..."); + if (!Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, true)) { + return false; + } + String[] kubeConfigFilenames = getKubeconfigFilenames(); + if (Utils.isNullOrEmpty(kubeConfigFilenames)) { + 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; + } + 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 = 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; + } + + /** + * 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 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 (kubeConfig != null) { + mergeKubeConfigContents(config, context, kubeConfig); + return true; + } + } catch (IOException ioException) { + throw KubernetesClientException.launderThrowable("Failed to parse the kubeconfig.", ioException); + } + + return false; + } + + private static void mergeKubeConfigContents(Config config, String context, io.fabric8.kubernetes.api.model.Config kubeConfig) + throws IOException { + config.setContexts(kubeConfig.getContexts()); + Context currentContext = setCurrentContext(context, config, kubeConfig); + Cluster currentCluster = KubeConfigUtils.getCluster(kubeConfig, currentContext); + if (currentContext != null) { + config.setNamespace(currentContext.getNamespace()); + } + if (currentCluster != null) { + config.setMasterUrl(currentCluster.getServer()); + config.setTrustCerts(currentCluster.getInsecureSkipTlsVerify() != null && currentCluster.getInsecureSkipTlsVerify()); + config.setDisableHostnameVerification( + currentCluster.getInsecureSkipTlsVerify() != null && currentCluster.getInsecureSkipTlsVerify()); + config.setCaCertData(currentCluster.getCertificateAuthorityData()); + if (currentContext != null) { + NamedAuthInfo currentAuthInfo = KubeConfigUtils.getAuthInfo(kubeConfig, currentContext.getUser()); + mergeKubeConfigAuthInfo(config, currentCluster, currentAuthInfo); + mergeProxyUrl(config, currentCluster.getProxyUrl()); + } + } + } + + 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 { + 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(); + 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.setClientKeyFile(clientKeyFile); + } + + private static void mergeKubeConfigAuthProviderConfig(Config config, AuthInfo currentAuthInfo) { + if (currentAuthInfo.getAuthProvider().getConfig() != null) { + config.setAuthProvider(currentAuthInfo.getAuthProvider()); + if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN))) { + // GKE token + config.setAutoOAuthToken(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN)); + } else if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN))) { + // OpenID Connect token + config.setAutoOAuthToken(currentAuthInfo.getAuthProvider().getConfig().get(ID_TOKEN)); + } + } + } + + private static void mergeKubeConfigExecCredential(Config config, ExecConfig exec, File configFile) + throws IOException { + if (exec != null) { + try { + ExecCredential ec = getExecCredentialFromExecConfig(exec, configFile); + if (ec != null && ec.status != null) { + if (ec.status.token != null) { + config.setAutoOAuthToken(ec.status.token); + } else if (Utils.isNotNullOrEmpty(ec.status.clientCertificateData) + && Utils.isNotNullOrEmpty(ec.status.clientKeyData)) { + config.setClientCertData(ec.status.clientCertificateData); + config.setClientKeyData(ec.status.clientKeyData); + } else { + LOGGER.warn("No token or certificate returned"); + } + } + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw KubernetesClientException.launderThrowable("failure while running exec credential ", interruptedException); + } + } + } + + protected static ExecCredential getExecCredentialFromExecConfig(ExecConfig exec, File configFile) + throws IOException, InterruptedException { + String apiVersion = exec.getApiVersion(); + List env = exec.getEnv(); + // TODO check behavior of tty & stdin + ProcessBuilder pb = new ProcessBuilder( + getAuthenticatorCommandFromExecConfig(exec, configFile, Utils.getSystemPathVariable())); + pb.redirectErrorStream(true); + if (env != null) { + Map environment = pb.environment(); + env.forEach(var -> environment.put(var.getName(), var.getValue())); + } + Process p = pb.start(); + String output; + try (InputStream is = p.getInputStream()) { + output = IOHelpers.readFully(is); + } + if (p.waitFor() != 0) { + LOGGER.warn(output); + } + try { + ExecCredential ec = Serialization.unmarshal(output, ExecCredential.class); + if (!apiVersion.equals(ec.apiVersion)) { + LOGGER.warn("Wrong apiVersion {} vs. {}", ec.apiVersion, apiVersion); + } else { + return ec; + } + } catch (Exception ex) { + LOGGER.warn("Error unmarshalling ExecCredential", ex); + } + return null; + } + + protected static List getAuthenticatorCommandFromExecConfig(ExecConfig exec, File configFile, + String systemPathValue) { + String command = exec.getCommand(); + if (command.contains(File.separator) && !command.startsWith(File.separator) && configFile != null) { + // Appears to be a relative path; normalize. Spec is vague about how to detect this situation. + command = Paths.get(configFile.getAbsolutePath()).resolveSibling(command).normalize().toString(); + } + List argv = new ArrayList<>(Utils.getCommandPlatformPrefix()); + command = getCommandWithFullyQualifiedPath(command, systemPathValue); + + command = shellQuote(command); + + List args = exec.getArgs(); + if (Utils.isNotNullOrEmpty(args)) { + command += " " + args + .stream() + .map(Config::shellQuote) + .collect(Collectors.joining(" ")); + } + argv.add(command); + return argv; + } + + private static String shellQuote(String value) { + if (value.contains(" ") || value.contains("\"") || value.contains("'")) { + return "\"" + value.replace("\"", "\\\"") + "\""; + } + return value; + } + + protected static String getCommandWithFullyQualifiedPath(String command, String pathValue) { + String[] pathParts = pathValue.split(File.pathSeparator); + + // Iterate through path in order to find executable file + for (String pathPart : pathParts) { + File commandFile = new File(pathPart + File.separator + command); + if (commandFile.exists()) { + return commandFile.getAbsolutePath(); + } + } + + return command; + } + + 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; + NamedContext currentNamedContext = KubeConfigUtils.getCurrentContext(kubeConfig); + if (currentNamedContext != null) { + config.setCurrentContext(currentNamedContext); + currentContext = currentNamedContext.getContext(); + } + return currentContext; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class ExecCredential { + public String kind; + public String apiVersion; + public ExecCredentialSpec spec; + public ExecCredentialStatus status; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class ExecCredentialSpec { + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class ExecCredentialStatus { + public String token; + public String clientCertificateData; + public String clientKeyData; + // TODO expirationTimestamp + } + + private static boolean tryNamespaceFromPath(Config config) { + LOGGER.debug("Trying to configure client namespace from Kubernetes service account namespace path..."); + if (Utils.getSystemPropertyOrEnvVar(KUBERNETES_TRYNAMESPACE_PATH_SYSTEM_PROPERTY, true)) { + String serviceAccountNamespace = Utils.getSystemPropertyOrEnvVar(KUBERNETES_NAMESPACE_FILE, KUBERNETES_NAMESPACE_PATH); + boolean serviceAccountNamespaceExists = Files.isRegularFile(new File(serviceAccountNamespace).toPath()); + if (serviceAccountNamespaceExists) { + LOGGER.debug("Found service account namespace at: [{}].", serviceAccountNamespace); + try { + String namespace = new String(Files.readAllBytes(new File(serviceAccountNamespace).toPath())); + config.setNamespace(namespace.replace(System.lineSeparator(), "")); + return true; + } catch (IOException e) { + LOGGER.error("Error reading service account namespace from: [" + serviceAccountNamespace + "].", e); + } + } else { + LOGGER.debug("Did not find service account namespace at: [{}]. Ignoring.", serviceAccountNamespace); + } + } + return false; + } + + protected static String getHomeDir(Predicate directoryExists, UnaryOperator getEnvVar) { + String home = getEnvVar.apply("HOME"); + if (Utils.isNotNullOrEmpty(home) && directoryExists.test(home)) { + return home; + } + String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); + if (osName.startsWith("win")) { + String homeDrive = getEnvVar.apply("HOMEDRIVE"); + String homePath = getEnvVar.apply("HOMEPATH"); + if (homeDrive != null && !homeDrive.isEmpty() && homePath != null && !homePath.isEmpty()) { + String homeDir = homeDrive + homePath; + if (directoryExists.test(homeDir)) { + return homeDir; + } + } + String userProfile = getEnvVar.apply("USERPROFILE"); + if (Utils.isNotNullOrEmpty(userProfile) && directoryExists.test(userProfile)) { + return userProfile; + } + } + + // Fall back to user.home should never really get here + return System.getProperty("user.home", "."); + } + + public static String getKeyAlgorithm(InputStream inputStream) throws IOException { + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + String line, algorithm = null; + + while ((line = bufferedReader.readLine()) != null) { + if (line.contains("BEGIN EC PRIVATE KEY")) + algorithm = "EC"; + else if (line.contains("BEGIN RSA PRIVATE KEY")) { + algorithm = "RSA"; + } + } + return algorithm; + } + } + + public static String getKeyAlgorithm(String clientKeyFile, String clientKeyData) { + // Check if any system property is set + if (Utils.getSystemPropertyOrEnvVar(KUBERNETES_CLIENT_KEY_ALGO_SYSTEM_PROPERTY) != null) { + return Utils.getSystemPropertyOrEnvVar(KUBERNETES_CLIENT_KEY_ALGO_SYSTEM_PROPERTY); + } + + // Detect algorithm + try { + if (clientKeyData != null || clientKeyFile != null) { + ByteArrayInputStream keyInputStream = CertUtils.getInputStreamFromDataOrFile(clientKeyData, clientKeyFile); + return getKeyAlgorithm(keyInputStream); + } + } catch (IOException exception) { + LOGGER.debug("Failure in determining private key algorithm type, defaulting to RSA {}", exception.getMessage()); + } + return null; + } + + @JsonProperty("oauthToken") + public String getOauthToken() { + return oauthToken; + } + + public void setOauthToken(String oauthToken) { + this.oauthToken = oauthToken; + } + + @JsonProperty("password") + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @JsonProperty("username") + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @JsonProperty("impersonateUsername") + public String getImpersonateUsername() { + return getRequestConfig().getImpersonateUsername(); + } + + public void setImpersonateUsername(String impersonateUsername) { + this.requestConfig.setImpersonateUsername(impersonateUsername); + } + + @JsonProperty("impersonateGroups") + public String[] getImpersonateGroups() { + return getRequestConfig().getImpersonateGroups(); + } + + public void setImpersonateGroups(String... impersonateGroup) { + this.requestConfig.setImpersonateGroups(impersonateGroup); + } + + @JsonProperty("impersonateExtras") + public Map> getImpersonateExtras() { + return getRequestConfig().getImpersonateExtras(); + } + + public void setImpersonateExtras(Map> impersonateExtras) { + this.requestConfig.setImpersonateExtras(impersonateExtras); + } + + @JsonProperty("clientKeyPassphrase") + public String getClientKeyPassphrase() { + return clientKeyPassphrase; + } + + public void setClientKeyPassphrase(String clientKeyPassphrase) { + this.clientKeyPassphrase = clientKeyPassphrase; + } + + @JsonProperty("clientKeyAlgo") + public String getClientKeyAlgo() { + return clientKeyAlgo; + } + + public void setClientKeyAlgo(String clientKeyAlgo) { + this.clientKeyAlgo = clientKeyAlgo; + } + + @JsonProperty("clientKeyData") + public String getClientKeyData() { + return clientKeyData; + } + + public void setClientKeyData(String clientKeyData) { + this.clientKeyData = clientKeyData; + } + + @JsonProperty("clientKeyFile") + public String getClientKeyFile() { + return clientKeyFile; + } + + public void setClientKeyFile(String clientKeyFile) { + this.clientKeyFile = clientKeyFile; + } + + @JsonProperty("clientCertData") + public String getClientCertData() { + return clientCertData; + } + + public void setClientCertData(String clientCertData) { + this.clientCertData = clientCertData; + } + + @JsonProperty("clientCertFile") + public String getClientCertFile() { + return clientCertFile; + } + + public void setClientCertFile(String clientCertFile) { + this.clientCertFile = clientCertFile; + } + + @JsonProperty("caCertData") + public String getCaCertData() { + return caCertData; + } + + public void setCaCertData(String caCertData) { + this.caCertData = caCertData; + } + + @JsonProperty("caCertFile") + public String getCaCertFile() { + return caCertFile; + } + + public void setCaCertFile(String caCertFile) { + this.caCertFile = caCertFile; + } + + @JsonProperty("apiVersion") + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + + @JsonProperty("masterUrl") + public String getMasterUrl() { + return masterUrl; + } + + public void setMasterUrl(String masterUrl) { + //We set the masterUrl because it's needed by ensureHttps + this.masterUrl = masterUrl; + this.masterUrl = ensureEndsWithSlash(ensureHttps(masterUrl, this)); + } + + @JsonProperty("trustCerts") + public boolean isTrustCerts() { + return Optional.ofNullable(trustCerts).orElse(false); + } + + Boolean getTrustCerts() { + return trustCerts; + } + + public void setTrustCerts(boolean trustCerts) { + this.trustCerts = trustCerts; + } + + @JsonProperty("disableHostnameVerification") + public boolean isDisableHostnameVerification() { + return Optional.ofNullable(disableHostnameVerification).orElse(false); + } + + boolean getDisableHostnameVerification() { + return disableHostnameVerification; + } + + public void setDisableHostnameVerification(boolean disableHostnameVerification) { + this.disableHostnameVerification = disableHostnameVerification; + } + + @JsonProperty("watchReconnectInterval") + public int getWatchReconnectInterval() { + return requestConfig.getWatchReconnectInterval(); + } + + public void setWatchReconnectInterval(int watchReconnectInterval) { + this.requestConfig.setWatchReconnectInterval(watchReconnectInterval); + } + + @JsonProperty("watchReconnectLimit") + public int getWatchReconnectLimit() { + return getRequestConfig().getWatchReconnectLimit(); + } + + public void setWatchReconnectLimit(int watchReconnectLimit) { + this.requestConfig.setWatchReconnectLimit(watchReconnectLimit); + } + + public Map getErrorMessages() { + return errorMessages; + } + + public void setErrorMessages(Map errorMessages) { + this.errorMessages = errorMessages; + } + + public static ConfigBuilder builder() { + return new ConfigBuilder(); + } + + @JsonProperty("connectionTimeout") + public int getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + @JsonProperty("uploadRequestTimeout") + public int getUploadRequestTimeout() { + return getRequestConfig().getUploadRequestTimeout(); + } + + public void setUploadRequestTimeout(int requestTimeout) { + this.requestConfig.setUploadRequestTimeout(requestTimeout); + } + + @JsonProperty("requestTimeout") + public int getRequestTimeout() { + return getRequestConfig().getRequestTimeout(); + } + + public void setRequestTimeout(int requestTimeout) { + this.requestConfig.setRequestTimeout(requestTimeout); + } + + @JsonProperty("requestRetryBackoffLimit") + public int getRequestRetryBackoffLimit() { + return getRequestConfig().getRequestRetryBackoffLimit(); + } + + public void setRequestRetryBackoffLimit(int requestRetryBackoffLimit) { + requestConfig.setRequestRetryBackoffLimit(requestRetryBackoffLimit); + } + + @JsonProperty("requestRetryBackoffInterval") + public int getRequestRetryBackoffInterval() { + return getRequestConfig().getRequestRetryBackoffInterval(); + } + + public void setRequestRetryBackoffInterval(int requestRetryBackoffInterval) { + requestConfig.setRequestRetryBackoffInterval(requestRetryBackoffInterval); + } + + @JsonProperty("scaleTimeout") + public long getScaleTimeout() { + return getRequestConfig().getScaleTimeout(); + } + + public void setScaleTimeout(long scaleTimeout) { + this.requestConfig.setScaleTimeout(scaleTimeout); + } + + @JsonProperty("loggingInterval") + public int getLoggingInterval() { + return getRequestConfig().getLoggingInterval(); + } + + public void setLoggingInterval(int loggingInterval) { + this.requestConfig.setLoggingInterval(loggingInterval); + } + + @JsonProperty("http2Disable") + public boolean isHttp2Disable() { + return Optional.ofNullable(http2Disable).orElse(false); + } + + Boolean getHttp2Disable() { + return http2Disable; + } + + public void setHttp2Disable(Boolean http2Disable) { + this.http2Disable = http2Disable; + } + + public void setHttpProxy(String httpProxy) { + this.httpProxy = httpProxy; + } + + @JsonProperty("httpProxy") + public String getHttpProxy() { + return httpProxy; + } + + public void setHttpsProxy(String httpsProxy) { + this.httpsProxy = httpsProxy; + } + + @JsonProperty("httpsProxy") + public String getHttpsProxy() { + return httpsProxy; + } + + public void setNoProxy(String[] noProxy) { + this.noProxy = noProxy; + } + + @JsonProperty("noProxy") + public String[] getNoProxy() { + return noProxy; + } + + @JsonProperty("namespace") + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + @JsonProperty("defaultNamespace") + public boolean isDefaultNamespace() { + return Optional.ofNullable(defaultNamespace).orElse(true); + } + + public void setDefaultNamespace(boolean defaultNamespace) { + this.defaultNamespace = defaultNamespace; + } + + @JsonProperty("userAgent") + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } + + @JsonProperty("tlsVersions") + public TlsVersion[] getTlsVersions() { + return tlsVersions; + } + + public void setTlsVersions(TlsVersion[] tlsVersions) { + this.tlsVersions = tlsVersions; + } + + @JsonProperty("websocketPingInterval") + public long getWebsocketPingInterval() { + return websocketPingInterval; + } + + public void setWebsocketPingInterval(Long websocketPingInterval) { + this.websocketPingInterval = websocketPingInterval; + } + + public int getMaxConcurrentRequests() { + return maxConcurrentRequests; + } + + public void setMaxConcurrentRequests(int maxConcurrentRequests) { + this.maxConcurrentRequests = maxConcurrentRequests; + } + + public int getMaxConcurrentRequestsPerHost() { + return maxConcurrentRequestsPerHost; + } + + public void setMaxConcurrentRequestsPerHost(int maxConcurrentRequestsPerHost) { + this.maxConcurrentRequestsPerHost = maxConcurrentRequestsPerHost; + } + + @JsonProperty("proxyUsername") + public String getProxyUsername() { + return proxyUsername; + } + + public void setProxyUsername(String proxyUsername) { + this.proxyUsername = proxyUsername; + } + + @JsonProperty("proxyPassword") + public String getProxyPassword() { + return proxyPassword; + } + + public void setProxyPassword(String proxyPassword) { + this.proxyPassword = proxyPassword; + } + + public RequestConfig getRequestConfig() { + return this.requestConfig; + } + + public void setTrustStorePassphrase(String trustStorePassphrase) { + this.trustStorePassphrase = trustStorePassphrase; + } + + @JsonProperty("trustStorePassphrase") + public String getTrustStorePassphrase() { + return trustStorePassphrase; + } + + public void setKeyStorePassphrase(String keyStorePassphrase) { + this.keyStorePassphrase = keyStorePassphrase; + } + + @JsonProperty("keyStorePassphrase") + public String getKeyStorePassphrase() { + return keyStorePassphrase; + } + + public void setTrustStoreFile(String trustStoreFile) { + this.trustStoreFile = trustStoreFile; + } + + @JsonProperty("trustStoreFile") + public String getTrustStoreFile() { + return trustStoreFile; + } + + public void setKeyStoreFile(String keyStoreFile) { + this.keyStoreFile = keyStoreFile; + } + + @JsonProperty("keyStoreFile") + public String getKeyStoreFile() { + return keyStoreFile; + } + + @JsonIgnore + public OAuthTokenProvider getOauthTokenProvider() { + return this.oauthTokenProvider; + } + + public void setOauthTokenProvider(OAuthTokenProvider oauthTokenProvider) { + this.oauthTokenProvider = oauthTokenProvider; + } + + @JsonProperty("customHeaders") + public Map getCustomHeaders() { + return customHeaders; + } + + public void setCustomHeaders(Map customHeaders) { + this.customHeaders = customHeaders; + } + + public boolean getAutoConfigure() { + return autoConfigure; + } + + /** + * Returns all the {@link NamedContext}s that exist in the kube config + * + * @return all the contexts + * + * @see NamedContext + */ + public List getContexts() { + return contexts; + } + + public void setContexts(List contexts) { + this.contexts = contexts; + } + + /** + * Returns the current context that's defined in the kube config. Returns {@code null} if there's none + * + * @return the current context + * + * @see NamedContext + */ + public NamedContext getCurrentContext() { + return currentContext; + } + + public void setCurrentContext(NamedContext context) { + this.currentContext = context; + } + + /** + * + * Returns the path to the file that this configuration was loaded from. Returns {@code null} if no file was used. + * + * @return the kubeConfig file + */ + public File getFile() { + 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 + public Readiness getReadiness() { + return Readiness.getInstance(); + } + + public void setAuthProvider(AuthProviderConfig authProvider) { + this.authProvider = authProvider; + } + + public AuthProviderConfig getAuthProvider() { + return authProvider; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + public void setFile(File file) { + setFiles(Collections.singletonList(file)); + } + + public void setFiles(List files) { + if (Utils.isNullOrEmpty(files)) { + this.kubeConfigFiles = Collections.emptyList(); + } else { + this.kubeConfigFiles = files.stream() + .map(KubeConfigFile::new) + .collect(Collectors.toList()); + } + } + + 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 + */ + 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) { + this.autoConfigure = autoConfigure; + } + + public String getAutoOAuthToken() { + return autoOAuthToken; + } + + public void setAutoOAuthToken(String autoOAuthToken) { + this.autoOAuthToken = autoOAuthToken; + } + + public boolean isOnlyHttpWatches() { + return Optional.ofNullable(onlyHttpWatches).orElse(false); + } + + public void setOnlyHttpWatches(boolean onlyHttpWatches) { + this.onlyHttpWatches = onlyHttpWatches; + } + +} diff --git a/src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java b/src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java new file mode 100644 index 000000000..9c223e9ed --- /dev/null +++ b/src/main/java/io/fabric8/kubernetes/client/KubeConfigFile.java @@ -0,0 +1,84 @@ +/* + * 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.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); + + private final File file; + private boolean parsed = false; + private Config config; + + public KubeConfigFile(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 File getFile() { + return file; + } + + public Config getConfig() { + if (!parsed) { + this.config = createConfig(file); + this.parsed = true; + } + return config; + } + + private Config createConfig(File file) { + Config config = null; + try { + if (isReadable(file)) { + LOGGER.debug("Found for Kubernetes config at: [{}].", file.getPath()); + config = KubeConfigUtils.parseConfig(file); + } + } catch (IOException e) { + LOGGER.debug("Kubernetes file at [{}] is not a valid config. Ignoring.", file.getPath(), e); + } + return config; + } + + 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/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java b/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java new file mode 100644 index 000000000..4f017dd1a --- /dev/null +++ b/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java @@ -0,0 +1,244 @@ +/* + * 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.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.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +/** + * Helper class for working with the YAML config file thats located in + * ~/.kube/config which is updated when you use commands + * like osc login and osc project myproject + */ +public class KubeConfigUtils { + private KubeConfigUtils() { + } + + public static Config parseConfig(File file) throws IOException { + return Serialization.unmarshal(Files.newInputStream(file.toPath()), Config.class); + } + + public static Config parseConfigFromString(String contents) { + return Serialization.unmarshal(contents, Config.class); + } + + /** + * Returns the current context in the given config + * + * @param config Config object + * @return returns context in config if found, otherwise null + */ + 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) { + context = contexts.stream() + .filter(toInspect -> name.equals(toInspect.getName())) + .findAny() + .orElse(null); + } + } + return context; + } + + /** + * 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) { + 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 + * + * @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) { + Cluster cluster = null; + if (config != null && context != null) { + String clusterName = context.getCluster(); + if (clusterName != null) { + List clusters = config.getClusters(); + if (clusters != null) { + cluster = clusters.stream() + .filter(c -> c.getName().equals(clusterName)) + .findAny() + .map(NamedCluster::getCluster) + .orElse(null); + } + } + } + return cluster; + } + + /** + * 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 + */ + 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; + } + } + 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, String kubeConfigPath) throws IOException { + try (FileWriter writer = new FileWriter(kubeConfigPath)) { + 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/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java b/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java new file mode 100644 index 000000000..030ace7eb --- /dev/null +++ b/src/main/java/io/fabric8/kubernetes/client/utils/OpenIDConnectionUtils.java @@ -0,0 +1,362 @@ +/* + * 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.utils; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +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; +import io.fabric8.kubernetes.client.internal.KubeConfigUtils; +import io.fabric8.kubernetes.client.internal.SSLUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.TrustManager; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.time.Instant; +import java.util.Base64; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * Utility class for OpenID token refresh. + */ +public class OpenIDConnectionUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(OpenIDConnectionUtils.class); + + public static final String ID_TOKEN_KUBECONFIG = "id-token"; + public static final String ISSUER_KUBECONFIG = "idp-issuer-url"; + public static final String REFRESH_TOKEN_KUBECONFIG = "refresh-token"; + private static final String REFRESH_TOKEN_PARAM = "refresh_token"; + public static final String GRANT_TYPE_PARAM = "grant_type"; + public static final String CLIENT_ID_PARAM = "client_id"; + public static final String CLIENT_SECRET_PARAM = "client_secret"; + public static final String CLIENT_ID_KUBECONFIG = "client-id"; + public static final String CLIENT_SECRET_KUBECONFIG = "client-secret"; + private static final String IDP_CERT_DATA = "idp-certificate-authority-data"; + private static final String WELL_KNOWN_OPENID_CONFIGURATION = ".well-known/openid-configuration"; + private static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token"; + private static final String JWT_TOKEN_EXPIRY_TIMESTAMP_KEY = "exp"; + private static final String JWT_PARTS_DELIMITER_REGEX = "\\."; + private static final int TOKEN_EXPIRY_DELTA = 10; + + private OpenIDConnectionUtils() { + } + + /** + * Fetch OpenID Connect token from Kubeconfig, check whether it's still valid or not; If expired handle + * token refresh with OpenID Connection provider APIs + * + * @param currentAuthProviderConfig current AuthInfo's AuthProvider config as a map + * @return access token for interacting with Kubernetes API + */ + public static CompletableFuture resolveOIDCTokenFromAuthConfig( + Config currentConfig, Map currentAuthProviderConfig, HttpClient.Builder clientBuilder) { + String originalToken = currentAuthProviderConfig.get(ID_TOKEN_KUBECONFIG); + String idpCert = currentAuthProviderConfig.getOrDefault(IDP_CERT_DATA, getClientCertDataFromConfig(currentConfig)); + if (isTokenRefreshSupported(currentAuthProviderConfig)) { + final HttpClient httpClient = initHttpClientWithPemCert(idpCert, clientBuilder); + final CompletableFuture result = getOpenIdConfiguration(httpClient, currentAuthProviderConfig) + .thenCompose(openIdConfiguration -> refreshOpenIdToken(httpClient, currentAuthProviderConfig, openIdConfiguration)) + .thenApply(oAuthToken -> persistOAuthToken(currentConfig, oAuthToken, null)) + .thenApply(oAuthToken -> { + if (oAuthToken == null || Utils.isNullOrEmpty(oAuthToken.idToken)) { + LOGGER.warn("token response did not contain an id_token, either the scope \\\"openid\\\" wasn't " + + "requested upon login, or the provider doesn't support id_tokens as part of the refresh response."); + return originalToken; + } + return oAuthToken.idToken; + }); + result.whenComplete((s, t) -> httpClient.close()); + return result; + } + return CompletableFuture.completedFuture(originalToken); + } + + /** + * Whether we should try to do token refresh or not, checks whether refresh-token key is set in + * HashMap or not + * + * @param currentAuthProviderConfig a HashMap of current AuthProvider configuration + * @return boolean value whether refresh-token is present or not. + */ + static boolean isTokenRefreshSupported(Map currentAuthProviderConfig) { + return Utils.isNotNull(currentAuthProviderConfig.get(REFRESH_TOKEN_KUBECONFIG)); + } + + /** + * OpenID providers publish their metadata at a well-known URL which looks like this: + * https://[base-server-url]/.well-known/openid-configuration + * + * This method performs an HTTP GET at this public URL and fetches response as a Map. + * + * @param client HttpClient for doing HTTP Get to well known URL of OpenID provider + * @param authProviderConfig OpenID Connect provider information + * @return the OpenID Configuration as returned by the OpenID provider + */ + private static CompletableFuture getOpenIdConfiguration(HttpClient client, + Map authProviderConfig) { + final HttpRequest request = client.newHttpRequestBuilder() + .uri(resolveWellKnownUrlForOpenIDIssuer(authProviderConfig)).build(); + return client.sendAsync(request, String.class).thenApply(response -> { + try { + if (response.isSuccessful() && response.body() != null) { + return Serialization.unmarshal(response.body(), OpenIdConfiguration.class); + } else { + // Don't produce an error that's too huge (e.g. if we get HTML back for some reason). + String responseBody = response.body(); + LOGGER.warn("oidc: failed to query metadata endpoint: {} {}", response.code(), responseBody); + } + } catch (Exception e) { + LOGGER.warn("Could not refresh OIDC token, failure in getting refresh URL", e); + } + return null; + }); + } + + /** + * Issue Token Refresh HTTP Request to OIDC Provider + */ + private static CompletableFuture refreshOpenIdToken( + HttpClient httpClient, Map authProviderConfig, OpenIdConfiguration openIdConfiguration) { + if (openIdConfiguration == null || Utils.isNullOrEmpty(openIdConfiguration.tokenEndpoint)) { + LOGGER.warn("oidc: discovery object doesn't contain a valid token endpoint: {}", openIdConfiguration); + return CompletableFuture.completedFuture(null); + } + final HttpRequest request = initTokenRefreshHttpRequest(httpClient, authProviderConfig, + openIdConfiguration.tokenEndpoint); + return httpClient.sendAsync(request, String.class).thenApply(r -> { + String body = r.body(); + if (body != null) { + // Get response body as string + if (r.isSuccessful()) { + // Deserialize response body into a Map and return + try { + return Serialization.unmarshal(body, OAuthToken.class); + } catch (Exception e) { + LOGGER.warn("Failure in fetching refresh token: ", e); + } + } else { + // Log error response body + LOGGER.warn("Response: {}", body); + } + } + return null; + }); + } + + /** + * Save Updated Access and Refresh token in local KubeConfig file and in-memory Config object. + * + * @param currentConfig current Config object. + * @param oAuthToken OAuth token information as received from OpenID provider. + * @param token new token to be persisted in KubeConfig (if not null). + * @return the oAuthToken for chaining and further processing. + */ + public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAuthToken, String token) { + final Map authProviderConfig = new HashMap<>(); + if (oAuthToken != null) { + authProviderConfig.put(ID_TOKEN_KUBECONFIG, oAuthToken.idToken); + authProviderConfig.put(REFRESH_TOKEN_KUBECONFIG, oAuthToken.refreshToken); + persistOAuthTokenToFile(currentConfig.getAuthProvider(), authProviderConfig); + } + 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 String userName = currentConfig.getCurrentContext().getContext().getUser(); + 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; + } + final NamedAuthInfo namedAuthInfo = getOrCreateNamedAuthInfo(userName, kubeConfigFile.getConfig()); + setAuthProviderAndToken(token, authProviderConfig, namedAuthInfo); + + KubeConfigUtils.persistKubeConfigIntoFile(kubeConfigFile.getConfig(), kubeConfigFile.getFile().getAbsolutePath()); + } catch (IOException ex) { + LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG", ex); + } + } + } + + private static void setAuthProviderAndToken(String token, Map authProviderConfig, + NamedAuthInfo namedAuthInfo) { + if (namedAuthInfo.getUser() == null) { + namedAuthInfo.setUser(new AuthInfo()); + } + if (namedAuthInfo.getUser().getAuthProvider() == null) { + namedAuthInfo.getUser().setAuthProvider(new AuthProviderConfig()); + } + namedAuthInfo.getUser().getAuthProvider().getConfig().putAll(authProviderConfig); + if (Utils.isNotNullOrEmpty(token)) { + namedAuthInfo.getUser().setToken(token); + } + } + + 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)); + } + + /** + * Well known URL for getting OpenID Connect metadata. + * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig + * + * @param authProviderConfig containing the issuing authority URL + * @return well known URL for corresponding OpenID provider + */ + private static String resolveWellKnownUrlForOpenIDIssuer(Map authProviderConfig) { + return URLUtils.join(authProviderConfig.get(ISSUER_KUBECONFIG), "/", WELL_KNOWN_OPENID_CONFIGURATION); + } + + private static HttpClient initHttpClientWithPemCert(String idpCert, HttpClient.Builder clientBuilder) { + // fist, lets get the pem + String pemCert = new String(java.util.Base64.getDecoder().decode(idpCert)); + try { + final TrustManager[] trustManagers = SSLUtils.trustManagers(pemCert, null, false, null, null); + final KeyManager[] keyManagers = SSLUtils.keyManagers(pemCert, null, null, null, null, null, null, null); + + clientBuilder.sslContext(keyManagers, trustManagers); + return clientBuilder.build(); + } catch (KeyStoreException | InvalidKeySpecException | NoSuchAlgorithmException | IOException | UnrecoverableKeyException + | CertificateException e) { + throw KubernetesClientException.launderThrowable("Could not import idp certificate", e); + } + } + + private static HttpRequest initTokenRefreshHttpRequest( + HttpClient client, Map authProviderConfig, String tokenRefreshUrl) { + + final String clientId = authProviderConfig.get(CLIENT_ID_KUBECONFIG); + final String clientSecret = authProviderConfig.getOrDefault(CLIENT_SECRET_KUBECONFIG, ""); + final HttpRequest.Builder httpRequestBuilder = client.newHttpRequestBuilder().uri(tokenRefreshUrl); + final String credentials = java.util.Base64.getEncoder().encodeToString((clientId + ':' + clientSecret) + .getBytes(StandardCharsets.UTF_8)); + httpRequestBuilder.header("Authorization", "Basic " + credentials); + + final Map requestBody = new LinkedHashMap<>(); + requestBody.put(REFRESH_TOKEN_PARAM, authProviderConfig.get(REFRESH_TOKEN_KUBECONFIG)); + requestBody.put(GRANT_TYPE_PARAM, GRANT_TYPE_REFRESH_TOKEN); + requestBody.put(CLIENT_ID_PARAM, clientId); + requestBody.put(CLIENT_SECRET_PARAM, clientSecret); + + httpRequestBuilder.post(requestBody); + return httpRequestBuilder.build(); + } + + public static boolean idTokenExpired(Config config) { + if (config.getAuthProvider() != null && config.getAuthProvider().getConfig() != null) { + Map authProviderConfig = config.getAuthProvider().getConfig(); + String accessToken = authProviderConfig.get(ID_TOKEN_KUBECONFIG); + if (isValidJwt(accessToken)) { + try { + String[] jwtParts = accessToken.split(JWT_PARTS_DELIMITER_REGEX); + String jwtPayload = jwtParts[1]; + String jwtPayloadDecoded = new String(Base64.getDecoder().decode(jwtPayload)); + Map jwtPayloadMap = Serialization.unmarshal(jwtPayloadDecoded, Map.class); + int expiryTimestampInSeconds = (Integer) jwtPayloadMap.get(JWT_TOKEN_EXPIRY_TIMESTAMP_KEY); + return Instant.ofEpochSecond(expiryTimestampInSeconds) + .minusSeconds(TOKEN_EXPIRY_DELTA) + .isBefore(Instant.now()); + } catch (Exception e) { + return true; + } + } + } + return true; + } + + private static boolean isValidJwt(String token) { + if (token != null && !token.isEmpty()) { + String[] jwtParts = token.split(JWT_PARTS_DELIMITER_REGEX); + return jwtParts.length == 3; + } + return false; + } + + private static String getClientCertDataFromConfig(Config config) { + if (config.getCaCertData() != null && !config.getCaCertData().isEmpty()) { + return config.getCaCertData(); + } + try { + if (config.getCaCertFile() != null) { + return java.util.Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(config.getCaCertFile()))); + } + } catch (IOException e) { + LOGGER.debug("Failure in reading certificate data from {}", config.getCaCertFile()); + } + return null; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class OpenIdConfiguration { + @JsonProperty("token_endpoint") + private String tokenEndpoint; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class OAuthToken { + @JsonProperty("id_token") + private String idToken; + @JsonProperty("refresh_token") + private String refreshToken; + } +} diff --git a/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java b/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java new file mode 100644 index 000000000..8db382b5c --- /dev/null +++ b/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java @@ -0,0 +1,565 @@ +/* + * 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.utils; + +import io.fabric8.kubernetes.api.Pluralize; +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.annotation.Annotation; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +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; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.LongSupplier; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class Utils { + + private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); + private static final String ALL_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"; + public static final String WINDOWS = "win"; + public static final String OS_NAME = "os.name"; + public static final String PATH_WINDOWS = "Path"; + public static final String PATH_UNIX = "PATH"; + private static final Random random = new Random(); + private static final AtomicLong leastSigBits = new AtomicLong(); + + private static final CachedSingleThreadScheduler SHARED_SCHEDULER = new CachedSingleThreadScheduler(); + + private Utils() { + } + + public static T checkNotNull(T ref, String message) { + if (ref == null) { + throw new NullPointerException(message); + } + return ref; + } + + public static String getSystemPropertyOrEnvVar(String systemPropertyName, String envVarName, String defaultValue) { + String answer = System.getProperty(systemPropertyName); + if (isNotNullOrEmpty(answer)) { + return answer; + } + + answer = System.getenv(envVarName); + if (isNotNullOrEmpty(answer)) { + return answer; + } + + return defaultValue; + } + + public static String convertSystemPropertyNameToEnvVar(String systemPropertyName) { + return systemPropertyName.toUpperCase(Locale.ROOT).replaceAll("[.-]", "_"); + } + + public static String getEnvVar(String envVarName, String defaultValue) { + String answer = System.getenv(envVarName); + return isNotNullOrEmpty(answer) ? answer : defaultValue; + } + + public static String getSystemPropertyOrEnvVar(String systemPropertyName, String defaultValue) { + return getSystemPropertyOrEnvVar(systemPropertyName, convertSystemPropertyNameToEnvVar(systemPropertyName), defaultValue); + } + + public static String getSystemPropertyOrEnvVar(String systemPropertyName) { + return getSystemPropertyOrEnvVar(systemPropertyName, (String) null); + } + + public static boolean getSystemPropertyOrEnvVar(String systemPropertyName, Boolean defaultValue) { + String result = getSystemPropertyOrEnvVar(systemPropertyName, defaultValue.toString()); + return Boolean.parseBoolean(result); + } + + public static int getSystemPropertyOrEnvVar(String systemPropertyName, int defaultValue) { + String result = getSystemPropertyOrEnvVar(systemPropertyName, Integer.toString(defaultValue)); + return Integer.parseInt(result); + } + + public static String join(final Object[] array) { + return join(array, ','); + } + + public static String join(final Object[] array, final char separator) { + if (array == null) { + return null; + } + if (array.length == 0) { + return ""; + } + final StringBuilder buf = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + if (i > 0) { + buf.append(separator); + } + if (array[i] != null) { + buf.append(array[i]); + } + } + return buf.toString(); + } + + /** + * Wait until another thread signals the completion of a task. + * If an exception is passed, it will be propagated to the caller. + * + * @param future The communication channel. + * @param amount The amount of time to wait. If less than 0, wait indefinitely + * @param timeUnit The time unit. + * + * @return a boolean value indicating resource is ready or not. + */ + public static boolean waitUntilReady(Future future, long amount, TimeUnit timeUnit) { + try { + if (amount < 0) { + future.get(); + } else { + future.get(amount, timeUnit); + } + return true; + } catch (TimeoutException e) { + return false; + } catch (ExecutionException e) { + Throwable t = e; + if (e.getCause() != null) { + t = e.getCause(); + } + t.addSuppressed(new Throwable("waiting here")); + throw KubernetesClientException.launderThrowable(t); + } catch (Exception e) { + throw KubernetesClientException.launderThrowable(e); + } + } + + /** + * Similar to {@link #waitUntilReady(Future, long, TimeUnit)}, but will always throw an exception if not ready + */ + public static void waitUntilReadyOrFail(Future future, long amount, TimeUnit timeUnit) { + if (!waitUntilReady(future, amount, timeUnit)) { + throw KubernetesClientException.launderThrowable(new TimeoutException("not ready after " + amount + " " + timeUnit)); + } + } + + /** + * Closes and flushes the specified {@link Closeable} items. + * + * @param closeables An {@link Iterable} of {@link Closeable} items. + */ + public static void closeQuietly(Iterable closeables) { + for (Closeable c : closeables) { + try { + //Check if we also need to flush + if (c instanceof Flushable) { + ((Flushable) c).flush(); + } + + if (c != null) { + c.close(); + } + } catch (IOException e) { + LOGGER.debug("Error closing: {}", c); + } + } + } + + /** + * Closes and flushes the specified {@link Closeable} items. + * + * @param closeables An array of {@link Closeable} items. + */ + public static void closeQuietly(Closeable... closeables) { + closeQuietly(Arrays.asList(closeables)); + } + + public static String coalesce(String... items) { + for (String str : items) { + if (str != null) { + return str; + } + } + return null; + } + + /** + * Utility method to generate UUIDs. + * This is taken from Spring Framework's SimpleIdGenerator + * + * @return generated UUID + */ + public static UUID generateId() { + return new UUID(0, leastSigBits.incrementAndGet()); + } + + public static String randomString(int length) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + int index = random.nextInt(ALL_CHARS.length()); + sb.append(ALL_CHARS.charAt(index)); + } + return sb.toString(); + } + + public static String filePath(URL path) { + if (path == null) { + throw new KubernetesClientException("Path is required"); + } + try { + return Paths.get(path.toURI()).toString(); + } catch (URISyntaxException e) { + throw KubernetesClientException.launderThrowable(e); + } + } + + /** + * Replaces all occurrences of the from text with to text without any regular expressions + * + * @param text text string + * @param from from string + * @param to to string + * @return returns processed string + */ + public static String replaceAllWithoutRegex(String text, String from, String to) { + if (text == null) { + return null; + } + int idx = 0; + while (true) { + idx = text.indexOf(from, idx); + if (idx >= 0) { + text = text.substring(0, idx) + to + text.substring(idx + from.length()); + + // lets start searching after the end of the `to` to avoid possible infinite recursion + idx += to.length(); + } else { + break; + } + } + return text; + } + + public static boolean isNullOrEmpty(String str) { + return str == null || str.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) { + return !isNullOrEmpty(str); + } + + public static boolean isNotNullOrEmpty(String[] array) { + return !isNullOrEmpty(array); + } + + public static boolean isNullOrEmpty(String[] array) { + return array == null || array.length == 0; + } + + public static boolean isNotNull(T... refList) { + return Optional.ofNullable(refList) + .map(refs -> Stream.of(refs).allMatch(Objects::nonNull)) + .orElse(false); + } + + public static T getNonNullOrElse(T obj, T defaultObj) { + return obj != null ? obj : defaultObj; + } + + public static String getProperty(Map properties, String propertyName, String defaultValue) { + String answer = (String) properties.get(propertyName); + if (!isNullOrEmpty(answer)) { + return answer; + } + + return getSystemPropertyOrEnvVar(propertyName, defaultValue); + } + + public static String getProperty(Map properties, String propertyName) { + return getProperty(properties, propertyName, null); + } + + /** + * Converts string to URL encoded string. + * + * @param str Url as string + * @return returns encoded string + */ + public static String toUrlEncoded(String str) { + try { + return URLEncoder.encode(str, StandardCharsets.UTF_8.displayName()); + } catch (UnsupportedEncodingException exception) { + // Ignore + } + return null; + } + + /** + * + * @param kind + * @return + * @deprecated use {@link io.fabric8.kubernetes.api.model.HasMetadata#getPlural(Class)} + */ + @Deprecated + public static String getPluralFromKind(String kind) { + return Pluralize.toPlural(kind.toLowerCase(Locale.ROOT)); + } + + /** + * Reads @Namespaced annotation in resource class to check whether + * resource is namespaced or not + * + * @param kubernetesResourceType class for resource + * @return boolean value indicating it's namespaced or not + */ + public static boolean isResourceNamespaced(Class kubernetesResourceType) { + return Namespaced.class.isAssignableFrom(kubernetesResourceType); + } + + public static String getAnnotationValue(Class kubernetesResourceType, Class annotationClass) { + Annotation annotation = kubernetesResourceType.getAnnotation(annotationClass); + if (annotation instanceof Group) { + return ((Group) annotation).value(); + } else if (annotation instanceof Version) { + return ((Version) annotation).value(); + } + return null; + } + + /** + * Interpolates a String containing variable placeholders with the values provided in the valuesMap. + * + *

+ * This method is intended to interpolate templates loaded from YAML and JSON files. + * + *

+ * Placeholders are indicated by the dollar sign and curly braces ({@code ${VARIABLE_KEY}}). + * + *

+ * Placeholders can also be indicated by the dollar sign and double curly braces ({@code ${{VARIABLE_KEY}}}), + * when this notation is used, the resulting value will be unquoted (if applicable), expected values should be JSON + * compatible. + * + * @see OpenShift + * Templates + * @param valuesMap to interpolate in the String + * @param templateInput raw input containing a String with placeholders ready to be interpolated + * @return the interpolated String + */ + public static String interpolateString(String templateInput, Map valuesMap) { + return Optional.ofNullable(valuesMap).orElse(Collections.emptyMap()).entrySet().stream() + .filter(entry -> entry.getKey() != null) + .filter(entry -> entry.getValue() != null) + .flatMap(entry -> { + final String key = entry.getKey(); + final String value = entry.getValue(); + return Stream.of( + new AbstractMap.SimpleEntry<>("${" + key + "}", value), + new AbstractMap.SimpleEntry<>("\"${{" + key + "}}\"", value), + new AbstractMap.SimpleEntry<>("${{" + key + "}}", value)); + }) + .map(explodedParam -> (Function) s -> s.replace(explodedParam.getKey(), explodedParam.getValue())) + .reduce(Function.identity(), Function::andThen) + .apply(Objects.requireNonNull(templateInput, "templateInput is required")); + } + + /** + * Check whether platform is windows or not + * + * @return boolean value indicating whether OS is Windows or not. + */ + public static boolean isWindowsOperatingSystem() { + return getOperatingSystemFromSystemProperty().toLowerCase().contains(WINDOWS); + } + + /** + * Get system PATH variable + * + * @return a string containing value of PATH + */ + public static String getSystemPathVariable() { + return System.getenv(isWindowsOperatingSystem() ? PATH_WINDOWS : PATH_UNIX); + } + + /** + * Returns prefixes needed to invoke specified command + * in a subprocess. + * + * @return a list of strings containing prefixes + */ + public static List getCommandPlatformPrefix() { + List platformPrefixParts = new ArrayList<>(); + if (Utils.isWindowsOperatingSystem()) { + platformPrefixParts.add("cmd.exe"); + platformPrefixParts.add("/c"); + } else { + platformPrefixParts.add("sh"); + platformPrefixParts.add("-c"); + } + return platformPrefixParts; + } + + private static String getOperatingSystemFromSystemProperty() { + return System.getProperty(OS_NAME); + } + + /** + * Create a {@link ThreadFactory} with daemon threads and a thread + * name based upon the object passed in. + */ + public static ThreadFactory daemonThreadFactory(Object forObject) { + String name = forObject.getClass().getSimpleName() + "-" + System.identityHashCode(forObject); + return daemonThreadFactory(name); + } + + static ThreadFactory daemonThreadFactory(String name) { + return new ThreadFactory() { + ThreadFactory threadFactory = Executors.defaultThreadFactory(); + + @Override + public Thread newThread(Runnable r) { + Thread ret = threadFactory.newThread(r); + ret.setName(name + "-" + ret.getName()); + ret.setDaemon(true); + return ret; + } + }; + } + + /** + * Schedule a task to run in the given {@link Executor} - which should run the task in a different thread as to not + * hold the scheduling thread + */ + public static CompletableFuture schedule(Executor executor, Runnable command, long delay, TimeUnit unit) { + // to be replaced in java 9+ with CompletableFuture.runAsync(command, CompletableFuture.delayedExecutor(delay, unit, executor)); + CompletableFuture result = new CompletableFuture<>(); + ScheduledFuture scheduledFuture = SHARED_SCHEDULER.schedule(() -> { + try { + executor.execute(command); + result.complete(null); + } catch (Throwable t) { + result.completeExceptionally(t); + } + }, delay, unit); + result.whenComplete((v, t) -> { + scheduledFuture.cancel(true); + }); + return result; + } + + /** + * Schedule a repeated task to run in the given {@link Executor} - which should run the task in a different thread as to not + * hold the scheduling thread. + *

+ * Has the same general contract as {@link ScheduledThreadPoolExecutor#scheduleAtFixedRate(Runnable, long, long, TimeUnit)} + */ + public static CompletableFuture scheduleAtFixedRate(Executor executor, Runnable command, long initialDelay, long delay, + TimeUnit unit) { + CompletableFuture completion = new CompletableFuture<>(); + scheduleWithVariableRate(completion, executor, command, initialDelay, () -> delay, unit); + return completion; + } + + /** + * Schedule a repeated task to run in the given {@link Executor} - which should run the task in a different thread as to not + * hold the scheduling thread. + *

+ * + * @param nextDelay provides the relative next delay - that is the values are applied cumulatively to the initial start + * time. Supplying a fixed value produces a fixed rate. + */ + public static void scheduleWithVariableRate(CompletableFuture completion, Executor executor, Runnable command, + long initialDelay, + LongSupplier nextDelay, TimeUnit unit) { + AtomicReference> currentScheduledFuture = new AtomicReference<>(); + AtomicLong next = new AtomicLong(unit.convert(System.nanoTime(), TimeUnit.NANOSECONDS) + Math.max(0, initialDelay)); + schedule(() -> CompletableFuture.runAsync(command, executor), initialDelay, unit, completion, nextDelay, next, + currentScheduledFuture); + // remove on cancel is true, so this may proactively clean up + completion.whenComplete((v, t) -> Optional.ofNullable(currentScheduledFuture.get()).ifPresent(s -> s.cancel(true))); + } + + private static void schedule(Supplier> runner, long delay, TimeUnit unit, + CompletableFuture completion, LongSupplier nextDelay, AtomicLong next, + AtomicReference> currentScheduledFuture) { + currentScheduledFuture.set(SHARED_SCHEDULER.schedule(() -> { + if (completion.isDone()) { + return; + } + CompletableFuture runAsync = runner.get(); + runAsync.whenComplete((v, t) -> { + if (t != null) { + completion.completeExceptionally(t); + } else if (!completion.isDone()) { + schedule(runner, next.addAndGet(nextDelay.getAsLong()) - unit.convert(System.nanoTime(), TimeUnit.NANOSECONDS), + unit, completion, nextDelay, next, currentScheduledFuture); + } + }); + }, delay, unit)); + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt index d9add854c..15e8bbda6 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt @@ -12,7 +12,6 @@ package com.redhat.devtools.intellij.kubernetes.model import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.logger -import com.redhat.devtools.intellij.common.utils.ConfigHelper import com.redhat.devtools.intellij.common.utils.ConfigWatcher import com.redhat.devtools.intellij.common.utils.ExecHelper import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter @@ -27,10 +26,9 @@ import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.NAME_P import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_IS_OPENSHIFT import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_KUBERNETES_VERSION import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_OPENSHIFT_VERSION +import io.fabric8.kubernetes.api.model.Config import io.fabric8.kubernetes.api.model.HasMetadata -import io.fabric8.kubernetes.client.Config import io.fabric8.kubernetes.client.KubernetesClient -import java.nio.file.Paths import java.util.concurrent.CompletionException import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read @@ -86,16 +84,14 @@ open class AllContexts( = { namespace, context -> ClientAdapter.Factory.create(namespace, context) } ) : IAllContexts { - init { - watchKubeConfig() - } - private val lock = ReentrantReadWriteLock() - private val client = ResettableLazyProperty { - lock.write { + protected open val client = ResettableLazyProperty { + val client = lock.write { clientFactory.invoke(null, null) } + watchKubeConfig(client) + client } override val current: IActiveContext? @@ -110,10 +106,11 @@ open class AllContexts( lock.write { if (_all.isEmpty()) { try { - val all = createContexts(client.get(), client.get()?.config) + val client = client.get() + val all = createContexts(client, client?.config) _all.addAll(all) } catch (e: Exception) { - // + logger().warn("Could not load all contexts.", e) } } return _all @@ -196,8 +193,10 @@ open class AllContexts( return config.allContexts .map { if (config.isCurrent(it)) { + logger().debug("Adding active context ${it.name}") createActiveContext(client) ?: Context(it) } else { + logger().debug("Adding inactive context ${it.name}") Context(it) } } @@ -241,26 +240,28 @@ open class AllContexts( } } - protected open fun watchKubeConfig() { - val path = Paths.get(Config.getKubeconfigFilename()) + protected open fun watchKubeConfig(client: ClientAdapter) { + val files = client.config.files + val paths = files.map { file -> file.toPath() } /** * [ConfigWatcher] cannot add/remove listeners nor can it get closed (and stop the [java.nio.file.WatchService]). * We therefore have to create a single instance in here rather than using it in a shielded/private way within * [com.redhat.devtools.intellij.kubernetes.model.client.ClientConfig]. * Closing/Recreating [ConfigWatcher] is needed when used within [com.redhat.devtools.intellij.kubernetes.model.client.ClientConfig]. * The latter gets closed/recreated whenever the context changes in - * [com.redhat.devtools.intellij.kubernetes.model.client.KubeConfigAdapter]. + * [com.redhat.devtools.intellij.kubernetes.model.client.KubeConfigPersistence]. */ - val watcher = ConfigWatcher(path) { _, config: io.fabric8.kubernetes.api.model.Config? -> onKubeConfigChanged(config) } + val watcher = ConfigWatcher(paths) { _, config: Config? -> + onKubeConfigChanged(config) + } runAsync(watcher::run) } - protected open fun onKubeConfigChanged(fileConfig: io.fabric8.kubernetes.api.model.Config?) { + protected open fun onKubeConfigChanged(fileConfig: Config?) { lock.read { fileConfig ?: return val client = client.get() ?: return - val clientConfig = client.config.configuration - if (ConfigHelper.areEqual(fileConfig, clientConfig)) { + if (client.config.isEqual(fileConfig)) { return } this.client.reset() // create new client when accessed diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfig.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfig.kt index 76f871534..4f753f5c2 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfig.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfig.kt @@ -10,13 +10,14 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model.client +import com.intellij.openapi.diagnostic.logger import com.redhat.devtools.intellij.common.utils.ConfigHelper import com.redhat.devtools.intellij.kubernetes.CompletableFutureUtils.PLATFORM_EXECUTOR -import io.fabric8.kubernetes.api.model.Context import io.fabric8.kubernetes.api.model.NamedContext import io.fabric8.kubernetes.client.Client import io.fabric8.kubernetes.client.Config import io.fabric8.kubernetes.client.internal.KubeConfigUtils +import java.io.File import java.util.concurrent.CompletableFuture import java.util.concurrent.Executor @@ -24,15 +25,16 @@ import java.util.concurrent.Executor * An adapter to access [io.fabric8.kubernetes.client.Config]. * It also saves the kube config [KubeConfigUtils] when it changes the client config. */ -open class ClientConfig(private val client: Client, private val executor: Executor = PLATFORM_EXECUTOR) { +open class ClientConfig( + private val client: Client, + private val executor: Executor = PLATFORM_EXECUTOR, + private val persistence: (io.fabric8.kubernetes.api.model.Config?, String?) -> Unit = KubeConfigUtils::persistKubeConfigIntoFile +) { - open var currentContext: NamedContext? + open val currentContext: NamedContext? get() { return configuration.currentContext } - set(context) { - configuration.currentContext = context - } open val allContexts: List get() { @@ -43,73 +45,62 @@ open class ClientConfig(private val client: Client, private val executor: Execut client.configuration } - protected open val kubeConfig: KubeConfigAdapter by lazy { - KubeConfigAdapter() - } + val files: List + get() { + return configuration.files + } fun save(): CompletableFuture { return CompletableFuture.supplyAsync( { - if (!kubeConfig.exists()) { - return@supplyAsync false + val toSave = mutableMapOf() + val withCurrentContext = configuration.fileWithCurrentContext + if (withCurrentContext != null + && setCurrentContext(withCurrentContext.config) + ) { + toSave[withCurrentContext.file] = withCurrentContext.config } - val fromFile = kubeConfig.load() ?: return@supplyAsync false - if (setCurrentContext( - currentContext, - KubeConfigUtils.getCurrentContext(fromFile), - fromFile - ).or( // no short-circuit - setCurrentNamespace( - currentContext?.context, - KubeConfigUtils.getCurrentContext(fromFile)?.context - ) - ) + val withCurrentNamespace = configuration.getFileWithContext(currentContext?.name) + if (withCurrentNamespace != null + && setCurrentNamespace(withCurrentNamespace.config) ) { - kubeConfig.save(fromFile) - return@supplyAsync true - } else { - return@supplyAsync false + toSave[withCurrentNamespace.file] = withCurrentNamespace.config + } + toSave.forEach { + save(it.value, it.key) } + toSave.isNotEmpty() }, executor ) } - private fun setCurrentContext( - currentContext: NamedContext?, - kubeConfigCurrentContext: NamedContext?, - kubeConfig: io.fabric8.kubernetes.api.model.Config - ): Boolean { - return if (currentContext != null - && !ConfigHelper.areEqual(currentContext, kubeConfigCurrentContext) - ) { - kubeConfig.currentContext = currentContext.name + private fun save(kubeConfig: io.fabric8.kubernetes.api.model.Config?, file: File?) { + if (kubeConfig != null + && file?.absolutePath != null) { + logger().debug("Saving ${file.absolutePath}.") + persistence.invoke(kubeConfig, file.absolutePath) + } + } + + private fun setCurrentNamespace(kubeConfig: io.fabric8.kubernetes.api.model.Config?): Boolean { + val currentNamespace = currentContext?.context?.namespace ?: return false + val context = KubeConfigUtils.getContext(kubeConfig, currentContext?.name) + return if (context?.context != null + && context.context.namespace != currentNamespace) { + context.context.namespace = currentNamespace true } else { false } } - /** - * Sets the namespace in the given source [Context] to the given target [Context]. - * Does nothing if the target config has no current context - * or if the source config has no current context - * or if setting it would not change it. - * - * @param source Context whose namespace should be copied - * @param target Context whose namespace should be overriden - * @return - */ - private fun setCurrentNamespace( - source: Context?, - target: Context? - ): Boolean { - val sourceNamespace = source?.namespace ?: return false - val targetNamespace = target?.namespace - return if (target != null - && sourceNamespace != targetNamespace - ) { - target.namespace = source.namespace + private fun setCurrentContext(kubeConfig: io.fabric8.kubernetes.api.model.Config?): Boolean { + val currentContext = currentContext?.name ?: return false + return if ( + kubeConfig != null + && currentContext != kubeConfig.currentContext) { + kubeConfig.currentContext = currentContext true } else { false @@ -119,4 +110,8 @@ open class ClientConfig(private val client: Client, private val executor: Execut fun isCurrent(context: NamedContext): Boolean { return context == currentContext } + + fun isEqual(config: io.fabric8.kubernetes.api.model.Config): Boolean { + return ConfigHelper.areEqual(config, configuration) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/KubeConfigAdapter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/KubeConfigAdapter.kt deleted file mode 100644 index 27bdfdeca..000000000 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/KubeConfigAdapter.kt +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2021 Red Hat, Inc. - * Distributed under license by Red Hat, Inc. All rights reserved. - * This program is made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v20.html - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - ******************************************************************************/ -package com.redhat.devtools.intellij.kubernetes.model.client - -import io.fabric8.kubernetes.api.model.Config -import io.fabric8.kubernetes.client.internal.KubeConfigUtils -import java.io.File - -/** - * A class that allows to access the kube config file that's by default at ~/.kube/config - * (but may be configured to be at a different location). This class respects this by relying on the - * {@link io.fabric8.kubernetes.client.Config} for the location instead of using a hard coded path. - */ -class KubeConfigAdapter { - - private val file: File by lazy { - File(io.fabric8.kubernetes.client.Config.getKubeconfigFilename()) - } - - fun exists(): Boolean { - return file.exists() - } - - fun load(): Config? { - if (!exists()) { - return null - } - return KubeConfigUtils.parseConfig(file) - } - - fun save(config: Config) { - KubeConfigUtils.persistKubeConfigIntoFile(config, file.absolutePath) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/IActiveContext.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/IActiveContext.kt index 37abc2621..e362073bf 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/IActiveContext.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/IActiveContext.kt @@ -10,6 +10,7 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model.context +import com.intellij.openapi.diagnostic.logger import com.redhat.devtools.intellij.common.kubernetes.ClusterInfo import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter @@ -34,12 +35,14 @@ interface IActiveContext: IContext { ): IActiveContext? { val currentContext = client.config.currentContext ?: return null return if (client.isOpenShift()) { + logger().warn("Current context ${currentContext.name} is OpenShift") OpenShiftContext( currentContext, observable, client as OSClientAdapter ) } else { + logger().warn("Current context ${currentContext.name} is Kubernetes") KubernetesContext( currentContext, observable, diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContextsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContextsTest.kt index df5cbc036..388f4919e 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContextsTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContextsTest.kt @@ -10,17 +10,7 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.clearInvocations -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.times -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import com.nhaarman.mockitokotlin2.* import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE1 @@ -35,17 +25,9 @@ import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.clientConfig import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.clientFactory import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.context import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind -import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException -import io.fabric8.kubernetes.api.model.Config -import io.fabric8.kubernetes.api.model.ConfigBuilder -import io.fabric8.kubernetes.api.model.HasMetadata -import io.fabric8.kubernetes.api.model.NamedAuthInfoBuilder -import io.fabric8.kubernetes.api.model.Namespace -import io.fabric8.kubernetes.api.model.NamespaceList -import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.* import io.fabric8.kubernetes.api.model.apps.Deployment import io.fabric8.kubernetes.client.KubernetesClient -import io.fabric8.kubernetes.client.KubernetesClientException import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation import io.fabric8.kubernetes.client.dsl.Resource import org.assertj.core.api.Assertions.assertThat @@ -82,14 +64,6 @@ class AllContextsTest { private val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) - @Test - fun `when instantiated, it should watch kube config`() { - // given - // when - // then - assertThat(allContexts.watchStarted).isTrue - } - @Test fun `#refresh() should close existing context`() { // given @@ -385,67 +359,42 @@ class AllContextsTest { } @Test - fun `#onKubeConfigChanged() should NOT fire if new config is null`() { + fun `#client#get should start watching kubeconfigs`() { // given + assertThat(allContexts.watchStarted).isFalse // when - allContexts.onKubeConfigChanged(null) + allContexts.client.get() // then - verify(modelChange, never()).fireAllContextsChanged() + assertThat(allContexts.watchStarted).isTrue } @Test - fun `#onKubeConfigChanged() should NOT fire if existing config and given config are equal`() { + fun `#onKubeConfigChanged() should NOT fire if new config is null`() { // given - val kubeConfig = ConfigBuilder() - .withCurrentContext(clientConfig.currentContext?.name) - .withContexts(clientConfig.allContexts) - .withUsers(NamedAuthInfoBuilder() - .withName(currentContext.context.user) - .withNewUser() - .withToken(clientConfig.configuration.oauthToken) - .endUser() - .build()) - .build() // when - allContexts.onKubeConfigChanged(kubeConfig) + allContexts.onKubeConfigChanged(null) // then verify(modelChange, never()).fireAllContextsChanged() } @Test - fun `#onKubeConfigChanged() should fire if given config has different current context`() { + fun `#onKubeConfigChanged() should NOT fire if given kubeConfig is equal to current client config`() { // given - assertThat(namedContext1).isNotEqualTo(currentContext) - val kubeConfig = ConfigBuilder() - .withCurrentContext(namedContext1.name) - .withContexts(clientConfig.allContexts) - .withUsers(NamedAuthInfoBuilder() - .withName(currentContext.context.user) - .withNewUser() - .withToken(clientConfig.configuration.oauthToken) - .endUser() - .build()) - .build() + val kubeConfig = ConfigBuilder().build() + whenever(clientConfig.isEqual(kubeConfig)) + .thenReturn(true) // when allContexts.onKubeConfigChanged(kubeConfig) // then - verify(modelChange).fireAllContextsChanged() + verify(modelChange, never()).fireAllContextsChanged() } @Test - fun `#onKubeConfigChanged() should fire if given config has different contexts`() { + fun `#onKubeConfigChanged() should fire if given kubeConfig is NOT equal to current client config`() { // given - val contexts = listOf(mock(), *clientConfig.allContexts.toTypedArray()) - val kubeConfig = ConfigBuilder() - .withCurrentContext(clientConfig.currentContext?.name) - .withContexts(contexts) - .withUsers(NamedAuthInfoBuilder() - .withName(currentContext.context.user) - .withNewUser() - .withToken(clientConfig.configuration.oauthToken) - .endUser() - .build()) - .build() + val kubeConfig = ConfigBuilder().build() + whenever(clientConfig.isEqual(kubeConfig)) + .thenReturn(false) // when allContexts.onKubeConfigChanged(kubeConfig) // then @@ -453,19 +402,11 @@ class AllContextsTest { } @Test - fun `#onKubeConfigChanged() should close client if given config has different current context`() { + fun `#onKubeConfigChanged() should close client if given config is NOT equal to current context`() { // given - assertThat(namedContext1).isNotEqualTo(currentContext) - val kubeConfig = ConfigBuilder() - .withCurrentContext(namedContext1.name) - .withContexts(clientConfig.allContexts) - .withUsers(NamedAuthInfoBuilder() - .withName(currentContext.context.user) - .withNewUser() - .withToken(clientConfig.configuration.oauthToken) - .endUser() - .build()) - .build() + val kubeConfig = ConfigBuilder().build() + whenever(clientConfig.isEqual(kubeConfig)) + .thenReturn(false) allContexts.current // when allContexts.onKubeConfigChanged(kubeConfig) @@ -474,19 +415,11 @@ class AllContextsTest { } @Test - fun `#onKubeConfigChanged() should close current context if given config has different current context`() { + fun `#onKubeConfigChanged() should close current context if given config is NOT equal to current context`() { // given - assertThat(namedContext1).isNotEqualTo(currentContext) - val kubeConfig = ConfigBuilder() - .withCurrentContext(namedContext1.name) - .withContexts(clientConfig.allContexts) - .withUsers(NamedAuthInfoBuilder() - .withName(currentContext.context.user) - .withNewUser() - .withToken(clientConfig.configuration.oauthToken) - .endUser() - .build()) - .build() + val kubeConfig = ConfigBuilder().build() + whenever(clientConfig.isEqual(kubeConfig)) + .thenReturn(false) allContexts.current // when allContexts.onKubeConfigChanged(kubeConfig) @@ -517,12 +450,6 @@ class AllContextsTest { } } - private fun client(e: KubernetesClientException): KubernetesClient { - return mock { - on { namespaces() } doThrow e - } - } - private class TestableAllContexts( modelChange: IResourceModelObservable, contextFactory: (ClientAdapter, IResourceModelObservable) -> IActiveContext, @@ -531,6 +458,8 @@ class AllContextsTest { var watchStarted = false + public override val client = super.client + override fun reportTelemetry(context: IActiveContext) { // prevent telemetry reporting } @@ -539,7 +468,7 @@ class AllContextsTest { runnable.invoke() // run directly, not in IDEA pooled threads } - override fun watchKubeConfig() { + override fun watchKubeConfig(client: ClientAdapter) { // don't watch filesystem (override super method) watchStarted = true } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfigTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfigTest.kt index 898c989b5..609b5d499 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfigTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientConfigTest.kt @@ -10,228 +10,383 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model.client -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.argThat -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never -import com.nhaarman.mockitokotlin2.spy -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import com.nhaarman.mockitokotlin2.* import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks -import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.doReturnCurrentContextAndAllContexts -import io.fabric8.kubernetes.api.model.ConfigBuilder -import io.fabric8.kubernetes.api.model.ContextBuilder +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.kubeConfigFile +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.namedContext +import io.fabric8.kubernetes.api.model.AuthInfo +import io.fabric8.kubernetes.api.model.NamedAuthInfo +import io.fabric8.kubernetes.api.model.NamedAuthInfoBuilder import io.fabric8.kubernetes.api.model.NamedContext import io.fabric8.kubernetes.api.model.NamedContextBuilder import io.fabric8.kubernetes.client.Client import io.fabric8.kubernetes.client.Config -import io.fabric8.kubernetes.client.internal.KubeConfigUtils import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import java.io.File class ClientConfigTest { - private val namedContext1 = - context("ctx1", "namespace1", "cluster1", "user1") - private val namedContext2 = - context("ctx2", "namespace2", "cluster2", "user2") - private val namedContext3 = - context("ctx3", "namespace3", "cluster3", "user3") - private val currentContext = namedContext2 - private val allContexts = listOf(namedContext1, namedContext2, namedContext3) - private val clientKubeConfig: Config = ClientMocks.config(currentContext, allContexts) - private val client: Client = createClient(clientKubeConfig) - private val fileKubeConfig: io.fabric8.kubernetes.api.model.Config = apiConfig(currentContext.name, allContexts) - private val kubeConfigAdapter: KubeConfigAdapter = kubeConfig(true, fileKubeConfig) - private val clientConfig = spy(TestableClientConfig(client, kubeConfigAdapter)) - - @Test - fun `#currentContext should return config#currentContext`() { - // given - // when - clientConfig.currentContext - // then - verify(clientKubeConfig).currentContext - } - - @Test - fun `#allContexts should return config#contexts`() { - // given - // when - clientConfig.allContexts - // then - verify(clientKubeConfig).contexts - } - - @Test - fun `#isCurrent should return true if context is equal`() { - // given - // when - val isCurrent = clientConfig.isCurrent(currentContext) - // then - assertThat(isCurrent).isTrue() - } - - @Test - fun `#isCurrent should return false if context isn't equal`() { - // given - // when - val isCurrent = clientConfig.isCurrent(namedContext3) - // then - assertThat(isCurrent).isFalse() - } - - @Test - fun `#save should NOT save if kubeConfig doesnt exist`() { - // given - doReturn(false) - .whenever(kubeConfigAdapter).exists() - // when - clientConfig.save().join() - // then - verify(kubeConfigAdapter, never()).save(any()) - } - - @Test - fun `#save should NOT save if kubeConfig has same current context same namespace and same current context as client config`() { - // given - // when - clientConfig.save().join() - // then - verify(kubeConfigAdapter, never()).save(any()) - } - - @Test - fun `#save should save if kubeConfig has different current context as client config`() { - // given - clientKubeConfig.currentContext.name = namedContext3.name - assertThat(fileKubeConfig.currentContext).isNotEqualTo(clientKubeConfig.currentContext.name) - // when - clientConfig.save().join() - // then - verify(kubeConfigAdapter).save(any()) - } - - @Test - fun `#save should save if kubeConfig has same current context but current namespace that differs from client config`() { - // given - val newCurrentContext = context( - currentContext.name, - "R2-D2", - currentContext.context.cluster, - currentContext.context.user) - val newAllContexts = mutableListOf(*allContexts.toTypedArray()) - newAllContexts.removeIf { it.name == currentContext.name } - newAllContexts.add(newCurrentContext) - fileKubeConfig.contexts = newAllContexts - // when - clientConfig.save().join() - // then - verify(kubeConfigAdapter).save(any()) - } - - @Test - fun `#save should update current context in kube config if differs from current context in client config`() { - // given - val newCurrentContext = namedContext3 - doReturn(newCurrentContext) - .whenever(clientKubeConfig).currentContext - assertThat(KubeConfigUtils.getCurrentContext(fileKubeConfig)) - .isNotEqualTo(clientKubeConfig.currentContext) - // when - clientConfig.save().join() - // then - verify(kubeConfigAdapter).save(argThat { - this.currentContext == newCurrentContext.name - }) - } - - @Test - fun `#save should leave current namespace in old context untouched when updating current context in kube config`() { - // given - val newCurrentContext = namedContext3 - doReturn(newCurrentContext) - .whenever(clientKubeConfig).currentContext - assertThat(KubeConfigUtils.getCurrentContext(fileKubeConfig)) - .isNotEqualTo(clientKubeConfig.currentContext) - val context = KubeConfigUtils.getCurrentContext(fileKubeConfig) - val currentBeforeSave = context.name - val namespaceBeforeSave = context.context.namespace - // when - clientConfig.save().join() - // then - verify(kubeConfigAdapter).save(argThat { - val afterSave = fileKubeConfig.contexts.find { - namedContext -> namedContext.name == currentBeforeSave } - afterSave!!.context.namespace == namespaceBeforeSave - }) - } - - @Test - fun `#save should update current namespace in kube config if only differs from current in client config but not in current context`() { - // given - val newCurrentContext = context(currentContext.name, - "RD-2D", - currentContext.context.cluster, - currentContext.context.user) - val newAllContexts = replaceCurrentContext(newCurrentContext, currentContext.name, allContexts) - doReturnCurrentContextAndAllContexts(newCurrentContext, newAllContexts, clientKubeConfig) - assertThat(KubeConfigUtils.getCurrentContext(fileKubeConfig).context.namespace) - .isNotEqualTo(clientKubeConfig.currentContext.context.namespace) - // when - clientConfig.save().join() - // then - verify(kubeConfigAdapter).save(argThat { - this.currentContext == this@ClientConfigTest.currentContext.name - && KubeConfigUtils.getCurrentContext(this).context.namespace == newCurrentContext.context.namespace - }) - } - - private fun replaceCurrentContext( - newContext: NamedContext, - currentContext: String, - allContexts: List - ): List { - val newAllContexts = mutableListOf(*allContexts.toTypedArray()) - val existingContext = clientKubeConfig.contexts.find { it.name == currentContext } - newAllContexts.remove(existingContext) - newAllContexts.add(newContext) - return newAllContexts - } - - private fun context(name: String, namespace: String, cluster: String, user: String): NamedContext { - val context = ContextBuilder() - .withNamespace(namespace) - .withCluster(cluster) - .withUser(user) - .build() - return NamedContextBuilder() - .withName(name) - .withContext(context) - .build() - } - - private fun createClient(config: Config): Client { - return mock { - on { configuration } doReturn config - } - } - - private fun kubeConfig(exists: Boolean, config: io.fabric8.kubernetes.api.model.Config): com.redhat.devtools.intellij.kubernetes.model.client.KubeConfigAdapter { - return mock { - on { exists() } doReturn exists - on { load() } doReturn config - } - } - - private fun apiConfig(currentContext: String, allContexts: List): io.fabric8.kubernetes.api.model.Config { - return ConfigBuilder() - .withCurrentContext(currentContext) - .withContexts(allContexts) - .build() - } - - private class TestableClientConfig(client: Client, override val kubeConfig: KubeConfigAdapter) - : ClientConfig(client, { it.run() }) + private val ctx1 = + namedContext("ctx1", "namespace1", "cluster1", "user1") + private val ctx2 = + namedContext("ctx2", "namespace2", "cluster2", "user2") + private val ctx3 = + namedContext("ctx3", "namespace3", "cluster3", "user3") + private val ctx4 = + namedContext("ctx4", "namespace4", "cluster4", "user4") + private val ctx5 = + namedContext("ctx5", "namespace5", "cluster5", "user5") + private val ctx6 = + namedContext("ctx6", "namespace6", "cluster6", "user6") + private val currentContext = ctx2 + private val allContexts = listOf(ctx1, ctx2, ctx3) + + private val user5 = namedAuthInfo("user5", "user5", "token5") + + // kubeConfigFile current context ctx5 + private val ctx5FileWithCurrentContext = mockFile("ctx5FileWithCurrent") + private val ctx5ConfigWithCurrentContext = kubeConfig( + ctx5.name, + null + ) + private val ctx5KubeConfigFileWithCurrentContext = kubeConfigFile( + ctx5FileWithCurrentContext, + ctx5ConfigWithCurrentContext + ) + + // kubeConfigFile context ctx5 + private val ctx5FileWithCurrentNamespace = mockFile("ctx5FileWithContext") + private val ctx5ConfigWithCurrentNamespace = kubeConfig( + null, + listOf(ctx4, ctx5, ctx6), + listOf(user5) + ) + private val ctx5KubeConfigFileWithCurrentNamespace = kubeConfigFile( + ctx5FileWithCurrentNamespace, + ctx5ConfigWithCurrentNamespace + ) + private val config: Config = ClientMocks.config( + currentContext, + allContexts + ) + private val client: Client = createClient(config) + private val persistence: (io.fabric8.kubernetes.api.model.Config?, String?) -> Unit = mock() + private val clientConfig = spy(TestableClientConfig(client, persistence)) + + @Test + fun `#currentContext should return config#currentContext`() { + // given + // when + clientConfig.currentContext + // then + verify(config).currentContext + } + + @Test + fun `#allContexts should return config#allContexts`() { + // given + // when + clientConfig.allContexts + // then + verify(config).contexts + } + + @Test + fun `#isCurrent should return true if context is equal`() { + // given + // when + val isCurrent = clientConfig.isCurrent(currentContext) + // then + assertThat(isCurrent).isTrue() + } + + @Test + fun `#isCurrent should return false if context isn't equal`() { + // given + // when + val isCurrent = clientConfig.isCurrent(ctx3) + // then + assertThat(isCurrent).isFalse() + } + + @Test + fun `#save should NOT save if no file with current context nor with current namespace exists`() { + // given + val config = client.configuration + doReturn(null) + .whenever(config).getFileWithCurrentContext() + doReturn(null) + .whenever(config).getFileWithContext(any()) + // when + clientConfig.save().join() + // then + verify(persistence, never()).invoke(any(), any()) + + } + + @Test + fun `#save should NOT save if files are same in current namespace and current context than client config`() { + // given + val config = client.configuration + // same current context + whenever(config.currentContext) + .thenReturn(ctx5) + whenever(config.getFileWithCurrentContext()) + .thenReturn(this.ctx5KubeConfigFileWithCurrentContext) + // same current namespace + whenever(config.getFileWithContext(ctx5.name)) + .thenReturn(ctx5KubeConfigFileWithCurrentNamespace) + // when + clientConfig.save().join() + // then + verify(persistence, never()).invoke(any(), any()) + } + + @Test + fun `#save should save file with current context if it has different current context than client config`() { + // given + val config = client.configuration + // different current context + whenever(config.currentContext) + .thenReturn(ctx6) + whenever(config.getFileWithCurrentContext()) + .thenReturn(ctx5KubeConfigFileWithCurrentContext) + // same current namespace + whenever(config.getFileWithContext(ctx5.name)) + .thenReturn(ctx5KubeConfigFileWithCurrentNamespace) + // when + clientConfig.save().join() + // then + verify(persistence).invoke(ctx5ConfigWithCurrentContext, ctx5FileWithCurrentContext.absolutePath) + } + + @Test + fun `#save should set current context in KubeConfigFile if it has different current context than client config`() { + // given + val config = client.configuration + // different current context + whenever(config.currentContext) + .thenReturn(ctx6) + whenever(config.getFileWithCurrentContext()) + .thenReturn(ctx5KubeConfigFileWithCurrentContext) + // same current namespace + whenever(config.getFileWithContext(ctx5.name)) + .thenReturn(ctx5KubeConfigFileWithCurrentNamespace) + // when + clientConfig.save().join() + // then + verify(ctx5ConfigWithCurrentContext).currentContext = ctx6.name + } + + @Test + fun `#save should save file with current namespace if it has different current namespace than client config`() { + // given + val config = client.configuration + // same current context + whenever(config.currentContext) + .thenReturn(ctx5) + whenever(config.getFileWithCurrentContext()) + .thenReturn(ctx5KubeConfigFileWithCurrentContext) + // different current namespace + val ctx5WithDifferentNamespace = kubeConfig( + null, + listOf( + namedContext(ctx5.name,"R2-D2") + ) + ) + val kubeConfigFile = kubeConfigFile(ctx5FileWithCurrentNamespace, ctx5WithDifferentNamespace) + whenever(config.getFileWithContext(ctx5.name)) + .thenReturn(kubeConfigFile) + // when + clientConfig.save().join() + // then + verify(persistence).invoke(ctx5WithDifferentNamespace, ctx5FileWithCurrentNamespace.absolutePath) + } + + @Test + fun `#save should set current namespace if kubeConfigFile has different current namespace than client config`() { + // given + val config = client.configuration + // same current context + whenever(config.currentContext) + .thenReturn(ctx5) + whenever(config.getFileWithCurrentContext()) + .thenReturn(ctx5KubeConfigFileWithCurrentContext) + // different current namespace + val ctx5ContextWithDifferentNamespace = namedContext(ctx5.name,"R2-D2") + val ctx5ConfigWithDifferentNamespace = kubeConfig( + null, + listOf( + ctx5ContextWithDifferentNamespace + ) + ) + val kubeConfigFile = kubeConfigFile(ctx5FileWithCurrentNamespace, ctx5ConfigWithDifferentNamespace) + whenever(config.getFileWithContext(ctx5.name)) + .thenReturn(kubeConfigFile) + // when + clientConfig.save().join() + // then + verify(ctx5ContextWithDifferentNamespace.context).namespace = ctx5.context.namespace + } + + @Test + fun `#save should save file with current context and file with current namespace if both differ from client config`() { + // given + val config = client.configuration + // different current context + whenever(config.currentContext) + .thenReturn(ctx2) + whenever(config.getFileWithCurrentContext()) + .thenReturn(ctx5KubeConfigFileWithCurrentContext) + // different current namespace + val ctx2KubeConfigFileWithCurrentNamespaceClone = kubeConfig( + null, + listOf( + namedContext(ctx2.name,"R2-D2") + ) + ) + val ctx2ConfigWithCurrentNamespaceClone = kubeConfigFile(ctx5FileWithCurrentNamespace, ctx2KubeConfigFileWithCurrentNamespaceClone) + whenever(config.getFileWithContext(ctx2.name)) + .thenReturn(ctx2ConfigWithCurrentNamespaceClone) + // when + clientConfig.save().join() + // then + verify(persistence).invoke(ctx5ConfigWithCurrentContext, ctx5FileWithCurrentContext.absolutePath) + verify(persistence).invoke(ctx2KubeConfigFileWithCurrentNamespaceClone, ctx5FileWithCurrentNamespace.absolutePath) + } + + @Test + fun `#isEqual should return true if given config is equal client config`() { + // given + val kubeConfig = kubeConfig( + config.currentContext.name, + config.contexts, + listOf(namedAuthInfo(config.currentContext.context.user)) + ) + // when + val isEqual = clientConfig.isEqual(kubeConfig) + // then + assertThat(isEqual).isTrue() + } + + @Test + fun `#isEqual should return false if given config differs from client config in current context`() { + // given + val kubeConfig = kubeConfig( + "skywalker", + config.contexts, + listOf(namedAuthInfo(config.currentContext.context.user)) + ) + // when + val isEqual = clientConfig.isEqual(kubeConfig) + // then + assertThat(isEqual).isFalse() + } + + @Test + fun `#isEqual should return false if given config differs from client config in current namespace`() { + // given + val differentNamespace = NamedContextBuilder(config.currentContext) + .editContext() + .withNamespace("skywalker") + .endContext() + .build() + val allContexts = replaceByName(differentNamespace, config.contexts) + val kubeConfig = kubeConfig( + config.currentContext.name, + allContexts // contains current context clone with different namespace + ) + // when + val isEqual = clientConfig.isEqual(kubeConfig) + // then + assertThat(isEqual).isFalse() + } + + private fun replaceByName(context: NamedContext, allContexts: List): List { + val newList = allContexts.toMutableList() + newList.replaceAll { + if (it.name == context.name) { + context + } else { + it + } + } + return newList + } + + @Test + fun `#isEqual should return false if given config differs from client config in token`() { + // given + whenever(config.autoOAuthToken) + .doReturn("skywalker") + val equalKubeConfig = kubeConfig( + config.currentContext.name, + allContexts, + listOf( + NamedAuthInfoBuilder() + .withName(config.currentContext.context.user) + .build() + ) + ) + val differentToken = NamedAuthInfoBuilder() + .withName(config.currentContext.context.user) + .withNewUser() + .withToken("iceplanet") + .endUser() + .build() + val unequalKubeConfig = kubeConfig( + config.currentContext.name, + allContexts, + listOf(differentToken) + ) + // when + val notEqual = clientConfig.isEqual(unequalKubeConfig) + val equal = clientConfig.isEqual(equalKubeConfig) + // then + assertThat(equal).isTrue() + assertThat(notEqual).isFalse() + } + + private fun createClient(config: Config): Client { + return mock { + on { configuration } doReturn config + } + } + + private fun kubeConfig( + currentContext: String?, + allContexts: List?, + allUsers: List? = null + ): io.fabric8.kubernetes.api.model.Config { + + return mock { + on { this.currentContext } doReturn currentContext + on { this.contexts } doReturn allContexts + on { this.users } doReturn allUsers + } + } + + private fun namedAuthInfo(name: String, username: String? = null, token: String? = null): NamedAuthInfo { + val authInfo: AuthInfo = mock { + on { this.username } doReturn username + on { this.token } doReturn token + } + return mock { + on { this.name } doReturn name + on { this.user } doReturn authInfo + } + } + + private fun mockFile(absolutePath: String): File { + return mock { + on { this.absolutePath } doReturn absolutePath + } + } + + private class TestableClientConfig( + client: Client, + persistence: (io.fabric8.kubernetes.api.model.Config?, absolutePath: String?) -> Unit + ) : ClientConfig(client, { it.run() }, persistence) + } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt index 9e39ebf0b..a2f145471 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt @@ -43,6 +43,7 @@ import io.fabric8.kubernetes.api.model.batch.v1.Job import io.fabric8.kubernetes.api.model.batch.v1.JobSpec import io.fabric8.kubernetes.client.Client import io.fabric8.kubernetes.client.Config +import io.fabric8.kubernetes.client.KubeConfigFile import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.NamespacedKubernetesClient import io.fabric8.kubernetes.client.V1ApiextensionAPIGroupDSL @@ -60,6 +61,7 @@ import io.fabric8.kubernetes.client.dsl.PodResource import io.fabric8.kubernetes.client.dsl.Resource import io.fabric8.kubernetes.client.dsl.internal.HasMetadataOperation import io.fabric8.kubernetes.client.extension.ExtensibleResource +import java.io.File import java.net.URL @@ -232,7 +234,7 @@ object ClientMocks { .whenever(op).inContainer(name) } - fun namedContext(name: String, namespace: String, cluster: String, user: String): NamedContext { + fun namedContext(name: String, namespace: String, cluster: String? = null, user: String? = null): NamedContext { val context: Context = context(namespace, cluster, user) return namedContext(name, context) } @@ -244,7 +246,7 @@ object ClientMocks { } } - private fun context(namespace: String, cluster: String, user: String): Context { + fun context(namespace: String, cluster: String? = null, user: String? = null): Context { return mock { on { this.namespace } doReturn namespace on { this.cluster } doReturn cluster @@ -256,7 +258,9 @@ object ClientMocks { currentContext: NamedContext?, contexts: List, masterUrl: String = "https://localhost", - apiVersion: String = "v1" + apiVersion: String = "v1", + withCurrentConfig: KubeConfigFile? = null, + withContext: KubeConfigFile? = null ): Config { return mock { on { this.currentContext } doReturn currentContext @@ -264,6 +268,8 @@ object ClientMocks { on { this.masterUrl } doReturn masterUrl on { this.apiVersion } doReturn apiVersion on { this.requestConfig } doReturn mock() + on { this.fileWithCurrentContext } doReturn withCurrentConfig + on { this.getFileWithContext(any()) } doReturn withContext } } @@ -485,5 +491,11 @@ object ClientMocks { return resource } + fun kubeConfigFile(file: File, config: io.fabric8.kubernetes.api.model.Config): KubeConfigFile { + return mock { + on { this.file } doReturn file + on { this.config } doReturn config + } + } }