Skip to content

Commit

Permalink
feat: add namespace filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
f1ames committed Oct 2, 2023
1 parent 13a9515 commit 1dd65a4
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 22 deletions.
2 changes: 1 addition & 1 deletion admission-webhook/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
52 changes: 46 additions & 6 deletions admission-webhook/src/utils/policy-manager.ts
Original file line number Diff line number Diff line change
@@ -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 & {
Expand All @@ -10,6 +11,11 @@ export type MonoklePolicy = KubernetesObject & {
export type MonoklePolicyBindingConfiguration = {
policyName: string
validationActions: ['Warn']
matchResources?: {
namespaceSelector?: {
matchLabels?: Record<string, string>
}
}
}

export type MonoklePolicyBinding = KubernetesObject & {
Expand All @@ -21,7 +27,7 @@ export type MonokleApplicablePolicy = {
binding: MonoklePolicyBindingConfiguration
}

export class PolicyManager {
export class PolicyManager extends EventEmitter{
private _policies = new Map<string, MonoklePolicy>(); // Map<policyName, policy>
private _bindings = new Map<string, MonoklePolicyBinding>(); // Map<bindingName, binding> // @TODO use policyName as key instead of bindingName?

Expand All @@ -31,6 +37,8 @@ export class PolicyManager {
private readonly _ignoreNamespaces: string[],
private readonly _logger: ReturnType<typeof pino>,
) {
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));
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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);
}
}

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;
}
}
8 changes: 6 additions & 2 deletions admission-webhook/src/utils/validation-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down Expand Up @@ -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;
}
Expand Down
38 changes: 25 additions & 13 deletions admission-webhook/src/utils/validator-manager.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,34 +12,42 @@ export class ValidatorManager {

constructor(
private readonly _policyManager: PolicyManager,
private readonly _logger: ReturnType<typeof pino>,
) {
// @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 [];
}

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(
{
Expand All @@ -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);
}
}
}
}
11 changes: 11 additions & 0 deletions examples/policy-binding-sample-scoped.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 1dd65a4

Please sign in to comment.