Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Multiple Kubernetes Schemas and CRDs #841

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions src/languageserver/handlers/notificationHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LanguageService, SchemaConfiguration } from '../../languageservice/yaml
import {
CustomSchemaRequest,
DynamicCustomSchemaRequestRegistration,
KubernetesSchemaURLsNotification,
SchemaAssociationNotification,
SchemaSelectionRequests,
VSCodeContentRequestRegistration,
Expand Down Expand Up @@ -36,10 +37,15 @@ export class NotificationHandlers {
this.schemaAssociationNotificationHandler(associations)
);
this.connection.onNotification(DynamicCustomSchemaRequestRegistration.type, () => this.dynamicSchemaRequestHandler());
this.connection.onNotification(KubernetesSchemaURLsNotification.type, (url) => this.kubernetesSchemaURLsNotification(url));
this.connection.onNotification(VSCodeContentRequestRegistration.type, () => this.vscodeContentRequestHandler());
this.connection.onNotification(SchemaSelectionRequests.type, () => this.schemaSelectionRequestHandler());
}

private kubernetesSchemaURLsNotification(urls: string[]): void {
this.yamlSettings.kubernetesSchemaUrls = urls;
}

/**
* Received a notification from the client with schema associations from other extensions
* Update the associations in the server
Expand Down
19 changes: 10 additions & 9 deletions src/languageserver/handlers/settingsHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { configure as configureHttpRequests, xhr } from 'request-light';
import { Connection, DidChangeConfigurationNotification, DocumentFormattingRequest } from 'vscode-languageserver';
import { convertErrorToTelemetryMsg } from '../../languageservice/utils/objects';
import { isRelativePath, relativeToAbsolutePath } from '../../languageservice/utils/paths';
import { checkSchemaURI, JSON_SCHEMASTORE_URL, KUBERNETES_SCHEMA_URL } from '../../languageservice/utils/schemaUrls';
import { checkSchemaURI, JSON_SCHEMASTORE_URL } from '../../languageservice/utils/schemaUrls';
import { LanguageService, LanguageSettings, SchemaPriority } from '../../languageservice/yamlLanguageService';
import { SchemaSelectionRequests } from '../../requestTypes';
import { Settings, SettingsState } from '../../yamlSettings';
Expand Down Expand Up @@ -323,7 +323,7 @@ export class SettingsHandler {
* @param languageSettings current server settings
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private configureSchemas(
public configureSchemas(
uri: string,
fileMatch: string[],
schema: unknown,
Expand All @@ -338,14 +338,15 @@ export class SettingsHandler {
languageSettings.schemas.push({ uri, fileMatch: fileMatch, schema: schema, priority: priorityLevel });
}

if (fileMatch.constructor === Array && uri === KUBERNETES_SCHEMA_URL) {
fileMatch.forEach((url) => {
this.yamlSettings.specificValidatorPaths.push(url);
});
} else if (uri === KUBERNETES_SCHEMA_URL) {
this.yamlSettings.specificValidatorPaths.push(fileMatch);
if (this.yamlSettings.kubernetesSchemaUrls.includes(uri)) {
if (fileMatch.constructor === Array) {
fileMatch.forEach((url) => {
this.yamlSettings.specificValidatorPaths.push(url);
});
} else {
this.yamlSettings.specificValidatorPaths.push(fileMatch);
}
}

return languageSettings;
}
}
5 changes: 5 additions & 0 deletions src/requestTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export namespace DynamicCustomSchemaRequestRegistration {
export const type: NotificationType<unknown> = new NotificationType('yaml/registerCustomSchemaRequest');
}

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace KubernetesSchemaURLsNotification {
export const type: NotificationType<string[]> = new NotificationType('yaml/kubernetesSchemaURLs');
}

export namespace VSCodeContentRequestRegistration {
export const type: NotificationType<unknown> = new NotificationType('yaml/registerContentRequest');
}
Expand Down
3 changes: 2 additions & 1 deletion src/yamlSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { JSON_SCHEMASTORE_URL, KUBERNETES_SCHEMA_URL } from './languageservice/utils/schemaUrls';
import { YamlVersion } from './languageservice/parser/yamlParser07';

// Client settings interface to grab settings relevant for the language server
Expand Down Expand Up @@ -77,6 +77,7 @@ export class SettingsState {
customTags = [];
schemaStoreEnabled = true;
schemaStoreUrl = JSON_SCHEMASTORE_URL;
kubernetesSchemaUrls = [KUBERNETES_SCHEMA_URL];
indentation: string | undefined = undefined;
disableAdditionalProperties = false;
disableDefaultProperties = false;
Expand Down
89 changes: 88 additions & 1 deletion test/schemaValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import { Diagnostic, DiagnosticSeverity, Position } from 'vscode-languageserver-
import { expect } from 'chai';
import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings';
import { ValidationHandler } from '../src/languageserver/handlers/validationHandlers';
import { LanguageService } from '../src/languageservice/yamlLanguageService';
import { LanguageService, SchemaPriority } from '../src/languageservice/yamlLanguageService';
import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls';
import { IProblem } from '../src/languageservice/parser/jsonParser07';
import { JSONSchema } from '../src/languageservice/jsonSchema';
import { TestTelemetry } from './utils/testsTypes';
import { SettingsHandler } from '../src/languageserver/handlers/settingsHandlers';
import { Connection } from 'vscode-languageserver';

describe('Validation Tests', () => {
let languageSettingsSetup: ServiceSetup;
Expand Down Expand Up @@ -1108,6 +1110,11 @@ obj:
});

describe('Test with custom kubernetes schemas', function () {
afterEach(() => {
// remove Kubernetes setting not to affect next tests
languageService.configure(languageSettingsSetup.withKubernetes(false).languageSettings);
yamlSettings.specificValidatorPaths = [];
});
it('Test that properties that match multiple enums get validated properly', (done) => {
languageService.configure(languageSettingsSetup.withKubernetes().languageSettings);
yamlSettings.specificValidatorPaths = ['*.yml', '*.yaml'];
Expand Down Expand Up @@ -1153,6 +1160,86 @@ obj:
})
.then(done, done);
});

it('single custom kubernetes schema version should return validation errors', async () => {
const customKubernetesSchemaVersion =
'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.26.1-standalone-strict/all.json';
yamlSettings.kubernetesSchemaUrls = [customKubernetesSchemaVersion];
const settingsHandler = new SettingsHandler({} as Connection, languageService, yamlSettings, validationHandler, telemetry);
const initialSettings = languageSettingsSetup.withKubernetes(true).languageSettings;
const kubernetesSettings = settingsHandler.configureSchemas(
customKubernetesSchemaVersion,
['*k8s.yml'],
undefined,
initialSettings,
SchemaPriority.SchemaAssociation
);
languageService.configure(kubernetesSettings);
const content = `apiVersion: apps/v1\nkind: Deployment\nfoo: bar`;
const result = await parseSetup(content, 'invalid-k8s.yml');
expect(result.length).to.eq(1);
expect(result[0].message).to.eq('Property foo is not allowed.');
});

it('single openshift schema version should return validation errors', async () => {
const customOpenshiftSchemaVersion =
'https://raw.githubusercontent.com/tricktron/CRDs-catalog/f-openshift-v4.11/openshift.io/v4.11/all.json';
yamlSettings.kubernetesSchemaUrls = [customOpenshiftSchemaVersion];
const settingsHandler = new SettingsHandler({} as Connection, languageService, yamlSettings, validationHandler, telemetry);
const initialSettings = languageSettingsSetup.withKubernetes(true).languageSettings;
const kubernetesSettings = settingsHandler.configureSchemas(
customOpenshiftSchemaVersion,
['*oc.yml'],
undefined,
initialSettings,
SchemaPriority.SchemaAssociation
);
languageService.configure(kubernetesSettings);
const content = `apiVersion: route.openshift.io/v1\nkind: Route\nfoo: bar`;
const result = await parseSetup(content, 'invalid-oc.yml');
expect(result.length).to.eq(2);
expect(result[0].message).to.eq('Missing property "spec".');
expect(result[1].message).to.eq('Property foo is not allowed.');
});

it('custom kubernetes schema version and openshift custom resource definition (CRD) should return validation errors', async () => {
const customKubernetesSchemaVersion =
'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.26.1-standalone-strict/all.json';
const customOpenshiftSchemaVersion =
'https://raw.githubusercontent.com/tricktron/CRDs-catalog/f-openshift-v4.11/openshift.io/v4.11/all.json';
yamlSettings.kubernetesSchemaUrls = [customKubernetesSchemaVersion, customOpenshiftSchemaVersion];
const settingsHandler = new SettingsHandler({} as Connection, languageService, yamlSettings, validationHandler, telemetry);
const initialSettings = languageSettingsSetup.withKubernetes(true).languageSettings;
const kubernetesSettings = settingsHandler.configureSchemas(
customKubernetesSchemaVersion,
['*k8s.yml'],
undefined,
initialSettings,
SchemaPriority.SchemaAssociation
);
const openshiftSettings = settingsHandler.configureSchemas(
customOpenshiftSchemaVersion,
['*oc.yml'],
undefined,
initialSettings,
SchemaPriority.SchemaAssociation
);
languageService.configure(kubernetesSettings);
languageService.configure(openshiftSettings);
const kubernetes = `apiVersion: apps/v1\nkind: Deployment\nfoo: bar`;
const openshift = `apiVersion: route.openshift.io/v1\nkind: Route\nbaz: abc`;

const kubernetesResult = await parseSetup(kubernetes, 'invalid-k8s.yml');

expect(kubernetesResult.length).to.eq(1);
expect(kubernetesResult[0].message).to.eq('Property foo is not allowed.');

const openshiftResult = await parseSetup(openshift, 'invalid-oc.yml');

expect(openshiftResult.length).to.eq(2);
expect(openshiftResult[0].message).to.eq('Missing property "spec".');
expect(openshiftResult[1].message).to.eq('Property baz is not allowed.');
});
});

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