Skip to content

Commit

Permalink
feat: introduce MonoklePolicyBinding [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
f1ames committed Sep 28, 2023
1 parent 80249e1 commit 6f908b3
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 124 deletions.
111 changes: 62 additions & 49 deletions admission-webhook/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,83 @@
import pino from 'pino';
import {AnnotationSuppressor, FingerprintSuppressor, MonokleValidator, RemotePluginLoader, SchemaLoader, DisabledFixer, ResourceParser} from '@monokle/validation';
import {Config} from '@monokle/validation';
import {MonoklePolicy, getInformer} from './utils/informer.js';
import {ValidationServer} from './utils/validation-server.js';
import {getInformer} from './utils/get-informer.js';
import {MonoklePolicy, MonoklePolicyBinding, PolicyManager} from './utils/policy-manager.js';

const logger = pino({
name: 'Monokle',
level: 'trace',
});

const policies: Map<string, Config> = new Map();
const validators: Map<string, MonokleValidator> = new Map();

(async() => {
// INFORMER
const onPolicy = async (policy: MonoklePolicy) => {
logger.info({msg: 'Informer: Policy updated', policy});

const policyNamespace = policy.metadata?.namespace;
if (!policyNamespace) {
logger.error({msg: 'Informer: Policy namespace is empty', metadata: policy.metadata});
return;
const policyInformer = await getInformer<MonoklePolicy>(
'monokle.io',
'v1',
'policies',
(err: any) => {
logger.error({msg: 'Informer: Policies: Error', err});
}
);

policies.set(policyNamespace, policy.spec);

if (!validators.has(policyNamespace)) {
validators.set(
policyNamespace,
new MonokleValidator(
{
loader: new RemotePluginLoader(),
parser: new ResourceParser(),
schemaLoader: new SchemaLoader(),
suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()],
fixer: new DisabledFixer(),
},
{}
)
);
const bindingsInformer = await getInformer<MonoklePolicyBinding>(
'monokle.io',
'v1',
'policybindings',
(err: any) => {
logger.error({msg: 'Informer: Bindings: Error', err});
}
);

await validators.get(policyNamespace)!.preload(policy.spec);
}
const policyManager = new PolicyManager(policyInformer, bindingsInformer, logger);

const onPolicyRemoval = async (policy: MonoklePolicy) => {
logger.info('Informer: Policy removed');
// POLICY MANAGER

const policyNamespace = policy.metadata?.namespace;
if (!policyNamespace) {
logger.error({msg: 'Informer: Policy namespace is empty', metadata: policy.metadata});
return;
}
// // INFORMER
// const onPolicy = async (policy: MonoklePolicy) => {
// logger.info({msg: 'Informer: Policy updated', policy});

// const policyNamespace = policy.metadata?.namespace;
// if (!policyNamespace) {
// logger.error({msg: 'Informer: Policy namespace is empty', metadata: policy.metadata});
// return;
// }

// policies.set(policyNamespace, policy.spec);

// if (!validators.has(policyNamespace)) {
// validators.set(
// policyNamespace,
// new MonokleValidator(
// {
// loader: new RemotePluginLoader(),
// parser: new ResourceParser(),
// schemaLoader: new SchemaLoader(),
// suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()],
// fixer: new DisabledFixer(),
// },
// {}
// )
// );
// }

// await validators.get(policyNamespace)!.preload(policy.spec);
// }

// const onPolicyRemoval = async (policy: MonoklePolicy) => {
// logger.info('Informer: Policy removed');

policies.delete(policyNamespace);
validators.delete(policyNamespace);
}
// const policyNamespace = policy.metadata?.namespace;
// if (!policyNamespace) {
// logger.error({msg: 'Informer: Policy namespace is empty', metadata: policy.metadata});
// return;
// }

const onError = (err: any) => {
logger.error({msg: 'Informer: Error', err});
}
// policies.delete(policyNamespace);
// validators.delete(policyNamespace);
// }

const informer = await getInformer(onPolicy, onPolicy, onPolicyRemoval, onError);

// SERVER
const server = new ValidationServer(validators, logger);
// // SERVER
// const server = new ValidationServer(validators, logger);

await server.start();
// await server.start();
})();
47 changes: 47 additions & 0 deletions admission-webhook/src/utils/get-informer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import k8s from '@kubernetes/client-node';

export type Informer<TValue extends k8s.KubernetesObject> = k8s.Informer<TValue> & k8s.ObjectCache<TValue>;

const ERROR_RESTART_INTERVAL = 500;

export async function getInformer<TValue extends k8s.KubernetesObject>(group: string, version: string, plural: string, onError?: k8s.ErrorCallback) {
let informer: Informer<TValue> | null = null;

let tries = 0;
while (!informer) {
try {
tries++;
informer = await createInformer<TValue>(group, version, plural, onError);
return informer;
} catch (err: any) {
if (onError) {
onError(err);
}

await new Promise((resolve) => setTimeout(resolve, ERROR_RESTART_INTERVAL));
}
}

return informer;
}

async function createInformer<TValue extends k8s.KubernetesObject>(group: string, version: string, plural: string, onError?: k8s.ErrorCallback) {
const kc = new k8s.KubeConfig();
kc.loadFromCluster();

const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi)
const listFn = () => k8sApi.listClusterCustomObject(group, version, plural);
const informer = k8s.makeInformer<TValue>(kc, `/apis/${group}/${version}/${plural}`, listFn as any);

informer.on('error', (err) => {
if (onError) {
onError(err);
}

setTimeout(async () => {
await informer.start();
}, ERROR_RESTART_INTERVAL);
});

return informer;
}
66 changes: 0 additions & 66 deletions admission-webhook/src/utils/informer.ts

This file was deleted.

79 changes: 79 additions & 0 deletions admission-webhook/src/utils/policy-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import pino from 'pino';
import {KubernetesObject} from '@kubernetes/client-node';
import {Config} from '@monokle/validation';
import {Informer} from './get-informer';

export type MonoklePolicy = KubernetesObject & {
spec: Config
}

export type MonoklePolicyBinding = KubernetesObject & {
spec: {
policyName: string
validationActions: 'Warn'
}
}

export class PolicyManager {
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?

constructor(
private readonly _policyInformer: Informer<MonoklePolicy>,
private readonly _bindingInformer: Informer<MonoklePolicyBinding>,
private readonly _logger: ReturnType<typeof pino>,
) {
this._policyInformer.on('add', this.onPolicy);
this._policyInformer.on('update', this.onPolicy);
this._policyInformer.on('delete', this.onPolicyRemoval);

this._bindingInformer.on('add', this.onBinding);
this._bindingInformer.on('update', this.onBinding);
this._bindingInformer.on('delete', this.onBindingRemoval);
}

async start() {
await this._policyInformer.start();
await this._bindingInformer.start();
}

getMatchingPolicies() { // @TODO pass resource data so it can be matched according to matchResources definition (when it's implemented)
if (this._bindings.size === 0) {
return [];
}

return Array.from(this._bindings.values()).map((binding) => {
const policy = this._policies.get(binding.spec.policyName);

if (!policy) {
this._logger.error({msg: 'Binding is pointing to missing policy', binding});
}

return policy;
}).filter((policy) => policy !== undefined);
}

private onPolicy(policy: MonoklePolicy) {
this._logger.debug({msg: 'Policy updated', policy});

this._policies.set(policy.metadata!.name!, policy);
}

private onPolicyRemoval(policy: MonoklePolicy) {
this._logger.debug({msg: 'Policy updated', policy});

this._policies.delete(policy.metadata!.name!);
}

private onBinding(binding: MonoklePolicyBinding) {
this._logger.debug({msg: 'Binding updated', binding});

this._bindings.set(binding.metadata!.name!, binding);
}

private onBindingRemoval(binding: MonoklePolicyBinding) {
this._logger.debug({msg: 'Binding updated', binding});

this._bindings.delete(binding.metadata!.name!);
}
}
7 changes: 7 additions & 0 deletions examples/policy-binding-sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: monokle.com/v1alpha1
kind: MonoklePolicyBinding
metadata:
name: policy-sample-binding
spec:
policyName: "policy-sample"
validationActions: [Warn]
2 changes: 1 addition & 1 deletion examples/policy-sample.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
apiVersion: monokle.com/v1
apiVersion: monokle.com/v1alpha1
kind: MonoklePolicy
metadata:
name: policy-sample
Expand Down
29 changes: 29 additions & 0 deletions k8s/manifests/monokle-policy-binding-crd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: policybindings.monokle.com
spec:
group: monokle.com
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
policyName:
type: string
validationActions:
type: string
enum: [Warn]
scope: Cluster
names:
plural: policybindings
singular: policybinding
kind: MonoklePolicyBinding
shortNames:
- mpb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ metadata:
spec:
group: monokle.com
versions:
- name: v1
- name: v1alpha1
served: true
storage: true
schema:
Expand Down Expand Up @@ -37,7 +37,7 @@ spec:
type: object
additionalProperties:
type: string
scope: Namespaced
scope: Cluster
names:
plural: policies
singular: policy
Expand Down
Loading

0 comments on commit 6f908b3

Please sign in to comment.