# A NixOS module which enables remotely-triggered deployment updates.
{ config, lib, pkgs, ... }:
let
  # A handy alias for our part of the configuration.
  cfg = config.services.private-storage.deployment;

  # Compute an authorized_keys line that allows the holder of a certain key to
  # execute a certain command *only*.
  restrictedKey =
    { authorizedKey, command, gridName }:
    # `restrict` means "disable all the things" then `command` means "but
    # enable running this one command" (the client does not have to supply the
    # command; if they authenticate, this is the command that will run).
    "restrict,command=\"sudo ${command}\" ${authorizedKey}";

  update-script = pkgs.writeShellScript "update-deployment"
    ''
    set -euxo pipefail

    nix-env --profile /nix/var/nix/profiles/system --set -f ${lib.escapeShellArg ../top-level.nix} --argstr gridname ${lib.escapeShellArg cfg.gridName} --argstr hostname ${lib.escapeShellArg config.networking.hostName}

    /nix/var/nix/profiles/system/bin/switch-to-configuration switch
    '';
in {
  options = {
    services.private-storage.deployment.authorizedKey = lib.mkOption {
      type = lib.types.str;
      example = lib.literalExample ''
        ssh-ed25519 AAAAC3N...
      '';
      description = ''
        The SSH public key to authorize to trigger a deployment update.
      '';
    };
    services.private-storage.deployment.gridName = lib.mkOption {
      type = lib.types.str;
      example = lib.literalExample "staging";
      description = ''
        The name of the grid configuration to use to update this deployment.
      '';
    };
  };

  config = {
    # Configure the system to use our binary cache so that deployment updates
    # only require downloading pre-built software, not building it ourselves.
    nix = {
      binaryCachePublicKeys = [
        "saxtons.private.storage:MplOcEH8G/6mRlhlKkbA8GdeFR3dhCFsSszrspE/ZwY="
      ];
      binaryCaches = [
        "http://saxtons.private.storage"
      ];
    };

    # This package is used by top-level.nix above.
    system.extraDependencies = [ pkgs.morph.lib ];

    security.sudo.extraRules = [
      {
        users = ["deployment"];
        commands = [ "${update-script}" ];
        runAs = "root";
      }
    ];

    # Raise the hard-limit on the size of $XDG_RUNTIME_DIR (ie
    # /run/user/<uid>).  The default of 10% is too small on some systems for
    # the temporary state morph creates to do the self-update.
    services.logind.extraConfig = ''
      RuntimeDirectorySize=50%
    '';

    # Configure the deployment user.
    users.users.deployment = {
      # A user must be either normal or system.  A normal user uses the
      # default shell, has a home directory created for it at the usual
      # location, and is in the "users" group.  That's pretty much what we
      # want for the deployment user.
      isNormalUser = true;

      # Authorize the supplied key to run the deployment update command.
      openssh.authorizedKeys.keys = [
        (restrictedKey {
          inherit (cfg) authorizedKey gridName;
          command = update-script;
        })
      ];
    };
  };
}