-{ pkgs ? import <nixpkgs> { }, }:
+{
+ pkgs ? import <nixpkgs> { },
+}:
-pkgs.lib.makeScope pkgs.newScope (self:
- with self; {
+pkgs.lib.makeScope pkgs.newScope (
+ self: with self; {
homeless-gpg = callPackage ./pkgs/homeless-gpg.nix { };
keyed-gpg = callPackage ./pkgs/keyed-gpg.nix { };
- })
+ }
+)
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, version 3.
-{ config, lib, pkgs, ... }:
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
with lib;
let
local-pkgs = import ../. { inherit pkgs; };
auto-upgrade-script = pkgs.writeShellScript "auto-upgrade" ''
${pkgs.coreutils}/bin/nice -n 17 \
${pkgs.util-linux}/bin/ionice -c 3 \
- ${pkgs.util-linux}/bin/flock /run/auto-upgrade-with-pinch ${
- pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
- set -eo pipefail
-
- dry_run=false
- pinch_args=()
- if [[ "$1" == --dry-run ]];then
- dry_run=true
- pinch_args=( --dry-run )
- fi
-
- hydrate() {
- if "$dry_run";then
- echo "Would run: $*"
- else
- "$@"
- fi
- }
-
- die() {
- echo "$*" >&2
- exit 1
- }
-
- in_tmpdir() {
- d=$(${pkgs.coreutils}/bin/mktemp -d)
- pushd "$d"
+ ${pkgs.util-linux}/bin/flock /run/auto-upgrade-with-pinch ${pkgs.writeShellScript "auto-upgrade-with-lock-held" ''
+ set -eo pipefail
+
+ dry_run=false
+ pinch_args=()
+ if [[ "$1" == --dry-run ]];then
+ dry_run=true
+ pinch_args=( --dry-run )
+ fi
+
+ hydrate() {
+ if "$dry_run";then
+ echo "Would run: $*"
+ else
"$@"
- popd
- ${pkgs.coreutils}/bin/rm -r "$d"
- }
-
- ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (''
+ fi
+ }
+
+ die() {
+ echo "$*" >&2
+ exit 1
+ }
+
+ in_tmpdir() {
+ d=$(${pkgs.coreutils}/bin/mktemp -d)
+ pushd "$d"
+ "$@"
+ popd
+ ${pkgs.coreutils}/bin/rm -r "$d"
+ }
+
+ ${optionalString (cfg.upgradeConfigOwnershipPolicy != "any") (
+ ''
verify_ownership() {
if [[ "$1" != /* ]];then
die "Unexpected relative path: $1"
fi
fi
'';
- }."${cfg.upgradeConfigOwnershipPolicy}"
+ }
+ ."${cfg.upgradeConfigOwnershipPolicy}"
}
}
''
- + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}")
- cfg.upgradeConfig)}
-
- config=$(${pkgs.nix}/bin/nix-instantiate --eval --strict --json -A config \
- --arg upgradeConfig ${
- escapeShellArg ("["
- + lib.concatMapStringsSep " " lib.strings.escapeNixString
- cfg.upgradeConfig + "]")
- } ${../upgrade-config.nix})
-
- config_query() {
- ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
- }
-
- repo_query() {
- config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
- }
-
- userenv_query() {
- config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
- }
-
- # Pull updates
- while read path;do
- hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
- ${pull-repo-script} "$path" "$(repo_query "$path" "")"
- done < <( config_query '.repos | keys []' )
-
- # Update channels
- config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
-
- # Build
- in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
- while read user;do
- pushd /
- hydrate /run/wrappers/bin/sudo -u "$user" \
- ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
- popd
- done < <( config_query '.userEnvironments | keys []' )
- sync
-
- # Install
- hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
+ + concatMapStringsSep "\n" (f: "verify_ownership ${escapeShellArg f}") cfg.upgradeConfig
+ )}
+
+ config=$(${pkgs.nix}/bin/nix-instantiate --eval --strict --json -A config \
+ --arg upgradeConfig ${
+ escapeShellArg (
+ "[" + lib.concatMapStringsSep " " lib.strings.escapeNixString cfg.upgradeConfig + "]"
+ )
+ } ${../upgrade-config.nix})
+
+ config_query() {
+ ${pkgs.jq}/bin/jq -r "$@" <<< "$config"
+ }
+
+ repo_query() {
+ config_query --arg path "$1" ".repos[\$ARGS.named.path]$2"
+ }
+
+ userenv_query() {
+ config_query --arg user "$1" ".userEnvironments[\$ARGS.named.user]$2"
+ }
+
+ # Pull updates
+ while read path;do
+ hydrate /run/wrappers/bin/sudo -u "$(repo_query "$path" .user)" \
+ ${pull-repo-script} "$path" "$(repo_query "$path" "")"
+ done < <( config_query '.repos | keys []' )
+
+ # Update channels
+ config_query '.pinchFiles[]' | ${pkgs.findutils}/bin/xargs --no-run-if-empty --delimiter=\\n ${pkgs.pinch}/bin/pinch update "''${pinch_args[@]}"
+
+ # Build
+ in_tmpdir hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild build
+ while read user;do
+ pushd /
+ hydrate /run/wrappers/bin/sudo -u "$user" \
+ ${pkgs.nix}/bin/nix-build --no-out-link '<nixpkgs>' -A "$(userenv_query "$user" .package)"
+ popd
+ done < <( config_query '.userEnvironments | keys []' )
+ sync
+
+ # Install
+ hydrate ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch
+ sync
+ while read user;do
+ remove_arg=-r
+ if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
+ remove_arg=
+ fi
+ hydrate /run/wrappers/bin/sudo -u "$user" \
+ ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
sync
- while read user;do
- remove_arg=-r
- if [[ "$(userenv_query "$user" .otherPackagesAction)" == keep ]];then
- remove_arg=
- fi
- hydrate /run/wrappers/bin/sudo -u "$user" \
- ${pkgs.nix}/bin/nix-env -f '<nixpkgs>' $remove_arg -iA "$(userenv_query "$user" .package)"
- sync
- done < <( config_query '.userEnvironments | keys []' )
- ''
- }
+ done < <( config_query '.userEnvironments | keys []' )
+ ''}
'';
-in {
+in
+{
options = {
system.autoUpgradeWithPinch = {
};
upgradeConfigOwnershipPolicy = mkOption {
- type = types.enum [ "root" "wheel" "any" ];
+ type = types.enum [
+ "root"
+ "wheel"
+ "any"
+ ];
default = "root";
description = ''
Verify ownership of upgrade config files before using them for
config = lib.mkIf cfg.enable {
- security.sudo.extraRules = lib.mkAfter [{
- groups = [ "users" ];
- commands = [{
- command = "${auto-upgrade-script}";
- options = [ "NOPASSWD" "NOSETENV" ];
- }];
- }];
+ security.sudo.extraRules = lib.mkAfter [
+ {
+ groups = [ "users" ];
+ commands = [
+ {
+ command = "${auto-upgrade-script}";
+ options = [
+ "NOPASSWD"
+ "NOSETENV"
+ ];
+ }
+ ];
+ }
+ ];
# NOSETENV above still allows through ~17 vars, including PATH. Block those
# as well:
security.sudo.extraConfig = ''
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig.Type = "oneshot";
- environment = config.nix.envVars // {
- inherit (config.environment.sessionVariables) NIX_PATH;
- HOME = "/root";
- } // config.networking.proxy.envVars;
+ environment =
+ config.nix.envVars
+ // {
+ inherit (config.environment.sessionVariables) NIX_PATH;
+ HOME = "/root";
+ }
+ // config.networking.proxy.envVars;
path = with pkgs; [
config.nix.package.out
self: super:
let
- fallback-git-cache = self.python3Packages.callPackage
- ({ buildPythonPackage, fetchgit, setuptools, git, backoff, mypy, }:
- buildPythonPackage rec {
- pname = "git-cache";
- version = "1.5.0";
- src = fetchgit {
- url = "https://git.scottworley.com/pub/git/git-cache";
- rev = "v${version}";
- hash = "sha256-g4TS/zX3e29Q3ThsCAX2wLLlYbi8fdux5uqAc+b/Oww=";
- };
- pyproject = true;
- build-system = [ setuptools ];
- propagatedBuildInputs = [ backoff ];
- nativeCheckInputs = [ git mypy ];
- doCheck = true;
- checkPhase = "./test.sh";
- }) { };
+ fallback-git-cache = self.python3Packages.callPackage (
+ {
+ buildPythonPackage,
+ fetchgit,
+ setuptools,
+ git,
+ backoff,
+ mypy,
+ }:
+ buildPythonPackage rec {
+ pname = "git-cache";
+ version = "1.5.0";
+ src = fetchgit {
+ url = "https://git.scottworley.com/pub/git/git-cache";
+ rev = "v${version}";
+ hash = "sha256-g4TS/zX3e29Q3ThsCAX2wLLlYbi8fdux5uqAc+b/Oww=";
+ };
+ pyproject = true;
+ build-system = [ setuptools ];
+ propagatedBuildInputs = [ backoff ];
+ nativeCheckInputs = [
+ git
+ mypy
+ ];
+ doCheck = true;
+ checkPhase = "./test.sh";
+ }
+ ) { };
- fallback-pinch = self.python3Packages.callPackage
- ({ buildPythonPackage, fetchgit, setuptools, nix, git, mypy, git-cache, }:
- buildPythonPackage rec {
- pname = "pinch";
- version = "3.3.2";
- src = fetchgit {
- url = "https://git.scottworley.com/pub/git/pinch";
- rev = "v${version}";
- hash = "sha256-UB1hAEX7bD2TfdDv5EOWH1aaLluvzvpW80EjdCBuCCU=";
- };
- pyproject = true;
- build-system = [ setuptools ];
- propagatedBuildInputs = [ git-cache ];
- nativeCheckInputs = [ nix git mypy ];
- doCheck = true;
- checkPhase = "./test.sh";
- }) { git-cache = self.python3Packages.git-cache or fallback-git-cache; };
+ fallback-pinch = self.python3Packages.callPackage (
+ {
+ buildPythonPackage,
+ fetchgit,
+ setuptools,
+ nix,
+ git,
+ mypy,
+ git-cache,
+ }:
+ buildPythonPackage rec {
+ pname = "pinch";
+ version = "3.3.2";
+ src = fetchgit {
+ url = "https://git.scottworley.com/pub/git/pinch";
+ rev = "v${version}";
+ hash = "sha256-UB1hAEX7bD2TfdDv5EOWH1aaLluvzvpW80EjdCBuCCU=";
+ };
+ pyproject = true;
+ build-system = [ setuptools ];
+ propagatedBuildInputs = [ git-cache ];
+ nativeCheckInputs = [
+ nix
+ git
+ mypy
+ ];
+ doCheck = true;
+ checkPhase = "./test.sh";
+ }
+ ) { git-cache = self.python3Packages.git-cache or fallback-git-cache; };
-in { pinch = super.pinch or fallback-pinch; }
+in
+{
+ pinch = super.pinch or fallback-pinch;
+}
self: super: {
- polite-merge = if builtins.hasAttr "polite-merge" super then
- super.polite-merge
- else
- self.callPackage ({ fetchgit, git, stdenv, }:
- stdenv.mkDerivation rec {
- pname = "polite-merge";
- version = "2.4.2";
- src = fetchgit {
- url = "https://git.scottworley.com/pub/git/polite-merge";
- rev = "v${version}";
- hash = "sha256-CUNKLCwIFwwVaA9opw9yql5AGej/ozQv8k1YR/cfV4I=";
- };
- postUnpack = "patchShebangs .";
- nativeCheckInputs = [ git ];
- doCheck = true;
- preInstall = "export prefix";
- }) { };
+ polite-merge =
+ if builtins.hasAttr "polite-merge" super then
+ super.polite-merge
+ else
+ self.callPackage (
+ {
+ fetchgit,
+ git,
+ stdenv,
+ }:
+ stdenv.mkDerivation rec {
+ pname = "polite-merge";
+ version = "2.4.2";
+ src = fetchgit {
+ url = "https://git.scottworley.com/pub/git/polite-merge";
+ rev = "v${version}";
+ hash = "sha256-CUNKLCwIFwwVaA9opw9yql5AGej/ozQv8k1YR/cfV4I=";
+ };
+ postUnpack = "patchShebangs .";
+ nativeCheckInputs = [ git ];
+ doCheck = true;
+ preInstall = "export prefix";
+ }
+ ) { };
}
-
-{ coreutils, gnupg, writeShellScript }:
+{
+ coreutils,
+ gnupg,
+ writeShellScript,
+}:
writeShellScript "homeless-gpg" ''
set -eo pipefail
# Following the instructions at https://tribut.de/blog/git-commit-signatures-trusted-keys
# Use with git with -c gpg.program='keyedgpg /path/to/keyfile.asc'
-{ coreutils, gawk, homeless-gpg, lib, writeShellScript, }:
+{
+ coreutils,
+ gawk,
+ homeless-gpg,
+ lib,
+ writeShellScript,
+}:
keyfiles:
writeShellScript "keyed-gpg" ''
set -eo pipefail
${homeless-gpg} --keyring="$keyring" "''${trusted_key_args[@]}" "$@"
''
-
-{ upgradeConfig, lib ? (import <nixpkgs> { }).lib, }:
+{
+ upgradeConfig,
+ lib ? (import <nixpkgs> { }).lib,
+}:
with lib;
evalModules {
- modules = upgradeConfig ++ [{
- options = {
+ modules = upgradeConfig ++ [
+ {
+ options = {
- enable = mkOption {
- type = types.bool;
- default = false;
- description = ''
- Whether to periodically upgrade NixOS to the latest version.
- Presumes that /etc/nixos is a git repo with a remote and
- contains a pinch file called "channels".
- '';
- };
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to periodically upgrade NixOS to the latest version.
+ Presumes that /etc/nixos is a git repo with a remote and
+ contains a pinch file called "channels".
+ '';
+ };
- dates = mkOption {
- default = "04:40";
- type = types.str;
- description = ''
- Specification (in the format described by
- <citerefentry><refentrytitle>systemd.time</refentrytitle>
- <manvolnum>7</manvolnum></citerefentry>) of the time at
- which the update will occur.
- '';
- };
+ dates = mkOption {
+ default = "04:40";
+ type = types.str;
+ description = ''
+ Specification (in the format described by
+ <citerefentry><refentrytitle>systemd.time</refentrytitle>
+ <manvolnum>7</manvolnum></citerefentry>) of the time at
+ which the update will occur.
+ '';
+ };
- repos = mkOption {
- description = ''
- 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.
- '';
- default = { };
- 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.
+ repos = mkOption {
+ description = ''
+ 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.
+ '';
+ default = { };
+ 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.
- (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";
+ (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>.
+ '';
+ };
+ };
+ }
+ );
+ example = {
+ "/etc/nixos" = {
+ url = "https://github.com/chkno/auto-upgrade-demo-nixos";
+ user = "root";
+ signingKeys = [ ./admins.asc ];
};
- remoteBranch = mkOption {
- type = types.str;
- default = "master";
+ "/home/alice/.config/nixpkgs" = {
+ url = "https://github.com/chkno/auto-upgrade-demo-user-nixpkgs";
+ user = "alice";
+ signingKeys = [
+ ./admins.asc
+ ./alice.asc
+ ];
};
- 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>.
- '';
- };
- };
- });
- 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 ];
};
};
- };
- pinchFiles = mkOption {
- description = ''
- Pinch files to use for channel updates. Typically these are inside
- <literal>repos</literal>' paths.
- '';
- type = types.listOf types.path;
- default = [ ];
- example = [ "/etc/nixos/channels" ];
- };
+ pinchFiles = mkOption {
+ description = ''
+ Pinch files to use for channel updates. Typically these are inside
+ <literal>repos</literal>' paths.
+ '';
+ type = types.listOf types.path;
+ default = [ ];
+ example = [ "/etc/nixos/channels" ];
+ };
- userEnvironments = mkOption {
- description = ''
- User environments to update as part of an upgrade run.
- '';
- default = { };
- 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 <literal>package</literal>.
+ userEnvironments = mkOption {
+ description = ''
+ User environments to update as part of an upgrade run.
+ '';
+ default = { };
+ 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 <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.
- };
+ 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 = { };
};
- });
- example = { alice = { }; };
+ };
};
- };
- }];
+ }
+ ];
}