From 36ef8a365151cfd17a444620b3e30973ba08d0be Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Tue, 14 Jan 2025 19:22:18 +0100 Subject: [PATCH] fix: display kubeconfig parse error on startup (#809) Signed-off-by: Andre Dietisheim --- .../actions/SetAsCurrentClusterAction.kt | 4 +- .../intellij/kubernetes/model/AllContexts.kt | 8 +- .../kubernetes/model/context/ActiveContext.kt | 2 +- .../kubernetes/model/context/Context.kt | 11 +- .../model/context/IActiveContext.kt | 4 - .../kubernetes/tree/KubernetesDescriptors.kt | 8 +- .../intellij/kubernetes/tree/TreeStructure.kt | 53 +- .../kubernetes/model/AllContextsTest.kt | 875 +++++++++--------- .../kubernetes/model/ResourceModelTest.kt | 16 +- .../intellij/kubernetes/model/mocks/Mocks.kt | 19 +- 10 files changed, 512 insertions(+), 488 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/SetAsCurrentClusterAction.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/SetAsCurrentClusterAction.kt index 2684e1b20..7054e5320 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/SetAsCurrentClusterAction.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/SetAsCurrentClusterAction.kt @@ -25,14 +25,14 @@ class SetAsCurrentClusterAction: StructureTreeAction(IContext::class.java) { val context: IContext = selectedNode?.getElement() ?: return val telemetry = TelemetryService.instance .action(NAME_PREFIX_CONTEXT + "switch") - run("Setting ${context.context.name} as current cluster...", true, + run("Setting ${context.name} as current cluster...", true, Progressive { try { getResourceModel()?.setCurrentContext(context) telemetry.success().send() } catch (e: Exception) { logger().warn( - "Could not set current context to ${context.context.name}.", e + "Could not set current context to ${context.name}.", e ) telemetry.error(e).send() } 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 e6e3a4daf..7d3f83a9e 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 @@ -21,6 +21,8 @@ import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext import com.redhat.devtools.intellij.kubernetes.model.context.IContext import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind import com.redhat.devtools.intellij.kubernetes.model.util.ResettableLazyProperty +import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException +import com.redhat.devtools.intellij.kubernetes.model.util.toMessage import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.NAME_PREFIX_CONTEXT import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService.PROP_IS_OPENSHIFT @@ -113,7 +115,7 @@ open class AllContexts( val all = createContexts(client.get(), client.get()?.config) _all.addAll(all) } catch (e: Exception) { - // + throw ResourceException("Could not parse kube config: ${toMessage(e)}", e) } } return _all @@ -124,7 +126,7 @@ open class AllContexts( if (current == context) { return current } - val newClient = clientFactory.invoke(context.context.context.namespace, context.context.name) + val newClient = clientFactory.invoke(context.namespace, context.name) val new = setCurrentContext(newClient, emptyList()) if (new != null) { modelChange.fireAllContextsChanged() @@ -134,7 +136,7 @@ open class AllContexts( override fun setCurrentNamespace(namespace: String): IActiveContext? { val old = this.current ?: return null - val newClient = clientFactory.invoke(namespace, old.context.name) + val newClient = clientFactory.invoke(namespace, old.name) val new = setCurrentContext(newClient, old.getWatched()) if (new != null) { modelChange.fireCurrentNamespaceChanged(new, old) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/ActiveContext.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/ActiveContext.kt index ced25a804..0d9138412 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/ActiveContext.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/ActiveContext.kt @@ -487,7 +487,7 @@ abstract class ActiveContext( } override fun close() { - logger>().debug("Closing context ${context.name}.") + logger>().debug("Closing context $name.") watch.close() dashboard.close() } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/Context.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/Context.kt index 9d2c50d95..9d6a0c0d0 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/Context.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/Context.kt @@ -13,10 +13,15 @@ package com.redhat.devtools.intellij.kubernetes.model.context import io.fabric8.kubernetes.api.model.NamedContext interface IContext { - val context: NamedContext val active: Boolean + val name: String? + val namespace: String? } -open class Context(override val context: NamedContext): IContext { +open class Context(private val context: NamedContext): IContext { override val active: Boolean = false -} \ No newline at end of file + override val name: String? + get() = context.name + override val namespace: String? + get() = context.context?.namespace +} 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..6f1b0c18d 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 @@ -65,10 +65,6 @@ interface IActiveContext: IContext { } } - val name: String? - get() { - return context.name - } /** * The master url for this context. This is the url of the cluster for this context. */ diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/KubernetesDescriptors.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/KubernetesDescriptors.kt index 1bd34bb38..40927b30c 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/KubernetesDescriptors.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/KubernetesDescriptors.kt @@ -50,7 +50,13 @@ import javax.swing.Icon object KubernetesDescriptors { - fun createDescriptor(element: Any, childrenKind: ResourceKind?, parent: NodeDescriptor<*>?, model: IResourceModel, project: Project): NodeDescriptor<*>? { + fun createDescriptor( + element: Any, + childrenKind: ResourceKind?, + parent: NodeDescriptor<*>?, + model: IResourceModel, + project: Project + ): NodeDescriptor<*>? { return when { element is DescriptorFactory<*> -> element.create(parent, model, project) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/TreeStructure.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/TreeStructure.kt index 842aa5e38..a59ab9f46 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/TreeStructure.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/TreeStructure.kt @@ -70,7 +70,7 @@ open class TreeStructure( private fun getChildElements(element: Any, contribution: ITreeStructureContribution): Collection { return try { contribution.getChildElements(element) - } catch (e: java.lang.Exception) { + } catch (e: Exception) { logger().warn(e) listOf(e) } @@ -101,24 +101,31 @@ open class TreeStructure( } override fun createDescriptor(element: Any, parent: NodeDescriptor<*>?): NodeDescriptor<*> { - val descriptor: NodeDescriptor<*>? = + return try { + val descriptor: NodeDescriptor<*>? = getValidContributions() - .map { it.createDescriptor(element, parent, project) } - .find { it != null } - if (descriptor != null) { - return descriptor - } - return when (element) { - is IContext -> ContextDescriptor(element, parent, model, project) - is Exception -> ErrorDescriptor(element, parent, model, project) - is Folder -> FolderDescriptor(element, parent, model, project) - else -> Descriptor(element, null, parent, model, project) + .map { it.createDescriptor(element, parent, project) } + .find { it != null } + descriptor ?: when (element) { + is IContext -> ContextDescriptor(element, parent, model, project) + is Exception -> ErrorDescriptor(element, parent, model, project) + is Folder -> FolderDescriptor(element, parent, model, project) + else -> Descriptor(element, null, parent, model, project) + } + } catch (e: Exception) { + ErrorDescriptor(e, parent, model, project) } } private fun getValidContributions(): Collection { return contributions - .filter { it.canContribute() } + .filter { + try { + it.canContribute() + } catch (e: Exception) { + false + } + } } private fun getTreeStructureExtensions(model: IResourceModel): List { @@ -167,11 +174,7 @@ open class TreeStructure( project ) { override fun getLabel(element: C?): String { - return if (element?.context?.name == null) { - "" - } else { - element.context.name - } + return element?.name ?: "" } override fun getIcon(element: C): Icon? { @@ -237,26 +240,22 @@ open class TreeStructure( } private class ErrorDescriptor( - exception: java.lang.Exception, + exception: Exception, parent: NodeDescriptor<*>?, model: IResourceModel, project: Project - ) : Descriptor( + ) : Descriptor( exception, null, parent, model, project ) { - override fun getLabel(element: java.lang.Exception?): String { - return getMessage(element) - } - - private fun getMessage(e: Exception?): String { - return toMessage(e) + override fun getLabel(element: Exception?): String { + return toMessage(element) } - override fun getIcon(element: java.lang.Exception): Icon { + override fun getIcon(element: Exception): Icon { return AllIcons.General.BalloonError } } 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 bac829c99..0682baa01 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 @@ -22,7 +22,10 @@ import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter +import com.redhat.devtools.intellij.kubernetes.model.context.ActiveContext +import com.redhat.devtools.intellij.kubernetes.model.context.Context import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext +import com.redhat.devtools.intellij.kubernetes.model.context.IContext import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE1 import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE2 import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE3 @@ -46,6 +49,7 @@ 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 +import org.assertj.core.api.Assertions.tuple import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher @@ -53,443 +57,450 @@ import org.mockito.Mockito class AllContextsTest { - private val modelChange: IResourceModelObservable = mock() - private val namedContext1 = - namedContext("ctx1", NAMESPACE1.metadata.name, "cluster1", "user1") - private val namedContext2 = - namedContext("ctx2", NAMESPACE2.metadata.name, "cluster2", "user2") - private val namedContext3 = - namedContext("ctx3", NAMESPACE3.metadata.name, "cluster3", "user3") - private val currentContext = namedContext2 - private val namespace: Namespace = resource(namedContext2.context.namespace, null, "someNamespaceUid", "v1") - private val activeContext: IActiveContext = activeContext(namespace, currentContext) - private val contextFactory: (ClientAdapter, IResourceModelObservable) -> IActiveContext = - Mocks.contextFactory(activeContext) - private val contexts = listOf(namedContext1, currentContext, namedContext3) - private val token = "42" - private val configuration = mock() { - on { currentContext } doReturn this@AllContextsTest.currentContext - on { contexts } doReturn contexts - on { oauthToken } doReturn token - } - private val client = client(true) - private val clientConfig = clientConfig(currentContext, contexts) - private val clientAdapter = clientAdapter(clientConfig, client) - private val clientFactory = clientFactory(clientAdapter) - - 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 - // when - allContexts.refresh() - // then - verify(activeContext).close() - } - - @Test - fun `#refresh() causes contexts be reloaded from client config`() { - // given - val oldAll = listOf(*allContexts.all.toTypedArray()) - val newAll = listOf(namedContext3, namedContext2, namedContext1) - assertThat(oldAll).isNotEqualTo(newAll) - doReturn(newAll) - .whenever(clientConfig).allContexts - // when - allContexts.refresh() - // then - val namedContexts = allContexts.all - .map { context -> - context.context - } - assertThat(namedContexts).containsExactlyElementsOf(newAll) - } - - @Test - fun `#refresh() causes current context to be reloaded from client config`() { - // given - val oldCurrent = allContexts.current?.context - val newCurrent = namedContext1 - assertThat(oldCurrent).isNotEqualTo(newCurrent) - doReturn(activeContext(resource(newCurrent.context.namespace, apiVersion = "v1"), newCurrent)) - .whenever(contextFactory).invoke(any(), any()) - // when - allContexts.refresh() - // then - assertThat(allContexts.current?.context).isEqualTo(newCurrent) - } - - @Test - fun `#refresh() should fire all contexts moified`() { - // given - // when - allContexts.refresh() - // then - verify(modelChange).fireAllContextsChanged() - } - - @Test - fun `#all should not load twice`() { - // given - // when - allContexts.all - allContexts.all - // then - verify(clientConfig, times(1)).allContexts - } - - @Test - fun `#current should NOT create new context if there's an active context in list of all contexts`() { - // given - doReturn(listOf(namedContext1, namedContext2, namedContext3)) - .whenever(clientConfig).allContexts - doReturn(namedContext1) - .whenever(clientConfig).currentContext - allContexts.all // create list of all contexts - clearInvocations(contextFactory) - // when - allContexts.current - // then - verify(contextFactory, never()).invoke(any(), any()) - } - - @Test - fun `#current() should create new context if #refresh() was called before`() { - // given - allContexts.current // trigger creation of context - allContexts.refresh() - clearInvocations(contextFactory) - // when - allContexts.current - // then - verify(contextFactory).invoke(any(), any()) // anyOrNull() bcs NamedContext is nullable - } - - @Test - fun `#setCurrentContext(context) should NOT create new active context if same context is already set`() { - // given - allContexts.current // create current context - clearInvocations(contextFactory) // clear invocation so that it's not counted - // when - allContexts.setCurrentContext(activeContext) - // then - verify(contextFactory, never()).invoke(any(), any()) - } - - @Test - fun `#setCurrentContext(context) should create new active context`() { - // given - assertThat(allContexts.current?.context).isNotEqualTo(namedContext3) // create current context - clearInvocations(contextFactory) // clear invocation so that it's not counted - // when - allContexts.setCurrentContext(context(namedContext3)) - // then - allContexts.all // reload contexts - verify(contextFactory).invoke(any(), any()) - } - - @Test - fun `#setCurrentContext(context) should replace existing context in list of all contexts`() { - // given - val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) - val newCurrentContext = context(namedContext1) - assertThat(allContexts.current).isNotEqualTo(newCurrentContext) - val old = allContexts.current - assertThat(old).isNotEqualTo(newCurrentContext) - allContexts.all // create all contexts - val activeContext = activeContext(resource(newCurrentContext.context.context.namespace), newCurrentContext.context) - /** - * Trying to use {@code com.nhaarman.mockitokotlin2.doReturn} leads to - * "Overload Resolution Ambiguity" with {@code org.mockito.Mockito.doReturn} in intellij. - * Gradle compiles it just fine - * - * @see KT-22961 - * @see fix-overload-resolution-ambiguity - */ - Mockito.doReturn(activeContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - // when - val currentContext = allContexts.setCurrentContext(newCurrentContext) - // then - assertThat(allContexts.all).contains(currentContext) - assertThat(allContexts.all).doesNotContain(old) - } - - @Test - fun `#setCurrentContext(context) should close current context`() { - // given - allContexts.all // create all contexts - val newCurrentContext = activeContext(namespace, namedContext3) - val currentContext = allContexts.current!! - Mockito.doReturn(newCurrentContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - // when - allContexts.setCurrentContext(newCurrentContext) - // then - verify(currentContext).close() - } - - @Test - fun `#setCurrentContext(context) should create new client`() { - // given - allContexts.all // create all contexts - val newCurrentContext = activeContext(namespace, namedContext3) - Mockito.doReturn(newCurrentContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - clearInvocations(clientFactory) - // when - allContexts.setCurrentContext(newCurrentContext) - // then - verify(clientFactory).invoke(anyOrNull(), anyOrNull()) - } - - @Test - fun `#setCurrentContext(context) should fireAllContextsChanged`() { - // given - allContexts.all // create all contexts - val newCurrentContext = activeContext(namespace, namedContext3) - Mockito.doReturn(newCurrentContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - // when - allContexts.setCurrentContext(newCurrentContext) - // then - verify(modelChange).fireAllContextsChanged() - } - - @Test - fun `#setCurrentContext(context) should NOT watch existing kinds on new context`() { - // given - allContexts.all // create all contexts - Mockito.doReturn(listOf( - mock>(), - mock>())) - .`when`(activeContext).getWatched() - val newCurrentContext = activeContext(namespace, namedContext3) - Mockito.doReturn(newCurrentContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - // when - allContexts.setCurrentContext(newCurrentContext) - // then - verify(newCurrentContext, never()).watch(any>()) - } - - @Test - fun `#setCurrentContext(context) should save client config `() { - // given - allContexts.all // create all contexts - val newCurrentContext = activeContext(namespace, namedContext3) - Mockito.doReturn(newCurrentContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - // when - allContexts.setCurrentContext(newCurrentContext) - // then - verify(clientConfig).save() - } - - @Test - fun `#setCurrentContext(context) should cause all contexts to be reloaded from client`() { - // given - allContexts.all // create all contexts - val newCurrentContext = activeContext(namespace, namedContext3) - Mockito.doReturn(newCurrentContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - clearInvocations(clientConfig) - // when - allContexts.setCurrentContext(newCurrentContext) - allContexts.all // cause reload - // then - verify(clientConfig).allContexts - } - - @Test - fun `#setCurrentNamespace(namespace) should watch existing kinds on new context`() { - // given - allContexts.all // create all contexts - val podKind = mock>() - val deploymentKind = mock>() - Mockito.doReturn(listOf(podKind, deploymentKind)) - .`when`(activeContext).getWatched() - val newCurrentContext = activeContext(namespace, namedContext3) - Mockito.doReturn(newCurrentContext) - .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call - // when - allContexts.setCurrentNamespace("darth-vader") - // then - verify(newCurrentContext).watchAll(argThat(ArgumentMatcher { - it.contains(podKind) - && it.contains(deploymentKind) - && it.size == 2 - })) - } - - @Test - fun `#setCurrentNamespace(namespace) should return null if current context is null`() { - // given - val clientConfig = clientConfig(null, contexts) - val client = client(true) - val clientAdapter = clientAdapter(clientConfig, client) - val clientFactory = clientFactory(clientAdapter) - val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) - // when - val newContext = allContexts.setCurrentNamespace("rebellion") - // then - assertThat(newContext).isNull() - } - - @Test - fun `#setCurrentNamespace(namespace) should observable#fireCurrentNamespaceChanged`() { - // given - // when - allContexts.setCurrentNamespace("dark side") - // then - verify(modelChange).fireCurrentNamespaceChanged(anyOrNull(), anyOrNull()) - } - - @Test - fun `#setCurrentNamespace(namespace) should NOT observable#fireCurrentNamespaceChanged if new current context is null`() { - // given - val client = client(true) - val clientAdapter = clientAdapter(null, client) // no config so there are no contexts - val clientFactory = clientFactory(clientAdapter) - val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) - // when - allContexts.setCurrentNamespace("dark side") - // then - verify(modelChange, never()).fireCurrentNamespaceChanged(anyOrNull(), anyOrNull()) - } - - @Test - fun `#setCurrentNamespace(namespace) should create client with given namespace`() { - // given - // when - allContexts.setCurrentNamespace("dark side") - // then - val namespace = ArgumentCaptor.forClass(String::class.java) - val contextName = ArgumentCaptor.forClass(String::class.java) - // 2x: init, setCurrentContext - verify(clientFactory, times(2)).invoke(namespace.capture(), contextName.capture()) - assertThat(namespace.allValues[1]).isEqualTo("dark side") - assertThat(contextName.allValues[1]).isEqualTo(activeContext.context.name) - } - - @Test - fun `#onKubeConfigChanged() should NOT fire if new config is null`() { - // given - // when - allContexts.onKubeConfigChanged(null) - // then - verify(modelChange, never()).fireAllContextsChanged() - } - - @Test - fun `#onKubeConfigChanged() should NOT fire if existing config and given config are equal`() { - // given - val updated = mock() - doReturn(true) - .whenever(clientConfig).isEqualConfig(any()) - // when - allContexts.onKubeConfigChanged(updated) - // then - verify(modelChange, never()).fireAllContextsChanged() - } - - @Test - fun `#onKubeConfigChanged() should fire if existing config and given config are not equal`() { - // given - val updated = mock() - doReturn(false) - .whenever(clientConfig).isEqualConfig(any()) - // when - allContexts.onKubeConfigChanged(updated) - // then - verify(modelChange).fireAllContextsChanged() - } - - @Test - fun `#onKubeConfigChanged() should close current context if existing config and given config are not equal`() { - // given - val updated = mock() - doReturn(false) - .whenever(clientConfig).isEqualConfig(any()) - // when - allContexts.onKubeConfigChanged(updated) - // then - verify(activeContext).close() - } - - @Test - fun `#onKubeConfigChanged() should get all contexts (again) if existing config and given config are not equal`() { - // given - val updated = mock() - doReturn(false) - .whenever(clientConfig).isEqualConfig(any()) - // when - allContexts.onKubeConfigChanged(updated) - // then - verify(clientConfig).allContexts - } - - /** - * Returns a client mock that answers with the given boolean to the call - * [client.namespaces().withName("").isReady] - * - * @param namespaceResourceIsReady the boolean to return to the call - * @return a client mock that answers to the query if a given namespace is ready - */ - private fun client(namespaceResourceIsReady: Boolean): KubernetesClient { - val namespaceResource: Resource = mock { - on { isReady } doReturn namespaceResourceIsReady - } - val namespacesOperation: NonNamespaceOperation> = mock { - on { withName(any()) } doReturn namespaceResource - } - return client(namespacesOperation) - } - - private fun client(namespacesOp: NonNamespaceOperation>): KubernetesClient { - return mock { - on { namespaces() } doReturn namespacesOp - } - } - - private fun client(e: KubernetesClientException): KubernetesClient { - return mock { - on { namespaces() } doThrow e - } - } - - private class TestableAllContexts( + private val modelChange: IResourceModelObservable = mock() + private val namedContext1 = + namedContext("ctx1", NAMESPACE1.metadata.name, "cluster1", "user1") + private val namedContext2 = + namedContext("ctx2", NAMESPACE2.metadata.name, "cluster2", "user2") + private val namedContext3 = + namedContext("ctx3", NAMESPACE3.metadata.name, "cluster3", "user3") + private val currentContext = namedContext2 + private val namespace: Namespace = resource(namedContext2.context.namespace, null, "someNamespaceUid", "v1") + private val activeContext: IActiveContext = activeContext(namespace, currentContext) + private val contextFactory: (ClientAdapter, IResourceModelObservable) -> IActiveContext = + Mocks.contextFactory(activeContext) + private val contexts = listOf(namedContext1, currentContext, namedContext3) + private val token = "42" + private val configuration = mock() { + on { currentContext } doReturn this@AllContextsTest.currentContext + on { contexts } doReturn contexts + on { oauthToken } doReturn token + } + private val client = client(true) + private val clientConfig = clientConfig(currentContext, contexts) + private val clientAdapter = clientAdapter(clientConfig, client) + private val clientFactory = clientFactory(clientAdapter) + + 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 + // when + allContexts.refresh() + // then + verify(activeContext).close() + } + + @Test + fun `#refresh() causes contexts be reloaded from client config`() { + // given + val newNamedContexts = listOf(namedContext3, namedContext2, namedContext1) + doReturn(newNamedContexts) + .whenever(clientConfig).allContexts + val newContexts = newNamedContexts.map { namedContext -> + Context(namedContext) + } + // when + allContexts.refresh() + // then + assertThat(allContexts.all) + .extracting(IContext::name, IContext::namespace) + .containsExactly( + tuple(newNamedContexts[0].name, newNamedContexts[0].context.namespace), + tuple(newNamedContexts[1].name, newNamedContexts[1].context.namespace), + tuple(newNamedContexts[2].name, newNamedContexts[2].context.namespace) + ) + } + + @Test + fun `#refresh() causes current context to be reloaded from client config`() { + // given + val newCurrent = namedContext1 + doReturn(activeContext(resource(newCurrent.context.namespace, apiVersion = "v1"), newCurrent)) + .whenever(contextFactory).invoke(any(), any()) + // when + allContexts.refresh() + // then + assertThat(allContexts.current) + .matches { context -> context?.name == newCurrent.name } + .matches { context -> context?.namespace == newCurrent.context.namespace } + } + + @Test + fun `#refresh() should fire all contexts modified`() { + // given + // when + allContexts.refresh() + // then + verify(modelChange).fireAllContextsChanged() + } + + @Test + fun `#all should not load twice`() { + // given + // when + allContexts.all + allContexts.all + // then + verify(clientConfig, times(1)).allContexts + } + + @Test + fun `#current should NOT create new context if there's an active context in list of all contexts`() { + // given + doReturn(listOf(namedContext1, namedContext2, namedContext3)) + .whenever(clientConfig).allContexts + doReturn(namedContext1) + .whenever(clientConfig).currentContext + allContexts.all // create list of all contexts + clearInvocations(contextFactory) + // when + allContexts.current + // then + verify(contextFactory, never()).invoke(any(), any()) + } + + @Test + fun `#current() should create new context if #refresh() was called before`() { + // given + allContexts.current // trigger creation of context + allContexts.refresh() + clearInvocations(contextFactory) + // when + allContexts.current + // then + verify(contextFactory).invoke(any(), any()) // anyOrNull() bcs NamedContext is nullable + } + + @Test + fun `#setCurrentContext(context) should NOT create new active context if same context is already set`() { + // given + allContexts.current // create current context + clearInvocations(contextFactory) // clear invocation so that it's not counted + // when + allContexts.setCurrentContext(activeContext) + // then + verify(contextFactory, never()).invoke(any(), any()) + } + + @Test + fun `#setCurrentContext(context) should create new active context`() { + // given + assertThat(allContexts.current?.name).isNotEqualTo(namedContext3.name) // create current context + clearInvocations(contextFactory) // clear invocation so that it's not counted + // when + allContexts.setCurrentContext(context(namedContext3)) + // then + allContexts.all // reload contexts + verify(contextFactory).invoke(any(), any()) + } + + @Test + fun `#setCurrentContext(context) should replace existing context in list of all contexts`() { + // given + val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) + val newCurrentContext = context(namedContext1) + assertThat(allContexts.current).isNotEqualTo(newCurrentContext) + val old = allContexts.current + assertThat(old).isNotEqualTo(newCurrentContext) + allContexts.all // create all contexts + val activeContext = activeContext(resource(newCurrentContext.namespace!!), namedContext1) + /** + * Trying to use {@code com.nhaarman.mockitokotlin2.doReturn} leads to + * "Overload Resolution Ambiguity" with {@code org.mockito.Mockito.doReturn} in intellij. + * Gradle compiles it just fine + * + * @see KT-22961 + * @see fix-overload-resolution-ambiguity + */ + Mockito.doReturn(activeContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + // when + val currentContext = allContexts.setCurrentContext(newCurrentContext) + // then + assertThat(allContexts.all).contains(currentContext) + assertThat(allContexts.all).doesNotContain(old) + } + + + @Test + fun `#setCurrentContext(context) should close current context`() { + // given + allContexts.all // create all contexts + val newCurrentContext = activeContext(namespace, namedContext3) + val currentContext = allContexts.current!! + Mockito.doReturn(newCurrentContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + // when + allContexts.setCurrentContext(newCurrentContext) + // then + verify(currentContext).close() + } + + @Test + fun `#setCurrentContext(context) should create new client`() { + // given + allContexts.all // create all contexts + val newCurrentContext = activeContext(namespace, namedContext3) + Mockito.doReturn(newCurrentContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + clearInvocations(clientFactory) + // when + allContexts.setCurrentContext(newCurrentContext) + // then + verify(clientFactory).invoke(anyOrNull(), anyOrNull()) + } + + @Test + fun `#setCurrentContext(context) should fireAllContextsChanged`() { + // given + allContexts.all // create all contexts + val newCurrentContext = activeContext(namespace, namedContext3) + Mockito.doReturn(newCurrentContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + // when + allContexts.setCurrentContext(newCurrentContext) + // then + verify(modelChange).fireAllContextsChanged() + } + + @Test + fun `#setCurrentContext(context) should NOT watch existing kinds on new context`() { + // given + allContexts.all // create all contexts + Mockito.doReturn( + listOf( + mock>(), + mock>() + ) + ) + .`when`(activeContext).getWatched() + val newCurrentContext = activeContext(namespace, namedContext3) + Mockito.doReturn(newCurrentContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + // when + allContexts.setCurrentContext(newCurrentContext) + // then + verify(newCurrentContext, never()).watch(any>()) + } + + @Test + fun `#setCurrentContext(context) should save client config `() { + // given + allContexts.all // create all contexts + val newCurrentContext = activeContext(namespace, namedContext3) + Mockito.doReturn(newCurrentContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + // when + allContexts.setCurrentContext(newCurrentContext) + // then + verify(clientConfig).save() + } + + @Test + fun `#setCurrentContext(context) should cause all contexts to be reloaded from client`() { + // given + allContexts.all // create all contexts + val newCurrentContext = activeContext(namespace, namedContext3) + Mockito.doReturn(newCurrentContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + clearInvocations(clientConfig) + // when + allContexts.setCurrentContext(newCurrentContext) + allContexts.all // cause reload + // then + verify(clientConfig).allContexts + } + + @Test + fun `#setCurrentNamespace(namespace) should watch existing kinds on new context`() { + // given + allContexts.all // create all contexts + val podKind = mock>() + val deploymentKind = mock>() + Mockito.doReturn(listOf(podKind, deploymentKind)) + .`when`(activeContext).getWatched() + val newCurrentContext = activeContext(namespace, namedContext3) + Mockito.doReturn(newCurrentContext) + .`when`(contextFactory).invoke(any(), any()) // returned on 2nd call + // when + allContexts.setCurrentNamespace("darth-vader") + // then + verify(newCurrentContext).watchAll(argThat(ArgumentMatcher { + it.contains(podKind) + && it.contains(deploymentKind) + && it.size == 2 + })) + } + + @Test + fun `#setCurrentNamespace(namespace) should return null if current context is null`() { + // given + val clientConfig = clientConfig(null, contexts) + val client = client(true) + val clientAdapter = clientAdapter(clientConfig, client) + val clientFactory = clientFactory(clientAdapter) + val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) + // when + val newContext = allContexts.setCurrentNamespace("rebellion") + // then + assertThat(newContext).isNull() + } + + @Test + fun `#setCurrentNamespace(namespace) should observable#fireCurrentNamespaceChanged`() { + // given + // when + allContexts.setCurrentNamespace("dark side") + // then + verify(modelChange).fireCurrentNamespaceChanged(anyOrNull(), anyOrNull()) + } + + @Test + fun `#setCurrentNamespace(namespace) should NOT observable#fireCurrentNamespaceChanged if new current context is null`() { + // given + val client = client(true) + val clientAdapter = clientAdapter(null, client) // no config so there are no contexts + val clientFactory = clientFactory(clientAdapter) + val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) + // when + allContexts.setCurrentNamespace("dark side") + // then + verify(modelChange, never()).fireCurrentNamespaceChanged(anyOrNull(), anyOrNull()) + } + + @Test + fun `#setCurrentNamespace(namespace) should create client with given namespace`() { + // given + // when + allContexts.setCurrentNamespace("dark side") + // then + val namespace = ArgumentCaptor.forClass(String::class.java) + val contextName = ArgumentCaptor.forClass(String::class.java) + // 2x: init, setCurrentContext + verify(clientFactory, times(2)).invoke(namespace.capture(), contextName.capture()) + assertThat(namespace.allValues[1]).isEqualTo("dark side") + assertThat(contextName.allValues[1]).isEqualTo(activeContext.name) + } + + @Test + fun `#onKubeConfigChanged() should NOT fire if new config is null`() { + // given + // when + allContexts.onKubeConfigChanged(null) + // then + verify(modelChange, never()).fireAllContextsChanged() + } + + @Test + fun `#onKubeConfigChanged() should NOT fire if existing config and given config are equal`() { + // given + val updated = mock() + doReturn(true) + .whenever(clientConfig).isEqualConfig(any()) + // when + allContexts.onKubeConfigChanged(updated) + // then + verify(modelChange, never()).fireAllContextsChanged() + } + + @Test + fun `#onKubeConfigChanged() should fire if existing config and given config are not equal`() { + // given + val updated = mock() + doReturn(false) + .whenever(clientConfig).isEqualConfig(any()) + // when + allContexts.onKubeConfigChanged(updated) + // then + verify(modelChange).fireAllContextsChanged() + } + + @Test + fun `#onKubeConfigChanged() should close current context if existing config and given config are not equal`() { + // given + val updated = mock() + doReturn(false) + .whenever(clientConfig).isEqualConfig(any()) + // when + allContexts.onKubeConfigChanged(updated) + // then + verify(activeContext).close() + } + + @Test + fun `#onKubeConfigChanged() should get all contexts (again) if existing config and given config are not equal`() { + // given + val updated = mock() + doReturn(false) + .whenever(clientConfig).isEqualConfig(any()) + // when + allContexts.onKubeConfigChanged(updated) + // then + verify(clientConfig).allContexts + } + + /** + * Returns a client mock that answers with the given boolean to the call + * [client.namespaces().withName("").isReady] + * + * @param namespaceResourceIsReady the boolean to return to the call + * @return a client mock that answers to the query if a given namespace is ready + */ + private fun client(namespaceResourceIsReady: Boolean): KubernetesClient { + val namespaceResource: Resource = mock { + on { isReady } doReturn namespaceResourceIsReady + } + val namespacesOperation: NonNamespaceOperation> = mock { + on { withName(any()) } doReturn namespaceResource + } + return client(namespacesOperation) + } + + private fun client(namespacesOp: NonNamespaceOperation>): KubernetesClient { + return mock { + on { namespaces() } doReturn namespacesOp + } + } + + private fun client(e: KubernetesClientException): KubernetesClient { + return mock { + on { namespaces() } doThrow e + } + } + + private class TestableAllContexts( modelChange: IResourceModelObservable, contextFactory: (ClientAdapter, IResourceModelObservable) -> IActiveContext, - clientFactory: (String?, String?) -> ClientAdapter - ) : AllContexts(contextFactory, modelChange, clientFactory) { + clientFactory: (String?, String?) -> ClientAdapter + ) : AllContexts(contextFactory, modelChange, clientFactory) { - var watchStarted = false + var watchStarted = false - public override fun onKubeConfigChanged(updated: io.fabric8.kubernetes.client.Config?) { - super.onKubeConfigChanged(updated) - } + public override fun onKubeConfigChanged(updated: io.fabric8.kubernetes.client.Config?) { + super.onKubeConfigChanged(updated) + } - override fun reportTelemetry(context: IActiveContext) { - // prevent telemetry reporting - } + override fun reportTelemetry(context: IActiveContext) { + // prevent telemetry reporting + } - override fun runAsync(runnable: () -> Unit) { - runnable.invoke() // run directly, not in IDEA pooled threads - } + override fun runAsync(runnable: () -> Unit) { + runnable.invoke() // run directly, not in IDEA pooled threads + } - override fun watchKubeConfigs() { - // don't watch filesystem (override super method) - watchStarted = true - } - } + override fun watchKubeConfigs() { + // don't watch filesystem (override super method) + watchStarted = true + } + } } \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModelTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModelTest.kt index 2907fd980..4b3f3cf98 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModelTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModelTest.kt @@ -39,16 +39,18 @@ import java.util.function.Predicate class ResourceModelTest { - private val modelChange: IResourceModelObservable = mock() - private val namespace: Namespace = resource("papa smurf", null, "papaUid", "v1") - private val activeContext: IActiveContext = activeContext(namespace, mock()) - private val namedContext1 = - namedContext("ctx1", "namespace1", "cluster1", "user1") + namedContext("ctx1", "namespace1", "cluster1", "user1") private val namedContext2 = - namedContext("ctx2", "namespace2", "cluster2", "user2") + namedContext("ctx2", "namespace2", "cluster2", "user2") private val namedContext3 = - namedContext("ctx3", "namespace3", "cluster3", "user3") + namedContext("ctx3", "namespace3", "cluster3", "user3") + + private val namespace: Namespace = resource("papa smurf", null, "papaUid", "v1") + + private val activeContext: IActiveContext = activeContext(namespace, namedContext2) + + private val modelChange: IResourceModelObservable = mock() private val allContexts = createContexts(activeContext, listOf( context(namedContext1), diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/Mocks.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/Mocks.kt index 6b13f388f..a07b9fa00 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/Mocks.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/Mocks.kt @@ -70,12 +70,13 @@ object Mocks { } } - fun context(namedContext: NamedContext) - : IContext { - return mock { - Mockito.doReturn(namedContext) - .`when`(mock).context - } + fun context(namedContext: NamedContext): IContext { + val context = mock() + doReturn(namedContext.name) + .whenever(context).name + doReturn(namedContext.context.namespace) + .whenever(context).namespace + return context } fun activeContext(currentNamespace: Namespace, context: NamedContext) @@ -83,8 +84,10 @@ object Mocks { val mock = mock>() doReturn(currentNamespace.metadata.name) .whenever(mock).getCurrentNamespace() - doReturn(context) - .whenever(mock).context + doReturn(context.name) + .whenever(mock).name + doReturn(context.context.namespace) + .whenever(mock).namespace doReturn(true) .whenever(mock).active return mock