From 9cce0e95c4b45a533dd2fbb61c5630676670a8bb Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Fri, 6 Sep 2024 21:23:23 +0200 Subject: [PATCH] save kube conf files when current ctx or ns changes Signed-off-by: Andre Dietisheim --- build.gradle | 11 + .../io/fabric8/kubernetes/client/Config.java | 23 +- .../client/internal/KubeConfigUtils.java | 27 +- .../intellij/kubernetes/model/AllContexts.kt | 6 +- .../kubernetes/model/client/ClientConfig.kt | 102 ++- .../model/client/KubeConfigAdapter.kt | 42 -- .../kubernetes/model/AllContextsTest.kt | 90 +-- .../model/client/ClientConfigTest.kt | 583 +++++++++++------- .../kubernetes/model/mocks/ClientMocks.kt | 18 +- 9 files changed, 501 insertions(+), 401 deletions(-) delete mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/KubeConfigAdapter.kt diff --git a/build.gradle b/build.gradle index a4bb960db..6de49fd62 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/src/main/java/io/fabric8/kubernetes/client/Config.java b/src/main/java/io/fabric8/kubernetes/client/Config.java index 1ab4f19ee..f36a301d2 100644 --- a/src/main/java/io/fabric8/kubernetes/client/Config.java +++ b/src/main/java/io/fabric8/kubernetes/client/Config.java @@ -1789,13 +1789,30 @@ public File getFile() { } public KubeConfigFile getFileWithAuthInfo(String name) { - if (Utils.isNullOrEmpty(name) - || Utils.isNullOrEmpty(getFiles())) { + 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 -> KubeConfigUtils.hasAuthInfoNamed(entry.getConfig(), name)) + .filter(entry -> predicate.test(entry.getConfig())) .findFirst() .orElse(null); } diff --git a/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java b/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java index 5d7fdcffb..4f017dd1a 100644 --- a/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java +++ b/src/main/java/io/fabric8/kubernetes/client/internal/KubeConfigUtils.java @@ -60,16 +60,31 @@ public static Config parseConfigFromString(String contents) { public static NamedContext getCurrentContext(Config config) { String contextName = config.getCurrentContext(); if (contextName != null) { + return getContext(config, contextName); + } + return null; + } + + /** + * Returns the {@link NamedContext} with the given name. + * Returns {@code null} otherwise + * + * @param config the config to search + * @param name the context name to match + * @return the context with the the given name + */ + public static NamedContext getContext(Config config, String name) { + NamedContext context = null; + if (config != null && name != null) { List contexts = config.getContexts(); if (contexts != null) { - for (NamedContext context : contexts) { - if (contextName.equals(context.getName())) { - return context; - } - } + context = contexts.stream() + .filter(toInspect -> name.equals(toInspect.getName())) + .findAny() + .orElse(null); } } - return null; + return context; } /** 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 b710fafec..0b76294bd 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 @@ -249,7 +248,7 @@ open class AllContexts( * [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(Paths.get(filename)) { _, config: io.fabric8.kubernetes.api.model.Config? -> onKubeConfigChanged(config) } runAsync(watcher::run) @@ -259,8 +258,7 @@ open class AllContexts( 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..e41571cf3 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,57 @@ open class ClientConfig(private val client: Client, private val executor: Execut client.configuration } - protected open val kubeConfig: KubeConfigAdapter by lazy { - KubeConfigAdapter() - } - 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 +105,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/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContextsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContextsTest.kt index df5cbc036..8a62aa118 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 @@ -21,6 +21,7 @@ import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever +import com.redhat.devtools.intellij.common.utils.ConfigHelper 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 @@ -394,18 +395,11 @@ class AllContextsTest { } @Test - fun `#onKubeConfigChanged() should NOT fire if existing config and given config are equal`() { + fun `#onKubeConfigChanged() should NOT fire if given kubeConfig is equal to current client config`() { // 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() + val kubeConfig = ConfigBuilder().build() + whenever(clientConfig.isEqual(kubeConfig)) + .thenReturn(true) // when allContexts.onKubeConfigChanged(kubeConfig) // then @@ -413,19 +407,11 @@ class AllContextsTest { } @Test - fun `#onKubeConfigChanged() should fire if given config has different current context`() { + fun `#onKubeConfigChanged() should fire if given kubeConfig is NOT 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(false) // when allContexts.onKubeConfigChanged(kubeConfig) // then @@ -433,39 +419,11 @@ class AllContextsTest { } @Test - fun `#onKubeConfigChanged() should fire if given config has different contexts`() { + fun `#onKubeConfigChanged() should close client if given config is NOT equal to current context`() { // 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() - // when - allContexts.onKubeConfigChanged(kubeConfig) - // then - verify(modelChange).fireAllContextsChanged() - } - - @Test - fun `#onKubeConfigChanged() should close client if given config has different 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 +432,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 +467,6 @@ class AllContextsTest { } } - private fun client(e: KubernetesClientException): KubernetesClient { - return mock { - on { namespaces() } doThrow e - } - } - private class TestableAllContexts( modelChange: IResourceModelObservable, contextFactory: (ClientAdapter, IResourceModelObservable) -> IActiveContext, 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 0663e9425..e644057cb 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 @@ -234,7 +236,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) } @@ -246,7 +248,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 @@ -258,7 +260,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 @@ -266,6 +270,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 } } @@ -487,5 +493,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 + } + } }