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
Original file line number Diff line number Diff line change
@@ -1,49 +1,82 @@
import { SingleYAMLDocument } from '../parser/yamlParser07';
import { JSONDocument } from '../parser/jsonDocument';
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';

/**
* Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document.
* 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
* Attempt to retrieve the schema for a given YAML document based on the Kubernetes GroupVersionKind (GVK).
*
* First, checks for a schema for a matching builtin resource, then it checks for a schema for a CRD.
*
* @param doc the yaml document being validated
* @param kubernetesSchema the resolved copy of the Kubernetes builtin
* @param crdCatalogURI the catalog uri to use to find schemas for custom resource definitions
* @returns a schema uri, or undefined if no specific schema can be identified
*/
export function autoDetectKubernetesSchemaFromDocument(
export function autoDetectKubernetesSchema(
doc: SingleYAMLDocument | JSONDocument,
crdCatalogURI: string,
kubernetesSchema: ResolvedSchema
kubernetesSchema: ResolvedSchema,
crdCatalogURI: string
): string | undefined {
const res = getGroupVersionKindFromDocument(doc);
if (!res) {
const gvk = getGroupVersionKindFromDocument(doc);
if (!gvk || !gvk.group || !gvk.version || !gvk.kind) {
return undefined;
}
const { group, version, kind } = res;
if (!group || !version || !kind) {
return undefined;
const builtinResource = autoDetectBuiltinResource(gvk, kubernetesSchema);
if (builtinResource) {
return builtinResource;
}
const customResource = autoDetectCustomResource(gvk, crdCatalogURI);
if (customResource) {
return customResource;
}
return undefined;
}

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

const groupWithoutK8sIO = group.replace('.k8s.io', '').replace('rbac.authorization', 'rbac');
const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`;
const k8sSchema: JSONSchema = kubernetesSchema.schema;
const kubernetesBuildIns: string[] = (k8sSchema.oneOf || [])
const matchingBuiltin: string | undefined = (k8sSchema.oneOf || [])
.map((s) => {
if (typeof s === 'boolean') {
return undefined;
}
return s._$ref || s.$ref;
})
.filter((ref) => ref)
.map((ref) => ref.replace('_definitions.json#/definitions/', '').toLowerCase());
const groupWithoutK8sIO = group.replace('.k8s.io', '').replace('rbac.authorization', 'rbac');
const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`;
.find((ref) => {
if (!ref) {
return false;
}
const lowercaseRef = ref.replace('_definitions.json#/definitions/', '').toLowerCase();
return lowercaseRef === k8sTypeName;
});

if (kubernetesBuildIns.includes(k8sTypeName)) {
return undefined;
if (matchingBuiltin) {
return BASE_KUBERNETES_SCHEMA_URL + matchingBuiltin;
}

return undefined;
}

/**
* Retrieve schema by auto-detecting the Kubernetes GroupVersionKind (GVK) from the document.
* 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
*/
export function autoDetectCustomResource(gvk: GroupVersionKind, crdCatalogURI: string): string | undefined {
const { group, version, kind } = gvk;

const groupWithoutK8sIO = group.replace('.k8s.io', '').replace('rbac.authorization', 'rbac');
const k8sTypeName = `io.k8s.api.${groupWithoutK8sIO.toLowerCase()}.${version.toLowerCase()}.${kind.toLowerCase()}`;

if (k8sTypeName.includes('openshift.io')) {
return `${crdCatalogURI}/openshift/v4.15-strict/${kind.toLowerCase()}_${group.toLowerCase()}_${version.toLowerCase()}.json`;
}
Expand All @@ -52,14 +85,18 @@ export function autoDetectKubernetesSchemaFromDocument(
return schemaURL;
}

type GroupVersionKind = {
group: string;
version: string;
kind: string;
};

/**
* 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 {
export function getGroupVersionKindFromDocument(doc: SingleYAMLDocument | JSONDocument): GroupVersionKind | undefined {
if (doc instanceof SingleYAMLDocument) {
try {
const rootJSON = doc.root.internalNode.toJSON();
Expand Down
32 changes: 16 additions & 16 deletions src/languageservice/services/yamlSchemaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { JSONSchema, JSONSchemaRef, JSONSchemaMap, SchemaDialect } from '../jsonSchema';
import { SchemaPriority, SchemaRequestService, WorkspaceContextService } from '../yamlLanguageService';
import { SettingsState } from '../../yamlSettings';
import {
UnresolvedSchema,
ResolvedSchema,
ISchemaContributions,
JSONSchemaService,
ResolvedSchema,
SchemaDependencies,
ISchemaContributions,
SchemaHandle,
UnresolvedSchema,
} from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';
import { SettingsState } from '../../yamlSettings';
import { JSONSchema, JSONSchemaMap, JSONSchemaRef, SchemaDialect } from '../jsonSchema';
import { SchemaPriority, SchemaRequestService, WorkspaceContextService } from '../yamlLanguageService';

import { URI } from 'vscode-uri';
import * as l10n from '@vscode/l10n';
import { SingleYAMLDocument } from '../parser/yamlParser07';
import { JSONDocument } from '../parser/jsonDocument';
import * as path from 'path';
import { getSchemaFromModeline } from './modelineUtil';
import { URI } from 'vscode-uri';
import { JSONSchemaDescriptionExt } from '../../requestTypes';
import { JSONDocument } from '../parser/jsonDocument';
import { SingleYAMLDocument } from '../parser/yamlParser07';
import { SchemaVersions } from '../yamlTypes';
import { getSchemaFromModeline } from './modelineUtil';

import { parse } from 'yaml';
import * as Json from 'jsonc-parser';
import Ajv, { DefinedError, type AnySchemaObject, type ValidateFunction } from 'ajv';
import Ajv4 from 'ajv-draft-04';
import Ajv2019 from 'ajv/dist/2019';
import Ajv2020 from 'ajv/dist/2020';
import { autoDetectKubernetesSchemaFromDocument } from './crdUtil';
import * as Json from 'jsonc-parser';
import { parse } from 'yaml';
import { CRD_CATALOG_URL, KUBERNETES_SCHEMA_URL } from '../utils/schemaUrls';
import { autoDetectKubernetesSchema } from './k8sSchemaUtil';

const ajv4 = new Ajv4({ allErrors: true });
const ajv7 = new Ajv({ allErrors: true });
Expand Down Expand Up @@ -992,10 +992,10 @@ export class YAMLSchemaService extends JSONSchemaService {
if (!k8sAllSchema) {
k8sAllSchema = await this.getResolvedSchema(KUBERNETES_SCHEMA_URL);
}
const kubeSchema = autoDetectKubernetesSchemaFromDocument(
const kubeSchema = autoDetectKubernetesSchema(
doc,
this.yamlSettings.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL,
k8sAllSchema
k8sAllSchema,
this.yamlSettings.kubernetesCRDStoreUrl ?? CRD_CATALOG_URL
);
if (kubeSchema) {
schemas.push(kubeSchema);
Expand Down
5 changes: 3 additions & 2 deletions src/languageservice/utils/schemaUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { JSONSchema, JSONSchemaRef } from '../jsonSchema';
import { isBoolean } from './objects';
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 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 JSON_SCHEMASTORE_URL = 'https://www.schemastore.org/api/json/catalog.json';
export const CRD_CATALOG_URL = 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main';

Expand Down
2 changes: 1 addition & 1 deletion test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings';
import { Diagnostic, MarkupContent, Position } from 'vscode-languageserver-types';
import { LineCounter } from 'yaml';
import { getSchemaFromModeline } from '../src/languageservice/services/modelineUtil';
import { getGroupVersionKindFromDocument } from '../src/languageservice/services/crdUtil';
import { getGroupVersionKindFromDocument } from '../src/languageservice/services/k8sSchemaUtil';

const requestServiceMock = function (uri: string): Promise<string> {
return Promise.reject<string>(`Resource ${uri} not found.`);
Expand Down
38 changes: 38 additions & 0 deletions test/schemaValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,10 @@ obj:
});

describe('Test with custom kubernetes schemas', function () {
after(() => {
languageSettingsSetup.languageSettings.schemas.pop();
languageService.configure(languageSettingsSetup.languageSettings);
});
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 @@ -1257,6 +1261,40 @@ obj:
})
.then(done, done);
});

it('Test that it validates against the correct schema based on the GroupVersionKind', (done) => {
languageService.configure(
languageSettingsSetup.withKubernetes().withSchemaFileMatch({ uri: KUBERNETES_SCHEMA_URL, fileMatch: ['*.yml', '*.yaml'] })
.languageSettings
);
yamlSettings.specificValidatorPaths = ['*.yml', '*.yaml'];
const content = `apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: foo
spec:
foo: bar
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: foo
minReplicas: 2
maxReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80`;
const validator = parseSetup(content);
validator
.then(function (result) {
assert.equal(result.length, 1);
assert.equal(result[0].message, `Property foo is not allowed.`);
})
.then(done, done);
});
});

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