1{ config, lib, pkgs, ... }:
2
3let
4 cfg = config.services.self-deploy;
5
6 workingDirectory = "/var/lib/nixos-self-deploy";
7 repositoryDirectory = "${workingDirectory}/repo";
8 outPath = "${workingDirectory}/system";
9
10 gitWithRepo = "git -C ${repositoryDirectory}";
11
12 renderNixArgs = args:
13 let
14 toArg = key: value:
15 if builtins.isString value
16 then " --argstr ${lib.escapeShellArg key} ${lib.escapeShellArg value}"
17 else " --arg ${lib.escapeShellArg key} ${lib.escapeShellArg (toString value)}";
18 in
19 lib.concatStrings (lib.mapAttrsToList toArg args);
20
21 isPathType = x: lib.strings.isCoercibleToString x && builtins.substring 0 1 (toString x) == "/";
22
23in
24{
25 options.services.self-deploy = {
26 enable = lib.mkEnableOption "self-deploy";
27
28 nixFile = lib.mkOption {
29 type = lib.types.path;
30
31 default = "/default.nix";
32
33 description = ''
34 Path to nix file in repository. Leading '/' refers to root of
35 git repository.
36 '';
37 };
38
39 nixAttribute = lib.mkOption {
40 type = lib.types.str;
41
42 description = ''
43 Attribute of `nixFile` that builds the current system.
44 '';
45 };
46
47 nixArgs = lib.mkOption {
48 type = lib.types.attrs;
49
50 default = { };
51
52 description = ''
53 Arguments to `nix-build` passed as `--argstr` or `--arg` depending on
54 the type.
55 '';
56 };
57
58 switchCommand = lib.mkOption {
59 type = lib.types.enum [ "boot" "switch" "dry-activate" "test" ];
60
61 default = "switch";
62
63 description = ''
64 The `switch-to-configuration` subcommand used.
65 '';
66 };
67
68 repository = lib.mkOption {
69 type = with lib.types; oneOf [ path str ];
70
71 description = ''
72 The repository to fetch from. Must be properly formatted for git.
73
74 If this value is set to a path (must begin with `/`) then it's
75 assumed that the repository is local and the resulting service
76 won't wait for the network to be up.
77
78 If the repository will be fetched over SSH, you must add an
79 entry to `programs.ssh.knownHosts` for the SSH host for the fetch
80 to be successful.
81 '';
82 };
83
84 sshKeyFile = lib.mkOption {
85 type = with lib.types; nullOr path;
86
87 default = null;
88
89 description = ''
90 Path to SSH private key used to fetch private repositories over
91 SSH.
92 '';
93 };
94
95 branch = lib.mkOption {
96 type = lib.types.str;
97
98 default = "master";
99
100 description = ''
101 Branch to track
102
103 Technically speaking any ref can be specified here, as this is
104 passed directly to a `git fetch`, but for the use-case of
105 continuous deployment you're likely to want to specify a branch.
106 '';
107 };
108
109 startAt = lib.mkOption {
110 type = with lib.types; either str (listOf str);
111
112 default = "hourly";
113
114 description = ''
115 The schedule on which to run the `self-deploy` service. Format
116 specified by `systemd.time 7`.
117
118 This value can also be a list of `systemd.time 7` formatted
119 strings, in which case the service will be started on multiple
120 schedules.
121 '';
122 };
123 };
124
125 config = lib.mkIf cfg.enable {
126 systemd.services.self-deploy = {
127 wantedBy = [ "multi-user.target" ];
128
129 requires = lib.mkIf (!(isPathType cfg.repository)) [ "network-online.target" ];
130
131 environment.GIT_SSH_COMMAND = lib.mkIf (!(isNull cfg.sshKeyFile))
132 "${pkgs.openssh}/bin/ssh -i ${lib.escapeShellArg cfg.sshKeyFile}";
133
134 restartIfChanged = false;
135
136 path = with pkgs; [
137 git
138 nix
139 systemd
140 ];
141
142 script = ''
143 if [ ! -e ${repositoryDirectory} ]; then
144 mkdir --parents ${repositoryDirectory}
145 git init ${repositoryDirectory}
146 fi
147
148 ${gitWithRepo} fetch ${lib.escapeShellArg cfg.repository} ${lib.escapeShellArg cfg.branch}
149
150 ${gitWithRepo} checkout FETCH_HEAD
151
152 nix-build${renderNixArgs cfg.nixArgs} ${lib.cli.toGNUCommandLineShell { } {
153 attr = cfg.nixAttribute;
154 out-link = outPath;
155 }} ${lib.escapeShellArg "${repositoryDirectory}${cfg.nixFile}"}
156
157 ${lib.optionalString (cfg.switchCommand != "test")
158 "nix-env --profile /nix/var/nix/profiles/system --set ${outPath}"}
159
160 ${outPath}/bin/switch-to-configuration ${cfg.switchCommand}
161
162 rm ${outPath}
163
164 ${gitWithRepo} gc --prune=all
165
166 ${lib.optionalString (cfg.switchCommand == "boot") "systemctl reboot"}
167 '';
168 };
169 };
170}