Skip to content

Commit b998e03

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 b998e03

File tree

5 files changed

+108
-52
lines changed

5 files changed

+108
-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/yamlSchemaService.test.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as url from 'url';
1010
import * as SchemaService from '../src/languageservice/services/yamlSchemaService';
1111
import { parse } from '../src/languageservice/parser/yamlParser07';
1212
import { SettingsState } from '../src/yamlSettings';
13-
import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls';
13+
import { BASE_KUBERNETES_SCHEMA_URL, KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls';
1414

1515
const expect = chai.expect;
1616
chai.use(sinonChai);
@@ -416,11 +416,17 @@ spec:
416416
const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings);
417417
service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']);
418418
const resolvedSchema = await service.getSchemaForResource('test.yaml', yamlDock.documents[0]);
419-
expect(resolvedSchema.schema.url).eqls(KUBERNETES_SCHEMA_URL);
419+
expect(resolvedSchema.schema.url).eqls(
420+
BASE_KUBERNETES_SCHEMA_URL + '_definitions.json#/definitions/io.k8s.api.admissionregistration.v1.MutatingWebhook'
421+
);
420422

421423
expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL);
422424
expect(requestServiceMock).calledWithExactly('file:///_definitions.json');
423-
expect(requestServiceMock).calledTwice;
425+
expect(requestServiceMock).calledWithExactly(
426+
BASE_KUBERNETES_SCHEMA_URL + '_definitions.json#/definitions/io.k8s.api.admissionregistration.v1.MutatingWebhook'
427+
);
428+
expect(requestServiceMock).calledWithExactly(BASE_KUBERNETES_SCHEMA_URL + '_definitions.json');
429+
expect(requestServiceMock.callCount).equals(4);
424430
});
425431

426432
it('should not get schema from crd catalog if definition in kubernetes schema (multiple oneOf)', async () => {
@@ -450,11 +456,17 @@ spec:
450456
const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings);
451457
service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']);
452458
const resolvedSchema = await service.getSchemaForResource('test.yaml', yamlDock.documents[0]);
453-
expect(resolvedSchema.schema.url).eqls(KUBERNETES_SCHEMA_URL);
459+
expect(resolvedSchema.schema.url).eqls(
460+
BASE_KUBERNETES_SCHEMA_URL + '_definitions.json#/definitions/io.k8s.api.apps.v1.Deployment'
461+
);
454462

455463
expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL);
456464
expect(requestServiceMock).calledWithExactly('file:///_definitions.json');
457-
expect(requestServiceMock).calledTwice;
465+
expect(requestServiceMock).calledWithExactly(
466+
BASE_KUBERNETES_SCHEMA_URL + '_definitions.json#/definitions/io.k8s.api.apps.v1.Deployment'
467+
);
468+
expect(requestServiceMock).calledWithExactly(BASE_KUBERNETES_SCHEMA_URL + '_definitions.json');
469+
expect(requestServiceMock.callCount).equals(4);
458470
});
459471

460472
it('should not get schema from crd catalog for RBAC-related resources', async () => {
@@ -481,11 +493,17 @@ spec:
481493
const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings);
482494
service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']);
483495
const resolvedSchema = await service.getSchemaForResource('test.yaml', yamlDock.documents[0]);
484-
expect(resolvedSchema.schema.url).eqls(KUBERNETES_SCHEMA_URL);
496+
expect(resolvedSchema.schema.url).eqls(
497+
BASE_KUBERNETES_SCHEMA_URL + '_definitions.json#/definitions/io.k8s.api.rbac.v1.RoleBinding'
498+
);
485499

486500
expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL);
487501
expect(requestServiceMock).calledWithExactly('file:///_definitions.json');
488-
expect(requestServiceMock).calledTwice;
502+
expect(requestServiceMock).calledWithExactly(
503+
BASE_KUBERNETES_SCHEMA_URL + '_definitions.json#/definitions/io.k8s.api.rbac.v1.RoleBinding'
504+
);
505+
expect(requestServiceMock).calledWithExactly(BASE_KUBERNETES_SCHEMA_URL + '_definitions.json');
506+
expect(requestServiceMock.callCount).equals(4);
489507
});
490508
});
491509
});

0 commit comments

Comments
 (0)