1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 cfg = config.services.keter;
9 yaml = pkgs.formats.yaml { };
10in
11{
12 meta = {
13 maintainers = with lib.maintainers; [ jappie ];
14 };
15
16 imports = [
17 (lib.mkRenamedOptionModule [ "services" "keter" "keterRoot" ] [ "services" "keter" "root" ])
18 (lib.mkRenamedOptionModule [ "services" "keter" "keterPackage" ] [ "services" "keter" "package" ])
19 ];
20
21 options.services.keter = {
22 enable = lib.mkEnableOption ''
23 keter, a web app deployment manager.
24 Note that this module only support loading of webapps:
25 Keep an old app running and swap the ports when the new one is booted
26 '';
27
28 root = lib.mkOption {
29 type = lib.types.str;
30 default = "/var/lib/keter";
31 description = "Mutable state folder for keter";
32 };
33
34 package = lib.mkPackageOption pkgs [ "haskellPackages" "keter" ] { };
35
36 globalKeterConfig = lib.mkOption {
37 type = lib.types.submodule {
38 freeformType = yaml.type;
39 options = {
40 ip-from-header = lib.mkOption {
41 default = true;
42 type = lib.types.bool;
43 description = "You want that ip-from-header in the nginx setup case. It allows nginx setting the original ip address rather then it being localhost (due to reverse proxying)";
44 };
45 listeners = lib.mkOption {
46 default = [
47 {
48 host = "*";
49 port = 6981;
50 }
51 ];
52 type = lib.types.listOf (
53 lib.types.submodule {
54 options = {
55 host = lib.mkOption {
56 type = lib.types.str;
57 description = "host";
58 };
59 port = lib.mkOption {
60 type = lib.types.port;
61 description = "port";
62 };
63 };
64 }
65 );
66 description = ''
67 You want that ip-from-header in
68 the nginx setup case.
69 It allows nginx setting the original ip address rather
70 then it being localhost (due to reverse proxying).
71 However if you configure keter to accept connections
72 directly you may want to set this to false.'';
73 };
74 rotate-logs = lib.mkOption {
75 default = false;
76 type = lib.types.bool;
77 description = ''
78 emits keter logs and it's applications to stderr.
79 which allows journald to capture them.
80 Set to true to let keter put the logs in files
81 (useful on non systemd systems, this is the old approach
82 where keter handled log management)'';
83 };
84 };
85 };
86 description = "Global config for keter, see <https://github.com/snoyberg/keter/blob/master/etc/keter-config.yaml> for reference";
87 };
88
89 bundle = {
90 appName = lib.mkOption {
91 type = lib.types.str;
92 default = "myapp";
93 description = "The name keter assigns to this bundle";
94 };
95
96 executable = lib.mkOption {
97 type = lib.types.path;
98 description = "The executable to be run";
99 };
100
101 domain = lib.mkOption {
102 type = lib.types.str;
103 default = "example.com";
104 description = "The domain keter will bind to";
105 };
106
107 publicScript = lib.mkOption {
108 type = lib.types.str;
109 default = "";
110 description = ''
111 Allows loading of public environment variables,
112 these are emitted to the log so it shouldn't contain secrets.
113 '';
114 example = "ADMIN_EMAIL=hi@example.com";
115 };
116
117 secretScript = lib.mkOption {
118 type = lib.types.str;
119 default = "";
120 description = "Allows loading of private environment variables";
121 example = "MY_AWS_KEY=$(cat /run/keys/AWS_ACCESS_KEY_ID)";
122 };
123 };
124
125 };
126
127 config = lib.mkIf cfg.enable (
128 let
129 incoming = "${cfg.root}/incoming";
130
131 globalKeterConfigFile = pkgs.writeTextFile {
132 name = "keter-config.yml";
133 text = (lib.generators.toYAML { } (cfg.globalKeterConfig // { root = cfg.root; }));
134 };
135
136 # If things are expected to change often, put it in the bundle!
137 bundle = pkgs.callPackage ./bundle.nix (
138 cfg.bundle
139 // {
140 keterExecutable = executable;
141 keterDomain = cfg.bundle.domain;
142 }
143 );
144
145 # This indirection is required to ensure the nix path
146 # gets copied over to the target machine in remote deployments.
147 # Furthermore, it's important that we use exec to
148 # run the binary otherwise we get process leakage due to this
149 # being executed on every change.
150 executable = pkgs.writeShellScript "bundle-wrapper" ''
151 set -e
152 ${cfg.bundle.secretScript}
153 set -xe
154 ${cfg.bundle.publicScript}
155 exec ${cfg.bundle.executable}
156 '';
157
158 in
159 {
160 systemd.services.keter = {
161 description = "keter app loader";
162 script = ''
163 set -xe
164 mkdir -p ${incoming}
165 ${lib.getExe cfg.package} ${globalKeterConfigFile};
166 '';
167 wantedBy = [
168 "multi-user.target"
169 "nginx.service"
170 ];
171
172 serviceConfig = {
173 Restart = "always";
174 RestartSec = "10s";
175 };
176
177 after = [
178 "network.target"
179 "local-fs.target"
180 "postgresql.target"
181 ];
182 };
183
184 # On deploy this will load our app, by moving it into the incoming dir
185 # If the bundle content changes, this will run again.
186 # Because the bundle content contains the nix path to the executable,
187 # we inherit nix based cache busting.
188 systemd.services.load-keter-bundle = {
189 description = "load keter bundle into incoming folder";
190 after = [ "keter.service" ];
191 wantedBy = [ "multi-user.target" ];
192 # we can't override keter bundles because it'll stop the previous app
193 # https://github.com/snoyberg/keter#deploying
194 script = ''
195 set -xe
196 cp ${bundle}/bundle.tar.gz.keter ${incoming}/${cfg.bundle.appName}.keter
197 '';
198 path = [
199 executable
200 cfg.bundle.executable
201 ]; # this is a hack to get the executable copied over to the machine.
202 };
203 }
204 );
205}