diff --git a/.changeset/slimy-steaks-walk.md b/.changeset/slimy-steaks-walk.md new file mode 100644 index 000000000..6526b3300 --- /dev/null +++ b/.changeset/slimy-steaks-walk.md @@ -0,0 +1,5 @@ +--- +"@monokle/validation": patch +--- + +Handle messageExpression diff --git a/packages/validation/src/__tests__/MonokleValidator.vap.test.ts b/packages/validation/src/__tests__/MonokleValidator.vap.test.ts index 972dab725..cdb037c17 100644 --- a/packages/validation/src/__tests__/MonokleValidator.vap.test.ts +++ b/packages/validation/src/__tests__/MonokleValidator.vap.test.ts @@ -18,7 +18,7 @@ import { PARAMS_VALIDATING_ADMISSION_POLICY, PARAMS_VALIDATING_ADMISSION_POLICY_BINDING, } from './resources/admissionPolicy/ParamsValidatorResources.js'; -import {CRD, CRD_2, CUSTOM_RESOURCE} from './resources/admissionPolicy/CRDValidatorResources.js'; +import {CRD, CRD_MESSAGE_EXPRESSION, CUSTOM_RESOURCE} from './resources/admissionPolicy/CRDValidatorResources.js'; it('test basic admission policy', async () => { const parser = new ResourceParser(); @@ -73,6 +73,19 @@ it('test crd admission policy', async () => { expect(hasErrors).toBe(1); }); +it('test crd with message expression admission policy', async () => { + const parser = new ResourceParser(); + + const validator = createTestValidator(parser); + + const response = await validator.validate({ + resources: [CRD_MESSAGE_EXPRESSION, CUSTOM_RESOURCE], + }); + + const hasErrors = response.runs.reduce((sum, r) => sum + r.results.length, 0); + expect(hasErrors).toBe(1); +}); + function createTestValidator(parser: ResourceParser, config?: ValidationConfig) { return new MonokleValidator( { diff --git a/packages/validation/src/__tests__/resources/admissionPolicy/BasicValidatorResources.ts b/packages/validation/src/__tests__/resources/admissionPolicy/BasicValidatorResources.ts index cc68735d7..b40423f20 100644 --- a/packages/validation/src/__tests__/resources/admissionPolicy/BasicValidatorResources.ts +++ b/packages/validation/src/__tests__/resources/admissionPolicy/BasicValidatorResources.ts @@ -28,6 +28,7 @@ export const VALIDATING_ADMISSION_POLICY: Resource = { validations: [ { expression: 'object.spec.replicas > 3', + messageExpression: '"spec replicas should be greater than 3"', }, ], }, diff --git a/packages/validation/src/__tests__/resources/admissionPolicy/CRDValidatorResources.ts b/packages/validation/src/__tests__/resources/admissionPolicy/CRDValidatorResources.ts index 708cd4c0b..8b4046155 100644 --- a/packages/validation/src/__tests__/resources/admissionPolicy/CRDValidatorResources.ts +++ b/packages/validation/src/__tests__/resources/admissionPolicy/CRDValidatorResources.ts @@ -143,3 +143,62 @@ export const CUSTOM_RESOURCE: Resource = { id: '13cf92c8d3181f-0', name: 'my-new-cron-object', }; + +export const CRD_MESSAGE_EXPRESSION: Resource = { + fileId: 'd4fc87d28c6bd', + filePath: 'crd.yaml', + fileOffset: 0, + text: 'apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n name: mycustomresources.example.com\nspec:\n group: example.com\n names:\n kind: MyCustomResource\n plural: mycustomresources\n singular: mycustomresource\n shortNames:\n - mcr\n scope: Namespaced\n versions:\n - name: v1\n schema:\n openAPIV3Schema:\n type: object\n properties:\n spec:\n type: object\n x-kubernetes-validation:\n - rule: "self.replicas <= self.maxReplicas"\n message: "replicas must be less than or equal to maxReplicas"\n properties:\n replicas:\n type: integer\n maxReplicas:\n type: integer\n required:\n - replicas\n - maxReplicas\n', + apiVersion: 'apiextensions.k8s.io/v1', + kind: 'CustomResourceDefinition', + content: { + apiVersion: 'apiextensions.k8s.io/v1', + kind: 'CustomResourceDefinition', + metadata: { + name: 'mycustomresources.example.com', + }, + spec: { + group: 'example.com', + names: { + kind: 'MyCustomResource', + plural: 'mycustomresources', + singular: 'mycustomresource', + shortNames: ['mcr'], + }, + scope: 'Namespaced', + versions: [ + { + name: 'v1', + schema: { + openAPIV3Schema: { + type: 'object', + properties: { + spec: { + type: 'object', + 'x-kubernetes-validation': [ + { + rule: 'self.replicas <= self.maxReplicas', + message: 'replicas must be less than or equal to maxReplicas', + messageExpression: '"replicas exceeded max limit of " + string(self.maxReplicas)', + }, + ], + properties: { + replicas: { + type: 'integer', + }, + maxReplicas: { + type: 'integer', + }, + }, + required: ['replicas', 'maxReplicas'], + }, + }, + }, + }, + }, + ], + }, + }, + id: 'd4fc87d28c6bd-0', + name: 'mycustomresources.example.com', +}; diff --git a/packages/validation/src/validators/admission-policy/types.ts b/packages/validation/src/validators/admission-policy/types.ts index 2296459bc..40aa02dd7 100644 --- a/packages/validation/src/validators/admission-policy/types.ts +++ b/packages/validation/src/validators/admission-policy/types.ts @@ -3,7 +3,8 @@ import {RuleLevel} from '../../commonExports.js'; export type Expression = { expression: string; - message: string; + message?: string; + messageExpression?: string; }; export type PolicyExpressionsAndFilteredResources = Record< diff --git a/packages/validation/src/validators/admission-policy/utils.ts b/packages/validation/src/validators/admission-policy/utils.ts index ee02cf32b..569d5ff30 100644 --- a/packages/validation/src/validators/admission-policy/utils.ts +++ b/packages/validation/src/validators/admission-policy/utils.ts @@ -33,7 +33,11 @@ export function getCRDExpressions(schema: CustomSchema, crd: Resource): Record' ? resource.content : resource.content[property]}) - ).output; + const expressionParams = YAML.stringify({ + self: property === '' ? resource.content : resource.content[property], + }); + output = this.evaluateCelExpression(expression, expressionParams); + + if (messageExpression) { + messageExpressionOutput = this.evaluateCelExpression(messageExpression, expressionParams); + } } if (output === 'true' || output.includes('ERROR:') || !output) { return []; } + if ( + messageExpressionOutput && + !messageExpressionOutput.includes('failed to evaluate') && + !messageExpressionOutput.includes('ERROR:') + ) { + ruleMessage = messageExpressionOutput; + } + if (output.includes('failed to evaluate')) { return [ this.adaptToValidationResult( resource, ['kind'], 'VAP002', - message ?? 'Admission policy failed to evaluate expression', + ruleMessage ?? 'Admission policy failed to evaluate expression', level ), ].filter(isDefined); @@ -150,7 +169,7 @@ export class AdmissionPolicyValidator extends AbstractPlugin { resource, ['kind'], 'VAP001', - message ?? 'Admission policy conditions violated', + ruleMessage ?? 'Admission policy conditions violated', level ), ].filter(isDefined); @@ -289,6 +308,7 @@ export class AdmissionPolicyValidator extends AbstractPlugin { expressions: validations.map((v: any) => ({ expression: v.expression, message: v.message, + messageExpression: v.messageExpression, })), level, params, @@ -321,4 +341,8 @@ export class AdmissionPolicyValidator extends AbstractPlugin { level, }); } + + private evaluateCelExpression(expression: string, params: string) { + return (globalThis as any).eval(expression, params).output; + } }