Skip to content

Commit 236ad2e

Browse files
committed
feat: validate using LocalInspectionTool (#861)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent 8d15c05 commit 236ad2e

File tree

14 files changed

+1585
-178
lines changed

14 files changed

+1585
-178
lines changed

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ dependencies {
8383
implementation(libs.kubernetes.httpclient.okhttp)
8484
implementation(libs.jackson.core)
8585
implementation(libs.commons.lang3)
86+
implementation(libs.snakeyaml)
87+
implementation(libs.json)
88+
implementation(libs.json.schema)
8689

8790
// for unit tests
8891
testImplementation(libs.assertj.core)

gradle/libs.versions.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ devtools-common-ui-test = "0.4.4-SNAPSHOT"
1111
junit-platform = "1.11.3"
1212
junit-jupiter = "5.11.3"
1313
gson = "2.8.9"
14+
snakeyaml = "2.2"
15+
json = "20250517"
16+
everit-json-schema = "1.14.6"
1417

1518
# plugins
1619
gradleIntelliJPlugin = "2.2.1"
@@ -36,6 +39,9 @@ junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.r
3639
junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit-jupiter" }
3740
junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit-jupiter" }
3841
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
42+
snakeyaml = { group = "org.yaml", name = "snakeyaml", version.ref = "snakeyaml" }
43+
json = { group = "org.json", name = "json", version.ref = "json" }
44+
json-schema = {group="com.github.erosb", name="everit-json-schema", version.ref="everit-json-schema"}
3945

4046
[plugins]
4147
gradleIntelliJPlugin = { id = "org.jetbrains.intellij.platform", version.ref = "gradleIntelliJPlugin" }

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/util/ResourcePsiElementUtils.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.intellij.json.psi.JsonFile
1414
import com.intellij.json.psi.JsonObject
1515
import com.intellij.json.psi.JsonProperty
1616
import com.intellij.json.psi.JsonValue
17+
import com.intellij.openapi.util.text.StringUtil
1718
import com.intellij.psi.PsiElement
1819
import com.intellij.psi.PsiFile
1920
import com.redhat.devtools.intellij.common.validation.KubernetesTypeInfo
@@ -23,6 +24,8 @@ import org.jetbrains.yaml.psi.YAMLKeyValue
2324
import org.jetbrains.yaml.psi.YAMLMapping
2425
import org.jetbrains.yaml.psi.YAMLValue
2526

27+
private const val KEY_API_VERSION = "apiVersion"
28+
private const val KEY_KIND = "kind"
2629
private const val KEY_METADATA = "metadata"
2730
private const val KEY_NAME = "name"
2831
private const val KEY_LABELS = "labels"
@@ -105,6 +108,35 @@ fun JsonObject.getResourceName(): JsonValue? {
105108
?.value
106109
}
107110

111+
fun PsiElement.getKind(): PsiElement? {
112+
return when(this) {
113+
is YAMLDocument -> (this.topLevelValue as? YAMLMapping)?.getKind()
114+
is YAMLMapping -> getKind()
115+
is JsonObject -> getKind()
116+
else -> null
117+
}
118+
}
119+
120+
fun JsonObject.getKind(): JsonValue? {
121+
return this.findProperty(KEY_KIND)
122+
?.value
123+
}
124+
125+
fun YAMLMapping.getKind(): YAMLValue? {
126+
return this.getKeyValueByKey(KEY_KIND)
127+
?.value
128+
}
129+
130+
fun JsonObject.getApiVersion(): JsonValue? {
131+
return this.findProperty(KEY_API_VERSION)
132+
?.value
133+
}
134+
135+
fun YAMLMapping.getApiVersion(): YAMLValue? {
136+
return this.getKeyValueByKey(KEY_API_VERSION)
137+
?.value
138+
}
139+
108140
fun JsonObject.getMetadata(): JsonObject? {
109141
return this.findProperty(KEY_METADATA)
110142
?.value as? JsonObject
@@ -263,3 +295,11 @@ fun PsiElement.getDataValue(): PsiElement? {
263295
}
264296
}
265297

298+
fun unquote(value: String?): String? {
299+
return if (value.isNullOrBlank()) {
300+
return value
301+
} else {
302+
StringUtil.unquoteString(value)
303+
}
304+
}
305+
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.validation
12+
13+
import com.intellij.openapi.diagnostic.logger
14+
import io.fabric8.kubernetes.client.utils.ApiVersionUtil
15+
import java.io.IOException
16+
import java.io.InputStream
17+
import java.util.concurrent.ConcurrentHashMap
18+
19+
object KubernetesSchema {
20+
private val cache = ConcurrentHashMap<String, String>()
21+
22+
/**
23+
* downloaded from https://github.com/yannh/kubernetes-json-schema/tree/master/v1.31.10-standalone-strict
24+
*/
25+
private const val SCHEMA_BASE_PATH = "/schemas/k8s.io"
26+
27+
fun get(kind: String, apiVersion: String): String? {
28+
if (kind.isBlank()
29+
|| apiVersion.isBlank()) {
30+
logger<KubernetesSchema>().debug("Invalid parameters: kind='$kind', apiVersion='$apiVersion'")
31+
return null
32+
}
33+
34+
val schemaKey = "$apiVersion/$kind"
35+
return cache[schemaKey] ?: run {
36+
val schema = load(kind, apiVersion)
37+
if (schema != null) {
38+
cache[schemaKey] = schema
39+
}
40+
schema
41+
}
42+
}
43+
44+
fun clearCache() {
45+
cache.clear()
46+
}
47+
48+
private fun load(kind: String, apiVersion: String): String? {
49+
// Try different naming patterns to find the schema file
50+
val possibleFileNames = getPossibleFileNames(kind, apiVersion)
51+
52+
return possibleFileNames.asSequence()
53+
.mapNotNull { fileName ->
54+
load(fileName)
55+
}
56+
.firstOrNull()
57+
.also { schema ->
58+
if (schema == null) {
59+
logger<KubernetesSchema>().warn("No schema found for kind: '$kind' with apiVersion: '$apiVersion'")
60+
}
61+
}
62+
}
63+
64+
private fun load(fileName: String): String? {
65+
val resourcePath = "$SCHEMA_BASE_PATH/$fileName"
66+
logger<KubernetesSchema>().debug("Trying to load schema from $resourcePath")
67+
68+
return try {
69+
return loadSchema(resourcePath)
70+
?: loadSchema(resourcePath.removePrefix("/"))
71+
} catch (e: IOException) {
72+
logger<KubernetesSchema>().debug("Failed to load schema from $resourcePath", e)
73+
null
74+
}
75+
}
76+
77+
private fun loadSchema(path: String): String? {
78+
val inputStream = KubernetesSchema::class.java.getResourceAsStream(path)
79+
return inputStream?.use { stream ->
80+
val schema = stream.readBytes().toString(Charsets.UTF_8)
81+
logger<KubernetesSchema>().info("Successfully loaded schema from $path")
82+
schema
83+
}
84+
}
85+
86+
/**
87+
* Returns possible file names for a given kind and apiVersion.
88+
* This function attempts to generate several possible file names based on the
89+
* provided `kind` and `apiVersion`. The generated names are ordered by likelihood
90+
* of a match, starting with the most specific (including group and version) and becoming more general.
91+
*
92+
* @param kind The kind of the Kubernetes resource (e.g., "Pod", "Deployment").
93+
* @param apiVersion The apiVersion of the Kubernetes resource (e.g., "v1", "apps/v1").
94+
* @return A list of possible schema file names, ordered by specificity.
95+
*/
96+
internal fun getPossibleFileNames(kind: String, apiVersion: String): List<String> {
97+
val kindLower = kind.lowercase()
98+
val fileNames = mutableListOf<String>()
99+
100+
// Parse apiVersion to extract group and version
101+
val group = ApiVersionUtil.trimGroupOrNull(apiVersion) ?: ""
102+
val version = ApiVersionUtil.trimVersion(apiVersion)
103+
104+
// Pattern 1: kind-group-version.json (e.g., deployment-apps-v1.json)
105+
if (group.isNotEmpty()) {
106+
fileNames.add("$kindLower-$group-$version.json")
107+
}
108+
109+
// Pattern 2: kind-version.json (e.g., pod-v1.json)
110+
if (version.isNotEmpty()) {
111+
fileNames.add("$kindLower-$version.json")
112+
}
113+
114+
// Pattern 3: kind.json (e.g., deployment.json)
115+
fileNames.add("$kindLower.json")
116+
return fileNames
117+
}
118+
}

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/validation/KubernetesSchemaProvider.kt

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/validation/KubernetesSchemaProviderFactory.kt

Lines changed: 0 additions & 107 deletions
This file was deleted.

0 commit comments

Comments
 (0)