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 15b48d3
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 33 deletions.
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,41 @@ For getting info about CRDs:
```bash
kubectl get crd
kubectl describe crd policies.monokle.com

kubectl get monoklepolicy -n webhook-demo
kubectl describe monoklepolicy policy-sample -n webhook-demo
kubectl describe crd policybindings.monokle.com
```

#### Testing

First you need to create policy resource, for example:

```bash
kubectl -n webhook-demo apply -f examples/policy-sample.yaml
kubectl -n webhook-demo apply -f examples/policy-binding-sample.yaml
kubectl apply -f examples/policy-sample.yaml
kubectl apply -f examples/policy-sample-2.yaml
```

Then it needs to be bind to be used for validation. Either without scope (globally to all, but ignored namespaces) or with `matchResource` field:

```bash
kubectl apply -f examples/policy-binding-sample.yaml
kubectl apply -f examples/policy-binding-scoped-sample.yaml
```

> Admission controller will still work without policy resource but then it will be like running validation with all plugins disabled.
You can inspect deployed policies with:

```bash
kubectl get policy
kubectl describe policy

kubectl get policybinding
kubectl describe policybinding
```

Then you can try to create sample resource and see webhook response:

```bash
kubectl -n webhook-demo create -f examples/pod-warning.yaml
kubectl apply -f examples/pod-valid.yaml
kubectl apply -f examples/pod-warning.yaml
kubectl apply -f examples/pod-errors.yaml
```

#### Iterating
Expand Down
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
63 changes: 57 additions & 6 deletions admission-webhook/src/utils/policy-manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {EventEmitter} from "events";
import pino from 'pino';
import {KubernetesObject} from '@kubernetes/client-node';
import {Config} from '@monokle/validation';
import {InformerWrapper} from './get-informer';
import {AdmissionRequestObject} from "./validation-server";

export type MonoklePolicy = KubernetesObject & {
spec: Config
Expand All @@ -10,6 +12,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,16 +28,18 @@ 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?
private _bindings = new Map<string, MonoklePolicyBinding>(); // Map<bindingName, binding>

constructor(
private readonly _policyInformer: InformerWrapper<MonoklePolicy>,
private readonly _bindingInformer: InformerWrapper<MonoklePolicyBinding>,
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 +54,17 @@ 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: AdmissionRequestObject, namespace: string): MonokleApplicablePolicy[] {
this._logger.debug({policies: this._policies.size, bindings: this._bindings.size});

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 +74,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 +90,55 @@ 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: AdmissionRequestObject): boolean {
const namespaceMatchLabels = binding.spec.matchResources?.namespaceSelector?.matchLabels;

this._logger.trace({
msg: 'Checking if resource matches binding',
namespaceMatchLabels,
resourceMetadata: resource.metadata.labels
});

if (!namespaceMatchLabels) {
return true;
}

for (const key of Object.keys(namespaceMatchLabels)) {
if (resource.metadata.labels?.[key] !== namespaceMatchLabels[key]) {
if (!(key === 'namespace' && resource.metadata.namespace === namespaceMatchLabels[key])) {
return false;
}
}
}

return true;
}
}
32 changes: 26 additions & 6 deletions admission-webhook/src/utils/validation-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@ import pino from 'pino';
import path from "path";
import {readFileSync} from "fs";
import {Resource} from "@monokle/validation";
import {V1ValidatingWebhookConfiguration, V1ObjectMeta} from "@kubernetes/client-node";
import {V1ObjectMeta} from "@kubernetes/client-node";
import {ValidatorManager} from "./validator-manager";

export type ValidationServerOptions = {
port: number;
host: string;
};

export type AdmissionRequest = V1ValidatingWebhookConfiguration & {
request?: V1ObjectMeta & {
object?: Resource
export type AdmissionRequestObject = {
apiVersion: string
kind: string
metadata: V1ObjectMeta
spec: any
status: any
};

export type AdmissionRequest = {
apiVersion: string
kind: string
request: {
name: string
namespace: string
uid: string
object: AdmissionRequestObject
}
};

Expand Down Expand Up @@ -44,7 +57,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 +122,19 @@ 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);

this._logger.debug({msg: 'Matching validators', count: validators.length});

if (validators.length === 0) {
return response;
}
Expand Down
37 changes: 25 additions & 12 deletions admission-webhook/src/utils/validator-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pino from 'pino';
import {AnnotationSuppressor, Config, DisabledFixer, FingerprintSuppressor, MonokleValidator, RemotePluginLoader, ResourceParser, SchemaLoader} from "@monokle/validation";
import {MonokleApplicablePolicy, PolicyManager} from "./policy-manager";
import {MonokleApplicablePolicy, MonoklePolicy, PolicyManager} from "./policy-manager";
import {AdmissionRequestObject} from './validation-server';

export type MonokleApplicableValidator = {
validator: MonokleValidator,
Expand All @@ -11,34 +13,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: AdmissionRequestObject, 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 +57,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);
}
}
}
}
2 changes: 1 addition & 1 deletion examples/policy-binding-sample.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: monokle.com/v1alpha1
kind: MonoklePolicyBinding
metadata:
name: policy-sample-binding
name: policy-binding-sample
spec:
policyName: "policy-sample"
validationActions: [Warn]
11 changes: 11 additions & 0 deletions examples/policy-binding-scoped-sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: monokle.com/v1alpha1
kind: MonoklePolicyBinding
metadata:
name: policy-binding-scoped-sample
spec:
policyName: "policy-sample-2"
validationActions: [Warn]
matchResources:
namespaceSelector:
matchLabels:
namespace: default
8 changes: 8 additions & 0 deletions examples/policy-sample-2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: monokle.com/v1alpha1
kind: MonoklePolicy
metadata:
name: policy-sample-2
spec:
plugins:
open-policy-agent: true
pod-security-standards: true
10 changes: 10 additions & 0 deletions k8s/manifests/monokle-policy-binding-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ spec:
items:
type: string
enum: [Warn]
matchResources:
type: object
properties:
namespaceSelector:
type: object
properties:
matchLabels:
type: object
additionalProperties:
type: string
scope: Cluster
names:
plural: policybindings
Expand Down

0 comments on commit 15b48d3

Please sign in to comment.