Loading
FretLink / @clementd

Config As Code?
Yup, but properly.
Have some Dhall

$ whoami

dev haskell, former CTO, I've worked with several langages, I used scala a lot (not anymore). Nowadays, haskell, rust, JS (a bit)
mise en relation chargeurs et transporteurs. suivi plus transparent, aider les petits transporteurs environnement technique historique: node mongo. nouveaux services : haskell / pg

Config is solved, we have yaml

if you agree with this, you won’t like the rest

Config is solved, we have toml*

toml solves issues with yaml,
but these are not the ones I’m interested in

Configuration is complicated and repetitive

especially with infra as code and software-defined everything

Config languages are simple*

Configuration languages are designed to be simple. no abstraction, no indirection (* yaml is an error)

The case for config as code

Webpack

webpack config file: module export

Webpack




const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  }
};

Webpack - pros

plain objects, you can use variables, ternaries, maybe functions

“it’s just JavaScript”

not a new language to learn (who knows the nginx config file format?)

simple abstraction

you can split your config in several parts,
use properly named variables
maybe use functions

imports

you can split your config file is several files
and use import to join them

Webpack - cons

“it’s just JavaScript”

I don’t want my config files to turn into angular 1.2
new HtmlWebpackPlugin <- what does it do?

npm install

now the config file has deps, so you need to npm install before
looking at what’s in it

side effects

when is webpack run?

side effects

does it modify other config files?

side effects

can I cache the webpack config?

side effects

does it depend on software that’s not in package.json?

😱 side effects 😱

does reading the config drop my database?

no caching

no automated security audit

just a black box

that may or may not have visible effects

Pretty printing a JSON does not trash your hard drive

config files should just describe standalone values (eg more or less the JSON AST)

Config is not code

config has a different lifecycle from code, and different requirements
boundary that’s important to respect
examples: xmonad, things that you have to recompile to configure

Desirable properties

not talking about syntax bikeshedding

Clear syntax

won’t bikeshed here, but the syntax has to be unambiguous
avoid yaml’s mistakes (whitespace sensitivity, ambiguous literals)

Clear semantics

it should reduce to more or less an AST equivalent to JSON
records, lists, scalars (bools, strings, numbers)

Imports

we should be able to split config files as we please
in a structured, and well defined way
(eg, we compose values, not concat files)

Some abstraction

at least variables, restricted functions are ok
(eg to build records)

No turing completeness

a turing-complete interpreter can be turned into anything
huge security risks (or just DoS)

No side* effects

Evaluating a config file should do nothing except evaluating it

Some side effects are ok, though

env vars are so useful, it’d be a shame to forego them

Dhall

made just for this (and some more!)

Clear syntax




{- Comments -}
{ firstKey = "string value"
, otherKey = 42
, nested = [ { otherValue = True } ]
, multiline = ''
              I can do
              as well''
}

not whitespace significant records are defined with { / = / , records are defined with [ / , multiline strings, naturals, booleans

Clear semantics




{- Comments -}
{ firstKey = "string value"
, otherKey = 42
, nested = [ { otherValue = True } ]
, multiline = ''
              I can do
              as well''
}

the semantics is: reduce the file to this AST. Nothing more

Imports

let otherFile = ./other.dhall
in otherFile


we should be able to split config files as we please
in a structured, and well defined way
(eg, we compose values, not concat files)

Some abstraction





{- Don't repeat yourself -}
let user = "bill"
in  { home       = "/home/${user}"
    , privateKey = "/home/${user}/id_ed25519"
    , publicKey  = "/home/${user}/id_ed25519.pub"
    }

variables and functions (what else?)

Some abstraction





let
  makeUser = \(user : Text) ->
    { home       = "/home/${user}"
    , privateKey = "/home/${user}/id_ed25519"
    , publicKey  = "/home/${user}/id_ed25519.pub"
    }
in makeUser "bill"

simple functions

😍 Types 😍

for complex configs (especially when there is no config checker available)
faster feedback loop
great tool to reason about your config files
(useful to fight complexity)

Scalars

let port    : Natural = 8080
let offset  : Integer = +12
let ratio   : Double  = 12.0
let string  : Text    = "just a string"
let boolean : Bool    = True
in …
all literals are unambiguous (take that, yaml)

Composite



let maybe : Optional Natural =
      Some 8080
let list : List Text =
      ["a", "string"]
let record : { key : Text } =
      { key = "value" }
in  …
list are homogeneous
records types are structural

Records

let User = { name : Text }
let Admin = User //\\ { level : Text }
let localAdmin: Admin =
      { name = "John Doe"
      , level = "local"
      }
in localAdmin
record types can be aliased and combined to avoid repetition

Union types

real config files are messy

let Host =
      { address : Text
      , port : Natural
      }
let Socket =
      < LocalFile : Text
      | RemoteHost : Host
      >
in  Socket.RemoteHost
      { address = "10.0.0.1"
      , port = 8080
      }

you can get back heterogeneous lists with union types

No turing completeness

general recursion is not allowed in the language
recursive structures are not directly possible

State of the art type system

it’s not external checks, it’s a type system proven to be sound
all the interesting properties of dhall are thanks to the choice
of a good type system.

No side* effects

the semantics are: reduce this expression as much as possible. no side effects are possible to express, this way

Some side effects are ok




-- read a string (without the quotes)
{ currentHome = env:HOME as Text

-- read any dhall expression
, currentPort = env:PORT }

Reading env vars is fine

Some side effects are ok





let map = https://prelude.dhall-lang.org/List/map

in map Natural Bool Natural/even [ 2, 3, 5 ]

http imports. super easy to reuse code
stdlib distributed this way

Some side effects are ok






let map =
  https://prelude.dhall-lang.org/List/map
    sha256:c60cc328c8aab8253e7372ae973ab7fd7b37448dc5bd9602f8e17b52a950d57b

in map Natural Bool Natural/even [ 2, 3, 5 ]

you can freeze imports to avoid changes and use cache socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 0 open(“/home/clementd/.cache/dhall/c60cc328c8aab8253e7372ae973ab7fd7b37448dc5bd9602f8e17b52a950d57b”, O_RDONLY|O_NOCTTY|O_NONBLOCK) = 0

Clear semantics

cat evenList.dhall \
  | dhall resolve \
  | dhall normalize

first we resolve all imports (http + env)
then we evaluate (β-reduce)

Clear semantics






let map =
  https://prelude.dhall-lang.org/List/map
    sha256:c60cc328c8aab8253e7372ae973ab7fd7b37448dc5bd9602f8e17b52a950d57b

in map Natural Bool Natural/even [ 2, 3, 5 ]

to go from this

Clear semantics

[ True, False, False ]



to this
for instance: commit the thing above, but the ops team can read
the generated config, or automated tools / linters can act on
the generated output.

Editor support

syntax highlighting in many editors, some of them have extra
support

dhall-lsp work in progress, so that should benefit everyone