Skip to content

fix: allow to edit secrets/configmaps in multi-resource yaml documents (#852) #853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
# libraries
kubernetes-client = "7.1.0"
devtools-common = "1.9.8"
devtools-common = "1.9.9-SNAPSHOT"
jackson-core = "2.17.0"
commons-lang3 = "3.12.0"
assertj-core = "3.22.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@ import com.intellij.codeInsight.hints.InlayHintsProvider
import com.intellij.codeInsight.hints.InlayHintsSink
import com.intellij.codeInsight.hints.NoSettings
import com.intellij.codeInsight.hints.SettingsKey
import com.intellij.json.psi.JsonFile
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.ui.dsl.builder.panel
import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo
import com.redhat.devtools.intellij.kubernetes.editor.util.getContent
import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo
import com.redhat.devtools.intellij.kubernetes.editor.inlay.base64.Base64Presentations
import org.jetbrains.yaml.psi.YAMLFile
import javax.swing.JComponent


internal class Base64ValueInlayHintsProvider : InlayHintsProvider<NoSettings> {
internal class ResourceEditorInlayHintsProvider : InlayHintsProvider<NoSettings> {

override val key: SettingsKey<NoSettings> = SettingsKey("KubernetesResource.hints")
override val name: String = "Kubernetes"
Expand All @@ -49,22 +53,46 @@ internal class Base64ValueInlayHintsProvider : InlayHintsProvider<NoSettings> {
}
}

override fun getCollectorFor(file: PsiFile, editor: Editor, settings: NoSettings, sink: InlayHintsSink): InlayHintsCollector? {
val info = KubernetesResourceInfo.extractMeta(file) ?: return null
return Collector(editor, info)
override fun getCollectorFor(file: PsiFile, editor: Editor, settings: NoSettings, sink: InlayHintsSink): InlayHintsCollector {
return Collector(editor)
}

private class Collector(editor: Editor, private val info: KubernetesResourceInfo) : FactoryInlayHintsCollector(editor) {
private class Collector(editor: Editor) : FactoryInlayHintsCollector(editor) {

override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
if (element !is PsiFile
|| !element.isValid) {
if (!element.isValid) {
return true
}
val content = getContent(element) ?: return true
val factory = Base64Presentations.create(content, info, sink, editor) ?: return true
factory.create()
return false
return when(element) {
is YAMLFile -> {
create(element, sink, editor)
false
}
is JsonFile -> {
create(element, sink, editor)
false
}
else -> true
}
}

private fun create(file: YAMLFile, sink: InlayHintsSink, editor: Editor) {
return ReadAction.run<Exception> {
file.documents.forEach { document ->
val info = KubernetesTypeInfo.create(document) ?: return@forEach
val element = document.topLevelValue ?: return@forEach
Base64Presentations.create(element, info, sink, editor)?.create()
}
}
}

private fun create(file: JsonFile, sink: InlayHintsSink, editor: Editor) {
return ReadAction.run<Exception> {
val info = KubernetesTypeInfo.create(file) ?: return@run
val element = file.topLevelValue ?: return@run
Base64Presentations.create(element, info, sink, editor)?.create()
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
@file:Suppress("UnstableApiUsage")
package com.redhat.devtools.intellij.kubernetes.editor.inlay
package com.redhat.devtools.intellij.kubernetes.editor.inlay.base64

import com.intellij.codeInsight.hints.InlayHintsSink
import com.intellij.codeInsight.hints.presentation.InlayPresentation
Expand All @@ -18,12 +18,12 @@ import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo
import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo
import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon
import com.redhat.devtools.intellij.kubernetes.editor.inlay.Base64Presentations.InlayPresentationsFactory
import com.redhat.devtools.intellij.kubernetes.editor.inlay.Base64Presentations.create
import com.redhat.devtools.intellij.kubernetes.editor.inlay.base64.Base64Presentations.InlayPresentationsFactory
import com.redhat.devtools.intellij.kubernetes.editor.inlay.base64.Base64Presentations.create
import com.redhat.devtools.intellij.kubernetes.editor.util.getBinaryData
import com.redhat.devtools.intellij.kubernetes.editor.util.getData
import com.redhat.devtools.intellij.kubernetes.editor.util.getDataValue
import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource
import com.redhat.devtools.intellij.kubernetes.model.util.trimWithEllipsis
import org.jetbrains.concurrency.runAsync
Expand All @@ -40,15 +40,15 @@ object Base64Presentations {
private const val SECRET_RESOURCE_KIND = "Secret"
private const val CONFIGMAP_RESOURCE_KIND = "ConfigMap"

fun create(content: PsiElement, info: KubernetesResourceInfo, sink: InlayHintsSink, editor: Editor): InlayPresentationsFactory? {
fun create(element: PsiElement, info: KubernetesTypeInfo, sink: InlayHintsSink, editor: Editor): InlayPresentationsFactory? {
return when {
isKubernetesResource(SECRET_RESOURCE_KIND, info) -> {
val data = getData(content) ?: return null
val data = getDataValue(element) ?: return null
StringPresentationsFactory(data, sink, editor)
}

isKubernetesResource(CONFIGMAP_RESOURCE_KIND, info) -> {
val binaryData = getBinaryData(content) ?: return null
val binaryData = getBinaryData(element) ?: return null
BinaryPresentationsFactory(binaryData, sink, editor)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.intellij.kubernetes.editor.inlay
package com.redhat.devtools.intellij.kubernetes.editor.inlay.base64

import com.intellij.psi.PsiElement
import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,26 @@ import com.intellij.json.psi.JsonFile
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonValue
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.util.text.Strings
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiNamedElement
import com.redhat.devtools.intellij.common.validation.KubernetesResourceInfo
import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo
import com.redhat.devtools.intellij.kubernetes.editor.ResourceEditor
import org.jetbrains.yaml.YAMLElementGenerator
import org.jetbrains.yaml.YAMLUtil
import org.jetbrains.yaml.psi.YAMLFile
import org.jetbrains.yaml.psi.YAMLDocument
import org.jetbrains.yaml.psi.YAMLKeyValue
import org.jetbrains.yaml.psi.YAMLMapping
import org.jetbrains.yaml.psi.YAMLPsiElement
import org.jetbrains.yaml.psi.YAMLSequence
import org.jetbrains.yaml.psi.YAMLValue
import java.util.Base64
import java.util.*


private const val KEY_METADATA = "metadata"
private const val KEY_DATA = "data"
Expand Down Expand Up @@ -63,13 +65,13 @@ fun hasKubernetesResource(file: VirtualFile?, project: Project): Boolean {
* @param resourceInfo the resource info to inspect
*/
fun isKubernetesResource(resourceInfo: KubernetesResourceInfo?): Boolean {
return resourceInfo?.typeInfo?.apiGroup?.isNotBlank() ?: false
&& resourceInfo?.typeInfo?.kind?.isNotBlank() ?: false
return resourceInfo?.apiGroup?.isNotBlank() ?: false
&& resourceInfo?.kind?.isNotBlank() ?: false
}

fun isKubernetesResource(kind: String, resourceInfo: KubernetesResourceInfo?): Boolean {
return resourceInfo?.typeInfo?.apiGroup?.isNotBlank() ?: false
&& kind == resourceInfo?.typeInfo?.kind
fun isKubernetesResource(kind: String, info: KubernetesTypeInfo?): Boolean {
return info?.apiGroup?.isNotBlank() != null
&& kind == info.kind
}

/**
Expand All @@ -86,139 +88,29 @@ fun getKubernetesResourceInfo(file: VirtualFile?, project: Project): KubernetesR
return try {
ReadAction.compute<KubernetesResourceInfo, RuntimeException> {
val psiFile = PsiManager.getInstance(project).findFile(file) ?: return@compute null
KubernetesResourceInfo.extractMeta(psiFile)
KubernetesResourceInfo.create(psiFile)
}
} catch (e: RuntimeException) {
null
}
}

/**
* Sets or creates the given [resourceVersion] in the given document for the given [PsiDocumentManager] and [Project].
* The document is **not** committed to allow further modifications before a commit to happen.
* Does nothing if the resource version or the document is `null`.
*
* @param resourceVersion the resource version to set/create
* @param document the document to set the resource version to
* @param manager the [PsiDocumentManager] to use for the operation
* @param project the [Project] to use
*/
fun setResourceVersion(resourceVersion: String?, document: Document?, manager: PsiDocumentManager, project: Project) {
if (resourceVersion == null
|| document == null) {
return
}
val metadata = getMetadata(document, manager) ?: return
createOrUpdateResourceVersion(resourceVersion, metadata, project)
}

private fun createOrUpdateResourceVersion(resourceVersion: String, metadata: PsiElement, project: Project) {
when (metadata) {
is YAMLKeyValue -> createOrUpdateResourceVersion(resourceVersion, metadata, project)
is JsonProperty -> createOrUpdateResourceVersion(resourceVersion, metadata, project)
}
}

private fun createOrUpdateResourceVersion(resourceVersion: String, metadata: JsonProperty, project: Project) {
val metadataObject = metadata.value ?: return
val generator = JsonElementGenerator(project)
val version = generator.createProperty(KEY_RESOURCE_VERSION, "\"$resourceVersion\"")
val existingVersion = getResourceVersion(metadata)
if (existingVersion != null) {
metadataObject.addAfter(version, existingVersion)
existingVersion.delete()
} else {
metadataObject.addBefore(generator.createComma(), metadataObject.lastChild)
metadataObject.addBefore(version, metadataObject.lastChild)
}
}

private fun createOrUpdateResourceVersion(resourceVersion: String, metadata: YAMLKeyValue, project: Project) {
val metadataObject = metadata.value ?: return
val existingVersion = getResourceVersion(metadata)
val generator = YAMLElementGenerator.getInstance(project)
val version = generator.createYamlKeyValue(KEY_RESOURCE_VERSION, "\"$resourceVersion\"")
if (existingVersion != null) {
existingVersion.setValue(version.value!!)
} else {
metadataObject.add(generator.createEol())
metadataObject.add(generator.createIndent(YAMLUtil.getIndentToThisElement(metadataObject)))
metadataObject.add(version)
}
}

fun getContent(element: PsiElement): PsiElement? {
if (element !is PsiFile) {
return null
}
return getContent(element)
}

private fun getContent(file: PsiFile): PsiElement? {
return when (file) {
is YAMLFile -> {
if (file.documents == null
|| file.documents.isEmpty()) {
return null
}
file.documents[0].topLevelValue
}
is JsonFile ->
file.topLevelValue
else -> null
}
}

fun getMetadata(document: Document?, psi: PsiDocumentManager): PsiElement? {
if (document == null) {
return null
}
val file = psi.getPsiFile(document) ?: return null
val content = getContent(file) ?: return null
return getMetadata(content)
}

/**
* Returns the [PsiElement] named "metadata" within the children of the given [PsiElement].
* Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise.
*
* @param element the PsiElement whose "metadata" child should be found.
* @return the PsiElement named "metadata"
*/
private fun getMetadata(content: PsiElement): PsiElement? {
return when (content) {
is YAMLValue ->
content.children
.filterIsInstance<YAMLKeyValue>()
.find { it.name == KEY_METADATA }
is JsonValue ->
content.children.toList()
.filterIsInstance<JsonProperty>()
.find { it.name == KEY_METADATA }
else ->
null
}
}

/**
* Returns the [PsiElement] named "data" within the children of the given [PsiElement].
* Only [YAMLKeyValue] and [JsonProperty] are supported. Returns `null` otherwise.
*
* @param element the PsiElement whose "data" child should be found.
* @return the PsiElement named "data"
*/
fun getData(element: PsiElement): PsiElement? {
return when (element) {
is YAMLPsiElement ->
element.children
.filterIsInstance<YAMLKeyValue>()
.find { it.name == KEY_DATA }
?.value
is JsonElement ->
element.children.toList()
.filterIsInstance<JsonProperty>()
.find { it.name == KEY_DATA }
?.value
fun getDataValue(element: PsiElement): PsiElement? {
val dataElement = element.children
.filterIsInstance<PsiNamedElement>()
.find { it.name == KEY_DATA }
return when (dataElement) {
is YAMLKeyValue ->
dataElement.value
is JsonProperty ->
dataElement.value
else ->
null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ object TelemetryService {
}

fun sendTelemetry(info: KubernetesResourceInfo?, telemetry: TelemetryMessageBuilder.ActionMessage) {
telemetry.property(PROP_RESOURCE_KIND, kindOrUnknown(info?.typeInfo)).send()
telemetry.property(PROP_RESOURCE_KIND, kindOrUnknown(info?.kind, info?.apiGroup)).send()
}

fun getKinds(resources: Collection<HasMetadata>): String {
Expand All @@ -69,16 +69,13 @@ object TelemetryService {
}

private fun kindOrUnknown(kind: ResourceKind<*>?): String {
return if (kind != null) {
"${kind.version}/${kind.kind}"
} else {
"unknown"
}
return kindOrUnknown(kind?.kind, kind?.version);
}

private fun kindOrUnknown(info: KubernetesTypeInfo?): String {
return if (info != null) {
"${info.apiGroup}/${info.kind}"
private fun kindOrUnknown(kind: String?, apiGroup: String?): String {
return if (kind != null
&& apiGroup != null) {
"${kind}/${apiGroup}"
} else {
"unknown"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class KubernetesSchemaProvider(
if (psiFile == null) {
false
} else {
val fileInfo = KubernetesTypeInfo.extractMeta(psiFile)
val fileInfo = KubernetesTypeInfo.create(psiFile)
info == fileInfo
}
})
Expand Down
Loading
Loading