Skip to content

Commit f04a1f4

Browse files
authored
Merge pull request #1 from EarnestResearch/dev
Create Kubernetes Webhook project
2 parents be7a044 + dd8250b commit f04a1f4

File tree

17 files changed

+1403
-0
lines changed

17 files changed

+1403
-0
lines changed

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use nix

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.stack-work/
2+
*.pem
3+
*.csr
4+
result
5+
*.lock
6+
.pre-commit-config.yaml
7+
dist-newstyle/
8+
dist/

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
The MIT License
3+
4+
Copyright (c) 2020 Earnest Research
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# kubernetes-webhook-haskell
2+
3+
This library lets you create [Kubernetes Admission Webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) in Haskell.
4+
5+
Using webhooks in Kubernetes requires some configuration, in the [dhall](./dhall) directory you can find some useful templates that you can reuse to deploy your webhooks. [cert-manager](https://cert-manager.io/docs/) is required to be installed in the cluster to use the templates.
6+
7+
Example webhook using Servant:
8+
```hs
9+
module Kubernetes.Example
10+
( startApp,
11+
app,
12+
)
13+
where
14+
15+
import Control.Monad.IO.Class
16+
import qualified Data.Aeson as A
17+
import qualified Data.ByteString as BS
18+
import qualified Data.HashMap.Strict as HM
19+
import Data.Text
20+
import GHC.Generics
21+
import qualified Kubernetes.Webhook as W
22+
import Network.Wai
23+
import Network.Wai.Handler.Warp
24+
import Network.Wai.Handler.WarpTLS
25+
import Servant
26+
import System.Environment
27+
28+
type API =
29+
"mutate" :> ReqBody '[JSON] W.AdmissionReviewRequest :> Post '[JSON] W.AdmissionReviewResponse
30+
31+
data Toleration
32+
= Toleration
33+
{ effect :: Maybe TolerationEffect,
34+
key :: Maybe Text,
35+
operator :: Maybe TolerationOperator,
36+
tolerationSeconds :: Maybe Integer,
37+
value :: Maybe Text
38+
}
39+
deriving (Generic, A.ToJSON)
40+
41+
data TolerationEffect = NoSchedule | PreferNoSchedule | NoExecute deriving (Generic, A.ToJSON)
42+
43+
data TolerationOperator = Exists | Equal deriving (Generic, A.ToJSON)
44+
45+
testToleration :: Toleration
46+
testToleration =
47+
Toleration
48+
{ effect = Just NoSchedule,
49+
key = Just "dedicated",
50+
operator = Just Equal,
51+
tolerationSeconds = Nothing,
52+
value = Just "test"
53+
}
54+
55+
startApp :: IO ()
56+
startApp = do
57+
let tlsOpts = tlsSettings "/certs/tls.crt" "/certs/tls.key"
58+
warpOpts = setPort 8080 defaultSettings
59+
runTLS tlsOpts warpOpts app
60+
61+
app :: Application
62+
app = serve api server
63+
64+
api :: Proxy API
65+
api = Proxy
66+
67+
server :: Server API
68+
server = mutate
69+
70+
mutate :: W.AdmissionReviewRequest -> Handler W.AdmissionReviewResponse
71+
mutate req = pure $ W.mutatingWebhook req (\_ -> Right W.Allowed) addToleration
72+
73+
addToleration :: W.Patch
74+
addToleration =
75+
W.Patch
76+
[ W.PatchOperation
77+
{ op = W.Add,
78+
path = "/spec/tolerations/-",
79+
from = Nothing,
80+
value = Just $ A.toJSON testToleration
81+
}
82+
]
83+
```
84+

Setup.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain

default.nix

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{ pkgs ? import ./nixpkgs {}
2+
}:
3+
pkgs.haskell-nix.stackProject {
4+
src = pkgs.haskell-nix.haskellLib.cleanGit { src = ./.; };
5+
}

dhall/Config.dhall

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
let k8s =
2+
https://raw.githubusercontent.com/EarnestResearch/dhall-packages/master/kubernetes/k8s/1.14.dhall sha256:7839bf40f940757e4d71d3c1b84d878f6a4873c3b2706ae4be307b5991acdcac
3+
4+
in { name : Text
5+
, namespace : Text
6+
, imageName : Text
7+
, namespaceSelector : Optional k8s.LabelSelector.Type
8+
, port : Natural
9+
, path : Text
10+
, failurePolicy : Optional Text
11+
, rules : List k8s.RuleWithOperations.Type
12+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
let k8s =
2+
https://raw.githubusercontent.com/EarnestResearch/dhall-packages/master/kubernetes/k8s/1.14.dhall
3+
4+
let cert-manager =
5+
https://raw.githubusercontent.com/EarnestResearch/dhall-packages/master/kubernetes/cert-manager/package.dhall
6+
7+
let certsPath = "/certs"
8+
9+
let Config = ./Config.dhall
10+
11+
let labels = \(config : Config) -> toMap { app = config.name }
12+
13+
let deployment =
14+
\(config : Config)
15+
-> k8s.Deployment::{
16+
, metadata = k8s.ObjectMeta::{ name = config.name }
17+
, spec =
18+
Some
19+
k8s.DeploymentSpec::{
20+
, selector = k8s.LabelSelector::{ matchLabels = labels config }
21+
, template =
22+
k8s.PodTemplateSpec::{
23+
, metadata =
24+
k8s.ObjectMeta::{
25+
, name = config.name
26+
, labels = labels config
27+
}
28+
, spec =
29+
Some
30+
k8s.PodSpec::{
31+
, containers =
32+
[ k8s.Container::{
33+
, name = config.name
34+
, image = Some config.imageName
35+
, ports =
36+
[ k8s.ContainerPort::{
37+
, containerPort = config.port
38+
}
39+
]
40+
, env =
41+
[ k8s.EnvVar::{
42+
, name = "CERTIFICATE_FILE"
43+
, value = Some "${certsPath}/tls.crt"
44+
}
45+
, k8s.EnvVar::{
46+
, name = "KEY_FILE"
47+
, value = Some "${certsPath}/tls.key"
48+
}
49+
]
50+
, volumeMounts =
51+
[ k8s.VolumeMount::{
52+
, name = "certs"
53+
, mountPath = certsPath
54+
, readOnly = Some True
55+
}
56+
]
57+
}
58+
]
59+
, volumes =
60+
[ k8s.Volume::{
61+
, name = "certs"
62+
, secret =
63+
Some
64+
k8s.SecretVolumeSource::{
65+
, secretName = Some config.name
66+
}
67+
}
68+
]
69+
}
70+
}
71+
}
72+
}
73+
74+
let service =
75+
\(config : Config)
76+
-> k8s.Service::{
77+
, metadata = k8s.ObjectMeta::{ name = config.name }
78+
, spec =
79+
Some
80+
k8s.ServiceSpec::{
81+
, selector = labels config
82+
, ports =
83+
[ k8s.ServicePort::{
84+
, targetPort = Some (k8s.IntOrString.Int config.port)
85+
, port = 443
86+
}
87+
]
88+
}
89+
}
90+
91+
let issuer =
92+
\(config : Config)
93+
-> cert-manager.Issuer::{
94+
, metadata = k8s.ObjectMeta::{ name = config.name }
95+
, spec =
96+
cert-manager.IssuerSpec.SelfSigned
97+
cert-manager.SelfSignedIssuerSpec::{=}
98+
}
99+
100+
let certificate =
101+
\(config : Config)
102+
-> cert-manager.Certificate::{
103+
, metadata = k8s.ObjectMeta::{ name = config.name }
104+
, spec =
105+
cert-manager.CertificateSpec::{
106+
, secretName = config.name
107+
, issuerRef = { name = config.name, kind = (issuer config).kind }
108+
, commonName = Some "${config.name}.${config.namespace}.svc"
109+
, dnsNames =
110+
Some
111+
[ config.name
112+
, "${config.name}.${config.namespace}"
113+
, "${config.name}.${config.namespace}.svc"
114+
, "${config.name}.${config.namespace}.svc.cluster.local"
115+
, "${config.name}:443"
116+
, "${config.name}.${config.namespace}:443"
117+
, "${config.name}.${config.namespace}.svc:443"
118+
, "${config.name}.${config.namespace}.svc.cluster.local:443"
119+
, "localhost:8080"
120+
]
121+
, usages = Some [ "any" ]
122+
, isCA = Some False
123+
}
124+
}
125+
126+
let mutatingWebhookConfiguration =
127+
\(config : Config)
128+
-> k8s.MutatingWebhookConfiguration::{
129+
, metadata =
130+
k8s.ObjectMeta::{
131+
, name = config.name
132+
, labels = labels config
133+
, annotations =
134+
toMap
135+
{ `cert-manager.io/inject-ca-from` =
136+
"${config.namespace}/${config.name}"
137+
}
138+
}
139+
, webhooks =
140+
[ k8s.Webhook::{
141+
, name = "${config.name}.${config.namespace}.svc"
142+
, clientConfig =
143+
k8s.WebhookClientConfig::{
144+
, service =
145+
Some
146+
{ name = config.name
147+
, namespace = config.namespace
148+
, path = Some config.path
149+
}
150+
}
151+
, failurePolicy = config.failurePolicy
152+
, admissionReviewVersions = [ "v1beta1" ]
153+
, rules = config.rules
154+
, namespaceSelector = config.namespaceSelector
155+
}
156+
]
157+
}
158+
159+
let union =
160+
< Deployment : k8s.Deployment.Type
161+
| Service : k8s.Service.Type
162+
| MWH : k8s.MutatingWebhookConfiguration.Type
163+
| Certificate : cert-manager.Certificate.Type
164+
| ClusterIssuer : cert-manager.ClusterIssuer.Type
165+
>
166+
167+
in \(config : Config)
168+
-> { apiVersion = "v1"
169+
, kind = "List"
170+
, items =
171+
[ union.Deployment (deployment config)
172+
, union.Service (service config)
173+
, union.MWH (mutatingWebhookConfiguration config)
174+
, union.Certificate (certificate config)
175+
, union.ClusterIssuer (issuer config)
176+
]
177+
}

0 commit comments

Comments
 (0)