diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ClusterResource.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ClusterResource.kt index 829aa8f5b..2df0002c8 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ClusterResource.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ClusterResource.kt @@ -20,6 +20,7 @@ import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException import com.redhat.devtools.intellij.kubernetes.model.util.areEqual import com.redhat.devtools.intellij.kubernetes.model.util.isNotFound import com.redhat.devtools.intellij.kubernetes.model.util.isSameResource +import com.redhat.devtools.intellij.kubernetes.model.util.isUnauthorized import com.redhat.devtools.intellij.kubernetes.model.util.isUnsupported import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.client.KubernetesClient @@ -192,6 +193,25 @@ open class ClusterResource protected constructor( && e.isUnsupported()) } + /** + * Returns `true` if this [ClusterResource] is authorized. Returns `false` otherwise. + * + * @return true if this ClusterResource is authorized + * + * @see HasMetadata.getKind + * @see HasMetadata.getApiVersion + */ + fun isAuthorized(): Boolean { + val e = try { + pull() + null + } catch(re: ResourceException) { + re.cause + } + return !(e is KubernetesClientException + && e.isUnauthorized()) + } + fun isDeleted(): Boolean { synchronized(this) { return isDeleted diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt index 44a24b74d..2991dbdc1 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResource.kt @@ -115,6 +115,9 @@ open class EditorResource( !isSupported() -> Error("Unsupported kind ${resource.kind} in version ${resource.apiVersion}") + !isAuthorized() -> + Error("Authentication with cluster failed. Verify username and password, refresh token, etc.") + !hasName(resource) && !hasGenerateName(resource) -> Error("Resource has neither name nor generateName.", null as String?) @@ -271,6 +274,10 @@ open class EditorResource( return clusterResource?.isSupported() ?: false } + private fun isAuthorized(): Boolean { + return clusterResource?.isAuthorized() ?: false + } + /** * Returns `true` if the given resource has changes that don't exist in the resource * that was last pulled/pushed from/to the cluster. diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt index 8ce921ffd..1b926f195 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt @@ -35,9 +35,13 @@ import com.redhat.devtools.intellij.kubernetes.editor.util.getDocument import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource import com.redhat.devtools.intellij.kubernetes.model.IResourceModel import com.redhat.devtools.intellij.kubernetes.model.IResourceModelListener +import com.redhat.devtools.intellij.kubernetes.model.Notification import com.redhat.devtools.intellij.kubernetes.model.context.IActiveContext +import com.redhat.devtools.intellij.kubernetes.model.util.KubernetesClientExceptionUtils.statusMessage +import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException import com.redhat.devtools.intellij.kubernetes.model.util.toMessage import com.redhat.devtools.intellij.kubernetes.model.util.toTitle +import com.redhat.devtools.intellij.kubernetes.model.util.trimWithEllipsis import io.fabric8.kubernetes.api.model.HasMetadata /** @@ -299,11 +303,39 @@ open class ResourceEditor( fun diff() { val manager = getPsiDocumentManager.invoke(project) val file = editor.file ?: return + var fileType: FileType? = null + var beforeDiff: String? = null runInUI { val document = getDocument.invoke(editor) ?: return@runInUI - val resourcesOnCluster = editorResources.getAllResourcesOnCluster() - val serialized = serialize(resourcesOnCluster, getFileType(document, manager)) ?: return@runInUI - diff.open(file, serialized) { onDiffClosed(document.text) } + fileType = getFileType(document, manager) ?: return@runInUI + beforeDiff = document.text + } + if (fileType == null + || beforeDiff == null + ) { + return + } + runAsync { + try { + val resourcesOnCluster = editorResources.getAllResourcesOnCluster() + val serialized = serialize(resourcesOnCluster, fileType) ?: return@runAsync + runInUI { + diff.open(file, serialized) { onDiffClosed(beforeDiff) } + } + } catch(e: ResourceException) { + val message = trimWithEllipsis(e.message, 100) + val causeMessage = statusMessage(e.cause) + Notification() + .error("Could not open diff", + if (causeMessage == null) { + message ?: "" + } else if (message == null) { + causeMessage + } else { + "$message: $causeMessage" + } + ) + } } } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/KubernetesClientExceptionUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/KubernetesClientExceptionUtils.kt new file mode 100644 index 000000000..1120f065f --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/KubernetesClientExceptionUtils.kt @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2023 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.util + +import io.fabric8.kubernetes.api.model.Status +import io.fabric8.kubernetes.client.KubernetesClientException + +object KubernetesClientExceptionUtils { + + fun statusMessage(t: Throwable?): String? { + return status(t)?.message + } + + fun status(t: Throwable?): Status? { + return if (t is KubernetesClientException) { + t.status + } else { + null + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceTest.kt index 43f5aa56d..8ea42fe4e 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceTest.kt @@ -169,6 +169,8 @@ class EditorResourceTest { assertThat(editorResource.getState()).isNotInstanceOf(Pushed::class.java) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() doReturn(true) // after a push: resource exists on cluster .whenever(clusterResource).exists() doReturn(false) // after a push: resource is not deleted on cluster @@ -187,6 +189,8 @@ class EditorResourceTest { val editorResource = createEditorResource(POD2) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() assertThat(editorResource.getState()).isNotInstanceOf(Error::class.java) doThrow(ResourceException("interference with the force")) .whenever(clusterResource).push(any()) @@ -250,6 +254,8 @@ class EditorResourceTest { assertThat(editorResource.getState()).isNotInstanceOf(Pushed::class.java) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() doReturn(true) // after a pull: resource exists on cluster .whenever(clusterResource).exists() doReturn(false) // after a pull: resource is not deleted on cluster @@ -269,6 +275,8 @@ class EditorResourceTest { editorResource.setLastPushedPulled(POD2) // not modified doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() assertThat(editorResource.getState()).isNotInstanceOf(Error::class.java) doThrow(ResourceException("interference with the force")) .whenever(clusterResource).pull(any()) @@ -287,6 +295,8 @@ class EditorResourceTest { editorResource.setLastPushedPulled(POD2) // modified = (current resource != lastPushedPulled) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() // when val state = editorResource.getState() // then @@ -298,6 +308,8 @@ class EditorResourceTest { // given doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() val editorResource = createEditorResource(POD2) val error = Error("oh my!") editorResource.setState(error) @@ -348,6 +360,8 @@ class EditorResourceTest { val editorResource = createEditorResource(resource) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() // when val state = editorResource.getState() // then @@ -366,6 +380,8 @@ class EditorResourceTest { val editorResource = createEditorResource(resource) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() // when val state = editorResource.getState() // then @@ -378,6 +394,8 @@ class EditorResourceTest { val editorResource = createEditorResource(POD2) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() doReturn(true) .whenever(clusterResource).isDeleted() // when @@ -392,6 +410,8 @@ class EditorResourceTest { val editorResource = createEditorResource(POD2) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() doReturn(false) .whenever(clusterResource).exists() // when @@ -406,6 +426,8 @@ class EditorResourceTest { // given doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() doReturn(true) // don't create modified state because it doesnt exist on cluster .whenever(clusterResource).exists() val editorResource = createEditorResource(POD2) @@ -428,6 +450,8 @@ class EditorResourceTest { val editorResource = createEditorResource(POD2) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() doReturn(true) .whenever(clusterResource).isOutdatedVersion(any()) doReturn(true) // don't return modified state because it doesnt exist @@ -445,6 +469,8 @@ class EditorResourceTest { val editorResource = createEditorResource(POD2) doReturn(true) .whenever(clusterResource).isSupported() + doReturn(true) + .whenever(clusterResource).isAuthorized() doReturn(true) .whenever(clusterResource).isOutdatedVersion(any()) doReturn(true) // don't return modified state because it doesnt exist @@ -457,6 +483,32 @@ class EditorResourceTest { assertThat(state).isInstanceOf(Error::class.java) } + @Test + fun `#getState should return Error if resource is NOT supported on cluster`() { + // given + val editorResource = createEditorResource(POD2) + doReturn(false) + .whenever(clusterResource).isSupported() + // when + val state = editorResource.getState() + // then + assertThat(state).isInstanceOf(Error::class.java) + } + + @Test + fun `#getState should return Error if cluster is not authorized`() { + // given + val editorResource = createEditorResource(POD2) + doReturn(true) + .whenever(clusterResource).isSupported() + doReturn(false) + .whenever(clusterResource).isAuthorized() + // when + val state = editorResource.getState() + // then + assertThat(state).isInstanceOf(Error::class.java) + } + @Test fun `#dispose should close clusterResource that was created`() { // given