diff --git a/src/languageserver/handlers/settingsHandlers.ts b/src/languageserver/handlers/settingsHandlers.ts index e2351ea7..18327333 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 00000000..16d770be --- /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 453b74eb..19c56d7e 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 457a9d09..2c03d835 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 fc026034..8bf88c3f 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 1747b1c4..43ac1bbc 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');