From e8f76cf87090b80290233fe3870fe52e55d11658 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Mon, 10 Mar 2025 19:04:48 +0100 Subject: [PATCH 1/3] feat: hint ref'd by matchLabels/-Expressions & list them in editor (#642) Signed-off-by: Andre Dietisheim --- build.gradle.kts | 6 +- .../editor/ResourceEditorFactory.kt | 2 +- .../inlay/ResourceEditorInlayHintsProvider.kt | 39 +- .../inlay/base64/Base64Presentations.kt | 65 ++- .../editor/inlay/base64/Base64ValueAdapter.kt | 8 +- .../inlay/selector/SelectorPresentations.kt | 168 +++++++ .../inlay/selector/ShowUsagesDispatcher.kt | 85 ++++ .../editor/util/KubernetesTypeInfoUtils.kt | 74 +++ .../kubernetes/editor/util/PsiElements.kt | 75 +++ .../editor/util/ResourceEditorUtils.kt | 76 +-- .../editor/util/ResourcePsiElementUtils.kt | 265 ++++++++++ .../editor/util/SelectorPsiElementUtils.kt | 160 +++++++ .../intellij/kubernetes/usage/LabelsFilter.kt | 153 ++++++ .../usage/SelectorDescriptionProvider.kt | 63 +++ .../kubernetes/usage/SelectorUsageSearcher.kt | 100 ++++ .../usage/SelectorUsagesHandlerFactory.kt | 41 ++ .../kubernetes/usage/SelectorsFilter.kt | 45 ++ src/main/resources/META-INF/plugin.xml | 9 + src/main/resources/icons/label.svg | 128 +++++ src/main/resources/icons/label2.svg | 152 ++++++ src/main/resources/icons/selector.svg | 127 +++++ src/main/resources/icons/selector2.svg | 167 +++++++ .../editor/inlay/Base64PresentationsTest.kt | 159 ++++-- .../editor/inlay/Base64ValueAdapterTest.kt | 39 +- .../editor/mocks/PsiElementMocks.kt | 283 +++++++---- .../editor/mocks/ResourcePsiElementMocks.kt | 149 ++++++ .../editor/util/ResourceEditorUtilsTest.kt | 59 --- .../util/ResourcePsiElementUtilsTest.kt | 453 ++++++++++++++++++ .../util/SelectorPsiElementUtilsTest.kt | 410 ++++++++++++++++ .../kubernetes/usage/LabelsFilterTest.kt | 216 +++++++++ .../kubernetes/usage/SelectorsFilterTest.kt | 245 ++++++++++ 31 files changed, 3688 insertions(+), 333 deletions(-) create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/KubernetesTypeInfoUtils.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/PsiElements.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtils.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtils.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorDescriptionProvider.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsagesHandlerFactory.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt create mode 100644 src/main/resources/icons/label.svg create mode 100644 src/main/resources/icons/label2.svg create mode 100644 src/main/resources/icons/selector.svg create mode 100644 src/main/resources/icons/selector2.svg create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/ResourcePsiElementMocks.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtilsTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtilsTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 94a6e39f9..407b9bcc5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,9 +32,13 @@ java { repositories { mavenLocal() - maven { url = uri("https://repository.jboss.org") } + /* + * github repo with intellij-common needs to be listed before jboss repository. Both have 1.9.9-SNAPSHOT + * First hit wins regardless of timestamp + */ maven { url = uri("https://raw.githubusercontent.com/redhat-developer/intellij-common/repository/snapshots") } maven { url = uri("https://raw.githubusercontent.com/redhat-developer/intellij-common/repository/releases") } + maven { url = uri("https://repository.jboss.org") } mavenCentral() intellijPlatform { defaultRepositories() diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorFactory.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorFactory.kt index f8552dd74..0db24b34c 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorFactory.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorFactory.kt @@ -171,6 +171,6 @@ open class ResourceEditorFactory protected constructor( /* for testing purposes */ protected open fun getTelemetryMessageBuilder(): TelemetryMessageBuilder { - return TelemetryService.instance; + return TelemetryService.instance } } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt index a4adbae60..6b575f208 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt @@ -20,8 +20,8 @@ import com.intellij.codeInsight.hints.InlayHintsProvider import com.intellij.codeInsight.hints.InlayHintsSink import com.intellij.codeInsight.hints.NoSettings import com.intellij.codeInsight.hints.SettingsKey +import com.intellij.codeInsight.hints.presentation.PresentationFactory import com.intellij.json.psi.JsonFile -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ReadAction import com.intellij.openapi.editor.Editor import com.intellij.psi.PsiElement @@ -29,6 +29,8 @@ import com.intellij.psi.PsiFile import com.intellij.ui.dsl.builder.panel import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo import com.redhat.devtools.intellij.kubernetes.editor.inlay.base64.Base64Presentations +import com.redhat.devtools.intellij.kubernetes.editor.inlay.selector.SelectorPresentations +import com.redhat.devtools.intellij.kubernetes.editor.util.PsiElements import org.jetbrains.yaml.psi.YAMLFile import javax.swing.JComponent @@ -65,34 +67,45 @@ internal class ResourceEditorInlayHintsProvider : InlayHintsProvider } return when(element) { is YAMLFile -> { - create(element, sink, editor) + create(element, sink, editor, factory) false } is JsonFile -> { - create(element, sink, editor) + create(element, sink, editor, factory) false } else -> true } } - private fun create(file: YAMLFile, sink: InlayHintsSink, editor: Editor) { + private fun create(file: YAMLFile, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) { return ReadAction.run { - file.documents.forEach { document -> - val info = KubernetesTypeInfo.create(document) ?: return@forEach - val element = document.topLevelValue ?: return@forEach - Base64Presentations.create(element, info, sink, editor)?.create() - } + file.documents + .mapNotNull { document -> document.topLevelValue } + .forEach { element -> + createPresentations(element, sink, editor, factory) + } } } - private fun create(file: JsonFile, sink: InlayHintsSink, editor: Editor) { + private fun create(file: JsonFile, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) { return ReadAction.run { - val info = KubernetesTypeInfo.create(file) ?: return@run - val element = file.topLevelValue ?: return@run - Base64Presentations.create(element, info, sink, editor)?.create() + file.allTopLevelValues.forEach { element -> + createPresentations(element, sink, editor, factory) + } } } + private fun createPresentations(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) { + val info = KubernetesTypeInfo.create(element) ?: return + Base64Presentations.create(element, info, sink, editor, factory) + + val fileType = editor.virtualFile?.fileType ?: return + val project = editor.project ?: return + //PsiElements.getAll(fileType, project) + val allElements = PsiElements.getAllNoExclusions(fileType, project) + SelectorPresentations.createForSelector(element, allElements, sink, editor, factory) + SelectorPresentations.createForAllLabels(element, allElements, sink, editor, factory) + } } } \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64Presentations.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64Presentations.kt index 08c0bf24f..5fada9b16 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64Presentations.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64Presentations.kt @@ -40,26 +40,41 @@ object Base64Presentations { private const val SECRET_RESOURCE_KIND = "Secret" private const val CONFIGMAP_RESOURCE_KIND = "ConfigMap" - fun create(element: PsiElement, info: KubernetesTypeInfo, sink: InlayHintsSink, editor: Editor): InlayPresentationsFactory? { - return when { + fun create( + element: PsiElement, + info: KubernetesTypeInfo, + sink: InlayHintsSink, + editor: Editor, + factory: PresentationFactory, + /* for testing purposes */ + stringPresentationFactory: (element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit + = { element, sink, editor, factory -> + StringPresentationsFactory(element, sink, editor, factory).create() + }, + /* for testing purposes */ + binaryPresentationFactory: (element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit + = { element, sink, editor, factory -> + BinaryPresentationsFactory(element, sink, editor, factory).create() + }, + ) { + when { isKubernetesResource(SECRET_RESOURCE_KIND, info) -> { - val data = getDataValue(element) ?: return null - StringPresentationsFactory(data, sink, editor) + val data = element.getDataValue() ?: return + stringPresentationFactory.invoke(data, sink, editor, factory) } isKubernetesResource(CONFIGMAP_RESOURCE_KIND, info) -> { - val binaryData = getBinaryData(element) ?: return null - BinaryPresentationsFactory(binaryData, sink, editor) + val binaryData = element.getBinaryData() ?: return + binaryPresentationFactory.invoke(binaryData, sink, editor, factory) } - - else -> null } } abstract class InlayPresentationsFactory( private val element: PsiElement, protected val sink: InlayHintsSink, - protected val editor: Editor + protected val editor: Editor, + protected val factory: PresentationFactory ) { protected companion object { @@ -69,17 +84,15 @@ object Base64Presentations { fun create(): Collection { return element.children.mapNotNull { child -> - val adapter = Base64ValueAdapter(child) - create(adapter) + create(Base64ValueAdapter(child)) } } protected abstract fun create(adapter: Base64ValueAdapter): InlayPresentation? - } - class StringPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor) - : InlayPresentationsFactory(element, sink, editor) { + class StringPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) + : InlayPresentationsFactory(element, sink, editor, factory) { override fun create(adapter: Base64ValueAdapter): InlayPresentation? { val decoded = adapter.getDecoded() ?: return null @@ -89,20 +102,22 @@ object Base64Presentations { onValidValue(adapter::set, editor.project), editor )::show - val presentation = create(decoded, onClick, editor) ?: return null + val presentation = create(decoded, onClick, factory) ?: return null sink.addInlineElement(offset, false, presentation, false) return presentation } - private fun create(text: String, onClick: (event: MouseEvent) -> Unit, editor: Editor): InlayPresentation? { - val factory = PresentationFactory(editor) + private fun create(text: String, onClick: (event: MouseEvent) -> Unit, factory: PresentationFactory): InlayPresentation? { val trimmed = trimWithEllipsis(text, INLAY_HINT_MAX_WIDTH) ?: return null - val textPresentation = factory.smallText(trimmed) - val hoverPresentation = factory.referenceOnHover(textPresentation) { event, _ -> - onClick.invoke(event) - } - val tooltipPresentation = factory.withTooltip("Click to change value", hoverPresentation) - return factory.roundWithBackground(tooltipPresentation) + return factory.roundWithBackground( + factory.withTooltip( + "Click to change value", + factory.referenceOnHover( + factory.smallText(trimmed)) { event, _ -> + onClick.invoke(event) + } + ) + ) } private fun onValidValue(setter: (value: String, wrapAt: Int) -> Unit, project: Project?) @@ -118,8 +133,8 @@ object Base64Presentations { } - class BinaryPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor) - : InlayPresentationsFactory(element, sink, editor) { + class BinaryPresentationsFactory(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) + : InlayPresentationsFactory(element, sink, editor, factory) { override fun create(adapter: Base64ValueAdapter): InlayPresentation? { val decoded = adapter.getDecodedBytes() ?: return null diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64ValueAdapter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64ValueAdapter.kt index 6169cc282..fdb6e160c 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64ValueAdapter.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/base64/Base64ValueAdapter.kt @@ -10,12 +10,14 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.editor.inlay.base64 +import com.intellij.json.psi.JsonProperty import com.intellij.psi.PsiElement import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64 import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64ToBytes import com.redhat.devtools.intellij.kubernetes.editor.util.encodeBase64 import com.redhat.devtools.intellij.kubernetes.editor.util.getValue import com.redhat.devtools.intellij.kubernetes.editor.util.setValue +import org.jetbrains.yaml.psi.YAMLKeyValue class Base64ValueAdapter(private val element: PsiElement) { @@ -80,6 +82,10 @@ class Base64ValueAdapter(private val element: PsiElement) { } fun getStartOffset(): Int? { - return com.redhat.devtools.intellij.kubernetes.editor.util.getStartOffset(element) + return when(element) { + is YAMLKeyValue -> element.value?.textRange?.startOffset + is JsonProperty -> element.value?.textRange?.startOffset + else -> null + } } } \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt new file mode 100644 index 000000000..e5bc38c94 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt @@ -0,0 +1,168 @@ +/******************************************************************************* + * 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.editor.inlay.selector + +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.presentation.InlayPresentation +import com.intellij.codeInsight.hints.presentation.PresentationFactory +import com.intellij.find.actions.ShowUsagesAction +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.ui.IconManager +import com.intellij.ui.awt.RelativePoint +import com.redhat.devtools.intellij.kubernetes.editor.util.getKey +import com.redhat.devtools.intellij.kubernetes.editor.util.getLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector +import com.redhat.devtools.intellij.kubernetes.editor.util.getTemplate +import com.redhat.devtools.intellij.kubernetes.editor.util.hasTemplate +import com.redhat.devtools.intellij.kubernetes.usage.LabelsFilter +import com.redhat.devtools.intellij.kubernetes.usage.SelectorsFilter +import java.awt.Point +import java.awt.event.MouseEvent +import javax.swing.Icon + + +object SelectorPresentations { + + private val selectorIcon = IconManager.getInstance().getIcon("icons/selector.svg", javaClass) + private val labelIcon = IconManager.getInstance().getIcon("icons/label.svg", javaClass) + + fun createForSelector( + element: PsiElement, + allElements: List, + sink: InlayHintsSink, + editor: Editor, + factory: PresentationFactory + ) { + val filter = LabelsFilter(element) + val matchingElements = allElements + .filter(filter::isAccepted) + val selectorAttribute = element.getSelector()?.parent + ?: return + + create( + selectorAttribute, + "${matchingElements.size} matching", + "Click to see matching labels", + selectorIcon, + editor, + sink, + factory + ) + } + + fun createForAllLabels( + element: PsiElement, + allElements: List, + sink: InlayHintsSink, + editor: Editor, + factory: PresentationFactory + ) { + createForLabels(element, element.getLabels(), allElements, sink, editor, factory) + if (element.hasTemplate()) { + createForLabels(element, element.getTemplate()?.getLabels(), allElements, sink, editor, factory) + } + } + + private fun createForLabels( + resource: PsiElement, + labels: PsiElement?, + allElements: List, + sink: InlayHintsSink, + editor: Editor, + factory: PresentationFactory + ) { + val labelsAttribute = labels?.parent + ?: return + val filter = SelectorsFilter(resource) + val matchingElements = allElements + .filter(filter::isAccepted) + create( + labelsAttribute, + "${matchingElements.size} matching", + "Click to see matching selectors", + labelIcon, + editor, + sink, + factory + ) + } + + private fun create( + element: PsiElement, + text: String, + toolTip: String, + icon: Icon, + editor: Editor, + sink: InlayHintsSink, + factory: PresentationFactory + ) { + val offset = element.getKey()?.textRange?.endOffset // to the right of the key + ?: return + + val textPresentation = createText(element, text, toolTip, editor, factory) + sink.addInlineElement(offset, true, textPresentation, true) + + val iconPresentation = createIcon(element, factory, editor, icon) + sink.addInlineElement(offset, true, iconPresentation, true) + } + + private fun createText( + element: PsiElement, + text: String, + toolTip: String, + editor: Editor, + factory: PresentationFactory + ): InlayPresentation { + return factory.withTooltip( + toolTip, + factory.referenceOnHover( + factory.roundWithBackground( + factory.text( + text + ) + ), + onClick(editor, element) + ) + ) + } + + private fun createIcon( + selector: PsiElement, + factory: PresentationFactory, + editor: Editor, + icon: Icon + ): InlayPresentation { + val iconPresentation = factory.referenceOnHover( + factory.roundWithBackground( + factory.smallScaledIcon(icon) + ), + onClick(editor, selector) + ) + return iconPresentation + } + + private fun onClick(editor: Editor, hintedKeyValue: PsiElement): + (event: MouseEvent, _: Point) -> Unit { + + return { event, point -> + val project = editor.project + if (project != null) { + ShowUsagesAction.startFindUsages(hintedKeyValue, RelativePoint(event), editor) + //ShowUsagesDispatcher.runWithCustomScope(project, hintedKeyValue) + //ShowUsagesDispatcher.findUsageManager(project, hintedKeyValue, editor) + } + } + } + + +} + diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt new file mode 100644 index 000000000..f8c1e0089 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt @@ -0,0 +1,85 @@ +package com.redhat.devtools.intellij.kubernetes.editor.inlay.selector + +import com.intellij.find.actions.ShowUsagesAction +import com.intellij.find.findUsages.FindUsagesManager +import com.intellij.find.findUsages.FindUsagesOptions +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.actionSystem.impl.SimpleDataContext +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.SearchScope + +object ShowUsagesDispatcher { + + @JvmStatic + fun runWithCustomScope(project: Project, element: PsiElement) { + val showUsagesAction: AnAction = ActionManager.getInstance().getAction(ShowUsagesAction.ID) ?: return + + // Create custom DataContext + val dataContext = SimpleDataContext.builder() + .add(CommonDataKeys.PROJECT, project) + .add(CommonDataKeys.PSI_ELEMENT, element) + .add(CommonDataKeys.PSI_FILE, element.containingFile) + .add(DataKey.create(FindUsagesOptions::class.java.name), createFindUsagesOptions(project, element)) + .build() + + // Create AnActionEvent + val actionEvent = AnActionEvent.createFromDataContext(ActionPlaces.UNKNOWN, null, dataContext) + + // Perform the action + showUsagesAction.actionPerformed(actionEvent) + } + + private fun createFindUsagesOptions(project: Project, element: PsiElement): FindUsagesOptions { + val options = FindUsagesOptions(project) + //val customScope: SearchScope = GlobalSearchScope.everythingScope(project) + options.searchScope = NoExclusionsScope(project) + return options + } + + fun findUsageManager(project: Project, element: PsiElement, editor: Editor) { + val fileEditor = getFileEditor(editor) + val findUsagesManager = FindUsagesManager(project) + val customScope: SearchScope = NoExclusionsScope(project) + + findUsagesManager.findUsages(element, null, fileEditor, false, customScope) + } + + private fun getFileEditor(editor: Editor): FileEditor? { + val virtualFile: VirtualFile = editor.virtualFile ?: return null + val project = editor.project ?: return null + val fileEditors = FileEditorManager.getInstance(project).getEditors(virtualFile) + + return fileEditors.find { fileEditor -> fileEditor is TextEditor && fileEditor.editor == editor } + } + + class NoExclusionsScope(project: Project) : GlobalSearchScope(project) { + override fun contains(file: VirtualFile): Boolean { + return true; // Include EVERY file, even excluded ones + } + + override fun isSearchInModuleContent(module: Module): Boolean { + return true + } + + override fun isSearchInModuleContent(module: Module, testSources: Boolean): Boolean { + return true; + } + + override fun isSearchInLibraries(): Boolean { + return true; + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/KubernetesTypeInfoUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/KubernetesTypeInfoUtils.kt new file mode 100644 index 000000000..e9b8e0b28 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/KubernetesTypeInfoUtils.kt @@ -0,0 +1,74 @@ +/******************************************************************************* + * 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.editor.util + +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo + +private const val KIND_DEPLOYMENT = "Deployment" +private const val KIND_CRON_JOB = "CronJob" +private const val KIND_DAEMON_SET = "DaemonSet" +private const val KIND_JOB = "Job" +private const val KIND_NETWORK_POLICY = "NetworkPolicy" +private const val KIND_PERSISTENT_VOLUME = "PersistentVolume" +private const val KIND_PERSISTENT_VOLUME_CLAIM = "PersistentVolumeClaim" +private const val KIND_POD = "Pod" +private const val KIND_POD_DISRUPTION_BUDGET = "PodDisruptionBudget" +private const val KIND_REPLICA_SET = "ReplicaSet" +private const val KIND_SERVICE = "Service" +private const val KIND_STATEFUL_SET = "StatefulSet" + +fun KubernetesTypeInfo.isCronJob(): Boolean { + return this.kind == KIND_CRON_JOB +} + +fun KubernetesTypeInfo.isDaemonSet(): Boolean { + return this.kind == KIND_DAEMON_SET +} + +fun KubernetesTypeInfo.isDeployment(): Boolean { + return this.kind == KIND_DEPLOYMENT +} + +fun KubernetesTypeInfo.isJob(): Boolean { + return this.kind == KIND_JOB +} + +fun KubernetesTypeInfo.isNetworkPolicy(): Boolean { + return this.kind == KIND_NETWORK_POLICY +} + +fun KubernetesTypeInfo.isPersistentVolume(): Boolean { + return this.kind == KIND_PERSISTENT_VOLUME +} + +fun KubernetesTypeInfo.isPersistentVolumeClaim(): Boolean { + return this.kind == KIND_PERSISTENT_VOLUME_CLAIM +} + +fun KubernetesTypeInfo.isPod(): Boolean { + return this.kind == KIND_POD +} + +fun KubernetesTypeInfo.isPodDisruptionBudget(): Boolean { + return this.kind == KIND_POD_DISRUPTION_BUDGET +} + +fun KubernetesTypeInfo.isReplicaSet(): Boolean { + return this.kind == KIND_REPLICA_SET +} + +fun KubernetesTypeInfo.isService(): Boolean { + return this.kind == KIND_SERVICE +} + +fun KubernetesTypeInfo.isStatefulSet(): Boolean { + return this.kind == KIND_STATEFUL_SET +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/PsiElements.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/PsiElements.kt new file mode 100644 index 000000000..d78b40244 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/PsiElements.kt @@ -0,0 +1,75 @@ +/******************************************************************************* + * 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.editor.util + +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileVisitor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.psi.search.FileTypeIndex +import com.intellij.psi.search.ProjectScope + +object PsiElements { + + fun getAll(type: FileType, project: Project?): List { + if (project == null) { + return emptyList() + } + + val scope = ProjectScope.getEverythingScope(project) + val psiManager = PsiManager.getInstance(project) + + return FileTypeIndex.getFiles(type, scope) + .mapNotNull { file -> psiManager.findFile(file) } + .flatMap { file -> file.getAllElements() } + } + + fun getAllNoExclusions(fileType: FileType, project: Project): List { + val basePath = project.basePath ?: return emptyList() + val projectBaseDir = LocalFileSystem.getInstance() + .findFileByPath(basePath) ?: return emptyList() + + val collector = AllFilesCollector(fileType) + VfsUtilCore.visitChildrenRecursively(projectBaseDir, collector) + val manager = PsiManager.getInstance(project) + return collector.getCollected() + .mapNotNull { file -> manager.findFile(file) } + .flatMap { psiFile -> psiFile.getAllElements() } + } + + private class AllFilesCollector(private val fileType: FileType): VirtualFileVisitor() { + + private val collected = HashSet() + + override fun visitFile(file: VirtualFile): Boolean { + if (!isFileType(file, fileType)) { + return true + } + collected.add(file) + return true + } + + fun getCollected(): Collection { + return collected + } + + private fun isFileType(file: VirtualFile, fileType: FileType): Boolean { + return !file.isDirectory + && !file.fileType.isBinary + && fileType == file.fileType + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt index 1158889c9..36c02183c 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtils.kt @@ -10,36 +10,23 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.editor.util -import com.intellij.json.psi.JsonElement import com.intellij.json.psi.JsonElementGenerator -import com.intellij.json.psi.JsonFile import com.intellij.json.psi.JsonProperty -import com.intellij.json.psi.JsonValue import com.intellij.openapi.application.ReadAction import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.project.Project -import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.Strings import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiElement import com.intellij.psi.PsiManager -import com.intellij.psi.PsiNamedElement import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo import com.redhat.devtools.intellij.kubernetes.editor.ResourceEditor import org.jetbrains.yaml.YAMLElementGenerator -import org.jetbrains.yaml.psi.YAMLDocument import org.jetbrains.yaml.psi.YAMLKeyValue -import org.jetbrains.yaml.psi.YAMLMapping -import org.jetbrains.yaml.psi.YAMLPsiElement -import org.jetbrains.yaml.psi.YAMLSequence -import org.jetbrains.yaml.psi.YAMLValue -import java.util.* +import java.util.Base64 -private const val KEY_METADATA = "metadata" -private const val KEY_DATA = "data" -private const val KEY_BINARY_DATA = "binaryData" private const val KEY_RESOURCE_VERSION = "resourceVersion" /** @@ -95,51 +82,6 @@ fun getKubernetesResourceInfo(file: VirtualFile?, project: Project): KubernetesR } } -/** - * Returns the [PsiElement] named "data" within the children of the given [PsiElement]. - * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. - * - * @param element the PsiElement whose "data" child should be found. - * @return the PsiElement named "data" - */ -fun getDataValue(element: PsiElement): PsiElement? { - val dataElement = element.children - .filterIsInstance() - .find { it.name == KEY_DATA } - return when (dataElement) { - is YAMLKeyValue -> - dataElement.value - is JsonProperty -> - dataElement.value - else -> - null - } -} - -/** - * Returns the [PsiElement] named "binaryData" within the children of the given [PsiElement]. - * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. - * - * @param element the PsiElement whose "binaryData" child should be found. - * @return the PsiElement named "binaryData" - */ -fun getBinaryData(element: PsiElement): PsiElement? { - return when (element) { - is YAMLPsiElement -> - element.children - .filterIsInstance() - .find { it.name == KEY_BINARY_DATA } - ?.value - is JsonElement -> - element.children.toList() - .filterIsInstance() - .find { it.name == KEY_BINARY_DATA } - ?.value - else -> - null - } -} - /** * Returns a base64 decoded String for the given base64 encoded String. * Returns `null` if decoding fails. @@ -203,21 +145,6 @@ fun getValue(element: PsiElement): String? { } } -/** - * Returns the startOffset in the [YAMLValue] or [JsonValue] of the given [PsiElement]. - * Returns `null` otherwise. - * - * @param element the psi element to retrieve the startOffset from - * @return the startOffset in the value of the given psi element - */ -fun getStartOffset(element: PsiElement): Int? { - return when (element) { - is YAMLKeyValue -> element.value?.textRange?.startOffset - is JsonProperty -> element.value?.textRange?.startOffset - else -> null - } -} - fun setValue(value: String, element: PsiElement) { val newElement = when (element) { is YAMLKeyValue -> @@ -273,4 +200,3 @@ fun getExistingResourceEditor(editor: FileEditor?): ResourceEditor? { fun getExistingResourceEditor(file: VirtualFile?): ResourceEditor? { return file?.getUserData(ResourceEditor.KEY_RESOURCE_EDITOR) } - diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtils.kt new file mode 100644 index 000000000..e27b7dd94 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtils.kt @@ -0,0 +1,265 @@ +/******************************************************************************* + * 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.editor.util + +import com.intellij.json.psi.JsonFile +import com.intellij.json.psi.JsonObject +import com.intellij.json.psi.JsonProperty +import com.intellij.json.psi.JsonValue +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo +import org.jetbrains.yaml.psi.YAMLDocument +import org.jetbrains.yaml.psi.YAMLFile +import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.YAMLMapping +import org.jetbrains.yaml.psi.YAMLValue + +private const val KEY_METADATA = "metadata" +private const val KEY_NAME = "name" +private const val KEY_LABELS = "labels" +private const val KEY_SPEC = "spec" +private const val KEY_SELECTOR = "selector" +private const val KEY_TEMPLATE = "template" +private const val KEY_BINARY_DATA = "binaryData" +private const val KEY_DATA = "data" + + +fun PsiFile.getAllElements(): List { + return when(this) { + is YAMLFile -> this.documents.mapNotNull { document -> document.topLevelValue } + is JsonFile -> this.allTopLevelValues + else -> emptyList() + } +} + +fun PsiElement.getKey(): PsiElement?{ + return when(this) { + is YAMLKeyValue -> this.key + is JsonProperty -> this.nameElement + else -> null + } +} + +fun PsiElement.getValue(): PsiElement?{ + return when(this) { + is YAMLKeyValue -> this.value + is JsonProperty -> this.value + else -> null + } +} + +fun PsiFile.hasKubernetesResource(): Boolean { + return KubernetesTypeInfo.create(this) != null +} + +fun PsiElement.isKubernetesResource(): Boolean { + return when (this) { + is PsiFile -> false + is YAMLDocument -> false + else -> KubernetesTypeInfo.create(this) != null + } +} + +fun PsiElement.getKubernetesTypeInfo(): KubernetesTypeInfo? { + return when { + this is PsiFile || this is YAMLDocument -> null // KubernetesTypeInfo.create() creates a type for nested elements + else -> KubernetesTypeInfo.create(this) + } +} + +fun PsiElement.getResource(): PsiElement? { + return when { + this.isKubernetesResource() -> + this + parent != null -> + parent.getResource() + else -> + null + } +} + +fun PsiElement.getResourceName(): PsiElement? { + return when(this) { + is YAMLMapping -> getResourceName() + is JsonObject -> getResourceName() + else -> null + } +} + +fun YAMLMapping.getResourceName(): YAMLValue? { + return getMetadata()?.getKeyValueByKey(KEY_NAME) + ?.value +} + +fun JsonObject.getResourceName(): JsonValue? { + return getMetadata()?.findProperty(KEY_NAME) + ?.value +} + +fun JsonObject.getMetadata(): JsonObject? { + return this.findProperty(KEY_METADATA) + ?.value as? JsonObject +} + +fun YAMLMapping.getMetadata(): YAMLMapping? { + return this.getKeyValueByKey(KEY_METADATA) + ?.value as? YAMLMapping +} + +fun PsiElement.isLabels(): Boolean { + return when (this) { + is YAMLKeyValue -> KEY_LABELS == this.keyText + is JsonProperty -> KEY_LABELS == this.name + else -> false + } +} + +fun PsiElement.hasLabels(): Boolean { + return when(this) { + is YAMLMapping -> hasLabels() + is JsonObject -> hasLabels() + else -> false + } +} + +fun YAMLMapping.hasLabels(): Boolean { + return true == this.getLabels()?.keyValues?.isNotEmpty() +} + +fun JsonObject.hasLabels(): Boolean { + return true == this.getLabels()?.propertyList?.isNotEmpty() +} + +fun PsiElement.getLabels(): PsiElement? { + return when(this) { + is YAMLMapping -> this.getLabels() + is JsonObject -> this.getLabels() + else -> null + } +} + +fun YAMLMapping.getLabels(): YAMLMapping? { + return this.getMetadata() + ?.getKeyValueByKey(KEY_LABELS) + ?.value as? YAMLMapping +} + +fun JsonObject.getLabels(): JsonObject? { + return this.getMetadata() + ?.findProperty(KEY_LABELS)?.value as? JsonObject +} + +fun PsiElement.hasSelector(): Boolean { + return this.getSelector() != null +} + +fun PsiElement.isSelector(): Boolean { + return when (this) { + is YAMLKeyValue -> KEY_SELECTOR == this.keyText + is JsonProperty -> KEY_SELECTOR == this.name + else -> false + } +} + +fun PsiElement.getSelector(): PsiElement? { + return when (this) { + is YAMLMapping -> this.getSelector() + is JsonObject -> this.getSelector() + else -> null + } +} + +fun YAMLMapping.getSelector(): YAMLMapping? { + return (this.getKeyValueByKey(KEY_SPEC)?.value as? YAMLMapping) + ?.getKeyValueByKey(KEY_SELECTOR)?.value as? YAMLMapping +} + +fun JsonObject.getSelector(): JsonObject? { + return (this.findProperty(KEY_SPEC)?.value as? JsonObject) + ?.findProperty(KEY_SELECTOR)?.value as? JsonObject +} + +fun PsiElement.hasTemplate(): Boolean { + return this.getTemplate() != null +} + +fun PsiElement.getTemplate(): PsiElement? { + return when(this) { + is YAMLMapping -> this.getTemplate() + is JsonObject -> this.getTemplate() + else -> + null + } +} + +fun YAMLMapping.getTemplate(): YAMLMapping? { + return (this.getKeyValueByKey(KEY_SPEC)?.value as? YAMLMapping) + ?.getKeyValueByKey(KEY_TEMPLATE)?.value as? YAMLMapping +} + +fun JsonObject.getTemplate(): JsonObject? { + return (this.findProperty(KEY_SPEC)?.value as? JsonObject?) + ?.findProperty(KEY_TEMPLATE)?.value as? JsonObject? +} +fun PsiElement.hasTemplateLabels(): Boolean { + return this.getTemplateLabels() != null +} + +fun PsiElement.getTemplateLabels(): PsiElement? { + return when(this) { + is YAMLMapping -> this.getTemplateLabels() + is JsonObject -> this.getTemplateLabels() + else -> + null + } +} + +fun YAMLMapping.getTemplateLabels(): YAMLMapping? { + return this.getTemplate()?.getLabels() +} + +fun JsonObject.getTemplateLabels(): JsonObject? { + return this.getTemplate()?.getLabels() +} + +/** + * Returns the [PsiElement] named "binaryData". + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @return the PsiElement named "binaryData" + */ +fun PsiElement.getBinaryData(): PsiElement? { + return when (this) { + is YAMLMapping -> + this.getKeyValueByKey(KEY_BINARY_DATA)?.value + is JsonObject -> + this.findProperty(KEY_BINARY_DATA)?.value + else -> + null + } +} + +/** + * Returns the [PsiElement] named "data" within the children of the given [PsiElement]. + * Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise. + * + * @param element the PsiElement whose "data" child should be found. + * @return the PsiElement named "data" + */ +fun PsiElement.getDataValue(): PsiElement? { + return when (this) { + is YAMLMapping -> this.getKeyValueByKey(KEY_DATA)?.value + is JsonObject -> this.findProperty(KEY_DATA)?.value + else -> null + } +} + diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtils.kt new file mode 100644 index 000000000..facdf72ef --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtils.kt @@ -0,0 +1,160 @@ +/******************************************************************************* + * 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.editor.util + +import com.intellij.json.psi.JsonObject +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiElement +import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.YAMLMapping +import org.jetbrains.yaml.psi.YAMLSequence +import org.jetbrains.yaml.psi.YAMLSequenceItem + +private const val KEY_MATCH_LABELS = "matchLabels" +private const val KEY_MATCH_EXPRESSIONS = "matchExpressions" + +private const val KEY_KEY = "key" +private const val KEY_OPERATOR = "operator" +private enum class OPERATORS { In, NotIn, Exists, DoesNotExist } +private const val KEY_VALUES = "values" + +fun PsiElement.hasMatchExpressions(): Boolean { + return when(this) { + is YAMLMapping -> true == this.getMatchExpressions()?.items?.isNotEmpty() + is JsonObject -> true == this.getMatchExpressions()?.propertyList?.isNotEmpty() + else -> false + } +} + +fun YAMLMapping.getMatchExpressions(): YAMLSequence? { + return this.getSelector() + ?.getKeyValueByKey(KEY_MATCH_EXPRESSIONS) + ?.value as? YAMLSequence? +} + +fun JsonObject.getMatchExpressions(): JsonObject? { + return this.getSelector() + ?.findProperty(KEY_MATCH_EXPRESSIONS) + ?.value as? JsonObject? +} + +fun PsiElement.areMatchingMatchLabels(labels: PsiElement): Boolean { + return when(this) { + is YAMLMapping -> { + val yamlLabels = labels as? YAMLMapping ?: return false + this.areMatchingMatchLabels(yamlLabels) + } + is JsonObject -> + false + else -> + false + } +} + +private fun YAMLMapping.areMatchingMatchLabels(labels: YAMLMapping): Boolean { + val matchLabels = this.getMatchLabels() ?: return false + return matchLabels.keyValues.all { matchLabel -> + this.isMatchingMatchLabel(matchLabel, labels) + } +} + +private fun YAMLMapping.isMatchingMatchLabel(matchLabel: YAMLKeyValue, labels: YAMLMapping): Boolean { + val labelName = matchLabel.keyText + val labelValue = matchLabel.valueText + val matching = labels.keyValues.find { it.keyText == labelName } ?: return false + return matching.valueText == labelValue +} + +fun PsiElement.areMatchingMatchExpressions(labels: PsiElement): Boolean { + return when(this) { + is YAMLMapping -> { + val yamlLabels = labels as? YAMLMapping? ?: return false + this.areMatchingMatchExpressions(yamlLabels) + } + is JsonObject -> + false + else -> + false + } +} + +private fun YAMLMapping.areMatchingMatchExpressions(labels: YAMLMapping): Boolean { + val expressions = this.getMatchExpressions() ?: return false + return expressions.items.all { expression -> + expression.isMatchingMatchExpression(labels) + } +} + +fun YAMLSequenceItem.isMatchingMatchExpression(labels: YAMLMapping): Boolean { + val expression = value as? YAMLMapping ?: return false + val key = expression.getKeyValueByKey(KEY_KEY)?.valueText ?: return false + val operator = expression.getKeyValueByKey(KEY_OPERATOR)?.valueText + val values = expression.getKeyValueByKey(KEY_VALUES)?.value as? YAMLSequence + + val label = labels.getKeyValueByKey(key) // key label in expression not found in element labels + + return when (operator) { + OPERATORS.In.name -> { + label != null + && values != null + && values.items.any { + StringUtil.unquoteString(it.text) == StringUtil.unquoteString(label.valueText) + } + } + + OPERATORS.NotIn.name -> { + label == null + || values == null + || values.items.any { + StringUtil.unquoteString(it.text) == StringUtil.unquoteString(label.valueText) + } + } + + OPERATORS.Exists.name -> + label != null + + OPERATORS.DoesNotExist.name -> + label == null + + else -> + false + } +} + +fun PsiElement.hasMatchLabels(): Boolean { + return when(this) { + is YAMLMapping -> true == this.getMatchLabels()?.keyValues?.isNotEmpty() + is JsonObject -> true == this.getMatchLabels()?.propertyList?.isNotEmpty() + else -> false + } +} + +fun YAMLMapping.getMatchLabels(): YAMLMapping? { + val selector = this.getSelector() ?: return null + val matchLabels = selector.getKeyValueByKey(KEY_MATCH_LABELS) + return if (matchLabels != null) { + matchLabels.value as? YAMLMapping? + } else { + // Service can have matchLabels as direct children without the 'matchLabels' key. + selector + } +} + +private fun JsonObject.getMatchLabels(): JsonObject? { + val selector = this.getSelector() ?: return null + val matchLabels = selector.findProperty(KEY_MATCH_LABELS) + return if (matchLabels != null) { + matchLabels.value as? JsonObject? + } else { + // Service can have matchLabels as direct children without the 'matchLabels' key. + selector + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt new file mode 100644 index 000000000..032074b85 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.usage + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiElementFilter +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.areMatchingMatchExpressions +import com.redhat.devtools.intellij.kubernetes.editor.util.areMatchingMatchLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.getLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.getResource +import com.redhat.devtools.intellij.kubernetes.editor.util.getTemplateLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.hasMatchExpressions +import com.redhat.devtools.intellij.kubernetes.editor.util.hasMatchLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.hasSelector +import com.redhat.devtools.intellij.kubernetes.editor.util.isCronJob +import com.redhat.devtools.intellij.kubernetes.editor.util.isDaemonSet +import com.redhat.devtools.intellij.kubernetes.editor.util.isDeployment +import com.redhat.devtools.intellij.kubernetes.editor.util.isJob +import com.redhat.devtools.intellij.kubernetes.editor.util.isNetworkPolicy +import com.redhat.devtools.intellij.kubernetes.editor.util.isPersistentVolume +import com.redhat.devtools.intellij.kubernetes.editor.util.isPersistentVolumeClaim +import com.redhat.devtools.intellij.kubernetes.editor.util.isPod +import com.redhat.devtools.intellij.kubernetes.editor.util.isPodDisruptionBudget +import com.redhat.devtools.intellij.kubernetes.editor.util.isReplicaSet +import com.redhat.devtools.intellij.kubernetes.editor.util.isService +import com.redhat.devtools.intellij.kubernetes.editor.util.isStatefulSet + +/** + * A filter that accepts labels, that are matching a given selector. + */ +class LabelsFilter(selector: PsiElement): PsiElementFilter { + + private val selectorResource: PsiElement? by lazy { + selector.getResource() + } + + private val selectorResourceType: KubernetesTypeInfo? by lazy { + selectorResource?.getKubernetesTypeInfo() + } + + private val hasSelector: Boolean by lazy { + selectorResource?.hasSelector() ?: false + } + + private val hasMatchLabels: Boolean by lazy { + selectorResource?.hasMatchLabels() ?: false + } + + private val hasMatchExpressions: Boolean by lazy { + selectorResource?.hasMatchExpressions() ?: false + } + + override fun isAccepted(toAccept: PsiElement): Boolean { + if (selectorResourceType == null + || !hasSelector) { + return false + } + + val labeledResourceType = toAccept.getKubernetesTypeInfo() ?: return false + if (!canSelect(labeledResourceType)) { + return false + } + + val labels = getLabels(labeledResourceType, toAccept, selectorResourceType) ?: return false + val selectorResource = selectorResource ?: return false + return when { + hasMatchLabels && hasMatchExpressions -> + selectorResource.areMatchingMatchLabels(labels) + && selectorResource.areMatchingMatchExpressions(labels) + + hasMatchLabels -> + selectorResource.areMatchingMatchLabels(labels) + + hasMatchExpressions -> + selectorResource.areMatchingMatchExpressions(labels) + + else -> false + } + } + + private fun canSelect(type: KubernetesTypeInfo): Boolean { + val selectorType = selectorResourceType ?: return false + return when { + selectorType.isDeployment() -> + type.isPod() + || type.isDeployment() // can select deployment template + + selectorType.isCronJob() -> + type.isPod() + || type.isCronJob() // template + + selectorType.isDaemonSet() -> + type.isPod() + || type.isDaemonSet() // template + + selectorType.isJob() -> + type.isPod() + || type.isJob() // template + + selectorType.isReplicaSet() -> + type.isPod() + || type.isReplicaSet() // template + + selectorType.isStatefulSet() -> + type.isPod() + || type.isStatefulSet() // template + + selectorType.isNetworkPolicy() + || selectorType.isPodDisruptionBudget() + || selectorType.isService() -> + type.isPod() + + selectorType.isPersistentVolumeClaim() -> + type.isPersistentVolume() + || type.isPersistentVolumeClaim() + + else -> + false + } + } + + private fun getLabels( + labeledType: KubernetesTypeInfo, + labeledResource: PsiElement, + selectorResourceType: KubernetesTypeInfo? + ): PsiElement? { + return when { + selectorResourceType == null -> + null + + (selectorResourceType.isCronJob() && labeledType.isCronJob()) + || (selectorResourceType.isDaemonSet() && labeledType.isDaemonSet()) + || (selectorResourceType.isDeployment() && labeledType.isDeployment()) + || (selectorResourceType.isJob() && labeledType.isJob()) + || (selectorResourceType.isReplicaSet() && labeledType.isReplicaSet()) + || (selectorResourceType.isStatefulSet() && labeledType.isStatefulSet()) -> + labeledResource.getTemplateLabels() + + else -> + labeledResource.getLabels() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorDescriptionProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorDescriptionProvider.kt new file mode 100644 index 000000000..99611039b --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorDescriptionProvider.kt @@ -0,0 +1,63 @@ +/******************************************************************************* + * 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.usage + +import com.intellij.json.psi.JsonObject +import com.intellij.psi.ElementDescriptionLocation +import com.intellij.psi.ElementDescriptionProvider +import com.intellij.psi.PsiElement +import com.intellij.usageView.UsageViewLongNameLocation +import com.intellij.usageView.UsageViewNodeTextLocation +import com.intellij.usageView.UsageViewTypeLocation +import com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.getResource +import com.redhat.devtools.intellij.kubernetes.editor.util.getResourceName +import com.redhat.devtools.intellij.kubernetes.editor.util.isLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.isSelector +import org.jetbrains.yaml.psi.YAMLMapping + +class SelectorDescriptionProvider: ElementDescriptionProvider { + override fun getElementDescription(searchArgument: PsiElement, location: ElementDescriptionLocation): String? { + return when (location) { + is UsageViewTypeLocation -> + getUsageViewTypeDescription(searchArgument) + + is UsageViewNodeTextLocation -> + searchArgument.containingFile.virtualFile.name + + is UsageViewLongNameLocation -> + "" // prevent default description provider from adding "selector"/"labels" + + is YAMLMapping -> + "YAMLBlock" + + is JsonObject -> + "JsonObject" + + + else -> null + } + } + + private fun getUsageViewTypeDescription(element: PsiElement): String { + val resource = element.getResource() + val type = resource?.getKubernetesTypeInfo()?.kind + val name = element.getResource()?.getResourceName()?.text + return when { + element.isSelector() -> + "Labels matching selector in $type '$name'" + element.isLabels() -> + "Selectors matching labels in $type '$name'" + else -> + "Matching $type '$name'" + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt new file mode 100644 index 000000000..6fe16533d --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt @@ -0,0 +1,100 @@ +/******************************************************************************* + * 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.usage + +import com.intellij.find.findUsages.CustomUsageSearcher +import com.intellij.find.findUsages.FindUsagesOptions +import com.intellij.openapi.application.ReadAction +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiElementFilter +import com.intellij.usageView.UsageInfo +import com.intellij.usages.Usage +import com.intellij.usages.UsageInfo2UsageAdapter +import com.intellij.util.Processor +import com.redhat.devtools.intellij.kubernetes.editor.util.PsiElements +import com.redhat.devtools.intellij.kubernetes.editor.util.getLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.getResource +import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector +import com.redhat.devtools.intellij.kubernetes.editor.util.isLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.isSelector + +class SelectorUsageSearcher : CustomUsageSearcher() { + + override fun processElementUsages( + searchParameter: PsiElement, + processor: Processor, + options: FindUsagesOptions + ) { + ReadAction.run { + if (!searchParameter.isValid) { + return@run + } + + val file = searchParameter.containingFile + if (file == null + || !file.isValid + ) { + return@run + } + + /** + * dont use scope: + * [com.intellij.find.actions.ShowUsagesAction.startFindUsages] is using [GlobalSearchScope.projectScope] + * which excludes non-classpath folders. I did not find a way to force a custom scope into the + * [com.intellij.find.findUsages.FindUsagesOptions]. + **/ + //val searchScope = options.searchScope + //if (searchScope.contains(file.virtualFile)) { + getAllMatching(searchParameter) + .forEach { matchingElement -> + val searchResult = getMatchingAttribute(searchParameter, matchingElement) + if (searchResult != null) { + processor.process( + UsageInfo2UsageAdapter(UsageInfo(searchResult)) + ) + } + } + } + } + + private fun getAllMatching(searchParameter: PsiElement): Collection { + val fileType = searchParameter.containingFile.fileType + val project = searchParameter.project + val filter = getFilter(searchParameter) ?: return emptyList() + return PsiElements.getAllNoExclusions(fileType, project) + .filter(filter::isAccepted) + } + + private fun getFilter(searchParameter: PsiElement): PsiElementFilter? { + val resource = searchParameter.getResource() ?: return null + return when { + searchParameter.isSelector() -> + LabelsFilter(resource) + + searchParameter.isLabels() -> + SelectorsFilter(resource) + + else -> + null + } + } + + private fun getMatchingAttribute(searchParameter: PsiElement, matchingElement: PsiElement): PsiElement? { + return when { + searchParameter.isSelector() -> + matchingElement.getLabels()?.parent // beginning of labels block/property + searchParameter.isLabels() -> + matchingElement.getSelector()?.parent // beginning of selector block/property + else -> + null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsagesHandlerFactory.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsagesHandlerFactory.kt new file mode 100644 index 000000000..fc676f61b --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsagesHandlerFactory.kt @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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.usage + +import com.intellij.find.findUsages.FindUsagesHandler +import com.intellij.find.findUsages.FindUsagesHandlerFactory +import com.intellij.find.findUsages.FindUsagesOptions +import com.intellij.psi.PsiElement +import com.intellij.usageView.UsageInfo +import com.intellij.util.Processor +import com.redhat.devtools.intellij.kubernetes.editor.util.isLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.isSelector + +class SelectorUsagesHandlerFactory : FindUsagesHandlerFactory() { + + override fun canFindUsages(element: PsiElement): Boolean { + return element.isSelector() + || element.isLabels() + } + + override fun createFindUsagesHandler(element: PsiElement, forHighlightUsages: Boolean): FindUsagesHandler { + return object : FindUsagesHandler(element) { + override fun processElementUsages( + element: PsiElement, + processor: Processor, + options: FindUsagesOptions + ): Boolean { + return true + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt new file mode 100644 index 000000000..04219f2a6 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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.usage + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiElementFilter +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.hasLabels +import com.redhat.devtools.intellij.kubernetes.editor.util.hasSelector +import com.redhat.devtools.intellij.kubernetes.editor.util.hasTemplateLabels + +/** + * A filter that accepts selectors that are matching a given label + */ +class SelectorsFilter(private val labeledResource: PsiElement): PsiElementFilter { + + private val labeledResourceType: KubernetesTypeInfo? by lazy { + labeledResource.getKubernetesTypeInfo() + } + + private val hasLabels: Boolean by lazy { + labeledResource.hasLabels() + || labeledResource.hasTemplateLabels() + } + + override fun isAccepted(toAccept: PsiElement): Boolean { + if (labeledResourceType == null + || !hasLabels + || !toAccept.hasSelector()) { + return false + } + return LabelsFilter(toAccept) + .isAccepted(labeledResource) + } + +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 11e824970..b1330b6bb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -238,6 +238,15 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/icons/label2.svg b/src/main/resources/icons/label2.svg new file mode 100644 index 000000000..b6f7a47b7 --- /dev/null +++ b/src/main/resources/icons/label2.svg @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/icons/selector.svg b/src/main/resources/icons/selector.svg new file mode 100644 index 000000000..784258109 --- /dev/null +++ b/src/main/resources/icons/selector.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/icons/selector2.svg b/src/main/resources/icons/selector2.svg new file mode 100644 index 000000000..c5977d45c --- /dev/null +++ b/src/main/resources/icons/selector2.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsTest.kt index 9948fb068..abf4145a6 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64PresentationsTest.kt @@ -10,71 +10,124 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.editor.inlay +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.presentation.PresentationFactory +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo import com.redhat.devtools.intellij.kubernetes.editor.inlay.base64.Base64Presentations import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue -import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLValue -import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesResourceInfo +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLMapping import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesTypeInfo -import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.yaml.psi.YAMLMapping +import org.junit.Before import org.junit.Test class Base64PresentationsTest { - private val secret = kubernetesTypeInfo("Secret", "v1") - private val configMap = kubernetesTypeInfo("ConfigMap", "v1") - private val pod = kubernetesTypeInfo("Pod", "v1") + private lateinit var secret: KubernetesTypeInfo + private lateinit var configMap: KubernetesTypeInfo + private lateinit var pod: KubernetesTypeInfo - private val dataElement = createYAMLKeyValue("data") - private val binaryDataElement = createYAMLKeyValue("binaryData") + private lateinit var yamlElement: YAMLMapping + private lateinit var stringPresentation: + (element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit + private lateinit var binaryPresentation: + (element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit - @Test - fun `#create should create factory for Secret if has data`() { - // given - val content = createYAMLValue(arrayOf(dataElement)) - // when - val factory = Base64Presentations.create(content, secret, mock(), mock()) - // then - assertThat(factory).isNotNull() - } + @Before + fun before() { + this.secret = kubernetesTypeInfo("Secret", "v1") + this.configMap = kubernetesTypeInfo("ConfigMap", "v1") + this.pod = kubernetesTypeInfo("Pod", "v1") + this.yamlElement = mock() + this.stringPresentation = + mock<(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit>() + this.binaryPresentation = + mock<(element: PsiElement, sink: InlayHintsSink, editor: Editor, factory: PresentationFactory) -> Unit>() + } - @Test - fun `#create should NOT create factory for Secret if has NO data`() { - // given - val content = createYAMLValue(emptyArray()) - // when - val factory = Base64Presentations.create(content, secret, mock(), mock()) - // then - assertThat(factory).isNull() - } + @Test + fun `#create invokes string presentation factory if secret has data`() { + // given + val dataMapping = createYAMLMapping( + listOf( + createYAMLKeyValue("token-id", "NWVtaXRq"), + createYAMLKeyValue("token-secret", "a3E0Z2lodnN6emduMXAwcg==") + ) + ) + createYAMLKeyValue("data", dataMapping, yamlElement) + // when + Base64Presentations.create(yamlElement, secret, mock(), mock(), mock(), stringPresentation) + // then + verify(stringPresentation).invoke(any(), any(), any(), any()) + } - @Test - fun `#create should create factory for ConfigMap if has binaryData`() { - // given - val content = createYAMLValue(arrayOf(binaryDataElement)) - // when - val factory = Base64Presentations.create(content, configMap, mock(), mock()) - // then - assertThat(factory).isNotNull() - } + @Test + fun `#create does NOT invoke string presentation factory if secret has NO data`() { + // given + // when + Base64Presentations.create(yamlElement, secret, mock(), mock(), mock(), stringPresentation) + // then + verify(stringPresentation, never()).invoke(any(), any(), any(), any()) + } - @Test - fun `#create NOT should create factory for ConfigMap if has NO binaryData`() { - // given - val content = createYAMLValue(emptyArray()) - // when - val factory = Base64Presentations.create(content, configMap, mock(), mock()) - // then - assertThat(factory).isNull() - } + @Test + fun `#create invokes binary presentation factory if ConfigMap has binaryData`() { + // given + val binaryDataMapping = createYAMLMapping( + listOf( + createYAMLKeyValue("my-binary-file.bin", "U29tZSBiYXNlNjQgZW5jb2RlZCBiaW5hcnkgZGF0YQ"), + createYAMLKeyValue("another-binary.dat", "VGhpcyBpcyBhbm90aGVyIGV4YW1wbGUgb2YgYmluYXJ5IGRhdGE") + ) + ) + createYAMLKeyValue("binaryData", binaryDataMapping, yamlElement) + // when + Base64Presentations.create(yamlElement, configMap, mock(), mock(), mock(), mock(), binaryPresentation) + // then + verify(binaryPresentation).invoke(any(), any(), any(), any()) + } - @Test - fun `#create should NOT create factory for Pod`() { - // given - // when - val factory = Base64Presentations.create(mock(), pod, mock(), mock()) - // then - assertThat(factory).isNull() - } -} \ No newline at end of file + + @Test + fun `#create does NOT invoke binary presentation factory if ConfigMap has NO binaryData`() { + // given + // when + Base64Presentations.create( + yamlElement, configMap, mock(), mock(), mock(), mock(), binaryPresentation + ) + // then + verify(binaryPresentation, never()).invoke(any(), any(), any(), any()) + } + + @Test + fun `#create should NOT create factory for Pod`() { + // given + val binaryDataMapping = createYAMLMapping( + listOf( + createYAMLKeyValue("my-binary-file.bin", "U29tZSBiYXNlNjQgZW5jb2RlZCBiaW5hcnkgZGF0YQ"), + createYAMLKeyValue("another-binary.dat", "VGhpcyBpcyBhbm90aGVyIGV4YW1wbGUgb2YgYmluYXJ5IGRhdGE") + ) + ) + createYAMLKeyValue("binaryData", binaryDataMapping, yamlElement) + val dataMapping = createYAMLMapping( + listOf( + createYAMLKeyValue("token-id", "NWVtaXRq"), + createYAMLKeyValue("token-secret", "a3E0Z2lodnN6emduMXAwcg==") + ) + ) + createYAMLKeyValue("data", dataMapping, yamlElement) + // when + Base64Presentations.create( + mock(), pod, mock(), mock(), mock(), stringPresentation, binaryPresentation + ) + // then + verify(stringPresentation, never()).invoke(any(), any(), any(), any()) + verify(binaryPresentation, never()).invoke(any(), any(), any(), any()) + } +} diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapterTest.kt index 32db504c7..360d578cf 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapterTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/Base64ValueAdapterTest.kt @@ -17,10 +17,12 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.redhat.devtools.intellij.kubernetes.editor.inlay.base64.Base64ValueAdapter +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonObject import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonProperty import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonPsiFileFactory import com.redhat.devtools.intellij.kubernetes.editor.mocks.createProjectWithServices import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLGenerator +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLMapping import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue import org.assertj.core.api.Assertions.assertThat import org.jetbrains.yaml.psi.YAMLKeyValue @@ -37,7 +39,7 @@ class Base64ValueAdapterTest { @Test fun `#get should return value of YAMLKeyValue`() { // given - val element = createYAMLKeyValue(value = "yoda", project = project) + val element = createYAMLKeyValue("jedi", value = "yoda", project = project) val adapter = Base64ValueAdapter(element) // when val text = adapter.get() @@ -48,7 +50,7 @@ class Base64ValueAdapterTest { @Test fun `#get should return value with quotes`() { // given - val element = createYAMLKeyValue(value = "\"yoda\"", project = project) + val element = createYAMLKeyValue(key = "jedi", value = "\"yoda\"", project = project) val adapter = Base64ValueAdapter(element) // when val text = adapter.get() @@ -59,7 +61,7 @@ class Base64ValueAdapterTest { @Test fun `#get should return value of JsonProperty`() { // given - val element = createJsonProperty(value = "yoda", project = project) + val element = createJsonProperty("jedi", "yoda", project = project) val adapter = Base64ValueAdapter(element) // when val text = adapter.get() @@ -81,7 +83,7 @@ class Base64ValueAdapterTest { @Test fun `#getDecoded should return value decoded value`() { // given - val element = createYAMLKeyValue(value = toBase64("skywalker"), project = project) + val element = createYAMLKeyValue("jedi", value = toBase64("skywalker"), project = project) val adapter = Base64ValueAdapter(element) // when val text = adapter.getDecoded() @@ -92,7 +94,7 @@ class Base64ValueAdapterTest { @Test fun `#getDecoded should return null if value isn't valid base64`() { // given - val element = createYAMLKeyValue(value = toBase64("skywalker") + "bogus", project = project) + val element = createYAMLKeyValue("anakin", toBase64("skywalker") + "bogus", project = project) val adapter = Base64ValueAdapter(element) // when val text = adapter.getDecoded() @@ -103,7 +105,7 @@ class Base64ValueAdapterTest { @Test fun `#getDecoded should return null if value is null`() { // given - val element = createYAMLKeyValue(value = null, project = project) + val element = createYAMLKeyValue("c-3po", null, project = project) val adapter = Base64ValueAdapter(element) // when val text = adapter.getDecoded() @@ -114,7 +116,7 @@ class Base64ValueAdapterTest { @Test fun `#getDecoded should return value without quotes`() { // given - val element = createYAMLKeyValue(value = "\"" + toBase64("yoda") + "\"", project = project) + val element = createYAMLKeyValue("jedi", value = "\"" + toBase64("yoda") + "\"", project = project) val adapter = Base64ValueAdapter(element) // when val text = adapter.getDecoded() @@ -125,7 +127,7 @@ class Base64ValueAdapterTest { @Test fun `#getDecodedBytes should return decoded bytes`() { // given - val element = createYAMLKeyValue(value = toBase64("skywalker"), project = project) + val element = createYAMLKeyValue("jedi", value = toBase64("skywalker"), project = project) val adapter = Base64ValueAdapter(element) // when val bytes = adapter.getDecodedBytes() @@ -136,7 +138,7 @@ class Base64ValueAdapterTest { @Test fun `#getDecodedBytes should return null for null value`() { // given - val element = createYAMLKeyValue(value = null, project = project) + val element = createYAMLKeyValue("jedi", value = null, project = project) val adapter = Base64ValueAdapter(element) // when val bytes = adapter.getDecodedBytes() @@ -147,7 +149,7 @@ class Base64ValueAdapterTest { @Test fun `#set should add new YAMKeyValue to parent and delete current element`() { // given - val parent = createYAMLKeyValue("group", "jedis", project = project) + val parent = createYAMLMapping("group", "jedis") val element = createYAMLKeyValue("jedi", "yoda", parent, project) val adapter = Base64ValueAdapter(element) // when @@ -160,8 +162,8 @@ class Base64ValueAdapterTest { @Test fun `#set should create new YAMKeyValue with same key and given base64 encoded value`() { // given - val parent = createYAMLKeyValue("group", "jedis", project = project) - val element = createYAMLKeyValue("jedi", "yoda", parent, project) + val parent = createYAMLMapping("group", "jedis") + val element = createYAMLKeyValue("jedi", "yoda", parent, project = project) val adapter = Base64ValueAdapter(element) // when adapter.set("obiwan") @@ -172,7 +174,7 @@ class Base64ValueAdapterTest { @Test fun `#set should create new multiline value if existing value is multiline`() { // given - val parent = createYAMLKeyValue("group", "jedis", project = project) + val parent = createYAMLMapping("group", "jedis") val element = createYAMLKeyValue("jedi", "|\nyoda", parent, project) val adapter = Base64ValueAdapter(element) // when @@ -184,8 +186,8 @@ class Base64ValueAdapterTest { @Test fun `#set should create new quoted value if existing value is quoted`() { // given - val parent = createYAMLKeyValue("group", "jedis", project = project) - val element = createYAMLKeyValue("jedi", "\"yoda\"", parent, project) + val parent = createYAMLMapping("group", "jedis") + val element = createYAMLKeyValue("jedi", "\"yoda\"", parent, project = project) val adapter = Base64ValueAdapter(element) // when adapter.set("anakin") @@ -196,7 +198,7 @@ class Base64ValueAdapterTest { @Test fun `#set should wrap new value at given position`() { // given - val parent = createYAMLKeyValue("group", "jedis", project = project) + val parent = createYAMLMapping("group", "jedis") val element = createYAMLKeyValue("jedi", "yoda", parent, project) val adapter = Base64ValueAdapter(element) // when @@ -209,10 +211,11 @@ class Base64ValueAdapterTest { @Test fun `#set should add new JsonProperty to parent and delete current element`() { // given - val properties: MutableList = mutableListOf() + val properties = mutableListOf() val psiFileFactory = createJsonPsiFileFactory(properties) val project = createProjectWithServices(psiFileFactory = psiFileFactory) - val parent = createJsonProperty("group", "jedis", project = project) + val parentProperties = listOf() + val parent = createJsonObject("group", parentProperties, project = project) val property = createJsonProperty("jedi", "yoda", parent, project) properties.add(property) val adapter = Base64ValueAdapter(property) diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/PsiElementMocks.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/PsiElementMocks.kt index f6aa9d34e..a4053626c 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/PsiElementMocks.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/PsiElementMocks.kt @@ -16,130 +16,239 @@ import com.intellij.json.psi.JsonValue import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFile import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiNamedElement import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doAnswer import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo import org.jetbrains.yaml.YAMLElementGenerator import org.jetbrains.yaml.psi.YAMLDocument -import org.jetbrains.yaml.psi.YAMLFile import org.jetbrains.yaml.psi.YAMLKeyValue -import org.jetbrains.yaml.psi.YAMLPsiElement +import org.jetbrains.yaml.psi.YAMLMapping +import org.jetbrains.yaml.psi.YAMLSequence +import org.jetbrains.yaml.psi.YAMLSequenceItem import org.jetbrains.yaml.psi.YAMLValue -import kotlin.random.Random +import org.mockito.MockedStatic +import org.mockito.Mockito + +fun createProjectWithServices( + yamlGenerator: YAMLElementGenerator? = null, + psiFileFactory: PsiFileFactory? = null +): Project { + return mock { + on { getService(any>()) } doAnswer { invocation -> + when { + YAMLElementGenerator::class.java == invocation.getArgument>(0) -> + yamlGenerator + + PsiFileFactory::class.java == invocation.getArgument>(0) -> + psiFileFactory + + else -> null + } + } + } +} + +fun createYAMLMapping(children: List): YAMLMapping { + return mock { + on { keyValues } doReturn children + doAnswer { invocation -> + val requestedKey = invocation.getArgument(0) + children.find { label -> label.keyText == requestedKey } + }.whenever(mock).getKeyValueByKey(any()) + + on { mock.children } doReturn children.toTypedArray() + // invoked by PsiElementVisitor + on { acceptChildren(any()) } doAnswer { invocation -> + val visitor = invocation.getArgument(0) + children.forEach { it.accept(visitor) } + } + children.forEach{ doReturn(mock).whenever(it).parent } + } +} fun createYAMLKeyValue( - key: String = Random.nextInt().toString(), - value: String? = null, - parent: YAMLKeyValue? = null, - project: Project = mock() + key: String, + value: String? = null, + parent: YAMLMapping? = null, + project: Project = mock() ): YAMLKeyValue { - val valueElement: YAMLValue = mock { - on { getText() } doReturn value - } - return mock { - on { getName() } doReturn key - on { getKeyText() } doReturn key - on { getValue() } doReturn valueElement - on { getParent() } doReturn parent - on { getProject() } doReturn project - } + val valueElement = mock { + on { text } doReturn value + } + return createYAMLKeyValue(key, valueElement, parent, project) } -fun createJsonProperty( - name: String = Random.nextInt().toString(), - value: String? = null, - parent: JsonProperty? = null, - project: Project = mock() -): JsonProperty { - val valueElement: JsonValue = mock { - on { getText() } doReturn value - } - return mock { - on { getName() } doReturn name - on { getValue() } doReturn valueElement - on { getParent() } doReturn parent - on { getProject() } doReturn project - } +fun createYAMLKeyValue(key: String, value: YAMLValue, parent: YAMLMapping? = null, project: Project = mock()): YAMLKeyValue { + val keyElement = mock { + on { name } doReturn key + on { mock.project } doReturn project + } + return createYAMLKeyValue(keyElement, value, parent, project) } -fun createJsonObject( - name: String = Random.nextInt().toString(), - properties: List = emptyList(), - parent: PsiElement? = null, - project: Project = mock() -): JsonObject { - return mock { - on { getChildren() } doReturn properties.toTypedArray() - on { getPropertyList() } doReturn properties - on { getName() } doReturn name - on { getParent() } doReturn parent - on { getProject() } doReturn project - } +fun createYAMLKeyValue( + key: PsiNamedElement, + value: YAMLValue, + parent: YAMLMapping?, + project: Project = mock() +): YAMLKeyValue { + val keyName = key.name!! + val valueText = value.text + val keyValue = mock { + on { mock.key } doReturn key + on { mock.keyText } doReturn keyName + on { mock.value } doReturn value + on { mock.valueText } doReturn valueText + on { mock.parent } doReturn parent + on { mock.project } doReturn project + on { accept(any()) } doAnswer { invocation -> + val visitor = invocation.getArgument(0) + visitor.visitElement(mock) + } + } + whenever(value.parent) + .thenReturn(keyValue) + if (parent != null) { + whenever(parent.getKeyValueByKey(keyName)) + .thenReturn(keyValue) + } + return keyValue } -fun createProjectWithServices( - yamlGenerator: YAMLElementGenerator? = null, - psiFileFactory: PsiFileFactory? = null -): Project { - return mock { - on { getService(any>()) } doAnswer { invocation -> - when { - YAMLElementGenerator::class.java == invocation.getArgument>(0) -> - yamlGenerator +fun createYAMLMapping(key: String, value: String): YAMLMapping { + val keyValue = createYAMLKeyValue(key, value) + return createYAMLMapping(listOf(keyValue)) +} - PsiFileFactory::class.java == invocation.getArgument>(0) -> - psiFileFactory +fun createYAMLDocument(yamlValue: YAMLValue): YAMLDocument { + return mock { + on { topLevelValue } doReturn yamlValue + } +} - else -> null - } - } - } +fun createYAMLSequence(expressions: List): YAMLSequence { + return mock { + on { mock.items } doReturn expressions + } } -fun createYAMLFile(documents: List?): YAMLFile { - return mock { - on { getDocuments() } doReturn documents - } +fun createYAMLGenerator(): YAMLElementGenerator { + return mock { + on { createYamlKeyValue(any(), any()) } doReturn mock() + } } -fun createYAMLValue(children: Array): YAMLValue { - return mock { - on { getChildren() } doReturn children - } +fun createJsonObject( + name: String? = null, + properties: List = emptyList(), + parent: PsiElement? = null, + project: Project = mock() +): JsonObject { + return mock { + on { mock.children } doReturn properties.toTypedArray() + on { mock.propertyList } doReturn properties + on { mock.name } doReturn name + on { mock.parent } doReturn parent + on { mock.project } doReturn project + } } -fun createYAMLDocument(yamlValue: YAMLValue): YAMLDocument { - return mock { - on { getTopLevelValue() } doReturn yamlValue - } +fun createJsonObject(children: List): JsonObject { + return mock { + on { propertyList } doReturn children + doAnswer { invocation -> + val requestedKey = invocation.getArgument(0) + children.find { label -> label.name == requestedKey } + }.whenever(mock).findProperty(any()) + + on { mock.children } doReturn children.toTypedArray() + // invoked by PsiElementVisitor + on { acceptChildren(any()) } doAnswer { invocation -> + val visitor = invocation.getArgument(0) + children.forEach { it.accept(visitor) } + } + children.forEach{ doReturn(mock).whenever(it).parent } + } +} + +fun createJsonProperty( + name: String, + value: String, + parent: JsonObject? = null, + project: Project = mock() +): JsonProperty { + val valueElement = mock { + on { mock.text } doReturn value + } + return createJsonProperty(name, valueElement, parent, project) } +fun createJsonProperty( + name: String, + valueElement: JsonValue, + parent: JsonObject? = null, + project: Project = mock() +): JsonProperty { + val nameElement = mock { + on { mock.name } doReturn name + } + return createJsonProperty(nameElement, valueElement, parent, project) +} + +fun createJsonProperty( + nameElement: JsonValue, + valueElement: JsonValue, + parent: JsonObject?, + project: Project = mock() +): JsonProperty { + val name = nameElement.name!! + val property = mock { + on { mock.name } doReturn name + on { mock.nameElement } doReturn nameElement + on { mock.value } doReturn valueElement + on { mock.parent } doReturn parent + on { mock.project } doReturn project + } + whenever(valueElement.parent) + .thenReturn(property) + if (parent != null) { + whenever(parent.findProperty(name)) + .thenReturn(property) + } + return property +} fun createJsonPsiFile(properties: List): PsiFile { - val firstChild: JsonObject = mock { - on { getPropertyList() } doReturn properties - } - return mock { - on { getFirstChild() } doReturn firstChild - } + val firstChild: JsonObject = mock { + on { propertyList } doReturn properties + } + return mock { + on { getFirstChild() } doReturn firstChild + } } fun createJsonPsiFileFactory(properties: List): PsiFileFactory { - val file = createJsonPsiFile(properties) - return createPsiFileFactory(file) + val file = createJsonPsiFile(properties) + return createPsiFileFactory(file) } fun createPsiFileFactory(psiFile: PsiFile): PsiFileFactory { - return mock { - on { createFileFromText(any(), any(), any()) } doReturn psiFile - } + return mock { + on { createFileFromText(any(), any(), any()) } doReturn psiFile + } } -fun createYAMLGenerator(): YAMLElementGenerator { - return mock { - on { createYamlKeyValue(any(), any()) } doReturn mock() - } +fun runWithMockKubernetesTypeInfo(type: KubernetesTypeInfo, test: (staticMock: MockedStatic) -> Unit) { + Mockito.mockStatic(KubernetesTypeInfo::class.java).use { staticMock -> + whenever(KubernetesTypeInfo.create(any())) + .thenReturn(type) + test.invoke(staticMock) + } } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/ResourcePsiElementMocks.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/ResourcePsiElementMocks.kt new file mode 100644 index 000000000..e34c94def --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/mocks/ResourcePsiElementMocks.kt @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.editor.mocks + +import com.intellij.json.psi.JsonObject +import com.intellij.json.psi.JsonProperty +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector +import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.YAMLMapping +import org.jetbrains.yaml.psi.YAMLSequence +import org.jetbrains.yaml.psi.YAMLSequenceItem + +fun YAMLMapping.createLabels(labels: List): YAMLKeyValue { + val labelMappings = createYAMLMapping(labels) + return this.createLabels(labelMappings) +} + +private const val KEY_LABELS = "labels" +private const val KEY_MATCHLABELS = "matchLabels" +private const val KEY_MATCHEXPRESSIONS = "matchExpressions" +private const val KEY_MATCHEXPRESSIONS_KEY = "key" +private const val KEY_MATCHEXPRESSIONS_OPERATOR = "operator" +private const val KEY_MATCHEXPRESSIONS_VALUES = "values" +private const val KEY_METADATA = "metadata" +private const val KEY_SELECTOR = "selector" +private const val KEY_SPEC = "spec" +private const val KEY_TEMPLATE = "template" + +fun YAMLMapping.createLabels(labelsChildren: YAMLMapping): YAMLKeyValue { + val metadataChildren = mock() + createYAMLKeyValue(KEY_METADATA, metadataChildren, this) + return createYAMLKeyValue(KEY_LABELS, labelsChildren, metadataChildren) +} + +fun JsonObject.createLabels(labels: List): JsonProperty { + val labelMappings = createJsonObject(properties = labels) + return this.createLabels(labelMappings) +} + +fun JsonObject.createLabels(labelsChildren: JsonObject): JsonProperty { + val metadataChildren = mock() + createJsonProperty(KEY_METADATA, metadataChildren, this) + return createJsonProperty(KEY_LABELS, labelsChildren, metadataChildren) +} + +fun YAMLMapping.createTemplate(templateChildren: YAMLMapping): YAMLKeyValue { + var spec = getKeyValueByKey(KEY_SPEC) + if (spec == null) { + val specMapping = mock() + spec = createYAMLKeyValue(KEY_SPEC, specMapping, this) + } + return createYAMLKeyValue(KEY_TEMPLATE, templateChildren, spec.value as YAMLMapping) +} + +fun JsonObject.createTemplate(templateChildren: JsonObject): JsonProperty { + var spec = findProperty(KEY_SPEC) + if (spec == null) { + val specMapping = mock() + spec = createJsonProperty(KEY_SPEC, specMapping, this) + } + return createJsonProperty(KEY_TEMPLATE, templateChildren, spec.value as JsonObject) +} + +fun YAMLMapping.createMatchLabels(matchLabels: YAMLMapping): YAMLKeyValue { + var selectorChildren = getSelector() + if (selectorChildren == null) { + selectorChildren = mock() + this.createSelector(selectorChildren) + } + return createYAMLKeyValue(KEY_MATCHLABELS, matchLabels, selectorChildren) +} + +fun YAMLMapping.createMatchExpressions(matchExpressionsChildren: YAMLSequence): YAMLKeyValue { + var selectorChildren = getSelector() + if (selectorChildren == null) { + selectorChildren = mock() + this.createSelector(selectorChildren) + } + return createYAMLKeyValue(KEY_MATCHEXPRESSIONS, matchExpressionsChildren, selectorChildren) +} + +fun createYAMLSequenceItem(key: String, operator: String, values: List): YAMLSequenceItem { + val keyElement = mock { + on { valueText } doReturn key + } + val operatorElement = mock { + on { valueText } doReturn operator + } + val valueItems = values.map { value -> + mock { + on { text } doReturn value + } + } + val valuesSequence = mock { + on { items } doReturn valueItems + } + val valuesElement = mock { + on { value } doReturn valuesSequence + } + + val mapping = mock { + on { getKeyValueByKey(KEY_MATCHEXPRESSIONS_KEY) } doReturn keyElement + on { getKeyValueByKey(KEY_MATCHEXPRESSIONS_OPERATOR) } doReturn operatorElement + on { getKeyValueByKey(KEY_MATCHEXPRESSIONS_VALUES) } doReturn valuesElement + } + return mock { + on { mock.value } doReturn mapping + } +} + +fun YAMLMapping.createSelector(selectorChildren: YAMLMapping = mock()): YAMLKeyValue { + var spec = getKeyValueByKey(KEY_SPEC) + if (spec == null) { + val specChildren = mock() + spec = createYAMLKeyValue(KEY_SPEC, specChildren, this) + } + return createYAMLKeyValue(KEY_SELECTOR, selectorChildren, spec.value as YAMLMapping) +} + +fun JsonObject.createSelector(selectorChildren: JsonObject = mock()): JsonProperty { + var spec = findProperty(KEY_SPEC) + if (spec == null) { + val specChildren = mock() + spec = createJsonProperty(KEY_SPEC, specChildren, this) + } + return createJsonProperty(KEY_SELECTOR, selectorChildren, spec.value as JsonObject) +} + +fun YAMLMapping.createMetadata(): YAMLMapping { + val metadataChildren = mock() + createYAMLKeyValue(KEY_METADATA, metadataChildren, this) + return metadataChildren +} + +fun JsonObject.createMetadata(): JsonObject { + val metadataChildren = mock() + createJsonProperty(KEY_METADATA, metadataChildren, this) + return metadataChildren +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtilsTest.kt index b76048c54..82079f481 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtilsTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourceEditorUtilsTest.kt @@ -13,10 +13,6 @@ package com.redhat.devtools.intellij.kubernetes.editor.util import com.nhaarman.mockitokotlin2.mock import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo -import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonObject -import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonProperty -import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue -import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLValue import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesResourceInfo import com.redhat.devtools.intellij.kubernetes.model.mocks.Mocks.kubernetesTypeInfo import org.assertj.core.api.Assertions.assertThat @@ -84,61 +80,6 @@ class ResourceEditorUtilsTest { assertThat(isKubernetesResource).isFalse() } - @Test - fun `#getDataValue should return YAMLKeyValue named data`() { - // given - val data = createYAMLKeyValue("data") - val parent = createYAMLValue(arrayOf(data)) - // when - val found = getDataValue(parent) - // then - assertThat(found).isNotNull() - } - - @Test - fun `#getDataValue should return null if there is no child named data`() { - // given - val yoda = createYAMLKeyValue("yoda") - val parent = createYAMLValue(arrayOf(yoda)) - // when - val found = getDataValue(parent) - // then - assertThat(found).isNull() - } - - @Test - fun `#getDataValue should return JsonProperty named data`() { - // given - val data = createJsonProperty("data", value = "anakin") - val parent = createJsonObject(properties = listOf(data)) - // when - val found = getDataValue(parent) - // then - assertThat(found?.text).isEqualTo("anakin") - } - - @Test - fun `#getBinaryData should return YAMLKeyValue named binaryData`() { - // given - val data = createYAMLKeyValue("binaryData") - val parent = createYAMLValue(arrayOf(data)) - // when - val found = getBinaryData(parent) - // then - assertThat(found).isNotNull() - } - - @Test - fun `#getBinaryData should return null if there is no child named binaryData`() { - // given - val anakin = createYAMLKeyValue("anakin") - val parent = createYAMLValue(arrayOf(anakin)) - // when - val found = getBinaryData(parent) - // then - assertThat(found).isNull() - } - @Test fun `#decodeBase64 should return base64 decoded value for given string`() { // given diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtilsTest.kt new file mode 100644 index 000000000..9ece43712 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtilsTest.kt @@ -0,0 +1,453 @@ +/******************************************************************************* + * 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.editor.util + +import com.intellij.json.psi.JsonFile +import com.intellij.json.psi.JsonObject +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.xml.XmlFile +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.whenever +import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonObject +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createJsonProperty +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createLabels +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMetadata +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createSelector +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createTemplate +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLMapping +import com.redhat.devtools.intellij.kubernetes.editor.mocks.runWithMockKubernetesTypeInfo +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.yaml.psi.YAMLDocument +import org.jetbrains.yaml.psi.YAMLFile +import org.jetbrains.yaml.psi.YAMLMapping +import org.junit.Before +import org.junit.Test +import org.mockito.MockedStatic + +class ResourcePsiElementUtilsTest { + + private lateinit var yamlElement: YAMLMapping + private lateinit var jsonElement: JsonObject + + @Before + fun before() { + this.yamlElement = mock() + this.jsonElement = mock() + } + + @Test + fun `#getAllElements for YAMLFile returns topLevelValues`() { + // given + val yamlFile = mock() + val yamlDocument = mock() + whenever(yamlFile.documents) + .thenReturn(listOf(yamlDocument)) + val topLevelValue = mock() + whenever(yamlDocument.topLevelValue) + .thenReturn(topLevelValue) + // when + val result = yamlFile.getAllElements() + // then + assertThat(result).containsExactly(topLevelValue) + } + + @Test + fun `#getAllElements for JsonFile returns allTopLevelValues`() { + // given + val jsonFile = mock() + val topLevelValue = mock() + whenever(jsonFile.allTopLevelValues) + .thenReturn(listOf(topLevelValue)) + // when + val result = jsonFile.getAllElements() + // then + assertThat(result).containsExactly(topLevelValue) + } + + @Test + fun `#getAllElements for unknown file returns null`() { + // given + val xmlFile = mock() + // when + val result = xmlFile.getAllElements() + // then + assertThat(result).isEmpty() + } + + @Test + fun `#getResourceName for PsiElement YAML returns metadata`() { + // given + val metadata = yamlElement.createMetadata() + val name = createYAMLKeyValue("name", "yoda", metadata) + // when + val result = (yamlElement as PsiElement).getResourceName() + // then + assertThat(result).isEqualTo(name.value) + } + + @Test + fun `#getResourceName for PsiElement Json returns metadata`() { + // given + val metadata = jsonElement.createMetadata() + val name = createJsonProperty("name", "yoda", metadata) + // when + val result = (jsonElement as PsiElement).getResourceName() + // then + assertThat(result).isEqualTo(name.value) + } + + @Test + fun `#getMetadata for Json returns metadata`() { + // given + val metadata = jsonElement.createMetadata() + // when + val returned = jsonElement.getMetadata() + // then + assertThat(returned).isSameAs(metadata) + } + + @Test + fun `#getMetadata for Json without metadata returns null`() { + // given + whenever(jsonElement.findProperty("metadata")) + .thenReturn(null) + // when + val returned = jsonElement.getMetadata() + // then + assertThat(returned).isNull() + } + + @Test + fun `#getMetadata for YAML returns metadata`() { + // given + val metadataChildren = yamlElement.createMetadata() + // when + val returned = yamlElement.getMetadata() + // then + assertThat(returned).isSameAs(metadataChildren) + } + + @Test + fun `#getMetadata for YAML without metadata returns null`() { + // given + whenever(yamlElement.getKeyValueByKey("metadata")) + .thenReturn(null) + // when + val result = yamlElement.getMetadata() + // then + assertThat(result).isNull() + } + + @Test + fun `#hasLabels for Yaml returns true if object has labels`() { + // given + val label = createYAMLKeyValue("jedi", "leia") + yamlElement.createLabels(listOf(label)) + // when + val hasLabels = yamlElement.hasLabels() + // then + assertThat(hasLabels).isTrue() + } + + @Test + fun `#hasLabels for YAML returns false if object has no labels`() { + // given + // when + val hasLabels = yamlElement.hasLabels() + // then + assertThat(hasLabels).isFalse() + } + + @Test + fun `#hasLabels for Json returns true if object has labels`() { + // given + val label = createJsonProperty("jedi", "yoda") + jsonElement.createLabels(listOf(label)) + // when + val hasLabels = jsonElement.hasLabels() + // then + assertThat(hasLabels).isTrue() + } + + @Test + fun `#hasLabels for Json returns false if object has no labels`() { + // given + // when + val hasLabels = jsonElement.hasLabels() + // then + assertThat(hasLabels).isFalse() + } + + @Test + fun `#getLabels for Json returns labels object`() { + // given + val labelsProperty = jsonElement.createLabels(emptyList()) + // when + val returned = jsonElement.getLabels() + // then + assertThat(returned).isSameAs(labelsProperty.value) + } + + @Test + fun `#getLabels for Json returns null if labels dont exist`() { + // given no labels are created + // when + val returned = jsonElement.getLabels() + // then + assertThat(returned).isNull() + } + + @Test + fun `#getLabels for YAML returns labels`() { + // given + val metadata = yamlElement.createMetadata() + val labelsMapping = mock() + createYAMLKeyValue("labels", labelsMapping, metadata) + // when + val returned = yamlElement.getLabels() + // then + assertThat(returned).isSameAs(labelsMapping) + } + + @Test + fun `#getLabels returns null if labels dont exist`() { + // given no labels are created + // when + val returned = yamlElement.getLabels() + + // then + assertThat(returned).isNull() + } + + @Test + fun `#hasSelector returns true when selector exists for YAML `() { + // given + val selector = yamlElement.createSelector() + // when + val hasSelector = yamlElement.hasSelector() + // then + assertThat(hasSelector).isTrue() + } + + @Test + fun `#getSelector for YAML returns selector`() { + // given + val selectorKeyValue = yamlElement.createSelector() + // when + val selector = yamlElement.getSelector() + // then + assertThat(selector).isSameAs(selectorKeyValue.value) + } + + @Test + fun `#getSelector for Json returns selector`() { + // given + val selectorKeyValue = jsonElement.createSelector() + // when + val selector = jsonElement.getSelector() + // then + assertThat(selector).isSameAs(selectorKeyValue.value) + } + + @Test + fun `#getKey for YAML returns key of YamlKeyValue`() { + // given + val selectorKeyValue = yamlElement.createSelector() + // when + val selectorKey = (selectorKeyValue as PsiElement).getKey() // extension method defined for PsiElement + // then + assertThat(selectorKey).isSameAs(selectorKeyValue.key) + } + + @Test + fun `#getKey for Json returns nameElement of JsonProperty`() { + // given + val selectorKeyValue = jsonElement.createSelector() + // when + val selectorKey = (selectorKeyValue as PsiElement).getKey() // extension method defined for PsiElement + // then + assertThat(selectorKey).isSameAs(selectorKeyValue.nameElement) + } + + @Test + fun `#getTemplate for YAML returns template mapping`() { + // given + val specMapping = mock() + createYAMLKeyValue("spec", specMapping, yamlElement) + val templateMapping = mock() + createYAMLKeyValue("template", templateMapping, specMapping) + // when + val result = yamlElement.getTemplate() + // then + assertThat(result).isSameAs(templateMapping) + } + + @Test + fun `#getTemplateLabel for YAML returns template labels`() { + // given + val templateLabels = createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "skywalker") + )) + yamlElement.createTemplate( + createYAMLMapping(listOf( + createYAMLKeyValue( + "metadata", + createYAMLMapping(listOf( + createYAMLKeyValue( + "labels", + templateLabels + ) + )) + ) + )) + ) + // when + val found = yamlElement.getTemplateLabels() + // then + assertThat(found).isSameAs(templateLabels) + } + + @Test + fun `#getTemplateLabel for Json returns template labels`() { + // given + val templateLabels = createJsonObject(listOf( + createJsonProperty("jedi", "skywalker") + )) + jsonElement.createTemplate( + createJsonObject(listOf( + createJsonProperty( + "metadata", + createJsonObject(listOf( + createJsonProperty( + "labels", + templateLabels + ) + )) + ) + )) + ) + // when + val found = jsonElement.getTemplateLabels() + // then + assertThat(found).isSameAs(templateLabels) + } + + @Test + fun `#isKubernetesResource for PsiFile returns false`() { + // given + val psiFile = mock() + // when + val result = psiFile.isKubernetesResource() + // then + assertThat(result).isFalse() + } + + @Test + fun `#isKubernetesResource for YAMLFile does NOT create KubernetesTypeInfo`() { + // given + val yamlFile = mock() + runWithMockKubernetesTypeInfo(mock()) { staticMock: MockedStatic -> + // when + yamlFile.isKubernetesResource() + // then + staticMock.verify({ KubernetesTypeInfo.create(yamlFile) }, never()) + } + } + + @Test + fun `#isKubernetesResource for YAMLMapping creates KubernetesTypeInfo`() { + // given + val yamlMapping = mock() + runWithMockKubernetesTypeInfo(mock()) { staticMock: MockedStatic -> + // when + yamlMapping.isKubernetesResource() + // then + staticMock.verify { KubernetesTypeInfo.create(yamlMapping) } + } + } + + @Test + fun `#getKubernetesTypeInfo for PsiFile returns null`() { + // given + val psiFile = mock() + // when + val result = psiFile.getKubernetesTypeInfo() + // then + assertThat(result).isNull() + } + + @Test + fun `#getKubernetesTypeInfo for YAMLMapping creates KubernetesTypeInfo`() { + // given + val yamlMapping = mock() + runWithMockKubernetesTypeInfo(mock()) { staticMock: MockedStatic -> + // when + yamlMapping.getKubernetesTypeInfo() + // then + staticMock.verify { KubernetesTypeInfo.create(yamlMapping) } + } + } + + @Test + fun `#getBinaryData should return YAMLKeyValue named binaryData`() { + // given + createYAMLKeyValue("binaryData", mock(), yamlElement) + // when + val found = yamlElement.getBinaryData() + // then + assertThat(found).isNotNull() + } + + @Test + fun `#getBinaryData should return null if there is no child named binaryData`() { + // given + createYAMLKeyValue("anakin", mock(), yamlElement) + // when + val found = yamlElement.getBinaryData() + // then + assertThat(found).isNull() + } + + @Test + fun `#getDataValue should return YAMLKeyValue named data`() { + // given + val data = createYAMLKeyValue("data", "yoda", yamlElement) + // when + val found = yamlElement.getDataValue() + // then + assertThat(found).isEqualTo(data.value) + } + + @Test + fun `#getDataValue should return null if there is no child named data`() { + // given + createYAMLKeyValue("yoda", mock(), yamlElement) + // when + val found = yamlElement.getDataValue() + // then + assertThat(found).isNull() + } + + @Test + fun `#getDataValue should return JsonProperty named data`() { + // given + createJsonProperty("data", value = "anakin", jsonElement) + // when + val found = jsonElement.getDataValue() + // then + assertThat(found?.text).isEqualTo("anakin") + } + +} diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtilsTest.kt new file mode 100644 index 000000000..0ff222286 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/SelectorPsiElementUtilsTest.kt @@ -0,0 +1,410 @@ +/******************************************************************************* + * 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.editor.util + +import com.nhaarman.mockitokotlin2.mock +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchExpressions +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchLabels +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createSelector +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequenceItem +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLMapping +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequence +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.yaml.psi.YAMLMapping +import org.junit.Test + +class PsiElementExtensionsTest { + + private val yamlElement = mock() + + @Test + fun `#getMatchLabels should return matchLabels if labels exist`() { + // given + val matching = createYAMLKeyValue("princess", "leia") + val matchLabels = createYAMLMapping(listOf(matching)) + yamlElement.createMatchLabels(matchLabels) + + // when + val nonEmpty = yamlElement.getMatchLabels() + + // then + assertThat(nonEmpty).isSameAs(matchLabels) + } + + @Test + fun `#getMatchLabels should return matchLabels if labels exist as direct children to selector`() { + // given + val matching = createYAMLKeyValue("c-3p0", "droid") + val matchLabels = createYAMLMapping(listOf(matching)) + yamlElement.createSelector(matchLabels) + + // when + val nonEmpty = yamlElement.getMatchLabels() + + // then + assertThat(nonEmpty).isSameAs(matchLabels) + } + + @Test + fun `#getMatchLabels should return empty matchLabels if list of labels is empty`() { + // given + val matchLabels = createYAMLMapping(emptyList()) + yamlElement.createSelector(matchLabels) // no intermediary matchLabels key + + // when + val isEmpty = yamlElement.getMatchLabels() + + // then + assertThat(isEmpty!!.keyValues).isEmpty() + } + + @Test + fun `#hasMatchLabels should return true when labels exist`() { + // given + val matching = createYAMLKeyValue("jedi", "yoda") + val matchLabels = createYAMLMapping(listOf(matching)) + yamlElement.createMatchLabels(matchLabels) + + // when + val hasMatchLabels = yamlElement.hasMatchLabels() + + // then + assertThat(hasMatchLabels).isTrue() + } + + @Test + fun `#hasMatchLabels should return true when labels exist as direct children of selector`() { + // given + val matching = createYAMLKeyValue("jedi", "yoda") + val matchLabels = createYAMLMapping(listOf(matching)) + yamlElement.createSelector(matchLabels) // no intermediary matchLabels key + + // when + val hasMatchLabels = yamlElement.hasMatchLabels() + + // then + assertThat(hasMatchLabels).isTrue() + } + + @Test + fun `#hasMatchLabels should return false labels do not exist`() { + // given + val matchLabels = createYAMLMapping(emptyList()) + yamlElement.createMatchLabels(matchLabels) + + // when + val doesNotHaveMatchLabels = yamlElement.hasMatchLabels() + + // then + assertThat(doesNotHaveMatchLabels).isFalse() + } + + @Test + fun `#areMatchingMatchLabels returns true if all labels match`() { + // given + val labels = createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "yoda") + )) + + val matchLabels = createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "yoda") + )) + + yamlElement.createMatchLabels(matchLabels) + + // when + val areMatching = yamlElement.areMatchingMatchLabels(labels) + + // then + assertThat(areMatching).isTrue() + } + + @Test + fun `#areMatchingMatchLabels returns false if not all required by matchLabels exist in labels`() { + // given + val labels = createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "yoda") + )) + + val matchLabels = createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "yoda"), + createYAMLKeyValue("obiwan", "yoda") // missing from labels + )) + + yamlElement.createMatchLabels(matchLabels) + + // when + val isMatching = yamlElement.areMatchingMatchLabels(labels) + + // then + assertThat(isMatching).isFalse() + } + + @Test + fun `#areMatchingMatchLabels returns true if matchLabels are empty`() { + // given + val labels = createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "yoda") + )) + + val matchLabels = createYAMLMapping(emptyList()) + + yamlElement.createMatchLabels(matchLabels) + + // when + val isMatching = yamlElement.areMatchingMatchLabels(labels) + + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `#getMatchExpressions returns matchExpressions if exist`() { + // given + val empty = createYAMLSequence(emptyList()) + val matchExpressions = yamlElement.createMatchExpressions(empty) + + // when + val found = yamlElement.getMatchExpressions() + + // then + assertThat(found).isEqualTo(matchExpressions.value) + } + + @Test + fun `#hasMatchExpressions returns false if match expressions are empty`() { + // given + val empty = createYAMLSequence(emptyList()) + yamlElement.createMatchExpressions(empty) + + // when + val hasMatchExpressions = yamlElement.hasMatchExpressions() + + // then + assertThat(hasMatchExpressions).isFalse() + } + + @Test + fun `#hasMatchExpressions returns true if match expressions exist`() { + // given + val nonEmpty = createYAMLSequence(listOf(mock())) + yamlElement.createMatchExpressions(nonEmpty) + + // when + val hasMatchExpressions = yamlElement.hasMatchExpressions() + + // then + assertThat(hasMatchExpressions).isTrue() + } + + @Test + fun `isMatchingMatchExpression returns true if labels match In-expression in key and value of alternatives`() { + // given + val expression = createYAMLSequenceItem("yoda", "In", listOf("jedi", "goblin")) + + val matching = createYAMLKeyValue("yoda", "goblin") + val irrelevant = createYAMLKeyValue("obiwan", "jedi") + val labels = createYAMLMapping(listOf(matching, irrelevant)) + + // when + val isMatching = expression.isMatchingMatchExpression(labels) + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `isMatchingMatchExpression returns true if labels match In-expression in key and quoted value`() { + // given + val expression = createYAMLSequenceItem("yoda", "In", listOf("\"jedi\"")) + + val matching = createYAMLKeyValue("yoda", "jedi") + val labels = createYAMLMapping(listOf(matching)) + + // when + val isMatching = expression.isMatchingMatchExpression(labels) + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `isMatchingMatchExpression returns false if labels match In-expression in key but not in value`() { + // given + val expression = createYAMLSequenceItem("yoda", "In", listOf("jedi")) + + val differsInValue = createYAMLKeyValue("yoda", "goblin") + val labels = createYAMLMapping(listOf(differsInValue)) + + // when + val isMatching = expression.isMatchingMatchExpression(labels) + // then + assertThat(isMatching).isFalse() + } + + @Test + fun `isMatchingMatchExpression returns false if labels do not match In-expression in key`() { + // given + val expression = createYAMLSequenceItem("obiwan", "In", listOf("jedi")) + + val differsInKey = createYAMLKeyValue("yoda", "jedi") + val labels = createYAMLMapping(listOf(differsInKey)) + + // when + val isMatching = expression.isMatchingMatchExpression(labels) + // then + assertThat(isMatching).isFalse() + } + + @Test + fun `isMatchingMatchExpression returns false empty labels are checked against In-expression`() { + // given + val expression = createYAMLSequenceItem("obiwan", "In", listOf("jedi")) + val emptyLabels = createYAMLMapping(emptyList()) + + // when + val isMatching = expression.isMatchingMatchExpression(emptyLabels) + // then + assertThat(isMatching).isFalse() + } + + @Test + fun `#isMatchingMatchExpression returns true if labels match NotIn-expression in key but not in value`() { + // given + val expression = createYAMLSequenceItem("yoda", "NotIn", listOf("jedi")) + + val yoda = createYAMLKeyValue("yoda", "jedi") + val matchesExpression = createYAMLMapping(listOf(yoda)) + + // when + val isMatching = expression.isMatchingMatchExpression(matchesExpression) + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `#isMatchingMatchExpression returns true because empty labels are missing match NotIn-expression`() { + // given + val expression = createYAMLSequenceItem("yoda", "NotIn", listOf("jedi")) + val emptyLabels = createYAMLMapping(emptyList()) + + // when + val isMatching = expression.isMatchingMatchExpression(emptyLabels) + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `#isMatchingMatchExpression returns true if labels match Exists-expression by key with arbitrary value`() { + // given + val expression = createYAMLSequenceItem("yoda", "Exists", emptyList()) + val matchesInKey = createYAMLKeyValue("yoda", "jedi") + val matchesExpression = createYAMLMapping(listOf(matchesInKey)) + + // when + val isMatching = expression.isMatchingMatchExpression(matchesExpression) + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `#isMatchingMatchExpression returns false if labels do not match Exists-expression by key`() { + // given + val expression = createYAMLSequenceItem("yoda", "Exists", emptyList()) + val matchesInKey = createYAMLKeyValue("r2d2", "android") + val matchesExpression = createYAMLMapping(listOf(matchesInKey)) + + // when + val isMatching = expression.isMatchingMatchExpression(matchesExpression) + // then + assertThat(isMatching).isFalse() + } + + @Test + fun `isMatchingMatchExpression returns true if labels are missing key of DoesNotExist-expression`() { + // given + val expression = createYAMLSequenceItem("yoda", "DoesNotExist", emptyList()) + val isMissingKey = createYAMLKeyValue("r2d2", "android") + val matchesExpression = createYAMLMapping(listOf(isMissingKey)) + + // when + val isMatching = expression.isMatchingMatchExpression(matchesExpression) + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `isMatchingMatchExpression returns false have key that should DoesNotExist`() { + // given + val expression = createYAMLSequenceItem("yoda", "DoesNotExist", emptyList()) + val matchesInKey = createYAMLKeyValue("yoda", "jedi") // value should be "green" + val matchesExpression = createYAMLMapping(listOf(matchesInKey)) + + // when + val isMatching = expression.isMatchingMatchExpression(matchesExpression) + // then + assertThat(isMatching).isFalse() + } + + @Test + fun `#areMatchingMatchExpressions returns true if labels are matching all match expressions`() { + // given + yamlElement.createMatchExpressions( + createYAMLSequence(listOf( + createYAMLSequenceItem("yoda", "Exists", emptyList()), + createYAMLSequenceItem("yoda", "In", listOf("green")) + )) + ) + + val matchesAllExpressions = createYAMLMapping(listOf( + createYAMLKeyValue("yoda", "green") + )) + + // when + val isMatching = yamlElement.areMatchingMatchExpressions(matchesAllExpressions) + // then + assertThat(isMatching).isTrue() + } + + @Test + fun `#areMatchingMatchExpressions returns false if labels DO NOT match all match expressions`() { + // given + yamlElement.createMatchExpressions( + createYAMLSequence(listOf( + createYAMLSequenceItem("yoda", "Exists", emptyList()), + createYAMLSequenceItem("yoda", "In", listOf("green")) + )) + ) + val doesNotMatchInExpression = createYAMLMapping(listOf( + createYAMLKeyValue("yoda", "jedi") + )) + + // when + val isMatching = yamlElement.areMatchingMatchExpressions(doesNotMatchInExpression) + // then + assertThat(isMatching).isFalse() + } + + @Test + fun `#areMatchingMatchExpressions returns true if match expressions are empty`() { + // given + yamlElement.createMatchExpressions( + createYAMLSequence(emptyList()) // no match expressions, only key + ) + val matchingLabels = createYAMLMapping( + listOf(createYAMLKeyValue("obiwan", "jedi")) + ) + + // when + val isMatching = yamlElement.areMatchingMatchExpressions(matchingLabels) + // then + assertThat(isMatching).isTrue() + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt new file mode 100644 index 000000000..8134db39e --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt @@ -0,0 +1,216 @@ +/******************************************************************************* + * Copyright (c) 2020 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.usage + +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createLabels +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchExpressions +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchLabels +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createSelector +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createTemplate +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequenceItem +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLMapping +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequence +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.yaml.psi.YAMLMapping +import org.junit.Before +import org.junit.Test + +class LabelsFilterTest { + + companion object { + private const val LABEL_KEY = "droid" + private const val LABEL_VALUE = "c-3p0" + } + + private lateinit var filter: LabelsFilter + + private lateinit var hasMatchLabelsAndExpressions: YAMLMapping + private lateinit var matchingPod: YAMLMapping + + @Before + fun before() { + this.hasMatchLabelsAndExpressions = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasMatchLabelsAndExpressions.createSelector( + createYAMLMapping(listOf( + createYAMLKeyValue( + "matchLabels", + createYAMLMapping(listOf(createYAMLKeyValue(LABEL_KEY, LABEL_VALUE))) + ), + createYAMLKeyValue( + "matchExpressions", + createYAMLSequence(listOf( + createYAMLSequenceItem(LABEL_KEY, "In", listOf(LABEL_VALUE, "humanoid")) + )) + )) + ) + ) + + this.matchingPod = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Pod") + )) + matchingPod.createLabels( + createYAMLMapping(listOf( + createYAMLKeyValue(LABEL_KEY, LABEL_VALUE) + )) + ) + + this.filter = LabelsFilter(hasMatchLabelsAndExpressions) + } + + @Test + fun `#isAccepted returns true if pod labels match all selector labels and expressions`() { + // given + // when + val isAccepted = filter.isAccepted(matchingPod) + // then + assertThat(isAccepted).isTrue() + } + + @Test + fun `#isAccepted returns true if pod labels match all selector expressions`() { + // given + val hasMatchExpressions = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasMatchExpressions.createMatchExpressions( + createYAMLSequence(listOf( + createYAMLSequenceItem("jedi", "In", listOf("yoda", "obiwan", "luke")), // matches jedi/obiwan + createYAMLSequenceItem("droid", "Exists", emptyList()) // matches droid/r2d2 + )) + ) + + val filter = LabelsFilter(hasMatchExpressions) + val additionalLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Pod") + )) + additionalLabels.createLabels( + createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "obiwan"), + createYAMLKeyValue("droid", "r2d2") + )) + ) + // when + val isAccepted = filter.isAccepted(additionalLabels) + // then + assertThat(isAccepted).isTrue() + } + + @Test + fun `#isAccepted returns true if pod labels match all selector labels`() { + // given + val hasMatchLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasMatchLabels.createMatchLabels( + createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "obiwan"), + createYAMLKeyValue("droid", "r2d2") + )) + ) + + val filter = LabelsFilter(hasMatchLabels) + val additionalLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Pod") + )) + additionalLabels.createLabels( + createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "obiwan"), + createYAMLKeyValue("droid", "r2d2"), + createYAMLKeyValue("sith", "darth vader") + )) + ) + // when + val isAccepted = filter.isAccepted(additionalLabels) + // then + assertThat(isAccepted).isTrue() + } + + @Test + fun `#isAccepted returns false if selecting deployment has no selector`() { + // given + val hasNoSelector = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + + val filter = LabelsFilter(hasNoSelector) + // when + val isAccepted = filter.isAccepted(matchingPod) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns false if selecting element is no k8s resource`() { + // given + val isNoResource = createYAMLMapping(emptyList()) // missing kind, apiVersion + + val filter = LabelsFilter(isNoResource) + // when + val isAccepted = filter.isAccepted(matchingPod) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns false if filtered element is no k8s resource`() { + // given + val notAResource = createYAMLMapping( + emptyList() + ) + // when + val isAccepted = filter.isAccepted(notAResource) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns false if deployment is selector and filtered element is neither deployment nor pod`() { + // given + val notPodNorDeployment = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Service") // only pod or deployment supported + )) + // when + val isAccepted = filter.isAccepted(notPodNorDeployment) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns true if deployment is selector and filtered element is deployment with matching template labels`() { + // given + val matchingTemplateLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + matchingTemplateLabels.createTemplate( + createYAMLMapping(listOf( + createYAMLKeyValue( + "metadata", + createYAMLMapping(listOf( + createYAMLKeyValue( + "labels", + createYAMLMapping(listOf( + createYAMLKeyValue(LABEL_KEY, LABEL_VALUE) // matching labels in spec>template + )) + ) + )) + ) + )) + ) + // when + val isAccepted = filter.isAccepted(matchingTemplateLabels) + // then + assertThat(isAccepted).isTrue() + } + +} diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt new file mode 100644 index 000000000..f5a5148c1 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt @@ -0,0 +1,245 @@ +/******************************************************************************* + * Copyright (c) 2020 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.usage + +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createLabels +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchExpressions +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchLabels +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createSelector +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createTemplate +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequenceItem +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLMapping +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequence +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.yaml.psi.YAMLMapping +import org.junit.Before +import org.junit.Test + +class SelectorsFilterTest { + + companion object { + private const val LABEL_KEY = "princess" + private const val LABEL_VALUE = "leia" + } + + private lateinit var filter: SelectorsFilter + + private lateinit var pod: YAMLMapping + private lateinit var podMatchingSelector: YAMLMapping + + @Before + fun before() { + this.pod = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Pod") + )) + pod.createLabels( + createYAMLMapping(listOf(createYAMLKeyValue(LABEL_KEY, LABEL_VALUE))) + ) + + this.podMatchingSelector = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + this.podMatchingSelector.createSelector( + createYAMLMapping(listOf( + createYAMLKeyValue( + "matchLabels", + createYAMLMapping(listOf( + createYAMLKeyValue( + LABEL_KEY, + LABEL_VALUE + )) + ) + ), + createYAMLKeyValue( + "matchExpressions", + createYAMLSequence(listOf( + createYAMLSequenceItem(LABEL_KEY, "In", listOf(LABEL_VALUE, "humanoid")) + )) + )) + ) + ) + + this.filter = SelectorsFilter(pod) + } + + @Test + fun `#isAccepted returns true if filtered element labels matching all selector labels and expressions`() { + // given + // when + val isAccepted = filter.isAccepted(podMatchingSelector) + // then + assertThat(isAccepted).isTrue() + } + + @Test + fun `#isAccepted returns true if all selector expressions match pod labels`() { + // given + val additionalLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Pod") + )) + additionalLabels.createLabels( + createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "obiwan"), + createYAMLKeyValue("droid", "r2d2") + )) + ) + val filter = SelectorsFilter(additionalLabels) + val hasMatchExpressions = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasMatchExpressions.createMatchExpressions( + createYAMLSequence(listOf( + createYAMLSequenceItem("jedi", "In", listOf("yoda", "obiwan", "luke")), // matches jedi/obiwan + createYAMLSequenceItem("droid", "Exists", emptyList()) // matches droid/r2d2 + )) + ) + + // when + val isAccepted = filter.isAccepted(hasMatchExpressions) + // then + assertThat(isAccepted).isTrue() + } + + @Test + fun `#isAccepted returns true if selector matches all pod labels`() { + // given + val additionalLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Pod") + )) + additionalLabels.createLabels( + createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "obiwan"), + createYAMLKeyValue("droid", "r2d2"), + createYAMLKeyValue("sith", "darth vader") + )) + ) + val filter = SelectorsFilter(additionalLabels) + val hasMatchLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasMatchLabels.createMatchLabels( + createYAMLMapping(listOf( + createYAMLKeyValue("jedi", "obiwan"), + createYAMLKeyValue("droid", "r2d2") + )) + ) + // when + val isAccepted = filter.isAccepted(hasMatchLabels) + // then + assertThat(isAccepted).isTrue() + } + + @Test + fun `#isAccepted returns false if selecting pod has no labels`() { + // given + val hasNoLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasNoLabels.createLabels( + createYAMLMapping(emptyList()) + ) + val filter = SelectorsFilter(hasNoLabels) + val hasMatchLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasMatchLabels.createMatchLabels( + createYAMLMapping(emptyList()) // match all labels + ) + // when + val isAccepted = filter.isAccepted(hasMatchLabels) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns false if selecting element is no k8s resource`() { + // given + val isNoResource = createYAMLMapping(emptyList()) // missing kind, apiVersion + val filter = SelectorsFilter(isNoResource) + val hasMatchLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + hasMatchLabels.createMatchLabels( + createYAMLMapping(emptyList()) // match all labels + ) + // when + val isAccepted = filter.isAccepted(hasMatchLabels) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns false if filtered element is no k8s resource`() { + // given + val notAResource = createYAMLMapping( + emptyList() + ) + // when + val isAccepted = filter.isAccepted(notAResource) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns false if filtered deployment has no selector`() { + // given + val hasNoSelector = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + // no selector created + // when + val isAccepted = filter.isAccepted(hasNoSelector) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns false if filtering element is a pod but filtered element is no supported type`() { + // given + val unsupported = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Jedi") // should be Deployment, StatefulSet, CronJob etc. + )) + // create selector not to fail on selector check + unsupported.createSelector( + createYAMLMapping(emptyList()) // match all + ) + // when + val isAccepted = filter.isAccepted(unsupported) + // then + assertThat(isAccepted).isFalse() + } + + @Test + fun `#isAccepted returns true if filtering is deployment with template labels and filtered is deployment with matching labels`() { + // given + podMatchingSelector.createTemplate( + createYAMLMapping(listOf( + createYAMLKeyValue( + "metadata", + createYAMLMapping(listOf( + createYAMLKeyValue( + "labels", + createYAMLMapping(listOf( + createYAMLKeyValue(LABEL_KEY, LABEL_VALUE) // matching labels in spec>template + )) + ) + )) + ) + )) + ) + val filter = SelectorsFilter(podMatchingSelector) + // when + val isAccepted = filter.isAccepted(podMatchingSelector) + // then + assertThat(isAccepted).isTrue() + } +} From 1bcfbe4198491c741d571a4c22d94ccb05717113 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Mon, 14 Apr 2025 11:59:27 +0200 Subject: [PATCH 2/3] fix: dont freeze editor when closing editor with offline cluster Freezing was caused by race conditions in the locks. Closing the editor disposes the editor resources. Disposing the editor resource closes the cluster connection. When closing the watch, a new resource state is retrieved, this then ended in the resource lock waiting for the connection timeout. Solved it by introducing a new "Disposed" state which is returned whenever an editor resource is disposed. No further state retrieval occurrs. Signed-off-by: Andre Dietisheim --- .../kubernetes/editor/EditorResource.kt | 34 ++++++++------ .../kubernetes/editor/EditorResourceState.kt | 14 ++++++ .../kubernetes/editor/EditorResources.kt | 2 +- .../kubernetes/model/ResourceWatch.kt | 12 +++-- .../editor/EditorResourceStateTest.kt | 46 +++++++++++++++++++ .../kubernetes/editor/EditorResourceTest.kt | 23 ++++++++++ .../kubernetes/editor/EditorResourcesTest.kt | 5 +- .../kubernetes/model/ResourceWatchTest.kt | 6 ++- 8 files changed, 122 insertions(+), 20 deletions(-) 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 ff645eada..d3f09ff3d 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 @@ -24,6 +24,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.client.KubernetesClient import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil import io.fabric8.kubernetes.client.utils.Serialization +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -39,7 +40,7 @@ open class EditorResource( ) -> ClusterResource? = ClusterResource.Factory::create ) { - var disposed: Boolean = false + var disposed: AtomicBoolean = AtomicBoolean(false) /** for testing purposes */ private var state: EditorResourceState? = null /** for testing purposes */ @@ -94,6 +95,9 @@ open class EditorResource( /** for testing purposes */ open fun setState(state: EditorResourceState?) { + if (disposed.get()) { + return + } resourceChangeMutex.withLock { this.state = state } @@ -108,14 +112,18 @@ open class EditorResource( * @see [createState] */ fun getState(): EditorResourceState { - return resourceChangeMutex.withLock { - val existingState = this.state - if (existingState == null) { - val newState = createState(resource, null) - setState(newState) - newState - } else { - existingState + return if (disposed.get()) { + Disposed() + } else { + resourceChangeMutex.withLock { + val existingState = this.state + if (existingState == null) { + val newState = createState(resource, null) + setState(newState) + newState + } else { + existingState + } } } } @@ -262,7 +270,7 @@ open class EditorResource( /** for testing purposes */ protected open fun setLastPushedPulled(resource: HasMetadata?) { resourceChangeMutex.withLock { - lastPushedPulled = resource + this.lastPushedPulled = resource } } @@ -332,13 +340,13 @@ open class EditorResource( } fun dispose() { - if (disposed) { + if (disposed.get()) { return } - this.disposed = true - clusterResource?.close() + this.disposed.set(true) setLastPushedPulled(null) setResourceVersion(null) + clusterResource?.close() } private fun createClusterResource(resource: HasMetadata): ClusterResource? { diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceState.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceState.kt index 220147200..de24dae8a 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceState.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceState.kt @@ -63,6 +63,20 @@ class Error(val title: String, val message: String? = null): EditorResourceState } } +class Disposed: EditorResourceState() { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + return other is Disposed + } + + override fun hashCode(): Int { + return Objects.hash() + } +} + open class Identical: EditorResourceState() abstract class Different(val exists: Boolean, val isOutdatedVersion: Boolean): EditorResourceState() { diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResources.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResources.kt index 7400dbd68..8780fe3ca 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResources.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResources.kt @@ -59,7 +59,7 @@ open class EditorResources( // remove editor resource for old resource that doesn't exist anymore !new.contains(identifier) // or editor resource that was disposed (ex. change in namespace, context) - || editorResource.disposed + || editorResource.disposed.get() } resources.keys.removeAll(toRemove.keys) toRemove.values.forEach { editorResource -> diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt index 7d6aab47c..773b4ffd5 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt @@ -11,11 +11,13 @@ package com.redhat.devtools.intellij.kubernetes.model import com.intellij.openapi.diagnostic.logger +import com.intellij.util.concurrency.AppExecutorUtil import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.client.KubernetesClientException import io.fabric8.kubernetes.client.Watch import io.fabric8.kubernetes.client.Watcher import io.fabric8.kubernetes.client.WatcherException +import org.jetbrains.concurrency.runAsync import java.util.* import java.util.concurrent.BlockingDeque import java.util.concurrent.ConcurrentHashMap @@ -29,15 +31,15 @@ import java.util.concurrent.LinkedBlockingDeque */ open class ResourceWatch( protected val watchOperations: BlockingDeque> = LinkedBlockingDeque(), - watchOperationsRunner: Runnable = WatchOperationsRunner(watchOperations) + watchOperationsRunner: Runnable = WatchOperationsRunner(watchOperations), + private val executor: (runnable: Runnable) -> Unit = { runnable -> AppExecutorUtil.getAppExecutorService().execute(runnable) } ) { companion object { @JvmField val WATCH_OPERATION_ENQUEUED: Watch = Watch { } } protected open val watches: ConcurrentHashMap = ConcurrentHashMap() - private val executor: ExecutorService = Executors.newWorkStealingPool(20) - private val thread = executor.submit(watchOperationsRunner) + private val thread = executor.invoke(watchOperationsRunner) open fun watchAll( toWatch: Collection) -> Watch?>>, @@ -91,7 +93,9 @@ open class ResourceWatch( } fun close() { - closeAll(watches.entries.toList()) + executor.invoke { + closeAll(watches.entries.toList()) + } } private fun closeAll(entries: Collection>) { diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceStateTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceStateTest.kt index 8bddf37cb..de3371bc6 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceStateTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceStateTest.kt @@ -114,6 +114,8 @@ class EditorResourceStateTest { assertThat(Error("yoda", "is a green gnome")).isNotEqualTo(Error("yoda", "is a jedi")) assertThat(Error("obiwan", "is a jedi")).isNotEqualTo(Error("yoda", "is a jedi")) + assertThat(Error("obiwan", "is a jedi")).isNotEqualTo(Disposed()) + assertThat(Error("yoda", "is a jedi")).isNotEqualTo(Identical()) assertThat(Error("yoda", "is a jedi")).isNotEqualTo(Modified(true, true)) @@ -133,12 +135,41 @@ class EditorResourceStateTest { assertThat(Error("yoda", "is a jedi")).isNotEqualTo(Pulled()) } + @Test + fun `Disposed is not equal to all the other states`() { + // given + // when + assertThat(Disposed()).isNotEqualTo(Error("yoda", "is a jedi")) + + assertThat(Disposed()).isEqualTo(Disposed()) + + assertThat(Disposed()).isNotEqualTo(Identical()) + + assertThat(Disposed()).isNotEqualTo(Modified(true, true)) + assertThat(Disposed()).isNotEqualTo(Modified(true, false)) + assertThat(Disposed()).isNotEqualTo(Modified(false, false)) + + assertThat(Disposed()).isNotEqualTo(DeletedOnCluster()) + + assertThat(Disposed()).isNotEqualTo(Outdated()) + + assertThat(Disposed()).isNotEqualTo(Created(true)) + assertThat(Disposed()).isNotEqualTo(Created(false)) + + assertThat(Disposed()).isNotEqualTo(Updated(true)) + assertThat(Disposed()).isNotEqualTo(Updated(false)) + + assertThat(Disposed()).isNotEqualTo(Pulled()) + } + @Test fun `Identical is not equal to all the other states`() { // given // when assertThat(Identical()).isNotEqualTo(Error("yoda", "is a jedi")) + assertThat(Identical()).isNotEqualTo(Disposed()) + assertThat(Identical()).isEqualTo(Identical()) assertThat(Identical()).isNotEqualTo(Modified(true, true)) @@ -167,6 +198,10 @@ class EditorResourceStateTest { assertThat(Modified(false, false)).isNotEqualTo(Error("yoda", "is a jedi")) assertThat(Modified(false, true)).isNotEqualTo(Error("yoda", "is a jedi")) + assertThat(Modified(true, true)).isNotEqualTo(Disposed()) + assertThat(Modified(false, true)).isNotEqualTo(Disposed()) + assertThat(Modified(false, false)).isNotEqualTo(Disposed()) + assertThat(Modified(true, true)).isNotEqualTo(Identical()) assertThat(Modified(true, false)).isNotEqualTo(Identical()) assertThat(Modified(false, false)).isNotEqualTo(Identical()) @@ -217,6 +252,8 @@ class EditorResourceStateTest { // when assertThat(DeletedOnCluster()).isNotEqualTo(Error("darth vader", "is on the dark side")) + assertThat(DeletedOnCluster()).isNotEqualTo(Disposed()) + assertThat(DeletedOnCluster()).isNotEqualTo(Identical()) assertThat(DeletedOnCluster()).isNotEqualTo(Modified(true, true)) @@ -243,6 +280,8 @@ class EditorResourceStateTest { // when assertThat(Outdated()).isNotEqualTo(Error("obiwan", "is a jedi")) + assertThat(Outdated()).isNotEqualTo(Disposed()) + assertThat(Outdated()).isNotEqualTo(Identical()) assertThat(Outdated()).isNotEqualTo(Modified(true, true)) @@ -270,6 +309,8 @@ class EditorResourceStateTest { assertThat(Created(true)).isNotEqualTo(Error("darth vader", "is on the dark side")) assertThat(Created(false)).isNotEqualTo(Error("darth vader", "is on the dark side")) + assertThat(Created(false)).isNotEqualTo(Disposed()) + assertThat(Created(true)).isNotEqualTo(Identical()) assertThat(Created(false)).isNotEqualTo(Identical()) @@ -309,6 +350,9 @@ class EditorResourceStateTest { assertThat(Updated(true)).isNotEqualTo(Error("darth vader", "is on the dark side")) assertThat(Updated(false)).isNotEqualTo(Error("darth vader", "is on the dark side")) + assertThat(Updated(true)).isNotEqualTo(Disposed()) + assertThat(Updated(false)).isNotEqualTo(Disposed()) + assertThat(Updated(true)).isNotEqualTo(Identical()) assertThat(Updated(false)).isNotEqualTo(Identical()) @@ -347,6 +391,8 @@ class EditorResourceStateTest { // when assertThat(Pulled()).isNotEqualTo(Error("darth vader", "is on the dark side")) + assertThat(Pulled()).isNotEqualTo(Disposed()) + assertThat(Pulled()).isNotEqualTo(Identical()) assertThat(Pulled()).isNotEqualTo(Modified(true, true)) 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 8261ef6fa..53d2cc806 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 @@ -513,6 +513,29 @@ class EditorResourceTest { assertThat(state).isInstanceOf(Error::class.java) } + @Test + fun `#getState should return Disposed if resource is disposed`() { + // given + val editorResource = createEditorResource(POD2) + editorResource.disposed.set(true) + val state = editorResource.getState() + // then + assertThat(state).isInstanceOf(Disposed::class.java) + } + + @Test + fun `#getState should return Disposed even if different state was set after disposal`() { + // given + val editorResource = createEditorResource(POD2) + editorResource.disposed.set(true) + val shouldBeIgnored = mock() + editorResource.setState(shouldBeIgnored) + // when + val state = editorResource.getState() + // then + assertThat(state).isInstanceOf(Disposed::class.java) + } + @Test fun `#dispose should close clusterResource that was created`() { // given diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourcesTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourcesTest.kt index e9a16a94b..5150da2bf 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourcesTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourcesTest.kt @@ -33,6 +33,7 @@ import java.util.* import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Test +import java.util.concurrent.atomic.AtomicBoolean class EditorResourcesTest { @@ -230,8 +231,10 @@ class EditorResourcesTest { } private fun createEditorResourceMock(resource: HasMetadata): EditorResource { + val disposed = AtomicBoolean(false) val editorResource: EditorResource = mock { - on { getResource() } doReturn resource + on { mock.getResource() } doReturn resource + on { mock.disposed } doReturn disposed } // store editorResource mock in list editorResources.add(editorResource) diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatchTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatchTest.kt index 153866ccc..b597b20e9 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatchTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatchTest.kt @@ -222,7 +222,11 @@ class ResourceWatchTest { class TestableResourceWatch( watchOperations: BlockingDeque>, watchOperationsRunner: Runnable = mock() - ): ResourceWatch>(watchOperations, watchOperationsRunner) { + ) : ResourceWatch>( + watchOperations, + watchOperationsRunner, + { runnable: Runnable -> runnable.run() }) { + public override val watches = spy(super.watches) override fun watch( From 9355acfa9cfcdd2c1a8325a55ac378d413be46cb Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Mon, 14 Apr 2025 23:06:21 +0200 Subject: [PATCH 3/3] fix: search result for selector is template labels (#642) --- .../kubernetes/editor/ResourceEditor.kt | 2 +- .../inlay/ResourceEditorInlayHintsProvider.kt | 1 - .../inlay/selector/SelectorPresentations.kt | 2 - .../inlay/selector/ShowUsagesDispatcher.kt | 85 ------------------- .../kubernetes/model/ResourceWatch.kt | 7 +- .../intellij/kubernetes/usage/LabelsFilter.kt | 8 +- .../usage/PsiElementMappingsFilter.kt | 18 ++++ .../kubernetes/usage/SelectorUsageSearcher.kt | 22 ++--- .../kubernetes/usage/SelectorsFilter.kt | 7 +- .../kubernetes/usage/LabelsFilterTest.kt | 41 ++++++++- .../kubernetes/usage/SelectorsFilterTest.kt | 55 ++++++++++-- 11 files changed, 123 insertions(+), 125 deletions(-) delete mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/PsiElementMappingsFilter.kt 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 c94b08345..22702f36a 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 @@ -428,7 +428,7 @@ open class ResourceEditor( override fun dispose() { resourceModel.removeListener(onNamespaceContextChanged) - editorResources.dispose() connection.dispose() + editorResources.dispose() } } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt index 6b575f208..98a90819e 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/ResourceEditorInlayHintsProvider.kt @@ -102,7 +102,6 @@ internal class ResourceEditorInlayHintsProvider : InlayHintsProvider val fileType = editor.virtualFile?.fileType ?: return val project = editor.project ?: return - //PsiElements.getAll(fileType, project) val allElements = PsiElements.getAllNoExclusions(fileType, project) SelectorPresentations.createForSelector(element, allElements, sink, editor, factory) SelectorPresentations.createForAllLabels(element, allElements, sink, editor, factory) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt index e5bc38c94..ef2b6abe2 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/SelectorPresentations.kt @@ -157,8 +157,6 @@ object SelectorPresentations { val project = editor.project if (project != null) { ShowUsagesAction.startFindUsages(hintedKeyValue, RelativePoint(event), editor) - //ShowUsagesDispatcher.runWithCustomScope(project, hintedKeyValue) - //ShowUsagesDispatcher.findUsageManager(project, hintedKeyValue, editor) } } } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt deleted file mode 100644 index f8c1e0089..000000000 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/inlay/selector/ShowUsagesDispatcher.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.redhat.devtools.intellij.kubernetes.editor.inlay.selector - -import com.intellij.find.actions.ShowUsagesAction -import com.intellij.find.findUsages.FindUsagesManager -import com.intellij.find.findUsages.FindUsagesOptions -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.ActionPlaces -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataKey -import com.intellij.openapi.actionSystem.impl.SimpleDataContext -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.FileEditor -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.TextEditor -import com.intellij.openapi.module.Module -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiElement -import com.intellij.psi.search.GlobalSearchScope -import com.intellij.psi.search.SearchScope - -object ShowUsagesDispatcher { - - @JvmStatic - fun runWithCustomScope(project: Project, element: PsiElement) { - val showUsagesAction: AnAction = ActionManager.getInstance().getAction(ShowUsagesAction.ID) ?: return - - // Create custom DataContext - val dataContext = SimpleDataContext.builder() - .add(CommonDataKeys.PROJECT, project) - .add(CommonDataKeys.PSI_ELEMENT, element) - .add(CommonDataKeys.PSI_FILE, element.containingFile) - .add(DataKey.create(FindUsagesOptions::class.java.name), createFindUsagesOptions(project, element)) - .build() - - // Create AnActionEvent - val actionEvent = AnActionEvent.createFromDataContext(ActionPlaces.UNKNOWN, null, dataContext) - - // Perform the action - showUsagesAction.actionPerformed(actionEvent) - } - - private fun createFindUsagesOptions(project: Project, element: PsiElement): FindUsagesOptions { - val options = FindUsagesOptions(project) - //val customScope: SearchScope = GlobalSearchScope.everythingScope(project) - options.searchScope = NoExclusionsScope(project) - return options - } - - fun findUsageManager(project: Project, element: PsiElement, editor: Editor) { - val fileEditor = getFileEditor(editor) - val findUsagesManager = FindUsagesManager(project) - val customScope: SearchScope = NoExclusionsScope(project) - - findUsagesManager.findUsages(element, null, fileEditor, false, customScope) - } - - private fun getFileEditor(editor: Editor): FileEditor? { - val virtualFile: VirtualFile = editor.virtualFile ?: return null - val project = editor.project ?: return null - val fileEditors = FileEditorManager.getInstance(project).getEditors(virtualFile) - - return fileEditors.find { fileEditor -> fileEditor is TextEditor && fileEditor.editor == editor } - } - - class NoExclusionsScope(project: Project) : GlobalSearchScope(project) { - override fun contains(file: VirtualFile): Boolean { - return true; // Include EVERY file, even excluded ones - } - - override fun isSearchInModuleContent(module: Module): Boolean { - return true - } - - override fun isSearchInModuleContent(module: Module, testSources: Boolean): Boolean { - return true; - } - - override fun isSearchInLibraries(): Boolean { - return true; - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt index 773b4ffd5..1af7afe65 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/ResourceWatch.kt @@ -13,16 +13,12 @@ package com.redhat.devtools.intellij.kubernetes.model import com.intellij.openapi.diagnostic.logger import com.intellij.util.concurrency.AppExecutorUtil import io.fabric8.kubernetes.api.model.HasMetadata -import io.fabric8.kubernetes.client.KubernetesClientException import io.fabric8.kubernetes.client.Watch import io.fabric8.kubernetes.client.Watcher import io.fabric8.kubernetes.client.WatcherException -import org.jetbrains.concurrency.runAsync import java.util.* import java.util.concurrent.BlockingDeque import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingDeque /** @@ -32,7 +28,8 @@ import java.util.concurrent.LinkedBlockingDeque open class ResourceWatch( protected val watchOperations: BlockingDeque> = LinkedBlockingDeque(), watchOperationsRunner: Runnable = WatchOperationsRunner(watchOperations), - private val executor: (runnable: Runnable) -> Unit = { runnable -> AppExecutorUtil.getAppExecutorService().execute(runnable) } + private val executor: (runnable: Runnable) -> Unit = { runnable -> + AppExecutorUtil.getAppExecutorService().execute(runnable) } ) { companion object { @JvmField val WATCH_OPERATION_ENQUEUED: Watch = Watch { } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt index 032074b85..6b2b99796 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilter.kt @@ -11,7 +11,6 @@ package com.redhat.devtools.intellij.kubernetes.usage import com.intellij.psi.PsiElement -import com.intellij.psi.util.PsiElementFilter import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo import com.redhat.devtools.intellij.kubernetes.editor.util.areMatchingMatchExpressions import com.redhat.devtools.intellij.kubernetes.editor.util.areMatchingMatchLabels @@ -38,7 +37,7 @@ import com.redhat.devtools.intellij.kubernetes.editor.util.isStatefulSet /** * A filter that accepts labels, that are matching a given selector. */ -class LabelsFilter(selector: PsiElement): PsiElementFilter { +class LabelsFilter(selector: PsiElement): PsiElementMappingsFilter { private val selectorResource: PsiElement? by lazy { selector.getResource() @@ -129,6 +128,11 @@ class LabelsFilter(selector: PsiElement): PsiElementFilter { } } + override fun getMatchingElement(element: PsiElement): PsiElement? { + val labeledType = element.getKubernetesTypeInfo() ?: return null + return getLabels(labeledType, element, selectorResourceType) + } + private fun getLabels( labeledType: KubernetesTypeInfo, labeledResource: PsiElement, diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/PsiElementMappingsFilter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/PsiElementMappingsFilter.kt new file mode 100644 index 000000000..8f2f00b07 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/PsiElementMappingsFilter.kt @@ -0,0 +1,18 @@ +/******************************************************************************* + * 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.usage + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiElementFilter + +interface PsiElementMappingsFilter: PsiElementFilter { + fun getMatchingElement(element: PsiElement): PsiElement? +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt index 6fe16533d..60be6e4a5 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorUsageSearcher.kt @@ -20,9 +20,7 @@ import com.intellij.usages.Usage import com.intellij.usages.UsageInfo2UsageAdapter import com.intellij.util.Processor import com.redhat.devtools.intellij.kubernetes.editor.util.PsiElements -import com.redhat.devtools.intellij.kubernetes.editor.util.getLabels import com.redhat.devtools.intellij.kubernetes.editor.util.getResource -import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector import com.redhat.devtools.intellij.kubernetes.editor.util.isLabels import com.redhat.devtools.intellij.kubernetes.editor.util.isSelector @@ -53,9 +51,10 @@ class SelectorUsageSearcher : CustomUsageSearcher() { **/ //val searchScope = options.searchScope //if (searchScope.contains(file.virtualFile)) { - getAllMatching(searchParameter) + val filter = getFilter(searchParameter) ?: return@run + getAllMatching(searchParameter, filter) .forEach { matchingElement -> - val searchResult = getMatchingAttribute(searchParameter, matchingElement) + val searchResult = filter.getMatchingElement(matchingElement)?.parent if (searchResult != null) { processor.process( UsageInfo2UsageAdapter(UsageInfo(searchResult)) @@ -65,15 +64,14 @@ class SelectorUsageSearcher : CustomUsageSearcher() { } } - private fun getAllMatching(searchParameter: PsiElement): Collection { + private fun getAllMatching(searchParameter: PsiElement, filter: PsiElementFilter): Collection { val fileType = searchParameter.containingFile.fileType val project = searchParameter.project - val filter = getFilter(searchParameter) ?: return emptyList() return PsiElements.getAllNoExclusions(fileType, project) .filter(filter::isAccepted) } - private fun getFilter(searchParameter: PsiElement): PsiElementFilter? { + private fun getFilter(searchParameter: PsiElement): PsiElementMappingsFilter? { val resource = searchParameter.getResource() ?: return null return when { searchParameter.isSelector() -> @@ -87,14 +85,4 @@ class SelectorUsageSearcher : CustomUsageSearcher() { } } - private fun getMatchingAttribute(searchParameter: PsiElement, matchingElement: PsiElement): PsiElement? { - return when { - searchParameter.isSelector() -> - matchingElement.getLabels()?.parent // beginning of labels block/property - searchParameter.isLabels() -> - matchingElement.getSelector()?.parent // beginning of selector block/property - else -> - null - } - } } \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt index 04219f2a6..b21682a1e 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilter.kt @@ -14,6 +14,7 @@ import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiElementFilter import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo import com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesTypeInfo +import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector import com.redhat.devtools.intellij.kubernetes.editor.util.hasLabels import com.redhat.devtools.intellij.kubernetes.editor.util.hasSelector import com.redhat.devtools.intellij.kubernetes.editor.util.hasTemplateLabels @@ -21,7 +22,7 @@ import com.redhat.devtools.intellij.kubernetes.editor.util.hasTemplateLabels /** * A filter that accepts selectors that are matching a given label */ -class SelectorsFilter(private val labeledResource: PsiElement): PsiElementFilter { +class SelectorsFilter(private val labeledResource: PsiElement): PsiElementMappingsFilter { private val labeledResourceType: KubernetesTypeInfo? by lazy { labeledResource.getKubernetesTypeInfo() @@ -32,6 +33,10 @@ class SelectorsFilter(private val labeledResource: PsiElement): PsiElementFilter || labeledResource.hasTemplateLabels() } + override fun getMatchingElement(element: PsiElement): PsiElement? { + return element.getSelector() + } + override fun isAccepted(toAccept: PsiElement): Boolean { if (labeledResourceType == null || !hasLabels diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt index 8134db39e..d377511e3 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/LabelsFilterTest.kt @@ -35,6 +35,7 @@ class LabelsFilterTest { private lateinit var hasMatchLabelsAndExpressions: YAMLMapping private lateinit var matchingPod: YAMLMapping + private lateinit var matchingPodLabels: YAMLMapping @Before fun before() { @@ -59,11 +60,11 @@ class LabelsFilterTest { this.matchingPod = createYAMLMapping(listOf( createYAMLKeyValue("kind", "Pod") )) - matchingPod.createLabels( + this.matchingPodLabels = matchingPod.createLabels( createYAMLMapping(listOf( createYAMLKeyValue(LABEL_KEY, LABEL_VALUE) )) - ) + ).value as YAMLMapping this.filter = LabelsFilter(hasMatchLabelsAndExpressions) } @@ -213,4 +214,40 @@ class LabelsFilterTest { assertThat(isAccepted).isTrue() } + @Test + fun `#getMatchingElement returns pod labels that match given deployment selector`() { + // given + // when + val matchingElement = filter.getMatchingElement(matchingPod) + // then + assertThat(matchingElement).isEqualTo(matchingPodLabels) + } + + @Test + fun `#getMatchingElement returns deployment template labels that match given deployment selector`() { + // given + val matchingTemplateLabels = createYAMLMapping(listOf( + createYAMLKeyValue("kind", "Deployment") + )) + val templateLabels = createYAMLMapping(listOf( + createYAMLKeyValue(LABEL_KEY, LABEL_VALUE) // matching labels in spec>template + )) + matchingTemplateLabels.createTemplate( + createYAMLMapping(listOf( + createYAMLKeyValue( + "metadata", + createYAMLMapping(listOf( + createYAMLKeyValue( + "labels", + templateLabels + ) + )) + ) + )) + ) + // when + val matchingElement = filter.getMatchingElement(matchingTemplateLabels) + // then + assertThat(matchingElement).isEqualTo(templateLabels) + } } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt index f5a5148c1..aad815526 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/usage/SelectorsFilterTest.kt @@ -10,15 +10,16 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.usage -import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue import com.redhat.devtools.intellij.kubernetes.editor.mocks.createLabels import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchExpressions import com.redhat.devtools.intellij.kubernetes.editor.mocks.createMatchLabels import com.redhat.devtools.intellij.kubernetes.editor.mocks.createSelector import com.redhat.devtools.intellij.kubernetes.editor.mocks.createTemplate -import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequenceItem +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLKeyValue import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLMapping import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequence +import com.redhat.devtools.intellij.kubernetes.editor.mocks.createYAMLSequenceItem +import com.redhat.devtools.intellij.kubernetes.editor.util.getSelector import org.assertj.core.api.Assertions.assertThat import org.jetbrains.yaml.psi.YAMLMapping import org.junit.Before @@ -34,7 +35,7 @@ class SelectorsFilterTest { private lateinit var filter: SelectorsFilter private lateinit var pod: YAMLMapping - private lateinit var podMatchingSelector: YAMLMapping + private lateinit var deploymentMatchingPod: YAMLMapping @Before fun before() { @@ -45,10 +46,10 @@ class SelectorsFilterTest { createYAMLMapping(listOf(createYAMLKeyValue(LABEL_KEY, LABEL_VALUE))) ) - this.podMatchingSelector = createYAMLMapping(listOf( + this.deploymentMatchingPod = createYAMLMapping(listOf( createYAMLKeyValue("kind", "Deployment") )) - this.podMatchingSelector.createSelector( + this.deploymentMatchingPod.createSelector( createYAMLMapping(listOf( createYAMLKeyValue( "matchLabels", @@ -75,7 +76,7 @@ class SelectorsFilterTest { fun `#isAccepted returns true if filtered element labels matching all selector labels and expressions`() { // given // when - val isAccepted = filter.isAccepted(podMatchingSelector) + val isAccepted = filter.isAccepted(deploymentMatchingPod) // then assertThat(isAccepted).isTrue() } @@ -221,7 +222,7 @@ class SelectorsFilterTest { @Test fun `#isAccepted returns true if filtering is deployment with template labels and filtered is deployment with matching labels`() { // given - podMatchingSelector.createTemplate( + deploymentMatchingPod.createTemplate( createYAMLMapping(listOf( createYAMLKeyValue( "metadata", @@ -236,10 +237,46 @@ class SelectorsFilterTest { ) )) ) - val filter = SelectorsFilter(podMatchingSelector) + val filter = SelectorsFilter(pod) // when - val isAccepted = filter.isAccepted(podMatchingSelector) + val isAccepted = filter.isAccepted(deploymentMatchingPod) // then assertThat(isAccepted).isTrue() } + + @Test + fun `#getMatchingElement returns deployment selector that matches given pod labels`() { + // given + val filter = SelectorsFilter(deploymentMatchingPod) + // when + val matchingElement = filter.getMatchingElement(deploymentMatchingPod) + // then + assertThat(matchingElement).isEqualTo(deploymentMatchingPod.getSelector()) + } + + @Test + fun `#getMatchingElement returns deployment selector that matches given deployment template labels`() { + // given + deploymentMatchingPod.createTemplate( + createYAMLMapping(listOf( + createYAMLKeyValue( + "metadata", + createYAMLMapping(listOf( + createYAMLKeyValue( + "labels", + createYAMLMapping(listOf( + createYAMLKeyValue(LABEL_KEY, LABEL_VALUE) // matching labels in spec>template + )) + ) + )) + ) + )) + ) + val filter = SelectorsFilter(deploymentMatchingPod) + // when + val matchingElement = filter.getMatchingElement(deploymentMatchingPod) + // then + assertThat(matchingElement).isEqualTo(deploymentMatchingPod.getSelector()) + } + }