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