Typecheck, template and modularize your Kubernetes definitions with Dhall
Go to file
Thomas Scholtes 4a7f02cd0d Extract and check examples (#27)
* We move all the example code from the readme to the `examples` folder.
* We provide a `scripts/build-readme.sh` script that inlines referenced
  examples in `README.md.in` and outputs `README.md`. The script also
  verifies that the output readme is the same as in version control.
* We provide a `scripts/build-examples.py` script that builds the Yaml
  output for all examples. The script also verifies that the generated
  Yaml files are the same as in version control.
2018-08-03 16:24:32 +02:00
default Improve convert code and fix alias issues (#25) 2018-07-14 11:10:37 +03:00
docs Extract and check examples (#27) 2018-08-03 16:24:32 +02:00
examples Extract and check examples (#27) 2018-08-03 16:24:32 +02:00
scripts Extract and check examples (#27) 2018-08-03 16:24:32 +02:00
types Improve convert code and fix alias issues (#25) 2018-07-14 11:10:37 +03:00
.gitignore Extract and check examples (#27) 2018-08-03 16:24:32 +02:00
convert.py Improve convert code and fix alias issues (#25) 2018-07-14 11:10:37 +03:00
LICENSE Create LICENSE 2018-05-27 19:55:07 +02:00
nixpkgs.nix Add 'shell.nix' for local development 2018-07-16 19:57:10 +02:00
README.md Extract and check examples (#27) 2018-08-03 16:24:32 +02:00
release.nix Extract and check examples (#27) 2018-08-03 16:24:32 +02:00
shell.nix Extract and check examples (#27) 2018-08-03 16:24:32 +02:00

dhall-kubernetes

Dhall bindings to Kubernetes. This will let you typecheck, template and modularize your Kubernetes definitions with Dhall.

Prerequisites

NOTE: dhall-kubernetes requires at least version 1.14.0 of the interpreter.

You can install the latest version with the following:

stack install dhall dhall-json --resolver=nightly

For a version compatible with a previous version, check out this commit.

Quick start

In the types folder you'll find the types for the Kubernetes definitions. E.g. here's the type for a Deployment.

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.

Since this might sound a bit abstract, let's go with some examples. You can find these examples in the ./examples folder and evaluate them there.

Example: Deployment

Let's say we have several services, whose configuration has this type:

-- examples/Service.dhall
{ name    : Text
, host    : Text
, version : Text
}

So a configuration for a service might look like this:

-- examples/service-foo.dhall
{ name    = "foo"
, host    = "foo.example.com"
, version = "1.0.1"
}

We can then make a Deployment object for this service:

-- 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})]
        }
    ]})
  }
} //
{ 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

We convert it to yaml with:

dhall-to-yaml --omitNull < deployment.dhall

And we get:

-- examples/out/deployment.yaml
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

Example: Ingress

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:

-- examples/ingress.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

-- 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

-- 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 = "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

As usual we get the yaml out by running:

dhall-to-yaml --omitNull < ingress.yaml.dhall

And we get:

-- examples/out/ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
spec:
  rules:
  - http:
      paths:
      - backend:
          servicePort: '80'
          serviceName: foo
    host: foo.example.com
  - http:
      paths:
      - backend:
          servicePort: '80'
          serviceName: default
    host: default.example.com
  tls:
  - hosts:
    - foo.example.com
    secretName: foo-certificate
  - hosts:
    - default.example.com
    secretName: default-certificate
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    kubernetes.io/ingress.allow-http: 'false'
  name: nginx

Development

Tests

All tests are defined in release.nix. We run these tests in CI in a Hydra project. You can run the tests locally with nix build --no-link release.nix.

Changing the README

We build README.md from docs/README.md.dhall and check it into source control. The build script ./scripts/build-readme.sh inlines source code from the examples directory. If you make changes to the readme or the examples you need to run scripts/build-readme.sh.