blob: 64ad1ce882708b153baf2129ec62309b9d239afb [file] [log] [blame]
Skyler Grey2ca6ccd2023-10-14 22:56:43 +00001{ config, lib, pkgs, helpers, base, ... }:
2lib.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 {})