From 8d9abeb2fc3053f16c3efefd334e6dba2061bfb4 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 16 Apr 2025 22:19:43 +0200 Subject: [PATCH 1/3] removed leftover system.err debug output Signed-off-by: Andre Dietisheim --- .../kubernetes/model/resource/openshift/OpenShiftReplicas.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/openshift/OpenShiftReplicas.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/openshift/OpenShiftReplicas.kt index 119afeb41..05f8ed3a2 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/openshift/OpenShiftReplicas.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/openshift/OpenShiftReplicas.kt @@ -64,10 +64,6 @@ class OpenShiftReplicas(resourceOperator: NonCachingSingleResourceOperator, getA private fun getDeploymentConfig(replicationController: ReplicationController): DeploymentConfig? { return getAllResources.getAll(DeploymentConfigsOperator.KIND, IActiveContext.ResourcesIn.CURRENT_NAMESPACE) .firstOrNull { deploymentConfig -> -System.err.println("dc =" + - "\nannotations: ${deploymentConfig.metadata.annotations.entries.joinToString { it.toString() } }" + - "\nlabels: ${deploymentConfig.metadata.labels.entries.joinToString { it.toString() } }" - ) DeploymentConfigForReplicationController(replicationController).test(deploymentConfig) } } From a3d0be32e0dafa561969a443928f6176187a2e05 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 16 Apr 2025 22:30:43 +0200 Subject: [PATCH 2/3] removed unused method OperatorFactory#createAll Signed-off-by: Andre Dietisheim --- .../kubernetes/model/resource/OperatorFactory.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/OperatorFactory.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/OperatorFactory.kt index 61a3dedde..876a6668b 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/OperatorFactory.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/OperatorFactory.kt @@ -105,15 +105,6 @@ object OperatorFactory { return openshift.map { it.second.invoke(client) } } - fun createAll(client: ClientAdapter): List>{ - return if (client.isOpenShift()) { - @Suppress("UNCHECKED_CAST") - createOpenShift(client as ClientAdapter) - } else { - createKubernetes(client) - } - } - inline fun > create( kind: ResourceKind, client: ClientAdapter From 2894d5d6e69026e7a01846cd03f7e8b8282bb8e8 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 16 Apr 2025 22:15:23 +0200 Subject: [PATCH 3/3] fix: create lazy OpenShift context to avoid check timeouts (#865) Signed-off-by: Andre Dietisheim --- .../intellij/kubernetes/model/AllContexts.kt | 5 +- .../kubernetes/model/ResourceModel.kt | 2 +- .../kubernetes/model/client/ClientAdapter.kt | 42 +++- .../kubernetes/model/context/ActiveContext.kt | 23 ++- .../kubernetes/model/context/Context.kt | 2 +- .../model/context/IActiveContext.kt | 48 +++-- .../model/context/KubernetesContext.kt | 15 +- .../model/context/LazyOpenShiftContext.kt | 112 ++++++++++ .../model/context/OpenShiftContext.kt | 14 +- .../kubernetes/tree/KubernetesDescriptors.kt | 39 +++- .../kubernetes/tree/OpenShiftDescriptors.kt | 24 ++- .../intellij/kubernetes/tree/TreeStructure.kt | 12 +- .../kubernetes/model/AllContextsTest.kt | 6 +- .../model/client/ClientAdapterTest.kt | 37 +++- .../model/context/KubernetesContextTest.kt | 4 +- .../model/context/LazyOpenShiftContextTest.kt | 194 ++++++++++++++++++ .../kubernetes/model/mocks/ClientMocks.kt | 4 +- .../intellij/kubernetes/model/mocks/Mocks.kt | 20 +- 18 files changed, 511 insertions(+), 92 deletions(-) create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContext.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContextTest.kt 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 1ad42f720..ca58cee9d 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 @@ -13,7 +13,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.ConfigWatcher -import com.redhat.devtools.intellij.common.utils.ExecHelper import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter import com.redhat.devtools.intellij.kubernetes.model.client.ClientConfig import com.redhat.devtools.intellij.kubernetes.model.context.Context @@ -77,7 +76,7 @@ interface IAllContexts { open class AllContexts( private val contextFactory: (ClientAdapter, IResourceModelObservable) -> IActiveContext? = - IActiveContext.Factory::create, + IActiveContext.Factory::createLazyOpenShift, private val modelChange: IResourceModelObservable, private val clientFactory: ( namespace: String?, @@ -223,7 +222,7 @@ open class AllContexts( } protected open fun reportTelemetry(context: IActiveContext) { - ExecHelper.submit { + runAsync { val telemetry = TelemetryService.instance.action(NAME_PREFIX_CONTEXT + "use") .property(PROP_IS_OPENSHIFT, context.isOpenShift().toString()) try { diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModel.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModel.kt index ba042cc21..a12b3cd4e 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModel.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceModel.kt @@ -83,7 +83,7 @@ open class ResourceModel : IResourceModel { } protected open val allContexts: IAllContexts by lazy { - AllContexts(IActiveContext.Factory::create, modelChange) + AllContexts(IActiveContext.Factory::createLazyOpenShift, modelChange) } override fun setCurrentContext(context: IContext) { diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt index 906620c25..e450f6f95 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapter.kt @@ -37,6 +37,10 @@ open class OSClientAdapter(client: OpenShiftClient, private val kubeClient: Kube ClientConfig(kubeClient.configuration) } + override fun toOpenShift(): OSClientAdapter { + return this + } + override fun isOpenShift(): Boolean { return true } @@ -52,17 +56,33 @@ open class KubeClientAdapter(client: KubernetesClient) : override fun isOpenShift(): Boolean { return false } + + override fun toOpenShift(): OSClientAdapter { + val kubeClient = get() + val osClient = kubeClient.adapt(NamespacedOpenShiftClient::class.java) + return OSClientAdapter(osClient, kubeClient) + } } abstract class ClientAdapter(private val fabric8Client: C) { companion object Factory { + const val TIMEOUT_CONNECTION = 5000 + const val TIMEOUT_REQUEST = 5000 + const val LIMIT_RECONNECT = 2 + fun create( namespace: String? = null, context: String? = null, clientBuilder: KubernetesClientBuilder? = null, - createConfig: (context: String?) -> Config = { context -> Config.autoConfigure(context) }, + createConfig: (context: String?) -> Config = { context -> + val config = Config.autoConfigure(context) + config.connectionTimeout = TIMEOUT_CONNECTION + config.requestTimeout = TIMEOUT_REQUEST + config.watchReconnectLimit = LIMIT_RECONNECT + config + }, externalTrustManagerProvider: ((toIntegrate: List) -> X509TrustManager)? = null ): ClientAdapter { KubeConfigEnvValue.copyToSystem() @@ -76,12 +96,14 @@ abstract class ClientAdapter(private val fabric8Client: C) setSslContext(httpClientBuilder, config, trustManager) } .build() - return if (ClusterHelper.isOpenShift(kubeClient)) { - val osClient = kubeClient.adapt(NamespacedOpenShiftClient::class.java) - OSClientAdapter(osClient, kubeClient) - } else { - KubeClientAdapter(kubeClient) - } + /** + * Always create kubernetes client. + * Upgrade existing client to OpenShift only async bcs checking if cluster is OpenShift is costly + * and may timeout if cluster is not reachable. + * @see [issue 865](https://github.com/redhat-developer/intellij-kubernetes/issues/865) + * @see ClientAdapter.toOpenShift + **/ + return KubeClientAdapter(kubeClient) } private fun setSslContext( @@ -114,6 +136,8 @@ abstract class ClientAdapter(private val fabric8Client: C) ClientConfig(fabric8Client.configuration) } + abstract fun toOpenShift(): OSClientAdapter + abstract fun isOpenShift(): Boolean fun get(): C { @@ -145,6 +169,10 @@ abstract class ClientAdapter(private val fabric8Client: C) } } + fun canAdaptToOpenShift(): Boolean { + return ClusterHelper.isOpenShift(fabric8Client) + } + open fun close() { clients.values.forEach{ it.close() } fabric8Client.close() 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 0d9138412..d48211de8 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 @@ -52,9 +52,9 @@ import java.net.URL abstract class ActiveContext( context: NamedContext, - private val modelChange: IResourceModelObservable, + protected val modelChange: IResourceModelObservable, val client: ClientAdapter, - protected open val dashboard: IDashboard, + protected open val dashboard: IDashboard? = null, private var singleResourceOperator: NonCachingSingleResourceOperator = NonCachingSingleResourceOperator(client), ) : Context(context), IActiveContext { @@ -72,8 +72,6 @@ abstract class ActiveContext( ClusterHelper.getClusterInfo(client.get()) } - protected abstract val namespaceKind : ResourceKind - private val extensionName: ExtensionPointName>> = ExtensionPointName("com.redhat.devtools.intellij.kubernetes.resourceOperators") @@ -307,7 +305,7 @@ abstract class ActiveContext( override fun stopWatch(kind: ResourceKind) { logger>().debug("Stop watching $kind resources.") watch.stopWatch(kind) - // don't notify invalidation change because this would cause UI to reload + // don't notify invalidation change because this would cause the UI to reload // and therefore to repopulate the cache immediately. // Any resource operation that eventually happens while the watch is not active would cause the cache // to become out-of-sync and it would therefore return invalid resources when asked to do so @@ -489,27 +487,32 @@ abstract class ActiveContext( override fun close() { logger>().debug("Closing context $name.") watch.close() - dashboard.close() + dashboard?.close() } private fun > getAllResourceOperators(type: Class

) : MutableMap, P> { val operators = mutableMapOf, P>() operators.putAll( - getInternalResourceOperators(client) + getInternalResourceOperators() .filterIsInstance(type) .associateBy { it.kind }) operators.putAll( - getExtensionResourceOperators(client) + getExtensionResourceOperators() .filterIsInstance(type) .associateBy { it.kind }) return operators } - protected abstract fun getInternalResourceOperators(client: ClientAdapter): List> + abstract override fun getInternalResourceOperators(): List> - protected open fun getExtensionResourceOperators(client: ClientAdapter): List> { + protected open fun getExtensionResourceOperators(): List> { return extensionName.extensionList .map { it.create(client.get()) } } + + override fun getDashboardUrl(): String? { + return dashboard?.get() + } + } 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 15d61a786..d7ded26bf 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 @@ -19,7 +19,7 @@ interface IContext { val namespace: String? } -open class Context(private val context: NamedContext): IContext { +open class Context(protected val context: NamedContext): IContext { override val active: Boolean = false override val name: String? get() = context.name 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 6f1b0c18d..d1cdcdb0b 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 @@ -15,6 +15,7 @@ import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter import com.redhat.devtools.intellij.kubernetes.model.client.KubeClientAdapter import com.redhat.devtools.intellij.kubernetes.model.client.OSClientAdapter +import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind import com.redhat.devtools.intellij.kubernetes.model.resource.kubernetes.KubernetesReplicas.Replicator import io.fabric8.kubernetes.api.model.GenericKubernetesResource @@ -23,30 +24,29 @@ import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.Watch import io.fabric8.kubernetes.client.Watcher +import io.fabric8.openshift.api.model.Project +import io.fabric8.openshift.client.OpenShiftClient import java.net.URL interface IActiveContext: IContext { companion object Factory { - fun create( + fun createLazyOpenShift( client: ClientAdapter, - observable: IResourceModelObservable + modelChange: IResourceModelObservable ): IActiveContext? { val currentContext = client.config.currentContext ?: return null - return if (client.isOpenShift()) { - OpenShiftContext( - currentContext, - observable, - client as OSClientAdapter - ) - } else { - KubernetesContext( - currentContext, - observable, - client as KubeClientAdapter - ) - } + return LazyOpenShiftContext(currentContext, modelChange, client as KubeClientAdapter) } + + fun createOpenShift( + client: OSClientAdapter, + modelChange: IResourceModelObservable + ): IActiveContext? { + val currentContext = client.config.currentContext ?: return null + return OpenShiftContext(currentContext, modelChange, client) + } + } /** @@ -65,6 +65,8 @@ interface IActiveContext: IContext { } } + val namespaceKind : ResourceKind + /** * The master url for this context. This is the url of the cluster for this context. */ @@ -75,6 +77,16 @@ interface IActiveContext: IContext { */ val version: ClusterInfo + /** + * Returns the list of internal [IResourceOperator]s that are available in this context. + * [IResourceOperator]s that are contributed by registrations to the extension point are not included. + * + * @return the list of [IResourceOperator]s that are available in this context. + * @see IResourceOperator + * @see com.redhat.devtools.intellij.kubernetes.model.context.ActiveContext.getExtensionResourceOperators + */ + fun getInternalResourceOperators(): List> + /** * Returns {@code true} if this context is an OpenShift context. This is true for context with an OpenShift cluster. */ @@ -129,7 +141,7 @@ interface IActiveContext: IContext { fun getAllResources(definition: CustomResourceDefinition): Collection /** - * Returns the latest version of the given resource from cluster. Returns `null` if none was found. + * Returns the latest version of the given resource from the cluster. Returns `null` if none was found. * * @param resource which is to be requested from cluster * @@ -265,7 +277,7 @@ interface IActiveContext: IContext { /** * Notifies the context that the given resource was replaced in the cluster. * Replaces the resource with the given new version if it exists. - * Does nothing otherwiese. + * Does nothing otherwise. * * * @param resource the new (version) of the resource @@ -279,7 +291,7 @@ interface IActiveContext: IContext { * * @return the url of the Dashboard for this context */ - fun getDashboardUrl(): String + fun getDashboardUrl(): String? /** * Closes and disposes this context. diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContext.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContext.kt index e9b105e14..cfb015a57 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContext.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContext.kt @@ -32,7 +32,7 @@ open class KubernetesContext( context: NamedContext, modelChange: IResourceModelObservable, client: KubeClientAdapter, -) : ActiveContext( +) : ActiveContext( context, modelChange, client, @@ -43,7 +43,7 @@ open class KubernetesContext( ) ) { - override val namespaceKind : ResourceKind = NamespacesOperator.KIND + override val namespaceKind : ResourceKind = NamespacesOperator.KIND private val replicasOperator = KubernetesReplicas( NonCachingSingleResourceOperator(client), @@ -54,12 +54,14 @@ open class KubernetesContext( } ) - override fun getInternalResourceOperators(client: ClientAdapter) + override fun getInternalResourceOperators() : List> { return OperatorFactory.createKubernetes(client) } - override fun isOpenShift() = false + override fun isOpenShift(): Boolean { + return false + } override fun setReplicas(replicas: Int, replicator: Replicator) { replicasOperator.set(replicas, replicator) @@ -69,9 +71,4 @@ open class KubernetesContext( return replicasOperator.get(resource) } - override fun getDashboardUrl(): String { - return dashboard.get() - } - - } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContext.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContext.kt new file mode 100644 index 000000000..033421cec --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContext.kt @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2025 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.context + +import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable +import com.redhat.devtools.intellij.kubernetes.model.client.KubeClientAdapter +import com.redhat.devtools.intellij.kubernetes.model.client.OSClientAdapter +import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator +import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind +import com.redhat.devtools.intellij.kubernetes.model.resource.kubernetes.KubernetesReplicas.Replicator +import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.api.model.NamedContext +import io.fabric8.kubernetes.client.KubernetesClient +import org.jetbrains.concurrency.runAsync +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write + +/** + * A delegating context that starts with a Kubernetes delegate. + * It then (async) tries to create and use an OpenShift context, notifying the change if it successfully could. + * It sticks to the Kubernetes if it can't. + * + * @see createOpenShiftDelegate + */ +class LazyOpenShiftContext( + context: NamedContext, + modelChange: IResourceModelObservable, + client: KubeClientAdapter, + /* for testing purposes */ + kubernetesContextFactory: ( + context: NamedContext, + modelChange: IResourceModelObservable, + client: KubeClientAdapter, + ) -> IActiveContext + = ::KubernetesContext, + /* for testing purposes */ + private val openshiftContextFactory: ( + client: OSClientAdapter, + modelChange: IResourceModelObservable + ) -> IActiveContext? + = IActiveContext.Factory::createOpenShift, + /* for testing purposes */ + runAsync: (runnable: () -> Unit) -> Unit + = ::runAsync +) : KubernetesContext(context, modelChange, client) { + + private val lock = ReentrantReadWriteLock() + private var delegate: IActiveContext = + kubernetesContextFactory.invoke(context, modelChange, client) + + init { + runAsync.invoke { + createOpenShiftDelegate() + } + } + + override val namespaceKind: ResourceKind + get() = delegate.namespaceKind + + override fun getInternalResourceOperators(): List> { + lock.read { + return delegate.getInternalResourceOperators() + } + } + + override fun isOpenShift(): Boolean { + lock.read { + return delegate.isOpenShift() + } + } + + override fun setReplicas(replicas: Int, replicator: Replicator) { + lock.read { + delegate.setReplicas(replicas, replicator) + } + } + + override fun getReplicas(resource: HasMetadata): Replicator? { + lock.read { + return delegate.getReplicas(resource) + } + } + + override fun getDashboardUrl(): String? { + lock.read { + return delegate.getDashboardUrl() + } + } + + private fun createOpenShiftDelegate() { + if (client.canAdaptToOpenShift()) { + val delegate = openshiftContextFactory.invoke( + client.toOpenShift(), + modelChange + ) ?: return + lock.write { + this.delegate = delegate + } + modelChange.fireModified(this) + } + } +} + diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/OpenShiftContext.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/OpenShiftContext.kt index 06bfb18d8..002a166b6 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/OpenShiftContext.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/OpenShiftContext.kt @@ -11,7 +11,6 @@ package com.redhat.devtools.intellij.kubernetes.model.context import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable -import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter import com.redhat.devtools.intellij.kubernetes.model.client.OSClientAdapter import com.redhat.devtools.intellij.kubernetes.model.dashboard.OpenShiftDashboard import com.redhat.devtools.intellij.kubernetes.model.resource.IResourceOperator @@ -30,7 +29,7 @@ import io.fabric8.openshift.client.OpenShiftClient open class OpenShiftContext( context: NamedContext, modelChange: IResourceModelObservable, - client: OSClientAdapter, + client: OSClientAdapter ) : ActiveContext( context, modelChange, @@ -43,6 +42,7 @@ open class OpenShiftContext( ) { override val namespaceKind = ProjectsOperator.KIND + private val replicasOperator = OpenShiftReplicas( NonCachingSingleResourceOperator(client), object: ResourcesRetrieval { @@ -52,11 +52,13 @@ open class OpenShiftContext( } ) - override fun getInternalResourceOperators(client: ClientAdapter): List> { + override fun getInternalResourceOperators(): List> { return OperatorFactory.createOpenShift(client) } - override fun isOpenShift() = true + override fun isOpenShift(): Boolean { + return true + } override fun setReplicas(replicas: Int, replicator: Replicator) { replicasOperator.set(replicas, replicator) @@ -65,8 +67,4 @@ open class OpenShiftContext( override fun getReplicas(resource: HasMetadata): Replicator? { return replicasOperator.get(resource) } - - override fun getDashboardUrl(): String { - return dashboard.get() - } } 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 40927b30c..f783db521 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 @@ -15,10 +15,12 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.IconLoader import com.redhat.devtools.intellij.kubernetes.model.IResourceModel import com.redhat.devtools.intellij.kubernetes.model.context.KubernetesContext +import com.redhat.devtools.intellij.kubernetes.model.context.LazyOpenShiftContext import com.redhat.devtools.intellij.kubernetes.model.resource.ResourceKind import com.redhat.devtools.intellij.kubernetes.model.util.getHighestPriorityVersion import com.redhat.devtools.intellij.kubernetes.tree.AbstractTreeStructureContribution.DescriptorFactory import com.redhat.devtools.intellij.kubernetes.tree.KubernetesStructure.NamespacesFolder +import com.redhat.devtools.intellij.kubernetes.tree.OpenShiftDescriptors.OPENSHIFT_CLUSTER_ICON import com.redhat.devtools.intellij.kubernetes.tree.TreeStructure.ContextDescriptor import com.redhat.devtools.intellij.kubernetes.tree.TreeStructure.Folder import com.redhat.devtools.intellij.kubernetes.tree.TreeStructure.FolderDescriptor @@ -50,6 +52,11 @@ import javax.swing.Icon object KubernetesDescriptors { + val KUBERNETES_CLUSTER_ICON = IconLoader.getIcon( + "/icons/kubernetes-cluster.svg", + KubernetesContext::class.java + ) + fun createDescriptor( element: Any, childrenKind: ResourceKind?, @@ -61,12 +68,16 @@ object KubernetesDescriptors { element is DescriptorFactory<*> -> element.create(parent, model, project) + element is LazyOpenShiftContext -> + LazyOpenShiftContextDescriptor(element, model, project) + + element is KubernetesContext -> + KubernetesContextDescriptor(element, model, project) + element is NamespacesFolder && !isOpenShift(model) -> NamespacesFolderDescriptor(element, parent, model, project) - element is KubernetesContext -> - KubernetesContextDescriptor(element, model, project) element is Namespace -> NamespaceDescriptor(element, parent, model, project) element is Node -> @@ -90,7 +101,7 @@ object KubernetesDescriptors { || element is GenericKubernetesResource || element is ReplicaSet || element is ReplicationController -> - ResourceDescriptor(element as HasMetadata, childrenKind, parent, model, project) + ResourceDescriptor(element, childrenKind, parent, model, project) element is CustomResourceDefinition -> CustomResourceDefinitionDescriptor(element, parent, model, project) @@ -103,7 +114,7 @@ object KubernetesDescriptors { return true == model.getCurrentContext()?.isOpenShift() } - private class KubernetesContextDescriptor( + class KubernetesContextDescriptor( element: KubernetesContext, model: IResourceModel, project: Project @@ -113,7 +124,25 @@ object KubernetesDescriptors { project = project ) { override fun getIcon(element: KubernetesContext): Icon { - return IconLoader.getIcon("/icons/kubernetes-cluster.svg", javaClass) + return KUBERNETES_CLUSTER_ICON + } + } + + private class LazyOpenShiftContextDescriptor( + context: LazyOpenShiftContext, + model: IResourceModel, + project: Project + ) : ContextDescriptor( + context = context, + model = model, + project = project + ) { + override fun getIcon(context: LazyOpenShiftContext): Icon { + return if (context.isOpenShift()) { + OPENSHIFT_CLUSTER_ICON + } else { + KUBERNETES_CLUSTER_ICON + } } } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/OpenShiftDescriptors.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/OpenShiftDescriptors.kt index 8987f5dc6..f0e16a0bc 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/OpenShiftDescriptors.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/tree/OpenShiftDescriptors.kt @@ -32,6 +32,11 @@ import javax.swing.Icon object OpenShiftDescriptors { + val OPENSHIFT_CLUSTER_ICON = IconLoader.getIcon( + "/icons/openshift-cluster.svg", + OpenShiftContext::class.java + ) + fun createDescriptor( element: Any, childrenKind: ResourceKind?, @@ -40,18 +45,24 @@ object OpenShiftDescriptors { project: Project ): NodeDescriptor<*>? { return when (element) { - is OpenShiftContext -> OpenShiftContextDescriptor(element, model, project) + is OpenShiftContext -> + OpenShiftContextDescriptor(element, model, project) + + is ProjectsFolder -> + ProjectsFolderDescriptor(element, parent, model, project) - is ProjectsFolder -> ProjectsFolderDescriptor(element, parent, model, project) + is io.fabric8.openshift.api.model.Project -> + ProjectDescriptor(element, parent, model, project) - is io.fabric8.openshift.api.model.Project -> ProjectDescriptor(element, parent, model, project) is ImageStream, is DeploymentConfig, is ReplicationController, is BuildConfig, is Build, - is Route -> ResourceDescriptor(element as HasMetadata, childrenKind, parent, model, project) - else -> null + is Route -> + ResourceDescriptor(element as HasMetadata, childrenKind, parent, model, project) + else -> + null } } @@ -64,8 +75,9 @@ object OpenShiftDescriptors { model = model, project = project ) { + override fun getIcon(element: OpenShiftContext): Icon { - return IconLoader.getIcon("/icons/openshift-cluster.svg", javaClass) + return OPENSHIFT_CLUSTER_ICON } } 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 988317457..623dd4923 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 @@ -114,11 +114,11 @@ open class TreeStructure( .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) - } + 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) } @@ -185,7 +185,7 @@ open class TreeStructure( } override fun getIcon(element: C): Icon? { - return IconLoader.getIcon("/icons/kubernetes-cluster.svg", javaClass) + return KubernetesDescriptors.KUBERNETES_CLUSTER_ICON } } 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 c15a78ff7..2ff394305 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 @@ -71,7 +71,7 @@ class AllContextsTest { private val token = "42" private val client = client(true) private val clientConfig = clientConfig(currentContext, contexts) - private val clientAdapter = clientAdapter(clientConfig, client) + private val clientAdapter = clientAdapter>(clientConfig, client) private val clientFactory = clientFactory(clientAdapter) private val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) @@ -337,7 +337,7 @@ class AllContextsTest { // given val clientConfig = clientConfig(null, contexts) val client = client(true) - val clientAdapter = clientAdapter(clientConfig, client) + val clientAdapter = clientAdapter>(clientConfig, client) val clientFactory = clientFactory(clientAdapter) val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) // when @@ -359,7 +359,7 @@ class AllContextsTest { 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 clientAdapter = clientAdapter>(null, client) // no config so there are no contexts val clientFactory = clientFactory(clientAdapter) val allContexts = TestableAllContexts(modelChange, contextFactory, clientFactory) // when diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt index 8f5d39370..9cb80750d 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/client/ClientAdapterTest.kt @@ -24,12 +24,12 @@ import io.fabric8.kubernetes.client.NamespacedKubernetesClient import io.fabric8.kubernetes.client.http.HttpClient import io.fabric8.kubernetes.client.impl.AppsAPIGroupClient import io.fabric8.openshift.client.NamespacedOpenShiftClient -import java.security.cert.X509Certificate -import javax.net.ssl.X509ExtendedTrustManager -import javax.net.ssl.X509TrustManager import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import java.security.cert.X509Certificate import java.util.function.Consumer +import javax.net.ssl.X509ExtendedTrustManager +import javax.net.ssl.X509TrustManager class ClientAdapterTest { @@ -66,6 +66,31 @@ class ClientAdapterTest { assertThat(isOpenShift).isFalse() } + @Test + fun `#toOpenShift should return same instance if it's already a OSClientAdapter`() { + // given + val clientAdapter = OSClientAdapter(mock(), mock()) + // when + val openShiftAdapter = clientAdapter.toOpenShift() + // then + assertThat(openShiftAdapter).isEqualTo(clientAdapter) + } + + @Test + fun `#toOpenShift should return adapt fabric8 client and create new OSClientAdapter`() { + // given + val osClient = mock() + val client = mock { + on { adapt(NamespacedOpenShiftClient::class.java) } doReturn osClient + } + val clientAdapter = KubeClientAdapter(client) + // when + val openShiftAdapter = clientAdapter.toOpenShift() + // then + verify(client).adapt(NamespacedOpenShiftClient::class.java) + assertThat(openShiftAdapter.get()).isEqualTo(osClient) + } + @Test fun `#get() should return client`() { // given @@ -143,7 +168,7 @@ class ClientAdapterTest { } @Test - fun `#create should return KubeClientAdapter if cluster is NOT OpenShift`() { + fun `#create should return KubeClientAdapter if cluster is Kubernetes`() { // given val clientBuilder = createClientBuilder(false) // when @@ -153,13 +178,13 @@ class ClientAdapterTest { } @Test - fun `#create should return OSClientAdapter if cluster is OpenShift`() { + fun `#create should return KubeClientAdapter if cluster is OpenShift`() { // given val clientBuilder = createClientBuilder(true) // when val adapter = ClientAdapter.Factory.create("namespace", "context", clientBuilder, createConfig, trustManagerProvider) // then - assertThat(adapter).isInstanceOf(OSClientAdapter::class.java) + assertThat(adapter).isInstanceOf(KubeClientAdapter::class.java) } @Suppress("SameParameterValue", "UNCHECKED_CAST") diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContextTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContextTest.kt index 6ee343086..170dd92fb 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContextTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/KubernetesContextTest.kt @@ -998,12 +998,12 @@ class KubernetesContextTest { return super.nonNamespacedOperators } - override fun getInternalResourceOperators(client: ClientAdapter) + override fun getInternalResourceOperators() : List> { return internalResourceOperators } - override fun getExtensionResourceOperators(client: ClientAdapter) + override fun getExtensionResourceOperators() : List> { return extensionResourceOperators } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContextTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContextTest.kt new file mode 100644 index 000000000..9309dedab --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/LazyOpenShiftContextTest.kt @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (c) 2025 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.context + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable +import com.redhat.devtools.intellij.kubernetes.model.client.KubeClientAdapter +import com.redhat.devtools.intellij.kubernetes.model.client.OSClientAdapter +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.client +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.namedContext +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource +import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.activeContext +import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.clientAdapter +import com.redhat.devtools.intellij.kubernetes.model.resource.kubernetes.NamespacesOperator +import com.redhat.devtools.intellij.kubernetes.model.resource.openshift.ProjectsOperator +import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.api.model.NamedContext +import io.fabric8.kubernetes.api.model.Namespace +import io.fabric8.kubernetes.client.KubernetesClient +import org.junit.Before +import org.junit.Test + +class LazyOpenShiftContextTest { + + private val context = namedContext("leia") + private var modelChange: IResourceModelObservable = mock() + + private lateinit var kubernetesClientAdapter: KubeClientAdapter + + private lateinit var openshiftClientAdapter: OSClientAdapter + + private lateinit var kubernetesContext: IActiveContext + private lateinit var kubernetesContextFactory: (context: NamedContext, + modelChange: IResourceModelObservable, + client: KubeClientAdapter + ) -> IActiveContext + private lateinit var openshiftContext: IActiveContext + private lateinit var openshiftContextFactory: ( + client: OSClientAdapter, + observable: IResourceModelObservable + ) -> IActiveContext? + + @Before + fun before() { + this.openshiftClientAdapter = clientAdapter( + null, + client( + "yoda", + emptyArray() + ) + ) + this.openshiftContext = activeContext( + resource("jedi"), + context, + ProjectsOperator.KIND, + isOpenshift = true + ) + this.openshiftContextFactory = mock { + on { invoke(any(), any()) } + .thenReturn(openshiftContext) + } + + this.kubernetesClientAdapter = clientAdapter( + null, client( + "skywalker", + emptyArray() + ), + openshiftClientAdapter + ) + + this.kubernetesContext = activeContext( + resource("jedi"), + context, + NamespacesOperator.KIND, + isOpenshift = false + ) + this.kubernetesContextFactory = mock { + on { invoke(any(), any(), any()) } + .thenReturn(kubernetesContext) + } + } + + @Test + fun `#constructor creates a kubernetes context`() { + // given + // when + createClusterAwareContext() + // then + verify(kubernetesContextFactory) + .invoke(context, modelChange, kubernetesClientAdapter) + } + + @Test + fun `#constructor creates an openshift context if client can adapt to OpenShift`() { + // given + doReturn(true) + .whenever(kubernetesClientAdapter).canAdaptToOpenShift() + // when + createClusterAwareContext() + // then + verify(openshiftContextFactory) + .invoke(openshiftClientAdapter, modelChange) + } + + @Test + fun `#constructor does not create an openshift context if client cannot adapt to OpenShift`() { + // given + doReturn(false) + .whenever(kubernetesClientAdapter).canAdaptToOpenShift() + // when + createClusterAwareContext() + // then + verify(openshiftContextFactory, never()) + .invoke(openshiftClientAdapter, modelChange) + } + + @Test + fun `#constructor notifies model change if it created an openshift context`() { + // given + doReturn(true) + .whenever(kubernetesClientAdapter).canAdaptToOpenShift() + // when + val context = createClusterAwareContext() + // then + verify(modelChange) + .fireModified(context) + } + + @Test + fun `#constructor does NOT notify model change if it did NOT create an openshift context`() { + // given + doReturn(false) + .whenever(kubernetesClientAdapter).canAdaptToOpenShift() + // when + val context = createClusterAwareContext() + // then + verify(modelChange, never()) + .fireModified(context) + } + + @Test + fun `#isOpenShift delegates to openshift context if client can adapt to OpenShift`() { + // given + doReturn(true) + .whenever(kubernetesClientAdapter).canAdaptToOpenShift() + val context = createClusterAwareContext() + // when + context.isOpenShift() + // then + verify(kubernetesContext, never()) + .isOpenShift() + verify(openshiftContext) + .isOpenShift() + } + + @Test + fun `#isOpenShift delegates to kubernetes context if client CANNOT adapt to OpenShift`() { + // given + doReturn(false) + .whenever(kubernetesClientAdapter).canAdaptToOpenShift() + val context = createClusterAwareContext() + // when + context.isOpenShift() + // then + verify(kubernetesContext) + .isOpenShift() + verify(openshiftContext, never()) + .isOpenShift() + } + + private fun createClusterAwareContext(): LazyOpenShiftContext { + return LazyOpenShiftContext( + context, + modelChange, + kubernetesClientAdapter, + kubernetesContextFactory, + openshiftContextFactory, + { runnable -> runnable.invoke() } + ) + } +} \ No newline at end of file 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 a5cc80e1b..6709cdd96 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 @@ -237,7 +237,7 @@ object ClientMocks { return namedContext(name, context) } - fun namedContext(name: String, context: Context? = null): NamedContext { + fun namedContext(name: String, context: Context? = mock()): NamedContext { return mock { on { this.name } doReturn name on { this.context } doReturn context @@ -484,6 +484,4 @@ object ClientMocks { .whenever(resource).involvedObject return resource } - - } 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 a7a810d4a..a397ff328 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 @@ -22,9 +22,11 @@ import com.redhat.devtools.intellij.kubernetes.model.IResourceModel import com.redhat.devtools.intellij.kubernetes.model.IResourceModelObservable import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter import com.redhat.devtools.intellij.kubernetes.model.client.ClientConfig +import com.redhat.devtools.intellij.kubernetes.model.client.OSClientAdapter 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.* +import com.redhat.devtools.intellij.kubernetes.model.resource.kubernetes.NamespacesOperator import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.api.model.NamedContext import io.fabric8.kubernetes.api.model.Namespace @@ -45,12 +47,14 @@ object Mocks { .whenever(this).invoke(anyOrNull(), anyOrNull()) } - fun clientAdapter(clientConfig: ClientConfig?, client: KubernetesClient? = null): ClientAdapter { - return mock>().apply { + inline fun > clientAdapter(clientConfig: ClientConfig?, client: KubernetesClient? = null, openshiftAdapter: OSClientAdapter? = null): A { + return mock().apply { doReturn(clientConfig) .whenever(this).config doReturn(client) .whenever(this).get() + doReturn(openshiftAdapter) + .whenever(this).toOpenShift() } } @@ -79,8 +83,12 @@ object Mocks { return context } - fun activeContext(currentNamespace: Namespace, context: NamedContext) - : IActiveContext { + fun activeContext( + currentNamespace: Namespace, + context: NamedContext, + namespaceKind: ResourceKind = NamespacesOperator.KIND, + isOpenshift: Boolean = false + ): IActiveContext { val mock = mock>() doReturn(currentNamespace.metadata.name) .whenever(mock).getCurrentNamespace() @@ -90,6 +98,10 @@ object Mocks { .whenever(mock).namespace doReturn(true) .whenever(mock).active + doReturn(namespaceKind) + .whenever(mock).namespaceKind + doReturn(isOpenshift) + .whenever(mock).isOpenShift() return mock }