{ pkgs, lib, config, ... }: let cfg = config.services.octodns; secrets = config.age.secrets; types = { ttlOptions = default: { ttl = lib.mkOption { type = lib.types.int; default = default; }; }; defaults = lib.types.submodule { options = { A = lib.mkOption { type = lib.types.submodule { options = types.ttlOptions cfg.defaults.ttl; }; default = { }; }; AAAA = lib.mkOption { type = lib.types.submodule { options = types.ttlOptions cfg.defaults.ttl; }; default = { }; }; CNAME = lib.mkOption { type = lib.types.submodule { options = types.ttlOptions cfg.defaults.ttl; }; default = { }; }; MX = lib.mkOption { type = lib.types.submodule { options = types.ttlOptions cfg.defaults.ttl; }; default = { }; }; } // (types.ttlOptions 3600); }; aRecord = lib.types.submodule { options = { values = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; }; ttl = lib.mkOption { type = lib.types.int; default = cfg.defaults.A.ttl; }; }; }; aaaaRecord = lib.types.submodule { options = { values = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; }; ttl = lib.mkOption { type = lib.types.int; default = cfg.defaults.AAAA.ttl; }; }; }; cnameRecord = lib.types.submodule { options = { target = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; }; toRoot = lib.mkOption { type = lib.types.bool; default = false; }; ttl = lib.mkOption { type = lib.types.int; default = cfg.defaults.CNAME.ttl; }; }; }; mxValue = lib.types.submodule { options = { exchange = lib.mkOption { type = lib.types.str; }; preference = lib.mkOption { type = lib.types.int; default = 10; }; }; }; mxRecord = lib.types.submodule { options = { values = lib.mkOption { type = lib.types.listOf types.mxValue; default = [ ]; }; ttl = lib.mkOption { type = lib.types.int; default = cfg.defaults.MX.ttl; }; }; }; records = lib.types.submodule { options = { A = lib.mkOption { type = types.aRecord; default = { }; }; AAAA = lib.mkOption { type = types.aaaaRecord; default = { }; }; CNAME = lib.mkOption { type = types.cnameRecord; default = { }; }; MX = lib.mkOption { type = types.mxRecord; default = { }; }; }; }; }; yamlFormat = pkgs.formats.yaml { }; zoneFile = yamlFormat.generate "octodns-zone" ( lib.filterAttrs (_: records: (lib.length records) > 0) ( lib.mapAttrs ( _: types: lib.filter ( { values ? [ ], value ? null, ... }: (lib.length values) > 0 || !(builtins.isNull value) ) ( lib.mapAttrsToList ( type: { ttl, ... }@options: if (type == "CNAME") then let inherit (options) target toRoot; value = if toRoot then "${config.networking.domain}." else target; in { inherit type ttl value; } else { inherit type ttl; inherit (options) values; } ) types ) ) cfg.records ) ); zonesDir = pkgs.linkFarm "octodns-zones" { "${config.networking.domain}.yaml" = zoneFile; }; configFile = yamlFormat.generate "octodns-config.yaml" { providers = { config = { class = "octodns.provider.yaml.YamlProvider"; directory = zonesDir; default_ttl = cfg.defaults.ttl; }; hetzner = { class = "octodns_hetzner.HetznerProvider"; token = "env/HETZNER_API_KEY"; }; }; zones."*" = { sources = [ "config" ]; targets = [ "hetzner" ]; }; }; octodns = pkgs.octodns.withProviders (_: [ pkgs.octodns-providers.hetzner ]); in { options.services.octodns = { enable = lib.mkEnableOption "Enable octodns"; records = lib.mkOption { type = lib.types.attrsOf types.records; default = { }; }; defaults = lib.mkOption { type = types.defaults; default = { }; }; }; config = lib.mkIf cfg.enable { systemd.services.octodns = { enable = true; description = "OctoDNS"; serviceConfig = { Type = "oneshot"; ExecStart = "${octodns}/bin/octodns-sync --config-file ${configFile} --doit"; DynamicUser = true; EnvironmentFile = secrets.hetzner.path; RemainAfterExit = true; }; wantedBy = lib.optionals config.services.nginx.enable [ "nginx.service" ]; before = lib.optionals config.services.nginx.enable [ "nginx.service" ]; }; }; }