From fa1f98ca8db08bd10614691593db8f1e76e81b87 Mon Sep 17 00:00:00 2001 From: Marius Svechla Date: Mon, 22 Apr 2024 20:04:28 +0200 Subject: [PATCH 01/13] 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 00f2d383c..0751bad75 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -77,6 +77,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 aeff53308..2450e2c58 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, @@ -31,6 +32,7 @@ import Ajv, { DefinedError, type AnySchemaObject, type ValidateFunction } from ' import Ajv4 from 'ajv-draft-04'; import Ajv2019 from 'ajv/dist/2019'; import Ajv2020 from 'ajv/dist/2020'; +import { autoDetectKubernetesSchemaFromDocument } from './crdUtil'; const ajv4 = new Ajv4({ allErrors: true }); const ajv7 = new Ajv({ allErrors: true }); @@ -120,6 +122,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(); @@ -127,12 +130,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 { @@ -435,6 +440,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 6c5353966..69a7aca90 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -197,7 +197,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 66ce9b25f..b3550fd71 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; @@ -90,6 +91,7 @@ export class SettingsState { }; keyOrdering = false; maxItemsComputed = 5000; + autoDetectKubernetesSchema = false; // File validation helpers pendingValidationRequests: { [uri: string]: NodeJS.Timeout } = {}; diff --git a/test/schema.test.ts b/test/schema.test.ts index 2e5aa735d..25093728f 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 9b341f616fb69d4872c4b3002bd4858a55c6898b Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 13:50:20 +0200 Subject: [PATCH 02/13] 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 0751bad75..07dd5d94e 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -78,6 +78,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 2450e2c58..7d005f8c4 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -33,6 +33,7 @@ import Ajv4 from 'ajv-draft-04'; import Ajv2019 from 'ajv/dist/2019'; import Ajv2020 from 'ajv/dist/2020'; import { autoDetectKubernetesSchemaFromDocument } from './crdUtil'; +import { KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; const ajv4 = new Ajv4({ allErrors: true }); const ajv7 = new Ajv({ allErrors: true }); @@ -441,13 +442,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) => { @@ -490,9 +484,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 b3550fd71..6bf84c8e6 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; @@ -92,6 +93,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.Timeout } = {}; diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index a98fb8791..f2bf91df5 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 03aed76aae6fe990d607155c0da6052890d5f043 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 17:31:15 +0200 Subject: [PATCH 03/13] 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 7d005f8c4..d651d256d 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -33,7 +33,7 @@ import Ajv4 from 'ajv-draft-04'; import Ajv2019 from 'ajv/dist/2019'; import Ajv2020 from 'ajv/dist/2020'; import { autoDetectKubernetesSchemaFromDocument } from './crdUtil'; -import { KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; +import { CRD_CATALOG_URL, KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; const ajv4 = new Ajv4({ allErrors: true }); const ajv7 = new Ajv({ allErrors: true }); @@ -487,11 +487,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 c17d9a655..82c5aa118 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.32.1-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 6bf84c8e6..1a9611591 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 @@ -93,7 +93,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.Timeout } = {}; diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index f2bf91df5..2cbda9776 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 d10995fb0a3cf2897914382142dafbdff00eef29 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 17:36:37 +0200 Subject: [PATCH 04/13] 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 e8524cb499573ccd3ac6d30c2084c2b3de476c9c Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 19 Apr 2025 17:51:05 +0200 Subject: [PATCH 05/13] 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 d651d256d..431558fc0 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -488,7 +488,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 c12a525d31eb3aec4b29bc13745ab959b3d93ace Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 20 Apr 2025 14:21:09 +0200 Subject: [PATCH 06/13] 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 3ffe2fdf1..3a1d7f5d4 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) - `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 07dd5d94e..ef24143a9 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 431558fc0..14d55bdbd 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -485,13 +485,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 1a9611591..eae456af4 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; @@ -80,6 +82,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; @@ -92,8 +96,6 @@ export class SettingsState { }; keyOrdering = false; maxItemsComputed = 5000; - autoDetectKubernetesSchema = false; - crdCatalogURI = CRD_CATALOG_URL; // File validation helpers pendingValidationRequests: { [uri: string]: NodeJS.Timeout } = {}; 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( ` { From a346aae9a554f2190ed7409bc16360bf17762729 Mon Sep 17 00:00:00 2001 From: Roland Wahl Date: Wed, 17 Dec 2025 16:22:08 +0100 Subject: [PATCH 07/13] fix eslint errors --- src/languageservice/services/crdUtil.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index 6afb45d5a..5024af532 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -29,13 +29,12 @@ export function autoDetectKubernetesSchemaFromDocument( } const k8sSchema: JSONSchema = kubernetesSchema.schema; - let kubernetesBuildIns: string[] = k8sSchema.oneOf + const kubernetesBuildIns: string[] = k8sSchema.oneOf .map((s) => { if (typeof s === 'boolean') { return undefined; } - // @ts-ignore - return s._$ref; + return s.$ref; }) .filter((ref) => ref) .map((ref) => ref.replace('_definitions.json#/definitions/', '').toLowerCase()); From 7794177e8fe621f8ca06b455ab24f68318db06c7 Mon Sep 17 00:00:00 2001 From: Roland Wahl Date: Thu, 18 Dec 2025 18:32:40 +0100 Subject: [PATCH 08/13] fix tests --- src/languageservice/services/crdUtil.ts | 2 +- test/yamlSchemaService.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index 5024af532..82baa5270 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -29,7 +29,7 @@ export function autoDetectKubernetesSchemaFromDocument( } const k8sSchema: JSONSchema = kubernetesSchema.schema; - const kubernetesBuildIns: string[] = k8sSchema.oneOf + const kubernetesBuildIns: string[] = (k8sSchema.oneOf || []) .map((s) => { if (typeof s === 'boolean') { return undefined; diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index dbec36abd..cd95b8c1b 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -158,7 +158,7 @@ describe('YAML Schema Service', () => { { "oneOf": [ { "$ref": "_definitions.json#/definitions/io.k8s.api.admissionregistration.v1.MutatingWebhook" - }, + } ] } ` @@ -191,7 +191,7 @@ describe('YAML Schema Service', () => { { "oneOf": [ { "$ref": "_definitions.json#/definitions/io.k8s.api.admissionregistration.v1.MutatingWebhook" - }, + } ] } ` From a944378b236e84af13f20cb54e6445b1dafa8cdb Mon Sep 17 00:00:00 2001 From: qvalentin Date: Fri, 19 Dec 2025 18:04:04 +0100 Subject: [PATCH 09/13] fix: do not use CRDs-catalog for builtin k8s ressources --- src/languageservice/services/crdUtil.ts | 6 ++-- test/schema.test.ts | 10 ++++++ test/yamlSchemaService.test.ts | 43 +++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index 82baa5270..0fc8d04e1 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -34,11 +34,13 @@ export function autoDetectKubernetesSchemaFromDocument( if (typeof s === 'boolean') { return undefined; } - return s.$ref; + // @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()}`; + const groupWithoutK8sIO = group.replace('.k8s.io', ''); + const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`; if (kubernetesBuildIns.includes(k8sTypeName)) { return KUBERNETES_SCHEMA_URL; diff --git a/test/schema.test.ts b/test/schema.test.ts index 25093728f..8d33caa41 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -707,6 +707,16 @@ describe('JSON Schema', () => { checkReturnGroupVersionKind('apiVersion: v1\nkind: Pod', true, undefined, 'v1', 'Pod'); }); + it('builtin kubernetes resource with complex apiVersion should get resolved ', async () => { + checkReturnGroupVersionKind( + 'apiVersion: admissionregistration.k8s.io/v1\nkind: MutatingWebhook', + false, + 'admissionregistration.k8s.io', + 'v1', + 'MutatingWebhook' + ); + }); + it('custom argo application CRD should get resolved', async () => { checkReturnGroupVersionKind( 'apiVersion: argoproj.io/v1alpha1\nkind: Application', diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index cd95b8c1b..0d01aff83 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -165,7 +165,11 @@ describe('YAML Schema Service', () => { ); const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings); service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); - await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + const resolvedeSchema = await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + + expect(resolvedeSchema.schema.url).eqls( + 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/argoproj.io/application_v1alpha1.json' + ); expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL); expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); @@ -198,13 +202,46 @@ describe('YAML Schema Service', () => { ); const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings); service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); - await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + const resolvedSchema = await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + expect(resolvedSchema.schema.url).eqls(KUBERNETES_SCHEMA_URL); expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL); expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); + expect(requestServiceMock).calledTwice; + }); + + it('should not get schema from crd catalog if definition in kubernetes schema (multiple oneOf)', async () => { + const documentContent = 'apiVersion: apps/v1\nkind: Deployment'; + const content = `${documentContent}`; + const yamlDock = parse(content); + + const settings = new SettingsState(); + settings.schemaAssociations = { + kubernetes: ['*.yaml'], + }; + settings.kubernetesCRDStoreEnabled = true; + requestServiceMock = sandbox.fake.resolves( + ` + { + "oneOf": [ + { + "$ref": "_definitions.json#/definitions/io.k8s.api.apps.v1.Deployment" + }, + { + "$ref": "_definitions.json#/definitions/io.k8s.api.apps.v1.DeploymentCondition" + } + ] + } + ` + ); + const service = new SchemaService.YAMLSchemaService(requestServiceMock, undefined, undefined, settings); + service.registerExternalSchema(KUBERNETES_SCHEMA_URL, ['*.yaml']); + const resolvedSchema = await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + expect(resolvedSchema.schema.url).eqls(KUBERNETES_SCHEMA_URL); expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL); - expect(requestServiceMock).calledThrice; + expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); + expect(requestServiceMock).calledTwice; }); }); }); From 3720271c47cca8b819ac2a99f2f061804003fc8a Mon Sep 17 00:00:00 2001 From: qvalentin Date: Fri, 19 Dec 2025 18:24:15 +0100 Subject: [PATCH 10/13] fix: clarify @ts-ignore --- src/languageservice/services/crdUtil.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index 0fc8d04e1..e3875b210 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -34,8 +34,8 @@ export function autoDetectKubernetesSchemaFromDocument( if (typeof s === 'boolean') { return undefined; } - // @ts-ignore - return s._$ref; + // @ts-ignore (only $ref is defined in the ts definition but it seems like it's actually _$ref) + return s._$ref || s.$ref; }) .filter((ref) => ref) .map((ref) => ref.replace('_definitions.json#/definitions/', '').toLowerCase()); From 1988d81f7bf8431afb50757581f167005b1bab36 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sat, 20 Dec 2025 09:59:08 +0100 Subject: [PATCH 11/13] fix: import type from ../jsonSchema --- src/languageservice/services/crdUtil.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index e3875b210..bb10ba609 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -2,7 +2,7 @@ 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 { JSONSchema } from '../jsonSchema'; import { KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; /** @@ -34,7 +34,6 @@ export function autoDetectKubernetesSchemaFromDocument( if (typeof s === 'boolean') { return undefined; } - // @ts-ignore (only $ref is defined in the ts definition but it seems like it's actually _$ref) return s._$ref || s.$ref; }) .filter((ref) => ref) From 8c68029e4135b7a1b668b71d923d67df2664dd66 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 30 Dec 2025 13:02:47 -0500 Subject: [PATCH 12/13] Handle case where custom schema provider is present The CRD schema resolving logic now runs even if a custom schema provider is present, allowing this to work properly in vscode-yaml. Signed-off-by: David Thompson --- .../services/yamlSchemaService.ts | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index 14d55bdbd..489b7b426 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -414,16 +414,35 @@ export class YAMLSchemaService extends JSONSchemaService { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resolveSchema = (): any => { + const resolveSchema = async (): Promise => { const seen: { [schemaId: string]: boolean } = Object.create(null); const schemas: string[] = []; + let k8sAllSchema: ResolvedSchema = undefined; for (const entry of this.filePatternAssociations) { if (entry.matchesPattern(resource)) { for (const schemaId of entry.getURIs()) { if (!seen[schemaId]) { - schemas.push(schemaId); - seen[schemaId] = true; + if (this.yamlSettings?.kubernetesCRDStoreEnabled && schemaId === KUBERNETES_SCHEMA_URL) { + if (!k8sAllSchema) { + k8sAllSchema = await this.getResolvedSchema(KUBERNETES_SCHEMA_URL); + } + const kubeSchema = autoDetectKubernetesSchemaFromDocument( + doc, + this.yamlSettings.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL, + k8sAllSchema + ); + if (kubeSchema) { + schemas.push(kubeSchema); + seen[schemaId] = true; + } else { + schemas.push(schemaId); + seen[schemaId] = true; + } + } else { + schemas.push(schemaId); + seen[schemaId] = true; + } } } } @@ -485,22 +504,6 @@ export class YAMLSchemaService extends JSONSchemaService { } ); } - 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.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL, - schema - ); - if (kubeSchema) { - return resolveSchemaForResource([kubeSchema]); - } - }); - } - } - } return resolveSchema(); } From ba55b1a0176501ec24bf4611e943a92f2f71e28a Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 30 Dec 2025 13:05:00 -0500 Subject: [PATCH 13/13] Add logic to handle OpenShift CRDs The CRD schemas for OpenShift are layed out slightly different from everything else in the CRD schema repo in order to provide different sets of schemas for different versions of OpenShift. Signed-off-by: David Thompson --- src/languageservice/services/crdUtil.ts | 7 ++-- test/yamlSchemaService.test.ts | 43 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/languageservice/services/crdUtil.ts b/src/languageservice/services/crdUtil.ts index bb10ba609..f091c6bce 100644 --- a/src/languageservice/services/crdUtil.ts +++ b/src/languageservice/services/crdUtil.ts @@ -3,7 +3,6 @@ import { JSONDocument } from '../parser/jsonParser07'; import { ResolvedSchema } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService'; import { JSONSchema } from '../jsonSchema'; -import { KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls'; /** * Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document. @@ -42,7 +41,11 @@ export function autoDetectKubernetesSchemaFromDocument( const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`; if (kubernetesBuildIns.includes(k8sTypeName)) { - return KUBERNETES_SCHEMA_URL; + return undefined; + } + + if (k8sTypeName.includes('openshift.io')) { + return `${crdCatalogURI}/openshift/v4.15-strict/${kind.toLowerCase()}_${group.toLowerCase()}_${version.toLowerCase()}.json`; } const schemaURL = `${crdCatalogURI}/${group.toLowerCase()}/${kind.toLowerCase()}_${version.toLowerCase()}.json`; diff --git a/test/yamlSchemaService.test.ts b/test/yamlSchemaService.test.ts index 0d01aff83..4aeb318ea 100644 --- a/test/yamlSchemaService.test.ts +++ b/test/yamlSchemaService.test.ts @@ -180,6 +180,49 @@ describe('YAML Schema Service', () => { expect(requestServiceMock).calledThrice; }); + it('should handle nonstandard location for OpenShift crd', async () => { + const documentContent = `apiVersion: route.openshift.io/v1 +kind: Route +spec: + to: + kind: Service + name: MyFirstService + weight: 100`; + const content = `${documentContent}`; + const yamlDock = parse(content); + + const settings = new SettingsState(); + settings.schemaAssociations = { + kubernetes: ['*.yaml'], + }; + settings.kubernetesCRDStoreEnabled = 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']); + const resolvedeSchema = await service.getSchemaForResource('test.yaml', yamlDock.documents[0]); + + expect(resolvedeSchema.schema.url).eqls( + 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/openshift/v4.15-strict/route_route.openshift.io_v1.json' + ); + + expect(requestServiceMock).calledWithExactly(KUBERNETES_SCHEMA_URL); + expect(requestServiceMock).calledWithExactly('file:///_definitions.json'); + + expect(requestServiceMock).calledWithExactly( + 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/openshift/v4.15-strict/route_route.openshift.io_v1.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}`;