Skyler Grey | 2ca6ccd | 2023-10-14 22:56:43 +0000 | [diff] [blame^] | 1 | { config, lib, pkgs, helpers, base, ... }: |
| 2 | lib.recursiveUpdate |
| 3 | { |
| 4 | options.clicks = { |
| 5 | nginx = { |
| 6 | services = lib.mkOption { |
| 7 | type = with lib.types; |
| 8 | listOf (submodule { |
| 9 | options = { |
| 10 | host = lib.mkOption { type = str; }; |
| 11 | extraHosts = lib.mkOption { type = listOf str; }; |
| 12 | secure = lib.mkOption { type = bool; }; |
| 13 | service = lib.mkOption { |
| 14 | type = let |
| 15 | validServiceTypes = { |
| 16 | "redirect" = { |
| 17 | to = ["string" str]; |
| 18 | permanent = ["bool" bool]; |
| 19 | }; |
| 20 | "reverseproxy" = { |
| 21 | to = ["string" str]; |
| 22 | }; |
| 23 | "php" = { |
| 24 | root = ["string" str]; |
| 25 | socket = ["string" str]; |
| 26 | }; |
| 27 | "directory" = { |
| 28 | private = ["bool" bool]; |
| 29 | root = ["string" str]; |
| 30 | }; |
| 31 | "file" = { |
| 32 | path = ["string" str]; |
| 33 | }; |
| 34 | "path" = { |
| 35 | path = ["string" str]; |
| 36 | service = ["set" serviceType]; |
| 37 | }; |
| 38 | "compose" = { |
| 39 | services = ["list" (listOf serviceType)]; |
| 40 | }; |
| 41 | "status" = { |
| 42 | statusCode = ["int" int]; |
| 43 | }; |
| 44 | }; |
| 45 | |
| 46 | serviceType = mkOptionType { |
| 47 | name = "Service"; |
| 48 | |
| 49 | description = "clicks Nginx service"; |
| 50 | descriptionClass = "noun"; |
| 51 | |
| 52 | check = (x: |
| 53 | if (builtins.typeOf x) != "set" |
| 54 | then lib.warn "clicks nginx services must be sets but ${x} is not a set" false |
| 55 | else if !(builtins.hasAttr "type" x) |
| 56 | then lib.warn "clicks nginx services must have a type attribute but ${x} does not" false |
| 57 | else if !(builtins.hasAttr x.type validServiceTypes) |
| 58 | then lib.warn "clicks nginx services must have a valid type, but ${x.type} is not one" false |
| 59 | else (let |
| 60 | optionTypes = |
| 61 | (builtins.mapAttrs (n: o: builtins.elemAt o 0) validServiceTypes.${x.type}) |
| 62 | // { type = "string"; }; |
| 63 | in (lib.pipe x [ |
| 64 | (builtins.mapAttrs (n: o: (builtins.hasAttr n optionTypes) && optionTypes.${n} == (builtins.typeOf o))) |
| 65 | lib.attrValues |
| 66 | (builtins.all (x: x)) |
| 67 | ]) && (lib.pipe optionTypes [ |
| 68 | (builtins.mapAttrs (n: _: builtins.hasAttr n x)) |
| 69 | lib.attrValues |
| 70 | (builtins.all (x: x)) |
| 71 | ] ))); |
| 72 | }; |
| 73 | in serviceType; |
| 74 | }; |
| 75 | type = lib.mkOption { type = strMatching "hosts"; }; |
| 76 | }; |
| 77 | }); |
| 78 | example = lib.literalExpression '' |
| 79 | with helpers.nginx; [ |
| 80 | (Host "example.clicks.codes" (ReverseProxy "generic:1001")) |
| 81 | ]''; |
| 82 | description = lib.mdDoc '' |
| 83 | Connects hostnames to services for your nginx server. We recommend using the Clicks helper to generate these |
| 84 | ''; |
| 85 | default = [ ]; |
| 86 | }; |
| 87 | serviceAliases = lib.mkOption { |
| 88 | type = with lib.types; |
| 89 | listOf (submodule { |
| 90 | options = { |
| 91 | host = lib.mkOption { |
| 92 | type = str; |
| 93 | example = "example.clicks.codes"; |
| 94 | description = '' |
| 95 | The ServerName of the server. If you override this in the nginx server block, you still need to put in the name of the attribute |
| 96 | ''; |
| 97 | }; |
| 98 | aliases = lib.mkOption { |
| 99 | type = listOf str; |
| 100 | example = [ "example2.clicks.codes" "example.coded.codes" ]; |
| 101 | description = '' |
| 102 | A list of servers to add as aliases |
| 103 | ''; |
| 104 | }; |
| 105 | type = lib.mkOption { type = strMatching "aliases"; }; |
| 106 | }; |
| 107 | }); |
| 108 | example = lib.literalExpression '' |
| 109 | with helpers.nginx; [ |
| 110 | (Host "example.clicks.codes" (ReverseProxy "generic:1001")) |
| 111 | ]''; |
| 112 | description = lib.mdDoc '' |
| 113 | Adds additional host names to your nginx server. If you're using `clicks.nginx.services` |
| 114 | you should generally use a Hosts block instead |
| 115 | ''; |
| 116 | default = [ ]; |
| 117 | }; |
| 118 | streams = lib.mkOption { |
| 119 | type = with lib.types; listOf (submodule { |
| 120 | options = { |
| 121 | internal = lib.mkOption { type = str; }; |
| 122 | external = lib.mkOption { type = port; }; |
| 123 | protocol = lib.mkOption { type = strMatching "^(tcp|udp)$"; }; |
| 124 | }; |
| 125 | }); |
| 126 | example = lib.literalExpression '' |
| 127 | with helpers.nginx; [ |
| 128 | (Stream 1001 "generic:1002" "tcp") |
| 129 | ]''; |
| 130 | description = lib.mdDoc '' |
| 131 | A list of servers to be placed in the nginx streams block. We recommend using the Clicks helper to generate these |
| 132 | ''; |
| 133 | default = [ ]; |
| 134 | }; |
| 135 | }; |
| 136 | }; |
| 137 | config = { |
| 138 | services.nginx = { |
| 139 | enable = true; |
| 140 | enableReload = true; |
| 141 | |
| 142 | virtualHosts = lib.recursiveUpdate (helpers.nginx.Merge |
| 143 | config.clicks.nginx.services) # clicks.nginx.services |
| 144 | (lib.pipe config.clicks.nginx.serviceAliases [ |
| 145 | (map (alias: { |
| 146 | name = alias.host; |
| 147 | value.serverAliases = alias.aliases; |
| 148 | })) |
| 149 | builtins.listToAttrs |
| 150 | ]); # clicks.nginx.serviceAliases |
| 151 | |
| 152 | streamConfig = builtins.concatStringsSep "\n" (map (stream: '' |
| 153 | server { |
| 154 | listen ${builtins.toString stream.external}${lib.optionalString (stream.protocol == "udp") " udp"}; |
| 155 | proxy_pass ${builtins.toString stream.internal}; |
| 156 | } |
| 157 | '') config.clicks.nginx.streams); |
| 158 | }; |
| 159 | |
| 160 | networking.firewall.allowedTCPPorts = lib.pipe config.clicks.nginx.streams [ |
| 161 | (builtins.filter (stream: stream.protocol == "tcp")) |
| 162 | (map (stream: stream.external)) |
| 163 | ]; |
| 164 | networking.firewall.allowedUDPPorts = lib.pipe config.clicks.nginx.streams [ |
| 165 | (builtins.filter (stream: stream.protocol == "udp")) |
| 166 | (map (stream: stream.external)) |
| 167 | ]; |
| 168 | |
| 169 | security.acme.defaults = { |
| 170 | email = "admin@clicks.codes"; |
| 171 | credentialsFile = config.sops.secrets.cloudflare_cert__api_token.path; |
| 172 | }; |
| 173 | security.acme.acceptTerms = true; |
| 174 | |
| 175 | sops.secrets.cloudflare_cert__api_token = { |
| 176 | mode = "0660"; |
| 177 | owner = config.users.users.nginx.name; |
| 178 | group = config.users.users.acme.group; |
| 179 | sopsFile = ../secrets/cloudflare-cert.env.bin; |
| 180 | format = "binary"; |
| 181 | }; |
| 182 | }; |
| 183 | } ( |
| 184 | if base != null |
| 185 | then { |
| 186 | config.security.acme.certs = builtins.mapAttrs (_: v: { webroot = null; dnsProvider = "cloudflare"; }) base.config.security.acme.certs; |
| 187 | } else {}) |