From 454f32e4c67aa8f6cd931078df8b73d55d544419 Mon Sep 17 00:00:00 2001 From: Alessandro Marrella Date: Fri, 7 Feb 2020 14:35:47 +0000 Subject: [PATCH] Add k8s webhook (#52) * Add kubernetes webhook template * Add note about Haskell * Freeze --- kubernetes/package.dhall | 3 + kubernetes/webhook/README.md | 33 ++++ kubernetes/webhook/Type.dhall | 13 ++ kubernetes/webhook/default.dhall | 7 + kubernetes/webhook/example.dhall | 26 +++ kubernetes/webhook/labels.dhall | 7 + kubernetes/webhook/package.dhall | 10 ++ .../webhook/renderMutatingWebhook.dhall | 169 ++++++++++++++++++ .../webhook/renderValidatingWebhook.dhall | 169 ++++++++++++++++++ kubernetes/webhook/schema.dhall | 20 +++ package.dhall | 2 +- 11 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 kubernetes/webhook/README.md create mode 100644 kubernetes/webhook/Type.dhall create mode 100644 kubernetes/webhook/default.dhall create mode 100644 kubernetes/webhook/example.dhall create mode 100644 kubernetes/webhook/labels.dhall create mode 100644 kubernetes/webhook/package.dhall create mode 100644 kubernetes/webhook/renderMutatingWebhook.dhall create mode 100644 kubernetes/webhook/renderValidatingWebhook.dhall create mode 100644 kubernetes/webhook/schema.dhall diff --git a/kubernetes/package.dhall b/kubernetes/package.dhall index 91fd981..ec4fdbf 100644 --- a/kubernetes/package.dhall +++ b/kubernetes/package.dhall @@ -19,4 +19,7 @@ , ambassador = ./ambassador/package.dhall sha256:5aaed07cae5f55f7a989ec39240716e6f9656c47681c552aca8fd138aaf14bcf ? ./ambassador/package.dhall +, webhook = + ./webhook/package.dhall sha256:a64f1ecb837e63b975ea0f0cb0304c926e3d4d3efab92bc6af163cdea6a24561 + ? ./webhook/package.dhall } diff --git a/kubernetes/webhook/README.md b/kubernetes/webhook/README.md new file mode 100644 index 0000000..59783ce --- /dev/null +++ b/kubernetes/webhook/README.md @@ -0,0 +1,33 @@ +# Kubernetes Webhook Template + +Opinionated template on how to set up a Kubernetes Webhook. +It requires [cert-manager](https://cert-manager.io) version 0.13.0 installed on the cluster. + +Example: +```dhall +-- change the imports to be http imports from this repository +let Webhook = ./package.dhall + +let k8s = ../k8s/1.14.dhall + +let exampleWebhook = + Webhook::{ + , imageName = "docker/whalesay:latest" -- replace with webhook image + , name = "whalesay" + , namespace = "default" + , path = "/mutate" + , port = 8080 + , rules = + [ k8s.RuleWithOperations::{ + , operations = [ "CREATE", "UPDATE" ] + , apiGroups = [ "" ] + , apiVersions = [ "v1" ] + , resources = [ "pods" ] + } + ] + } + +in Webhook.renderMutatingWebhook exampleWebhook +``` + +To implement a webhook in Haskell, you can use https://github.com/EarnestResearch/kubernetes-webhook-haskell/ diff --git a/kubernetes/webhook/Type.dhall b/kubernetes/webhook/Type.dhall new file mode 100644 index 0000000..5618466 --- /dev/null +++ b/kubernetes/webhook/Type.dhall @@ -0,0 +1,13 @@ +let k8s = + ../k8s/1.14.dhall sha256:7839bf40f940757e4d71d3c1b84d878f6a4873c3b2706ae4be307b5991acdcac + ? ../k8s/1.14.dhall + +in { name : Text + , namespace : Text + , imageName : Text + , namespaceSelector : Optional k8s.LabelSelector.Type + , port : Natural + , path : Text + , failurePolicy : Optional Text + , rules : List k8s.RuleWithOperations.Type + } diff --git a/kubernetes/webhook/default.dhall b/kubernetes/webhook/default.dhall new file mode 100644 index 0000000..58a3cff --- /dev/null +++ b/kubernetes/webhook/default.dhall @@ -0,0 +1,7 @@ +let k8s = + ../k8s/1.14.dhall sha256:7839bf40f940757e4d71d3c1b84d878f6a4873c3b2706ae4be307b5991acdcac + ? ../k8s/1.14.dhall + +in { namespaceSelector = None k8s.LabelSelector.Type + , failurePolicy = None Text + } diff --git a/kubernetes/webhook/example.dhall b/kubernetes/webhook/example.dhall new file mode 100644 index 0000000..ba577f6 --- /dev/null +++ b/kubernetes/webhook/example.dhall @@ -0,0 +1,26 @@ +let Webhook = + ./package.dhall sha256:a64f1ecb837e63b975ea0f0cb0304c926e3d4d3efab92bc6af163cdea6a24561 + ? ./package.dhall + +let k8s = + ../k8s/1.14.dhall sha256:7839bf40f940757e4d71d3c1b84d878f6a4873c3b2706ae4be307b5991acdcac + ? ../k8s/1.14.dhall + +let exampleWebhook = + Webhook::{ + , imageName = "docker/whalesay:latest" + , name = "whalesay" + , namespace = "default" + , path = "/mutate" + , port = 8080 + , rules = + [ k8s.RuleWithOperations::{ + , operations = [ "CREATE", "UPDATE" ] + , apiGroups = [ "" ] + , apiVersions = [ "v1" ] + , resources = [ "pods" ] + } + ] + } + +in Webhook.renderMutatingWebhook exampleWebhook diff --git a/kubernetes/webhook/labels.dhall b/kubernetes/webhook/labels.dhall new file mode 100644 index 0000000..3d6db2e --- /dev/null +++ b/kubernetes/webhook/labels.dhall @@ -0,0 +1,7 @@ +let Webhook = + ./Type.dhall sha256:c833a7c500b51ebe0c1879967d11b476428a885b276d70e5f8cc3b4da09890ad + ? ./Type.dhall + +let labels = \(config : Webhook) -> toMap { app = config.name } + +in labels diff --git a/kubernetes/webhook/package.dhall b/kubernetes/webhook/package.dhall new file mode 100644 index 0000000..8f5b68a --- /dev/null +++ b/kubernetes/webhook/package.dhall @@ -0,0 +1,10 @@ + ( ./schema.dhall sha256:668bf175901f6307293a86d6d43dbccb10ab74a7290547feea61be9417e6263f + ? ./schema.dhall + ) +/\ { renderMutatingWebhook = + ./renderMutatingWebhook.dhall sha256:13d289ee38ea74dca59563294afb662c10edf279f15561f528153f922c2971f2 + ? ./renderMutatingWebhook.dhall + , renderValidatingWebhook = + ./renderValidatingWebhook.dhall sha256:4f3fa3e0b227a632f90c66019dcd3e5d0bf4cb2eb1764b6a066b8e6878a7bc9d + ? ./renderValidatingWebhook.dhall + } diff --git a/kubernetes/webhook/renderMutatingWebhook.dhall b/kubernetes/webhook/renderMutatingWebhook.dhall new file mode 100644 index 0000000..26efc4b --- /dev/null +++ b/kubernetes/webhook/renderMutatingWebhook.dhall @@ -0,0 +1,169 @@ +let k8s = + ../k8s/1.14.dhall sha256:7839bf40f940757e4d71d3c1b84d878f6a4873c3b2706ae4be307b5991acdcac + ? ../k8s/1.14.dhall + +let cert-manager = + ../cert-manager/package.dhall sha256:84d8acf2650094d5129e8c75b8f6d6c94e5878767949354e4ff6d7eeae5cfe39 + ? ../cert-manager/package.dhall + +let certsPath = "/certs" + +let Webhook = + ./Type.dhall sha256:c833a7c500b51ebe0c1879967d11b476428a885b276d70e5f8cc3b4da09890ad + ? ./Type.dhall + +let labels = + ./labels.dhall sha256:05c731fe37f21baf0f03bdbe393472a33ba7dd791b924c5ecc2696042f27d6fc + ? ./labels.dhall + +let deployment = + \(webhook : Webhook) + -> k8s.Deployment::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = Some k8s.DeploymentSpec::{ + , selector = k8s.LabelSelector::{ matchLabels = labels webhook } + , template = k8s.PodTemplateSpec::{ + , metadata = k8s.ObjectMeta::{ + , name = webhook.name + , labels = labels webhook + } + , spec = Some k8s.PodSpec::{ + , containers = + [ k8s.Container::{ + , name = webhook.name + , image = Some webhook.imageName + , ports = + [ k8s.ContainerPort::{ containerPort = webhook.port } ] + , env = + [ k8s.EnvVar::{ + , name = "CERTIFICATE_FILE" + , value = Some "${certsPath}/tls.crt" + } + , k8s.EnvVar::{ + , name = "KEY_FILE" + , value = Some "${certsPath}/tls.key" + } + , k8s.EnvVar::{ + , name = "PORT" + , value = Some (Natural/show webhook.port) + } + ] + , volumeMounts = + [ k8s.VolumeMount::{ + , name = "certs" + , mountPath = certsPath + , readOnly = Some True + } + ] + } + ] + , volumes = + [ k8s.Volume::{ + , name = "certs" + , secret = Some k8s.SecretVolumeSource::{ + , secretName = Some webhook.name + } + } + ] + } + } + } + } + +let service = + \(webhook : Webhook) + -> k8s.Service::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = Some k8s.ServiceSpec::{ + , selector = labels webhook + , ports = + [ k8s.ServicePort::{ + , targetPort = Some (k8s.IntOrString.Int webhook.port) + , port = 443 + } + ] + } + } + +let issuer = + \(webhook : Webhook) + -> cert-manager.Issuer::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = + cert-manager.IssuerSpec.SelfSigned + cert-manager.SelfSignedIssuerSpec::{=} + } + +let certificate = + \(webhook : Webhook) + -> cert-manager.Certificate::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = cert-manager.CertificateSpec::{ + , secretName = webhook.name + , issuerRef = { name = webhook.name, kind = (issuer webhook).kind } + , commonName = Some "${webhook.name}.${webhook.namespace}.svc" + , dnsNames = Some + [ webhook.name + , "${webhook.name}.${webhook.namespace}" + , "${webhook.name}.${webhook.namespace}.svc" + , "${webhook.name}.${webhook.namespace}.svc.cluster.local" + , "${webhook.name}:443" + , "${webhook.name}.${webhook.namespace}:443" + , "${webhook.name}.${webhook.namespace}.svc:443" + , "${webhook.name}.${webhook.namespace}.svc.cluster.local:443" + , "localhost:8080" + ] + , usages = Some [ "any" ] + , isCA = Some False + } + } + +let mutatingWebhookConfiguration = + \(webhook : Webhook) + -> k8s.MutatingWebhookConfiguration::{ + , metadata = k8s.ObjectMeta::{ + , name = webhook.name + , labels = labels webhook + , annotations = + toMap + { `cert-manager.io/inject-ca-from` = + "${webhook.namespace}/${webhook.name}" + } + } + , webhooks = + [ k8s.Webhook::{ + , name = "${webhook.name}.${webhook.namespace}.svc" + , clientConfig = k8s.WebhookClientConfig::{ + , service = Some + { name = webhook.name + , namespace = webhook.namespace + , path = Some webhook.path + } + } + , failurePolicy = webhook.failurePolicy + , admissionReviewVersions = [ "v1beta1" ] + , rules = webhook.rules + , namespaceSelector = webhook.namespaceSelector + } + ] + } + +let union = + < Deployment : k8s.Deployment.Type + | Service : k8s.Service.Type + | MWH : k8s.MutatingWebhookConfiguration.Type + | Certificate : cert-manager.Certificate.Type + | ClusterIssuer : cert-manager.ClusterIssuer.Type + > + +in \(webhook : Webhook) + -> { apiVersion = "v1" + , kind = "List" + , items = + [ union.Deployment (deployment webhook) + , union.Service (service webhook) + , union.MWH (mutatingWebhookConfiguration webhook) + , union.Certificate (certificate webhook) + , union.ClusterIssuer (issuer webhook) + ] + } diff --git a/kubernetes/webhook/renderValidatingWebhook.dhall b/kubernetes/webhook/renderValidatingWebhook.dhall new file mode 100644 index 0000000..7c88af3 --- /dev/null +++ b/kubernetes/webhook/renderValidatingWebhook.dhall @@ -0,0 +1,169 @@ +let k8s = + ../k8s/1.14.dhall sha256:7839bf40f940757e4d71d3c1b84d878f6a4873c3b2706ae4be307b5991acdcac + ? ../k8s/1.14.dhall + +let cert-manager = + ../cert-manager/package.dhall sha256:84d8acf2650094d5129e8c75b8f6d6c94e5878767949354e4ff6d7eeae5cfe39 + ? ../cert-manager/package.dhall + +let certsPath = "/certs" + +let Webhook = + ./Type.dhall sha256:c833a7c500b51ebe0c1879967d11b476428a885b276d70e5f8cc3b4da09890ad + ? ./Type.dhall + +let labels = + ./labels.dhall sha256:05c731fe37f21baf0f03bdbe393472a33ba7dd791b924c5ecc2696042f27d6fc + ? ./labels.dhall + +let deployment = + \(webhook : Webhook) + -> k8s.Deployment::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = Some k8s.DeploymentSpec::{ + , selector = k8s.LabelSelector::{ matchLabels = labels webhook } + , template = k8s.PodTemplateSpec::{ + , metadata = k8s.ObjectMeta::{ + , name = webhook.name + , labels = labels webhook + } + , spec = Some k8s.PodSpec::{ + , containers = + [ k8s.Container::{ + , name = webhook.name + , image = Some webhook.imageName + , ports = + [ k8s.ContainerPort::{ containerPort = webhook.port } ] + , env = + [ k8s.EnvVar::{ + , name = "CERTIFICATE_FILE" + , value = Some "${certsPath}/tls.crt" + } + , k8s.EnvVar::{ + , name = "KEY_FILE" + , value = Some "${certsPath}/tls.key" + } + , k8s.EnvVar::{ + , name = "PORT" + , value = Some (Natural/show webhook.port) + } + ] + , volumeMounts = + [ k8s.VolumeMount::{ + , name = "certs" + , mountPath = certsPath + , readOnly = Some True + } + ] + } + ] + , volumes = + [ k8s.Volume::{ + , name = "certs" + , secret = Some k8s.SecretVolumeSource::{ + , secretName = Some webhook.name + } + } + ] + } + } + } + } + +let service = + \(webhook : Webhook) + -> k8s.Service::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = Some k8s.ServiceSpec::{ + , selector = labels webhook + , ports = + [ k8s.ServicePort::{ + , targetPort = Some (k8s.IntOrString.Int webhook.port) + , port = 443 + } + ] + } + } + +let issuer = + \(webhook : Webhook) + -> cert-manager.Issuer::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = + cert-manager.IssuerSpec.SelfSigned + cert-manager.SelfSignedIssuerSpec::{=} + } + +let certificate = + \(webhook : Webhook) + -> cert-manager.Certificate::{ + , metadata = k8s.ObjectMeta::{ name = webhook.name } + , spec = cert-manager.CertificateSpec::{ + , secretName = webhook.name + , issuerRef = { name = webhook.name, kind = (issuer webhook).kind } + , commonName = Some "${webhook.name}.${webhook.namespace}.svc" + , dnsNames = Some + [ webhook.name + , "${webhook.name}.${webhook.namespace}" + , "${webhook.name}.${webhook.namespace}.svc" + , "${webhook.name}.${webhook.namespace}.svc.cluster.local" + , "${webhook.name}:443" + , "${webhook.name}.${webhook.namespace}:443" + , "${webhook.name}.${webhook.namespace}.svc:443" + , "${webhook.name}.${webhook.namespace}.svc.cluster.local:443" + , "localhost:8080" + ] + , usages = Some [ "any" ] + , isCA = Some False + } + } + +let mutatingWebhookConfiguration = + \(webhook : Webhook) + -> k8s.ValidatingWebhookConfiguration::{ + , metadata = k8s.ObjectMeta::{ + , name = webhook.name + , labels = labels webhook + , annotations = + toMap + { `cert-manager.io/inject-ca-from` = + "${webhook.namespace}/${webhook.name}" + } + } + , webhooks = + [ k8s.Webhook::{ + , name = "${webhook.name}.${webhook.namespace}.svc" + , clientConfig = k8s.WebhookClientConfig::{ + , service = Some + { name = webhook.name + , namespace = webhook.namespace + , path = Some webhook.path + } + } + , failurePolicy = webhook.failurePolicy + , admissionReviewVersions = [ "v1beta1" ] + , rules = webhook.rules + , namespaceSelector = webhook.namespaceSelector + } + ] + } + +let union = + < Deployment : k8s.Deployment.Type + | Service : k8s.Service.Type + | MWH : k8s.ValidatingWebhookConfiguration.Type + | Certificate : cert-manager.Certificate.Type + | ClusterIssuer : cert-manager.ClusterIssuer.Type + > + +in \(webhook : Webhook) + -> { apiVersion = "v1" + , kind = "List" + , items = + [ union.Deployment (deployment webhook) + , union.Service (service webhook) + , union.MWH (mutatingWebhookConfiguration webhook) + , union.Certificate (certificate webhook) + , union.ClusterIssuer (issuer webhook) + ] + } diff --git a/kubernetes/webhook/schema.dhall b/kubernetes/webhook/schema.dhall new file mode 100644 index 0000000..7f96a2a --- /dev/null +++ b/kubernetes/webhook/schema.dhall @@ -0,0 +1,20 @@ +let k8s = + https://raw.githubusercontent.com/EarnestResearch/dhall-packages/master/kubernetes/k8s/1.14.dhall sha256:7839bf40f940757e4d71d3c1b84d878f6a4873c3b2706ae4be307b5991acdcac + +let T = + { name : Text + , namespace : Text + , imageName : Text + , namespaceSelector : Optional k8s.LabelSelector.Type + , port : Natural + , path : Text + , failurePolicy : Optional Text + , rules : List k8s.RuleWithOperations.Type + } + +let D = + { namespaceSelector = None k8s.LabelSelector.Type + , failurePolicy = None Text + } + +in { Type = T, default = D } diff --git a/package.dhall b/package.dhall index 2ca18f0..3b2ea7b 100644 --- a/package.dhall +++ b/package.dhall @@ -1,5 +1,5 @@ { kubernetes = - ./kubernetes/package.dhall sha256:37febb5b82233d35fe28c9df565da395706cc82bf75de51554f1ca52b8c8e6f4 + ./kubernetes/package.dhall sha256:a1c73064a7d365a5be1eba39c7d2ca882ab9d1fa53f38c3aa10ecb9067fa1131 ? ./kubernetes/package.dhall , Prelude = https://prelude.dhall-lang.org/v12.0.0/package.dhall sha256:aea6817682359ae1939f3a15926b84ad5763c24a3740103202d2eaaea4d01f4c