Skip to content

Commit 5007fd5

Browse files
committed
Base k8s schema resolution off of GroupVersionKind
This should improve validation and completion of Kubernetes resources drastically. Fixes #1213 Signed-off-by: David Thompson <davthomp@redhat.com>
1 parent 7c1ba50 commit 5007fd5

File tree

6 files changed

+275
-52
lines changed

6 files changed

+275
-52
lines changed

src/languageservice/services/crdUtil.ts renamed to src/languageservice/services/k8sSchemaUtil.ts

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,82 @@
1-
import { SingleYAMLDocument } from '../parser/yamlParser07';
21
import { JSONDocument } from '../parser/jsonDocument';
2+
import { SingleYAMLDocument } from '../parser/yamlParser07';
33

44
import { ResolvedSchema } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';
55
import { JSONSchema } from '../jsonSchema';
6+
import { BASE_KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls';
67

78
/**
8-
* Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document.
9-
* If there is no definition for the GVK in the main kubernetes schema,
10-
* the schema is then retrieved from the CRD catalog.
11-
* Public for testing purpose, not part of the API.
12-
* @param doc
13-
* @param crdCatalogURI The URL of the CRD catalog to retrieve the schema from
14-
* @param kubernetesSchema The main kubernetes schema, if it includes a definition for the GVK it will be used
9+
* Attempt to retrieve the schema for a given YAML document based on the Kubernetes GroupVersionKind (GVK).
10+
*
11+
* First, checks for a schema for a matching builtin resource, then it checks for a schema for a CRD.
12+
*
13+
* @param doc the yaml document being validated
14+
* @param kubernetesSchema the resolved copy of the Kubernetes builtin
15+
* @param crdCatalogURI the catalog uri to use to find schemas for custom resource definitions
16+
* @returns a schema uri, or undefined if no specific schema can be identified
1517
*/
16-
export function autoDetectKubernetesSchemaFromDocument(
18+
export function autoDetectKubernetesSchema(
1719
doc: SingleYAMLDocument | JSONDocument,
18-
crdCatalogURI: string,
19-
kubernetesSchema: ResolvedSchema
20+
kubernetesSchema: ResolvedSchema,
21+
crdCatalogURI: string
2022
): string | undefined {
21-
const res = getGroupVersionKindFromDocument(doc);
22-
if (!res) {
23+
const gvk = getGroupVersionKindFromDocument(doc);
24+
if (!gvk || !gvk.group || !gvk.version || !gvk.kind) {
2325
return undefined;
2426
}
25-
const { group, version, kind } = res;
26-
if (!group || !version || !kind) {
27-
return undefined;
27+
const builtinResource = autoDetectBuiltinResource(gvk, kubernetesSchema);
28+
if (builtinResource) {
29+
return builtinResource;
2830
}
31+
const customResource = autoDetectCustomResource(gvk, crdCatalogURI);
32+
if (customResource) {
33+
return customResource;
34+
}
35+
return undefined;
36+
}
2937

38+
function autoDetectBuiltinResource(gvk: GroupVersionKind, kubernetesSchema: ResolvedSchema): string | undefined {
39+
const { group, version, kind } = gvk;
40+
41+
const groupWithoutK8sIO = group.replace('.k8s.io', '').replace('rbac.authorization', 'rbac');
42+
const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`;
3043
const k8sSchema: JSONSchema = kubernetesSchema.schema;
31-
const kubernetesBuildIns: string[] = (k8sSchema.oneOf || [])
44+
const matchingBuiltin: string | undefined = (k8sSchema.oneOf || [])
3245
.map((s) => {
3346
if (typeof s === 'boolean') {
3447
return undefined;
3548
}
3649
return s._$ref || s.$ref;
3750
})
38-
.filter((ref) => ref)
39-
.map((ref) => ref.replace('_definitions.json#/definitions/', '').toLowerCase());
40-
const groupWithoutK8sIO = group.replace('.k8s.io', '').replace('rbac.authorization', 'rbac');
41-
const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`;
51+
.find((ref) => {
52+
if (!ref) {
53+
return false;
54+
}
55+
const lowercaseRef = ref.replace('_definitions.json#/definitions/', '').toLowerCase();
56+
return lowercaseRef === k8sTypeName;
57+
});
4258

43-
if (kubernetesBuildIns.includes(k8sTypeName)) {
44-
return undefined;
59+
if (matchingBuiltin) {
60+
return BASE_KUBERNETES_SCHEMA_URL + matchingBuiltin;
4561
}
4662

63+
return undefined;
64+
}
65+
66+
/**
67+
* Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document.
68+
* If there is no definition for the GVK in the main kubernetes schema,
69+
* the schema is then retrieved from the CRD catalog.
70+
* Public for testing purpose, not part of the API.
71+
* @param doc
72+
* @param crdCatalogURI The URL of the CRD catalog to retrieve the schema from
73+
*/
74+
export function autoDetectCustomResource(gvk: GroupVersionKind, crdCatalogURI: string): string | undefined {
75+
const { group, version, kind } = gvk;
76+
77+
const groupWithoutK8sIO = group.replace('.k8s.io', '').replace('rbac.authorization', 'rbac');
78+
const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`;
79+
4780
if (k8sTypeName.includes('openshift.io')) {
4881
return `${crdCatalogURI}/openshift/v4.15-strict/${kind.toLowerCase()}_${group.toLowerCase()}_${version.toLowerCase()}.json`;
4982
}
@@ -52,14 +85,18 @@ export function autoDetectKubernetesSchemaFromDocument(
5285
return schemaURL;
5386
}
5487

88+
type GroupVersionKind = {
89+
group: string;
90+
version: string;
91+
kind: string;
92+
};
93+
5594
/**
5695
* Retrieve the group, version and kind from the document.
5796
* Public for testing purpose, not part of the API.
5897
* @param doc
5998
*/
60-
export function getGroupVersionKindFromDocument(
61-
doc: SingleYAMLDocument | JSONDocument
62-
): { group: string; version: string; kind: string } | undefined {
99+
export function getGroupVersionKindFromDocument(doc: SingleYAMLDocument | JSONDocument): GroupVersionKind | undefined {
63100
if (doc instanceof SingleYAMLDocument) {
64101
try {
65102
const rootJSON = doc.root.internalNode.toJSON();

src/languageservice/services/yamlSchemaService.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,35 @@
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*--------------------------------------------------------------------------------------------*/
66

7-
import { JSONSchema, JSONSchemaRef, JSONSchemaMap, SchemaDialect } from '../jsonSchema';
8-
import { SchemaPriority, SchemaRequestService, WorkspaceContextService } from '../yamlLanguageService';
9-
import { SettingsState } from '../../yamlSettings';
107
import {
11-
UnresolvedSchema,
12-
ResolvedSchema,
8+
ISchemaContributions,
139
JSONSchemaService,
10+
ResolvedSchema,
1411
SchemaDependencies,
15-
ISchemaContributions,
1612
SchemaHandle,
13+
UnresolvedSchema,
1714
} from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';
15+
import { SettingsState } from '../../yamlSettings';
16+
import { JSONSchema, JSONSchemaMap, JSONSchemaRef, SchemaDialect } from '../jsonSchema';
17+
import { SchemaPriority, SchemaRequestService, WorkspaceContextService } from '../yamlLanguageService';
1818

19-
import { URI } from 'vscode-uri';
2019
import * as l10n from '@vscode/l10n';
21-
import { SingleYAMLDocument } from '../parser/yamlParser07';
22-
import { JSONDocument } from '../parser/jsonDocument';
2320
import * as path from 'path';
24-
import { getSchemaFromModeline } from './modelineUtil';
21+
import { URI } from 'vscode-uri';
2522
import { JSONSchemaDescriptionExt } from '../../requestTypes';
23+
import { JSONDocument } from '../parser/jsonDocument';
24+
import { SingleYAMLDocument } from '../parser/yamlParser07';
2625
import { SchemaVersions } from '../yamlTypes';
26+
import { getSchemaFromModeline } from './modelineUtil';
2727

28-
import { parse } from 'yaml';
29-
import * as Json from 'jsonc-parser';
3028
import Ajv, { DefinedError, type AnySchemaObject, type ValidateFunction } from 'ajv';
3129
import Ajv4 from 'ajv-draft-04';
3230
import Ajv2019 from 'ajv/dist/2019';
3331
import Ajv2020 from 'ajv/dist/2020';
34-
import { autoDetectKubernetesSchemaFromDocument } from './crdUtil';
32+
import * as Json from 'jsonc-parser';
33+
import { parse } from 'yaml';
3534
import { CRD_CATALOG_URL, KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls';
35+
import { autoDetectKubernetesSchema } from './k8sSchemaUtil';
3636

3737
const ajv4 = new Ajv4({ allErrors: true });
3838
const ajv7 = new Ajv({ allErrors: true });
@@ -992,10 +992,10 @@ export class YAMLSchemaService extends JSONSchemaService {
992992
if (!k8sAllSchema) {
993993
k8sAllSchema = await this.getResolvedSchema(KUBERNETES_SCHEMA_URL);
994994
}
995-
const kubeSchema = autoDetectKubernetesSchemaFromDocument(
995+
const kubeSchema = autoDetectKubernetesSchema(
996996
doc,
997-
this.yamlSettings.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL,
998-
k8sAllSchema
997+
k8sAllSchema,
998+
this.yamlSettings.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL
999999
);
10001000
if (kubeSchema) {
10011001
schemas.push(kubeSchema);

src/languageservice/utils/schemaUrls.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { JSONSchema, JSONSchemaRef } from '../jsonSchema';
55
import { isBoolean } from './objects';
66
import { isRelativePath, relativeToAbsolutePath } from './paths';
77

8-
export const KUBERNETES_SCHEMA_URL =
9-
'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.32.1-standalone-strict/all.json';
8+
export const BASE_KUBERNETES_SCHEMA_URL =
9+
'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.32.1-standalone-strict/';
10+
export const KUBERNETES_SCHEMA_URL = BASE_KUBERNETES_SCHEMA_URL + 'all.json';
1011
export const JSON_SCHEMASTORE_URL = 'https://www.schemastore.org/api/json/catalog.json';
1112
export const CRD_CATALOG_URL = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main';
1213

test/schema.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings';
2323
import { Diagnostic, MarkupContent, Position } from 'vscode-languageserver-types';
2424
import { LineCounter } from 'yaml';
2525
import { getSchemaFromModeline } from '../src/languageservice/services/modelineUtil';
26-
import { getGroupVersionKindFromDocument } from '../src/languageservice/services/crdUtil';
26+
import { getGroupVersionKindFromDocument } from '../src/languageservice/services/k8sSchemaUtil';
2727

2828
const requestServiceMock = function (uri: string): Promise<string> {
2929
return Promise.reject<string>(`Resource ${uri} not found.`);

test/schemaValidation.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,10 @@ obj:
12121212
});
12131213

12141214
describe('Test with custom kubernetes schemas', function () {
1215+
after(() => {
1216+
languageSettingsSetup.languageSettings.schemas.pop();
1217+
languageService.configure(languageSettingsSetup.languageSettings);
1218+
});
12151219
it('Test that properties that match multiple enums get validated properly', (done) => {
12161220
languageService.configure(languageSettingsSetup.withKubernetes().languageSettings);
12171221
yamlSettings.specificValidatorPaths = ['*.yml', '*.yaml'];
@@ -1257,6 +1261,40 @@ obj:
12571261
})
12581262
.then(done, done);
12591263
});
1264+
1265+
it('Test that it validates against the correct schema based on the GroupVersionKind', (done) => {
1266+
languageService.configure(
1267+
languageSettingsSetup.withKubernetes().withSchemaFileMatch({ uri: KUBERNETES_SCHEMA_URL, fileMatch: ['*.yml', '*.yaml'] })
1268+
.languageSettings
1269+
);
1270+
yamlSettings.specificValidatorPaths = ['*.yml', '*.yaml'];
1271+
const content = `apiVersion: autoscaling/v2
1272+
kind: HorizontalPodAutoscaler
1273+
metadata:
1274+
name: foo
1275+
spec:
1276+
foo: bar
1277+
scaleTargetRef:
1278+
apiVersion: apps/v1
1279+
kind: Deployment
1280+
name: foo
1281+
minReplicas: 2
1282+
maxReplicas: 3
1283+
metrics:
1284+
- type: Resource
1285+
resource:
1286+
name: cpu
1287+
target:
1288+
type: Utilization
1289+
averageUtilization: 80`;
1290+
const validator = parseSetup(content);
1291+
validator
1292+
.then(function (result) {
1293+
assert.equal(result.length, 1);
1294+
assert.equal(result[0].message, `Property foo is not allowed.`);
1295+
})
1296+
.then(done, done);
1297+
});
12601298
});
12611299

12621300
// https://github.com/redhat-developer/yaml-language-server/issues/118

0 commit comments

Comments
 (0)