X-Git-Url: http://git.scottworley.com/auto-upgrade-with-pinch/blobdiff_plain/eb0fa99c7cfcb863cfcc3c1ba15aa0fbc2fcb120..68be91dbe482a959797fd1a7efce7476ba9a3bf2:/modules/auto-upgrade.nix diff --git a/modules/auto-upgrade.nix b/modules/auto-upgrade.nix index 216c4ba..5ee707f 100644 --- a/modules/auto-upgrade.nix +++ b/modules/auto-upgrade.nix @@ -2,47 +2,114 @@ with lib; let cfg = config.system.autoUpgradeWithPinch; + pull-repo-script = path: repo: + pkgs.writeShellScript "pull-repo" '' + set -eo pipefail + + echo Pulling in ${escapeShellArg path} >&2 + + if [[ ! -e ${escapeShellArg path} ]];then + d=$(mktemp -d) + ${pkgs.git}/bin/git init "$d" + ${pkgs.git}/bin/git -C "$d" checkout -b \ + ${escapeShellArg repo.localBranch} + ${pkgs.git}/bin/git -C "$d" remote add \ + ${escapeShellArg repo.remoteName} \ + ${escapeShellArg repo.url} + ${pkgs.git}/bin/git -C "$d" branch -u \ + ${escapeShellArg repo.remoteBranch} + mkdir -p "$(dirname ${escapeShellArg path})" + mv "$d" ${escapeShellArg path} + fi + + cd ${escapeShellArg path} + + if [[ "$(${pkgs.git}/bin/git remote get-url \ + ${escapeShellArg repo.remoteName})" != \ + ${escapeShellArg repo.url} ]] + then + echo Expected git remote ${escapeShellArg repo.remoteName} \ + to point at ${escapeShellArg repo.url} \ + but it points at "$(${pkgs.git}/bin/git remote get-url \ + ${escapeShellArg repo.remoteName})" >&2 + ${ + { + abort = "exit 1"; + update = '' + echo Updating it >&2 + ${pkgs.git}/bin/git -C "$d" remote set-url \ + ${escapeShellArg repo.remoteName} \ + ${escapeShellArg repo.url} + ''; + }."${repo.onRemoteURLMismatch}" + } + fi + + ${pkgs.git}/bin/git fetch \ + ${escapeShellArg repo.remoteName} \ + ${escapeShellArg repo.remoteBranch} + + if [[ "$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)" != \ + ${escapeShellArg repo.localBranch} ]] + then + echo Could not merge because currently-checked-out \ + \""$(${pkgs.git}/bin/git rev-parse --abbrev-ref HEAD)"\" is not \ + \"${escapeShellArg repo.localBranch}\" + ${ + { + abort = "exit 1"; + continue = "exit 0"; + }."${repo.onBranchMismatch}" + } + fi + + + ${if repo.requireSignature then '' + PATH="${pkgs.keyedgit repo.signingKeys}/bin:$PATH" \ + ${pkgs.polite-merge}/bin/polite-merge --ff-only --verify-signatures + '' else '' + ${pkgs.polite-merge}/bin/polite-merge --ff-only + ''} + ''; + auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" '' - flock /run/auto-upgrade-with-pinch ${ + ${pkgs.utillinux}/bin/flock /run/auto-upgrade-with-pinch ${ pkgs.writeShellScript "auto-upgrade-with-lock-held" '' - set -e + set -eo pipefail in_tmpdir() { - d=$(mktemp -d) + d=$(${pkgs.coreutils}/bin/mktemp -d) pushd "$d" "$@" popd - rm -r "$d" + ${pkgs.coreutils}/bin/rm -r "$d" } - as_user() { - ${ - if cfg.userEnvironment.enable then '' - sudo -u ${escapeShellArg cfg.userEnvironment.user} "$@" - '' else '' - : - '' - } - } + # Pull updates + ${concatStringsSep "\n" (mapAttrsToList (path: repo: '' + /run/wrappers/bin/sudo -u ${escapeShellArg repo.user} \ + ${pull-repo-script path repo} + '') cfg.repos)} # Update channels - ( - cd /etc/nixos - ${pkgs.keyedgit cfg.key}/bin/git pull --ff-only --verify-signatures - ${pkgs.pinch}/bin/pinch update channels - ) + ${pkgs.pinch}/bin/pinch update ${escapeShellArgs cfg.pinchFiles} # Build in_tmpdir ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build - as_user nix-build '' -A ${ - escapeShellArg cfg.userEnvironment.package - } + ${concatStringsSep "\n" (mapAttrsToList (user: env: '' + /run/wrappers/bin/sudo -u ${escapeShellArg user} \ + nix-build --no-out-link '' \ + -A ${escapeShellArg env.package} + '') cfg.userEnvironments)} # Install ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch - as_user nix-env -f '' -riA ${ - escapeShellArg cfg.userEnvironment.package - } + ${concatStringsSep "\n" (mapAttrsToList (user: env: '' + /run/wrappers/bin/sudo -u ${escapeShellArg user} \ + nix-env -f '' \ + ${optionalString (env.otherPackagesAction != "keep") "-r"} \ + -iA ${escapeShellArg env.package} + '') cfg.userEnvironments)} '' } ''; @@ -71,47 +138,132 @@ in { ''; }; - key = mkOption { - type = types.path; + repos = mkOption { description = '' - GPG key that signs updates. Updates are only merged if the commit - at the tip of the remote branch is signed with this key. + Git repositories to pull before running pinch. These are maintained + as git checkouts at specified places in the filesystem with specified + ownership rather than kept read-only in the nix store so that humans + can use them both as points of intervention in the automation and to + author and push changes back up. ''; - }; + type = types.attrsOf (types.submodule { + options = { + url = mkOption { + description = "Remote git repo."; + type = types.str; + }; + remoteName = mkOption { + description = ''Name of the git remote. Customarily "origin".''; + type = types.str; + default = "origin"; + }; + onRemoteURLMismatch = mkOption { + description = '' + What to do if the remote URL in the git repo doesn't match the + URL configured here. + ''; + type = types.enum [ "update" "abort" ]; + default = "update"; + }; + onBranchMismatch = mkOption { + description = '' + What to do if a different branch is currently checked out. - userEnvironment = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Whether to update a user-environment as well. This update is done - with nix-env -riA. Note the -r! I.e., ALL OTHER PACKAGES INSTALLED - WITH nix-env WILL BE DELETED! - - This presumes that you have configured an "entire user environment" - package as shown in - https://nixos.wiki/wiki/FAQ#How_can_I_manage_software_with_nix-env_like_with_configuration.nix.3F - - To check if you're set up for this, run "nix-env --query". If it - only lists one package, you're good to go. - ''; + (Changes from remoteBranch are only ever + merged into localBranch, so if a different + branch is checked out, no remote changes will be merged.) + ''; + type = types.enum [ "continue" "abort" ]; + default = "continue"; + }; + user = mkOption { + description = "User as which to run 'git fetch'"; + type = types.str; + }; + localBranch = mkOption { + description = ""; + type = types.str; + default = "master"; + }; + remoteBranch = mkOption { + type = types.str; + default = "master"; + }; + requireSignature = mkOption { + type = types.bool; + default = true; + description = '' + Only pull when the tip of the remote ref is signed by a key + specifed in signingKeys. + ''; + }; + signingKeys = mkOption { + type = types.either types.path (types.listOf types.path); + description = '' + Files containing GPG keys that are authorized to sign updates. + Updates are only merged if the commit at the tip of the remote + ref is signed with one of these keys. + ''; + }; + }; + }); + example = { + "/etc/nixos" = { + url = "https://github.com/chkno/auto-upgrade-demo-nixos"; + user = "root"; + signingKeys = [ ./admins.asc ]; + }; + "/home/alice/.config/nixpkgs" = { + url = "https://github.com/chkno/auto-upgrade-demo-user-nixpkgs"; + user = "alice"; + signingKeys = [ ./admins.asc ./alice.asc ]; + }; }; + }; - user = mkOption { - type = types.str; - description = '' - The username of the user whose environment should be updated. - ''; - }; + pinchFiles = mkOption { + description = '' + Pinch files to use for channel updates. Typically these are inside + repos' paths. + ''; + type = types.listOf types.path; + example = [ "/etc/nixos/channels" ]; + }; - package = mkOption { - type = types.str; - example = "nixos.userPackages"; - description = '' - The name of the single package that is the user's entire environment. - ''; - }; + userEnvironments = mkOption { + description = '' + User environments to update as part of an upgrade run. + ''; + type = types.attrsOf (types.submodule { + options = { + package = mkOption { + type = types.str; + default = "userPackages"; + description = '' + The name of the single package that will be updated. You'll + want to create an 'entire user environment' package as shown in + https://nixos.wiki/wiki/FAQ#How_can_I_manage_software_with_nix-env_like_with_configuration.nix.3F + ''; + }; + otherPackagesAction = mkOption { + type = types.enum [ "remove" "keep" "abort" ]; + default = "remove"; + description = '' + What to do with packages other than package. + THIS DEFAULTS TO "remove", WHICH IS POTENTIALLY SOMEWHAT + DESTRUCTIVE! This is the default because it is the recommended + setting -- This module recommends managing your environment + through your one entire-environment package. + This keeps your environment declarative and ensures that all + packages receive regular updates. + ''; + # It seems like "upgrade" ought to be another choice here, powered + # by "nix-env --upgrade". But when I tried this, it didn't work. + }; + }; + }); + example = { alice = { }; }; }; }; }; @@ -135,9 +287,10 @@ in { nixpkgs.overlays = [ (import ../overlays/keyedgit.nix) (import ../overlays/pinch.nix) + (import ../overlays/polite-merge.nix) (self: super: { auto-upgrade = super.writeShellScriptBin "auto-upgrade" '' - sudo ${auto-upgrade-script} + /run/wrappers/bin/sudo ${auto-upgrade-script} ''; }) ]; @@ -165,34 +318,17 @@ in { ]; script = '' - set -e + set -eo pipefail # Chill for awhile before applying updates. If applying an update # badly breaks things, we want a window in which an operator can # intervene either to fix the problem or disable automatic updates. sleep 2h - # Wait until outside business hours - now=$(date +%s) - day_of_week=$(date +%u) - business_start=$(date -d 8:00 +%s) - business_end=$( date -d 17:00 +%s) - if (( day_of_week <= 5 && now > business_start && now < business_end ));then - delay=$((business_end - now)) - echo "Waiting $delay seconds so we don't upgrade during business hours" >&2 - sleep "$delay" - fi - ${auto-upgrade-script} ''; startAt = cfg.dates; }; - - assertions = [{ - assertion = cfg.userEnvironment.enable -> cfg.enable; - message = - "User environment upgrades cannot yet be enabled separately from system upgrades."; - }]; }; }