From af900c9094bb3150ab4050718011abefee92d52b Mon Sep 17 00:00:00 2001 From: Marius Svechla Date: Mon, 22 Apr 2024 20:04:28 +0200 Subject: [PATCH 1/6] feat: auto-detect Kubernetes schema automatically detect the Kubernetes schema based on the document's GroupVersionKind (GVK) and retrieve the matching schema from the CRD catalog. --- .../handlers/settingsHandlers.ts | 1 + src/languageservice/services/crdUtil.ts | 64 +++++++++++++++++++ .../services/yamlSchemaService.ts | 15 ++++- src/languageservice/yamlLanguageService.ts | 2 +- src/yamlSettings.ts | 2 + test/schema.test.ts | 55 ++++++++++++++++ 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/languageservice/services/crdUtil.ts diff --git a/src/languageserver/handlers/settingsHandlers.ts b/src/languageserver/handlers/settingsHandlers.ts index 95ddba773..cc9e482d8 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -78,6 +78,7 @@ export class SettingsHandler { this.yamlSettings.customTags = settings.yaml.customTags ? settings.yaml.customTags : []; this.yamlSettings.maxItemsComputed = Math.trunc(Math.max(0, Number(settings.yaml.maxItemsComputed))) || 5000; + this.yamlSettings.autoDetectKubernetesSchema = settings.yaml.autoDetectKubernetesSchema; if (settings.yaml.schemaStore) { this.yamlSettings.schemaStoreEnabled = settings.yaml.schemaStore.enable; diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts new file mode 100644 index 000000000..16d770be7 --- /dev/null +++ b/src/languageservice/services/crdUtil.ts @@ -0,0 +1,64 @@ +import { SingleYAMLDocument } from '../parser/yamlParser07'; +import { JSONDocument } from '../parser/jsonParser07'; + +const CRD_URI = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main'; + +/** + * Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document. + * The matching schema is then retrieved from the CRD catalog. + * Public for testing purpose, not part of the API. + * @param doc + */ +export function autoDetectKubernetesSchemaFromDocument(doc: SingleYAMLDocument | JSONDocument): string | undefined { + const res = getGroupVersionKindFromDocument(doc); + if (!res) { + return undefined; + } + + const { group, version, kind } = res; + if (!group || !version || !kind) { + return undefined; + } + + const schemaURL = `${CRD_URI}/${group.toLowerCase()}/${kind.toLowerCase()}_${version.toLowerCase()}.json`; + return schemaURL; +} + +/** + * Retrieve the group, version and kind from the document. + * Public for testing purpose, not part of the API. + * @param doc + */ +export function getGroupVersionKindFromDocument( + doc: SingleYAMLDocument | JSONDocument +): { group: string; version: string; kind: string } | undefined { + if (doc instanceof SingleYAMLDocument) { + try { + const rootJSON = doc.root.internalNode.toJSON(); + if (!rootJSON) { + return undefined; + } + + const groupVersion = rootJSON['apiVersion']; + if (!groupVersion) { + return undefined; + } + + const [group, version] = groupVersion.split('/'); + if (!group || !version) { + return undefined; + } + + const kind = rootJSON['kind']; + if (!kind) { + return undefined; + } + + return { group, version, kind }; + } catch (error) { + console.error('Error parsing YAML document:', error); + return undefined; + } + } + return undefined; +} diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index 453b74eb9..19c56d7ee 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -6,6 +6,7 @@ import { JSONSchema, JSONSchemaMap, JSONSchemaRef } from '../jsonSchema'; import { SchemaPriority, SchemaRequestService, WorkspaceContextService } from '../yamlLanguageService'; +import { SettingsState } from '../../yamlSettings'; import { UnresolvedSchema, ResolvedSchema, @@ -29,6 +30,7 @@ import { SchemaVersions } from '../yamlTypes'; import Ajv, { DefinedError } from 'ajv'; import { getSchemaTitle } from '../utils/schemaUtils'; +import { autoDetectKubernetesSchemaFromDocument } from './crdUtil'; const localize = nls.loadMessageBundle(); @@ -108,6 +110,7 @@ export class YAMLSchemaService extends JSONSchemaService { private filePatternAssociations: JSONSchemaService.FilePatternAssociation[]; private contextService: WorkspaceContextService; private requestService: SchemaRequestService; + private yamlSettings: SettingsState; public schemaPriorityMapping: Map>; private schemaUriToNameAndDescription = new Map(); @@ -115,12 +118,14 @@ export class YAMLSchemaService extends JSONSchemaService { constructor( requestService: SchemaRequestService, contextService?: WorkspaceContextService, - promiseConstructor?: PromiseConstructor + promiseConstructor?: PromiseConstructor, + yamlSettings?: SettingsState ) { super(requestService, contextService, promiseConstructor); this.customSchemaProvider = undefined; this.requestService = requestService; this.schemaPriorityMapping = new Map(); + this.yamlSettings = yamlSettings; } registerCustomSchemaProvider(customSchemaProvider: CustomSchemaProvider): void { @@ -416,6 +421,14 @@ export class YAMLSchemaService extends JSONSchemaService { if (modelineSchema) { return resolveSchemaForResource([modelineSchema]); } + + if (this.yamlSettings && this.yamlSettings.autoDetectKubernetesSchema) { + const kubeSchema = autoDetectKubernetesSchemaFromDocument(doc); + if (kubeSchema) { + return resolveSchemaForResource([kubeSchema]); + } + } + if (this.customSchemaProvider) { return this.customSchemaProvider(resource) .then((schemaUri) => { diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index 539371d8b..a644e4dde 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -189,7 +189,7 @@ export function getLanguageService(params: { yamlSettings?: SettingsState; clientCapabilities?: ClientCapabilities; }): LanguageService { - const schemaService = new YAMLSchemaService(params.schemaRequestService, params.workspaceContext); + const schemaService = new YAMLSchemaService(params.schemaRequestService, params.workspaceContext, null, params.yamlSettings); const completer = new YamlCompletion(schemaService, params.clientCapabilities, yamlDocumentsCache, params.telemetry); const hover = new YAMLHover(schemaService, params.telemetry); const yamlDocumentSymbols = new YAMLDocumentSymbols(schemaService, params.telemetry); diff --git a/src/yamlSettings.ts b/src/yamlSettings.ts index fc0260342..8bf88c3f2 100644 --- a/src/yamlSettings.ts +++ b/src/yamlSettings.ts @@ -32,6 +32,7 @@ export interface Settings { keyOrdering: boolean; maxItemsComputed: number; yamlVersion: YamlVersion; + autoDetectKubernetesSchema: boolean; }; http: { proxy: string; @@ -89,6 +90,7 @@ export class SettingsState { }; keyOrdering = false; maxItemsComputed = 5000; + autoDetectKubernetesSchema = false; // File validation helpers pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; diff --git a/test/schema.test.ts b/test/schema.test.ts index 1747b1c40..43ac1bbc0 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -14,6 +14,7 @@ import { LanguageService, SchemaPriority } from '../src'; import { MarkupContent, Position } from 'vscode-languageserver-types'; import { LineCounter } from 'yaml'; import { getSchemaFromModeline } from '../src/languageservice/services/modelineUtil'; +import { getGroupVersionKindFromDocument } from '../src/languageservice/services/crdUtil'; const requestServiceMock = function (uri: string): Promise { return Promise.reject(`Resource ${uri} not found.`); @@ -701,6 +702,60 @@ describe('JSON Schema', () => { }); }); + describe('Test getGroupVersionKindFromDocument', function () { + it('builtin kubernetes resource group should not get resolved', async () => { + checkReturnGroupVersionKind('apiVersion: v1\nkind: Pod', true, undefined, 'v1', 'Pod'); + }); + + it('custom argo application CRD should get resolved', async () => { + checkReturnGroupVersionKind( + 'apiVersion: argoproj.io/v1alpha1\nkind: Application', + false, + 'argoproj.io', + 'v1alpha1', + 'Application' + ); + }); + + it('custom argo application CRD with whitespace should get resolved', async () => { + checkReturnGroupVersionKind( + 'apiVersion: argoproj.io/v1alpha1\nkind: Application ', + false, + 'argoproj.io', + 'v1alpha1', + 'Application' + ); + }); + + it('custom argo application CRD with other fields should get resolved', async () => { + checkReturnGroupVersionKind( + 'someOtherVal: test\napiVersion: argoproj.io/v1alpha1\nkind: Application\nmetadata:\n name: my-app', + false, + 'argoproj.io', + 'v1alpha1', + 'Application' + ); + }); + + function checkReturnGroupVersionKind( + content: string, + error: boolean, + expectedGroup: string, + expectedVersion: string, + expectedKind: string + ): void { + const yamlDoc = parser.parse(content); + const res = getGroupVersionKindFromDocument(yamlDoc.documents[0]); + if (error) { + assert.strictEqual(res, undefined); + } else { + assert.strictEqual(res.group, expectedGroup); + assert.strictEqual(res.version, expectedVersion); + assert.strictEqual(res.kind, expectedKind); + } + } + }); + describe('Test getSchemaFromModeline', function () { it('simple case', async () => { checkReturnSchemaUrl('# yaml-language-server: $schema=expectedUrl', 'expectedUrl'); From 78d90cf302041cc4dec4f9cf62c96a4a13387baa Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 13:50:20 +0200 Subject: [PATCH 2/6] fix: only auto-detect k8s crd for k8s files --- .../handlers/settingsHandlers.ts | 1 + src/languageservice/services/crdUtil.ts | 9 ++++---- .../services/yamlSchemaService.ts | 21 +++++++++++-------- src/yamlSettings.ts | 2 ++ test/yamlSchemaService.test.ts | 21 +++++++++++++++++++ 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/languageserver/handlers/settingsHandlers.ts b/src/languageserver/handlers/settingsHandlers.ts index 18327333a..a58ef355a 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -79,6 +79,7 @@ export class SettingsHandler { this.yamlSettings.maxItemsComputed = Math.trunc(Math.max(0, Number(settings.yaml.maxItemsComputed))) || 5000; this.yamlSettings.autoDetectKubernetesSchema = settings.yaml.autoDetectKubernetesSchema; + this.yamlSettings.crdCatalogURI = settings.yaml.crdCatalogURI; if (settings.yaml.schemaStore) { this.yamlSettings.schemaStoreEnabled = settings.yaml.schemaStore.enable; diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index 16d770be7..0ec46ebae 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -1,15 +1,16 @@ import { SingleYAMLDocument } from '../parser/yamlParser07'; import { JSONDocument } from '../parser/jsonParser07'; -const CRD_URI = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main'; - /** * Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document. * The matching schema is then retrieved from the CRD catalog. * Public for testing purpose, not part of the API. * @param doc */ -export function autoDetectKubernetesSchemaFromDocument(doc: SingleYAMLDocument | JSONDocument): string | undefined { +export function autoDetectKubernetesSchemaFromDocument( + doc: SingleYAMLDocument | JSONDocument, + crdCatalogURI: string +): string | undefined { const res = getGroupVersionKindFromDocument(doc); if (!res) { return undefined; @@ -20,7 +21,7 @@ export function autoDetectKubernetesSchemaFromDocument(doc: SingleYAMLDocument | return undefined; } - const schemaURL = `${CRD_URI}/${group.toLowerCase()}/${kind.toLowerCase()}_${version.toLowerCase()}.json`; + const schemaURL = `${crdCatalogURI}/${group.toLowerCase()}/${kind.toLowerCase()}_${version.toLowerCase()}.json`; return schemaURL; } diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index 19c56d7ee..9e28efe07 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -31,6 +31,7 @@ import { SchemaVersions } from '../yamlTypes'; import Ajv, { DefinedError } from 'ajv'; import { getSchemaTitle } from '../utils/schemaUtils'; import { autoDetectKubernetesSchemaFromDocument } from './crdUtil'; +import { KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; const localize = nls.loadMessageBundle(); @@ -422,13 +423,6 @@ export class YAMLSchemaService extends JSONSchemaService { return resolveSchemaForResource([modelineSchema]); } - if (this.yamlSettings && this.yamlSettings.autoDetectKubernetesSchema) { - const kubeSchema = autoDetectKubernetesSchemaFromDocument(doc); - if (kubeSchema) { - return resolveSchemaForResource([kubeSchema]); - } - } - if (this.customSchemaProvider) { return this.customSchemaProvider(resource) .then((schemaUri) => { @@ -471,9 +465,18 @@ export class YAMLSchemaService extends JSONSchemaService { return resolveSchema(); } ); - } else { - return resolveSchema(); } + if (this.yamlSettings?.autoDetectKubernetesSchema) { + for (const entry of this.filePatternAssociations) { + if (entry.schemas[0] == KUBERNETES_SCHEMA_URL && entry.matchesPattern(resource)) { + const kubeSchema = autoDetectKubernetesSchemaFromDocument(doc, this.yamlSettings.crdCatalogURI); + if (kubeSchema) { + return resolveSchemaForResource([kubeSchema]); + } + } + } + } + return resolveSchema(); } // Set the priority of a schema in the schema service diff --git a/src/yamlSettings.ts b/src/yamlSettings.ts index 8bf88c3f2..ef8d8a96e 100644 --- a/src/yamlSettings.ts +++ b/src/yamlSettings.ts @@ -33,6 +33,7 @@ export interface Settings { maxItemsComputed: number; yamlVersion: YamlVersion; autoDetectKubernetesSchema: boolean; + crdCatalogURI: string; }; http: { proxy: string; @@ -91,6 +92,7 @@ export class SettingsState { keyOrdering = false; maxItemsComputed = 5000; autoDetectKubernetesSchema = false; + crdCatalogURI = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main'; // File validation helpers pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index 62746a419..603a74c3b 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -8,6 +8,8 @@ import * as sinonChai from 'sinon-chai'; import * as path from 'path'; import * as SchemaService from '../src/languageservice/services/yamlSchemaService'; import { parse } from '../src/languageservice/parser/yamlParser07'; +import { SettingsState } from '../src/yamlSettings'; +import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls'; const expect = chai.expect; chai.use(sinonChai); @@ -140,5 +142,24 @@ describe('YAML Schema Service', () => { expect(requestServiceMock).calledOnceWith('https://json-schema.org/draft-07/schema#'); }); + + it('should handle crd catalog', () => { + const documentContent = 'apiVersion: argoproj.io/v1alpha1\nkind: Application'; + const content = `${documentContent}`; + const yamlDock = parse(content); + + const settings = new SettingsState(); + settings.schemaAssociations = { + kubernetes: ['*.yaml'], + }; + settings.autoDetectKubernetesSchema = true; + const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings); + service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); + service.getSchemaForResource('', yamlDock.documents[0]); + + expect(requestServiceMock).calledOnceWith( + 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/argoproj.io/application_v1alpha1.json' + ); + }); }); }); From aded32488edc526a2c5b0b1629522a572e28d06e Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 17:31:15 +0200 Subject: [PATCH 3/6] feat: check if GVK in main kubeSchema --- src/languageservice/services/crdUtil.ts | 30 +++++++++- .../services/yamlSchemaService.ts | 18 ++++-- src/languageservice/utils/schemaUrls.ts | 1 + src/yamlSettings.ts | 4 +- test/yamlSchemaService.test.ts | 57 ++++++++++++++++++- 5 files changed, 96 insertions(+), 14 deletions(-) diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index 0ec46ebae..6afb45d5a 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -1,26 +1,50 @@ import { SingleYAMLDocument } from '../parser/yamlParser07'; import { JSONDocument } from '../parser/jsonParser07'; +import { ResolvedSchema } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService'; +import { JSONSchema } from 'vscode-json-languageservice/lib/umd/jsonSchema'; +import { KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; + /** * Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document. - * The matching schema is then retrieved from the CRD catalog. + * If there is no definition for the GVK in the main kubernetes schema, + * the schema is then retrieved from the CRD catalog. * Public for testing purpose, not part of the API. * @param doc + * @param crdCatalogURI The URL of the CRD catalog to retrieve the schema from + * @param kubernetesSchema The main kubernetes schema, if it includes a definition for the GVK it will be used */ export function autoDetectKubernetesSchemaFromDocument( doc: SingleYAMLDocument | JSONDocument, - crdCatalogURI: string + crdCatalogURI: string, + kubernetesSchema: ResolvedSchema ): string | undefined { const res = getGroupVersionKindFromDocument(doc); if (!res) { return undefined; } - const { group, version, kind } = res; if (!group || !version || !kind) { return undefined; } + const k8sSchema: JSONSchema = kubernetesSchema.schema; + let kubernetesBuildIns: string[] = k8sSchema.oneOf + .map((s) => { + if (typeof s === 'boolean') { + return undefined; + } + // @ts-ignore + return s._$ref; + }) + .filter((ref) => ref) + .map((ref) => ref.replace('_definitions.json#/definitions/', '').toLowerCase()); + const k8sTypeName = `io.k8s.api.${group.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`; + + if (kubernetesBuildIns.includes(k8sTypeName)) { + return KUBERNETES_SCHEMA_URL; + } + const schemaURL = `${crdCatalogURI}/${group.toLowerCase()}/${kind.toLowerCase()}_${version.toLowerCase()}.json`; return schemaURL; } diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index 9e28efe07..01cf51d59 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -31,7 +31,7 @@ import { SchemaVersions } from '../yamlTypes'; import Ajv, { DefinedError } from 'ajv'; import { getSchemaTitle } from '../utils/schemaUtils'; import { autoDetectKubernetesSchemaFromDocument } from './crdUtil'; -import { KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; +import { CRD_CATALOG_URL, KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; const localize = nls.loadMessageBundle(); @@ -468,11 +468,17 @@ export class YAMLSchemaService extends JSONSchemaService { } if (this.yamlSettings?.autoDetectKubernetesSchema) { for (const entry of this.filePatternAssociations) { - if (entry.schemas[0] == KUBERNETES_SCHEMA_URL && entry.matchesPattern(resource)) { - const kubeSchema = autoDetectKubernetesSchemaFromDocument(doc, this.yamlSettings.crdCatalogURI); - if (kubeSchema) { - return resolveSchemaForResource([kubeSchema]); - } + if (entry.uris && entry.uris[0] == KUBERNETES_SCHEMA_URL && entry.matchesPattern(resource)) { + resolveSchemaForResource([KUBERNETES_SCHEMA_URL]).then((schema) => { + const kubeSchema = autoDetectKubernetesSchemaFromDocument( + doc, + this.yamlSettings.crdCatalogURI ?? CRD_CATALOG_URL, + schema + ); + if (kubeSchema) { + return resolveSchemaForResource([kubeSchema]); + } + }); } } } diff --git a/src/languageservice/utils/schemaUrls.ts b/src/languageservice/utils/schemaUrls.ts index ff474e10e..1dfc975e0 100644 --- a/src/languageservice/utils/schemaUrls.ts +++ b/src/languageservice/utils/schemaUrls.ts @@ -8,6 +8,7 @@ import { isRelativePath, relativeToAbsolutePath } from './paths'; export const KUBERNETES_SCHEMA_URL = 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.4-standalone-strict/all.json'; export const JSON_SCHEMASTORE_URL = 'https://www.schemastore.org/api/json/catalog.json'; +export const CRD_CATALOG_URL = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main'; export function checkSchemaURI( workspaceFolders: WorkspaceFolder[], diff --git a/src/yamlSettings.ts b/src/yamlSettings.ts index ef8d8a96e..3848b2cb6 100644 --- a/src/yamlSettings.ts +++ b/src/yamlSettings.ts @@ -4,7 +4,7 @@ import { ISchemaAssociations } from './requestTypes'; import { URI } from 'vscode-uri'; import { JSONSchema } from './languageservice/jsonSchema'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { JSON_SCHEMASTORE_URL } from './languageservice/utils/schemaUrls'; +import { CRD_CATALOG_URL, JSON_SCHEMASTORE_URL } from './languageservice/utils/schemaUrls'; import { YamlVersion } from './languageservice/parser/yamlParser07'; // Client settings interface to grab settings relevant for the language server @@ -92,7 +92,7 @@ export class SettingsState { keyOrdering = false; maxItemsComputed = 5000; autoDetectKubernetesSchema = false; - crdCatalogURI = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main'; + crdCatalogURI = CRD_CATALOG_URL; // File validation helpers pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index 603a74c3b..1034bf1f4 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -143,7 +143,7 @@ describe('YAML Schema Service', () => { expect(requestServiceMock).calledOnceWith('https://json-schema.org/draft-07/schema#'); }); - it('should handle crd catalog', () => { + it('should handle crd catalog for crd', async () => { const documentContent = 'apiVersion: argoproj.io/v1alpha1\nkind: Application'; const content = `${documentContent}`; const yamlDock = parse(content); @@ -153,13 +153,64 @@ describe('YAML Schema Service', () => { kubernetes: ['*.yaml'], }; settings.autoDetectKubernetesSchema = true; + requestServiceMock = sandbox.fake.resolves( + ` + { + "oneOf": [ { + "$ref": "_definitions.json#/definitions/io.k8s.api.admissionregistration.v1.MutatingWebhook" + }, + ] + } + ` + ); const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings); service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); - service.getSchemaForResource('', yamlDock.documents[0]); + await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + + expect(requestServiceMock).calledWithExactly( + 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.4-standalone-strict/all.json' + ); + expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); - expect(requestServiceMock).calledOnceWith( + expect(requestServiceMock).calledWithExactly( 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/argoproj.io/application_v1alpha1.json' ); + expect(requestServiceMock).calledThrice; + }); + + it('should not get schema from crd catalog if definition in kubernetes schema', async () => { + const documentContent = 'apiVersion: admissionregistration.k8s.io/v1\nkind: MutatingWebhook'; + const content = `${documentContent}`; + const yamlDock = parse(content); + + const settings = new SettingsState(); + settings.schemaAssociations = { + kubernetes: ['*.yaml'], + }; + settings.autoDetectKubernetesSchema = true; + requestServiceMock = sandbox.fake.resolves( + ` + { + "oneOf": [ { + "$ref": "_definitions.json#/definitions/io.k8s.api.admissionregistration.v1.MutatingWebhook" + }, + ] + } + ` + ); + const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings); + service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); + await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + + expect(requestServiceMock).calledWithExactly( + 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.4-standalone-strict/all.json' + ); + expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); + + expect(requestServiceMock).calledWithExactly( + 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.4-standalone-strict/all.json' + ); + expect(requestServiceMock).calledThrice; }); }); }); From 9f813f9bb43b1b6344655f7634ac2cf385fae1d4 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 17:36:37 +0200 Subject: [PATCH 4/6] fix: test --- test/yamlSchemaService.test.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index 2cbda9776..8a5aa3194 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -167,9 +167,7 @@ describe('YAML Schema Service', () => { service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); - expect(requestServiceMock).calledWithExactly( - 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.4-standalone-strict/all.json' - ); + expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL); expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); expect(requestServiceMock).calledWithExactly( @@ -202,14 +200,10 @@ describe('YAML Schema Service', () => { service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); - expect(requestServiceMock).calledWithExactly( - 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.4-standalone-strict/all.json' - ); + expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL); expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); - expect(requestServiceMock).calledWithExactly( - 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.22.4-standalone-strict/all.json' - ); + expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL); expect(requestServiceMock).calledThrice; }); }); From 2918ad43b5049f2650c7f0761c2a65b724f13b90 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 17:51:05 +0200 Subject: [PATCH 5/6] fix: promises were made --- src/languageservice/services/yamlSchemaService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index 7aa253482..ddb739113 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -469,7 +469,7 @@ export class YAMLSchemaService extends JSONSchemaService { if (this.yamlSettings?.autoDetectKubernetesSchema) { for (const entry of this.filePatternAssociations) { if (entry.uris && entry.uris[0] == KUBERNETES_SCHEMA_URL && entry.matchesPattern(resource)) { - resolveSchemaForResource([KUBERNETES_SCHEMA_URL]).then((schema) => { + return resolveSchemaForResource([KUBERNETES_SCHEMA_URL]).then((schema) => { const kubeSchema = autoDetectKubernetesSchemaFromDocument( doc, this.yamlSettings.crdCatalogURI ?? CRD_CATALOG_URL, From bb8a2f8e94e388e740dcc2a412d6fcc7b92c491e Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 20 Apr 2025 14:21:09 +0200 Subject: [PATCH 6/6] fix: rename crd options and document them --- README.md | 2 ++ src/languageserver/handlers/settingsHandlers.ts | 10 ++++++++-- src/languageservice/services/yamlSchemaService.ts | 4 ++-- src/yamlSettings.ts | 10 ++++++---- test/yamlSchemaService.test.ts | 4 ++-- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6d6c052a8..088df583b 100755 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ The following settings are supported: - `yaml.schemas`: Helps you associate schemas with files in a glob pattern - `yaml.schemaStore.enable`: When set to true the YAML language server will pull in all available schemas from [JSON Schema Store](https://www.schemastore.org/json/) - `yaml.schemaStore.url`: URL of a schema store catalog to use when downloading schemas. +- `yaml.kubernetesCRDStore.enable`: When set to true the YAML language server will parse Kubernetes CRDs automatically and download them from the [CRD store](https://github.com/datreeio/CRDs-catalog). +- `yaml.kubernetesCRDStore.url`: URL of a crd store catalog to use when downloading schemas. Defaults to `https://raw.githubusercontent.com/datreeio/CRDs-catalog/main`. - `yaml.customTags`: Array of custom tags that the parser will validate against. It has two ways to be used. Either an item in the array is a custom tag such as "!Ref" and it will automatically map !Ref to scalar or you can specify the type of the object !Ref should be e.g. "!Ref sequence". The type of object can be either scalar (for strings and booleans), sequence (for arrays), map (for objects). - `yaml.maxItemsComputed`: The maximum number of outline symbols and folding regions computed (limited for performance reasons). - `[yaml].editor.tabSize`: the number of spaces to use when autocompleting. Takes priority over editor.tabSize. diff --git a/src/languageserver/handlers/settingsHandlers.ts b/src/languageserver/handlers/settingsHandlers.ts index 1deec97c8..e16fadd58 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -77,8 +77,6 @@ export class SettingsHandler { this.yamlSettings.customTags = settings.yaml.customTags ? settings.yaml.customTags : []; this.yamlSettings.maxItemsComputed = Math.trunc(Math.max(0, Number(settings.yaml.maxItemsComputed))) || 5000; - this.yamlSettings.autoDetectKubernetesSchema = settings.yaml.autoDetectKubernetesSchema; - this.yamlSettings.crdCatalogURI = settings.yaml.crdCatalogURI; if (settings.yaml.schemaStore) { this.yamlSettings.schemaStoreEnabled = settings.yaml.schemaStore.enable; @@ -86,6 +84,14 @@ export class SettingsHandler { this.yamlSettings.schemaStoreUrl = settings.yaml.schemaStore.url; } } + + if (settings.yaml.kubernetesCRDStore) { + this.yamlSettings.kubernetesCRDStoreEnabled = settings.yaml.kubernetesCRDStore.enable; + if (settings.yaml.kubernetesCRDStore.url?.length !== 0) { + this.yamlSettings.kubernetesCRDStoreUrl = settings.yaml.kubernetesCRDStore.url; + } + } + if (settings.files?.associations) { for (const [ext, languageId] of Object.entries(settings.files.associations)) { if (languageId === 'yaml') { diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index ddb739113..831596e9d 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -466,13 +466,13 @@ export class YAMLSchemaService extends JSONSchemaService { } ); } - if (this.yamlSettings?.autoDetectKubernetesSchema) { + if (this.yamlSettings?.kubernetesCRDStoreEnabled) { for (const entry of this.filePatternAssociations) { if (entry.uris && entry.uris[0] == KUBERNETES_SCHEMA_URL && entry.matchesPattern(resource)) { return resolveSchemaForResource([KUBERNETES_SCHEMA_URL]).then((schema) => { const kubeSchema = autoDetectKubernetesSchemaFromDocument( doc, - this.yamlSettings.crdCatalogURI ?? CRD_CATALOG_URL, + this.yamlSettings.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL, schema ); if (kubeSchema) { diff --git a/src/yamlSettings.ts b/src/yamlSettings.ts index 3848b2cb6..6a3c759cd 100644 --- a/src/yamlSettings.ts +++ b/src/yamlSettings.ts @@ -20,6 +20,10 @@ export interface Settings { url: string; enable: boolean; }; + kubernetesCRDStore: { + url: string; + enable: boolean; + }; disableDefaultProperties: boolean; disableAdditionalProperties: boolean; suggest: { @@ -32,8 +36,6 @@ export interface Settings { keyOrdering: boolean; maxItemsComputed: number; yamlVersion: YamlVersion; - autoDetectKubernetesSchema: boolean; - crdCatalogURI: string; }; http: { proxy: string; @@ -79,6 +81,8 @@ export class SettingsState { customTags = []; schemaStoreEnabled = true; schemaStoreUrl = JSON_SCHEMASTORE_URL; + kubernetesCRDStoreEnabled = true; + kubernetesCRDStoreUrl = CRD_CATALOG_URL; indentation: string | undefined = undefined; disableAdditionalProperties = false; disableDefaultProperties = false; @@ -91,8 +95,6 @@ export class SettingsState { }; keyOrdering = false; maxItemsComputed = 5000; - autoDetectKubernetesSchema = false; - crdCatalogURI = CRD_CATALOG_URL; // File validation helpers pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index 8a5aa3194..dbec36abd 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -152,7 +152,7 @@ describe('YAML Schema Service', () => { settings.schemaAssociations = { kubernetes: ['*.yaml'], }; - settings.autoDetectKubernetesSchema = true; + settings.kubernetesCRDStoreEnabled = true; requestServiceMock = sandbox.fake.resolves( ` { @@ -185,7 +185,7 @@ describe('YAML Schema Service', () => { settings.schemaAssociations = { kubernetes: ['*.yaml'], }; - settings.autoDetectKubernetesSchema = true; + settings.kubernetesCRDStoreEnabled = true; requestServiceMock = sandbox.fake.resolves( ` {