From af3f66ce2709bf77880733c17306feb08cd9dd82 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Tue, 9 Jul 2024 15:59:44 +0200 Subject: [PATCH] feat: impl'd describe for resources (#553) Signed-off-by: Andre Dietisheim --- .../actions/DescribeResourceAction.kt | 52 ++ .../intellij/kubernetes/console/ConsoleTab.kt | 2 +- .../KubernetesEditorsTabTitleProvider.kt | 28 + .../editor/ResourceEditorTabTitleProvider.kt | 81 +- .../kubernetes/editor/ResourceFile.kt | 4 + .../kubernetes/editor/describe/Description.kt | 18 + .../editor/describe/DescriptionUtils.kt | 139 +++ .../describe/DescriptionViewerFactory.kt | 144 +++ .../DescriptionViewerTabTitleProvider.kt | 48 + .../editor/describe/YAMLDescription.kt | 70 ++ .../describe/describer/ContainersDescriber.kt | 382 ++++++++ .../editor/describe/describer/Describer.kt | 17 + .../PersistentVolumeClaimDescriber.kt | 37 + .../editor/describe/describer/PodDescriber.kt | 226 +++++ .../describe/describer/VolumesDescriber.kt | 425 +++++++++ .../editor/describe/paragraphs/Chapter.kt | 123 +++ .../editor/describe/paragraphs/HasChildren.kt | 36 + .../describe/paragraphs/NamedSequence.kt | 38 + .../editor/describe/paragraphs/NamedValue.kt | 30 + .../editor/describe/paragraphs/Paragraph.kt | 27 + .../intellij/kubernetes/model/AllContexts.kt | 6 +- .../model/resource/kubernetes/Filters.kt | 19 + .../kubernetes/model/util/ContainerUtils.kt | 9 +- .../kubernetes/model/util/PodUtils.kt | 70 ++ .../kubernetes/model/util/ResourceUtils.kt | 15 + src/main/kotlin/icons/Icons.kt | 2 + src/main/resources/META-INF/plugin.xml | 6 +- src/main/resources/icons/lupe.svg | 101 +++ .../ResourceEditorTabTitleProviderTest.kt | 30 +- .../editor/describe/DescriberTestUtils.kt | 59 ++ .../editor/describe/DescriptionUtilsTest.kt | 152 ++++ .../editor/describe/YAMLDescriptionTest.kt | 173 ++++ .../describer/ContainerDescriberPortsTest.kt | 176 ++++ .../describer/ContainerDescriberProbesTest.kt | 190 ++++ .../ContainerDescriberResourcesTest.kt | 130 +++ .../describer/ContainerDescriberStatusTest.kt | 221 +++++ .../describer/ContainerDescriberTest.kt | 376 ++++++++ .../describe/describer/PodDescriberTest.kt | 844 ++++++++++++++++++ .../editor/describe/paragraphs/ChapterTest.kt | 237 +++++ .../describe/paragraphs/NamedSequenceTest.kt | 50 ++ .../kubernetes/model/mocks/ClientMocks.kt | 28 + .../kubernetes/model/mocks/PodContainer.kt | 33 + .../kubernetes/model/mocks/PodMocks.kt | 14 +- .../kubernetes/EventForResourceTest.kt | 134 +++ .../model/util/ContainerUtilsTest.kt | 89 +- .../kubernetes/model/util/PodUtilsTest.kt | 104 +++ .../model/util/ResourceUtilsTest.kt | 29 +- 47 files changed, 5135 insertions(+), 89 deletions(-) create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/DescribeResourceAction.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/KubernetesEditorsTabTitleProvider.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/Description.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtils.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerFactory.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerTabTitleProvider.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescription.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainersDescriber.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/Describer.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PersistentVolumeClaimDescriber.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriber.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/VolumesDescriber.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Chapter.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/HasChildren.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequence.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedValue.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Paragraph.kt create mode 100644 src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtils.kt create mode 100644 src/main/resources/icons/lupe.svg create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriberTestUtils.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtilsTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescriptionTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberPortsTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberProbesTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberResourcesTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberStatusTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriberTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/ChapterTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequenceTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodContainer.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/EventForResourceTest.kt create mode 100644 src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtilsTest.kt diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/DescribeResourceAction.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/DescribeResourceAction.kt new file mode 100644 index 000000000..1a3c0964d --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/actions/DescribeResourceAction.kt @@ -0,0 +1,52 @@ +/******************************************************************************* + * 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.actions + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.diagnostic.logger +import com.redhat.devtools.intellij.common.actions.StructureTreeAction +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionViewerFactory +import com.redhat.devtools.intellij.kubernetes.model.Notification +import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.api.model.Pod +import javax.swing.tree.TreePath + +class DescribeResourceAction: StructureTreeAction() { + + override fun actionPerformed(event: AnActionEvent?, path: TreePath?, selected: Any?) { + // not called + } + + override fun actionPerformed(event: AnActionEvent?, path: Array?, selected: Array?) { + val descriptor = selected?.get(0)?.getDescriptor() ?: return + val project = descriptor.project ?: return + val toDescribe: HasMetadata = descriptor.element as? HasMetadata? ?: return + try { + DescriptionViewerFactory.instance.openEditor(toDescribe, project) + } catch (e: RuntimeException) { + logger().warn("Error opening editor ${toDescribe.metadata.name}", e) + Notification().error( + "Error opening editor ${toDescribe.metadata.name}", + "Could not open editor for ${toDescribe.kind} '${toDescribe.metadata.name}'." + ) + } + } + + override fun isVisible(selected: Array?): Boolean { + return selected?.size == 1 + && isVisible(selected.firstOrNull()) + } + + override fun isVisible(selected: Any?): Boolean { + val element = selected?.getElement() + return element is Pod + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/console/ConsoleTab.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/console/ConsoleTab.kt index 6a0d83c35..0957227d2 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/console/ConsoleTab.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/console/ConsoleTab.kt @@ -65,7 +65,7 @@ abstract class ConsoleTab( var i = 0 do { val container = model.getElementAt(i).container - if (isRunning(getStatus(container, pod.status))) { + if (isRunning(container.getStatus(pod.status))) { return i } } while (++i < model.size) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/KubernetesEditorsTabTitleProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/KubernetesEditorsTabTitleProvider.kt new file mode 100644 index 000000000..1825f7b6d --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/KubernetesEditorsTabTitleProvider.kt @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2021 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.editor + +import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider +import com.intellij.openapi.fileEditor.impl.UniqueNameEditorTabTitleProvider +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionViewerTabTitleProvider + +open class KubernetesEditorsTabTitleProvider( + private val fallback: EditorTabTitleProvider = UniqueNameEditorTabTitleProvider() +) : EditorTabTitleProvider { + + override fun getEditorTabTitle(project: Project, file: VirtualFile): String? { + return ResourceEditorTabTitleProvider().getEditorTabTitle(project, file) + ?: DescriptionViewerTabTitleProvider().getEditorTabTitle(project, file) + ?: fallback.getEditorTabTitle(project, file) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProvider.kt index 058f353cb..ae6ff082b 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProvider.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProvider.kt @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Red Hat, Inc. + * Copyright (c) 2024 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, @@ -11,54 +11,45 @@ package com.redhat.devtools.intellij.kubernetes.editor import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider -import com.intellij.openapi.fileEditor.impl.UniqueNameEditorTabTitleProvider import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource -open class ResourceEditorTabTitleProvider( - private val fallback: EditorTabTitleProvider = UniqueNameEditorTabTitleProvider() -) : EditorTabTitleProvider { - - companion object { - const val TITLE_UNKNOWN_CLUSTERRESOURCE = "Unknown Cluster Resource" - const val TITLE_UNKNOWN_NAME = "unknown name" - } - - override fun getEditorTabTitle(project: Project, file: VirtualFile): String? { - return if (isTemporary(file)) { - val resourceInfo = getKubernetesResourceInfo(file, project) - if (resourceInfo != null - && isKubernetesResource(resourceInfo) - ) { - getTitleFor(resourceInfo) - } else { - TITLE_UNKNOWN_CLUSTERRESOURCE - } - } else { - fallback.getEditorTabTitle(project, file) - } - } - - private fun getTitleFor(info: KubernetesResourceInfo): String { - val name = info.name ?: TITLE_UNKNOWN_NAME - val namespace = info.namespace - return if (namespace == null) { - name - } else { - "$name@$namespace" - } - } - - /* for testing purposes */ - protected open fun getKubernetesResourceInfo(file: VirtualFile, project: Project): KubernetesResourceInfo? { - return com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesResourceInfo(file, project) - } - - /* for testing purposes */ - protected open fun isTemporary(file: VirtualFile): Boolean { - return ResourceFile.isTemporary(file) - } +open class ResourceEditorTabTitleProvider: EditorTabTitleProvider { + companion object { + const val TITLE_UNKNOWN_CLUSTERRESOURCE = "Unknown Cluster Resource" + const val TITLE_UNKNOWN_NAME = "unknown name" + } + + override fun getEditorTabTitle(project: Project, file: VirtualFile): String? { + if (!isResourceFile(file)) { + return null + } + + val resourceInfo = getKubernetesResourceInfo(file, project) + return if (resourceInfo != null + && isKubernetesResource(resourceInfo) + ) { + getTitleFor(resourceInfo) + } else { + TITLE_UNKNOWN_CLUSTERRESOURCE + } + } + + private fun getTitleFor(info: KubernetesResourceInfo): String { + val name = info.name ?: TITLE_UNKNOWN_NAME + val namespace = info.namespace ?: return name + return "$name@$namespace" + } + + protected open fun isResourceFile(file: VirtualFile): Boolean { + return ResourceFile.isResourceFile(file) + } + + /* for testing purposes */ + protected open fun getKubernetesResourceInfo(file: VirtualFile, project: Project): KubernetesResourceInfo? { + return com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesResourceInfo(file, project) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceFile.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceFile.kt index 0ac851d12..4e5e48773 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceFile.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceFile.kt @@ -103,6 +103,10 @@ open class ResourceFile protected constructor( && virtualFile.path.startsWith(TEMP_FOLDER.toString()) } + fun isResourceFile(virtualFile: VirtualFile?): Boolean { + return isTemporary(virtualFile) + } + private fun isYamlOrJson(file: VirtualFile): Boolean { if (true == file.extension?.isBlank()) { return false diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/Description.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/Description.kt new file mode 100644 index 000000000..4334bd682 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/Description.kt @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter + +abstract class Description: Chapter("Document") { + + abstract fun toText(): String +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtils.kt new file mode 100644 index 000000000..350add844 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtils.kt @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Paragraph +import java.time.DateTimeException +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +object DescriptionConstants { + + object Labels { + const val NAME = "Name" + const val NAMESPACE = "Namespace" + } + + object Values { + const val NONE = "" + const val UNSET = "" + } +} + +fun createValueOrSequence(title: String, items: List?): Paragraph? { + return if (items.isNullOrEmpty()) { + null + } else if (items.size == 1) { + NamedValue(title, items.first()) + } else { + NamedSequence(title, items) + } +} + +fun createValues(map: Map?): List { + if (map.isNullOrEmpty()) { + return emptyList() + } + return map.entries.map { entry -> NamedValue(entry.key, entry.value) } +} + +fun toString(items: List?): String? { + return if (items.isNullOrEmpty()) { + null + } else { + items.joinToString("\n") + } +} + +fun toRFC1123Date(dateTime: String?): String? { + if (dateTime == null) { + return null + } + return try { + val parsed = LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_ZONED_DATE_TIME) + val zoned = parsed.atOffset(ZonedDateTime.now().offset) + DateTimeFormatter.RFC_1123_DATE_TIME.format(zoned) + } catch (e: DateTimeException) { + "Unrecognized Date: $dateTime" + } +} + +/** + * Returns a human-readable form of the given date/time since the given date/time. + * Returns `null` if the given dateTime is not understood. + * The logic is copied from k8s.io/apimachinery/util/duration/ duration/HumanDuration. + * + * @see [k8s.io/apimachinery/util/duration/duration/HumanDuration](https://github.com/kubernetes/apimachinery/blob/d7e1c5311169d5ece2db0ae0118066859aa6f7d8/pkg/util/duration/duration.go#L48) + * @see + */ +fun toHumanReadableDurationSince(dateTime: String?, since: LocalDateTime): String? { + if (dateTime == null) { + return null + } + return try { + val parsed = LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_ZONED_DATE_TIME) + val difference = if (since.isBefore(parsed)) { + Duration.between(since, parsed) + } else { + Duration.between(parsed, since) + } + val seconds = difference.toSeconds() + return when { + seconds < 60 * 2 -> + // < 2 minutes + "${seconds}s" + + seconds < 60 * 10 -> + // < 10 minutes + "${difference.toMinutesPart()}m${difference.toSecondsPart()}s" + + seconds < 60 * 60 * 3 -> + // < 3 hours + "${difference.toMinutes()}m" + + seconds < 60 * 60 * 8 -> + // < 8 hours + "${difference.toHoursPart()}h" + + seconds < 60 * 60 * 48 -> + // < 48 hours + "${difference.toHours()}h${difference.toMinutesPart()}m" + + seconds < 60 * 60 * 24 * 8 -> { + // < 192 hours + if (difference.toHoursPart() == 0) { + "${difference.toDaysPart()}d" + } else { + "${difference.toDaysPart()}d${difference.toHoursPart()}h" + } + } + + seconds < 60 * 60 * 24 * 365 * 2 -> + // < 2 years + "${difference.toDaysPart()}d" + + seconds < 60 * 60 * 24 * 365 * 8 -> { + // < 8 years + val years = difference.toDaysPart() / 365 + "${years}y${difference.toDaysPart() % 365}d" + } + + else -> + "${difference.toDaysPart() / 365}y" + } + } catch (e: DateTimeException) { + null + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerFactory.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerFactory.kt new file mode 100644 index 000000000..ea3a0ad21 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerFactory.kt @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.LightVirtualFile +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber +import com.redhat.devtools.intellij.kubernetes.model.util.isSameResource +import com.redhat.devtools.intellij.kubernetes.telemetry.TelemetryService +import com.redhat.devtools.intellij.telemetry.core.service.TelemetryMessageBuilder +import io.fabric8.kubernetes.api.model.HasMetadata +import io.fabric8.kubernetes.api.model.Pod +import org.jetbrains.yaml.YAMLFileType + + +open class DescriptionViewerFactory protected constructor() { + + companion object { + const val PREFIX_FILE_NAME = "Description-" + + val instance = DescriptionViewerFactory() + + private val KEY_RESOURCE = Key(HasMetadata::class.java.name) + + fun isDescriptionFile(file: VirtualFile?): Boolean { + return file?.name != null + && file.name.startsWith(PREFIX_FILE_NAME) + } + + fun getResource(file: VirtualFile?): HasMetadata? { + if (file == null) { + return null + } + return file.getUserData(KEY_RESOURCE) + } + } + + + fun openEditor(resource: HasMetadata, project: Project) { + val description = describe(resource) ?: return + val editor = getOpenedEditor(resource, project) + if (editor != null) { + putUserData(resource, editor.file) + replaceDocument(editor, description) + } else { + val file = createYamlFile(description) + putUserData(resource, file) + openNewEditor(file, project) + } + } + + private fun putUserData(resource: HasMetadata, file: VirtualFile) { + file.putUserData(KEY_RESOURCE, resource) + } + + private fun replaceDocument(editor: FileEditor, description: String) { + WriteAction.compute { + val document = FileDocumentManager.getInstance().getDocument(editor.file) ?: return@compute + document.setReadOnly(false) + document.setText(description) + document.setReadOnly(true) + } + } + + private fun describe(resource: HasMetadata): String? { + return when(resource) { + is Pod -> describe(resource) + else -> null + } + } + + private fun describe(pod: Pod): String { + val description = YAMLDescription() + PodDescriber(pod).addTo(description) + return description.toText() + } + + private fun createYamlFile(content: String): VirtualFile { + val filename = "$PREFIX_FILE_NAME${System.currentTimeMillis()}.tmp" + val file = LightVirtualFile(filename, YAMLFileType.YML, content) + file.isWritable = false + return file + } + + private fun openNewEditor(file: VirtualFile, project: Project) { + FileEditorManager.getInstance(project).openFile(file, true) + } + + private fun getOpenedEditor(resource: HasMetadata, project: Project): FileEditor? { + val manager = FileEditorManager.getInstance(project) + val file = getOpenedEditorFile(manager, resource) ?: return null + return manager.openFile(file, true).firstOrNull() + } + + private fun getOpenedEditorFile(manager: FileEditorManager, resource: HasMetadata): VirtualFile? { + return manager.openFiles.find { file -> + try { + val fileResource = getResource(file) + if (fileResource == null) { + false + } else { + resource.isSameResource(fileResource) + } + + } catch (e: Exception) { + false + } + } + } + + /** for testing purposes */ + protected open fun runAsync(runnable: () -> Unit) { + ApplicationManager.getApplication().executeOnPooledThread(runnable) + } + + /** for testing purposes */ + protected open fun runInUI(runnable: () -> Unit) { + if (ApplicationManager.getApplication().isDispatchThread) { + runnable.invoke() + } else { + ApplicationManager.getApplication().invokeLater(runnable) + } + } + + /* for testing purposes */ + protected open fun getTelemetryMessageBuilder(): TelemetryMessageBuilder { + return TelemetryService.instance; + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerTabTitleProvider.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerTabTitleProvider.kt new file mode 100644 index 000000000..dbf0c58c5 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionViewerTabTitleProvider.kt @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import io.fabric8.kubernetes.api.model.HasMetadata + +class DescriptionViewerTabTitleProvider: EditorTabTitleProvider { + + companion object { + const val TITLE_UNKNOWN_CLUSTERRESOURCE = "Unknown Cluster Resource" + const val TITLE_UNKNOWN_NAME = "unknown name" + } + + override fun getEditorTabTitle(project: Project, file: VirtualFile): String? { + if (!isDescribeFile(file)) { + return null + } + + val resource = DescriptionViewerFactory.getResource(file) + return getTitleFor(resource) + } + + private fun getTitleFor(resource: HasMetadata?): String { + val resourceLabel = if (resource == null) { + TITLE_UNKNOWN_CLUSTERRESOURCE + } else { + val kind = resource.kind + val name = resource.metadata?.name ?: TITLE_UNKNOWN_NAME + "$kind $name" + } + return "Describe $resourceLabel" + } + + private fun isDescribeFile(file: VirtualFile): Boolean { + return DescriptionViewerFactory.isDescriptionFile(file) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescription.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescription.kt new file mode 100644 index 000000000..9ee23c9c3 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescription.kt @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Paragraph +import org.yaml.snakeyaml.DumperOptions +import org.yaml.snakeyaml.Yaml +import java.util.stream.Collectors + +class YAMLDescription : Description() { + + private val yaml = let { + val options = DumperOptions().apply { + defaultFlowStyle = DumperOptions.FlowStyle.BLOCK + } + Yaml(options) + } + + override fun toText(): String { + val map = toMap(children) + if (map.isEmpty()) { + return "" + } + return yaml.dump(map) + } + + /* + * Turning objects into map because I couldn't convince snakeyaml to format like I wanted by using native configurations. + */ + private fun toMap(paragraphs: List): Map { + return paragraphs.stream() + .filter { paragraph -> paragraph.title.isNotBlank() } + .collect( + Collectors.toMap( + Paragraph::title, + { paragraph -> + when { + paragraph is NamedValue -> + // dont NPE if there is no value + paragraph.value ?: NONE + + paragraph is NamedSequence -> + paragraph.children + + paragraph is Chapter -> + toMap(paragraph.children) + + else -> + paragraph ?: NONE + } + }, + { existing: Any, _: Any -> existing }, + // keep ordering + { LinkedHashMap() } + ) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainersDescriber.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainersDescriber.kt new file mode 100644 index 000000000..54dc954f4 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainersDescriber.kt @@ -0,0 +1,382 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Paragraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.toRFC1123Date +import com.redhat.devtools.intellij.kubernetes.model.util.getStatus +import io.fabric8.kubernetes.api.model.Container +import io.fabric8.kubernetes.api.model.ContainerPort +import io.fabric8.kubernetes.api.model.ContainerState +import io.fabric8.kubernetes.api.model.EnvFromSource +import io.fabric8.kubernetes.api.model.EnvVar +import io.fabric8.kubernetes.api.model.HTTPGetAction +import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.PodStatus +import io.fabric8.kubernetes.api.model.Probe +import io.fabric8.kubernetes.api.model.VolumeMount + +class ContainersDescriber(private val pod: Pod): Describer { + + companion object Labels { + const val ARGS = "Args" + const val COMMAND = "Command" + const val CONFIG_MAP = "ConfigMap" + const val CONTAINER_ID = "Container ID" + const val CONTAINERS = "Containers" + const val ENVIRONMENT = "Environment" + const val ENVIRONMENT_VARIABLES_FROM = "Environment Variables from" + const val EXIT_CODE = "Exit Code" + const val FINISHED = "Finished" + const val HOST_PORT = "Host Port" + const val HOST_PORTS = "Host Ports" + const val IMAGE = "Image" + const val IMAGE_ID = "Image ID" + const val INIT_CONTAINERS = "Init Containers" + const val LAST_STATE = "Last State" + const val LIMITS = "Limits" + const val LIVENESS = "Liveness" + const val MESSAGE = "Message" + const val MOUNTS = "Mounts" + const val PORT = "Port" + const val PORTS = "Ports" + const val READINESS = "Readiness" + const val READY = "Ready" + const val REASON = "Reason" + const val RESTART_COUNT = "Restart Count" + const val REQUESTS = "Requests" + const val RUNNING = "Running" + const val SECRET = "Secret" + const val SIGNAL = "Signal" + const val STARTED = "Started" + const val STARTUP = "Startup" + const val STATE = "State" + const val TERMINATED = "Terminated" + const val WAITING = "Waiting" + } + + override fun addTo(chapter: Chapter): Chapter { + return chapter + .addChapterIfExists(INIT_CONTAINERS, createContainers(pod.spec?.initContainers, pod.status)) + .addChapter(CONTAINERS, createContainers(pod.spec?.containers, pod.status)) + } + + private fun createContainers(containers: List?, status: PodStatus?): List { + if (containers.isNullOrEmpty()) { + return emptyList() + } + return containers.map { container -> createContainer(container, status) } + } + + private fun createContainer(container: Container, status: PodStatus?): Paragraph { + val containerStatus = container.getStatus(status) + val chapter = Chapter(container.name) + chapter + .addIfExists(CONTAINER_ID, containerStatus?.containerID) + .addIfExists(IMAGE, container.image) + .addIfExists(IMAGE_ID, containerStatus?.imageID) + addPorts(container.ports, chapter) + addHostPorts(container.ports, chapter) + .addIfExists(COMMAND, com.redhat.devtools.intellij.kubernetes.editor.describe.toString(container.command)) + .addIfExists(ARGS, com.redhat.devtools.intellij.kubernetes.editor.describe.toString(container.args)) + addStatus(STATE, containerStatus?.state, chapter) + addStatus(LAST_STATE, containerStatus?.lastState, chapter) + .addIfExists(READY,containerStatus?.ready) + .addIfExists(RESTART_COUNT, containerStatus?.restartCount) + addResourceBounds(LIMITS, container.resources?.limits, chapter) + addResourceBounds(REQUESTS, container.resources?.requests, chapter) + .addIfExists(LIVENESS, createProbeString(container.livenessProbe)) + .addIfExists(STARTUP, createProbeString(container.startupProbe)) + .addIfExists(READINESS, createProbeString(container.readinessProbe)) + addEnvFrom(container.envFrom, chapter) + addEnv(container.env, chapter) + addVolumeMounts(container.volumeMounts, chapter) + return chapter + } + + private fun addEnvFrom(envFromSources: List?, parent: Chapter): Chapter { + if (envFromSources.isNullOrEmpty()) { + return parent + } + return parent + .addIfExists( + NamedSequence(ENVIRONMENT_VARIABLES_FROM) + .addIfExists( + envFromSources.map { envFrom -> + createEnvFrom(envFrom) + } + ) + ) + } + + private fun createEnvFrom(envFrom: EnvFromSource?): String? { + val description = when { + envFrom?.configMapRef != null -> { + EnvFromDescription( + CONFIG_MAP, + envFrom.configMapRef.name, + envFrom.configMapRef.optional ?: false, + envFrom.prefix) + } + envFrom?.secretRef != null -> { + EnvFromDescription( + SECRET, + envFrom.secretRef.name, + envFrom.secretRef.optional ?: false, + envFrom.prefix + ) + } + else -> + null + } + return description?.toString() + } + + private fun addEnv(envVars: MutableList?, parent: Chapter): Chapter { + val title = ENVIRONMENT + val paragraph = if (envVars.isNullOrEmpty()) { + NamedValue(title, NONE) + } else { + Chapter(title).addIfExists(envVars.mapNotNull { envVar -> + createEnvVar(envVar) + }) + } + return parent.addIfExists(paragraph) + } + + private fun createEnvVar(envVar: EnvVar?): Paragraph? { + if (envVar == null) { + return null + } + return if (envVar.valueFrom == null) { + createValueEnvVar(envVar) + } else { + createValueFromEnvVar(envVar) + } + } + + private fun createValueEnvVar(envVar: EnvVar): Paragraph { + val value = when { + envVar.value.isNotBlank() + && envVar.value.contains("\n") -> + "|\n ${envVar.value}" + + envVar.value.isNotBlank() -> + envVar.value + + else -> + NONE + } + return NamedValue(envVar.name, value) + } + + private fun createValueFromEnvVar(envVar: EnvVar): Paragraph? { + val value = when { + envVar.valueFrom.fieldRef != null -> { + // TODO: implement k8s.io/kubectl/pkg/describe/describe.go/resolverFn() + // see https://github.com/redhat-developer/intellij-kubernetes/issues/774 + val fieldRef = envVar.valueFrom.fieldRef + "(${fieldRef.apiVersion}:${fieldRef.fieldPath})" + } + envVar.valueFrom.resourceFieldRef != null -> { + // TODO: implement k8s.io/kubectl/pkg/util/resource/resourcehelper.ExtractContainerResourceValue(e.ValueFrom.ResourceFieldRef, &container) + // see https://github.com/redhat-developer/intellij-kubernetes/issues/774 + val resourceFieldRef = envVar.valueFrom.resourceFieldRef + "(${resourceFieldRef.containerName}:${resourceFieldRef.resource})" + } + envVar.valueFrom.secretKeyRef != null -> { + val secretKeyRef = envVar.valueFrom.secretKeyRef + " Optional: ${secretKeyRef.optional ?: false}" + } + envVar.valueFrom.configMapKeyRef != null -> { + val configMapKeyRef = envVar.valueFrom.configMapKeyRef + " Optional: ${configMapKeyRef.optional ?: false}" + } + else -> + null + } + return NamedValue(envVar.name, value) + } + + private fun addVolumeMounts(volumeMounts: List, parent: Chapter) { + val title = MOUNTS + if (volumeMounts.isEmpty()) { + parent.addIfExists(title, NONE) + return + } + parent.addIfExists( + NamedSequence(title, volumeMounts.map { volumeMount -> + createVolumeMount(volumeMount) + }) + ) + } + + private fun createVolumeMount(volumeMount: VolumeMount): String { + val flags = if (volumeMount.readOnly == true) { + "ro" + } else { + "rw" + } + val subPath = if (volumeMount.subPath.isNullOrEmpty()) { + "" + } else { + ", path = \"${volumeMount.subPath}\"" + } + return String.format("%s from %s (%s)", volumeMount.mountPath, volumeMount.name, "$flags$subPath") + } + + private fun addPorts(ports: List, chapter: Chapter): Chapter { + val title = if (ports.size > 1) PORTS else PORT + val value = toString({ port -> port.containerPort }, ports) + return chapter.add(title, value) + } + + private fun addHostPorts(ports: List, chapter: Chapter): Chapter { + val title = if (ports.size > 1) HOST_PORTS else HOST_PORT + val value = toString({ port -> port.hostPort }, ports) + return chapter.add(title, value) + } + + private fun toString(portProvider: (ContainerPort) -> Int?, ports: List?): String? { + if (ports.isNullOrEmpty()) { + return null + } + return ports.joinToString(", ") { port -> + "${portProvider.invoke(port) ?: 0}/${port.protocol}" + } + } + + private fun addStatus(label: String, state: ContainerState?, chapter: Chapter): Chapter { + return when { + state == null -> + return chapter + state.running != null -> { + chapter.addIfExists( + Chapter(label) + .addIfExists(STATE, RUNNING) + .addIfExists(STARTED, toRFC1123Date(state.running.startedAt)) + ) + } + state.waiting != null -> { + chapter.addIfExists( + Chapter(label) + .addIfExists(STATE, WAITING) + .addIfExists(REASON, state.waiting.reason) + ) + } + state.terminated != null -> { + chapter.addIfExists( + Chapter(label) + .addIfExists(STATE, TERMINATED) + .addIfExists(REASON, state.terminated.reason) + .addIfExists(MESSAGE, state.terminated.message) + .addIfExists(EXIT_CODE, state.terminated.exitCode) + .addIfExists(SIGNAL, state.terminated.signal) + .addIfExists(STARTED, toRFC1123Date(state.terminated.startedAt)) + .addIfExists(FINISHED, toRFC1123Date(state.terminated.finishedAt)) + ) + } + else -> + chapter.addIfExists(label, WAITING) + } + } + + private fun addResourceBounds(label: String, bounds: Map?, parent: Chapter): Chapter { + if (bounds.isNullOrEmpty()) { + return parent + } + val limits = Chapter(label) + bounds.toSortedMap().forEach { limit -> + limits.addIfExists(limit.key, limit.value.toString()) + } + return parent.addIfExists(limits) + } + + private fun createProbeString(probe: Probe?): String? { + if (probe == null) { + return null + } + val attributes = String.format("delay=%ds timeout=%ds period=%ds #success=%d #failure=%d", + probe.initialDelaySeconds ?: 0, + probe.timeoutSeconds ?: 0, + probe.periodSeconds ?: 0, + probe.successThreshold ?: 0, + probe.failureThreshold ?: 0) + return when { + probe.exec != null -> { + val command = probe.exec?.command?.joinToString (" ") ?: NONE + String.format("exec [%s] %s", command, attributes) + } + probe.httpGet != null -> { + String.format("http-get %s %s", toUrl(probe.httpGet), attributes) + } + probe.tcpSocket != null -> { + val host = probe.tcpSocket.host ?: "" // null is formatted to 'null' + val port = probe.tcpSocket.port?.value?.toString() + if (port.isNullOrBlank()) { + String.format("tcp-socket %s %s", host, attributes) + } else { + String.format("tcp-socket %s:%s %s", host, port, attributes) + } + } + probe.grpc != null -> { + String.format("grpc :%d %s %s", probe.grpc.port ?: NONE, probe.grpc.service, attributes) + } + else -> + String.format("unknown %s", attributes) + } + } + + private fun toUrl(httpGet: HTTPGetAction): String { + val scheme = httpGet.scheme?.lowercase() ?: "" + val host = httpGet.host ?: "" + val port = httpGet.port?.value?.toString() ?: "" + val path = if (httpGet.path == null) { + "" + } else if (!httpGet.path.startsWith("/")) { + "/${httpGet.path}" + } else { + httpGet.path + } + return "$scheme://$host:$port$path" + } + + private data class EnvFromDescription( + private val type: String, + private val name: String, + private val optional: Boolean, + private val prefix: String? = null + ) { + + private fun hasPrefix(): Boolean { + return !prefix.isNullOrBlank() + } + + override fun toString(): String { + return if (hasPrefix()) { + String.format("%s %s with prefix \"%s\" Optional: %s", + name, + type, + prefix, + optional) + } else { + String.format("%s %s Optional: %s", + name, + type, + optional) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/Describer.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/Describer.kt new file mode 100644 index 000000000..d41ed5ed4 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/Describer.kt @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter + +fun interface Describer { + fun addTo(chapter: Chapter): Chapter +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PersistentVolumeClaimDescriber.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PersistentVolumeClaimDescriber.kt new file mode 100644 index 000000000..920e278aa --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PersistentVolumeClaimDescriber.kt @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import io.fabric8.kubernetes.api.model.ObjectMeta +import io.fabric8.kubernetes.api.model.PersistentVolumeClaim +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimSpec +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimTemplate + +class PersistentVolumeClaimDescriber private constructor(private val metadata: ObjectMeta?, private val spec: PersistentVolumeClaimSpec): + Describer { + constructor(pvc: PersistentVolumeClaim): this(pvc.metadata, pvc.spec) + constructor(pvcTemplate: PersistentVolumeClaimTemplate): this(pvcTemplate.metadata, pvcTemplate.spec) + + override fun addTo(chapter: Chapter): Chapter { + return addTo(chapter, true) + } + + fun addTo(chapter: Chapter, full: Boolean): Chapter { + if (full) { + chapter.addIfExists("Name", metadata?.name) + chapter.addIfExists("Namespace", metadata?.namespace) + } + return chapter + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriber.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriber.kt new file mode 100644 index 000000000..c8decbe8b --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriber.kt @@ -0,0 +1,226 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Labels.NAME +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Labels.NAMESPACE +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import com.redhat.devtools.intellij.kubernetes.editor.describe.createValues +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Paragraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.toHumanReadableDurationSince +import com.redhat.devtools.intellij.kubernetes.editor.describe.toRFC1123Date +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils.isSeccompProfileLocalhost +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils.isTerminating +import com.redhat.devtools.intellij.kubernetes.model.util.TOLERATION_OPERATOR_EXISTS +import com.redhat.devtools.intellij.kubernetes.model.util.toBooleanOrNull +import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.PodCondition +import io.fabric8.kubernetes.api.model.PodIP +import io.fabric8.kubernetes.api.model.PodReadinessGate +import io.fabric8.kubernetes.api.model.PodStatus +import io.fabric8.kubernetes.api.model.SeccompProfile +import io.fabric8.kubernetes.api.model.Toleration +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil +import java.time.LocalDateTime + +class PodDescriber(private val pod: Pod): Describer { + + companion object Labels { + const val NO_HOST_IP = "" + + const val ANNOTATIONS = "Annotations" + const val CONDITIONS = "Conditions" + const val CONTROLLED_BY = "Controlled By" + const val IP = "IP" + const val IPS = "IPs" + const val LABELS = "Labels" + const val LOCALHOST_PROFILE = "LocalhostProfile" + const val MESSAGE = "Message" + const val NODE = "Node" + const val NODE_SELECTORS = "Node-Selectors" + const val NOMINATED_NODE_NAME = "NominatedNodeName" + const val PRIORITY = "Priority" + const val PRIORITY_CLASS_NAME = "Priority Class Name" + const val QOS_CLASS = "QoS Class" + const val READINESS_GATES = "Readiness Gates" + const val REASON = "Reason" + const val RUNTIME_CLASS_NAME = "Runtime Class Name" + const val SECCOMP_PROFILE = "SeccompProfile" + const val SERVICE_ACCOUNT = "Service Account" + const val START_TIME = "Start Time" + const val STATUS = "Status" + const val TERMINATION_GRACE_PERIOD = "Termination Grace Period" + const val TOLERATIONS = "Tolerations" + } + + override fun addTo(chapter: Chapter): Chapter { + chapter + .addIfExists(NAME, pod.metadata?.name) + .addIfExists(NAMESPACE, pod.metadata?.namespace) + .addIfExists(PRIORITY, pod.spec?.priority) + .addIfExists(PRIORITY_CLASS_NAME, pod.spec?.priorityClassName) + .addIfExists(RUNTIME_CLASS_NAME, pod.spec?.runtimeClassName) + .addIfExists(SERVICE_ACCOUNT, pod.spec?.serviceAccountName) + .add(NODE, createNode(pod)) + .addIfExists(START_TIME, toRFC1123Date(pod.status?.startTime)) + .addChapter(LABELS, createValues(pod.metadata?.labels)) + .addChapter(ANNOTATIONS, createValues(pod.metadata?.annotations)) + addStatus(pod, chapter) + .addIfExists(REASON, pod.status?.reason) + .addIfExists(MESSAGE, pod.status?.message) + addPodSeccompProfile(pod.spec?.securityContext?.seccompProfile, chapter) + // deprecated, to be removed once not available anymore + chapter.add(IP, pod.status?.podIP) + addIPs(pod.status?.podIPs, chapter) + .addIfExists(CONTROLLED_BY, createControlledBy(pod)) + .addIfExists(NOMINATED_NODE_NAME, pod.status?.nominatedNodeName) + ContainersDescriber(pod).addTo(chapter) + addReadinessGates(pod, chapter) + addConditions(pod.status, chapter) + if (pod.spec != null) { + VolumesDescriber(pod.spec.volumes).addTo(chapter) + } + chapter.add(QOS_CLASS, pod.status?.qosClass) + .addChapter(NODE_SELECTORS, createValues(pod.spec?.nodeSelector)) + .addSequence(TOLERATIONS, createTolerations(pod.spec?.tolerations)) + return chapter + } + + private fun addStatus(pod: Pod, chapter: Chapter): Chapter { + if (pod.isTerminating()) { + chapter + .addIfExists( + STATUS, "Terminating: (lasts ${ + toHumanReadableDurationSince(pod.metadata?.deletionTimestamp, LocalDateTime.now()) + }") + .addIfExists(TERMINATION_GRACE_PERIOD, "${pod.metadata.deletionGracePeriodSeconds}s") + } else { + chapter.addIfExists(STATUS, pod.status?.phase) + } + return chapter + } + + private fun addPodSeccompProfile(seccompProfile: SeccompProfile?, chapter: Chapter): Chapter { + chapter.addIfExists(SECCOMP_PROFILE, seccompProfile?.type) + if (seccompProfile?.isSeccompProfileLocalhost() == true) { + chapter.addIfExists(LOCALHOST_PROFILE, seccompProfile.localhostProfile) + } + return chapter + } + + private fun createNode(pod: Pod): String? { + return if (pod.spec?.nodeName.isNullOrBlank()) { + null + } else { + "${pod.spec.nodeName}/${ pod.status?.hostIP ?: NO_HOST_IP}" + } + } + + private fun addIPs(ips: List?, parent: Chapter): Chapter { + if (ips.isNullOrEmpty()) { + parent.addIfExists(IPS, NONE) + } else { + parent.addChapter(IPS, ips.mapNotNull { podIp -> + if (podIp.ip.isNullOrBlank()) { + null + } else { + NamedValue(IP, podIp.ip) + } + }) + } + return parent + } + + private fun createControlledBy(pod: Pod): String? { + val controller = KubernetesResourceUtil.getControllerUid(pod) + return if (controller != null) { + "${controller.kind}/${controller.name}" + } else { + null + } + } + + private fun addReadinessGates(pod: Pod, parent: Chapter): Chapter { + val readinessGates = pod.spec?.readinessGates + if (readinessGates.isNullOrEmpty()) { + return parent + } + val readinessChapter = Chapter(READINESS_GATES) + .addIfExists(readinessGates.mapNotNull { readinessGate -> + createReadinessGate(readinessGate, pod.status?.conditions) + } + ) + parent.addIfExists(readinessChapter) + return parent + } + + private fun createReadinessGate(readinessGate: PodReadinessGate, conditions: List?): Paragraph { + val condition = getCondition(readinessGate.conditionType, conditions) + return NamedValue( + readinessGate.conditionType, + condition?.status?.toBooleanOrNull() ?: NONE + ) + } + + private fun getCondition(type: String?, conditions: List?): PodCondition? { + if (type.isNullOrEmpty() + || conditions.isNullOrEmpty()) { + return null + } + return conditions.find { condition -> type == condition.type } + } + + private fun addConditions(status: PodStatus?, parent: Chapter): Chapter { + val conditions = status?.conditions + if (conditions.isNullOrEmpty()) { + return parent + } + val conditionsChapter = Chapter(CONDITIONS) + .addIfExists(conditions.mapNotNull { condition -> + NamedValue( + condition.type, + condition.status?.toBooleanOrNull() ?: NONE + ) + }) + return parent.addIfExists(conditionsChapter) + } + + private fun createTolerations(tolerations: List?): List? { + return tolerations?.map { toleration -> + toString(toleration) + } + } + + private fun toString(toleration: Toleration): String { + val builder = StringBuilder(toleration.key) + if (!toleration.value.isNullOrEmpty()) { + builder.append("=${toleration.value}") + } + if (!toleration.effect.isNullOrBlank()) { + builder.append(":${toleration.effect}") + } + if (toleration.operator == TOLERATION_OPERATOR_EXISTS + && toleration.value.isNullOrBlank()) { + if (!toleration.key.isNullOrEmpty() + || !toleration.effect.isNullOrEmpty()) { + builder.append(" ") + } + builder.append("op=Exists") + } + if (toleration.tolerationSeconds != null) { + builder.append(" for ${toleration.tolerationSeconds}s") + } + return builder.toString() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/VolumesDescriber.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/VolumesDescriber.kt new file mode 100644 index 000000000..906430464 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/VolumesDescriber.kt @@ -0,0 +1,425 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.UNSET +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Paragraph +import io.fabric8.kubernetes.api.model.AWSElasticBlockStoreVolumeSource +import io.fabric8.kubernetes.api.model.AzureDiskVolumeSource +import io.fabric8.kubernetes.api.model.AzureFileVolumeSource +import io.fabric8.kubernetes.api.model.CSIVolumeSource +import io.fabric8.kubernetes.api.model.CephFSVolumeSource +import io.fabric8.kubernetes.api.model.CinderVolumeSource +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSource +import io.fabric8.kubernetes.api.model.DownwardAPIVolumeSource +import io.fabric8.kubernetes.api.model.EmptyDirVolumeSource +import io.fabric8.kubernetes.api.model.EphemeralVolumeSource +import io.fabric8.kubernetes.api.model.FCVolumeSource +import io.fabric8.kubernetes.api.model.FlexVolumeSource +import io.fabric8.kubernetes.api.model.FlockerVolumeSource +import io.fabric8.kubernetes.api.model.GCEPersistentDiskVolumeSource +import io.fabric8.kubernetes.api.model.GitRepoVolumeSource +import io.fabric8.kubernetes.api.model.GlusterfsVolumeSource +import io.fabric8.kubernetes.api.model.ISCSIVolumeSource +import io.fabric8.kubernetes.api.model.NFSVolumeSource +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSource +import io.fabric8.kubernetes.api.model.PhotonPersistentDiskVolumeSource +import io.fabric8.kubernetes.api.model.PortworxVolumeSource +import io.fabric8.kubernetes.api.model.ProjectedVolumeSource +import io.fabric8.kubernetes.api.model.QuobyteVolumeSource +import io.fabric8.kubernetes.api.model.RBDVolumeSource +import io.fabric8.kubernetes.api.model.ScaleIOVolumeSource +import io.fabric8.kubernetes.api.model.SecretVolumeSource +import io.fabric8.kubernetes.api.model.StorageOSVolumeSource +import io.fabric8.kubernetes.api.model.Volume +import io.fabric8.kubernetes.api.model.VsphereVirtualDiskVolumeSource +import java.math.BigDecimal + +class VolumesDescriber(private val volumes: List): Describer { + + companion object { + const val TITLE_TYPE = "Type" + } + + override fun addTo(chapter: Chapter): Chapter { + chapter.addChapter("Volumes", createVolumes(volumes)) + return chapter + } + + private fun createVolumes(volumes: List?): List { + if (volumes.isNullOrEmpty()) { + return emptyList() + } + return volumes.mapNotNull { volume -> createVolume(volume) } + } + + private fun createVolume(volume: Volume): Paragraph? { + if (volume.name == null) { + return null + } + val paragraph = Chapter(volume.name) + when { + volume.hostPath != null -> + addHostPath(volume, paragraph) + + volume.emptyDir != null -> + addEmptyDir(volume.emptyDir, paragraph) + + volume.gcePersistentDisk != null -> + addGcePersistentDisk(volume.gcePersistentDisk, paragraph) + + volume.awsElasticBlockStore != null -> + addAwsElasticBlockStore(volume.awsElasticBlockStore, paragraph) + + volume.gitRepo != null -> + addGitRepo(volume.gitRepo, paragraph) + + volume.secret != null -> + addSecret(volume.secret, paragraph) + + volume.configMap != null -> + addConfigMap(volume.configMap, paragraph) + + volume.nfs != null -> + addNfs(volume.nfs, paragraph) + + volume.iscsi != null -> + addIscsi(volume.iscsi, paragraph) + + volume.glusterfs != null -> + addGlusterfs(volume.glusterfs, paragraph) + + volume.persistentVolumeClaim != null -> + addPersistentVolumeClaim(volume.persistentVolumeClaim, paragraph) + + volume.ephemeral != null -> + addEphemeral(volume.ephemeral, paragraph) + + volume.rbd != null -> + addRbd(volume.rbd, paragraph) + + volume.quobyte != null -> + addQuobyte(volume.quobyte, paragraph) + + volume.downwardAPI != null -> + addDownwardAPI(volume.downwardAPI, paragraph) + + volume.azureDisk != null -> + addAzureDisk(volume.azureDisk, paragraph) + + volume.vsphereVolume != null -> + addVsphereVolume(volume.vsphereVolume, paragraph) + + volume.cinder != null -> + addCinder(volume.cinder, paragraph) + + volume.photonPersistentDisk != null -> + addPhotoPersistentDisk(volume.photonPersistentDisk, paragraph) + + volume.portworxVolume != null -> + addPortworxVolume(volume.portworxVolume, paragraph) + + volume.scaleIO != null -> + addScaleIO(volume.scaleIO, paragraph) + + volume.cephfs != null -> + addCephfs(volume.cephfs, paragraph) + + volume.storageos != null -> + addStorageos(volume.storageos, paragraph) + + volume.fc != null -> + addFc(volume.fc, paragraph) + + volume.azureFile != null -> + addAzureFile(volume.azureFile, paragraph) + + volume.flexVolume != null -> + addFlexVolume(volume.flexVolume, paragraph) + + volume.flocker != null -> + addFlocker(volume.flocker, paragraph) + + volume.projected != null -> + addProjected(volume.projected, paragraph) + + volume.csi != null -> + addCsi(volume.csi, paragraph) + + else -> + paragraph.addIfExists(TITLE_TYPE, "unknown") + } + return paragraph + } + + private fun addCsi(csi: CSIVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "CSI (a Container Storage Interface (CSI) volume source)") + .add("Driver", csi.driver) + .add("FSType", csi.fsType) + .add("ReadOnly", csi.readOnly) + } + + private fun addHostPath(volume: Volume, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "HostPath (bare host directory volume)") + .add("Path", volume.hostPath.path) + .add("HostPathType", volume.hostPath.type) + } + + private fun addEmptyDir(emptyDir: EmptyDirVolumeSource, parent: Chapter) { + val sizeLimit = if (emptyDir.sizeLimit?.numericalAmount != null + && emptyDir.sizeLimit.numericalAmount > BigDecimal(0) + ) { + emptyDir.sizeLimit + } else { + UNSET + } + parent.addIfExists(TITLE_TYPE, "EmptyDir (a temporary directory that shares a pod's lifetime)") + .add("Medium", emptyDir.medium) + .add("SizeLimit", sizeLimit.toString()) + } + + private fun addGcePersistentDisk(gce: GCEPersistentDiskVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "GCEPersistentDisk (a Persistent Disk resource in Google Compute Engine)") + .add("PDName", gce.pdName) + .add("FSType", gce.fsType) + .add("Partition", gce.partition) + .add("Readonly", gce.readOnly) + } + + private fun addAwsElasticBlockStore(aws: AWSElasticBlockStoreVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "AWSElasticBlockStore (a Persistent Disk resource in AWS)") + .add("VolumeID", aws.volumeID) + .add("FSType", aws.fsType) + .add("Partition", aws.partition) + .add("ReadOnly", aws.readOnly) + } + + private fun addGitRepo(git: GitRepoVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "GitRepo (a volume that is pulled from git when the pod is created)") + .add("Repository", git.repository) + .add("Revision", git.revision) + } + + private fun addSecret(secret: SecretVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "Secret (a volume populated by a Secret)") + .add("SecretName", secret.secretName) + .add("Optional", secret.optional) + } + + private fun addConfigMap(configMap: ConfigMapVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "ConfigMap (a volume populated by a ConfigMap)") + .add("Name", configMap.name) + .add("Optional", configMap.optional) + } + + private fun addNfs(nfs: NFSVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "NFS (an NFS mount that lasts the lifetime of a pod)") + .add("Server", nfs.server) + .add("Path", nfs.path) + .add("ReadOnly", nfs.readOnly) + } + + private fun addIscsi(iscsi: ISCSIVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "ISCSI (an ISCSI Disk resource that is attached to a kubelet's host machine and then exposed to the pod)") + .add("TargetPortal", iscsi.targetPortal) + .add("IQN", iscsi.iqn) + .add("Lun", iscsi.lun) + .add("ISCSIInterface", iscsi.iscsiInterface) + .add("FSType", iscsi.fsType) + .add("ReadOnly", iscsi.readOnly) + .add("Portals", iscsi.portals?.joinToString(",")) + .add("DiscoveryCHAPAuth", iscsi.chapAuthDiscovery) + .add("SecretRef", iscsi.secretRef?.name) + .add("InitiatorName", iscsi.initiatorName) + } + + private fun addGlusterfs(gfs: GlusterfsVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "Glusterfs (a Glusterfs mount on the host that shares a pod's lifetime)") + .add("EndpointsName", gfs.endpoints) + .add("Path", gfs.path) + .add("ReadOnly", gfs.readOnly) + } + + private fun addPersistentVolumeClaim(pvc: PersistentVolumeClaimVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)") + .add("ClaimName", pvc.claimName) + .add("ReadOnly", pvc.readOnly) + } + + private fun addEphemeral(ephemeral: EphemeralVolumeSource, parent: Chapter) { + parent.addIfExists( + TITLE_TYPE, + "EphemeralVolume (an inline specification for a volume that gets created and deleted with the pod)" + ) + PersistentVolumeClaimDescriber(ephemeral.volumeClaimTemplate).addTo(parent) + } + + private fun addRbd(rbd: RBDVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "RBD (a Rados Block Device mount on the host that shares a pod's lifetime)") + .add("CephMonitors", rbd.monitors?.joinToString(", ")) + .add("RBDImage", rbd.image) + .add("FSType", rbd.fsType) + .add("RBDPool", rbd.pool) + .add("RadosUser", rbd.user) + .add("Keyring", rbd.keyring) + .add("SecretRef", rbd.secretRef?.name) + .add("ReadOnly", rbd.readOnly) + } + + private fun addQuobyte(quobyte: QuobyteVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "Quobyte (a Quobyte mount on the host that shares a pod's lifetime)") + .add("Registry", quobyte.registry) + .add("Volume", quobyte.volume) + .add("ReadOnly", quobyte.readOnly) + } + + private fun addDownwardAPI(downwardAPI: DownwardAPIVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "HostPath (bare host directory volume)") + .addIfExists(NamedSequence("Mappings").addIfExists( + downwardAPI.items.mapNotNull { file -> + when { + file.fieldRef != null -> + "${file.fieldRef.fieldPath} -> ${file.path}" + + file.resourceFieldRef != null -> + "${file.resourceFieldRef.resource} -> ${file.path}" + + else -> + null + } + } + )) + } + + private fun addAzureDisk(disk: AzureDiskVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "AzureDisk (an Azure Data Disk mount on the host and bind mount to the pod)") + .add("DiskName", disk.diskName) + .add("DiskURI", disk.diskURI) + .add("Kind", disk.kind) + .add("FSType", disk.fsType) + .add("CachingMode", disk.cachingMode) + .add("ReadOnly", disk.readOnly) + } + + private fun addVsphereVolume(volume: VsphereVirtualDiskVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "SphereVolume (a Persistent Disk resource in vSphere)") + .add("VolumePath", volume.volumePath) + .add("FSType", volume.fsType) + .add("StoragePolicyName", volume.storagePolicyName) + } + + private fun addCinder(cinder: CinderVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "Cinder (a Persistent Disk resource in OpenStack)") + .add("VolumeID", cinder.volumeID) + .add("FSType", cinder.fsType) + .add("ReadOnly", cinder.readOnly) + .add("SecretRef", cinder.secretRef?.name) + } + + private fun addPhotoPersistentDisk(disk: PhotonPersistentDiskVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "PhotonPersistentDisk (a Persistent Disk resource in photon platform)") + .add("PdID", disk.pdID) + .add("FSType", disk.fsType) + } + + private fun addPortworxVolume(volume: PortworxVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "PortworxVolume (a Portworx Volume resource)") + .add("VolumeID", volume.volumeID) + } + + private fun addScaleIO(scaleIO: ScaleIOVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "ScaleIO (a persistent volume backed by a block device in ScaleIO)") + .add("Gateway", scaleIO.gateway) + .add("System", scaleIO.system) + .add("Protection Domain", scaleIO.protectionDomain) + .add("Storage Pool", scaleIO.storagePool) + .add("Storage Mode", scaleIO.storageMode) + .add("VolumeName", scaleIO.volumeName) + .add("FSType", scaleIO.fsType) + .add("System", scaleIO.system) + .add("ReadOnly", scaleIO.readOnly) + } + + private fun addCephfs(cephfs: CephFSVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "CephFS (a CephFS mount on the host that shares a pod's lifetime)") + .add("Monitors", cephfs.monitors?.joinToString(", ")) + .add("Path", cephfs.path) + .add("User", cephfs.user) + .add("SecretFile", cephfs.secretFile) + .add("SecretRef", cephfs.secretRef?.name) + .add("ReadOnly", cephfs.readOnly) + } + + private fun addStorageos(storageos: StorageOSVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "StorageOS (a StorageOS Persistent Disk resource)") + .add("VolumeName", storageos.volumeName) + .add("VolumeNamespace", storageos.volumeNamespace) + .add("FSType", storageos.fsType) + .add("ReadOnly", storageos.readOnly) + } + + private fun addFc(fc: FCVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "FC (a Fibre Channel disk)") + .add("TargetWWNs", fc.targetWWNs?.joinToString(", ")) + .add("LUN", fc.lun?.toString(10)) + .addIfExists("FSType", fc.fsType) + .addIfExists("ReadOnly", fc.readOnly) + } + + private fun addAzureFile(file: AzureFileVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "AzureFile (an Azure File Service mount on the host and bind mount to the pod)") + .addIfExists("SecretName", file.secretName) + .addIfExists("ShareName", file.shareName) + .addIfExists("ReadOnly", file.readOnly) + } + + private fun addFlexVolume(volume: FlexVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "FlexVolume (a generic volume resource that is provisioned/attached using an exec based plugin)") + .add("Driver", volume.driver) + .add("FSType", volume.fsType) + .add("SecretRef", volume.secretRef?.name) + .add("ReadOnly", volume.readOnly) + .addSequence("Options", volume.options?.map { option -> + "${option.key} -> ${option.value}" + }) + } + + private fun addFlocker(flocker: FlockerVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "Flocker (a Flocker volume mounted by the Flocker agent)") + .add("DatasetName", flocker.datasetName) + .add("DatasetUUID", flocker.datasetUUID) + } + + private fun addProjected(projected: ProjectedVolumeSource, parent: Chapter) { + parent.addIfExists(TITLE_TYPE, "Projected (a volume that contains injected data from multiple sources)") + projected.sources.forEach { source -> + when { + source.secret != null -> { + parent.add("SecretName", source.secret.name) + parent.add("SecretOptionalName", source.secret.optional) + } + source.downwardAPI != null -> { + parent.add("DownwardAPI", true) + } + source.configMap != null -> { + parent.add("ConfigMapName", source.configMap.name) + parent.add("ConfigMapOptional", source.configMap.optional) + } + source.serviceAccountToken != null -> { + parent.add("TokenExpirationSeconds", source.serviceAccountToken.expirationSeconds) + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Chapter.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Chapter.kt new file mode 100644 index 000000000..542f3b44f --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Chapter.kt @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.paragraphs + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE + +open class Chapter(title: String, paragraphs: List = emptyList()) : HasChildren(title, paragraphs) { + + fun add(title: String, value: Boolean?): Chapter { + return addIfExists(title, value ?: false) + } + + fun add(title: String, value: Int?): Chapter { + return if (value == null) { + addIfExists(title, NONE) + } else { + addIfExists(title, value) + } + } + + fun add(title: String, value: Long?): Chapter { + if (value == null) { + addIfExists(title, NONE) + } else { + addIfExists(title, value) + } + return this + } + + fun add(title: String, value: String?): Chapter { + addIfExists(title, value ?: NONE) + return this + } + + fun addSequence(title: String, values: List?): Chapter { + if (values.isNullOrEmpty()) { + addIfExists(title, NONE) + } else { + addIfExists(NamedSequence(title, values)) + } + return this + } + + fun addChapter(title: String, paragraphs: List?): Chapter { + if (paragraphs.isNullOrEmpty()) { + addIfExists(title, NONE) + } else { + addIfExists(Chapter(title, paragraphs)) + } + return this + } + + fun addChapterIfExists(title: String, paragraphs: List): Chapter { + if (paragraphs.isNotEmpty()) { + addIfExists(Chapter(title, paragraphs)) + } + return this + } + + fun addIfExists(title: String, value: String?): Chapter { + if (value.isNullOrBlank()) { + return this + } + return addIfExists(NamedValue(title, value)) + } + + fun addIfExists(label: String, valueProvider: () -> String?): Chapter { + return addIfExists(label, valueProvider.invoke()) + } + + fun addIfExists(label: String, value: Int?): Chapter { + if (value == null) { + return this + } + return addIfExists(NamedValue(label, value)) + } + + fun addIfExists(label: String, value: Long?): Chapter { + if (value == null) { + return this + } + return addIfExists(NamedValue(label, value)) + } + + fun addIfExists(label: String, value: Boolean?): Chapter { + if (value == null) { + return this + } + return addIfExists(NamedValue(label, value)) + } + + fun addIfExists(title: String, paragraph: Paragraph): Chapter { + return addIfExists(Chapter(title, listOf(paragraph))) + } + + fun addIfExists(paragraph: Paragraph?): Chapter { + if (paragraph != null) { + children.add(paragraph) + } + return this + } + + fun addIfExists(paragraphs: List): Chapter { + paragraphs.forEach { addIfExists(it) } + return this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Chapter) return false + if (!super.equals(other)) return false + return true + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/HasChildren.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/HasChildren.kt new file mode 100644 index 000000000..d049d3353 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/HasChildren.kt @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.paragraphs + +open class HasChildren protected constructor(title: String, children: List = emptyList()): Paragraph(title) { + + val children = mutableListOf() + + init { + this.children.addAll(children) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HasChildren<*>) return false + if (!super.equals(other)) return false + + if (children != other.children) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + children.hashCode() + return result + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequence.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequence.kt new file mode 100644 index 000000000..12c880409 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequence.kt @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.paragraphs + +class NamedSequence(title: String, children: List = emptyList()): HasChildren(title, children) { + + fun addIfExists(value: Any?): NamedSequence { + if (value == null + || (value is String && value.isBlank())) { + return this + } + children.add(value) + return this + } + + fun addIfExists(values: List?): NamedSequence { + if (values.isNullOrEmpty()) { + return this + } + values.forEach { value -> addIfExists(value) } + return this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NamedSequence) return false + if (!super.equals(other)) return false + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedValue.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedValue.kt new file mode 100644 index 000000000..9605f7eb2 --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedValue.kt @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.paragraphs + +class NamedValue(title: String, val value: Any?): Paragraph(title) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NamedValue) return false + if (!super.equals(other)) return false + + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (value?.hashCode() ?: 0) + return result + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Paragraph.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Paragraph.kt new file mode 100644 index 000000000..384a135ad --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/Paragraph.kt @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.paragraphs + +abstract class Paragraph(val title: String) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Paragraph) return false + + if (title != other.title) return false + + return true + } + + override fun hashCode(): Int { + return title.hashCode() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt index b793acf84..d9add854c 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/AllContexts.kt @@ -109,8 +109,12 @@ open class AllContexts( get() { lock.write { if (_all.isEmpty()) { - val all = createContexts(client.get(), client.get()?.config) + try { + val all = createContexts(client.get(), client.get()?.config) _all.addAll(all) + } catch (e: Exception) { + // + } } return _all } diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/Filters.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/Filters.kt index 92bf058c3..828760564 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/Filters.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/Filters.kt @@ -10,6 +10,8 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model.resource.kubernetes +import com.jgoodies.common.base.Objects +import io.fabric8.kubernetes.api.model.Event import io.fabric8.kubernetes.api.model.HasMetadata import io.fabric8.kubernetes.api.model.Pod import io.fabric8.kubernetes.api.model.ReplicationController @@ -100,3 +102,20 @@ abstract class ResourceForPod(private val pod: Pod) : Predicate< abstract fun getSelectorLabels(resource: R): Map } + +class EventForResource(val resource: HasMetadata): Predicate { + override fun test(event: Event): Boolean { + val involved = event.involvedObject ?: return false + return Objects.equals(resource.kind, involved.kind) + && Objects.equals(resource.metadata?.name, involved.name) + && Objects.equals(resource.metadata?.namespace, involved.namespace) + && Objects.equals(resource.apiVersion, involved.apiVersion) + && Objects.equals(resource.metadata?.resourceVersion, involved.resourceVersion) + } +} + +class EventForResourceKind(val resource: HasMetadata): Predicate { + override fun test(event: Event): Boolean { + return resource.kind == event.involvedObject.kind + } +} diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtils.kt index 9b26271ea..d42c35706 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtils.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtils.kt @@ -34,9 +34,12 @@ fun getFirstContainer(resource: HasMetadata): Container? { } -fun getStatus(container: Container, podStatus: PodStatus): ContainerStatus? { - return getStatus(container, podStatus.containerStatuses) - ?: getStatus(container, podStatus.initContainerStatuses) +fun Container.getStatus(podStatus: PodStatus?): ContainerStatus? { + if (podStatus == null) { + return null + } + return getStatus(this, podStatus.containerStatuses) + ?: getStatus(this, podStatus.initContainerStatuses) } private fun getStatus(container: Container, containerStatus: List): ContainerStatus? { diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtils.kt new file mode 100644 index 000000000..9ecd71ebd --- /dev/null +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtils.kt @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.model.util + +import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.SeccompProfile + +object PodUtils { + + /* + * PodPending means the pod has been accepted by the system, but one or more of the containers + * has not been started. This includes time before being bound to a node, as well as time spent + * pulling images onto the host. + */ + const val PHASE_PENDING = "Pending" + + /* + * PodRunning means the pod has been bound to a node and all of the containers have been started. + * At least one container is still running or is in the process of being restarted. + */ + const val PHASE_RUNNING = "Running" + + /* + * PodSucceeded means that all containers in the pod have voluntarily terminated + * with a container exit code of 0, and the system is not going to restart any of these containers. + */ + const val PHASE_SUCCEEDED = "Succeeded" + + /* + * PodFailed means that all containers in the pod have terminated, and at least one container has + * terminated in a failure (exited with a non-zero exit code or was stopped by the system). + */ + const val PHASE_FAILED = "Failed" + + /* + * SeccompProfileTypeUnconfined indicates no seccomp profile is applied (A.K.A. unconfined). + */ + const val SECCOMP_PROFILE_UNCONFINED = "Unconfined" + /* + * SeccompProfileTypeRuntimeDefault represents the default container runtime seccomp profile. + */ + const val SECCOMP_PROFILE_RUNTIME_DEFAULT = "RuntimeDefault" + /* + * SeccompProfileTypeLocalhost indicates a profile defined in a file on the node should be used. + * The file's location relative to /seccomp. + */ + const val SECCOMP_PROFILE_LOCALHOST = "Localhost" + + fun Pod?.isTerminating(): Boolean { + if (this == null + || status == null) { + return false + } + return !metadata?.deletionTimestamp.isNullOrBlank() + && status.phase != PHASE_SUCCEEDED + && status.phase != PHASE_FAILED + } + + fun SeccompProfile.isSeccompProfileLocalhost(): Boolean { + return SECCOMP_PROFILE_LOCALHOST == this.type + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtils.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtils.kt index 87530fe62..75e2e6637 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtils.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtils.kt @@ -20,6 +20,8 @@ import java.util.stream.Collectors const val MARKER_WILL_BE_DELETED = "willBeDeleted" const val API_GROUP_VERSION_DELIMITER = '/' +const val TOLERATION_OPERATOR_EXISTS = "Exists" +const val TOLERATION_OPERATOR_EQUAL = "Equal" /** * Returns `true` if the given resource has the same @@ -416,4 +418,17 @@ fun toKindAndName(resource: HasMetadata) = fun hasLabel(value: String, label: String, resource: R): Boolean { return value == resource.metadata?.labels?.get(label) +} + +/** + * Returns `null` if the given value is `null`. Returns the boolean value for it otherwise. + * A string that does not represent a boolean value returns `false`. + * + * @return the boolean value for this string or null + */ +fun String?.toBooleanOrNull(): Boolean? { + if (this == null) { + return null + } + return this.toBoolean() } \ No newline at end of file diff --git a/src/main/kotlin/icons/Icons.kt b/src/main/kotlin/icons/Icons.kt index 42a41bab6..f149a2c24 100644 --- a/src/main/kotlin/icons/Icons.kt +++ b/src/main/kotlin/icons/Icons.kt @@ -21,6 +21,8 @@ object Icons { val consoles = loadIcon("icons/consoles.svg") @JvmField val terminal = loadIcon("icons/terminal.svg") + @JvmField + val describe = loadIcon("icons/lupe.svg") private fun loadIcon(path: String): Icon { return IconLoader.getIcon(path, Icons::class.java) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 25043c528..760f5a3b9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -227,7 +227,7 @@ serviceImplementation="com.redhat.devtools.intellij.kubernetes.model.ResourceModel"/> - + @@ -293,6 +293,10 @@ id="com.redhat.devtools.intellij.kubernetes.actions.OpenDashboardAction" text="Open Dashboard" icon="AllIcons.Nodes.EmptyNode"/> + diff --git a/src/main/resources/icons/lupe.svg b/src/main/resources/icons/lupe.svg new file mode 100644 index 000000000..5c5e8f074 --- /dev/null +++ b/src/main/resources/icons/lupe.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProviderTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProviderTest.kt index bb124387c..f71b6ab09 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProviderTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditorTabTitleProviderTest.kt @@ -11,7 +11,6 @@ package com.redhat.devtools.intellij.kubernetes.editor -import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.nhaarman.mockitokotlin2.* @@ -69,36 +68,17 @@ class ResourceEditorTabTitleProviderTest { assertThat(title).isEqualTo("$TITLE_UNKNOWN_NAME@$namespace") } - @Test - fun `#getTitle should ask fallback provider for title if file is NOT temporary`() { - // given - val isTemporary = false - val file: VirtualFile = mock() - val resourceInfo = kubernetesResourceInfo( - "", - "", - kubernetesTypeInfo("", "")) - val fallback: EditorTabTitleProvider = mock() - val provider = createResourceEditorTabTitleProvider(isTemporary, resourceInfo, fallback) - // when - provider.getEditorTabTitle(mock(), file) - // then - verify(fallback).getEditorTabTitle(any(), eq(file)) - } - private fun createResourceEditorTabTitleProvider( - isTemporary: Boolean, - info: KubernetesResourceInfo, - fallback: EditorTabTitleProvider = mock() - ): ResourceEditorTabTitleProvider { - return object : ResourceEditorTabTitleProvider(fallback) { + isResourceFile: Boolean, + info: KubernetesResourceInfo): ResourceEditorTabTitleProvider { + return object : ResourceEditorTabTitleProvider() { override fun getKubernetesResourceInfo(file: VirtualFile, project: Project): KubernetesResourceInfo { return info } - override fun isTemporary(file: VirtualFile): Boolean { - return isTemporary + override fun isResourceFile(file: VirtualFile): Boolean { + return isResourceFile } } } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriberTestUtils.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriberTestUtils.kt new file mode 100644 index 000000000..77a1a0a28 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriberTestUtils.kt @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.HasChildren +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Paragraph + +@Suppress("UNCHECKED_CAST") +object DescriberTestUtils { + + inline fun getParagraph(title: String, parent: HasChildren): T? { + return parent.children.find { paragraph -> + paragraph is T + && paragraph.title == title + } as T? + } + + inline fun getParagraph(path: List, parent: HasChildren): T? { + val iterator = path.iterator() + var paragraph: Paragraph? = parent + while (iterator.hasNext()) { + if (paragraph is HasChildren<*>) { + paragraph = getParagraph(iterator.next(), paragraph as HasChildren) + } else { + return null + } + } + return paragraph as? T? + } + + inline fun getChildren( + title: String, + parent: HasChildren? + ): List? { + if (parent == null) { + return null + } + val chapter = getParagraph(title, parent) ?: return null + if (chapter !is HasChildren<*>) { + return null + } + return chapter.children as List? + } + + fun toMap(children: List?): Map? { + return children?.associate { value -> + value.title to value.value + } + } +} diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtilsTest.kt new file mode 100644 index 000000000..e7955a54e --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/DescriptionUtilsTest.kt @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.time.LocalDateTime + +class DescriptionUtilsTest { + + @Test + fun `toHumanReadableDurationSince returns days if difference is less than 2y`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2023, 6, 14, 13, 49)) + // then + assertThat(since).isEqualTo("397d") + } + + @Test + fun `toHumanReadableDurationSince returns years and days if difference is less than 8y but more than 2y`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2020, 6, 14, 13, 49)) + // then + assertThat(since).isEqualTo("4y32d") + } + + @Test + fun `toHumanReadableDurationSince returns years if difference is more than 8y`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2015, 6, 14, 13, 49)) + // then + assertThat(since).isEqualTo("9y") + } + + @Test + fun `toHumanReadableDurationSince returns days and hours if difference is less than 8d and more than 2d`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2024, 7, 11, 13, 49)) + // then + assertThat(since).isEqualTo("4d1h") + } + + @Test + fun `toHumanReadableDurationSince returns only days if difference is less than 8d and more than 2d and does not differ in hours`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2024, 7, 11, 14, 59)) + // then + assertThat(since).isEqualTo("4d") + } + + @Test + fun `toHumanReadableDurationSince returns hours and minutes if difference is less than 2d but more than 8h`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2024, 7, 14, 13, 49)) + // then + assertThat(since).isEqualTo("25h10m") + } + + @Test + fun `toHumanReadableDurationSince returns hours if difference is less than 8h but more than 3h`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2024, 7, 15, 11, 49)) + // then + assertThat(since).isEqualTo("3h") + } + + @Test + fun `toHumanReadableDurationSince returns minutes if difference is less than 3h but more than 10m`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2024, 7, 15, 13, 49)) + // then + assertThat(since).isEqualTo("70m") + } + + @Test + fun `toHumanReadableDurationSince returns seconds if difference is less than 2m`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2024, 7, 15, 14, 58)) + // then + assertThat(since).isEqualTo("79s") + } + + @Test + fun `toHumanReadableDurationSince returns minutes and seconds if difference is less 10m but more than 2m`() { + // given + val deletionTimestamp = "2024-07-15T14:59:19Z" + // when + val since = toHumanReadableDurationSince(deletionTimestamp, LocalDateTime.of(2024, 7, 15, 14, 51)) + // then + assertThat(since).isEqualTo("8m19s") + } + + @Test + fun `createValueOrSequence returns null if given list is empty`() { + // given + val items = emptyList() + // when + val paragraph = createValueOrSequence("Items", items) + // then + assertThat(paragraph).isNull() + } + + @Test + fun `createValueOrSequence returns value if given list has only 1 entry`() { + // given + val items = listOf("leia") + // when + val paragraph = createValueOrSequence("Items", items) + // then + assertThat(paragraph).isExactlyInstanceOf(NamedValue::class.java) + assertThat((paragraph as NamedValue).value).isEqualTo("leia") + } + + @Test + fun `createValueOrSequence returns sequence if given list has several items`() { + // given + val items = listOf("leia", "luke", "obiwan") + // when + val paragraph = createValueOrSequence("Items", items) + // then + assertThat(paragraph).isExactlyInstanceOf(NamedSequence::class.java) + assertThat((paragraph as NamedSequence).children).containsOnly("leia", "luke", "obiwan") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescriptionTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescriptionTest.kt new file mode 100644 index 000000000..3a8889f79 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/YAMLDescriptionTest.kt @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.util.Collections + +class YAMLDescriptionTest { + + @Test + fun `toText should return title and value if document has 1 paragraph`() { + // given + val description = YAMLDescription() + description.addIfExists("jedi", "luke") + // when + val text = description.toText() + // then + assertThat(text).isEqualTo("jedi: luke\n") + } + + @Test + fun `toText should return title and integer value`() { + // given + val description = YAMLDescription() + description.addIfExists("jedi", 42 as Int) + // when + val text = description.toText() + // then + assertThat(text).isEqualTo("jedi: 42\n") + } + + @Test + fun `toText should return title and boolean value`() { + // given + val description = YAMLDescription() + description.addIfExists("jedi", true) + // when + val text = description.toText() + // then + assertThat(text).isEqualTo("jedi: true\n") + } + + @Test + fun `toText should NOT return any text if label is present but value is empty`() { + // given + val description = YAMLDescription() + description.addIfExists("jedi", "") + // when + val text = description.toText() + // then + assertThat(text).isEmpty() + } + + @Test + fun `toText should NOT return any text if label is present but value provider returns null`() { + // given + val description =YAMLDescription() + description.addIfExists("jedi") { null } + // when + val text = description.toText() + // then + assertThat(text).isEmpty() + } + + @Test + fun `toText should NOT return any text if label is empty`() { + // given + val description = YAMLDescription() + description.addIfExists("", "luke") + // when + val text = description.toText() + // then + assertThat(text).isEmpty() + } + + @Test + fun `toText should return title and 'none' is sub-paragraphs are empty`() { + // given + val description = YAMLDescription() + description.addChapter("jedi", Collections.emptyList()) + // when + val text = description.toText() + // then + assertThat(text).isEqualTo("jedi: $NONE\n") + } + + @Test + fun `toText should return title and 2 values if document has a chapter with 2 values`() { + // given + val description = YAMLDescription() + val jedis = Chapter("jedis") + .addIfExists(NamedValue("padawan", "luke")) + .addIfExists(NamedValue("master", "yoda")) + description.addIfExists(jedis) + // when + val text = description.toText() + // then + assertThat(text).isEqualTo( + """ + jedis: + padawan: luke + master: yoda + + """.trimIndent() + ) + } + + @Test + fun `toText should return 2 titles with values if document has 2 chapters with values`() { + // given + val description =YAMLDescription() + val jedis = Chapter("jedis").addIfExists( + NamedValue("padawan", "luke") + ) + val siths = Chapter("sith").addIfExists( + NamedValue("master", "darth vader") + ) + description + .addIfExists(jedis) + .addIfExists(siths) + // when + val text = description.toText() + // then + assertThat(text).isEqualTo( + """ + jedis: + padawan: luke + sith: + master: darth vader + + """.trimIndent() + ) + } + + @Test + fun `toText should return title and sequence of items`() { + // given + val description = YAMLDescription() + description.addIfExists( + NamedSequence( + "jedis", listOf( + "leia", + "luke", + "obiwan") + ) + ) + // when + val text = description.toText() + // then + assertThat(text).isEqualTo( + """ + jedis: + - leia + - luke + - obiwan + + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberPortsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberPortsTest.kt new file mode 100644 index 000000000..5cc3820f5 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberPortsTest.kt @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.CONTAINERS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.HOST_PORT +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.HOST_PORTS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.PORT +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.PORTS +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getParagraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import io.fabric8.kubernetes.api.model.ContainerBuilder +import io.fabric8.kubernetes.api.model.ContainerPortBuilder +import io.fabric8.kubernetes.api.model.PodBuilder +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ContainerDescriberPortsTest { + + @Test + fun `should describe port`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers( + ContainerBuilder() + .withName("leia") + .withPorts( + ContainerPortBuilder() + .withContainerPort(42) + .withProtocol("Ubese") + .build()) + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(listOf(CONTAINERS, "leia", PORT), description) + assertThat(paragraph?.value).isEqualTo("42/Ubese") + assertThat(getParagraph(listOf(CONTAINERS, "leia", PORTS), description)).isNull() + } + + @Test + fun `should describe port with NONE if no port is provided`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers( + ContainerBuilder() + .withName("leia") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", PORTS), description)).isNull() + val paragraph = getParagraph(listOf(CONTAINERS, "leia", PORT), description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe host port`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers( + ContainerBuilder() + .withName("leia") + .withPorts( + ContainerPortBuilder() + .withHostPort(42) + .withProtocol("Ubese") + .build() + ) + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", HOST_PORTS), description)).isNull() + val paragraph = getParagraph(listOf(CONTAINERS, "leia", HOST_PORT), description) + assertThat(paragraph?.value).isEqualTo("42/Ubese") + } + + @Test + fun `should describe host port with NONE if no port is provided`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers( + ContainerBuilder() + .withName("leia") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", HOST_PORTS), description)).isNull() + val paragraph = getParagraph(listOf(CONTAINERS, "leia", HOST_PORT), description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe host port with 0 if no host port is provided`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers( + ContainerBuilder() + .withName("leia") + .withPorts( + ContainerPortBuilder() + .withHostPort(null) // no host port + .withProtocol("Ubese") + .build() + ) + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", HOST_PORTS), description)).isNull() + val paragraph = getParagraph(listOf(CONTAINERS, "leia", HOST_PORT), description) + assertThat(paragraph?.value).isEqualTo("0/Ubese") + } + + @Test + fun `should describe host ports`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers( + ContainerBuilder() + .withName("leia") + .withPorts( + ContainerPortBuilder() + .withHostPort(42) + .withProtocol("Ubese") + .build(), + ContainerPortBuilder() + .withHostPort(84) + .withProtocol("Droidspeak") + .build() + ) + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", HOST_PORT), description)).isNull() + val paragraph = getParagraph(listOf(CONTAINERS, "leia", HOST_PORTS), description) + assertThat(paragraph?.value).isEqualTo("42/Ubese, 84/Droidspeak") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberProbesTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberProbesTest.kt new file mode 100644 index 000000000..0995bbab4 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberProbesTest.kt @@ -0,0 +1,190 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.CONTAINERS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.LIVENESS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.READINESS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.STARTUP +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getParagraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.model.mocks.PodContainer.podWithContainer +import io.fabric8.kubernetes.api.model.ContainerBuilder +import io.fabric8.kubernetes.api.model.ExecActionBuilder +import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder +import io.fabric8.kubernetes.api.model.ProbeBuilder +import io.fabric8.kubernetes.api.model.TCPSocketActionBuilder +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ContainerDescriberProbesTest { + + @Test + fun `should NOT describe liveness if it's not provided`() { + // given + val pod = podWithContainer(ContainerBuilder() + .withName("leia") + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", LIVENESS), description)?.value) + .isNull() + } + + @Test + fun `should describe liveness as unknown if exec, httpGet, tcpSocket, grpc are not defined`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("leia") + .withLivenessProbe(ProbeBuilder().build()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", LIVENESS), description)?.value) + .isEqualTo("unknown delay=0s timeout=0s period=0s #success=0 #failure=0") + } + + @Test + fun `should describe exec liveness`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("leia") + .withLivenessProbe( + ProbeBuilder() + .withExec( + ExecActionBuilder() + .withCommand("turn", "on", "the", "light", "saber") + .build()) + .withPeriodSeconds(42) + .withSuccessThreshold(84) + .build()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", LIVENESS), description)?.value) + .isEqualTo("exec [turn on the light saber] delay=0s timeout=0s period=42s #success=84 #failure=0") + } + + @Test + fun `should describe http-get liveness`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("leia") + .withLivenessProbe( + ProbeBuilder() + .withHttpGet( + HTTPGetActionBuilder() + .withScheme("https") + .withHost("abafar") + .withNewPort(42) + .withPath("void desert") + .build() + ) + .withInitialDelaySeconds(12) + .withPeriodSeconds(42) + .withFailureThreshold(84) + .build()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", LIVENESS), description)?.value) + .isEqualTo("http-get https://abafar:42/void desert delay=12s timeout=0s period=42s #success=0 #failure=84") + } + + @Test + fun `should describe tcp-socket liveness`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("leia") + .withLivenessProbe( + ProbeBuilder() + .withTcpSocket( + TCPSocketActionBuilder() + .withHost("abafar") + .withNewPort(42) + .build() + ) + .withTimeoutSeconds(42) + .withSuccessThreshold(84) + .build()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", LIVENESS), description)?.value) + .isEqualTo("tcp-socket abafar:42 delay=0s timeout=42s period=0s #success=84 #failure=0") + } + + @Test + fun `should describe exec startup probe`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("leia") + .withStartupProbe( + ProbeBuilder() + .withExec( + ExecActionBuilder() + .withCommand("turn", "on", "the", "light", "saber") + .build()) + .withInitialDelaySeconds(4222) + .withTimeoutSeconds(422) + .withSuccessThreshold(42) + .build()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", STARTUP), description)?.value) + .isEqualTo("exec [turn on the light saber] delay=4222s timeout=422s period=0s #success=42 #failure=0") + } + + @Test + fun `should describe exec readiness probe`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("leia") + .withReadinessProbe( + ProbeBuilder() + .withExec( + ExecActionBuilder() + .withCommand("turn", "on", "the", "light", "saber") + .build()) + .withTimeoutSeconds(42) + .withSuccessThreshold(1) + .withFailureThreshold(84) + .build()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", READINESS), description)?.value) + .isEqualTo("exec [turn on the light saber] delay=0s timeout=42s period=0s #success=1 #failure=84") + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberResourcesTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberResourcesTest.kt new file mode 100644 index 000000000..93bbcd7e6 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberResourcesTest.kt @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.CONTAINERS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.LIMITS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.REQUESTS +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getChildren +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getParagraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.toMap +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.HasChildren +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Paragraph +import com.redhat.devtools.intellij.kubernetes.model.mocks.PodContainer.podWithContainer +import io.fabric8.kubernetes.api.model.ContainerBuilder +import io.fabric8.kubernetes.api.model.Quantity +import io.fabric8.kubernetes.api.model.ResourceRequirements +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ContainerDescriberResourcesTest { + + @Test + fun `should NOT describe limits if none are provided`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("leia") + .withResources(ResourceRequirements()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", LIMITS), description)?.value) + .isNull() + } + + @Test + fun `should describe limits`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("R2-D2") + .withResources( + ResourceRequirements( + emptyList(), + mapOf( + "cpu" to Quantity("500", "m"), + "ephemeral-storage" to Quantity("2", "Gi"), + "memory" to Quantity("128", "Mi") + ), + emptyMap() + ) + ) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + val r2d2 = getParagraph>(listOf(CONTAINERS, "R2-D2"), description) + val limits = getChildren(LIMITS, r2d2) + assertThat(toMap(limits)).containsExactlyEntriesOf( + mapOf( + "cpu" to "500m", + "ephemeral-storage" to "2Gi", + "memory" to "128Mi" + ) + ) + } + + @Test + fun `should NOT describe requests if none are provided`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("R2-D2") + .withResources(ResourceRequirements()) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", REQUESTS), description)?.value) + .isNull() + } + + @Test + fun `should describe requests`() { + // given + val pod = podWithContainer( + ContainerBuilder() + .withName("R2-D2") + .withResources( + ResourceRequirements( + emptyList(), + emptyMap(), + mapOf( + "cpu" to Quantity("42", "m"), + "ephemeral-storage" to Quantity("42", "Gi"), + "memory" to Quantity("256", "Mi") + ) + ) + ) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + val r2d2 = getParagraph>(listOf(CONTAINERS, "R2-D2"), description) + val limits = getChildren(REQUESTS, r2d2) + assertThat(toMap(limits)).containsExactlyEntriesOf( + mapOf( + "cpu" to "42m", + "ephemeral-storage" to "42Gi", + "memory" to "256Mi" + ) + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberStatusTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberStatusTest.kt new file mode 100644 index 000000000..59876466b --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberStatusTest.kt @@ -0,0 +1,221 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.CONTAINERS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.EXIT_CODE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.FINISHED +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.LAST_STATE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.MESSAGE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.READY +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.REASON +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.RESTART_COUNT +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.RUNNING +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.SIGNAL +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.STARTED +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.STATE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.TERMINATED +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.WAITING +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getParagraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.model.mocks.PodContainer.podWithContainer +import io.fabric8.kubernetes.api.model.ContainerState +import io.fabric8.kubernetes.api.model.ContainerStateRunningBuilder +import io.fabric8.kubernetes.api.model.ContainerStateTerminatedBuilder +import io.fabric8.kubernetes.api.model.ContainerStateWaitingBuilder +import io.fabric8.kubernetes.api.model.ContainerStatusBuilder +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ContainerDescriberStatusTest { + + @Test + fun `should NOT describe status if it's not provided`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE), description)) + .isNull() + } + + @Test + fun `should describe status with waiting if it's unknown`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withNewStateLike( + ContainerState() // state that's no running, waiting nor terminated -> unknown + ) + .endState() + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE), description)?.value) + .isEqualTo(WAITING) + } + + @Test + fun `should describe running state`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withNewState() + .withRunning( + ContainerStateRunningBuilder() + .withStartedAt("2024-07-15T14:59:19Z") + .build() + ) + .endState() + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, STATE), description)?.value) + .isEqualTo(RUNNING) + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, STARTED), description)?.value.toString()) + .startsWith("Mon, 15 Jul 2024 14:59:19") // '+0200' in GMT+2, other value elsewhere + } + + @Test + fun `should describe waiting state`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withNewState() + .withWaiting( + ContainerStateWaitingBuilder() + .withReason("death star shield is not lowered yet") + .build() + ) + .endState() + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + val state = getParagraph(listOf(CONTAINERS, "leia", STATE, STATE), description) + assertThat(state?.value) + .isEqualTo(WAITING) + val started = getParagraph(listOf(CONTAINERS, "leia", STATE, REASON), description) + assertThat(started?.value) + .isEqualTo("death star shield is not lowered yet") + } + + @Test + fun `should describe terminated state`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withNewState() + .withTerminated( + ContainerStateTerminatedBuilder() + .withReason("death star did not lower shield") + .withMessage("try later") + .withExitCode(42) + .withSignal(84) + .withStartedAt("2024-06-15T14:59:19Z") + .withFinishedAt("2024-07-15T14:59:19Z") + .build() + ) + .endState() + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, STATE), description)?.value) + .isEqualTo(TERMINATED) + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, REASON), description)?.value) + .isEqualTo("death star did not lower shield") + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, MESSAGE), description)?.value) + .isEqualTo("try later") + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, EXIT_CODE), description)?.value) + .isEqualTo(42) + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, SIGNAL), description)?.value) + .isEqualTo(84) + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, STARTED), description)?.value.toString()) + .startsWith("Sat, 15 Jun 2024 14:59:19") // '+0200' in GMT+2, other value elsewhere + assertThat(getParagraph(listOf(CONTAINERS, "leia", STATE, FINISHED), description)?.value.toString()) + .startsWith("Mon, 15 Jul 2024 14:59:19") // '+0200' in GMT+2, other value elsewhere + } + + @Test + fun `should describe running last state`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withNewLastState() + .withRunning( + ContainerStateRunningBuilder() + .withStartedAt("2024-07-15T14:59:19Z") + .build() + ) + .endLastState() + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + val state = getParagraph(listOf(CONTAINERS, "leia", LAST_STATE, STATE), description) + assertThat(state?.value) + .isEqualTo(RUNNING) + assertThat(getParagraph(listOf(CONTAINERS, "leia", LAST_STATE, STARTED), description)?.value.toString()) + .startsWith("Mon, 15 Jul 2024 14:59:19") // '+0200' in GMT+2, other value elsewhere + } + + @Test + fun `should describe ready`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withReady(true) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", READY), description)?.value) + .isEqualTo(true) + } + + @Test + fun `should describe restart count`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withRestartCount(42) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", RESTART_COUNT), description)?.value) + .isEqualTo(42) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberTest.kt new file mode 100644 index 000000000..0899c73f9 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/ContainerDescriberTest.kt @@ -0,0 +1,376 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.ARGS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.COMMAND +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.CONTAINERS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.CONTAINER_ID +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.ENVIRONMENT +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.ENVIRONMENT_VARIABLES_FROM +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.IMAGE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.IMAGE_ID +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.INIT_CONTAINERS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.ContainersDescriber.Labels.MOUNTS +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getChildren +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getParagraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.model.mocks.PodContainer.podWithContainer +import io.fabric8.kubernetes.api.model.ContainerBuilder +import io.fabric8.kubernetes.api.model.ContainerStatusBuilder +import io.fabric8.kubernetes.api.model.EnvFromSourceBuilder +import io.fabric8.kubernetes.api.model.EnvVarBuilder +import io.fabric8.kubernetes.api.model.PodBuilder +import io.fabric8.kubernetes.api.model.VolumeMountBuilder +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ContainerDescriberTest { + + @Test + fun `should NOT describe init containers if there are none`() { + // given + val pod = PodBuilder() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + val initContainers = getParagraph(INIT_CONTAINERS, description) + assertThat(initContainers).isNull() + } + + @Test + fun `should describe containers with NONE if there are none`() { + // given + val pod = PodBuilder() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(CONTAINERS, description)?.value) + .isEqualTo(NONE) + } + + @Test + fun `should describe container name`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers(ContainerBuilder() + .withName("leia") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getChildren(CONTAINERS, description)?.first()?.title) + .isEqualTo("leia") + } + + @Test + fun `should describe container ID`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withContainerID("planet of the republic") + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", CONTAINER_ID), description)?.value) + .isEqualTo("planet of the republic") + } + + @Test + fun `should NOT describe container ID if there's none`() { + // given + val pod = PodBuilder() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + val containerId = getParagraph(listOf(CONTAINERS, "leia", CONTAINER_ID), description) + assertThat(containerId).isNull() + } + + @Test + fun `should describe image`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers(ContainerBuilder() + .withName("leia") + .withImage("princess") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", IMAGE), description)?.value) + .isEqualTo("princess") + } + + @Test + fun `should NOT describe image if there's none`() { + // given + val pod = PodBuilder() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", IMAGE), description)) + .isNull() + } + + @Test + fun `should describe image ID`() { + // given + val pod = podWithContainer( + ContainerStatusBuilder() + .withName("leia") + .withImageID("alderaan") + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", IMAGE_ID), description)?.value) + .isEqualTo("alderaan") + } + + @Test + fun `should NOT describe image ID if there's none`() { + // given + val pod = PodBuilder() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", IMAGE_ID), description)) + .isNull() + } + + @Test + fun `should describe command`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers(ContainerBuilder() + .withName("leia") + .withCommand("x-wings", "engage", "the", "death star") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", COMMAND), description)?.value) + .isEqualTo("x-wings\nengage\nthe\ndeath star") + } + + @Test + fun `should NOT describe command if none is provided`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers(ContainerBuilder() + .withName("leia") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", COMMAND), description)) + .isNull() + + } + + @Test + fun `should describe args`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers(ContainerBuilder() + .withName("leia") + .withArgs("argues", "a lot", "with", "Han Solo") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", ARGS), description)?.value) + .isEqualTo("argues\na lot\nwith\nHan Solo") + } + + @Test + fun `should NOT describe args if none are provided`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withContainers(ContainerBuilder() + .withName("leia") + .build() + ) + .endSpec() + .build() + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", ARGS), description)) + .isNull() + } + + @Test + fun `should NOT describe environment variables from if there are none`() { + // given + val pod = podWithContainer(ContainerBuilder() + .withName("leia") + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", ENVIRONMENT_VARIABLES_FROM), description)) + .isNull() + } + + @Test + fun `should describe environment variables from`() { + // given + val pod = podWithContainer(ContainerBuilder() + .withName("leia") + .withEnvFrom( + EnvFromSourceBuilder() + .withPrefix("pilot of") + .withNewConfigMapRef() + .withName("rebel army") + .withOptional(true) + .endConfigMapRef() + .build(), + EnvFromSourceBuilder() + .withNewSecretRef() + .withName("jedi army") + .withOptional(true) + .endSecretRef() + .build() + ) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", ENVIRONMENT_VARIABLES_FROM), description)?.children) + .containsExactly( + "rebel army ConfigMap with prefix \"pilot of\" Optional: true", + "jedi army Secret Optional: true" + ) + } + + @Test + fun `should describe environment`() { + // given + val pod = podWithContainer(ContainerBuilder() + .withName("leia") + .withEnv( + EnvVarBuilder() + .withName("gun") + .withValue("laser") + .build(), + EnvVarBuilder() + .withName("dress") + .withValue("jumpsuit") + .build() + ) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", ENVIRONMENT), description)?.children) + .containsExactly( + NamedValue("gun", "laser"), + NamedValue("dress", "jumpsuit") + ) + } + + @Test + fun `should NOT describe environment if there is none`() { + // given + val pod = podWithContainer(ContainerBuilder() + .withName("leia") + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", ENVIRONMENT), description)) + .isNull() + } + + @Test + fun `should describe mounts with NONE if there are none`() { + // given + val pod = podWithContainer(ContainerBuilder() + .withName("leia") + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", MOUNTS), description)) + .isNull() + } + + @Test + fun `should describe mounts`() { + // given + val pod = podWithContainer(ContainerBuilder() + .withName("leia") + .withVolumeMounts( + VolumeMountBuilder() + .withName("Alderaan") + .withMountPath("Cruiser Tantive IV") + .withSubPath("Captured over Tatooine") + .withReadOnly(true) + .build(), + VolumeMountBuilder() + .withName("Geonosis orbit") + .withMountPath("Death star") + .withReadOnly(false) + .build() + ) + .build() + ) + // when + val description = ContainersDescriber(pod).addTo(Chapter("")) + // then + assertThat(getParagraph(listOf(CONTAINERS, "leia", MOUNTS), description)?.children) + .containsExactly( + "Cruiser Tantive IV from Alderaan (ro, path = \"Captured over Tatooine\")", + "Death star from Geonosis orbit (rw)" + ) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriberTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriberTest.kt new file mode 100644 index 000000000..1ebafc75f --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/describer/PodDescriberTest.kt @@ -0,0 +1,844 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.describer + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getChildren +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.getParagraph +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriberTestUtils.toMap +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Labels.NAME +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Labels.NAMESPACE +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.NO_HOST_IP +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.ANNOTATIONS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.CONDITIONS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.CONTROLLED_BY +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.IP +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.IPS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.PRIORITY +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.PRIORITY_CLASS_NAME +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.RUNTIME_CLASS_NAME +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.NODE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.LABELS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.LOCALHOST_PROFILE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.MESSAGE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.NODE_SELECTORS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.NOMINATED_NODE_NAME +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.QOS_CLASS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.READINESS_GATES +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.REASON +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.SECCOMP_PROFILE +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.SERVICE_ACCOUNT +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.START_TIME +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.STATUS +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.TERMINATION_GRACE_PERIOD +import com.redhat.devtools.intellij.kubernetes.editor.describe.describer.PodDescriber.Labels.TOLERATIONS +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.Chapter +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedSequence +import com.redhat.devtools.intellij.kubernetes.editor.describe.paragraphs.NamedValue +import com.redhat.devtools.intellij.kubernetes.editor.describe.toRFC1123Date +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils +import com.redhat.devtools.intellij.kubernetes.model.util.TOLERATION_OPERATOR_EQUAL +import com.redhat.devtools.intellij.kubernetes.model.util.TOLERATION_OPERATOR_EXISTS +import io.fabric8.kubernetes.api.model.OwnerReference +import io.fabric8.kubernetes.api.model.PodBuilder +import io.fabric8.kubernetes.api.model.PodCondition +import io.fabric8.kubernetes.api.model.PodIP +import io.fabric8.kubernetes.api.model.PodReadinessGate +import io.fabric8.kubernetes.api.model.Toleration +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class PodDescriberTest { + + @Test + fun `should describe name`() { + // given + val pod = PodBuilder() + .withNewMetadata() + .withName("luke") + .endMetadata() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(NAME, description) + assertThat(paragraph?.value).isEqualTo("luke") + } + + @Test + fun `should describe namespace`() { + // given + val pod = PodBuilder() + .withNewMetadata() + .withNamespace("rebellion") + .endMetadata() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(NAMESPACE, description) + assertThat(paragraph?.value).isEqualTo("rebellion") + } + + @Test + fun `should describe priority`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withPriority(42) + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(PRIORITY, description) + assertThat(paragraph?.value).isEqualTo(42) + } + + @Test + fun `should describe priority class name`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withPriorityClassName("jedis") + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(PRIORITY_CLASS_NAME, description) + assertThat(paragraph?.value).isEqualTo("jedis") + } + + @Test + fun `should not describe priority class name if pod has no priority class name`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(PRIORITY_CLASS_NAME, description) + assertThat(paragraph).isNull() + } + + @Test + fun `should describe runtime class name`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withRuntimeClassName("x-wing") + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(RUNTIME_CLASS_NAME, description) + assertThat(paragraph?.value).isEqualTo("x-wing") + } + + @Test + fun `should NOT describe runtime class name if pod has no runtime class name`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(RUNTIME_CLASS_NAME, description) + assertThat(paragraph).isNull() + } + + @Test + fun `should describe service account name`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withServiceAccountName("obiwan") + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(SERVICE_ACCOUNT, description) + assertThat(paragraph?.value).isEqualTo("obiwan") + } + + @Test + fun `should not describe service account name if pod has no service account name`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(SERVICE_ACCOUNT, description) + assertThat(paragraph).isNull() + } + + @Test + fun `should describe node as NONE if pod has no node`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val nodeValue = getParagraph(NODE, description) + assertThat(nodeValue?.value).isEqualTo(NONE) + } + + @Test + fun `should describe node as name & hostIp if pod has node`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withNodeName("leia") + .endSpec() + .withNewStatus() + .withHostIP("jedi") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val nodeValue = getParagraph(NODE, description) + assertThat(nodeValue?.value).isEqualTo("leia/jedi") + } + + @Test + fun `should describe node as name & noIp if pod has no nodeIp`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withNodeName("leia") + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val nodeValue = getParagraph(NODE, description) + assertThat(nodeValue?.value).isEqualTo("leia/${NO_HOST_IP}") + } + + @Test + fun `should describe start time`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withStartTime("2024-07-15T14:59:19Z") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(START_TIME, description) + assertThat(paragraph?.value).isEqualTo(toRFC1123Date("2024-07-15T14:59:19Z")) + } + + @Test + fun `should NOT describe start time if pod has no start time`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(START_TIME, description) + assertThat(paragraph).isNull() + } + + @Test + fun `should describe labels`() { + // given + val pod = PodBuilder() + .withNewMetadata() + .withLabels( + mapOf( + "luke" to "skywalker", + "princess" to "leia" + ) + ) + .endMetadata() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraphs = toMap(getChildren(LABELS, description)) + assertThat(paragraphs).containsExactlyEntriesOf( + mapOf( + "luke" to "skywalker", + "princess" to "leia" + ) + ) + } + + @Test + fun `should describe labels with NONE if there are no labels`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(LABELS, description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe annotations`() { + // given + val pod = PodBuilder() + .withNewMetadata() + .withAnnotations( + mapOf( + "luke" to "skywalker", + "princess" to "leia" + ) + ) + .endMetadata() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraphs = toMap(getChildren(ANNOTATIONS, description)) + assertThat(paragraphs).containsExactlyEntriesOf( + mapOf( + "luke" to "skywalker", + "princess" to "leia" + ) + ) + } + + @Test + fun `should describe annotations with NONE if there are no annotations`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(ANNOTATIONS, description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe status if pod is NOT terminating`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withPhase("Looking for the force") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(STATUS, description) + assertThat(paragraph?.value).isEqualTo("Looking for the force") + } + + @Test + fun `should describe status and grace period if pod is terminating`() { + // given + val deletion = "2024-07-15T14:59:19Z" + val pod = PodBuilder() + .withNewMetadata() + .withDeletionTimestamp(deletion) + .withDeletionGracePeriodSeconds(42) + .endMetadata() + .withNewStatus() + .withPhase("Looking for the force") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val status = getParagraph(STATUS, description) + assertThat(status?.value.toString()).startsWith("Terminating: (lasts") + val gracePeriod = getParagraph(TERMINATION_GRACE_PERIOD, description) + assertThat(gracePeriod?.value).isEqualTo("42s") + } + + @Test + fun `should describe reason`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withReason("luke lost the light saber") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(REASON, description) + assertThat(paragraph?.value).isEqualTo("luke lost the light saber") + } + + @Test + fun `should NOT describe reason if pod has no reason`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(REASON, description) + assertThat(paragraph).isNull() + } + + @Test + fun `should NOT describe message if pod has no message`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(MESSAGE, description) + assertThat(paragraph).isNull() + } + + @Test + fun `should describe message`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withMessage("leia broke the light saber") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(MESSAGE, description) + assertThat(paragraph?.value).isEqualTo("leia broke the light saber") + } + + @Test + fun `should NOT describe seccomp profile if pod has no seccomp profile`() { + // given + val pod = PodBuilder() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(SECCOMP_PROFILE, description) + assertThat(paragraph).isNull() + } + + @Test + fun `should describe seccomp profile`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withNewSecurityContext() + .withNewSeccompProfile() + .withType("Ice Planet Hoth") + .endSeccompProfile() + .endSecurityContext() + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(SECCOMP_PROFILE, description) + assertThat(paragraph?.value).isEqualTo("Ice Planet Hoth") + val localhost = getParagraph(LOCALHOST_PROFILE, description) + assertThat(localhost).isNull() + } + + @Test + fun `should describe seccomp profile and localhost profile if seccomp profile is localhost`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withNewSecurityContext() + .withNewSeccompProfile() + .withType(PodUtils.SECCOMP_PROFILE_LOCALHOST) + .withLocalhostProfile("Alderaan") + .endSeccompProfile() + .endSecurityContext() + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val seccompProfile = getParagraph(SECCOMP_PROFILE, description) + assertThat(seccompProfile?.value).isEqualTo(PodUtils.SECCOMP_PROFILE_LOCALHOST) + val localhost = getParagraph(LOCALHOST_PROFILE, description) + assertThat(localhost?.value).isEqualTo("Alderaan") + } + + @Test + fun `should describe IP`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withPodIP("42") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(IP, description) + assertThat(paragraph?.value).isEqualTo("42") + } + + @Test + fun `should describe IP with NONE if pod has no IP`() { + // given + val pod = PodBuilder() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(IP, description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe IPs with NONE if pod has no IPs`() { + // given + val pod = PodBuilder() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(IPS, description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe IPs`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withPodIPs( + PodIP("42"), + PodIP("84"), + PodIP("168") + ) + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraphs = toMap(getChildren(IPS, description)) + assertThat(paragraphs).containsExactlyEntriesOf( + mapOf( + "IP" to "42", + "IP" to "84", + "IP" to "168" + ) + ) + } + + @Test + fun `should NOT describe controller if pod has no owner reference`() { + // given + val pod = PodBuilder() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(CONTROLLED_BY, description) + assertThat(paragraph?.value).isNull() + } + + @Test + fun `should NOT describe controller if there's no controller owner reference`() { + // given + val leia: OwnerReference = OwnerReference().apply { + kind = "princess" + name = "leia" + controller = false + } + + val yoda: OwnerReference = OwnerReference().apply { + kind = "jedi" + name = "yoda" + controller = false + } + + val pod = PodBuilder() + .withNewMetadata() + .withOwnerReferences(leia, yoda) + .endMetadata() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(CONTROLLED_BY, description) + assertThat(paragraph?.value).isNull() + } + + @Test + fun `should describe controller`() { + // given + val leia: OwnerReference = OwnerReference().apply { + kind = "princess" + name = "leia" + controller = true + } + val yoda: OwnerReference = OwnerReference().apply { + kind = "jedi" + name = "yoda" + controller = false + } + + val pod = PodBuilder() + .withNewMetadata() + .withOwnerReferences(yoda, leia) + .endMetadata() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(CONTROLLED_BY, description) + assertThat(paragraph?.value).isEqualTo("princess/leia") + } + + @Test + fun `should NOT describe nominated node name if pod has no nominated node name`() { + // given + val pod = PodBuilder() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(NOMINATED_NODE_NAME, description) + assertThat(paragraph?.value).isNull() + } + + @Test + fun `should describe nominated node name`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withNominatedNodeName("yoda") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(NOMINATED_NODE_NAME, description) + assertThat(paragraph?.value).isEqualTo("yoda") + } + + @Test + fun `should NOT describe readiness gates if pod has no readiness gates`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(READINESS_GATES, description) + assertThat(paragraph?.value).isNull() + } + + @Test + fun `should describe readiness gates`() { + // given + val condition1 = PodCondition().apply { + type = "rebellion saves the republic" + status = true.toString() + } + val condition2 = PodCondition().apply { + type = "emperor destroys the republic" + // no status -> + } + val pod = PodBuilder() + .withNewSpec() + .withReadinessGates( + PodReadinessGate(condition1.type), + PodReadinessGate(condition2.type) + ) + .endSpec() + .withNewStatus() + .withConditions( + condition1, + condition2 + ) + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraphs = toMap(getChildren(READINESS_GATES, description)) + assertThat(paragraphs).containsExactlyEntriesOf( + mapOf( + "rebellion saves the republic" to true, + "emperor destroys the republic" to NONE + ) + ) + } + + @Test + fun `should describe readiness gates with NONE value if condition doesn't exist`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withReadinessGates( + PodReadinessGate("yoda"), + ) + .endSpec() + // no conditions + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraphs = toMap(getChildren(READINESS_GATES, description)) + assertThat(paragraphs).containsExactlyEntriesOf( + mapOf( + "yoda" to NONE + ) + ) + } + + @Test + fun `should NOT describe conditions if pod has no conditions`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(CONDITIONS, description) + assertThat(paragraph?.value).isNull() + } + + @Test + fun `should describe conditions`() { + // given + val condition1 = PodCondition().apply { + type = "rebellion saves the republic" + status = true.toString() + } + val condition2 = PodCondition().apply { + type = "emperor destroys the republic" + status = "non-boolean" // false + } + val pod = PodBuilder() + .withNewStatus() + .withConditions( + condition1, + condition2 + ) + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraphs = toMap(getChildren(CONDITIONS, description)) + assertThat(paragraphs).containsExactlyEntriesOf( + mapOf( + "rebellion saves the republic" to true, + "emperor destroys the republic" to false + ) + ) + } + + @Test + fun `should describe QoS class`() { + // given + val pod = PodBuilder() + .withNewStatus() + .withQosClass("brave rebellion") + .endStatus() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(QOS_CLASS, description) + assertThat(paragraph?.value).isEqualTo("brave rebellion") + } + + @Test + fun `should describe QoS class with NONE if pod has no QoS class`() { + // given + val pod = PodBuilder() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(QOS_CLASS, description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe node selectors`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withNodeSelector( + mapOf( + "luke" to "death star", + "princess" to "alderaan" + ) + ) + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraphs = toMap(getChildren(NODE_SELECTORS, description)) + assertThat(paragraphs).containsExactlyEntriesOf( + mapOf( + "luke" to "death star", + "princess" to "alderaan" + ) + ) + } + + @Test + fun `should describe node selectors with NONE if there are no node selectors`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(NODE_SELECTORS, description) + assertThat(paragraph?.value).isEqualTo(NONE) + } + + @Test + fun `should describe tolerations`() { + // given + val pod = PodBuilder() + .withNewSpec() + .withTolerations( + Toleration( + "total-anger-control", + "yoda", + TOLERATION_OPERATOR_EXISTS, + 42, + null + ), + Toleration( + "hot-headed", + "anakin", + TOLERATION_OPERATOR_EQUAL, + null, + "eaten by anger" + ) + ) + .endSpec() + .build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph: NamedSequence? = getParagraph(TOLERATIONS, description) + assertThat(paragraph?.children).containsExactly( + "yoda:total-anger-control op=Exists for 42s", + "anakin=eaten by anger:hot-headed" + ) + } + + @Test + fun `should describe tolerations with NONE if there are no tolerations`() { + // given + val pod = PodBuilder().build() + // when + val description = PodDescriber(pod).addTo(Chapter("")) + // then + val paragraph = getParagraph(TOLERATIONS, description) + assertThat(paragraph?.value).isEqualTo(NONE) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/ChapterTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/ChapterTest.kt new file mode 100644 index 000000000..bb8612723 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/ChapterTest.kt @@ -0,0 +1,237 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.paragraphs + +import com.redhat.devtools.intellij.kubernetes.editor.describe.DescriptionConstants.Values.NONE +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ChapterTest { + + @Test + fun `#add adds Boolean value if it is NOT null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("obiwan is a jedi", true) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(true) + } + + @Test + fun `#add adds false if Boolean is null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("obiwan is a jedi", null as Boolean?) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(false) + } + + @Test + fun `#addIfExists does NOT add if Boolean value is null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.addIfExists("obiwan is a jedi", null as Boolean?) + // then + assertThat(chapter.children).isEmpty() + } + + @Test + fun `#add adds Int value if it is NOT null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("light sabers that obiwan owns", 42.toInt()) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(42) + } + + @Test + fun `#add adds NONE if Int is null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("light sabers that obiwan owns", null as Int?) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(NONE) + } + + + @Test + fun `#add adds Long value if it is NOT null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("capes that obiwan owns", 42.toLong()) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(42.toLong()) + } + + @Test + fun `#add adds NONE if Long is null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("capes that obiwan owns", null as Long?) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(NONE) + } + + @Test + fun `#add adds String value if it is NOT null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("name", "leia") + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo("leia") + } + + @Test + fun `#add adds NONE if String is null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.add("name", null as String?) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(NONE) + } + + @Test + fun `#addSequence adds NONE if values are null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.addSequence("dark side", null as List?) + // then + assertThat(chapter.children).hasSize(1) + assertThat(chapter.children.first()).isExactlyInstanceOf(NamedValue::class.java) + val child = chapter.children.first() as NamedValue + assertThat(child.value).isEqualTo(NONE) + } + + @Test + fun `#addSequence adds NONE if values are empty`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.addSequence("dark side", emptyList()) + // then + assertThat((chapter.children.first() as NamedValue).value).isEqualTo(NONE) + } + + @Test + fun `#addSequence adds sequence if values are NOT null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.addSequence( + "light side", listOf( + "leia", + "obiwan", + "luke" + ) + ) + // then + val sequence = (chapter.children.first() as NamedSequence) + assertThat(sequence.title).isEqualTo("light side") + assertThat(sequence.children).containsOnly( + "leia", + "obiwan", + "luke" + ) + } + + @Test + fun `#addChapter adds NONE if paragraphs are empty`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.addChapter("dark side", emptyList()) + // then + val child = chapter.children.first() as NamedValue + assertThat(child.title).isEqualTo("dark side") + assertThat(child.value).isEqualTo(NONE) + } + + @Test + fun `#addChapter adds NONE if chapters are null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.addChapter("dark side", null as List?) + // then + val child = chapter.children.first() as NamedValue + assertThat(child.title).isEqualTo("dark side") + assertThat(child.value).isEqualTo(NONE) + } + + @Test + fun `#addChapter adds chapters if chapters are NOT null`() { + // given + val chapter = Chapter("jedis") + assertThat(chapter.children).isEmpty() + // when + chapter.addChapter( + "light side", listOf( + NamedValue("princess", "leia"), + NamedValue("luke", "skywalker") + ) + ) + // then + val children = chapter.children.first() as Chapter + assertThat(children.title).isEqualTo("light side") + assertThat(children.children).containsOnly( + NamedValue("princess", "leia"), + NamedValue("luke", "skywalker") + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequenceTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequenceTest.kt new file mode 100644 index 000000000..f868058ab --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/describe/paragraphs/NamedSequenceTest.kt @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2024 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.describe.paragraphs + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class NamedSequenceTest { + + @Test + fun `#addIfExists adds if value is NOT null`() { + // given + val sequence = NamedSequence("jedis") + assertThat(sequence.children).isEmpty() + // when + sequence.addIfExists("obiwan") + // then + assertThat(sequence.children).hasSize(1) + } + + @Test + fun `#addIfExists does NOT add if value is null`() { + // given + val sequence = NamedSequence("jedis") + assertThat(sequence.children).isEmpty() + // when + sequence.addIfExists(null) + // then + assertThat(sequence.children).isEmpty() + } + + @Test + fun `#addIfExists does NOT add if value is blank string`() { + // given + val sequence = NamedSequence("jedis") + assertThat(sequence.children).isEmpty() + // when + sequence.addIfExists(" ") + // then + assertThat(sequence.children).isEmpty() + } +} diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt index c66b28325..0663e9425 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/ClientMocks.kt @@ -20,6 +20,7 @@ import io.fabric8.kubernetes.api.Pluralize import io.fabric8.kubernetes.api.model.APIResource import io.fabric8.kubernetes.api.model.Container import io.fabric8.kubernetes.api.model.Context +import io.fabric8.kubernetes.api.model.Event import io.fabric8.kubernetes.api.model.GenericKubernetesResource import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList import io.fabric8.kubernetes.api.model.HasMetadata @@ -27,6 +28,7 @@ import io.fabric8.kubernetes.api.model.NamedContext import io.fabric8.kubernetes.api.model.Namespace import io.fabric8.kubernetes.api.model.NamespaceList import io.fabric8.kubernetes.api.model.ObjectMeta +import io.fabric8.kubernetes.api.model.ObjectReference import io.fabric8.kubernetes.api.model.Pod import io.fabric8.kubernetes.api.model.PodList import io.fabric8.kubernetes.api.model.PodSpec @@ -460,4 +462,30 @@ object ClientMocks { .map { mock() } .toList() } + + fun objectReference(involved: HasMetadata): ObjectReference { + return mock().apply { + doReturn(involved.apiVersion) + .whenever(this).apiVersion + doReturn(involved.kind) + .whenever(this).kind + doReturn(involved.metadata.name) + .whenever(this).name + doReturn(involved.metadata.namespace) + .whenever(this).namespace + doReturn(involved.metadata.uid) + .whenever(this).uid + doReturn(involved.metadata.resourceVersion) + .whenever(this).resourceVersion + } + } + + fun event(name: String, involved: HasMetadata): Event { + val resource = resource(name) + doReturn(objectReference(involved)) + .whenever(resource).involvedObject + return resource + } + + } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodContainer.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodContainer.kt new file mode 100644 index 000000000..bef4271d4 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodContainer.kt @@ -0,0 +1,33 @@ +package com.redhat.devtools.intellij.kubernetes.model.mocks + +import io.fabric8.kubernetes.api.model.Container +import io.fabric8.kubernetes.api.model.ContainerBuilder +import io.fabric8.kubernetes.api.model.ContainerStatus +import io.fabric8.kubernetes.api.model.Pod +import io.fabric8.kubernetes.api.model.PodBuilder + +object PodContainer { + + fun podWithContainer(status: ContainerStatus): Pod { + return PodBuilder() + .withNewSpec() + .withContainers( + ContainerBuilder() + .withName(status.name) + .build() + ) + .endSpec() + .withNewStatus() + .withContainerStatuses(status) + .endStatus() + .build() + } + + fun podWithContainer(container: Container): Pod { + return PodBuilder() + .withNewSpec() + .withContainers(container) + .endSpec() + .build() + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodMocks.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodMocks.kt index ad5dc28ff..c77e2e395 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodMocks.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/mocks/PodMocks.kt @@ -36,11 +36,13 @@ fun podStatus(phase: String, reason: String): PodStatus { fun podStatus( initContainerStatuses: List = emptyList(), containerStatuses: List = emptyList(), - conditions: List = emptyList()): PodStatus { + conditions: List = emptyList(), + phase: String = ""): PodStatus { return mock { on(mock.initContainerStatuses) doReturn initContainerStatuses on(mock.containerStatuses) doReturn containerStatuses on(mock.conditions) doReturn conditions + on(mock.phase) doReturn phase } } @@ -51,8 +53,9 @@ fun condition(type: String? = null, status: String? = null): PodCondition { } } -fun containerStatus(ready: Boolean = false, state: ContainerState? = null): ContainerStatus { +fun containerStatus(name: String, ready: Boolean = false, state: ContainerState? = null): ContainerStatus { return mock { + on(mock.name) doReturn name on(mock.ready) doReturn ready on(mock.state) doReturn state } @@ -93,8 +96,9 @@ class PodMockBuilder(private val pod: Pod) { status(podStatus( initContainerStatuses = listOf( containerStatus( - state = containerState( - containerStateTerminated(42)))))) + pod.metadata.name, + state = containerState( + containerStateTerminated(42)))))) return this } @@ -104,7 +108,7 @@ class PodMockBuilder(private val pod: Pod) { return this } - fun deletion(timestamp: String): PodMockBuilder { + fun deletionTimestamp(timestamp: String): PodMockBuilder { whenever(pod.metadata.deletionTimestamp) .thenReturn(timestamp) return this diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/EventForResourceTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/EventForResourceTest.kt new file mode 100644 index 000000000..8fb82e555 --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/kubernetes/EventForResourceTest.kt @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.model.resource.kubernetes + +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE1 +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE2 +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.NAMESPACE3 +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD1 +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD2 +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD3 +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.event +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource +import io.fabric8.kubernetes.api.model.PodBuilder +import io.fabric8.kubernetes.api.model.batch.v1.Job +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class EventForResourceTest { + + private val pod1Event = event("${POD1.metadata.name}-0.17e47e1bbf7010bf", POD1) + private val pod2Event = event("${POD2.metadata.name}-0.17e47e1bbf75027f", POD2) + private val pod3Event = event("${POD3.metadata.name}-0.17e47e1c07d2256a", POD3) + private val namespace1Event = event("${NAMESPACE1.metadata.name}-17e4eca904439043", NAMESPACE1) + private val namespace2Event = event("${NAMESPACE2.metadata.name}-17e4814ead8405d5", NAMESPACE2) + private val namespace3Event = event("${NAMESPACE3.metadata.name}-17e4eca905569146", NAMESPACE3) + + private val events = listOf(pod1Event, namespace1Event, namespace2Event, pod2Event, pod3Event, namespace3Event) + + @Test + fun `#EventForResource returns events that match given resource`() { + // given + // when + val filtered = events.filter { event -> + EventForResource(POD2).test(event) + } + // then + assertThat(filtered).hasSize(1) + assertThat(filtered[0]).isEqualTo(pod2Event) + } + + @Test + fun `#EventForResource returns no events if given resource has different namespace`() { + // given + val pod = PodBuilder(POD2) + .editMetadata() + .withNamespace("death-star") + .endMetadata() + .build() + // when + val filtered = events.filter { event -> + EventForResource(pod).test(event) + } + // then + assertThat(filtered).isEmpty() + } + + @Test + fun `#EventForResource returns no events if given resource has different name`() { + // given + val pod = PodBuilder(POD2) + .editMetadata() + .withName("leia") + .endMetadata() + .build() + // when + val filtered = events.filter { event -> + EventForResource(pod).test(event) + } + // then + assertThat(filtered).isEmpty() + } + + @Test + fun `#EventForResource returns no events if given resource has different resource version`() { + // given + val pod = PodBuilder(POD2) + .editMetadata() + .withResourceVersion("therepublic") + .endMetadata() + .build() + // when + val filtered = events.filter { event -> + EventForResource(pod).test(event) + } + // then + assertThat(filtered).isEmpty() + } + + @Test + fun `#EventForResource returns no events if given resource has different apiVersion`() { + // given + val pod = PodBuilder(POD2) + .withApiVersion("rebellion") + .build() + // when + val filtered = events.filter { event -> + EventForResource(pod).test(event) + } + // then + assertThat(filtered).isEmpty() + } + + @Test + fun `#EventForResource returns no events if none matches given resource`() { + // given + // when + val filtered = events.filter { event -> + EventForResource(resource("luke should use the force")).test(event) + } + // then + assertThat(filtered).isEmpty() + } + + @Test + fun `#EventForResourceKind returns no events if none matches given resource`() { + // given + // when + val filtered = events.filter { event -> + EventForResourceKind(POD2).test(event) + } + // then + val numberOfPodEvents = events.filter { event -> event.involvedObject.kind == POD2.kind }.size + assertThat(filtered).hasSize(numberOfPodEvents) + } + +} diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtilsTest.kt index 06b5827d2..c44906001 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtilsTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ContainerUtilsTest.kt @@ -10,12 +10,14 @@ ******************************************************************************/ package com.redhat.devtools.intellij.kubernetes.model.util -import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD2 import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.POD3 +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.container import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.podSpec import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.setContainers +import com.redhat.devtools.intellij.kubernetes.model.mocks.containerStatus +import com.redhat.devtools.intellij.kubernetes.model.mocks.podStatus import io.fabric8.kubernetes.api.model.batch.v1.Job import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -23,10 +25,10 @@ import org.junit.Test class ContainerUtilsTest { @Test - fun `Pod#getFirstContainer() should return 1st container if exists`() { + fun `Pod#getFirstContainer() returns 1st container if exists`() { // given - val container1 = ClientMocks.container("luke-skywalker") - val container2 = ClientMocks.container("darth-vader") + val container1 = container("luke-skywalker") + val container2 = container("darth-vader") val pod = setContainers(POD2, container1, container2) // when val container = pod.getFirstContainer() @@ -46,11 +48,11 @@ class ContainerUtilsTest { } @Test - fun `Job#getFirstContainer() should return 1st container if exists`() { + fun `Job#getFirstContainer() returns 1st container if exists`() { // given val job = resource("destroy-the-death-star") - val container1 = ClientMocks.container("princess-leia") - val container2 = ClientMocks.container("luke-skywalker") + val container1 = container("princess-leia") + val container2 = container("luke-skywalker") val pod = setContainers(job, container1, container2) // when val container = pod.getFirstContainer() @@ -58,4 +60,77 @@ class ContainerUtilsTest { assertThat(container).isEqualTo(container1) } + @Test + fun `Container#getStatus returns null if podStatus is null`() { + // given + val container = container("princess-leia") + // when + val containerStatus = container.getStatus(null) + // then + assertThat(containerStatus).isNull() + } + + @Test + fun `Container#getStatus returns null if there's no container status in podStatus`() { + // given + val container = container("princess-leia") + val podStatus = podStatus( + initContainerStatuses = emptyList(), + containerStatuses = emptyList(), + phase = "waiting for the rebel fleet" + ) + // when + val containerStatus = container.getStatus(podStatus) + // then + assertThat(containerStatus).isNull() + } + + @Test + fun `Container#getStatus returns init container status with the same name`() { + // given + val container = container("princess-leia") + val containerStatus = containerStatus("princess-leia") + val podStatus = podStatus( + initContainerStatuses = listOf(containerStatus), + containerStatuses = emptyList(), + phase = "waiting for the rebel fleet" + ) + // when + val found = container.getStatus(podStatus) + // then + assertThat(found).isEqualTo(containerStatus) + } + + @Test + fun `Container#getStatus returns container status with the same name`() { + // given + val container = container("luke-skywalker") + val containerStatus = containerStatus("luke-skywalker") + val podStatus = podStatus( + initContainerStatuses = emptyList(), + containerStatuses = listOf(containerStatus), + phase = "waiting for the rebel fleet" + ) + // when + val found = container.getStatus(podStatus) + // then + assertThat(found).isEqualTo(containerStatus) + } + + @Test + fun `Container#getStatus returns container status with the same name even if init container status exists`() { + // given + val container = container("luke-skywalker") + val containerStatus = containerStatus("luke-skywalker") + val initContainerStatus = containerStatus("luke-skywalker") + val podStatus = podStatus( + initContainerStatuses = listOf(initContainerStatus), + containerStatuses = listOf(containerStatus), + phase = "waiting for the rebel fleet" + ) + // when + val found = container.getStatus(podStatus) + // then + assertThat(found).isEqualTo(containerStatus) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtilsTest.kt new file mode 100644 index 000000000..c5807ce5a --- /dev/null +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/PodUtilsTest.kt @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2022 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.intellij.kubernetes.model.util + +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.whenever +import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource +import com.redhat.devtools.intellij.kubernetes.model.mocks.PodMockBuilder +import com.redhat.devtools.intellij.kubernetes.model.mocks.podStatus +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils.PHASE_FAILED +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils.PHASE_PENDING +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils.PHASE_RUNNING +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils.PHASE_SUCCEEDED +import com.redhat.devtools.intellij.kubernetes.model.util.PodUtils.isTerminating +import io.fabric8.kubernetes.api.model.Pod +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test + +class PodUtilsTest { + + private var pod: Pod? = null + + @Before + fun before() { + this.pod = resource("luke", "jedis", "42", "v1", "1") + val metadata = pod?.metadata + doReturn("2024-07-15T14:59:19Z") + .whenever(metadata)?.deletionTimestamp + } + + @Test + fun `Pod#isTerminating() should return false if pod has no deletionTimestamp`() { + // given + val pod = pod() + PodMockBuilder(pod) + .deletionTimestamp("42 of September 4242") + // when + val terminating = pod.isTerminating() + // then + assertThat(terminating).isFalse() + } + + @Test + fun `Pod#isTerminating() should return false if pod has deletionTimestamp but is in SUCCEEDED phase`() { + // given + val pod = pod() + PodMockBuilder(pod) + .status(podStatus(phase = PHASE_SUCCEEDED)) + // when + val terminating = pod.isTerminating() + // then + assertThat(terminating).isFalse() + } + + @Test + fun `Pod#isTerminating() should return false if pod has deletionTimestamp but is in FAILED phase`() { + // given + val pod = pod() + PodMockBuilder(pod) + .status(podStatus(phase = PHASE_FAILED)) + // when + val terminating = pod.isTerminating() + // then + assertThat(terminating).isFalse() + } + + @Test + fun `Pod#isTerminating() should return true if pod has deletionTimestamp and is in RUNNING phase`() { + // given + val pod = pod() + PodMockBuilder(pod) + .status(podStatus(phase = PHASE_RUNNING)) + // when + val terminating = pod.isTerminating() + // then + assertThat(terminating).isTrue() + } + + @Test + fun `Pod#isTerminating() should return true if pod has deletionTimestamp and is in PENDING phase`() { + // given + val pod = pod() + PodMockBuilder(pod) + .status(podStatus(phase = PHASE_PENDING)) + // when + val terminating = pod.isTerminating() + // then + assertThat(terminating).isTrue() + } + + private fun pod(): Pod { + return pod ?: throw RuntimeException("pod not initialized") + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtilsTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtilsTest.kt index 5f07b4d3d..9361a5751 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtilsTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtilsTest.kt @@ -558,7 +558,7 @@ class ResourceUtilsTest { } @Test - fun `#hasManagedField should return false if resource has no managed fields `() { + fun `#hasManagedField should return false if resource has no managed fields`() { // given val meta = ObjectMetaBuilder().build() val neo = PodBuilder() @@ -570,4 +570,31 @@ class ResourceUtilsTest { assertThat(hasManagedFields).isFalse() } + @Test + fun `#toBooleanOrNull should return null if string is null`() { + // given + // when + val boolean = null.toBooleanOrNull() + // then + assertThat(boolean).isNull() + } + + @Test + fun `#toBooleanOrNull should return true if string is 'TRUE'`() { + // given + // when + val boolean = "TRUE".toBooleanOrNull() + // then + assertThat(boolean).isTrue() + } + + @Test + fun `#toBooleanOrNull should return false if string is 'BOGUS'`() { + // given + // when + val boolean = "BOGUS".toBooleanOrNull() + // then + assertThat(boolean).isFalse() + } + } \ No newline at end of file