]> git.scottworley.com Git - auto-upgrade-with-pinch/commitdiff
Sync multiple repos, update multiple users
authorScott Worley <scottworley@scottworley.com>
Tue, 2 Jun 2020 07:19:42 +0000 (00:19 -0700)
committerScott Worley <scottworley@scottworley.com>
Tue, 2 Jun 2020 07:22:30 +0000 (00:22 -0700)
Make a bunch of config schema changes while we still can before we have
any other users.

modules/auto-upgrade.nix

index d30a624b9e567d7a7d9397d6c0d1791abf33dfe4..ef705eb8ad30176d93202f6c99f1504e93c013ef 100644 (file)
 with lib;
 let
   cfg = config.system.autoUpgradeWithPinch;
-  pull-repo-snippet = ''
-    (
-      cd /etc/nixos
-      ${pkgs.git}/bin/git fetch
-      PATH="${pkgs.keyedgit cfg.keys}/bin:$PATH" \
-        ${pkgs.polite-merge}/bin/polite-merge --ff-only --verify-signatures
-    )
-  '';
+  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" ''
     ${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"
-        }
-
-        as_user() {
-          ${
-            if cfg.userEnvironment.enable then ''
-              /run/wrappers/bin/sudo -u ${escapeShellArg cfg.userEnvironment.user} "$@"
-            '' else ''
-              :
-            ''
-          }
+          ${pkgs.coreutils}/bin/rm -r "$d"
         }
 
         # Pull updates
-        ${pull-repo-snippet}
+        ${concatStringsSep "\n" (mapAttrsToList (path: repo: ''
+          /run/wrappers/bin/sudo -u ${escapeShellArg repo.user} \
+            ${pull-repo-script path repo}
+        '') cfg.repos)}
 
         # Update channels
-        ${pkgs.pinch}/bin/pinch update /etc/nixos/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 --no-out-link '<nixpkgs>' -A ${
-          escapeShellArg cfg.userEnvironment.package
-        }
+        ${concatStringsSep "\n" (mapAttrsToList (user: env: ''
+          /run/wrappers/bin/sudo -u ${escapeShellArg user} \
+            nix-build --no-out-link '<nixpkgs>' \
+            -A ${escapeShellArg env.package}
+        '') cfg.userEnvironments)}
 
         # Install
         ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
-        as_user nix-env -f '<nixpkgs>' -riA ${
-          escapeShellArg cfg.userEnvironment.package
-        }
+        ${concatStringsSep "\n" (mapAttrsToList (user: env: ''
+          /run/wrappers/bin/sudo -u ${escapeShellArg user} \
+            nix-env -f '<nixpkgs>' \
+            ${optionalString (env.otherPackagesAction != "keep") "-r"} \
+            -iA ${escapeShellArg env.package}
+        '') cfg.userEnvironments)}
       ''
     }
   '';
@@ -79,48 +138,132 @@ in {
         '';
       };
 
-      keys = mkOption {
-        type = types.path;
+      repos = mkOption {
         description = ''
-          File containing GPG keys that sign updates.  Updates are only merged
-          if the commit at the tip of the remote branch is signed with one of
-          these keys.
+          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 <literal>remoteBranch</literal> are only ever
+                merged into <literal>localBranch</literal>, 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 <literal>signingKeys</literal>.
+              '';
+            };
+            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
+          <literal>repos</literal>' 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 = "nixos.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 <literal>package</literal>.
 
+                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 <literal>package</literal>.
+                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 = { }; };
       };
     };
   };
@@ -175,7 +318,7 @@ 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
@@ -198,11 +341,5 @@ in {
 
       startAt = cfg.dates;
     };
-
-    assertions = [{
-      assertion = cfg.userEnvironment.enable -> cfg.enable;
-      message =
-        "User environment upgrades cannot yet be enabled separately from system upgrades.";
-    }];
   };
 }