diff --git a/admission-webhook/src/index.ts b/admission-webhook/src/index.ts index 66934d3..7f8f730 100644 --- a/admission-webhook/src/index.ts +++ b/admission-webhook/src/index.ts @@ -32,7 +32,7 @@ const logger = pino({ ); const policyManager = new PolicyManager(policyInformer, bindingsInformer, IGNORED_NAMESPACES, logger); - const validatorManager = new ValidatorManager(policyManager); + const validatorManager = new ValidatorManager(policyManager, logger); await policyManager.start(); diff --git a/admission-webhook/src/utils/policy-manager.ts b/admission-webhook/src/utils/policy-manager.ts index 4f43cc4..c02654f 100644 --- a/admission-webhook/src/utils/policy-manager.ts +++ b/admission-webhook/src/utils/policy-manager.ts @@ -1,6 +1,7 @@ +import {EventEmitter} from "events"; import pino from 'pino'; import {KubernetesObject} from '@kubernetes/client-node'; -import {Config} from '@monokle/validation'; +import {Config, Resource} from '@monokle/validation'; import {InformerWrapper} from './get-informer'; export type MonoklePolicy = KubernetesObject & { @@ -10,6 +11,11 @@ export type MonoklePolicy = KubernetesObject & { export type MonoklePolicyBindingConfiguration = { policyName: string validationActions: ['Warn'] + matchResources?: { + namespaceSelector?: { + matchLabels?: Record + } + } } export type MonoklePolicyBinding = KubernetesObject & { @@ -21,7 +27,7 @@ export type MonokleApplicablePolicy = { binding: MonoklePolicyBindingConfiguration } -export class PolicyManager { +export class PolicyManager extends EventEmitter{ private _policies = new Map(); // Map private _bindings = new Map(); // Map // @TODO use policyName as key instead of bindingName? @@ -31,6 +37,8 @@ export class PolicyManager { private readonly _ignoreNamespaces: string[], private readonly _logger: ReturnType, ) { + super(); + this._policyInformer.informer.on('add', this.onPolicy.bind(this)); this._policyInformer.informer.on('update', this.onPolicy.bind(this)); this._policyInformer.informer.on('delete', this.onPolicyRemoval.bind(this)); @@ -45,11 +53,15 @@ export class PolicyManager { await this._bindingInformer.start(); } - getMatchingPolicies(): MonokleApplicablePolicy[] { // @TODO pass resource data so it can be matched according to matchResources definition (when it's implemented) + getMatchingPolicies(resource: Resource, namespace: string): MonokleApplicablePolicy[] { if (this._bindings.size === 0) { return []; } + if (this._ignoreNamespaces.includes(namespace)) { + return []; + } + return Array.from(this._bindings.values()) .map((binding) => { const policy = this._policies.get(binding.spec.policyName); @@ -59,6 +71,10 @@ export class PolicyManager { return null; } + if (binding.spec.matchResources && !this.isResourceMatching(binding, resource)) { + return null; + } + return { policy: policy.spec, binding: binding.spec @@ -71,23 +87,47 @@ export class PolicyManager { this._logger.debug({msg: 'Policy updated', policy}); this._policies.set(policy.metadata!.name!, policy); + + this.emit('policyUpdated', policy); } private onPolicyRemoval(policy: MonoklePolicy) { - this._logger.debug({msg: 'Policy updated', policy}); + this._logger.debug({msg: 'Policy removed', policy}); this._policies.delete(policy.metadata!.name!); + + this.emit('policyRemoved', policy); } private onBinding(binding: MonoklePolicyBinding) { this._logger.debug({msg: 'Binding updated', binding}); this._bindings.set(binding.metadata!.name!, binding); + + this.emit('bindingUpdated', binding); } private onBindingRemoval(binding: MonoklePolicyBinding) { - this._logger.debug({msg: 'Binding updated', binding}); + this._logger.debug({msg: 'Binding removed', binding}); this._bindings.delete(binding.metadata!.name!); + + this.emit('bindingRemoved', binding); } -} \ No newline at end of file + + private isResourceMatching(binding: MonoklePolicyBinding, resource: Resource): boolean { + const namespaceMatchLabels = binding.spec.matchResources?.namespaceSelector?.matchLabels; + + if (!namespaceMatchLabels) { + return true; + } + + for (const key of Object.keys(namespaceMatchLabels)) { + if (resource.content?.metadata?.labels?.[key] !== namespaceMatchLabels[key]) { + return false; + } + } + + return true; + } +} diff --git a/admission-webhook/src/utils/validation-server.ts b/admission-webhook/src/utils/validation-server.ts index a8e885d..a070575 100644 --- a/admission-webhook/src/utils/validation-server.ts +++ b/admission-webhook/src/utils/validation-server.ts @@ -44,7 +44,6 @@ export class ValidationServer { this._shouldValidate = false; this._server = fastify({ - // @TODO do not require certs when running locally (for testing outside K8s cluster) https: { key: readFileSync(path.join('/run/secrets/tls', 'tls.key')), cert: readFileSync(path.join('/run/secrets/tls', 'tls.crt')) @@ -110,11 +109,16 @@ export class ValidationServer { if (!namespace) { this._logger.error({msg: 'No namespace found', metadata: body.request}); + return response; + } + const resource = body.request?.object; + if (!resource) { + this._logger.error({msg: 'No resource found', metadata: body.request}); return response; } - const validators = this._validators.getMatchingValidators(); + const validators = this._validators.getMatchingValidators(resource, namespace); if (validators.length === 0) { return response; } diff --git a/admission-webhook/src/utils/validator-manager.ts b/admission-webhook/src/utils/validator-manager.ts index ae9c190..988347c 100644 --- a/admission-webhook/src/utils/validator-manager.ts +++ b/admission-webhook/src/utils/validator-manager.ts @@ -1,5 +1,6 @@ -import {AnnotationSuppressor, Config, DisabledFixer, FingerprintSuppressor, MonokleValidator, RemotePluginLoader, ResourceParser, SchemaLoader} from "@monokle/validation"; -import {MonokleApplicablePolicy, PolicyManager} from "./policy-manager"; +import pino from 'pino'; +import {AnnotationSuppressor, Config, DisabledFixer, FingerprintSuppressor, MonokleValidator, RemotePluginLoader, Resource, ResourceParser, SchemaLoader} from "@monokle/validation"; +import {MonokleApplicablePolicy, MonoklePolicy, PolicyManager} from "./policy-manager"; export type MonokleApplicableValidator = { validator: MonokleValidator, @@ -11,14 +12,19 @@ export class ValidatorManager { constructor( private readonly _policyManager: PolicyManager, + private readonly _logger: ReturnType, ) { - // @TODO implement this._policyManager.on(policyUpdated, this._reloadValidator) - // We should preload configuration here instead of in getMatchingValidators since - // it would affect performance of the admission webhook response time + this._policyManager.on('policyUpdated', async (policy: MonoklePolicy) => { + await this.setupValidator(policy.metadata!.name!, policy.spec); + }); + + this._policyManager.on('policyRemoved', async (policy: MonoklePolicy) => { + await this._validators.delete(policy.metadata!.name!); + }); } - getMatchingValidators(): MonokleApplicableValidator[] { - const matchingPolicies = this._policyManager.getMatchingPolicies(); + getMatchingValidators(resource: Resource, namespace: string): MonokleApplicableValidator[] { + const matchingPolicies = this._policyManager.getMatchingPolicies(resource, namespace); if (matchingPolicies.length === 0) { return []; @@ -26,19 +32,22 @@ export class ValidatorManager { return matchingPolicies.map((policy) => { if (!this._validators.has(policy.binding.policyName)) { - this.setupValidator(policy.binding.policyName, policy.policy); + // This should not happen and means there is a bug in other place in the code. Raise warning and skip. + // Do not create validator instance here to keep this function sync and to keep processing time low. + this._logger.warn({msg: 'ValidatorManager: Validator not found', policyName: policy.binding.policyName}); + return null; } return { validator: this._validators.get(policy.binding.policyName)!, policy } - }); + }).filter((validator) => validator !== null) as MonokleApplicableValidator[]; } private async setupValidator(policyName: string, policy: Config) { if (this._validators.has(policyName)) { - this._validators.get(policyName)!.preload(policy); + await this._validators.get(policyName)!.preload(policy); } else { const validator = new MonokleValidator( { @@ -47,11 +56,14 @@ export class ValidatorManager { schemaLoader: new SchemaLoader(), suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], fixer: new DisabledFixer(), - }, - policy + } ); + // Run separately (instead of passing config to constructor) to make sure that validator + // is ready when 'setupValidator' function call fulfills. + await validator.preload(policy); + this._validators.set(policyName, validator); } } -} \ No newline at end of file +} diff --git a/examples/policy-binding-sample-scoped.yaml b/examples/policy-binding-sample-scoped.yaml new file mode 100644 index 0000000..99cec32 --- /dev/null +++ b/examples/policy-binding-sample-scoped.yaml @@ -0,0 +1,11 @@ +apiVersion: monokle.com/v1alpha1 +kind: MonoklePolicyBinding +metadata: + name: policy-sample-binding +spec: + policyName: "policy-sample" + validationActions: [Warn] + matchResources: + namespaceSelector: + matchLabels: + namespace: default \ No newline at end of file