Skip to content

Commit

Permalink
feat(config): support for scattered kubeconfig files
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <marc@marcnuri.com>
  • Loading branch information
manusa committed Nov 7, 2024
1 parent cb63dc6 commit 40f37ec
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -799,10 +799,12 @@ public static Config fromKubeconfig(String context, String kubeconfigContents, S
if (Utils.isNullOrEmpty(kubeconfigContents)) {
throw new KubernetesClientException("Could not create Config from kubeconfig");
}
final var kubeconfig = KubeConfigUtils.parseConfigFromString(kubeconfigContents);
final io.fabric8.kubernetes.api.model.Config kubeconfig;
if (kubeconfigPath != null) {
// TODO: temp workaround until the method is removed (marked for removal in 7.0.0)
kubeconfig.setAdditionalProperty("KUBERNETES_CONFIG_FILE_KEY", new File(kubeconfigPath));
kubeconfig = KubeConfigUtils.parseConfig(new File(kubeconfigPath));
} else {
kubeconfig = KubeConfigUtils.parseConfigFromString(kubeconfigContents);
}
KubeConfigUtils.merge(config, context, kubeconfig);
if (!disableAutoConfig()) {
Expand Down Expand Up @@ -1463,7 +1465,29 @@ public void setCurrentContext(NamedContext context) {
* @return the path to the kubeconfig file.
*/
public File getFile() {
return KubeConfigUtils.getFileFromContext(getCurrentContext());
return KubeConfigUtils.getFileWithNamedContext(getCurrentContext());
}

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

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

@JsonIgnore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static io.fabric8.kubernetes.client.Config.HTTPS_PROTOCOL_PREFIX;
Expand All @@ -55,7 +56,9 @@ public class KubeConfigUtils {

private static final Logger logger = LoggerFactory.getLogger(io.fabric8.kubernetes.client.Config.class);

private static final String KUBERNETES_CONFIG_FILE_KEY = "KUBERNETES_CONFIG_FILE_KEY";
private static final String KUBERNETES_CONFIG_CONTEXT_FILE_KEY = "KUBERNETES_CONFIG_CONTEXT_FILE_KEY";
private static final String KUBERNETES_CONFIG_CLUSTER_FILE_KEY = "KUBERNETES_CONFIG_CLUSTER_FILE_KEY";
private static final String KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY = "KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY";
private static final String ACCESS_TOKEN = "access-token";
private static final String ID_TOKEN = "id-token";

Expand All @@ -68,7 +71,16 @@ public static Config parseConfig(File kubeconfig) {
}
try (var fis = Files.newInputStream(kubeconfig.toPath())) {
final var ret = Serialization.unmarshal(fis, Config.class);
ret.setAdditionalProperty(KUBERNETES_CONFIG_FILE_KEY, kubeconfig);
if (ret.getContexts() != null) {
ret.getContexts().forEach(ctx -> ctx.getAdditionalProperties().put(KUBERNETES_CONFIG_CONTEXT_FILE_KEY, kubeconfig));
}
if (ret.getClusters() != null) {
ret.getClusters()
.forEach(cluster -> cluster.getAdditionalProperties().put(KUBERNETES_CONFIG_CLUSTER_FILE_KEY, kubeconfig));
}
if (ret.getUsers() != null) {
ret.getUsers().forEach(user -> user.getAdditionalProperties().put(KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY, kubeconfig));
}
return ret;
} catch (Exception e) {
throw KubernetesClientException.launderThrowable(kubeconfig + " (File) is not a parseable Kubernetes Config", e);
Expand All @@ -87,34 +99,60 @@ public static Config parseConfigFromString(String contents) {
* @throws IOException in case of failure while writing to file.
*/
public static void persistKubeConfigIntoFile(Config kubeconfig, File kubeConfigPath) throws IOException {
if (kubeconfig.getAdditionalProperties() != null) {
kubeconfig.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY);
}
if (kubeconfig.getContexts() != null) {
kubeconfig.getContexts().stream()
.filter(ctx -> ctx.getAdditionalProperties() != null)
.forEach(ctx -> ctx.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY));
kubeconfig.getContexts().forEach(c -> removeAdditionalProperties(c::getAdditionalProperties));
}
if (kubeconfig.getClusters() != null) {
kubeconfig.getClusters().forEach(c -> removeAdditionalProperties(c::getAdditionalProperties));
}
if (kubeconfig.getUsers() != null) {
kubeconfig.getUsers().stream()
.filter(u -> u.getAdditionalProperties() != null)
.forEach(u -> u.getAdditionalProperties().remove(KUBERNETES_CONFIG_FILE_KEY));
kubeconfig.getUsers().forEach(c -> removeAdditionalProperties(c::getAdditionalProperties));
}
Files.writeString(kubeConfigPath.toPath(), Serialization.asYaml(kubeconfig));
}

public static File getFileFromContext(NamedContext namedContext) {
return namedContext != null && namedContext.getAdditionalProperties() != null
&& namedContext.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) instanceof File
? (File) namedContext.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY)
: null;
/**
* Returns the file containing the context information if it was loaded using KubeConfigUtils#parseConfig.
*
* @param namedContext the context to get the file from.
* @return the file containing the context information if it was loaded using KubeConfigUtils#parseConfig or null.
*/
public static File getFileWithNamedContext(NamedContext namedContext) {
return getFile(namedContext != null ? namedContext::getAdditionalProperties : null, KUBERNETES_CONFIG_CONTEXT_FILE_KEY);
}

/**
* Returns the file containing the cluster information if it was loaded using KubeConfigUtils#parseConfig.
*
* @param namedContext the context to get the file from.
* @return the file containing the cluster information if it was loaded using KubeConfigUtils#parseConfig or null.
*/
public static File getFileWithNamedCluster(NamedContext namedContext) {
return getFile(namedContext != null ? namedContext::getAdditionalProperties : null, KUBERNETES_CONFIG_CLUSTER_FILE_KEY);
}

/**
* Returns the file containing the auth info information if it was loaded using KubeConfigUtils#parseConfig.
*
* @param namedContext the context to get the file from.
* @return the file containing the auth info information if it was loaded using KubeConfigUtils#parseConfig or null.
*/
public static File getFileWithNamedAuthInfo(NamedContext namedContext) {
return getFile(namedContext != null ? namedContext::getAdditionalProperties : null, KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY);
}

private static File getFileWithNamedCluster(NamedCluster namedCluster) {
return getFile(namedCluster != null ? namedCluster::getAdditionalProperties : null, KUBERNETES_CONFIG_CLUSTER_FILE_KEY);
}

public static File getFileFromAuthInfo(NamedAuthInfo namedAuthInfo) {
return namedAuthInfo != null && namedAuthInfo.getAdditionalProperties() != null
&& namedAuthInfo.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY) instanceof File
? (File) namedAuthInfo.getAdditionalProperties().get(KUBERNETES_CONFIG_FILE_KEY)
: null;
private static File getFileWithNamedAuthInfo(NamedAuthInfo namedAuthInfo) {
return getFile(namedAuthInfo != null ? namedAuthInfo::getAdditionalProperties : null, KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY);
}

private static File getFile(Supplier<Map<String, Object>> provider, String key) {
return provider != null && provider.get() != null && provider.get().get(key) instanceof File
? (File) provider.get().get(key)
: null;
}

/**
Expand Down Expand Up @@ -144,10 +182,12 @@ public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Strin
clientConfig.setCurrentContext(currentContext);
clientConfig.setNamespace(currentContext.getContext().getNamespace());
final var mergedClusters = mergeClusters(kubeconfigs);
if (mergedClusters.containsKey(currentContext.getContext().getCluster())) {
final var currentNamedCluster = mergedClusters.get(currentContext.getContext().getCluster());
if (currentNamedCluster != null) {
// If config was loaded using KubeConfigUtils#parseConfig, then the file is available in the additional properties
final File configFile = getFileFromContext(currentContext);
final var currentCluster = mergedClusters.get(currentContext.getContext().getCluster()).getCluster();
final var configFile = getFileWithNamedCluster(currentNamedCluster);
currentContext.setAdditionalProperty(KUBERNETES_CONFIG_CLUSTER_FILE_KEY, configFile);
final var currentCluster = currentNamedCluster.getCluster();
clientConfig.setMasterUrl(currentCluster.getServer());
clientConfig.setTrustCerts(Objects.equals(currentCluster.getInsecureSkipTlsVerify(), true));
clientConfig.setDisableHostnameVerification(Objects.equals(currentCluster.getInsecureSkipTlsVerify(), true));
Expand All @@ -169,10 +209,11 @@ public static void merge(io.fabric8.kubernetes.client.Config clientConfig, Strin
}
}
final var mergedUsers = mergeUsers(kubeconfigs);
if (mergedUsers.containsKey(currentContext.getContext().getUser())) {
final var currentNamedAuthInfo = mergedUsers.get(currentContext.getContext().getUser());
final var currentNamedAuthInfo = mergedUsers.get(currentContext.getContext().getUser());
if (currentNamedAuthInfo != null) {
// If config was loaded using KubeConfigUtils#parseConfig, then the file is available in the additional properties
final File configFile = getFileFromAuthInfo(currentNamedAuthInfo);
final var configFile = getFileWithNamedAuthInfo(currentNamedAuthInfo);
currentContext.setAdditionalProperty(KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY, configFile);
final var currentAuthInfo = currentNamedAuthInfo.getUser();
String clientCertFile = currentAuthInfo.getClientCertificate();
String clientKeyFile = currentAuthInfo.getClientKey();
Expand Down Expand Up @@ -203,8 +244,6 @@ private static Map<String, NamedContext> mergeContexts(io.fabric8.kubernetes.cli
if (kubeconfigs[i].getContexts() != null) {
for (NamedContext ctx : kubeconfigs[i].getContexts()) {
if (ctx.getContext() != null) {
// Contains KUBERNETES_CONFIG_FILE_KEY if config was parsed using KubeConfigUtils#parseConfig
ctx.getAdditionalProperties().putAll(kubeconfigs[i].getAdditionalProperties());
mergedContexts.put(ctx.getName(), ctx);
}
}
Expand Down Expand Up @@ -240,8 +279,6 @@ private static Map<String, NamedAuthInfo> mergeUsers(Config... kubeconfigs) {
if (kubeconfigs[i].getUsers() != null) {
for (NamedAuthInfo user : kubeconfigs[i].getUsers()) {
if (user.getUser() != null) {
// Contains KUBERNETES_CONFIG_FILE_KEY if config was parsed using KubeConfigUtils#parseConfig
user.getAdditionalProperties().putAll(kubeconfigs[i].getAdditionalProperties());
mergedUsers.put(user.getName(), user);
}
}
Expand Down Expand Up @@ -387,4 +424,13 @@ private static String absolutify(File relativeTo, String filename) {
}
return new File(relativeTo.getParentFile(), filename).getAbsolutePath();
}

private static void removeAdditionalProperties(Supplier<Map<String, Object>> provider) {
if (provider == null) {
return;
}
provider.get().remove(KUBERNETES_CONFIG_CONTEXT_FILE_KEY);
provider.get().remove(KUBERNETES_CONFIG_CLUSTER_FILE_KEY);
provider.get().remove(KUBERNETES_CONFIG_AUTH_INFO_FILE_KEY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,10 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut
.ifPresent(c -> c.putAll(authProviderConfig));
}
// Persist in file
if (currentConfig.getFile() != null && currentConfig.getCurrentContext() != null) {
if (currentConfig.getFileWithAuthInfo() != null && currentConfig.getCurrentContext() != null) {
try {
final io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils.parseConfig(currentConfig.getFile());
final io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils
.parseConfig(currentConfig.getFileWithAuthInfo());
final String userName = currentConfig.getCurrentContext().getContext().getUser();
NamedAuthInfo namedAuthInfo = kubeConfig.getUsers().stream().filter(n -> n.getName().equals(userName)).findFirst()
.orElseGet(() -> {
Expand All @@ -215,7 +216,7 @@ public static OAuthToken persistOAuthToken(Config currentConfig, OAuthToken oAut
if (Utils.isNotNullOrEmpty(token)) {
namedAuthInfo.getUser().setToken(token);
}
KubeConfigUtils.persistKubeConfigIntoFile(kubeConfig, currentConfig.getFile());
KubeConfigUtils.persistKubeConfigIntoFile(kubeConfig, currentConfig.getFileWithAuthInfo());
} catch (Exception ex) {
LOGGER.warn("oidc: failure while persisting new tokens into KUBECONFIG", ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ void withNoKubeConfigFiles() {
final var result = new ConfigBuilder().withAutoConfigure().build();
assertThat(result)
.hasFieldOrPropertyWithValue("autoConfigure", true)
.returns(null, Config::getFile);
.returns(null, Config::getFile)
.returns(null, Config::getFileWithClusterInfo)
.returns(null, Config::getFileWithAuthInfo);
}

@Test
Expand All @@ -55,7 +57,9 @@ void withNonExistentConfigFile() {
final var result = new ConfigBuilder().withAutoConfigure().build();
assertThat(result)
.hasFieldOrPropertyWithValue("autoConfigure", true)
.returns(null, Config::getFile);
.returns(null, Config::getFile)
.returns(null, Config::getFileWithClusterInfo)
.returns(null, Config::getFileWithAuthInfo);
}

@Test
Expand All @@ -65,7 +69,9 @@ void withEmptyConfigFile() throws IOException {
final var result = new ConfigBuilder().withAutoConfigure().build();
assertThat(result)
.hasFieldOrPropertyWithValue("autoConfigure", true)
.returns(null, Config::getFile);
.returns(null, Config::getFile)
.returns(null, Config::getFileWithClusterInfo)
.returns(null, Config::getFileWithAuthInfo);
}

@Test
Expand All @@ -75,6 +81,8 @@ void withSingleConfigFile() {
assertThat(result)
.hasFieldOrPropertyWithValue("autoConfigure", true)
.returns(resolveFile("/config-auto-configure/config-2.yaml"), Config::getFile)
.returns(resolveFile("/config-auto-configure/config-2.yaml"), Config::getFileWithClusterInfo)
.returns(resolveFile("/config-auto-configure/config-2.yaml"), Config::getFileWithAuthInfo)
.hasFieldOrPropertyWithValue("masterUrl", "https://config-2.example.com/")
.hasFieldOrPropertyWithValue("currentContext.name", "context-in-all-configs");

Expand All @@ -90,6 +98,8 @@ void withMultipleConfigFiles() {
assertThat(result)
.hasFieldOrPropertyWithValue("autoConfigure", true)
.returns(resolveFile("/config-auto-configure/config-1.yaml"), Config::getFile)
.returns(resolveFile("/config-auto-configure/config-1.yaml"), Config::getFileWithClusterInfo)
.returns(resolveFile("/config-auto-configure/config-1.yaml"), Config::getFileWithAuthInfo)
.hasFieldOrPropertyWithValue("masterUrl", "https://config-1.example.com/")
.hasFieldOrPropertyWithValue("currentContext.name", "context-in-all-configs");

Expand All @@ -106,12 +116,29 @@ void withMultipleConfigFilesAndContext() {
assertThat(result)
.hasFieldOrPropertyWithValue("autoConfigure", true)
.returns(resolveFile("/config-auto-configure/config-3.yaml"), Config::getFile)
.returns(resolveFile("/config-auto-configure/config-3.yaml"), Config::getFileWithClusterInfo)
.returns(resolveFile("/config-auto-configure/config-3.yaml"), Config::getFileWithAuthInfo)
.hasFieldOrPropertyWithValue("masterUrl", "https://config-3-special-cluster.example.com/")
.hasFieldOrPropertyWithValue("currentContext.name", "context-in-config-3");

}

// TODO: What if the user info is in a different file
@Test
void withMultipleConfigFilesAndScattered() {
System.setProperty("kubeconfig",
resolveFile("/config-auto-configure/scattered.yaml").getAbsolutePath() + File.pathSeparator +
resolveFile("/config-auto-configure/scattered-context.yaml").getAbsolutePath() + File.pathSeparator +
resolveFile("/config-auto-configure/scattered-cluster.yaml").getAbsolutePath() + File.pathSeparator +
resolveFile("/config-auto-configure/scattered-user.yaml").getAbsolutePath() + File.pathSeparator);
final var result = new ConfigBuilder().withAutoConfigure().build();
assertThat(result)
.hasFieldOrPropertyWithValue("autoConfigure", true)
.returns(resolveFile("/config-auto-configure/scattered-context.yaml"), Config::getFile)
.returns(resolveFile("/config-auto-configure/scattered-cluster.yaml"), Config::getFileWithClusterInfo)
.returns(resolveFile("/config-auto-configure/scattered-user.yaml"), Config::getFileWithAuthInfo)
.hasFieldOrPropertyWithValue("masterUrl", "https://scattered-cluster.example.com/")
.hasFieldOrPropertyWithValue("currentContext.name", "scattered-context");

}
}

private static File resolveFile(String path) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,11 @@ void setUp() {
System.setProperty("os.name", "Windows");
}

@AfterEach
void tearDown() {
System.setProperty("os.name", osNamePropToRestore);
}

@Test
void shouldUseHomeDriveHomePathOnWindows_WhenHomeEnvVariableIsNotSet() {
// Given
Expand Down Expand Up @@ -1021,10 +1026,6 @@ void shouldUseHomeEnvVariableOnWindows_WhenHomeEnvVariableIsSet() {
.isEqualTo("C:\\Users\\user\\workspace\\myworkspace\\tools\\cygwin\\");
}

@AfterEach
void tearDown() {
System.setProperty("os.name", osNamePropToRestore);
}
}

@Test
Expand Down
Loading

0 comments on commit 40f37ec

Please sign in to comment.