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