Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ The following settings are supported:
- `yaml.hover`: Enable/disable hover
- `yaml.completion`: Enable/disable autocompletion
- `yaml.schemas`: Helps you associate schemas with files in a glob pattern
- `yaml.kubernetesVersion`: Kubernetes version used to build the schema URL when `yaml.schemas` maps files to the `Kubernetes` keyword.
- `yaml.disableSchemaDetection`: Disables schema detection for matching YAML files. Modelines still apply.
- `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.
Expand Down
38 changes: 24 additions & 14 deletions src/languageserver/handlers/settingsHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ import { configure as configureHttpRequests, xhr } from 'request-light';
import { Connection, DidChangeConfigurationNotification, DocumentFormattingRequest } from 'vscode-languageserver';
import { CodeLensRefreshRequest } from 'vscode-languageserver-protocol';
import { isRelativePath, relativeToAbsolutePath } from '../../languageservice/utils/paths';
import {
checkSchemaURI,
EMPTY_SCHEMA_URL,
JSON_SCHEMASTORE_URL,
KUBERNETES_SCHEMA_URL,
} from '../../languageservice/utils/schemaUrls';
import { checkSchemaURI, EMPTY_SCHEMA_URL, isKubernetes, JSON_SCHEMASTORE_URL } from '../../languageservice/utils/schemaUrls';
import { equals } from '../../languageservice/utils/objects';
import { LanguageService, LanguageSettings, SchemaPriority, SchemasSettings } from '../../languageservice/yamlLanguageService';
import { SchemaSelectionRequests } from '../../requestTypes';
Expand Down Expand Up @@ -95,6 +90,13 @@ export class SettingsHandler {
if (Object.prototype.hasOwnProperty.call(settings.yaml, 'hoverSchemaSource')) {
this.yamlSettings.yamlHoverSchemaSource = settings.yaml.hoverSchemaSource;
}
if (Object.prototype.hasOwnProperty.call(settings.yaml, 'kubernetesVersion')) {
const match =
typeof settings.yaml.kubernetesVersion === 'string'
? /^v?(\d+)\.(\d+)\.(\d+)$/i.exec(settings.yaml.kubernetesVersion.trim())
: undefined;
this.yamlSettings.kubernetesVersion = match ? `v${match[1]}.${match[2]}.${match[3]}` : undefined;
}
this.yamlSettings.yamlDisableSchemaDetection = Array.isArray(settings.yaml.disableSchemaDetection)
? settings.yaml.disableSchemaDetection
: settings.yaml.disableSchemaDetection
Expand Down Expand Up @@ -179,7 +181,7 @@ export class SettingsHandler {

const schemaObj = {
fileMatch: Array.isArray(globPattern) ? globPattern : [globPattern],
uri: checkSchemaURI(this.yamlSettings.workspaceFolders, this.yamlSettings.workspaceRoot, uri, this.telemetry),
uri,
};
this.yamlSettings.schemaConfigurationSettings.push(schemaObj);
}
Expand Down Expand Up @@ -411,20 +413,28 @@ export class SettingsHandler {
languageSettings: LanguageSettings,
priorityLevel: number
): LanguageSettings {
uri = checkSchemaURI(this.yamlSettings.workspaceFolders, this.yamlSettings.workspaceRoot, uri, this.telemetry);
uri = checkSchemaURI(
this.yamlSettings.workspaceFolders,
this.yamlSettings.workspaceRoot,
uri,
this.telemetry,
this.yamlSettings.kubernetesVersion
);

if (schema === null) {
languageSettings.schemas.push({ uri, fileMatch: fileMatch, priority: priorityLevel });
} else {
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 (isKubernetes(uri)) {
if (Array.isArray(fileMatch)) {
fileMatch.forEach((pattern) => {
this.yamlSettings.specificValidatorPaths.push(pattern);
});
} else {
this.yamlSettings.specificValidatorPaths.push(fileMatch);
}
}

return languageSettings;
Expand Down
14 changes: 10 additions & 4 deletions src/languageservice/services/k8sSchemaUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { SingleYAMLDocument } from '../parser/yamlParser07';

import { ResolvedSchema } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';
import { JSONSchema } from '../jsonSchema';
import { BASE_KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls';

/**
* Attempt to retrieve the schema for a given YAML document based on the Kubernetes GroupVersionKind (GVK).
Expand All @@ -18,13 +17,14 @@ import { BASE_KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls';
export function autoDetectKubernetesSchema(
doc: SingleYAMLDocument | JSONDocument,
kubernetesSchema: ResolvedSchema,
kubernetesSchemaURI: string,
crdCatalogURI: string
): string | undefined {
const gvk = getGroupVersionKindFromDocument(doc);
if (!gvk || !gvk.group || !gvk.version || !gvk.kind) {
return undefined;
}
const builtinResource = autoDetectBuiltinResource(gvk, kubernetesSchema);
const builtinResource = autoDetectBuiltinResource(gvk, kubernetesSchema, kubernetesSchemaURI);
if (builtinResource) {
return builtinResource;
}
Expand All @@ -35,7 +35,11 @@ export function autoDetectKubernetesSchema(
return undefined;
}

function autoDetectBuiltinResource(gvk: GroupVersionKind, kubernetesSchema: ResolvedSchema): string | undefined {
function autoDetectBuiltinResource(
gvk: GroupVersionKind,
kubernetesSchema: ResolvedSchema,
kubernetesSchemaURI: string
): string | undefined {
const { group, version, kind } = gvk;

const groupWithoutK8sIO = group.replace('.k8s.io', '').replace('rbac.authorization', 'rbac');
Expand All @@ -57,7 +61,9 @@ function autoDetectBuiltinResource(gvk: GroupVersionKind, kubernetesSchema: Reso
});

if (matchingBuiltin) {
return BASE_KUBERNETES_SCHEMA_URL + matchingBuiltin;
const lastSlash = kubernetesSchemaURI.lastIndexOf('/');
const baseURL = lastSlash === -1 ? null : kubernetesSchemaURI.substring(0, lastSlash + 1);
return baseURL ? baseURL + matchingBuiltin : undefined;
}

return undefined;
Expand Down
9 changes: 6 additions & 3 deletions src/languageservice/services/yamlSchemaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Ajv2020 from 'ajv/dist/2020';
import type { Localize } from 'ajv-i18n/localize/types';
import * as Json from 'jsonc-parser';
import { parse } from 'yaml';
import { CRD_CATALOG_URL, EMPTY_SCHEMA_URL, KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls';
import { CRD_CATALOG_URL, EMPTY_SCHEMA_URL, isKubernetes } from '../utils/schemaUrls';
import { autoDetectKubernetesSchema } from './k8sSchemaUtil';

const ajv4 = new Ajv4({ allErrors: true });
Expand Down Expand Up @@ -1100,18 +1100,21 @@ export class YAMLSchemaService extends JSONSchemaService {
const seen: { [schemaId: string]: boolean } = Object.create(null);
const schemas: string[] = [];
let k8sAllSchema: ResolvedSchema = undefined;
let k8sSchemaUrl: string | undefined = undefined;

for (const entry of this.filePatternAssociations) {
if (entry.matchesPattern(resource)) {
for (const schemaId of entry.getURIs()) {
if (!seen[schemaId]) {
if (this.yamlSettings?.kubernetesCRDStoreEnabled && schemaId === KUBERNETES_SCHEMA_URL) {
if (this.yamlSettings?.kubernetesCRDStoreEnabled && isKubernetes(schemaId)) {
if (!k8sAllSchema) {
k8sAllSchema = await this.getResolvedSchema(KUBERNETES_SCHEMA_URL);
k8sSchemaUrl = schemaId;
k8sAllSchema = await this.getResolvedSchema(schemaId);
}
const kubeSchema = autoDetectKubernetesSchema(
doc,
k8sAllSchema,
k8sSchemaUrl ?? schemaId,
this.yamlSettings.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL
);
if (kubeSchema) {
Expand Down
15 changes: 13 additions & 2 deletions src/languageservice/services/yamlValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { AdditionalValidator } from './validation/types';
import { UnusedAnchorsValidator } from './validation/unused-anchors';
import { YAMLStyleValidator } from './validation/yaml-style';
import { MapKeyOrderValidator } from './validation/map-key-order';
import { getSchemaFromModeline } from './modelineUtil';
import { isKubernetes as isKubernetesSchemaURI } from '../utils/schemaUrls';

/**
* Convert a YAMLDocDiagnostic to a language server Diagnostic
Expand Down Expand Up @@ -80,6 +82,7 @@ export class YAMLValidation {
}

const validationResult = [];
let suppressKubernetesMatchesMultiple = isKubernetes;
try {
const yamlDocument: YAMLDocument = yamlDocumentsCache.getYamlDocument(
textDocument,
Expand All @@ -89,7 +92,9 @@ export class YAMLValidation {

let index = 0;
for (const currentYAMLDoc of yamlDocument.documents) {
currentYAMLDoc.isKubernetes = isKubernetes;
const currentDocumentIsKubernetes = isKubernetes || this.hasKubernetesModelineSchema(currentYAMLDoc);
currentYAMLDoc.isKubernetes = currentDocumentIsKubernetes;
suppressKubernetesMatchesMultiple = suppressKubernetesMatchesMultiple || currentDocumentIsKubernetes;
currentYAMLDoc.currentDocIndex = index;
currentYAMLDoc.disableAdditionalProperties = this.disableAdditionalProperties;
currentYAMLDoc.uri = textDocument.uri;
Expand Down Expand Up @@ -122,7 +127,7 @@ export class YAMLValidation {
* 'Matches many schemas' error for kubernetes
* for a better user experience.
*/
if (isKubernetes && err.message === this.MATCHES_MULTIPLE) {
if (suppressKubernetesMatchesMultiple && err.message === this.MATCHES_MULTIPLE) {
continue;
}

Expand Down Expand Up @@ -166,6 +171,12 @@ export class YAMLValidation {
}
);
}

private hasKubernetesModelineSchema(currentYAMLDoc: SingleYAMLDocument): boolean {
const schemaFromModeline = getSchemaFromModeline(currentYAMLDoc);
return typeof schemaFromModeline === 'string' && isKubernetesSchemaURI(schemaFromModeline);
}

private runAdditionalValidators(document: TextDocument, yarnDoc: SingleYAMLDocument): Diagnostic[] {
const result = [];

Expand Down
24 changes: 18 additions & 6 deletions src/languageservice/utils/schemaUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,34 @@ import { JSONSchema, JSONSchemaRef } from '../jsonSchema';
import { isBoolean } from './objects';
import { isRelativePath, relativeToAbsolutePath } from './paths';

export const BASE_KUBERNETES_SCHEMA_URL =
'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.32.1-standalone-strict/';
export const KUBERNETES_SCHEMA_URL = BASE_KUBERNETES_SCHEMA_URL + 'all.json';
export const DEFAULT_KUBERNETES_SCHEMA_VERSION = 'v1.34.1';
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 const EMPTY_SCHEMA_URL = 'vscode://schemas/empty';

const KUBERNETES_SCHEMA_URL_PATTERN =
/^https:\/\/raw\.githubusercontent\.com\/yannh\/kubernetes-json-schema\/master\/((?:v(\d+)\.(\d+)\.(\d+))-standalone-strict)\/all\.json$/;

export function isKubernetes(uri: string): boolean {
if (uri.trim().toLowerCase() === 'kubernetes') return true;
return KUBERNETES_SCHEMA_URL_PATTERN.test(uri);
}

export function checkSchemaURI(
workspaceFolders: WorkspaceFolder[],
workspaceRoot: URI,
uri: string,
telemetry: Telemetry
telemetry: Telemetry,
kubernetesVersion?: string
): string {
if (uri.trim().toLowerCase() === 'kubernetes') {
const k8sKeywordUsed = uri.trim().toLowerCase() === 'kubernetes';
if (k8sKeywordUsed || KUBERNETES_SCHEMA_URL_PATTERN.test(uri)) {
telemetry.send({ name: 'yaml.schema.configured', properties: { kubernetes: true } });
return KUBERNETES_SCHEMA_URL;
if (k8sKeywordUsed) {
return `https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/${kubernetesVersion ?? DEFAULT_KUBERNETES_SCHEMA_VERSION}-standalone-strict/all.json`;
} else {
return uri;
}
} else if (path.isAbsolute(uri) || /^[a-z]:[\\/]/i.test(uri)) {
const localPath = uri.split('#', 2)[0];
return URI.file(localPath).toString() + uri.substring(localPath.length);
Expand Down
2 changes: 2 additions & 0 deletions src/yamlSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface Settings {
url: string;
enable: boolean;
};
kubernetesVersion: string;
disableDefaultProperties: boolean;
disableAdditionalProperties: boolean;
suggest: {
Expand Down Expand Up @@ -90,6 +91,7 @@ export class SettingsState {
schemaStoreUrl = JSON_SCHEMASTORE_URL;
kubernetesCRDStoreEnabled = true;
kubernetesCRDStoreUrl = CRD_CATALOG_URL;
kubernetesVersion: string | undefined = undefined;
indentation: string | undefined = undefined;
disableAdditionalProperties = false;
disableDefaultProperties = false;
Expand Down
4 changes: 3 additions & 1 deletion test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as url from 'url';
import * as path from 'path';
import { XHRResponse, xhr } from 'request-light';
import { MODIFICATION_ACTIONS, SchemaDeletions } from '../src/languageservice/services/yamlSchemaService';
import { EMPTY_SCHEMA_URL, KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls';
import { EMPTY_SCHEMA_URL, DEFAULT_KUBERNETES_SCHEMA_VERSION } from '../src/languageservice/utils/schemaUrls';
import { expect } from 'chai';
import { ServiceSetup } from './utils/serviceSetup';
import {
Expand All @@ -25,6 +25,8 @@ import { LineCounter } from 'yaml';
import { getSchemaFromModeline } from '../src/languageservice/services/modelineUtil';
import { getGroupVersionKindFromDocument } from '../src/languageservice/services/k8sSchemaUtil';

const KUBERNETES_SCHEMA_URL = `https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/${DEFAULT_KUBERNETES_SCHEMA_VERSION}-standalone-strict/all.json`;

const requestServiceMock = function (uri: string): Promise<string> {
return Promise.reject<string>(`Resource ${uri} not found.`);
};
Expand Down
4 changes: 3 additions & 1 deletion test/schema2019Validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Diagnostic } from 'vscode-languageserver-types';
import { expect } from 'chai';
import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings';
import { ValidationHandler } from '../src/languageserver/handlers/validationHandlers';
import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls';
import { DEFAULT_KUBERNETES_SCHEMA_VERSION } from '../src/languageservice/utils/schemaUrls';
import { JSONSchema } from '../src/languageservice/jsonSchema';

describe('Validation Tests', () => {
Expand All @@ -17,6 +17,8 @@ describe('Validation Tests', () => {
let yamlSettings: SettingsState;
let schemaProvider: TestCustomSchemaProvider;

const KUBERNETES_SCHEMA_URL = `https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/${DEFAULT_KUBERNETES_SCHEMA_VERSION}-standalone-strict/all.json`;

const toContent = (data: unknown): string => JSON.stringify(data, null, 2);

before(() => {
Expand Down
4 changes: 3 additions & 1 deletion test/schema2020Validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Diagnostic } from 'vscode-languageserver-types';
import { expect } from 'chai';
import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings';
import { ValidationHandler } from '../src/languageserver/handlers/validationHandlers';
import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls';
import { DEFAULT_KUBERNETES_SCHEMA_VERSION } from '../src/languageservice/utils/schemaUrls';
import { JSONSchema } from '../src/languageservice/jsonSchema';

describe('Validation Tests', () => {
Expand All @@ -17,6 +17,8 @@ describe('Validation Tests', () => {
let yamlSettings: SettingsState;
let schemaProvider: TestCustomSchemaProvider;

const KUBERNETES_SCHEMA_URL = `https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/${DEFAULT_KUBERNETES_SCHEMA_VERSION}-standalone-strict/all.json`;

const toContent = (data: unknown): string => JSON.stringify(data, null, 2);

before(() => {
Expand Down
Loading
Loading