diff --git a/packages/validation/src/__tests__/MonokleValidator.vap.test.ts b/packages/validation/src/__tests__/MonokleValidator.vap.test.ts index dad5597d2..bf1e6c52f 100644 --- a/packages/validation/src/__tests__/MonokleValidator.vap.test.ts +++ b/packages/validation/src/__tests__/MonokleValidator.vap.test.ts @@ -10,7 +10,14 @@ import { VALIDATING_ADMISSION_POLICY, VALIDATING_ADMISSION_POLICY_BINDING, DEPLOYMENT, -} from './admissionPolicyValidatorResources.js'; +} from './resources/admissionPolicy/BasicValidatorResources.js'; +import { + PARAMS_CONFIG_MAP, + PARAMS_DEPLOYMENT, + PARAMS_NAMESPACE, + PARAMS_VALIDATING_ADMISSION_POLICY, + PARAMS_VALIDATING_ADMISSION_POLICY_BINDING, +} from './resources/admissionPolicy/ParamsValidatorResources.js'; it('test basic admission policy', async () => { const parser = new ResourceParser(); @@ -29,6 +36,29 @@ it('test basic admission policy', async () => { expect(hasErrors).toBe(1); }); +it('test params admission policy', async () => { + const parser = new ResourceParser(); + + const validator = createTestValidator(parser, { + plugins: { + 'admission-policy': true, + }, + }); + + const response = await validator.validate({ + resources: [ + PARAMS_NAMESPACE, + PARAMS_VALIDATING_ADMISSION_POLICY, + PARAMS_VALIDATING_ADMISSION_POLICY_BINDING, + PARAMS_DEPLOYMENT, + PARAMS_CONFIG_MAP, + ], + }); + + 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__/admissionPolicyValidatorResources.ts b/packages/validation/src/__tests__/resources/admissionPolicy/BasicValidatorResources.ts similarity index 98% rename from packages/validation/src/__tests__/admissionPolicyValidatorResources.ts rename to packages/validation/src/__tests__/resources/admissionPolicy/BasicValidatorResources.ts index f741ef4e5..cc68735d7 100644 --- a/packages/validation/src/__tests__/admissionPolicyValidatorResources.ts +++ b/packages/validation/src/__tests__/resources/admissionPolicy/BasicValidatorResources.ts @@ -1,4 +1,4 @@ -import {Resource} from '../index.js'; +import {Resource} from '../../../common/types.js'; export const VALIDATING_ADMISSION_POLICY: Resource = { fileId: '18acb3eb78d60', diff --git a/packages/validation/src/__tests__/resources/admissionPolicy/ParamsValidatorResources.ts b/packages/validation/src/__tests__/resources/admissionPolicy/ParamsValidatorResources.ts new file mode 100644 index 000000000..f8dd221a0 --- /dev/null +++ b/packages/validation/src/__tests__/resources/admissionPolicy/ParamsValidatorResources.ts @@ -0,0 +1,166 @@ +import {Resource} from '../../../common/types.js'; + +export const PARAMS_CONFIG_MAP: Resource = { + fileId: '174d42e877a174', + filePath: 'config-map.yaml', + fileOffset: 0, + text: 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: rule-config\n namespace: demo\nmaxReplicas: 5\n', + apiVersion: 'v1', + kind: 'ConfigMap', + content: { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { + name: 'rule-config', + namespace: 'demo', + }, + maxReplicas: 5, + }, + id: '174d42e877a174-0', + name: 'rule-config', + namespace: 'demo', +}; + +export const PARAMS_DEPLOYMENT: Resource = { + fileId: '2f92c46b9eb02', + filePath: 'deployment.yaml', + fileOffset: 0, + text: 'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: nginx-deployment\n namespace: demo\n labels:\n app: nginx\nspec:\n replicas: 5\n selector:\n matchLabels:\n app: nginx\n template:\n metadata:\n labels:\n app: nginx\n spec:\n containers:\n - name: nginx\n image: nginx:1.14.2\n ports:\n - containerPort: 80\n', + apiVersion: 'apps/v1', + kind: 'Deployment', + content: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'nginx-deployment', + namespace: 'demo', + labels: { + app: 'nginx', + }, + }, + spec: { + replicas: 4, + selector: { + matchLabels: { + app: 'nginx', + }, + }, + template: { + metadata: { + labels: { + app: 'nginx', + }, + }, + spec: { + containers: [ + { + name: 'nginx', + image: 'nginx:1.14.2', + ports: [ + { + containerPort: 80, + }, + ], + }, + ], + }, + }, + }, + }, + id: '2f92c46b9eb02-0', + name: 'nginx-deployment', + namespace: 'demo', +}; + +export const PARAMS_NAMESPACE: Resource = { + fileId: '1926d9cf253e4c', + filePath: 'namespace.yaml', + fileOffset: 0, + text: 'apiVersion: v1\nkind: Namespace\nmetadata:\n name: demo\n labels:\n environment: test\n', + apiVersion: 'v1', + kind: 'Namespace', + content: { + apiVersion: 'v1', + kind: 'Namespace', + metadata: { + name: 'demo', + labels: { + environment: 'test', + }, + }, + }, + id: '1926d9cf253e4c-0', + name: 'demo', +}; + +export const PARAMS_VALIDATING_ADMISSION_POLICY: Resource = { + fileId: '18acb3eb78d60', + filePath: 'policy.yaml', + fileOffset: 0, + text: 'apiVersion: admissionregistration.k8s.io/v1beta1\nkind: ValidatingAdmissionPolicy\nmetadata:\n name: "demo-policy.example.com"\nspec:\n paramKind:\n apiVersion: v1\n kind: ConfigMap\n matchConstraints:\n resourceRules:\n - apiGroups: [ "apps" ]\n apiVersions: [ "v1" ]\n operations: [ "CREATE", "UPDATE" ]\n resources: [ "deployments" ]\n validations:\n - expression: "object.spec.replicas > 6"\n', + apiVersion: 'admissionregistration.k8s.io/v1beta1', + kind: 'ValidatingAdmissionPolicy', + content: { + apiVersion: 'admissionregistration.k8s.io/v1beta1', + kind: 'ValidatingAdmissionPolicy', + metadata: { + name: 'demo-policy.example.com', + }, + spec: { + paramKind: { + apiVersion: 'v1', + kind: 'ConfigMap', + }, + matchConstraints: { + resourceRules: [ + { + apiGroups: ['apps'], + apiVersions: ['v1'], + operations: ['CREATE', 'UPDATE'], + resources: ['deployments'], + }, + ], + }, + validations: [ + { + expression: 'object.spec.replicas > params.maxReplicas', + }, + ], + }, + }, + id: '18acb3eb78d60-0', + name: 'demo-policy.example.com', +}; + +export const PARAMS_VALIDATING_ADMISSION_POLICY_BINDING: Resource = { + fileId: '19b972898cc6be', + filePath: 'policy-binding.yaml', + fileOffset: 0, + text: 'apiVersion: admissionregistration.k8s.io/v1beta1\nkind: ValidatingAdmissionPolicyBinding\nmetadata:\n name: "demo-binding-test.example.com"\nspec:\n policyName: "demo-policy.example.com"\n paramRef:\n name: rule-config\n namespace: demo\n validationActions: [ Deny ]\n matchResources:\n namespaceSelector:\n matchLabels:\n environment: test\n', + apiVersion: 'admissionregistration.k8s.io/v1beta1', + kind: 'ValidatingAdmissionPolicyBinding', + content: { + apiVersion: 'admissionregistration.k8s.io/v1beta1', + kind: 'ValidatingAdmissionPolicyBinding', + metadata: { + name: 'demo-binding-test.example.com', + }, + spec: { + policyName: 'demo-policy.example.com', + paramRef: { + name: 'rule-config', + namespace: 'demo', + }, + validationActions: ['Deny'], + matchResources: { + namespaceSelector: { + matchLabels: { + environment: 'test', + }, + }, + }, + }, + }, + id: '19b972898cc6be-0', + name: 'demo-binding-test.example.com', +}; diff --git a/packages/validation/src/validators/admission-policy/types.ts b/packages/validation/src/validators/admission-policy/types.ts index 39236b59d..55c80960c 100644 --- a/packages/validation/src/validators/admission-policy/types.ts +++ b/packages/validation/src/validators/admission-policy/types.ts @@ -8,7 +8,15 @@ export type Expression = { export type PolicyExpressionsAndFilteredResources = Record< string, - {expressions: Expression[]; resources: Resource[]; level: RuleLevel} + {expressions: Expression[]; resources: Resource[]; level: RuleLevel; params?: any} >; -export type PolicyBindingFilterResponse = Record; +export type ParamRef = { + name: string; + namespace: string; +}; + +export type PolicyBindingFilterResponse = Record< + string, + {resources: Resource[]; level: RuleLevel; paramRef?: ParamRef} +>; diff --git a/packages/validation/src/validators/admission-policy/validator.ts b/packages/validation/src/validators/admission-policy/validator.ts index e171a7254..3b6d44106 100644 --- a/packages/validation/src/validators/admission-policy/validator.ts +++ b/packages/validation/src/validators/admission-policy/validator.ts @@ -42,12 +42,12 @@ export class AdmissionPolicyValidator extends AbstractPlugin { const resourcesToBeValidated = this.getResourcesToBeValidated(resources); - for (const [policyName, {resources: filteredResources, expressions, level}] of Object.entries( + for (const [policyName, {resources: filteredResources, expressions, level, params}] of Object.entries( resourcesToBeValidated )) { for (const resource of filteredResources) { for (const expression of expressions) { - const errors = await this.validateResource(resource, expression, level); + const errors = await this.validateResource(resource, expression, level, params); results.push(...errors); } @@ -60,9 +60,10 @@ export class AdmissionPolicyValidator extends AbstractPlugin { private async validateResource( resource: Resource, {message, expression}: Expression, - level: RuleLevel + level: RuleLevel, + params?: any ): Promise { - const output = (globalThis as any).eval(expression, YAML.stringify({object: resource.content})).output; + const output = (globalThis as any).eval(expression, YAML.stringify({object: resource.content, params})).output; if (output === 'true' || output.includes('ERROR:')) { return []; @@ -133,6 +134,7 @@ export class AdmissionPolicyValidator extends AbstractPlugin { policyFilteredResources[policyName] = { resources: filteredResources, level: policyBinding.content?.spec?.validationActions?.includes('Deny') ? 'error' : 'warning', + paramRef: policyBinding.content?.spec?.paramRef, }; } @@ -145,7 +147,7 @@ export class AdmissionPolicyValidator extends AbstractPlugin { ): PolicyExpressionsAndFilteredResources { const filteredResourcesWithExpressions: PolicyExpressionsAndFilteredResources = {}; - for (const [policyName, {resources, level}] of Object.entries(policyFilteredResources)) { + for (const [policyName, {resources, level, paramRef}] of Object.entries(policyFilteredResources)) { const policy = policies.find(p => p.name === policyName); if (!policy) continue; @@ -188,6 +190,24 @@ export class AdmissionPolicyValidator extends AbstractPlugin { if (!filteredResourcesByPolicy.length) continue; + let params: any; + + const paramKind = policy.content?.spec?.paramKind; + + if (paramKind && paramRef) { + const paramResource = resources.find( + r => + r.kind === paramKind?.kind && + r.apiVersion === paramKind?.apiVersion && + r.content?.metadata?.name === paramRef.name && + r.content?.metadata?.namespace === paramRef.namespace + ); + + if (paramResource) { + params = paramResource.content; + } + } + filteredResourcesWithExpressions[policy.name] = { resources: filteredResourcesByPolicy, expressions: validations.map((v: any) => ({ @@ -195,6 +215,7 @@ export class AdmissionPolicyValidator extends AbstractPlugin { message: v.message, })), level, + params, }; }