about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/web-servers/keter/default.nix
blob: 3916c486475ded2805be52f179fc8d4a34dc613c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
{ config, pkgs, lib, ... }:
let
  cfg = config.services.keter;
  yaml = pkgs.formats.yaml { };
in
{
  meta = {
    maintainers = with lib.maintainers; [ jappie ];
  };

  imports = [
    (lib.mkRenamedOptionModule [ "services" "keter" "keterRoot" ] [ "services" "keter" "root" ])
    (lib.mkRenamedOptionModule [ "services" "keter" "keterPackage" ] [ "services" "keter" "package" ])
  ];

  options.services.keter = {
    enable = lib.mkEnableOption (lib.mdDoc ''keter, a web app deployment manager.
Note that this module only support loading of webapps:
Keep an old app running and swap the ports when the new one is booted.
'');

    root = lib.mkOption {
      type = lib.types.str;
      default = "/var/lib/keter";
      description = lib.mdDoc "Mutable state folder for keter";
    };

    package = lib.mkOption {
      type = lib.types.package;
      default = pkgs.haskellPackages.keter;
      defaultText = lib.literalExpression "pkgs.haskellPackages.keter";
      description = lib.mdDoc "The keter package to be used";
    };


    globalKeterConfig = lib.mkOption {
      type = lib.types.submodule {
        freeformType = yaml.type;
        options = {
          ip-from-header = lib.mkOption {
            default = true;
            type = lib.types.bool;
            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)";
          };
          listeners = lib.mkOption {
            default = [{ host = "*"; port = 6981; }];
            type = lib.types.listOf (lib.types.submodule {
              options = {
                host = lib.mkOption {
                  type = lib.types.str;
                  description = lib.mdDoc "host";
                };
                port = lib.mkOption {
                  type = lib.types.port;
                  description = lib.mdDoc "port";
                };
              };
            });
            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).
              However if you configure keter to accept connections
              directly you may want to set this to false.'';
          };
          rotate-logs = lib.mkOption {
            default = false;
            type = lib.types.bool;
            description = lib.mdDoc ''
              emits keter logs and it's applications to stderr.
              which allows journald to capture them.
              Set to true to let keter put the logs in files
              (useful on non systemd systems, this is the old approach
              where keter handled log management)'';
          };
        };
      };
      description = lib.mdDoc "Global config for keter, see <https://github.com/snoyberg/keter/blob/master/etc/keter-config.yaml> for reference";
    };

    bundle = {
      appName = lib.mkOption {
        type = lib.types.str;
        default = "myapp";
        description = lib.mdDoc "The name keter assigns to this bundle";
      };

      executable = lib.mkOption {
        type = lib.types.path;
        description = lib.mdDoc "The executable to be run";
      };

      domain = lib.mkOption {
        type = lib.types.str;
        default = "example.com";
        description = lib.mdDoc "The domain keter will bind to";
      };

      publicScript = lib.mkOption {
        type = lib.types.str;
        default = "";
        description = lib.mdDoc ''
          Allows loading of public environment variables,
          these are emitted to the log so it shouldn't contain secrets.
        '';
        example = "ADMIN_EMAIL=hi@example.com";
      };

      secretScript = lib.mkOption {
        type = lib.types.str;
        default = "";
        description = lib.mdDoc "Allows loading of private environment variables";
        example = "MY_AWS_KEY=$(cat /run/keys/AWS_ACCESS_KEY_ID)";
      };
    };

  };

  config = lib.mkIf cfg.enable (
    let
      incoming = "${cfg.root}/incoming";


      globalKeterConfigFile = pkgs.writeTextFile {
        name = "keter-config.yml";
        text = (lib.generators.toYAML { } (cfg.globalKeterConfig // { root = cfg.root; }));
      };

      # If things are expected to change often, put it in the bundle!
      bundle = pkgs.callPackage ./bundle.nix
        (cfg.bundle // { keterExecutable = executable; keterDomain = cfg.bundle.domain; });

      # This indirection is required to ensure the nix path
      # gets copied over to the target machine in remote deployments.
      # Furthermore, it's important that we use exec to
      # run the binary otherwise we get process leakage due to this
      # being executed on every change.
      executable = pkgs.writeShellScript "bundle-wrapper" ''
        set -e
        ${cfg.bundle.secretScript}
        set -xe
        ${cfg.bundle.publicScript}
        exec ${cfg.bundle.executable}
      '';

    in
    {
      systemd.services.keter = {
        description = "keter app loader";
        script = ''
          set -xe
          mkdir -p ${incoming}
          ${lib.getExe cfg.package} ${globalKeterConfigFile};
        '';
        wantedBy = [ "multi-user.target" "nginx.service" ];

        serviceConfig = {
          Restart = "always";
          RestartSec = "10s";
        };

        after = [
          "network.target"
          "local-fs.target"
          "postgresql.service"
        ];
      };

      # On deploy this will load our app, by moving it into the incoming dir
      # If the bundle content changes, this will run again.
      # Because the bundle content contains the nix path to the executable,
      # we inherit nix based cache busting.
      systemd.services.load-keter-bundle = {
        description = "load keter bundle into incoming folder";
        after = [ "keter.service" ];
        wantedBy = [ "multi-user.target" ];
        # we can't override keter bundles because it'll stop the previous app
        # https://github.com/snoyberg/keter#deploying
        script = ''
          set -xe
          cp ${bundle}/bundle.tar.gz.keter ${incoming}/${cfg.bundle.appName}.keter
        '';
        path = [
          executable
          cfg.bundle.executable
        ]; # this is a hack to get the executable copied over to the machine.
      };
    }
  );
}