From f8be1d55bc641cdb4ad6a1531dbb3e9cad6b5d2e Mon Sep 17 00:00:00 2001 From: Fabrizio Ferrai Date: Thu, 13 Sep 2018 13:33:48 +0300 Subject: [PATCH] Add high level API (#34) This adds a high level API (+ tests) for: - Deployment.v1 - Service.v1 --- README.md | 349 ++++++++++-------- api/Deployment/Container | 13 + api/Deployment/Deployment | 10 + api/Deployment/Mount | 4 + api/Deployment/Probe | 5 + api/Deployment/RawDefaults | 23 ++ api/Deployment/RawTypes | 18 + api/Deployment/default | 10 + api/Deployment/defaultContainer | 13 + api/Deployment/mkDeployment | 170 +++++++++ api/Service/RawDefaults | 9 + api/Service/RawTypes | 5 + api/Service/Service | 6 + api/Service/ServiceType | 5 + api/Service/default | 6 + api/Service/mkService | 51 +++ docs/README.md.dhall | 129 ++++--- examples/{Service.dhall => Config.dhall} | 0 examples/deployment.dhall | 68 +--- examples/deploymentRaw.dhall | 80 ++++ examples/ingress.dhall | 72 ---- examples/ingressRaw.dhall | 81 ++++ .../{service-foo.dhall => myConfig.dhall} | 0 examples/out/deployment.yaml | 31 +- examples/out/deploymentRaw.yaml | 22 ++ .../out/{ingress.yaml => ingressRaw.yaml} | 0 examples/out/service.yaml | 14 + examples/service.dhall | 8 + scripts/build-examples.py | 4 +- 29 files changed, 867 insertions(+), 339 deletions(-) create mode 100644 api/Deployment/Container create mode 100644 api/Deployment/Deployment create mode 100644 api/Deployment/Mount create mode 100644 api/Deployment/Probe create mode 100644 api/Deployment/RawDefaults create mode 100644 api/Deployment/RawTypes create mode 100644 api/Deployment/default create mode 100644 api/Deployment/defaultContainer create mode 100644 api/Deployment/mkDeployment create mode 100644 api/Service/RawDefaults create mode 100644 api/Service/RawTypes create mode 100644 api/Service/Service create mode 100644 api/Service/ServiceType create mode 100644 api/Service/default create mode 100644 api/Service/mkService rename examples/{Service.dhall => Config.dhall} (100%) create mode 100644 examples/deploymentRaw.dhall delete mode 100644 examples/ingress.dhall create mode 100644 examples/ingressRaw.dhall rename examples/{service-foo.dhall => myConfig.dhall} (100%) create mode 100644 examples/out/deploymentRaw.yaml rename examples/out/{ingress.yaml => ingressRaw.yaml} (100%) create mode 100644 examples/out/service.yaml create mode 100644 examples/service.dhall diff --git a/README.md b/README.md index b059648c..e7e20da4 100644 --- a/README.md +++ b/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 diff --git a/api/Deployment/Container b/api/Deployment/Container new file mode 100644 index 00000000..5bb77445 --- /dev/null +++ b/api/Deployment/Container @@ -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 +} diff --git a/api/Deployment/Deployment b/api/Deployment/Deployment new file mode 100644 index 00000000..72cd3522 --- /dev/null +++ b/api/Deployment/Deployment @@ -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 } +} diff --git a/api/Deployment/Mount b/api/Deployment/Mount new file mode 100644 index 00000000..bf5aa336 --- /dev/null +++ b/api/Deployment/Mount @@ -0,0 +1,4 @@ +{ mountPath : Text +, name : Text +, readOnly : Optional Bool +} \ No newline at end of file diff --git a/api/Deployment/Probe b/api/Deployment/Probe new file mode 100644 index 00000000..48ace013 --- /dev/null +++ b/api/Deployment/Probe @@ -0,0 +1,5 @@ +{ initial : Natural +, period : Natural +, port : Natural +, path : Text +} diff --git a/api/Deployment/RawDefaults b/api/Deployment/RawDefaults new file mode 100644 index 00000000..cfa25067 --- /dev/null +++ b/api/Deployment/RawDefaults @@ -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 +} \ No newline at end of file diff --git a/api/Deployment/RawTypes b/api/Deployment/RawTypes new file mode 100644 index 00000000..cfbd70b9 --- /dev/null +++ b/api/Deployment/RawTypes @@ -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 +} diff --git a/api/Deployment/default b/api/Deployment/default new file mode 100644 index 00000000..d6ab26a6 --- /dev/null +++ b/api/Deployment/default @@ -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 diff --git a/api/Deployment/defaultContainer b/api/Deployment/defaultContainer new file mode 100644 index 00000000..6c38ca6e --- /dev/null +++ b/api/Deployment/defaultContainer @@ -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 \ No newline at end of file diff --git a/api/Deployment/mkDeployment b/api/Deployment/mkDeployment new file mode 100644 index 00000000..f0e6a6ed --- /dev/null +++ b/api/Deployment/mkDeployment @@ -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 \ No newline at end of file diff --git a/api/Service/RawDefaults b/api/Service/RawDefaults new file mode 100644 index 00000000..1a7c0bdd --- /dev/null +++ b/api/Service/RawDefaults @@ -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 +} \ No newline at end of file diff --git a/api/Service/RawTypes b/api/Service/RawTypes new file mode 100644 index 00000000..373c622f --- /dev/null +++ b/api/Service/RawTypes @@ -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 +} \ No newline at end of file diff --git a/api/Service/Service b/api/Service/Service new file mode 100644 index 00000000..4eae3ca4 --- /dev/null +++ b/api/Service/Service @@ -0,0 +1,6 @@ +{ name : Text +, annotations : List { mapKey : Text, mapValue : Text } +, containerPort : Natural +, outPort : Natural +, type : ./ServiceType +} \ No newline at end of file diff --git a/api/Service/ServiceType b/api/Service/ServiceType new file mode 100644 index 00000000..16892781 --- /dev/null +++ b/api/Service/ServiceType @@ -0,0 +1,5 @@ +< ClusterIP : {} +| NodePort : {} +| LoadBalancer : {} +| ExternalName : {} +> \ No newline at end of file diff --git a/api/Service/default b/api/Service/default new file mode 100644 index 00000000..4f679031 --- /dev/null +++ b/api/Service/default @@ -0,0 +1,6 @@ +{ name = "CHANGEME" +, annotations = [] : List { mapKey : Text, mapValue : Text } +, containerPort = 8080 +, outPort = 80 +, type = (constructors ./ServiceType).NodePort {=} +} \ No newline at end of file diff --git a/api/Service/mkService b/api/Service/mkService new file mode 100644 index 00000000..329a8817 --- /dev/null +++ b/api/Service/mkService @@ -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 diff --git a/docs/README.md.dhall b/docs/README.md.dhall index fece2c7e..ecc2839d 100644 --- a/docs/README.md.dhall +++ b/docs/README.md.dhall @@ -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 '' diff --git a/examples/Service.dhall b/examples/Config.dhall similarity index 100% rename from examples/Service.dhall rename to examples/Config.dhall diff --git a/examples/deployment.dhall b/examples/deployment.dhall index cfde64e3..c54ea4fc 100644 --- a/examples/deployment.dhall +++ b/examples/deployment.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 diff --git a/examples/deploymentRaw.dhall b/examples/deploymentRaw.dhall new file mode 100644 index 00000000..30b14520 --- /dev/null +++ b/examples/deploymentRaw.dhall @@ -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 diff --git a/examples/ingress.dhall b/examples/ingress.dhall deleted file mode 100644 index 7a27bc2e..00000000 --- a/examples/ingress.dhall +++ /dev/null @@ -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 diff --git a/examples/ingressRaw.dhall b/examples/ingressRaw.dhall new file mode 100644 index 00000000..1d112de4 --- /dev/null +++ b/examples/ingressRaw.dhall @@ -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 ] } diff --git a/examples/service-foo.dhall b/examples/myConfig.dhall similarity index 100% rename from examples/service-foo.dhall rename to examples/myConfig.dhall diff --git a/examples/out/deployment.yaml b/examples/out/deployment.yaml index 061cda78..cef713ed 100644 --- a/examples/out/deployment.yaml +++ b/examples/out/deployment.yaml @@ -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 diff --git a/examples/out/deploymentRaw.yaml b/examples/out/deploymentRaw.yaml new file mode 100644 index 00000000..061cda78 --- /dev/null +++ b/examples/out/deploymentRaw.yaml @@ -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 diff --git a/examples/out/ingress.yaml b/examples/out/ingressRaw.yaml similarity index 100% rename from examples/out/ingress.yaml rename to examples/out/ingressRaw.yaml diff --git a/examples/out/service.yaml b/examples/out/service.yaml new file mode 100644 index 00000000..e76c3f55 --- /dev/null +++ b/examples/out/service.yaml @@ -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 diff --git a/examples/service.dhall b/examples/service.dhall new file mode 100644 index 00000000..baed473a --- /dev/null +++ b/examples/service.dhall @@ -0,0 +1,8 @@ +let config = + ../api/Service/default + // + { name = "nginx" + , containerPort = 80 + } + +in ../api/Service/mkService config diff --git a/scripts/build-examples.py b/scripts/build-examples.py index e3c5ce31..3ba7711d 100755 --- a/scripts/build-examples.py +++ b/scripts/build-examples.py @@ -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'