mirror of
https://github.com/dhall-lang/dhall-kubernetes.git
synced 2024-09-10 23:15:30 +03:00
Add high level API (#34)
This adds a high level API (+ tests) for: - Deployment.v1 - Service.v1
This commit is contained in:
parent
11a986ae11
commit
f8be1d55bc
349
README.md
349
README.md
@ -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,73 +183,82 @@ 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 }
|
||||
|
||||
-- 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"
|
||||
}
|
||||
-- A function to generate an ingress given a configuration
|
||||
in let mkIngress : Config -> Ingress =
|
||||
|
||||
-- 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
|
||||
}]}}
|
||||
\(config : Config) ->
|
||||
|
||||
-- Nginx ingress requires a default service as a catchall
|
||||
in let defaultService =
|
||||
{ name = "default"
|
||||
, host = "default.example.com"
|
||||
, version = " 1.0"
|
||||
}
|
||||
-- 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"
|
||||
}
|
||||
|
||||
-- List of services
|
||||
in let fooService = ./service-foo.dhall
|
||||
in let services =
|
||||
[ fooService
|
||||
, defaultService
|
||||
]
|
||||
-- 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-- 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 }
|
||||
-- Nginx ingress requires a default service as a catchall
|
||||
in let defaultService =
|
||||
{ name = "default"
|
||||
, host = "default.example.com"
|
||||
, version = " 1.0"
|
||||
}
|
||||
|
||||
in let annotations = Some genericRecord
|
||||
[ kv "kubernetes.io/ingress.class" "nginx"
|
||||
, kv "kubernetes.io/ingress.allow-http" "false"
|
||||
]
|
||||
-- List of services
|
||||
in let services = config.services # [ defaultService ]
|
||||
|
||||
-- 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)
|
||||
}
|
||||
-- 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 defaultIngress
|
||||
{ metadata = defaultMeta
|
||||
{ name = "nginx" } //
|
||||
{ annotations = annotations }
|
||||
} //
|
||||
{ spec = Some Spec spec } : Ingress
|
||||
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 ] }
|
||||
|
||||
```
|
||||
|
||||
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
13
api/Deployment/Container
Normal 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
10
api/Deployment/Deployment
Normal 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
4
api/Deployment/Mount
Normal file
@ -0,0 +1,4 @@
|
||||
{ mountPath : Text
|
||||
, name : Text
|
||||
, readOnly : Optional Bool
|
||||
}
|
5
api/Deployment/Probe
Normal file
5
api/Deployment/Probe
Normal file
@ -0,0 +1,5 @@
|
||||
{ initial : Natural
|
||||
, period : Natural
|
||||
, port : Natural
|
||||
, path : Text
|
||||
}
|
23
api/Deployment/RawDefaults
Normal file
23
api/Deployment/RawDefaults
Normal 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
18
api/Deployment/RawTypes
Normal 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
10
api/Deployment/default
Normal 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
|
13
api/Deployment/defaultContainer
Normal file
13
api/Deployment/defaultContainer
Normal 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
170
api/Deployment/mkDeployment
Normal 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
9
api/Service/RawDefaults
Normal 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
5
api/Service/RawTypes
Normal 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
6
api/Service/Service
Normal 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
5
api/Service/ServiceType
Normal file
@ -0,0 +1,5 @@
|
||||
< ClusterIP : {}
|
||||
| NodePort : {}
|
||||
| LoadBalancer : {}
|
||||
| ExternalName : {}
|
||||
>
|
6
api/Service/default
Normal file
6
api/Service/default
Normal 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
51
api/Service/mkService
Normal 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
|
@ -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
|
||||
''
|
||||
|
@ -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
|
||||
|
80
examples/deploymentRaw.dhall
Normal file
80
examples/deploymentRaw.dhall
Normal 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
|
@ -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
81
examples/ingressRaw.dhall
Normal 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 ] }
|
@ -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
|
||||
|
22
examples/out/deploymentRaw.yaml
Normal file
22
examples/out/deploymentRaw.yaml
Normal 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
14
examples/out/service.yaml
Normal 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
8
examples/service.dhall
Normal file
@ -0,0 +1,8 @@
|
||||
let config =
|
||||
../api/Service/default
|
||||
//
|
||||
{ name = "nginx"
|
||||
, containerPort = 80
|
||||
}
|
||||
|
||||
in ../api/Service/mkService config
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user