Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#2196 Cluster reporting implementation #28

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
14 changes: 6 additions & 8 deletions admission-controller/init/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import k8s from '@kubernetes/client-node';
import retry from 'async-retry';
import logger, { formatLog } from './utils/logger.js';
import { generateCertificates, isCertExpiring, isCertValid } from './utils/certificates.js';
import { getSecretCertificate, applySecretCertificate, getWebhookConfiguration, patchWebhookCertificate } from './utils/kubernetes.js';
import KubeClient from "./utils/kube-client.js";

const NAMESPACE = (process.env.MONOKLE_NAMESPACE || 'monokle-admission-controller');

Expand Down Expand Up @@ -33,30 +32,29 @@ const WEBHOOK_NAME = 'monokle-admission-controller-webhook';
// Such order of actions prevents from cases where secret (with cert) is updated but webhook is not.
// At the same time, entire process is treated as atomic one, if something goes wrong, retry from the beginning.
async function run(_bail: (e: Error) => void, _attempt: number) {
const kc = new k8s.KubeConfig();
kc.loadFromCluster();
KubeClient.buildKubeConfig();

const webhookConfig = await getWebhookConfiguration(NAMESPACE, WEBHOOK_NAME, kc);
const webhookConfig = await KubeClient.getWebhookConfiguration(NAMESPACE, WEBHOOK_NAME);
if (!webhookConfig) {
throw new Error(`Webhook ${NAMESPACE}/${WEBHOOK_NAME} does not exist.`);
}

const existingCert = await getSecretCertificate(NAMESPACE, SECRET_NAME, kc);
const existingCert = await KubeClient.getSecretCertificate(NAMESPACE, SECRET_NAME);
if (existingCert && isCertValid(existingCert.certificate) && !isCertExpiring(existingCert.certificate, 90)) {
logger.info('Valid cert already exists.');
return;
}

const certs = generateCertificates(NAMESPACE, 6);

const webhookPatched = patchWebhookCertificate(NAMESPACE, WEBHOOK_NAME, webhookConfig, certs.caCert, kc);
const webhookPatched = KubeClient.patchWebhookCertificate(NAMESPACE, WEBHOOK_NAME, webhookConfig, certs.caCert);
if (!webhookPatched) {
throw new Error('Failed to update webhook.');
}

logger.info('Webhook patched successfully.');

const certCreated = await applySecretCertificate(NAMESPACE, SECRET_NAME, certs.serverKey, certs.serverCert, kc);
const certCreated = await KubeClient.applySecretCertificate(NAMESPACE, SECRET_NAME, certs.serverKey, certs.serverCert);
if (!certCreated) {
throw new Error('Failed to create secret.');
}
Expand Down
163 changes: 163 additions & 0 deletions admission-controller/init/src/utils/kube-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import k8s from "@kubernetes/client-node";
import logger, {formatLog} from "./logger";
import forge from 'node-forge';

export type SecretCertificate = {
secret: k8s.V1Secret;
certificate: forge.pki.Certificate;
}

class KubeClient {
private readonly kc: k8s.KubeConfig;

constructor() {
this.kc = new k8s.KubeConfig();
}

buildKubeConfig() {
try {
if (process.env.KUBE_CONTEXT) {
this.kc.loadFromDefault();
this.kc.setCurrentContext(process.env.KUBE_CONTEXT);
}
else {
this.kc.loadFromCluster();
}
} catch (e) {
console.warn("Failed to load kubeconfig from file");
}
}

async getSecretCertificate(namespace: string, name: string): Promise<SecretCertificate | null> {
const k8sApi = this.kc.makeApiClient(k8s.CoreV1Api);

let res;
try {
res = await k8sApi.readNamespacedSecret(name, namespace);

const secret = res.body;

// Should not happen, means secret data is invalid/corrupted.
if (!secret.data?.['tls.crt']) {
throw new Error(`Secret ${namespace}/${name} does not contain a tls.crt.`);
}

const certificate = forge.pki.certificateFromPem(Buffer.from(secret.data['tls.crt'], 'base64').toString('utf8'));

return {
secret,
certificate
}
} catch (err: any) {
if (err.body?.code === 404) {
logger.info(formatLog(`No existing secret ${namespace}/${name}`, err, res));
} else {
logger.error(formatLog(`Failed to read secret ${namespace}/${name}`, err, res));
}

return null;
}
}

async applySecretCertificate(namespace: string, name: string, pk: forge.pki.PrivateKey, certificate: forge.pki.Certificate) {
const k8sApi = this.kc.makeApiClient(k8s.CoreV1Api);

const pemPK = forge.pki.privateKeyToPem(pk);
const pemCert = forge.pki.certificateToPem(certificate);

try {
await k8sApi.deleteNamespacedSecret(name, namespace);
} catch (err: any) {
logger.debug(formatLog(`Failed to delete secret ${namespace}/${name}`), err);
}

let res;
try {
res = await k8sApi.createNamespacedSecret(namespace, {
apiVersion: 'v1',
kind: 'Secret',
type: 'kubernetes.io/tls',
metadata: {
name,
},
data: {
'tls.crt': Buffer.from(pemCert).toString('base64'),
'tls.key': Buffer.from(pemPK).toString('base64'),
},
});

if (res.response.statusCode !== 201) {
throw new Error(`Failed to apply secret ${namespace}/${name} (non 201 status code)`);
}

return true;
} catch (err: any) {
logger.error(formatLog(`Failed to apply secret ${namespace}/${name}`, err, res));
return false;
}
}

async getWebhookConfiguration(namespace: string, name: string): Promise<k8s.V1ValidatingWebhookConfiguration | null> {
const client = this.kc.makeApiClient(k8s.KubernetesObjectApi);

let res;
try {
res = await client.read<k8s.V1ValidatingWebhookConfiguration>({
apiVersion: 'admissionregistration.k8s.io/v1',
kind: 'ValidatingWebhookConfiguration',
metadata: {
name,
namespace,
},
});

if (res.response.statusCode !== 200) {
throw new Error(`Failed to get webhook ${namespace}/${name} (non 200 status code)`);
}

return res.body;
} catch (err: any) {
logger.error(formatLog(`Failed to get webhook ${namespace}/${name}`, err, res));
return null;
}
}

async patchWebhookCertificate(namespace: string, name: string, webhook: k8s.V1ValidatingWebhookConfiguration, certificate: forge.pki.Certificate) {
const k8sApi = this.kc.makeApiClient(k8s.AdmissionregistrationV1Api);

let res;
try {
const webhookConfig = (webhook.webhooks || [])[0];
if (!webhookConfig) {
throw new Error(`Webhook ${namespace}/${name} does not exist`);
}

webhookConfig.clientConfig.caBundle = Buffer.from(forge.pki.certificateToPem(certificate)).toString('base64');

const resPatch = res = await k8sApi.patchValidatingWebhookConfiguration(name, {
webhooks: [webhookConfig],
metadata: {
labels: {
'monokle.io/updated': Date.now().toString(),
},
}
},
undefined, undefined, 'Monokle', undefined, undefined, {
headers: {
'Content-Type': 'application/merge-patch+json',
},
});

if (resPatch.response.statusCode !== 200) {
throw new Error(`Failed to patch webhook ${namespace}/${name} (non 200 status code)`);
}

return true;
} catch (err: any) {
logger.error(formatLog(`Failed to patch webhook ${namespace}/${name}`, err, res));
return false;
}
}
}

export default new KubeClient();
139 changes: 0 additions & 139 deletions admission-controller/init/src/utils/kubernetes.ts

This file was deleted.

28 changes: 28 additions & 0 deletions admission-controller/server/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": ["@typescript-eslint/eslint-plugin"],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"root": true,
"env": {
"node": true,
"jest": true
},
"ignorePatterns": ["dist/", "node_modules/"],
"rules": {
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",

"@typescript-eslint/no-unused-vars": "warn",

"eol-last": 1
}
}
4 changes: 4 additions & 0 deletions admission-controller/server/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
Loading
Loading