From f9ec5ad06e17734848bdb76ad5f756dd0fda2ffb Mon Sep 17 00:00:00 2001 From: Sebastian Woehrl Date: Tue, 31 May 2022 14:09:24 +0200 Subject: [PATCH] Change init behaviour --- README.md | 27 +++- .../constraint.yaml => templates/crds.yaml} | 7 +- charts/bridgekeeper/templates/deployment.yaml | 3 + charts/bridgekeeper/templates/init.yaml | 116 ++++++++++++++- charts/bridgekeeper/values.yaml | 4 +- src/constants.rs | 5 +- src/evaluator.rs | 2 +- src/helper/cleanup.rs | 10 +- src/helper/gencrd.rs | 13 +- src/helper/init.rs | 140 ++++++------------ src/server.rs | 15 ++ src/util/cert.rs | 23 +++ src/util/k8s_client.rs | 23 ++- src/util/mod.rs | 1 + src/util/webhook.rs | 103 +++++++++++++ 15 files changed, 384 insertions(+), 108 deletions(-) rename charts/bridgekeeper/{crds/constraint.yaml => templates/crds.yaml} (95%) create mode 100644 src/util/webhook.rs diff --git a/README.md b/README.md index fe62455..36b1937 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,30 @@ If you want to use the current master version from git: 1. Install the chart: `helm install --namespace bridgekeeper --create-namespace bridgekeeper ./charts/bridgekeeper` +### Configuration + +Bridgekeeper has a number of options to configure behaviour. They can be set via helm values: + +```yaml +replicaCount: 1 # Number of instances of bridgekeeper to run, should be >1 for production setups + +installCRDs: true # By default the helm chart installs the CRD, set to false if you want to do this in a separate workflow + +bridgekeeper: + # namespaces to ignore for validation, you should add the namespace you install bridgekeeper in + ignoreNamespaces: + - kube-system + - kube-public + - kube-node-lease + # If set to true any requests in non-ignored namespaces will fail while bridgekeeper is not available (sets failure policy to "Fail") + strictAdmission: false + audit: + # Set this to true if you want bridgekeeper to run regular audits, if enabled you should have replicaCount: 1 + enabled: false + # Audit interval in seconds + interval: 600 +``` + ### Writing constraints Bridgekeeper uses a custom resource called Constraint to manage policies. A constraint consists of a target that describes what kubernetes resources are to be validated by the constraint and a rule script written in python. @@ -122,7 +146,7 @@ This service is written in Rust and uses [kube-rs](https://github.com/clux/kube- 1. Compile binary: `cargo build` 2. Generate certificates and install webhook: `cargo run -- init --local host.k3d.internal:8081` 3. Install CRD: `kubectl apply -f charts/bridgekeeper/crds/constraint.yaml` -4. Launch bridgekeeper: `cargo run -- server --cert-dir .certs` +4. Launch bridgekeeper: `cargo run -- server --cert-dir .certs --local host.k3d.internal:8081` After you are finished, run `cargo run -- cleanup --local` to delete the webook. @@ -140,4 +164,3 @@ If you change the schema of the CRD (via `src/crd.rs`) you need to regenerate th ## Planned features * Give rules access to existing objects of the same type (to do e.g. uniqueness checks) -* Ability to modify/patch resources diff --git a/charts/bridgekeeper/crds/constraint.yaml b/charts/bridgekeeper/templates/crds.yaml similarity index 95% rename from charts/bridgekeeper/crds/constraint.yaml rename to charts/bridgekeeper/templates/crds.yaml index b6e3c6b..a815df3 100644 --- a/charts/bridgekeeper/crds/constraint.yaml +++ b/charts/bridgekeeper/templates/crds.yaml @@ -1,3 +1,4 @@ +{{- if .Values.installCRDs }} --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition @@ -6,12 +7,15 @@ metadata: spec: group: bridgekeeper.maibornwolff.de names: + categories: [] kind: Constraint plural: constraints + shortNames: [] singular: constraint scope: Cluster versions: - - name: v1alpha1 + - additionalPrinterColumns: [] + name: v1alpha1 schema: openAPIV3Schema: description: "Auto-generated derived type for ConstraintSpec via `CustomResource`" @@ -94,3 +98,4 @@ spec: storage: true subresources: status: {} +{{- end }} diff --git a/charts/bridgekeeper/templates/deployment.yaml b/charts/bridgekeeper/templates/deployment.yaml index 638721d..afb0b4c 100644 --- a/charts/bridgekeeper/templates/deployment.yaml +++ b/charts/bridgekeeper/templates/deployment.yaml @@ -36,6 +36,9 @@ spec: - bridgekeeper args: - server + {{- if .Values.bridgekeeper.strictAdmission }} + - "--strict-admission" + {{- end }} {{- if .Values.bridgekeeper.audit.enabled }} - --audit - "--audit-interval" diff --git a/charts/bridgekeeper/templates/init.yaml b/charts/bridgekeeper/templates/init.yaml index 1c5b4f2..1bcd28d 100644 --- a/charts/bridgekeeper/templates/init.yaml +++ b/charts/bridgekeeper/templates/init.yaml @@ -1,10 +1,124 @@ {{- if .Values.bridgekeeper.runInit }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "bridgekeeper.serviceAccountName" . }}-init + labels: + {{- include "bridgekeeper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "1" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + {{- include "bridgekeeper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "2" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: bridgekeeper-init-role +rules: +# The init job needs to create/update the webhook configurations +- apiGroups: + - admissionregistration.k8s.io + resources: + - mutatingwebhookconfigurations + - validatingwebhookconfigurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +# The init job needs to update namespaces with the ignore label +- apiGroups: + - "" + resources: + - namespaces + verbs: + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + {{- include "bridgekeeper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "3" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: bridgekeeper-init-role + namespace: "{{ .Release.Namespace }}" +rules: +# The init job creates a secret with the TLS certificate, the main program needs to read it +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + {{- include "bridgekeeper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "4" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: bridgekeeper-init-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: bridgekeeper-init-role +subjects: +- kind: ServiceAccount + name: {{ include "bridgekeeper.serviceAccountName" . }}-init + namespace: "{{ .Release.Namespace }}" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + {{- include "bridgekeeper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + name: bridgekeeper-init-rolebinding + namespace: "{{ .Release.Namespace }}" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: bridgekeeper-init-role +subjects: +- kind: ServiceAccount + name: {{ include "bridgekeeper.serviceAccountName" . }}-init + namespace: "{{ .Release.Namespace }}" +--- apiVersion: batch/v1 kind: Job metadata: name: "bridgekeeper-init" labels: {{- include "bridgekeeper.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "10" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: template: metadata: @@ -17,7 +131,7 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} - serviceAccountName: {{ include "bridgekeeper.serviceAccountName" . }} + serviceAccountName: {{ include "bridgekeeper.serviceAccountName" . }}-init automountServiceAccountToken: true containers: - name: bridgekeeper-init diff --git a/charts/bridgekeeper/values.yaml b/charts/bridgekeeper/values.yaml index eb2f242..cadcc06 100644 --- a/charts/bridgekeeper/values.yaml +++ b/charts/bridgekeeper/values.yaml @@ -1,5 +1,7 @@ replicaCount: 1 +installCRDs: true + image: repository: ghcr.io/maibornwolff/bridgekeeper pullPolicy: IfNotPresent @@ -52,7 +54,6 @@ affinity: {} bridgekeeper: # namespaces to ignore for validation - # Note: Changing this value after initial installation has no effect ignoreNamespaces: - kube-system - kube-public @@ -60,7 +61,6 @@ bridgekeeper: # Set this to false if you want to take care of creating and installing the server cert and the webhook configuration yourself runInit: true # If set to true any requests in non-ignored namespaces will fail while bridgekeeper is not available (sets failure policy to "Fail") - # Note: Changing this value after initial installation has no effect strictAdmission: false audit: # Set this to true if you want bridgekeeper to run regular audits diff --git a/src/constants.rs b/src/constants.rs index 429e6e7..aa633af 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -4,7 +4,8 @@ pub const CERT_FILENAME: &str = "tls.crt"; pub const KEY_FILENAME: &str = "tls.key"; pub const CACERT_FILENAME: &str = "tls.key"; pub const SECRET_NAME: &str = "bridgekeeper-webhook-server-cert"; -pub const WEBHOOK_NAME: &str = "bridgekeeper-webhook"; +pub const CONSTRAINT_VALIDATION_WEBHOOK_NAME: &str = "bridgekeeper-constraint-validation"; +pub const ADMISSION_WEBHOOK_NAME: &str = "bridgekeeper-webhook"; pub const SERVICE_NAME: &str = "bridgekeeper-webhook"; pub const MANAGER_NAME: &str = "bridgekeeper"; -pub const CRD_FILEPATH: &str = "charts/bridgekeeper/crds/constraint.yaml"; +pub const CRD_FILEPATH: &str = "charts/bridgekeeper/templates/crds.yaml"; diff --git a/src/evaluator.rs b/src/evaluator.rs index 114326e..a3a15a8 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -263,7 +263,7 @@ pub fn evaluate_constraint_audit( } fn extract_result( - name: &String, + name: &str, request: &ValidationRequest, result: &PyAny, ) -> (bool, Option, Option) { diff --git a/src/helper/cleanup.rs b/src/helper/cleanup.rs index fa823b5..425b52b 100644 --- a/src/helper/cleanup.rs +++ b/src/helper/cleanup.rs @@ -23,11 +23,17 @@ pub async fn run(args: Args) { // Delete webhook let webhook_api: kube::Api = kube::Api::all(client.clone()); - if let Err(err) = webhook_api.delete(WEBHOOK_NAME, &Default::default()).await { + if let Err(err) = webhook_api + .delete(ADMISSION_WEBHOOK_NAME, &Default::default()) + .await + { println!("Encountered error when deleting admission webhook: {}", err); } let webhook_api: kube::Api = kube::Api::all(client.clone()); - if let Err(err) = webhook_api.delete(WEBHOOK_NAME, &Default::default()).await { + if let Err(err) = webhook_api + .delete(CONSTRAINT_VALIDATION_WEBHOOK_NAME, &Default::default()) + .await + { println!( "Encountered error when deleting constraint validation webhook: {}", err diff --git a/src/helper/gencrd.rs b/src/helper/gencrd.rs index f417390..2da4451 100644 --- a/src/helper/gencrd.rs +++ b/src/helper/gencrd.rs @@ -10,11 +10,22 @@ pub struct Args { /// file to write yaml to #[argh(option, short = 'f')] pub file: Option, + /// do not wrap CRD yaml in helm template if condition + #[argh(switch)] + pub no_wrapping: bool, } pub fn run(args: Args) { let data = serde_yaml::to_string(&Constraint::crd()) .expect("Could not generate yaml from CRD definition"); let filepath = args.file.unwrap_or_else(|| CRD_FILEPATH.to_string()); - fs::write(filepath, data).expect("Unable to write crd yaml"); + let wrapped_data = "{{- if .Values.installCRDs }}\n".to_string() + &data + "{{- end }}\n"; + fs::write( + filepath, + match args.no_wrapping { + true => data, + false => wrapped_data, + }, + ) + .expect("Unable to write crd yaml"); } diff --git a/src/helper/init.rs b/src/helper/init.rs index 7d0ebba..929270e 100644 --- a/src/helper/init.rs +++ b/src/helper/init.rs @@ -1,15 +1,12 @@ +use crate::util::webhook::*; use crate::{constants::*, util::cert::CertKeyPair}; use argh::FromArgs; -use k8s_openapi::api::{ - admissionregistration::v1::{MutatingWebhookConfiguration, ValidatingWebhookConfiguration}, - core::v1::{Namespace, Secret}, -}; +use k8s_openapi::api::core::v1::{Namespace, Secret}; use k8s_openapi::ByteString; use kube::{ api::{Api, ObjectMeta, Patch, PatchParams}, - Client, Resource, + Client, }; -use serde::{de::DeserializeOwned, Serialize}; use serde_json::json; use std::io::prelude::*; use std::{ @@ -31,12 +28,11 @@ pub struct Args { /// whether or not to fail admission requests when bridgekeeper fails or is not reachable #[argh(switch)] strict_admission: bool, + /// overwrite existing objects and regenerate certificates + #[argh(switch)] + overwrite: bool, } -#[derive(rust_embed::RustEmbed)] -#[folder = "manifests/"] -struct Assets; - pub async fn run(args: Args) { let client = Client::try_default() .await @@ -44,22 +40,55 @@ pub async fn run(args: Args) { let namespace = std::env::var("NAMESPACE").unwrap_or_else(|_| "default".into()); // Create and store certificate - let cert = generate_and_store_certificates(&namespace, &args, &client).await; + let cert = if args.overwrite { + generate_and_store_certificates(&namespace, &args, &client).await + } else if let Some(cert) = retrieve_certificates(&namespace, &args, &client).await { + cert + } else { + generate_and_store_certificates(&namespace, &args, &client).await + }; // Create webhook - create_webhooks(&namespace, &cert, &args, &client).await; + create_webhooks(&cert, &args, &client).await; // Patch namespaces patch_namespaces(args, &client).await; } +async fn retrieve_certificates( + namespace: &str, + args: &Args, + client: &Client, +) -> Option { + if args.local.is_some() { + let cert = match std::fs::read_to_string(Path::new(LOCAL_CERTS_DIR).join(CERT_FILENAME)) { + Ok(data) => data, + Err(_) => return None, + }; + let key = match std::fs::read_to_string(Path::new(LOCAL_CERTS_DIR).join(KEY_FILENAME)) { + Ok(data) => data, + Err(_) => return None, + }; + Some(CertKeyPair { cert, key }) + } else { + let secret_api: Api = Api::namespaced(client.clone(), namespace); + match secret_api.get(SECRET_NAME).await { + Ok(secret) => match secret.data { + Some(data) => CertKeyPair::from_secret(&data), + None => None, + }, + Err(_) => None, + } + } +} + async fn generate_and_store_certificates( namespace: &String, args: &Args, client: &Client, ) -> CertKeyPair { let cert = - crate::util::cert::gen_cert(SERVICE_NAME.to_string(), &namespace, args.local.clone()); + crate::util::cert::gen_cert(SERVICE_NAME.to_string(), namespace, args.local.clone()); if args.local.is_some() { let _ = create_dir(LOCAL_CERTS_DIR); let mut cert_file = File::create(Path::new(LOCAL_CERTS_DIR).join(CERT_FILENAME)) @@ -73,7 +102,7 @@ async fn generate_and_store_certificates( .write_all(cert.key.as_bytes()) .expect("failed to write key"); } else { - let secret_api: Api = Api::namespaced(client.clone(), &namespace); + let secret_api: Api = Api::namespaced(client.clone(), namespace); let metadata = ObjectMeta { name: Some(SECRET_NAME.to_string()), namespace: Some(namespace.clone()), @@ -113,47 +142,10 @@ async fn generate_and_store_certificates( cert } -async fn create_webhooks(namespace: &str, cert: &CertKeyPair, args: &Args, client: &Client) { - let failure_policy = if args.strict_admission { - "Fail" - } else { - "Ignore" - }; - let webhook_data = if args.local.is_some() { - Assets::get("admission-controller-local.yaml") - } else { - Assets::get("admission-controller.yaml") - } - .expect("failed to read admission controller template"); - let webhook_data = String::from_utf8(webhook_data.data.to_vec()) - .expect("failed to parse admission controller template"); - apply_webhook::( - &client, - webhook_data, - &cert, - namespace, - &args.local, - failure_policy, - ) - .await; - - let webhook_data = if args.local.is_some() { - Assets::get("constraint-validation-controller-local.yaml") - } else { - Assets::get("constraint-validation-controller.yaml") - } - .expect("failed to read contraint admission controller template"); - let webhook_data = String::from_utf8(webhook_data.data.to_vec()) - .expect("failed to parse constraint admission controller template"); - apply_webhook::( - &client, - webhook_data, - &cert, - namespace, - &args.local, - failure_policy, - ) - .await; +async fn create_webhooks(cert: &CertKeyPair, args: &Args, client: &Client) { + create_constraint_validation_webhook(client, cert, &args.local, args.strict_admission) + .await + .expect("Failed to create constraint validation webhook"); } async fn patch_namespaces(args: Args, client: &Client) { @@ -175,41 +167,3 @@ async fn patch_namespaces(args: Args, client: &Client) { } } } - -async fn apply_webhook( - client: &kube::Client, - webhook_data: String, - cert: &CertKeyPair, - namespace: &str, - local: &Option, - failure_policy: &str, -) where - ::DynamicType: Default, - T: Clone, - T: Serialize, - T: DeserializeOwned, - T: std::fmt::Debug, -{ - let mut webhook_data = webhook_data - .replace("", &base64::encode(cert.cert.clone())) - .replace("", namespace) - .replace("", failure_policy); - if let Some(local_name) = local { - webhook_data = - webhook_data.replace("", &local_name.to_lowercase().replace("ip:", "")); - } - let webhook_data = serde_yaml::from_str(&webhook_data).expect("failed to read webhook data"); - - let webhook_api: kube::Api = kube::Api::all(client.clone()); - if let Ok(_res) = webhook_api.get(WEBHOOK_NAME).await { - println!("Webhook already exists. Deleting old resource"); - match webhook_api.delete(WEBHOOK_NAME, &Default::default()).await { - Ok(_res) => (), - Err(err) => println!("{:?}", err), - }; - } - match webhook_api.create(&Default::default(), &webhook_data).await { - Ok(_res) => (), - Err(err) => println!("{:?}", err), - }; -} diff --git a/src/server.rs b/src/server.rs index f1bf0d2..0ba142c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -19,6 +19,12 @@ pub struct Args { /// audit interval in seconds, by default 600s ( = 10 minutes) #[argh(option)] audit_interval: Option, + /// whether or not to fail admission requests when bridgekeeper fails or is not reachable + #[argh(switch)] + strict_admission: bool, + /// run in local mode, value is target for the webhook, e.g. host.k3d.internal:8081, if you use an ip specify as IP:192.168.1.1:8081 + #[argh(option)] + local: Option, } pub async fn run(args: Args) { @@ -29,6 +35,15 @@ pub async fn run(args: Args) { let cert_dir = args.cert_dir.unwrap_or_else(|| POD_CERTS_DIR.to_string()); let cert = crate::util::cert::wait_for_certs(cert_dir); + // Create admission webhook, ignore error as that likely means another intstance already updated the hook + let _ = crate::util::webhook::create_admission_webhook( + &client, + &cert, + &args.local, + args.strict_admission, + ) + .await; + // Initiate services let constraints = ConstraintStore::new(); let event_sender = init_event_watcher(&client); diff --git a/src/util/cert.rs b/src/util/cert.rs index 4438646..6d35a8f 100644 --- a/src/util/cert.rs +++ b/src/util/cert.rs @@ -1,4 +1,7 @@ +use k8s_openapi::ByteString; + use crate::constants::{CERT_FILENAME, KEY_FILENAME}; +use std::collections::BTreeMap; use std::fs::File; use std::io::prelude::*; use std::path::Path; @@ -9,6 +12,26 @@ pub struct CertKeyPair { pub key: String, } +impl CertKeyPair { + pub fn from_secret(map: &BTreeMap) -> Option { + let cert = match map.get(CERT_FILENAME) { + Some(data) => match String::from_utf8(data.0.clone()) { + Ok(data) => data, + Err(_) => return None, + }, + None => return None, + }; + let key = match map.get(KEY_FILENAME) { + Some(data) => match String::from_utf8(data.0.clone()) { + Ok(data) => data, + Err(_) => return None, + }, + None => return None, + }; + Some(CertKeyPair { cert, key }) + } +} + pub fn gen_cert(service_name: String, namespace: &str, local_name: Option) -> CertKeyPair { let mut params = rcgen::CertificateParams::default(); params diff --git a/src/util/k8s_client.rs b/src/util/k8s_client.rs index e02cad4..34c8720 100644 --- a/src/util/k8s_client.rs +++ b/src/util/k8s_client.rs @@ -2,9 +2,10 @@ use exponential_backoff::Backoff; use kube::{ api::{Api, ListParams, Patch, PatchParams}, core::ObjectList, + Resource, }; use lazy_static::lazy_static; -use serde::de::DeserializeOwned; +use serde::{de::DeserializeOwned, Serialize}; use std::time::Duration; lazy_static! { @@ -35,10 +36,26 @@ pub async fn patch_status_with_retry< patch: &Patch

, ) -> kube::Result { for duration in BACKOFF.iter() { - match api.patch_status(&name, &pp, &patch).await { + match api.patch_status(name, pp, patch).await { Ok(result) => return Ok(result), Err(_err) => tokio::time::sleep(duration).await, } } - api.patch_status(&name, &pp, &patch).await + api.patch_status(name, pp, patch).await +} + +pub async fn apply(api: &Api, name: &str, mut object: T) -> kube::Result +where + ::DynamicType: Default, + T: Clone, + T: Serialize, + T: DeserializeOwned, + T: std::fmt::Debug, +{ + if let Ok(res) = api.get(name).await { + object.meta_mut().resource_version = res.meta().resource_version.clone(); + api.replace(name, &Default::default(), &object).await + } else { + api.create(&Default::default(), &object).await + } } diff --git a/src/util/mod.rs b/src/util/mod.rs index 5413d72..b8e412d 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ pub mod cert; pub mod error; pub mod k8s_client; +pub mod webhook; diff --git a/src/util/webhook.rs b/src/util/webhook.rs new file mode 100644 index 0000000..8a1b851 --- /dev/null +++ b/src/util/webhook.rs @@ -0,0 +1,103 @@ +use crate::util::error::{kube_err, Result}; +use crate::{constants::*, util::cert::CertKeyPair, util::k8s_client::apply}; +use k8s_openapi::api::admissionregistration::v1::{ + MutatingWebhookConfiguration, ValidatingWebhookConfiguration, +}; +use kube::{Client, Resource}; +use serde::{de::DeserializeOwned, Serialize}; + +#[derive(rust_embed::RustEmbed)] +#[folder = "manifests/"] +struct Assets; + +pub async fn create_admission_webhook( + client: &Client, + cert: &CertKeyPair, + local: &Option, + strict_admission: bool, +) -> Result<()> { + let webhook_data = if local.is_some() { + Assets::get("admission-controller-local.yaml") + } else { + Assets::get("admission-controller.yaml") + } + .expect("failed to read admission controller template"); + let webhook_data = String::from_utf8(webhook_data.data.to_vec()) + .expect("failed to parse admission controller template"); + + match apply_webhook::( + client, + ADMISSION_WEBHOOK_NAME, + webhook_data, + cert, + local, + strict_admission, + ) + .await + { + Ok(_) => Ok(()), + Err(err) => Err(kube_err(err)), + } +} + +pub async fn create_constraint_validation_webhook( + client: &Client, + cert: &CertKeyPair, + local: &Option, + strict_admission: bool, +) -> Result<()> { + let webhook_data = if local.is_some() { + Assets::get("constraint-validation-controller-local.yaml") + } else { + Assets::get("constraint-validation-controller.yaml") + } + .expect("failed to read admission controller template"); + let webhook_data = String::from_utf8(webhook_data.data.to_vec()) + .expect("failed to parse admission controller template"); + + match apply_webhook::( + client, + CONSTRAINT_VALIDATION_WEBHOOK_NAME, + webhook_data, + cert, + local, + strict_admission, + ) + .await + { + Ok(_) => Ok(()), + Err(err) => Err(kube_err(err)), + } +} + +async fn apply_webhook( + client: &kube::Client, + name: &str, + webhook_data: String, + cert: &CertKeyPair, + local: &Option, + strict_admission: bool, +) -> kube::Result +where + ::DynamicType: Default, + T: Clone, + T: Serialize, + T: DeserializeOwned, + T: std::fmt::Debug, +{ + let failure_policy = if strict_admission { "Fail" } else { "Ignore" }; + let namespace = std::env::var("NAMESPACE").unwrap_or_else(|_| "default".into()); + let mut webhook_data = webhook_data + .replace("", &base64::encode(cert.cert.clone())) + .replace("", &namespace) + .replace("", failure_policy); + if let Some(local_name) = local { + webhook_data = + webhook_data.replace("", &local_name.to_lowercase().replace("ip:", "")); + } + let webhook_data: T = serde_yaml::from_str(&webhook_data).expect("failed to read webhook data"); + + let webhook_api: kube::Api = kube::Api::all(client.clone()); + + apply(&webhook_api, name, webhook_data).await +}