Add high level API (#34)

This adds a high level API (+ tests) for:
- Deployment.v1
- Service.v1
This commit is contained in:
Fabrizio Ferrai 2018-09-13 13:33:48 +03:00 committed by GitHub
parent 11a986ae11
commit f8be1d55bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 867 additions and 339 deletions

271
README.md
View File

@ -1,7 +1,30 @@
# `dhall-kubernetes`
Dhall bindings to Kubernetes.
This will let you typecheck, template and modularize your Kubernetes definitions with [Dhall][dhall-lang].
`dhall-kubernetes` contains [Dhall][dhall-lang] bindings to [Kubernetes][kubernetes],
so you can generate Kubernetes objects definitions from Dhall expressions.
This will let you easily typecheck, template and modularize your Kubernetes definitions.
## Why do I need this
Once you build a slightly non-trivial Kubernetes setup, with many objects floating
around, you'll encounter several issues:
1. Writing the definitions in YAML is really verbose, and the actually important
things don't stand out that much
2. Ok I have a bunch of objects that'll need to be configured together, how do I share data?
3. I'd like to reuse an object for different environments, but I cannot make it parametric..
4. In general, I'd really love to reuse parts of some definitions in other definitions
5. Oh no, I typoed a key and I had to wait until I pushed to the cluster to get an error back :(
The natural tendency is to reach for a templating language + a programming language to orchestrate that + some more configuration for it...
But this is just really messy (been there), and we can do better.
Dhall solves all of this, being a programming language with builtin templating,
all while being non-Turing complete, strongly typed and [strongly normalizing][normalization]
(i.e.: reduces everything to a normal form, no matter how much abstraction you build),
so saving you from the *"oh-noes-I-made-my-config-in-code-and-now-its-too-abstract"* nightmare.
For a Dhall Tutorial, see the [readme of the project][dhall-lang],
or the [full tutorial][dhall-tutorial].
## Prerequisites
@ -14,105 +37,42 @@ stack install dhall dhall-json --resolver=nightly
For a version compatible with a previous version, check out [this commit](https://github.com/dhall-lang/dhall-kubernetes/tree/b2357dcfa42a008efa203a850163d26f0d106e01).
## Quick start
## Quickstart - main API
In the `types` folder you'll find the types for the Kubernetes definitions. E.g.
[here's][Deployment] the type for a Deployment.
We provide a simple API for the most common cases (For a list, see the [api](./api) folder).
Since _most_ of the fields in all definitions are optional, for better
ergonomics while coding Dhall we also generate default values for all types, in
the `default` folder. When some fields are required, the default value is a
function whose input is a record of required fields, that returns the object
with these fields set. E.g. the default for the Deployment is [this
function][Deployment-default].
Let's say we'd like to configure a Deployment exposing an `nginx` webserver.
In the following example, we:
1. Define a `config` for our service, by merging a [default config][default-deployment]
(with the Dhall record-merge operator `//`) with a record with our parameters.
2. In there we define the details of the Deployment we care about (note that we do the same
"merging with defaults" operation for our container as well, so we don't have to specify
all the parameters)
3. We call the [`mkDeployment`][mkDeployment] function on our `config`
Since this might sound a bit abstract, let's go with some examples. You can find
these examples in the [`./examples` folder](./examples) and evaluate them there.
### Example: Deployment
Let's say we have several services, whose configuration has this type:
```haskell
-- examples/Service.dhall
{ name : Text
, host : Text
, version : Text
}
```
So a configuration for a service might look like this:
```haskell
-- examples/service-foo.dhall
{ name = "foo"
, host = "foo.example.com"
, version = "1.0.1"
}
```
We can then make a Deployment object for this service:
```haskell
-- examples/deployment.dhall
-- Prelude imports
let map = https://raw.githubusercontent.com/dhall-lang/Prelude/e44284bc37a5808861dacd4c8bd13d18411cb961/List/map
in let Some = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/Some
in let None = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/None
-- import dhall-kubernetes types and defaults
in let Deployment = ../types/io.k8s.api.apps.v1beta2.Deployment.dhall
in let Spec = ../types/io.k8s.api.apps.v1beta2.DeploymentSpec.dhall
in let PodSpec = ../types/io.k8s.api.core.v1.PodSpec.dhall
in let ContainerPort = ../types/io.k8s.api.core.v1.ContainerPort.dhall
in let defaultDeployment = ../default/io.k8s.api.apps.v1beta2.Deployment.dhall
in let defaultMeta = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta.dhall
in let defaultSpec = ../default/io.k8s.api.apps.v1beta2.DeploymentSpec.dhall
in let defaultTemplate = ../default/io.k8s.api.core.v1.PodTemplateSpec.dhall
in let defaultPodSpec = ../default/io.k8s.api.core.v1.PodSpec.dhall
in let defaultSelector = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector.dhall
in let defaultContainer = ../default/io.k8s.api.core.v1.Container.dhall
in let defaultContainerPort = ../default/io.k8s.api.core.v1.ContainerPort.dhall
-- and our service
in let fooService = ./service-foo.dhall
-- Generate the DeploymentSpec for the service
in let selector = Some
(List { mapKey : Text, mapValue : Text })
[{ mapKey = "app", mapValue = fooService.name }]
in let spec = defaultSpec
{ selector = defaultSelector // { matchLabels = selector }
, template = defaultTemplate
{ metadata = defaultMeta
{ name = fooService.name } // { labels = selector }
} //
{ spec = Some PodSpec (defaultPodSpec
{ containers = [
defaultContainer
{ name = fooService.name } //
{ image = Some Text "your-container-service.io/${fooService.name}:${fooService.version}"
, imagePullPolicy = Some Text "Always"
, ports = Some
(List ContainerPort)
[(defaultContainerPort {containerPort = 8080})]
let config =
../api/Deployment/default
//
{ name = "nginx"
, replicas = 2
, containers =
[ ../api/Deployment/defaultContainer
//
{ name = "nginx"
, imageName = "nginx"
, imageTag = "1.15.3"
, port = [ 80 ] : Optional Natural
}
]})
}
} //
{ replicas = Some Natural 2
, revisionHistoryLimit = Some Natural 10
]
}
-- and here's the Deployment
in defaultDeployment
{ metadata = defaultMeta { name = fooService.name }
} //
{ spec = Some Spec spec } : Deployment
in ../api/Deployment/mkDeployment config
```
We convert it to yaml with:
We then run this through `dhall-to-yaml` to generate our Kubernetes definition:
```bash
dhall-to-yaml --omitNull < deployment.dhall
@ -120,44 +80,96 @@ dhall-to-yaml --omitNull < deployment.dhall
And we get:
```yaml
-- examples/out/deployment.yaml
apiVersion: apps/v1beta2
## examples/out/deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
revisionHistoryLimit: 10
revisionHistoryLimit: 20
selector:
matchLabels:
app: foo
app: nginx
strategy:
rollingUpdate:
maxSurge: 5
maxUnavailable: 0
type: RollingUpdate
template:
spec:
containers:
- image: your-container-service.io/foo:1.0.1
- image: nginx:1.15.3
imagePullPolicy: Always
name: foo
env: []
volumeMounts: []
resources:
limits:
cpu: 500m
requests:
cpu: 10m
name: nginx
ports:
- containerPort: 8080
- containerPort: 80
volumes: []
metadata:
name: foo
name: nginx
labels:
app: foo
app: nginx
replicas: 2
metadata:
name: foo
name: nginx
```
## Advanced usage - raw API
### Example: Ingress
If the main API is not enough (e.g. the object you'd like to generate is not in the list),
you can just fall back on using the raw Types and defaults the library provides
(and Pull Request here your program afterwards!).
Let's say we want to generate an Ingress definition (for an [Nginx Ingress][nginx-ingress])
that contains TLS certs and routes for every service.
For more examples of using this API see the [`./examples` folder](./examples).
In the [`types`](./types) folder you'll find the types for the Kubernetes definitions. E.g.
[here's][Ingress] the type for the Ingress.
Since _most_ of the fields in all definitions are optional, for better
ergonomics while coding Dhall we also generate default values for all types, in
the [`default`](./default) folder. When some fields are required, the default value
is a function whose input is a record of required fields, that returns the object
with these fields set. E.g. the default for the Ingress is [this
function][Ingress-default].
Let's say we have a Service with the following configuration:
Let's say we now want to generate an Ingress definition (for an Nginx Ingress)
that contains TLS certs and routes for every service. It would be something like
this:
```haskell
-- examples/ingress.dhall
-- examples/myConfig.dhall
{ name = "foo"
, host = "foo.example.com"
, version = "1.0.1"
}
```
That has the following type:
```haskell
-- examples/Config.dhall
{ name : Text
, host : Text
, version : Text
}
```
We can now expose this service out to the world with the Ingress:
```haskell
-- examples/ingressRaw.dhall
-- Prelude imports
let map = https://raw.githubusercontent.com/dhall-lang/Prelude/e44284bc37a5808861dacd4c8bd13d18411cb961/List/map
in let Some = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/Some
in let None = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/None
let map = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/List/map
in let Some = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/Optional/Some
in let None = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/Optional/None
-- dhall-kubernetes types and defaults
in let TLS = ../types/io.k8s.api.extensions.v1beta1.IngressTLS.dhall
@ -171,10 +183,16 @@ in let defaultSpec = ../default/io.k8s.api.extensions.v1beta1.IngressSpec.dha
in let IntOrString = ../default/io.k8s.apimachinery.pkg.util.intstr.IntOrString.dhall
-- Our Service type
in let Service = ./Service.dhall
in let Service = ./Config.dhall
in let Config = { services : List Service }
-- A function to generate an ingress given a configuration
in let mkIngress : Config -> Ingress =
\(config : Config) ->
-- Given a service, make a TLS definition with their host and certificate
in let makeTLS = \(service : Service) ->
let makeTLS = \(service : Service) ->
{ hosts = Some (List Text) [ service.host ]
, secretName = Some Text "${service.name}-certificate"
}
@ -187,7 +205,10 @@ in let makeRule = \(service : Service) ->
, servicePort = IntOrString.Int 80
}
, path = None Text
}]}}
}
]
}
}
-- Nginx ingress requires a default service as a catchall
in let defaultService =
@ -197,11 +218,7 @@ in let defaultService =
}
-- List of services
in let fooService = ./service-foo.dhall
in let services =
[ fooService
, defaultService
]
in let services = config.services # [ defaultService ]
-- Some metadata annotations
-- NOTE: `dhall-to-yaml` will generate a record with arbitrary keys from a list
@ -225,19 +242,23 @@ in defaultIngress
{ name = "nginx" } //
{ annotations = annotations }
} //
{ spec = Some Spec spec } : Ingress
{ spec = Some Spec spec }
-- Here we import our example service, and generate the ingress with it
in mkIngress { services = [ ./myConfig.dhall ] }
```
As usual we get the yaml out by running:
As before we get the yaml out by running:
```bash
dhall-to-yaml --omitNull < ingress.yaml.dhall
```
And we get:
Result:
```yaml
-- examples/out/ingress.yaml
## examples/out/ingressRaw.yaml
apiVersion: extensions/v1beta1
kind: Ingress
spec:
@ -294,5 +315,11 @@ to run `scripts/build-readme.sh`.
[hydra-project]: http://hydra.dhall-lang.org/project/dhall-kubernetes
[dhall-lang]: https://github.com/dhall-lang/dhall-lang
[Deployment]: https://github.com/dhall-lang/dhall-kubernetes/blob/master/types/io.k8s.api.apps.v1beta2.Deployment.dhall
[Deployment-default]: https://github.com/dhall-lang/dhall-kubernetes/blob/master/default/io.k8s.api.apps.v1beta2.Deployment.dhall
[kubernetes]: https://kubernetes.io/
[normalization]: https://en.wikipedia.org/wiki/Normalization_property_(abstract_rewriting)
[nginx-ingress]: https://github.com/kubernetes/ingress-nginx
[dhall-tutorial]: http://hackage.haskell.org/package/dhall-1.17.0/docs/Dhall-Tutorial.html
[default-deployment]: ./api/Deployment/default
[mkDeployment]: ./api/Deployment/mkDeployment
[Ingress]: ./types/io.k8s.api.extensions.v1beta1.Ingress.dhall
[Ingress-default]: ./default/io.k8s.api.extensions.v1beta1.Ingress.dhall

13
api/Deployment/Container Normal file
View File

@ -0,0 +1,13 @@
{ name : Text
, imageName : Text
, imageTag : Text
, imagePullPolicy : Text
, minCPU : Natural
, maxCPU : Natural
, mounts : List ./Mount
, envVars : List { mapKey : Text, mapValue : Text }
, port : Optional Natural
, command : Optional (List Text)
, livenessProbe : Optional ./Probe
, readinessProbe : Optional ./Probe
}

10
api/Deployment/Deployment Normal file
View File

@ -0,0 +1,10 @@
{ name : Text
, replicas : Natural
, revisionHistoryLimit : Natural
, maxSurge : Natural
, maxUnavailable : Natural
, containers : List ./Container
, emptyVolumes : List { name : Text }
, secretVolumes : List { name : Text }
, pathVolumes : List { name : Text, path : Text }
}

4
api/Deployment/Mount Normal file
View File

@ -0,0 +1,4 @@
{ mountPath : Text
, name : Text
, readOnly : Optional Bool
}

5
api/Deployment/Probe Normal file
View File

@ -0,0 +1,5 @@
{ initial : Natural
, period : Natural
, port : Natural
, path : Text
}

View File

@ -0,0 +1,23 @@
let intOrString = ../../default/io.k8s.apimachinery.pkg.util.intstr.IntOrString.dhall
in
{ deployment = ../../default/io.k8s.api.apps.v1.Deployment.dhall
, container = ../../default/io.k8s.api.core.v1.Container.dhall
, containerPort = ../../default/io.k8s.api.core.v1.ContainerPort.dhall
, podSpec = ../../default/io.k8s.api.core.v1.PodSpec.dhall
, spec = ../../default/io.k8s.api.apps.v1.DeploymentSpec.dhall
, template = ../../default/io.k8s.api.core.v1.PodTemplateSpec.dhall
, probe = ../../default/io.k8s.api.core.v1.Probe.dhall
, httpGet = ../../default/io.k8s.api.core.v1.HTTPGetAction.dhall
, envVar = ../../default/io.k8s.api.core.v1.EnvVar.dhall
, mount = ../../default/io.k8s.api.core.v1.VolumeMount.dhall
, volume = ../../default/io.k8s.api.core.v1.Volume.dhall
, secretVolume = ../../default/io.k8s.api.core.v1.SecretVolumeSource.dhall
, emptyVolume = ../../default/io.k8s.api.core.v1.EmptyDirVolumeSource.dhall
, pathVolume = ../../default/io.k8s.api.core.v1.HostPathVolumeSource.dhall
, meta = ../../default/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta.dhall
, selector = ../../default/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector.dhall
, intOrString = intOrString
, Int = intOrString.Int
, String = intOrString.String
}

18
api/Deployment/RawTypes Normal file
View File

@ -0,0 +1,18 @@
{ Deployment = ../../types/io.k8s.api.apps.v1.Deployment.dhall
, Container = ../../types/io.k8s.api.core.v1.Container.dhall
, ContainerPort = ../../types/io.k8s.api.core.v1.ContainerPort.dhall
, PodSpec = ../../types/io.k8s.api.core.v1.PodSpec.dhall
, RollingUpdate = ../../types/io.k8s.api.apps.v1.RollingUpdateDeployment.dhall
, Spec = ../../types/io.k8s.api.apps.v1.DeploymentSpec.dhall
, Strategy = ../../types/io.k8s.api.apps.v1.DeploymentStrategy.dhall
, Resources = ../../types/io.k8s.api.core.v1.ResourceRequirements.dhall
, Probe = ../../types/io.k8s.api.core.v1.Probe.dhall
, HttpGet = ../../types/io.k8s.api.core.v1.HTTPGetAction.dhall
, EnvVar = ../../types/io.k8s.api.core.v1.EnvVar.dhall
, Mount = ../../types/io.k8s.api.core.v1.VolumeMount.dhall
, Volume = ../../types/io.k8s.api.core.v1.Volume.dhall
, SecretVolume = ../../types/io.k8s.api.core.v1.SecretVolumeSource.dhall
, EmptyVolume = ../../types/io.k8s.api.core.v1.EmptyDirVolumeSource.dhall
, PathVolume = ../../types/io.k8s.api.core.v1.HostPathVolumeSource.dhall
, IntOrString = ../../types/io.k8s.apimachinery.pkg.util.intstr.IntOrString.dhall
}

10
api/Deployment/default Normal file
View File

@ -0,0 +1,10 @@
{ name = "CHANGEME"
, replicas = 1
, revisionHistoryLimit = 20
, maxSurge = 5
, maxUnavailable = 0
, containers = [] : List ./Container
, emptyVolumes = [] : List { name : Text }
, secretVolumes = [] : List { name : Text }
, pathVolumes = [] : List { name : Text, path : Text }
} : ./Deployment

View File

@ -0,0 +1,13 @@
{ name = "CHANGEME"
, imageName = "SOME_IMAGE"
, imageTag = "0.1"
, imagePullPolicy = "Always"
, minCPU = 10
, maxCPU = 500
, mounts = [] : List ./Mount
, envVars = [] : List { mapKey : Text, mapValue : Text }
, port = [] : Optional Natural
, command = [] : Optional (List Text)
, livenessProbe = [] : Optional ./Probe
, readinessProbe = [] : Optional ./Probe
} : ./Container

170
api/Deployment/mkDeployment Normal file
View File

@ -0,0 +1,170 @@
-- Prelude
let Prelude = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/package.dhall
in let map = Prelude.`List`.map
in let Some = Prelude.`Optional`.Some
in let None = Prelude.`Optional`.None
in let kv = Prelude.JSON.keyText
-- Kubernetes types and defaults
in let Types = ./RawTypes
in let default = ./RawDefaults
-- Types for dynamic records
in let KV = { mapKey : Text, mapValue : Text }
in let ListKV = List KV
in let mkProbe : ./Probe → Optional Types.Probe =
λ(probe : ./Probe) →
Some Types.Probe
(default.probe //
{ initialDelaySeconds = Some Natural probe.initial
, periodSeconds = Some Natural probe.period
, httpGet = Some Types.HttpGet
(default.httpGet
{ port = default.Int probe.port } //
{ path = Some Text probe.path
})
})
in let mkEnvVar : KV → Types.EnvVar =
λ(var : KV) →
default.envVar
{ name = var.mapKey } //
{ value = Some Text var.mapValue }
in let mkEmptyVolume : { name : Text } → Types.Volume =
λ(vol : { name : Text }) →
default.volume
{ name = vol.name } //
{ emptyDir = Some Types.EmptyVolume default.emptyVolume }
in let mkSecretVolume : { name : Text } → Types.Volume =
λ(vol : { name : Text }) →
default.volume
{ name = vol.name } //
{ secret = Some Types.SecretVolume
(default.secretVolume // { secretName = Some Text vol.name } )
}
in let mkPathVolume : { name : Text, path : Text } → Types.Volume =
λ(vol : { name : Text, path : Text }) →
default.volume
{ name = vol.name } //
{ hostPath = Some Types.PathVolume
(default.pathVolume { path = vol.path })
}
in let mkMount : ./Mount → Types.Mount =
λ(mount : ./Mount) →
default.mount
{ mountPath = mount.mountPath
, name = mount.name
} //
{ readOnly = mount.readOnly }
in let mkContainer : ./Container → Types.Container =
λ(container : ./Container) →
default.container
{ name = container.name } //
{ image = Some Text "${container.imageName}:${container.imageTag}"
, imagePullPolicy = Some Text container.imagePullPolicy
, ports = Optional/fold
Natural
container.port
(Optional (List Types.ContainerPort))
(λ(port : Natural) → Some (List Types.ContainerPort)
[(default.containerPort { containerPort = port })])
(None (List Types.ContainerPort))
, resources = Some Types.Resources
{ limits = Some ListKV [kv "cpu" "${Natural/show container.maxCPU}m"]
, requests = Some ListKV [kv "cpu" "${Natural/show container.minCPU}m"]
}
, command = container.command
, volumeMounts = Some (List Types.Mount)
(map ./Mount Types.Mount mkMount container.mounts)
-- Poll the container to see if the it's alive or we should restart it
, livenessProbe = Optional/fold
./Probe
container.livenessProbe
(Optional Types.Probe)
mkProbe
(None Types.Probe)
-- Poll the container to see that it's ready for requests
, readinessProbe = Optional/fold
./Probe
container.readinessProbe
(Optional Types.Probe)
mkProbe
(None Types.Probe)
, env = Some (List Types.EnvVar)
(map { mapKey : Text , mapValue : Text } Types.EnvVar mkEnvVar container.envVars)
}
in let mkDeployment : ./Deployment → Types.Deployment =
λ(deployment : ./Deployment) →
let selector = Some ListKV [kv "app" deployment.name]
in let emptyVolumes = map { name : Text } Types.Volume mkEmptyVolume deployment.emptyVolumes
in let secretVolumes = map { name : Text } Types.Volume mkSecretVolume deployment.secretVolumes
in let pathVolumes = map { name : Text, path : Text } Types.Volume mkPathVolume deployment.pathVolumes
in let volumes = Some (List Types.Volume) (emptyVolumes # secretVolumes # pathVolumes)
in let spec = default.spec
{ selector = default.selector // { matchLabels = selector }
, template = default.template
{ metadata = default.meta
{ name = deployment.name } // { labels = selector }
} //
{ spec = Some Types.PodSpec (default.podSpec
{ containers = map ./Container Types.Container mkContainer deployment.containers
} //
{ volumes = volumes
})
}
} //
{ replicas = Some Natural deployment.replicas
-- Don't keep all the ReplicaSets
, revisionHistoryLimit = Some Natural deployment.revisionHistoryLimit
, strategy = Some Types.Strategy
-- Control the RollingUpdate so the app is always available. For more info see:
-- https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
{ type = Some Text "RollingUpdate"
, rollingUpdate = Some Types.RollingUpdate
{ maxSurge = Some Types.IntOrString (default.Int deployment.maxSurge)
, maxUnavailable = Some Types.IntOrString (default.Int deployment.maxUnavailable)
}
}
}
in default.deployment
{ metadata = default.meta { name = deployment.name }
} //
{ spec = Some Types.Spec spec
}
in mkDeployment

9
api/Service/RawDefaults Normal file
View File

@ -0,0 +1,9 @@
let intOrString = ../../default/io.k8s.apimachinery.pkg.util.intstr.IntOrString.dhall
in
{ service = ../../default/io.k8s.api.core.v1.Service.dhall
, spec = ../../default/io.k8s.api.core.v1.ServiceSpec.dhall
, port = ../../default/io.k8s.api.core.v1.ServicePort.dhall
, meta = ../../default/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta.dhall
, Int = intOrString.Int
, String = intOrString.String
}

5
api/Service/RawTypes Normal file
View File

@ -0,0 +1,5 @@
{ Service = ../../types/io.k8s.api.core.v1.Service.dhall
, Spec = ../../types/io.k8s.api.core.v1.ServiceSpec.dhall
, Port = ../../types/io.k8s.api.core.v1.ServicePort.dhall
, IntOrString = ../../types/io.k8s.apimachinery.pkg.util.intstr.IntOrString.dhall
}

6
api/Service/Service Normal file
View File

@ -0,0 +1,6 @@
{ name : Text
, annotations : List { mapKey : Text, mapValue : Text }
, containerPort : Natural
, outPort : Natural
, type : ./ServiceType
}

5
api/Service/ServiceType Normal file
View File

@ -0,0 +1,5 @@
< ClusterIP : {}
| NodePort : {}
| LoadBalancer : {}
| ExternalName : {}
>

6
api/Service/default Normal file
View File

@ -0,0 +1,6 @@
{ name = "CHANGEME"
, annotations = [] : List { mapKey : Text, mapValue : Text }
, containerPort = 8080
, outPort = 80
, type = (constructors ./ServiceType).NodePort {=}
}

51
api/Service/mkService Normal file
View File

@ -0,0 +1,51 @@
-- Prelude
let Prelude = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/package.dhall
in let Some = Prelude.`Optional`.Some
in let kv = Prelude.JSON.keyText
-- Kubernetes types and defaults
in let Types = ./RawTypes
in let default = ./RawDefaults
-- Types for dynamic records
in let KV = { mapKey : Text, mapValue : Text }
in let ListKV = List KV
in let mkService : ./Service → Types.Service =
λ(service : ./Service) →
let selector = Some ListKV [kv "app" service.name]
in let meta = default.meta
{ name = service.name } //
{ labels = selector
, annotations = Some ListKV service.annotations
}
-- Handlers for the ServiceType union
in let handlers =
{ ClusterIP = \(_ : {}) -> "ClusterIP"
, NodePort = \(_ : {}) -> "NodePort"
, LoadBalancer = \(_ : {}) -> "LoadBalancer"
, ExternalName = \(_ : {}) -> "ExternalName"
}
in let spec = default.spec //
{ type = Some Text (merge handlers service.type : Text)
, ports = Some (List Types.Port)
[ default.port
{ port = service.outPort } //
{ targetPort = Some Types.IntOrString (default.Int service.containerPort) }
]
, selector = selector
}
in default.service
{ metadata = meta
} //
{ spec = Some Types.Spec spec
} : Types.Service
in mkService

View File

@ -1,8 +1,31 @@
''
# `dhall-kubernetes`
Dhall bindings to Kubernetes.
This will let you typecheck, template and modularize your Kubernetes definitions with [Dhall][dhall-lang].
`dhall-kubernetes` contains [Dhall][dhall-lang] bindings to [Kubernetes][kubernetes],
so you can generate Kubernetes objects definitions from Dhall expressions.
This will let you easily typecheck, template and modularize your Kubernetes definitions.
## Why do I need this
Once you build a slightly non-trivial Kubernetes setup, with many objects floating
around, you'll encounter several issues:
1. Writing the definitions in YAML is really verbose, and the actually important
things don't stand out that much
2. Ok I have a bunch of objects that'll need to be configured together, how do I share data?
3. I'd like to reuse an object for different environments, but I cannot make it parametric..
4. In general, I'd really love to reuse parts of some definitions in other definitions
5. Oh no, I typoed a key and I had to wait until I pushed to the cluster to get an error back :(
The natural tendency is to reach for a templating language + a programming language to orchestrate that + some more configuration for it...
But this is just really messy (been there), and we can do better.
Dhall solves all of this, being a programming language with builtin templating,
all while being non-Turing complete, strongly typed and [strongly normalizing][normalization]
(i.e.: reduces everything to a normal form, no matter how much abstraction you build),
so saving you from the *"oh-noes-I-made-my-config-in-code-and-now-its-too-abstract"* nightmare.
For a Dhall Tutorial, see the [readme of the project][dhall-lang],
or the [full tutorial][dhall-tutorial].
## Prerequisites
@ -15,42 +38,25 @@ stack install dhall dhall-json --resolver=nightly
For a version compatible with a previous version, check out [this commit](https://github.com/dhall-lang/dhall-kubernetes/tree/b2357dcfa42a008efa203a850163d26f0d106e01).
## Quick start
## Quickstart - main API
In the `types` folder you'll find the types for the Kubernetes definitions. E.g.
[here's][Deployment] the type for a Deployment.
We provide a simple API for the most common cases (For a list, see the [api](./api) folder).
Since _most_ of the fields in all definitions are optional, for better
ergonomics while coding Dhall we also generate default values for all types, in
the `default` folder. When some fields are required, the default value is a
function whose input is a record of required fields, that returns the object
with these fields set. E.g. the default for the Deployment is [this
function][Deployment-default].
Let's say we'd like to configure a Deployment exposing an `nginx` webserver.
In the following example, we:
1. Define a `config` for our service, by merging a [default config][default-deployment]
(with the Dhall record-merge operator `//`) with a record with our parameters.
2. In there we define the details of the Deployment we care about (note that we do the same
"merging with defaults" operation for our container as well, so we don't have to specify
all the parameters)
3. We call the [`mkDeployment`][mkDeployment] function on our `config`
Since this might sound a bit abstract, let's go with some examples. You can find
these examples in the [`./examples` folder](./examples) and evaluate them there.
### Example: Deployment
Let's say we have several services, whose configuration has this type:
```haskell
-- examples/Service.dhall
${../examples/Service.dhall as Text}
```
So a configuration for a service might look like this:
```haskell
-- examples/service-foo.dhall
${../examples/service-foo.dhall as Text}
```
We can then make a Deployment object for this service:
```haskell
-- examples/deployment.dhall
${../examples/deployment.dhall as Text}
```
We convert it to yaml with:
We then run this through `dhall-to-yaml` to generate our Kubernetes definition:
```bash
dhall-to-yaml --omitNull < deployment.dhall
@ -58,31 +64,62 @@ dhall-to-yaml --omitNull < deployment.dhall
And we get:
```yaml
-- examples/out/deployment.yaml
## examples/out/deployment.yaml
${../examples/out/deployment.yaml as Text}
```
## Advanced usage - raw API
### Example: Ingress
If the main API is not enough (e.g. the object you'd like to generate is not in the list),
you can just fall back on using the raw Types and defaults the library provides
(and Pull Request here your program afterwards!).
Let's say we want to generate an Ingress definition (for an [Nginx Ingress][nginx-ingress])
that contains TLS certs and routes for every service.
For more examples of using this API see the [`./examples` folder](./examples).
In the [`types`](./types) folder you'll find the types for the Kubernetes definitions. E.g.
[here's][Ingress] the type for the Ingress.
Since _most_ of the fields in all definitions are optional, for better
ergonomics while coding Dhall we also generate default values for all types, in
the [`default`](./default) folder. When some fields are required, the default value
is a function whose input is a record of required fields, that returns the object
with these fields set. E.g. the default for the Ingress is [this
function][Ingress-default].
Let's say we have a Service with the following configuration:
Let's say we now want to generate an Ingress definition (for an Nginx Ingress)
that contains TLS certs and routes for every service. It would be something like
this:
```haskell
-- examples/ingress.dhall
${../examples/ingress.dhall as Text}
-- examples/myConfig.dhall
${../examples/myConfig.dhall as Text}
```
As usual we get the yaml out by running:
That has the following type:
```haskell
-- examples/Config.dhall
${../examples/Config.dhall as Text}
```
We can now expose this service out to the world with the Ingress:
```haskell
-- examples/ingressRaw.dhall
${../examples/ingressRaw.dhall as Text}
```
As before we get the yaml out by running:
```bash
dhall-to-yaml --omitNull < ingress.yaml.dhall
```
And we get:
Result:
```yaml
-- examples/out/ingress.yaml
${../examples/out/ingress.yaml as Text}
## examples/out/ingressRaw.yaml
${../examples/out/ingressRaw.yaml as Text}
```
## Development
@ -110,6 +147,12 @@ to run `scripts/build-readme.sh`.
[hydra-project]: http://hydra.dhall-lang.org/project/dhall-kubernetes
[dhall-lang]: https://github.com/dhall-lang/dhall-lang
[Deployment]: https://github.com/dhall-lang/dhall-kubernetes/blob/master/types/io.k8s.api.apps.v1beta2.Deployment.dhall
[Deployment-default]: https://github.com/dhall-lang/dhall-kubernetes/blob/master/default/io.k8s.api.apps.v1beta2.Deployment.dhall
[kubernetes]: https://kubernetes.io/
[normalization]: https://en.wikipedia.org/wiki/Normalization_property_(abstract_rewriting)
[nginx-ingress]: https://github.com/kubernetes/ingress-nginx
[dhall-tutorial]: http://hackage.haskell.org/package/dhall-1.17.0/docs/Dhall-Tutorial.html
[default-deployment]: ./api/Deployment/default
[mkDeployment]: ./api/Deployment/mkDeployment
[Ingress]: ./types/io.k8s.api.extensions.v1beta1.Ingress.dhall
[Ingress-default]: ./default/io.k8s.api.extensions.v1beta1.Ingress.dhall
''

View File

@ -1,55 +1,17 @@
-- Prelude imports
let map = https://raw.githubusercontent.com/dhall-lang/Prelude/e44284bc37a5808861dacd4c8bd13d18411cb961/List/map
in let Some = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/Some
in let None = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/None
-- import dhall-kubernetes types and defaults
in let Deployment = ../types/io.k8s.api.apps.v1beta2.Deployment.dhall
in let Spec = ../types/io.k8s.api.apps.v1beta2.DeploymentSpec.dhall
in let PodSpec = ../types/io.k8s.api.core.v1.PodSpec.dhall
in let ContainerPort = ../types/io.k8s.api.core.v1.ContainerPort.dhall
in let defaultDeployment = ../default/io.k8s.api.apps.v1beta2.Deployment.dhall
in let defaultMeta = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta.dhall
in let defaultSpec = ../default/io.k8s.api.apps.v1beta2.DeploymentSpec.dhall
in let defaultTemplate = ../default/io.k8s.api.core.v1.PodTemplateSpec.dhall
in let defaultPodSpec = ../default/io.k8s.api.core.v1.PodSpec.dhall
in let defaultSelector = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector.dhall
in let defaultContainer = ../default/io.k8s.api.core.v1.Container.dhall
in let defaultContainerPort = ../default/io.k8s.api.core.v1.ContainerPort.dhall
-- and our service
in let fooService = ./service-foo.dhall
-- Generate the DeploymentSpec for the service
in let selector = Some
(List { mapKey : Text, mapValue : Text })
[{ mapKey = "app", mapValue = fooService.name }]
in let spec = defaultSpec
{ selector = defaultSelector // { matchLabels = selector }
, template = defaultTemplate
{ metadata = defaultMeta
{ name = fooService.name } // { labels = selector }
} //
{ spec = Some PodSpec (defaultPodSpec
{ containers = [
defaultContainer
{ name = fooService.name } //
{ image = Some Text "your-container-service.io/${fooService.name}:${fooService.version}"
, imagePullPolicy = Some Text "Always"
, ports = Some
(List ContainerPort)
[(defaultContainerPort {containerPort = 8080})]
let config =
../api/Deployment/default
//
{ name = "nginx"
, replicas = 2
, containers =
[ ../api/Deployment/defaultContainer
//
{ name = "nginx"
, imageName = "nginx"
, imageTag = "1.15.3"
, port = [ 80 ] : Optional Natural
}
]})
}
} //
{ replicas = Some Natural 2
, revisionHistoryLimit = Some Natural 10
]
}
-- and here's the Deployment
in defaultDeployment
{ metadata = defaultMeta { name = fooService.name }
} //
{ spec = Some Spec spec } : Deployment
in ../api/Deployment/mkDeployment config

View File

@ -0,0 +1,80 @@
-- Prelude imports
let map = https://raw.githubusercontent.com/dhall-lang/Prelude/e44284bc37a5808861dacd4c8bd13d18411cb961/List/map
in let Some = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/Some
in let None = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/None
-- import dhall-kubernetes types and defaults
in let Deployment = ../types/io.k8s.api.apps.v1beta2.Deployment.dhall
in let Spec = ../types/io.k8s.api.apps.v1beta2.DeploymentSpec.dhall
in let PodSpec = ../types/io.k8s.api.core.v1.PodSpec.dhall
in let ContainerPort = ../types/io.k8s.api.core.v1.ContainerPort.dhall
in let defaultDeployment = ../default/io.k8s.api.apps.v1beta2.Deployment.dhall
in let defaultMeta = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta.dhall
in let defaultSpec = ../default/io.k8s.api.apps.v1beta2.DeploymentSpec.dhall
in let defaultTemplate = ../default/io.k8s.api.core.v1.PodTemplateSpec.dhall
in let defaultPodSpec = ../default/io.k8s.api.core.v1.PodSpec.dhall
in let defaultSelector = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.LabelSelector.dhall
in let defaultContainer = ../default/io.k8s.api.core.v1.Container.dhall
in let defaultContainerPort = ../default/io.k8s.api.core.v1.ContainerPort.dhall
{-
Here we import the Config type.
It's going to be the input to our mkDeployment function,
and contains the configuration for the Deployment.
-}
in let Config = ./Config.dhall
-- So here we define a function that outputs a Deployment
in let mkDeployment : Config -> Deployment =
\(deployment : Config) ->
let selector = Some (List { mapKey : Text, mapValue : Text })
[{ mapKey = "app", mapValue = deployment.name }]
in let spec = defaultSpec
{ selector = defaultSelector // { matchLabels = selector }
, template = defaultTemplate
{ metadata = defaultMeta
{ name = deployment.name } // { labels = selector }
} //
{ spec = Some PodSpec (defaultPodSpec
{ containers = [
defaultContainer
{ name = deployment.name } //
{ image = Some Text "your-container-service.io/${deployment.name}:${deployment.version}"
, imagePullPolicy = Some Text "Always"
, ports = Some (List ContainerPort)
[(defaultContainerPort {containerPort = 8080})]
}
]
})
}
} //
{ replicas = Some Natural 2
, revisionHistoryLimit = Some Natural 10
}
in defaultDeployment
{ metadata = defaultMeta { name = deployment.name }
} //
{ spec = Some Spec spec } : Deployment
{-
..and to keep the example self contained we import our config here.
A more modular approach would be to just define a function to make
the Deployment in this file, and then apply the right configuration
at the command line or in another Dhall file.
E.g.: `dhall-to-yaml --omitNull <<< "./examples/deploymentRaw.dhall ./myConfig.dhall"`
-}
in let myConfig = ./myConfig.dhall
-- and here we apply the deployment-making function to our config
in mkDeployment myConfig

View File

@ -1,72 +0,0 @@
-- Prelude imports
let map = https://raw.githubusercontent.com/dhall-lang/Prelude/e44284bc37a5808861dacd4c8bd13d18411cb961/List/map
in let Some = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/Some
in let None = https://raw.githubusercontent.com/dhall-lang/Prelude/c79c2bc3c46f129cc5b6d594ce298a381bcae92c/Optional/None
-- dhall-kubernetes types and defaults
in let TLS = ../types/io.k8s.api.extensions.v1beta1.IngressTLS.dhall
in let Rule = ../types/io.k8s.api.extensions.v1beta1.IngressRule.dhall
in let RuleVal = ../types/io.k8s.api.extensions.v1beta1.HTTPIngressRuleValue.dhall
in let Spec = ../types/io.k8s.api.extensions.v1beta1.IngressSpec.dhall
in let Ingress = ../types/io.k8s.api.extensions.v1beta1.Ingress.dhall
in let defaultIngress = ../default/io.k8s.api.extensions.v1beta1.Ingress.dhall
in let defaultMeta = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta.dhall
in let defaultSpec = ../default/io.k8s.api.extensions.v1beta1.IngressSpec.dhall
in let IntOrString = ../default/io.k8s.apimachinery.pkg.util.intstr.IntOrString.dhall
-- Our Service type
in let Service = ./Service.dhall
-- Given a service, make a TLS definition with their host and certificate
in let makeTLS = \(service : Service) ->
{ hosts = Some (List Text) [ service.host ]
, secretName = Some Text "${service.name}-certificate"
}
-- Given a service, make an Ingress Rule
in let makeRule = \(service : Service) ->
{ host = Some Text service.host
, http = Some RuleVal
{ paths = [ { backend = { serviceName = service.name
, servicePort = IntOrString.Int 80
}
, path = None Text
}]}}
-- Nginx ingress requires a default service as a catchall
in let defaultService =
{ name = "default"
, host = "default.example.com"
, version = " 1.0"
}
-- List of services
in let fooService = ./service-foo.dhall
in let services =
[ fooService
, defaultService
]
-- Some metadata annotations
-- NOTE: `dhall-to-yaml` will generate a record with arbitrary keys from a list
-- of records where mapKey is the key and mapValue is the value of that key
in let genericRecord = List { mapKey : Text, mapValue : Text }
in let kv = \(k : Text) -> \(v : Text) -> { mapKey = k, mapValue = v }
in let annotations = Some genericRecord
[ kv "kubernetes.io/ingress.class" "nginx"
, kv "kubernetes.io/ingress.allow-http" "false"
]
-- Generate spec from services
in let spec = defaultSpec //
{ tls = Some (List TLS) (map Service TLS makeTLS services)
, rules = Some (List Rule) (map Service Rule makeRule services)
}
in defaultIngress
{ metadata = defaultMeta
{ name = "nginx" } //
{ annotations = annotations }
} //
{ spec = Some Spec spec } : Ingress

81
examples/ingressRaw.dhall Normal file
View File

@ -0,0 +1,81 @@
-- Prelude imports
let map = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/List/map
in let Some = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/Optional/Some
in let None = https://raw.githubusercontent.com/dhall-lang/Prelude/v2.0.0/Optional/None
-- dhall-kubernetes types and defaults
in let TLS = ../types/io.k8s.api.extensions.v1beta1.IngressTLS.dhall
in let Rule = ../types/io.k8s.api.extensions.v1beta1.IngressRule.dhall
in let RuleVal = ../types/io.k8s.api.extensions.v1beta1.HTTPIngressRuleValue.dhall
in let Spec = ../types/io.k8s.api.extensions.v1beta1.IngressSpec.dhall
in let Ingress = ../types/io.k8s.api.extensions.v1beta1.Ingress.dhall
in let defaultIngress = ../default/io.k8s.api.extensions.v1beta1.Ingress.dhall
in let defaultMeta = ../default/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta.dhall
in let defaultSpec = ../default/io.k8s.api.extensions.v1beta1.IngressSpec.dhall
in let IntOrString = ../default/io.k8s.apimachinery.pkg.util.intstr.IntOrString.dhall
-- Our Service type
in let Service = ./Config.dhall
in let Config = { services : List Service }
-- A function to generate an ingress given a configuration
in let mkIngress : Config -> Ingress =
\(config : Config) ->
-- Given a service, make a TLS definition with their host and certificate
let makeTLS = \(service : Service) ->
{ hosts = Some (List Text) [ service.host ]
, secretName = Some Text "${service.name}-certificate"
}
-- Given a service, make an Ingress Rule
in let makeRule = \(service : Service) ->
{ host = Some Text service.host
, http = Some RuleVal
{ paths = [ { backend = { serviceName = service.name
, servicePort = IntOrString.Int 80
}
, path = None Text
}
]
}
}
-- Nginx ingress requires a default service as a catchall
in let defaultService =
{ name = "default"
, host = "default.example.com"
, version = " 1.0"
}
-- List of services
in let services = config.services # [ defaultService ]
-- Some metadata annotations
-- NOTE: `dhall-to-yaml` will generate a record with arbitrary keys from a list
-- of records where mapKey is the key and mapValue is the value of that key
in let genericRecord = List { mapKey : Text, mapValue : Text }
in let kv = \(k : Text) -> \(v : Text) -> { mapKey = k, mapValue = v }
in let annotations = Some genericRecord
[ kv "kubernetes.io/ingress.class" "nginx"
, kv "kubernetes.io/ingress.allow-http" "false"
]
-- Generate spec from services
in let spec = defaultSpec //
{ tls = Some (List TLS) (map Service TLS makeTLS services)
, rules = Some (List Rule) (map Service Rule makeRule services)
}
in defaultIngress
{ metadata = defaultMeta
{ name = "nginx" } //
{ annotations = annotations }
} //
{ spec = Some Spec spec }
-- Here we import our example service, and generate the ingress with it
in mkIngress { services = [ ./myConfig.dhall ] }

View File

@ -1,22 +1,35 @@
apiVersion: apps/v1beta2
apiVersion: apps/v1
kind: Deployment
spec:
revisionHistoryLimit: 10
revisionHistoryLimit: 20
selector:
matchLabels:
app: foo
app: nginx
strategy:
rollingUpdate:
maxSurge: 5
maxUnavailable: 0
type: RollingUpdate
template:
spec:
containers:
- image: your-container-service.io/foo:1.0.1
- image: nginx:1.15.3
imagePullPolicy: Always
name: foo
env: []
volumeMounts: []
resources:
limits:
cpu: 500m
requests:
cpu: 10m
name: nginx
ports:
- containerPort: 8080
- containerPort: 80
volumes: []
metadata:
name: foo
name: nginx
labels:
app: foo
app: nginx
replicas: 2
metadata:
name: foo
name: nginx

View File

@ -0,0 +1,22 @@
apiVersion: apps/v1beta2
kind: Deployment
spec:
revisionHistoryLimit: 10
selector:
matchLabels:
app: foo
template:
spec:
containers:
- image: your-container-service.io/foo:1.0.1
imagePullPolicy: Always
name: foo
ports:
- containerPort: 8080
metadata:
name: foo
labels:
app: foo
replicas: 2
metadata:
name: foo

14
examples/out/service.yaml Normal file
View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
spec:
selector:
app: nginx
ports:
- targetPort: 80
port: 80
type: NodePort
metadata:
annotations: {}
name: nginx
labels:
app: nginx

8
examples/service.dhall Normal file
View File

@ -0,0 +1,8 @@
let config =
../api/Service/default
//
{ name = "nginx"
, containerPort = 80
}
in ../api/Service/mkService config

View File

@ -15,8 +15,10 @@ import sys
examples_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'examples'))
examples = [
'deploymentRaw',
'ingressRaw',
'deployment',
'ingress'
'service'
]
TERM_FAIL = '\033[91m'