Skip to content

Commit

Permalink
feat: show/allow setting decoded base64 values in Secrets, ConfigMaps (
Browse files Browse the repository at this point in the history
…#663)

Signed-off-by: Andre Dietisheim <[email protected]>
  • Loading branch information
adietish committed Feb 14, 2024
1 parent 0a05c6a commit 4961ec5
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*******************************************************************************
* 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.balloon

import com.intellij.openapi.editor.Editor
import com.intellij.openapi.ui.ComponentValidator
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.ui.popup.JBPopupListener
import com.intellij.openapi.ui.popup.LightweightWindowEvent
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.ExpirableRunnable
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.wm.IdeFocusManager
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.util.ui.JBUI
import org.jetbrains.annotations.NotNull
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
import java.util.function.BiConsumer
import java.util.function.Supplier
import javax.swing.JPanel
import javax.swing.JTextField
import kotlin.math.max


class StringInputBalloon(@NotNull private val onValidValue: (String) -> Unit, @NotNull private val editor: Editor) {

companion object {
private const val MAX_WIDTH = 220.0
}

private var isValid = false

fun show(event: MouseEvent) {
val (field, balloon) = create()
balloon.show(RelativePoint(event), Balloon.Position.above)
val focusManager = IdeFocusManager.getInstance(editor.project)
focusManager.doWhenFocusSettlesDown(onFocused(focusManager, field))
}

private fun create(): Pair<JBTextField, Balloon> {
val panel = JPanel(BorderLayout())
val label = JBLabel("Value:")
label.border = JBUI.Borders.empty(0, 3, 0, 1)
panel.add(label, BorderLayout.WEST)
val field = JBTextField()
field.preferredSize = Dimension(
max(MAX_WIDTH, field.preferredSize.width.toDouble()).toInt(),
field.preferredSize.height
)
panel.add(field, BorderLayout.CENTER)
val balloon = createBalloon(panel)
val disposable = Disposer.newDisposable()
Disposer.register(balloon, disposable)
ComponentValidator(disposable)
.withValidator(ValueValidator(field))
.installOn(field)
.andRegisterOnDocumentListener(field)
.revalidate()
val keyListener = onKeyPressed(field, balloon)
field.addKeyListener(keyListener)
balloon.addListener(onClosed(field, keyListener))
return Pair(field, balloon)
}

private fun createBalloon(panel: JPanel): Balloon {
return JBPopupFactory.getInstance()
.createBalloonBuilder(panel)
.setCloseButtonEnabled(false)
.setBlockClicksThroughBalloon(true)
.setAnimationCycle(0)
.setHideOnKeyOutside(true)
.setHideOnClickOutside(true)
.setFillColor(panel.background)
.createBalloon()
}

private fun onClosed(field: JBTextField, keyListener: KeyAdapter): JBPopupListener {
return object : JBPopupListener {
override fun beforeShown(event: LightweightWindowEvent) {}
override fun onClosed(event: LightweightWindowEvent) {
field.removeKeyListener(keyListener)
}
}
}

private fun onKeyPressed(field: JTextField, balloon: Balloon) = object : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
when (e.keyCode) {
KeyEvent.VK_ESCAPE ->
balloon.hide()
KeyEvent.VK_ENTER ->
if (isValid) {
onValidValue.invoke(field.text)
}
}
}
}

private fun onFocused(focusManager: IdeFocusManager, field: JBTextField): ExpirableRunnable {
return object : ExpirableRunnable {

override fun run() {
focusManager.requestFocus(field, true)
field.selectAll()
}

override fun isExpired(): Boolean {
return false
}
}
}

private inner class ValueValidator(private val field: JTextField) : Supplier<ValidationInfo?> {

override fun get(): ValidationInfo? {
if (!field.isEnabled
|| !field.isVisible
) {
return null
}
return validate(field.text)
}

private fun validate(name: String): ValidationInfo? {
val validation = if (StringUtil.isEmptyOrSpaces(name)) {
ValidationInfo("Provide a value", field).asWarning()
} else {
null
}
this@StringInputBalloon.isValid = (validation == null)
return validation
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
@file:Suppress("UnstableApiUsage")

package com.redhat.devtools.intellij.kubernetes.editor.inlay

import com.intellij.codeInsight.hints.ChangeListener
import com.intellij.codeInsight.hints.FactoryInlayHintsCollector
import com.intellij.codeInsight.hints.ImmediateConfigurable
import com.intellij.codeInsight.hints.InlayHintsCollector
import com.intellij.codeInsight.hints.InlayHintsProvider
import com.intellij.codeInsight.hints.InlayHintsSink
import com.intellij.codeInsight.hints.NoSettings
import com.intellij.codeInsight.hints.SettingsKey
import com.intellij.codeInsight.hints.presentation.PresentationFactory
import com.intellij.json.psi.JsonProperty
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.progress.ModalTaskOwner.project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.suggested.startOffset
import com.intellij.ui.dsl.builder.panel
import com.redhat.devtools.intellij.kubernetes.balloon.StringInputBalloon
import com.redhat.devtools.intellij.kubernetes.editor.util.decodeBase64
import com.redhat.devtools.intellij.kubernetes.editor.util.getContent
import com.redhat.devtools.intellij.kubernetes.editor.util.getData
import com.redhat.devtools.intellij.kubernetes.editor.util.getKubernetesResourceInfo
import com.redhat.devtools.intellij.kubernetes.editor.util.isKubernetesResource
import com.redhat.devtools.intellij.kubernetes.editor.util.setValue
import org.jetbrains.yaml.psi.YAMLKeyValue
import javax.swing.JComponent


internal class Base64InlayHintsProvider : InlayHintsProvider<NoSettings> {

override val key: SettingsKey<NoSettings> = SettingsKey("LSP.hints")
override val name: String = "Kubernetes"
override val previewText: String = "Preview"

override fun createSettings(): NoSettings {
return NoSettings()
}

override fun createConfigurable(settings: NoSettings): ImmediateConfigurable {
return object : ImmediateConfigurable {
override fun createComponent(listener: ChangeListener): JComponent = panel {}

override val mainCheckboxText: String = "Show hints for:"

override val cases: List<ImmediateConfigurable.Case> = emptyList()
}
}

override fun getCollectorFor(
file: PsiFile,
editor: Editor,
settings: NoSettings,
sink: InlayHintsSink
): InlayHintsCollector? {
val project = editor.project ?: return null
val virtualFile = file.virtualFile ?: return null
val info = getKubernetesResourceInfo(virtualFile, project)
if (!isKubernetesResource("Secret", info)
&& !isKubernetesResource("ConfigMap", info)
) {
return null
}
return Collector(editor)
}

private class Collector(editor: Editor) : FactoryInlayHintsCollector(editor) {

override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean {
if (!element.isValid) {
return true
}
val content = getContent(element) ?: return true
val data = getData(content) ?: return true
data.children.toList()
.forEach { child ->
val value = when (child) {
is YAMLKeyValue -> child.value?.text
is JsonProperty -> child.value?.text
else -> null
}
val decoded = decodeBase64(value)
val offset = when (child) {
is YAMLKeyValue -> child.value?.startOffset
is JsonProperty -> child.value?.startOffset
else -> null
} ?: return true
if (decoded != null) {
val factory = PresentationFactory(editor as EditorImpl)
val text = factory.smallText(decoded)
val hover = factory.referenceOnHover(text) { event, translated -> StringInputBalloon(onValidValue(child, editor), editor).show(event) }
val tooltip = factory.withTooltip("Click to change value", hover)
val round = factory.roundWithBackground(tooltip)
sink.addInlineElement(offset, false, round, false)
}
}

return true
}

fun onValidValue(child: PsiElement, editor: Editor): (value: String) -> Unit {
return { value ->
ApplicationManager.getApplication().invokeLater {
WriteCommandAction.runWriteCommandAction(editor.project) {
setValue(value, child, editor.project)
}
}
}
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
******************************************************************************/
package com.redhat.devtools.intellij.kubernetes.editor.util

import com.intellij.json.psi.JsonElement
import com.intellij.json.psi.JsonElementGenerator
import com.intellij.json.psi.JsonFile
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonValue
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.Strings
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
Expand All @@ -27,9 +29,13 @@ import org.jetbrains.yaml.YAMLElementGenerator
import org.jetbrains.yaml.YAMLUtil
import org.jetbrains.yaml.psi.YAMLFile
import org.jetbrains.yaml.psi.YAMLKeyValue
import org.jetbrains.yaml.psi.YAMLPsiElement
import org.jetbrains.yaml.psi.YAMLValue
import org.jetbrains.yaml.psi.impl.YAMLKeyValueKeyManipulator
import java.util.Base64

private const val KEY_METADATA = "metadata"
private const val KEY_DATA = "data"
private const val KEY_RESOURCE_VERSION = "resourceVersion"

/**
Expand Down Expand Up @@ -59,6 +65,11 @@ fun isKubernetesResource(resourceInfo: KubernetesResourceInfo?): Boolean {
&& resourceInfo?.typeInfo?.kind?.isNotBlank() ?: false
}

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

/**
* Returns [KubernetesResourceInfo] for the given file and project. Returns `null` if it could not be retrieved.
*
Expand Down Expand Up @@ -134,6 +145,13 @@ private fun createOrUpdateResourceVersion(resourceVersion: String, metadata: YAM
}
}

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 -> {
Expand All @@ -154,7 +172,7 @@ fun getMetadata(document: Document?, psi: PsiDocumentManager): PsiElement? {
}
val file = psi.getPsiFile(document) ?: return null
val content = getContent(file) ?: return null
return getMetadata(content) ?: return null
return getMetadata(content)
}

private fun getMetadata(content: PsiElement): PsiElement? {
Expand All @@ -172,6 +190,53 @@ private fun getMetadata(content: PsiElement): PsiElement? {
}
}

fun getData(element: PsiElement): PsiElement? {
return when (element) {
is YAMLPsiElement ->
element.children
.filterIsInstance(YAMLKeyValue::class.java)
.find { it.name == KEY_DATA }
?.value
is JsonElement ->
element.children.toList()
.filterIsInstance(JsonProperty::class.java)
.find { it.name == KEY_DATA }
?.value
else ->
null
}
}

fun decodeBase64(value: String?): String? {
if (Strings.isEmptyOrSpaces(value)) {
return null
}
return try {
val bytes = Base64.getDecoder().decode(value)
String(bytes)
} catch (e: IllegalArgumentException) {
null
}
}

fun setValue(value: String, element: PsiElement, project: Project?) {
if (project == null) {
return
}
when (element) {
is YAMLKeyValue -> {
val textElement = YAMLElementGenerator.getInstance(project).createYamlDoubleQuotedString()
textElement.updateText(value)
element.setValue(textElement)
}
is JsonProperty -> {
val textElement = JsonElementGenerator(project).createProperty(element.name, value)
element.parent.addAfter(element, textElement)
element.delete()
}
}
}

private fun getResourceVersion(metadata: YAMLKeyValue): YAMLKeyValue? {
return metadata.value?.children
?.filterIsInstance(YAMLKeyValue::class.java)
Expand Down
Loading

0 comments on commit 4961ec5

Please sign in to comment.