Skip to content

Commit

Permalink
refactor: add support for policy binding [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
f1ames committed Sep 29, 2023
1 parent e65bf62 commit e6ce712
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 66 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ You can also do manual clean-up and re-run `./deploy.sh` script again:
kubectl delete all -n webhook-demo --all && \
kubectl delete validatingwebhookconfiguration.admissionregistration.k8s.io/demo-webhook -n webhook-demo && \
kubectl delete namespace webhook-demo && \
kubectl delete crd policies.monokle.com
kubectl delete crd policies.monokle.com && \
kubectl delete crd policybindings.monokle.com
```

### Refs
Expand Down
19 changes: 11 additions & 8 deletions admission-webhook/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,34 @@ import {MonoklePolicy, MonoklePolicyBinding, PolicyManager} from './utils/policy
import {ValidatorManager} from './utils/validator-manager.js';
import {ValidationServer} from './utils/validation-server.js';

const LOG_LEVEL = (process.env.MONOKLE_LOG_LEVEL || 'warn').toLowerCase();
const IGNORED_NAMESPACES = (process.env.MONOKLE_IGNORE_NAMESPACES || '').split(',');

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

(async() => {
const policyInformer = await getInformer<MonoklePolicy>(
'monokle.io',
'v1',
'monokle.com',
'v1alpha1',
'policies',
(err: any) => {
logger.error({msg: 'Informer: Policies: Error', err});
logger.error({msg: 'Informer: Policies: Error', err: err.message, body: err.body});
}
);

const bindingsInformer = await getInformer<MonoklePolicyBinding>(
'monokle.io',
'v1',
'monokle.com',
'v1alpha1',
'policybindings',
(err: any) => {
logger.error({msg: 'Informer: Bindings: Error', err});
logger.error({msg: 'Informer: Bindings: Error', err: err.message, body: err.body});
}
);

const policyManager = new PolicyManager(policyInformer, bindingsInformer, logger);
const policyManager = new PolicyManager(policyInformer, bindingsInformer, IGNORED_NAMESPACES, logger);
const validatorManager = new ValidatorManager(policyManager);

await policyManager.start();
Expand Down
46 changes: 30 additions & 16 deletions admission-webhook/src/utils/get-informer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,41 @@ import k8s from '@kubernetes/client-node';

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

export type InformerWrapper<TValue extends k8s.KubernetesObject> = {
informer: Informer<TValue>,
start: () => Promise<void>
}

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);
}
export async function getInformer<TValue extends k8s.KubernetesObject>(
group: string, version: string, plural: string, onError?: k8s.ErrorCallback
): Promise<InformerWrapper<TValue>> {
const informer = await createInformer<TValue>(group, version, plural, onError);
const start = createInformerStarter(informer, onError);

return {informer, start}
}

await new Promise((resolve) => setTimeout(resolve, ERROR_RESTART_INTERVAL));
function createInformerStarter(informer: Informer<any>, onError?: k8s.ErrorCallback) { // not sure if can try to start informer multiple times
return async () => {
let tries = 0;
let started = false;

while (!started) {
try {
tries++;
await informer.start();
started = true;
} 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) {
Expand Down
60 changes: 37 additions & 23 deletions admission-webhook/src/utils/policy-manager.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,70 @@
import pino from 'pino';
import {KubernetesObject} from '@kubernetes/client-node';
import {Config} from '@monokle/validation';
import {Informer} from './get-informer';
import {InformerWrapper} from './get-informer';

export type MonoklePolicy = KubernetesObject & {
spec: Config
}

export type MonoklePolicyBindingConfiguration = {
policyName: string
validationActions: ['Warn']
}

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

export type MonokleApplicablePolicy = {
policy: Config,
binding: MonoklePolicyBindingConfiguration
}

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 _policyInformer: InformerWrapper<MonoklePolicy>,
private readonly _bindingInformer: InformerWrapper<MonoklePolicyBinding>,
private readonly _ignoreNamespaces: string[],
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._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));

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

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)
getMatchingPolicies(): MonokleApplicablePolicy[] { // @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);
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 null;
}

return {
policy: policy.spec,
binding: binding.spec
}
})
.filter((policy) => policy !== null) as MonokleApplicablePolicy[];
}

private onPolicy(policy: MonoklePolicy) {
Expand Down
28 changes: 19 additions & 9 deletions admission-webhook/src/utils/validation-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,27 @@ export class ValidationServer {
}

const resourceForValidation = this._createResourceForValidation(body);
// @TODO iterate over validators and run them all
const validationResponse = await validators[0].validate({ resources: [resourceForValidation] });
const validationResponses = await Promise.all(validators.map(async (validator) => {
return {
result: await validator.validator.validate({ resources: [resourceForValidation] }),
policy: validator.policy
};
}
));

// @TODO each result may have different `validationActions` (defined in bindings) so it should be handled
// it can by be grouping results by action and then performing action for each group

const warnings = [];
const errors = [];
for (const result of validationResponse.runs) {
for (const item of result.results) {
if (item.level === "warning") {
warnings.push(item);
} else if (item.level === "error") {
errors.push(item);
for (const validationResponse of validationResponses) {
for (const result of validationResponse.result.runs) {
for (const item of result.results) {
if (item.level === "warning") {
warnings.push(item);
} else if (item.level === "error") {
errors.push(item);
}
}
}
}
Expand All @@ -163,7 +173,7 @@ export class ValidationServer {
}

this._logger.debug({response});
this._logger.trace({resourceForValidation, validationResponse});
this._logger.trace({resourceForValidation, validationResponses});

return response;
});
Expand Down
42 changes: 37 additions & 5 deletions admission-webhook/src/utils/validator-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {MonokleValidator} from "@monokle/validation";
import {PolicyManager} from "./policy-manager";
import {AnnotationSuppressor, Config, DisabledFixer, FingerprintSuppressor, MonokleValidator, RemotePluginLoader, ResourceParser, SchemaLoader} from "@monokle/validation";
import {MonokleApplicablePolicy, PolicyManager} from "./policy-manager";

export type MonokleApplicableValidator = {
validator: MonokleValidator,
policy: MonokleApplicablePolicy
}

export class ValidatorManager {
private _validators = new Map<string, MonokleValidator>(); // Map<policyName, validator>
Expand All @@ -12,14 +17,41 @@ export class ValidatorManager {
// it would affect performance of the admission webhook response time
}

getMatchingValidators(): MonokleValidator[] {
getMatchingValidators(): MonokleApplicableValidator[] {
const matchingPolicies = this._policyManager.getMatchingPolicies();

if (matchingPolicies.length === 0) {
return [];
}

// @TODO
return [];
return matchingPolicies.map((policy) => {
if (!this._validators.has(policy.binding.policyName)) {
this.setupValidator(policy.binding.policyName, policy.policy);
}

return {
validator: this._validators.get(policy.binding.policyName)!,
policy
}
});
}

private async setupValidator(policyName: string, policy: Config) {
if (this._validators.has(policyName)) {
this._validators.get(policyName)!.preload(policy);
} else {
const validator = new MonokleValidator(
{
loader: new RemotePluginLoader(),
parser: new ResourceParser(),
schemaLoader: new SchemaLoader(),
suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()],
fixer: new DisabledFixer(),
},
policy
);

this._validators.set(policyName, validator);
}
}
}
2 changes: 1 addition & 1 deletion k8s/manifests/service-account.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ metadata:
name: monokle-policies
subjects:
- kind: ServiceAccount
name: default
name: monokle-policies
namespace: webhook-demo
roleRef:
kind: ClusterRole
Expand Down
6 changes: 6 additions & 0 deletions k8s/templates/deployment.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ spec:
- name: webhook-tls-certs
mountPath: /run/secrets/tls
readOnly: true
env:
- name: MONOKLE_LOG_LEVEL
value: DEBUG
- name: MONOKLE_IGNORE_NAMESPACES
value: 'kube-node-lease,kube-public,kube-system'
volumes:
- name: webhook-tls-certs
secret:
secretName: webhook-server-tls
serviceAccountName: monokle-policies
---
apiVersion: v1
kind: Service
Expand Down
13 changes: 10 additions & 3 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,19 @@ ca_pem_b64="$(openssl base64 -A <"${keydir}/ca.crt")"
sed -e 's@${CA_PEM_B64}@'"$ca_pem_b64"'@g' <"${templdir}/webhook.yaml.template" > "${resdir}/webhook.yaml"
cp "${templdir}/deployment.yaml.template" "${resdir}/deployment.yaml"

# Cluster-wide
kubectl apply -f "${resdir}/monokle-policy-crd.yaml"
kubectl apply -f "${resdir}/monokle-policy-binding-crd.yaml"

# Namespaced
kubectl apply -f "${resdir}/service-account.yaml" -n webhook-demo

skaffold run -n webhook-demo -f k8s/skaffold.yaml
# kubectl apply -f deployment.yaml
sleep 2
kubectl apply -f "${resdir}/monokle-policy-crd.yaml"
kubectl apply -f "${resdir}/monokle-policy-binding-crd.yaml"
kubectl apply -f "${resdir}/service-account.yaml"
# kubectl apply -f "${resdir}/monokle-policy-crd.yaml"
# kubectl apply -f "${resdir}/monokle-policy-binding-crd.yaml"
# kubectl apply -f "${resdir}/service-account.yaml"
kubectl apply -f "${resdir}/webhook.yaml"

# Delete the key directory to prevent abuse (DO NOT USE THESE KEYS ANYWHERE ELSE).
Expand Down

0 comments on commit e6ce712

Please sign in to comment.