]> git.scottworley.com Git - auto-upgrade-with-pinch/blame - modules/auto-upgrade.nix
Formatting: Run nixfmt
[auto-upgrade-with-pinch] / modules / auto-upgrade.nix
CommitLineData
901670f5
SW
1{ config, lib, pkgs, ... }:
2with lib;
364c110c
SW
3let
4 cfg = config.system.autoUpgradeWithPinch;
eff66a9c
SW
5 pull-repo-script = pkgs.writeShellScript "pull-repo" ''
6 set -eo pipefail
4cbd961f 7
eff66a9c
SW
8 path=$1
9 config=$2
4cbd961f 10
eff66a9c
SW
11 prop() {
12 ${pkgs.jq}/bin/jq -r ".$1" <<< "$config"
13 }
f1a53b29 14
eff66a9c 15 echo Pulling in "$path" >&2
f1a53b29 16
eff66a9c
SW
17 if [[ ! -e "$path" ]];then
18 d=$(mktemp -d)
19 ${pkgs.git}/bin/git init "$d"
20 ${pkgs.git}/bin/git -C "$d" checkout -b "$(prop localBranch)"
21 ${pkgs.git}/bin/git -C "$d" remote add "$(prop remoteName)" "$(prop url)"
22 ${pkgs.git}/bin/git -C "$d" branch -u "$(prop remoteBranch)"
23 mkdir -p "$(${pkgs.coreutils}/bin/dirname "$path")"
24 mv "$d" "$path"
25 fi
4cbd961f 26
eff66a9c 27 cd "$path"
f1a53b29 28
eff66a9c
SW
29 if [[ "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" != "$(prop url)" ]]; then
30 echo Expected git remote "$(prop remoteName)" to point at "$(prop url)" \
31 but it points at "$(${pkgs.git}/bin/git remote get-url "$(prop remoteName)")" >&2
32 case "$(prop onRemoteURLMismatch)" in
33 abort) exit 1;;
34 update) echo Updating it >&2
35 ${pkgs.git}/bin/git -C "$d" remote set-url "$(prop remoteName)" "$(prop url)";;
36 esac
37 fi
4cbd961f 38
eff66a9c 39 ${pkgs.git}/bin/git fetch "$(prop remoteName)" "$(prop remoteBranch)"
4cbd961f 40
eff66a9c
SW
41 if [[ "$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)" != "$(prop localBranch)" ]];then
42 echo Could not merge because currently-checked-out \
43 \""$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)"\" is not \
44 \""$(prop localBranch)"\"
45 case "$(prop onBranchMismatch)" in
46 abort) exit 1;;
47 continue) exit 0;;
48 esac
49 fi
4cbd961f 50
eff66a9c
SW
51 if [[ "$(prop requireSignature)" == true ]]; then
52 ${pkgs.polite-merge}/bin/polite-merge \
53 -c gpg.program=${escapeShellArg (pkgs.keyedgpg cfg.signingKeys)} \
54 merge --ff-only --verify-signatures
55 else
56 ${pkgs.polite-merge}/bin/polite-merge merge --ff-only
57 fi
58 '';
9dbfef33 59
364c110c 60 auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
2b58720b 61 ${pkgs.utillinux}/bin/flock /run/auto-upgrade-with-pinch ${
364c110c 62 pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
4cbd961f 63 set -eo pipefail
eb0fa99c 64
f1a53b29
SW
65 dry_run=false
66 pinch_args=()
67 if [[ "$1" == --dry-run ]];then
68 dry_run=true
69 pinch_args=( --dry-run )
70 fi
71
72 hydrate() {
73 if "$dry_run";then
74 echo "Would run: $*"
75 else
76 "$@"
77 fi
78 }
79
80 die() {
81 echo "$*" >&2
82 exit 1
83 }
84
eb0fa99c 85 in_tmpdir() {
4cbd961f 86 d=$(${pkgs.coreutils}/bin/mktemp -d)
eb0fa99c
SW
87 pushd "$d"
88 "$@"
89 popd
4cbd961f 90 ${pkgs.coreutils}/bin/rm -r "$d"
eb0fa99c
SW
91 }
92
f1a53b29
SW
93 ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (''
94 verify_ownership() {
95 if [[ "$1" != /* ]];then
96 die "Unexpected relative path: $1"
97 fi
98 if [[ "$1" != / ]];then
99 verify_ownership "$(${pkgs.coreutils}/bin/dirname "$1")"
100 fi
101 if [[ ! -e "$1" ]];then
102 die "Could not find upgrade config: $1 does not exist"
103 fi
104 if [[ -h "$1" ]];then
105 verify_ownership "$(
106 ${pkgs.coreutils}/bin/realpath --no-symlinks \
107 "$(${pkgs.coreutils}/bin/dirname "$1")/$(${pkgs.coreutils}/bin/readlink "$1")"
108 )"
109 fi
110 perms="$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%M")"
111 if [[ "$perms" == d*t ]];then
112 die "Will not use upgrade config in sticky directory $1"
113 fi
114 owner=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%u")
115 if [[ "$owner" != root ]];then
116 die "Will not use upgrade config not owned by root in $1"
117 fi
118 if [[ "$perms" == l* ]];then
119 return 0 # Root-owned symlinks are fine
120 fi
121 if [[ "$perms" == *w? ]];then
122 die "Will not use world-writable upgrade config in $1"
123 fi
124 ${
125 {
126 root = ''
127 if [[ "$perms" == *w???? ]];then
128 die "Will not use group-writable upgrade config in $1"
129 fi
130 '';
131 wheel = ''
132 if [[ "$perms" == *w???? ]];then
133 group=$(${pkgs.findutils}/bin/find "$1" -maxdepth 0 -printf "%g")
134 if [[ "$group" != wheel ]];then
135 die "Will not use non-wheel-group group-writable upgrade config in $1"
136 fi
137 fi
138 '';
139 }."${cfg.upgradeConfigOwnershipPolicy}"
140 }
141 }
142 ''
143 + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}")
144 cfg.upgradeConfig)}
145
146 config=$(${pkgs.nix}/bin/nix eval --json -f ${../upgrade-config.nix} \
147 --arg upgradeConfig ${
148 escapeShellArg ("["
149 + lib.concatMapStringsSep " " lib.strings.escapeNixString
150 cfg.upgradeConfig + "]")
151 } config)
152
153 config_query() {
154 ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
155 }
156
157 repo_query() {
158 config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
159 }
160
161 userenv_query() {
162 config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
163 }
164
9dbfef33 165 # Pull updates
f1a53b29
SW
166 while read path;do
167 hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
168 ${pull-repo-script} "$path" "$(repo_query "$path" "")"
169 done < <( config_query '.repos | keys []' )
364c110c 170
fae44c38 171 # Update channels
f1a53b29 172 config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
fae44c38 173
eb0fa99c 174 # Build
f1a53b29
SW
175 in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
176 while read user;do
177 hydrate /run/wrappers/bin/sudo -u "$user" \
178 ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
179 done < <( config_query '.userEnvironments | keys []' )
eb0fa99c
SW
180
181 # Install
f1a53b29
SW
182 hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
183 while read user;do
184 remove_arg=-r
185 if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
186 remove_arg=
187 fi
188 hydrate /run/wrappers/bin/sudo -u "$user" \
189 ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
190 done < <( config_query '.userEnvironments | keys []' )
364c110c
SW
191 ''
192 }
193 '';
f1a53b29 194
901670f5
SW
195in {
196 options = {
197 system.autoUpgradeWithPinch = {
198
199 enable = mkOption {
200 type = types.bool;
201 default = false;
202 description = ''
203 Whether to periodically upgrade NixOS to the latest version.
204 Presumes that /etc/nixos is a git repo with a remote and
205 contains a pinch file called "channels".
206 '';
207 };
208
209 dates = mkOption {
210 default = "04:40";
211 type = types.str;
212 description = ''
213 Specification (in the format described by
214 <citerefentry><refentrytitle>systemd.time</refentrytitle>
215 <manvolnum>7</manvolnum></citerefentry>) of the time at
216 which the update will occur.
217 '';
218 };
d8537205 219
5ea02587
SW
220 signingKeys = mkOption {
221 type = types.listOf types.path;
222 description = ''
223 Files containing GPG keys that are authorized to sign updates.
224 Updates are only merged if the commit at the tip of the remote
225 ref is signed with one of these keys.
226 '';
227 };
228
f1a53b29
SW
229 upgradeConfig = mkOption {
230 type = types.listOf types.path;
d8537205 231 description = ''
f1a53b29
SW
232 Configuration files that specify what git repo paths to pull, what
233 pinch files to update from, and what user environments to update.
234 These are specified in separate configuration files processed at
235 update time so that changes to this configuration take effect in
236 the same update cycle.
d8537205 237 '';
4cbd961f 238 };
eb0fa99c 239
f1a53b29
SW
240 upgradeConfigOwnershipPolicy = mkOption {
241 type = types.enum [ "root" "wheel" "any" ];
242 default = "root";
4cbd961f 243 description = ''
f1a53b29
SW
244 Verify ownership of upgrade config files before using them for
245 system upgrades.
eb0fa99c 246
f1a53b29
SW
247 root = Config must be writable only by root.
248 wheel = Config must be writable only by root and wheel.
249 any = No checks. Not recommended.
4cbd961f 250 '';
eb0fa99c 251 };
901670f5
SW
252 };
253 };
254
255 config = lib.mkIf cfg.enable {
364c110c
SW
256
257 security.sudo.extraRules = lib.mkAfter [{
258 groups = [ "users" ];
259 commands = [{
260 command = "${auto-upgrade-script}";
261 options = [ "NOPASSWD" "NOSETENV" ];
262 }];
263 }];
264 # NOSETENV above still allows through ~17 vars, including PATH. Block those
265 # as well:
266 security.sudo.extraConfig = ''
267 Defaults!${auto-upgrade-script} !env_check
268 Defaults!${auto-upgrade-script} !env_keep
269 '';
270
d8537205 271 nixpkgs.overlays = [
f1a53b29 272 (import ../overlays/keyedgpg.nix)
d8537205 273 (import ../overlays/pinch.nix)
5048e8ce 274 (import ../overlays/polite-merge.nix)
5aaf4680
SW
275 (self: super: {
276 auto-upgrade = super.writeShellScriptBin "auto-upgrade" ''
4acf153c 277 /run/wrappers/bin/sudo ${auto-upgrade-script}
5aaf4680
SW
278 '';
279 })
d8537205 280 ];
5aaf4680
SW
281
282 environment.systemPackages = [ pkgs.auto-upgrade ];
283
901670f5
SW
284 systemd.services.nixos-upgrade = {
285 description = "NixOS Upgrade";
286 restartIfChanged = false;
287 unitConfig.X-StopOnRemoval = false;
288 serviceConfig.Type = "oneshot";
289 environment = config.nix.envVars // {
290 inherit (config.environment.sessionVariables) NIX_PATH;
291 HOME = "/root";
292 } // config.networking.proxy.envVars;
293
294 path = with pkgs; [
295 config.nix.package.out
296 coreutils
297 git
298 gitMinimal
299 gnutar
300 gzip
901670f5
SW
301 xz.bin
302 ];
303
304 script = ''
4cbd961f 305 set -eo pipefail
8569b965
SW
306
307 # Chill for awhile before applying updates. If applying an update
308 # badly breaks things, we want a window in which an operator can
309 # intervene either to fix the problem or disable automatic updates.
310 sleep 2h
311
364c110c 312 ${auto-upgrade-script}
901670f5
SW
313 '';
314
315 startAt = cfg.dates;
316 };
317 };
318}