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