Would you like to use Nix instead of consul-template templating language?

Then go ahead!

TIL that Nix can natively fetch Consul HTTP API with builtins.fetchurl. This combined with Nix templating/abstract abilities is awesome!

So I had to live with this consul-template template file:

        {{- range services -}}
          {{- $service:=.Name -}}
          {{- range .Tags -}}
            {{- if eq . "cluster" -}}
        upstream {{$service}} {
          zone upstream-{{$service}} 64k;
              {{ range service $service "passing" }}
          server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1;
          {{- else -}}server 127.0.0.1:65535; # force a 502
              {{- end}}
        }
            {{- end -}}
          {{- end -}}
        {{- end -}}

        {{range services}}
        {{$service:=.Name}} {{range .Tags}} {{if eq . "cluster" }}
        server {
          server_name {{$service}}.services.example.com;
          location / {
            proxy_pass http://{{$service}};
            proxy_set_header  X-Real-IP  $remote_addr;
            proxy_redirect off;
          }
        } {{end}} {{end}} {{end}}

It looks so crappy because this templating language is so crappy. You have to fight with whitespace because otherwise your generated config will be unreadable.

But what it looks like in language you know and love?

let
  services = builtins.fromJSON (builtins.readFile (builtins.fetchurl "http://localhost:8500/v1/agent/services"));
  lib = import <nixpkgs/lib>;
  inherit (lib) flip;
  # From https://github.com/NixOS/nixpkgs/pull/57091/files
  combined = x:
    if builtins.isFunction x
      then arg1: arg2: builtins.concatStringsSep "" (x arg2 arg1)
    else if builtins.isList x
      then builtins.concatStringsSep "" x
    else if builtins.isString x
      # then this is a separator
      then y:
        if builtins.isFunction y
          then arg1: arg2: builtins.concatStringsSep x (y arg2 arg1)
          # otherwise this must be a list
          else builtins.concatStringsSep x y
    else throw "'combined' called with unexpected arguments";


  interestingServices = lib.filter (service: lib.elem "cluster" service.Tags) (builtins.attrValues services);
  zipped = lib.zipAttrs (map (service: { ${service.Service} = service; }) interestingServices);

in
  combined "\n" lib.mapAttrsToList zipped (name: services: ''
       upstream ${name} {
         zone upstream-${name} 64k;
         ${ combined "\n" map services (service:
           "server ${service.Address}:${toString service.Port} max_fails=3 fail_timeout=60 weight=1;"
         )}
       }

       server {
         server_name ${name}.services.example.com;
         location / {
           proxy_pass http://${name};
           proxy_set_header  X-Real-IP  $remote_addr;
           proxy_redirect off;
         }
       }

     '')

You can render template with nix eval -f ./template.nix '' --raw and subscribe to incoming events with consul watch -type=services.

So clean and functional!

8 Likes

This is awesome, thanks for sharing this!