764 lines
27 KiB
Nix
764 lines
27 KiB
Nix
{ self }:
|
|
{ config, lib, pkgs, utils, ... }:
|
|
|
|
let
|
|
cfg = config.services.quixosHost;
|
|
secretsCfg = cfg.secrets;
|
|
quixosPackages = self.packages.${pkgs.stdenv.hostPlatform.system};
|
|
localPersistMount = "${utils.escapeSystemdPath "/persist-local"}.mount";
|
|
sharedPersistMount = "${utils.escapeSystemdPath "/persist"}.mount";
|
|
secretFiles = {
|
|
controlSecret = "${secretsCfg.runtimeDirectory}/control-secret";
|
|
stateContextSecret = "${secretsCfg.runtimeDirectory}/state-context-secret";
|
|
packageTreeGitUsername = "${secretsCfg.runtimeDirectory}/package-tree-git-username";
|
|
packageTreeGitPassword = "${secretsCfg.runtimeDirectory}/package-tree-git-password";
|
|
};
|
|
fetchAwsSsmParameter = parameterName: destination: ''
|
|
${pkgs.awscli2}/bin/aws ssm get-parameter \
|
|
--with-decryption \
|
|
--name ${lib.escapeShellArg parameterName} \
|
|
--query Parameter.Value \
|
|
--output text > ${lib.escapeShellArg destination}.tmp
|
|
install -m 0640 -o root -g quixos-control ${lib.escapeShellArg destination}.tmp ${lib.escapeShellArg destination}
|
|
rm -f ${lib.escapeShellArg destination}.tmp
|
|
'';
|
|
installFileSecret = source: destination: ''
|
|
install -m 0640 -o root -g quixos-control ${lib.escapeShellArg source} ${lib.escapeShellArg destination}
|
|
'';
|
|
packageTreeGitAskPass = pkgs.writeShellScript "quixos-package-tree-git-askpass" ''
|
|
case "$1" in
|
|
*Username*)
|
|
username="$(cat ${lib.escapeShellArg secretFiles.packageTreeGitUsername} 2>/dev/null || true)"
|
|
if [ -z "$username" ]; then
|
|
username="oauth2"
|
|
fi
|
|
printf '%s\n' "$username"
|
|
;;
|
|
*Password*)
|
|
cat ${lib.escapeShellArg secretFiles.packageTreeGitPassword} 2>/dev/null || true
|
|
;;
|
|
*)
|
|
printf '\n'
|
|
;;
|
|
esac
|
|
'';
|
|
qxControl = pkgs.writeShellApplication {
|
|
name = "qx-control";
|
|
text = ''
|
|
if [ -z "''${GIT_ASKPASS:-}" ]; then
|
|
GIT_ASKPASS=${lib.escapeShellArg packageTreeGitAskPass}
|
|
export GIT_ASKPASS
|
|
fi
|
|
if [ -z "''${GIT_TERMINAL_PROMPT:-}" ]; then
|
|
GIT_TERMINAL_PROMPT=0
|
|
export GIT_TERMINAL_PROMPT
|
|
fi
|
|
if [ -z "''${GIT_AUTHOR_NAME:-}" ]; then
|
|
GIT_AUTHOR_NAME="Quixos Runtime"
|
|
export GIT_AUTHOR_NAME
|
|
fi
|
|
if [ -z "''${GIT_AUTHOR_EMAIL:-}" ]; then
|
|
GIT_AUTHOR_EMAIL="quixos-runtime@localhost"
|
|
export GIT_AUTHOR_EMAIL
|
|
fi
|
|
if [ -z "''${GIT_COMMITTER_NAME:-}" ]; then
|
|
GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
|
|
export GIT_COMMITTER_NAME
|
|
fi
|
|
if [ -z "''${GIT_COMMITTER_EMAIL:-}" ]; then
|
|
GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
|
|
export GIT_COMMITTER_EMAIL
|
|
fi
|
|
|
|
if [ -z "''${QUIXOS_CONTROL_SECRET:-}" ] && [ -r ${lib.escapeShellArg secretFiles.controlSecret} ]; then
|
|
QUIXOS_CONTROL_SECRET="$(cat ${lib.escapeShellArg secretFiles.controlSecret})"
|
|
export QUIXOS_CONTROL_SECRET
|
|
fi
|
|
|
|
exec ${quixosPackages.qx-control}/bin/qx-control "$@"
|
|
'';
|
|
};
|
|
quixosCodexDir = "/persist/var/lib/quixos/codex";
|
|
quixosCodexRule = ''prefix_rule(pattern=[\x22qx-control\x22],decision=\x22allow\x22)'';
|
|
in
|
|
{
|
|
imports = [
|
|
(lib.mkRenamedOptionModule
|
|
[ "services" "quixosHost" "caddyPersistZfsPool" ]
|
|
[ "services" "quixosHost" "sharedPersistZfsPool" ])
|
|
(lib.mkRenamedOptionModule
|
|
[ "services" "quixosHost" "caddyPersistDevice" ]
|
|
[ "services" "quixosHost" "sharedPersistDevice" ])
|
|
];
|
|
|
|
options.services.quixosHost = {
|
|
enable = lib.mkEnableOption "Quixos host";
|
|
|
|
domain = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Public domain served by Caddy.";
|
|
};
|
|
|
|
packagesPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/srv/quixos/packages";
|
|
description = "Mutable package-tree checkout path.";
|
|
};
|
|
|
|
dataRoot = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/var/lib/quixos";
|
|
description = "Persistent Quixos runtime state root.";
|
|
};
|
|
|
|
secretsEnvironmentFile = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/secrets/quixos.env";
|
|
description = "Environment file containing Quixos secrets.";
|
|
};
|
|
|
|
packageTreeGitEnvironmentFile = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/secrets/quixos-git.env";
|
|
visible = false;
|
|
description = "Optional environment file containing Git credentials for the package tree.";
|
|
};
|
|
|
|
packageTreeRepo = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "https://gitea-external.egads.tutti.syntaxblitz.net/quixos-packages/packages.git";
|
|
description = "Git URL for the mutable package-tree super-repo.";
|
|
};
|
|
|
|
packageTreeRef = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "master";
|
|
description = "Package-tree ref to clone on first boot.";
|
|
};
|
|
|
|
forcePackageTreeRefOnBoot = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Whether each boot should force the package tree back to packageTreeRef.";
|
|
};
|
|
|
|
persistZfsPool = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "quixos-persist";
|
|
description = "ZFS pool name on the deployment-local persistent block device mounted at /persist-local.";
|
|
};
|
|
|
|
persistDevice = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Stable device path for the deployment-local persistent block device. If set, Quixos initializes a blank device as persistZfsPool before mounting /persist-local.";
|
|
};
|
|
|
|
sharedPersistZfsPool = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "quixos-shared";
|
|
description = "ZFS pool name on the long-lived shared persistent block device mounted at /persist.";
|
|
};
|
|
|
|
sharedPersistDevice = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Stable device path for the long-lived shared persistent block device. If set, Quixos initializes a blank device as sharedPersistZfsPool before mounting /persist.";
|
|
};
|
|
|
|
nixZfsPool = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "nixos-zfs-nix";
|
|
description = "ZFS pool name supplied by the matched AMI /nix volume.";
|
|
};
|
|
|
|
hostId = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "71756978";
|
|
description = "Eight hex digit hostId required by ZFS.";
|
|
};
|
|
|
|
enableSsh = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable inbound SSH in the NixOS host config. AWS ingress is managed by OpenTofu.";
|
|
};
|
|
|
|
secrets = {
|
|
provider = lib.mkOption {
|
|
type = lib.types.enum [ "files" "aws-ssm" ];
|
|
default = "files";
|
|
description = "Secret backend used to materialize Quixos runtime credentials.";
|
|
};
|
|
|
|
runtimeDirectory = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/run/quixos/secrets";
|
|
description = "Runtime directory containing materialized secret files.";
|
|
};
|
|
|
|
files = {
|
|
controlSecretPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/secrets/control-secret";
|
|
description = "File backend path for the control secret.";
|
|
};
|
|
|
|
stateContextSecretPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/secrets/state-context-secret";
|
|
description = "File backend path for the state context secret.";
|
|
};
|
|
|
|
packageTreeGitUsernamePath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/secrets/package-tree-git-username";
|
|
description = "File backend path for the package tree Git username.";
|
|
};
|
|
|
|
packageTreeGitPasswordPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/persist-local/secrets/package-tree-git-password";
|
|
description = "File backend path for the package tree Git password.";
|
|
};
|
|
};
|
|
|
|
awsSsm = {
|
|
controlSecretParameter = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "SSM parameter name for the control secret.";
|
|
};
|
|
|
|
stateContextSecretParameter = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "SSM parameter name for the state context secret.";
|
|
};
|
|
|
|
packageTreeGitUsernameParameter = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "SSM parameter name for the package tree Git username.";
|
|
};
|
|
|
|
packageTreeGitPasswordParameter = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "SSM parameter name for the package tree Git password.";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = secretsCfg.provider != "aws-ssm" || (
|
|
secretsCfg.awsSsm.controlSecretParameter != null
|
|
&& secretsCfg.awsSsm.stateContextSecretParameter != null
|
|
&& secretsCfg.awsSsm.packageTreeGitUsernameParameter != null
|
|
&& secretsCfg.awsSsm.packageTreeGitPasswordParameter != null
|
|
);
|
|
message = "services.quixosHost.secrets.awsSsm.*Parameter options must all be set when provider is aws-ssm.";
|
|
}
|
|
];
|
|
|
|
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
|
|
|
networking.hostId = cfg.hostId;
|
|
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
|
boot.supportedFilesystems = [ "zfs" ];
|
|
boot.zfs.extraPools = [ cfg.nixZfsPool ];
|
|
boot.zfs.forceImportRoot = false;
|
|
services.zfs.autoScrub = {
|
|
enable = true;
|
|
pools = [ cfg.nixZfsPool cfg.persistZfsPool cfg.sharedPersistZfsPool ];
|
|
};
|
|
|
|
fileSystems."/nix" = {
|
|
device = "${cfg.nixZfsPool}/nix";
|
|
fsType = "zfs";
|
|
neededForBoot = true;
|
|
};
|
|
|
|
fileSystems."/persist-local" = {
|
|
device = "${cfg.persistZfsPool}/persist";
|
|
fsType = "zfs";
|
|
options =
|
|
[ "defaults" ]
|
|
++ lib.optionals (cfg.persistDevice != null) [
|
|
"x-systemd.requires=quixos-persist-init.service"
|
|
"x-systemd.after=quixos-persist-init.service"
|
|
];
|
|
};
|
|
|
|
fileSystems."/persist" = {
|
|
device = "${cfg.sharedPersistZfsPool}/persist";
|
|
fsType = "zfs";
|
|
options =
|
|
[ "defaults" ]
|
|
++ lib.optionals (cfg.sharedPersistDevice != null) [
|
|
"x-systemd.requires=quixos-shared-persist-init.service"
|
|
"x-systemd.after=quixos-shared-persist-init.service"
|
|
];
|
|
};
|
|
|
|
systemd.services.quixos-persist-init = lib.mkIf (cfg.persistDevice != null) {
|
|
description = "Initialize blank Quixos persistent ZFS volume";
|
|
requiredBy = [ "zfs-import-${cfg.persistZfsPool}.service" ];
|
|
before = [ "zfs-import-${cfg.persistZfsPool}.service" localPersistMount "local-fs.target" ];
|
|
after = [ "nix.mount" ];
|
|
requires = [ "nix.mount" ];
|
|
path = [ pkgs.coreutils pkgs.gnugrep pkgs.util-linux pkgs.zfs ];
|
|
# This unit runs in the early filesystem transaction; default service
|
|
# dependencies would put it after sysinit and create an ordering cycle.
|
|
unitConfig = {
|
|
DefaultDependencies = false;
|
|
};
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -eu
|
|
|
|
device=${lib.escapeShellArg cfg.persistDevice}
|
|
pool=${lib.escapeShellArg cfg.persistZfsPool}
|
|
dataset="$pool/persist"
|
|
|
|
for _ in $(seq 1 600); do
|
|
if [ -e "$device" ]; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [ ! -e "$device" ]; then
|
|
echo "Persist device did not appear: $device" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if zpool list -H "$pool" >/dev/null 2>&1; then
|
|
if ! zfs list -H "$dataset" >/dev/null 2>&1; then
|
|
echo "ZFS pool '$pool' is imported but dataset '$dataset' is missing." >&2
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if zpool import -d /dev/disk/by-id | grep -Eq "^[[:space:]]*pool:[[:space:]]+$pool$"; then
|
|
exit 0
|
|
fi
|
|
|
|
if wipefs --noheadings --output TYPE "$device" | grep -q .; then
|
|
echo "Persist device has existing signatures but pool '$pool' could not be imported: $device" >&2
|
|
wipefs "$device" >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
zpool create \
|
|
-f \
|
|
-o ashift=12 \
|
|
-O acltype=posixacl \
|
|
-O atime=off \
|
|
-O compression=zstd \
|
|
-O mountpoint=none \
|
|
-O xattr=sa \
|
|
"$pool" "$device"
|
|
|
|
zfs create -o mountpoint=legacy "$dataset"
|
|
zpool export "$pool"
|
|
'';
|
|
};
|
|
|
|
systemd.services.quixos-shared-persist-init = lib.mkIf (cfg.sharedPersistDevice != null) {
|
|
description = "Initialize blank Quixos shared ZFS volume";
|
|
requiredBy = [ "zfs-import-${cfg.sharedPersistZfsPool}.service" ];
|
|
before = [ "zfs-import-${cfg.sharedPersistZfsPool}.service" sharedPersistMount "local-fs.target" ];
|
|
after = [ "nix.mount" ];
|
|
requires = [ "nix.mount" ];
|
|
path = [ pkgs.coreutils pkgs.gnugrep pkgs.util-linux pkgs.zfs ];
|
|
unitConfig = {
|
|
DefaultDependencies = false;
|
|
};
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -eu
|
|
|
|
device=${lib.escapeShellArg cfg.sharedPersistDevice}
|
|
pool=${lib.escapeShellArg cfg.sharedPersistZfsPool}
|
|
legacy_pool=quixos-caddy
|
|
dataset="$pool/persist"
|
|
|
|
for _ in $(seq 1 600); do
|
|
if [ -e "$device" ]; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
if [ ! -e "$device" ]; then
|
|
echo "Shared persist device did not appear: $device" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if zpool list -H "$pool" >/dev/null 2>&1; then
|
|
if ! zfs list -H "$dataset" >/dev/null 2>&1; then
|
|
echo "ZFS pool '$pool' is imported but dataset '$dataset' is missing." >&2
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if zpool import -d /dev/disk/by-id | grep -Eq "^[[:space:]]*pool:[[:space:]]+$pool$"; then
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$pool" != "$legacy_pool" ] && zpool import -d /dev/disk/by-id | grep -Eq "^[[:space:]]*pool:[[:space:]]+$legacy_pool$"; then
|
|
zpool import -d /dev/disk/by-id "$legacy_pool" "$pool"
|
|
if ! zfs list -H "$dataset" >/dev/null 2>&1; then
|
|
echo "Imported legacy pool '$legacy_pool' as '$pool', but dataset '$dataset' is missing." >&2
|
|
exit 1
|
|
fi
|
|
zpool export "$pool"
|
|
exit 0
|
|
fi
|
|
|
|
if wipefs --noheadings --output TYPE "$device" | grep -q .; then
|
|
echo "Shared persist device has existing signatures but pool '$pool' could not be imported: $device" >&2
|
|
wipefs "$device" >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
zpool create \
|
|
-f \
|
|
-o ashift=12 \
|
|
-O acltype=posixacl \
|
|
-O atime=off \
|
|
-O compression=zstd \
|
|
-O mountpoint=none \
|
|
-O xattr=sa \
|
|
"$pool" "$device"
|
|
|
|
zfs create -o mountpoint=legacy "$dataset"
|
|
zpool export "$pool"
|
|
'';
|
|
};
|
|
|
|
users.groups.quixos = {};
|
|
users.groups.quixos-control = {};
|
|
users.users.quixos = {
|
|
isSystemUser = true;
|
|
group = "quixos";
|
|
extraGroups = [ "quixos-control" ];
|
|
home = cfg.dataRoot;
|
|
createHome = true;
|
|
shell = pkgs.bashInteractive;
|
|
};
|
|
users.users.caddy.extraGroups = [ "quixos" "quixos-control" ];
|
|
|
|
environment.systemPackages = [
|
|
pkgs.codex
|
|
pkgs.git
|
|
pkgs.nix
|
|
pkgs.ripgrep
|
|
pkgs.amazon-ssm-agent
|
|
qxControl
|
|
];
|
|
programs.bash.shellAliases = {
|
|
nixrb = "sudo nixos-rebuild switch --flake /etc/nixos#quixos-host";
|
|
nixfu = "cd /etc/nixos && sudo nix flake update";
|
|
nixrbu = "cd /etc/nixos && sudo nix flake update && sudo nixos-rebuild switch --flake /etc/nixos#quixos-host";
|
|
};
|
|
environment.variables.QUIXOS_APP_ROOT = "${self}";
|
|
environment.variables.QUIXOS_CONTROL_SOCKET = "/run/quixos/control.sock";
|
|
environment.variables.QUIXOS_PACKAGES_PATH = cfg.packagesPath;
|
|
environment.variables.QUIXOS_PACKAGE_TEMPLATES_DIR = "${self}/package-templates";
|
|
environment.variables.QUIXOS_WORKSPACE_ROOT = "/persist-local/srv/quixos";
|
|
|
|
systemd.sockets.nix-daemon = {
|
|
after = [ "nix.mount" ];
|
|
requires = [ "nix.mount" ];
|
|
};
|
|
|
|
systemd.services.nix-daemon = {
|
|
after = [ "nix.mount" ];
|
|
requires = [ "nix.mount" ];
|
|
};
|
|
|
|
services.amazon-ssm-agent.enable = true;
|
|
|
|
services.openssh = {
|
|
enable = lib.mkForce cfg.enableSsh;
|
|
hostKeys = [
|
|
{
|
|
path = "/persist/etc/ssh/ssh_host_ed25519_key";
|
|
type = "ed25519";
|
|
}
|
|
{
|
|
path = "/persist/etc/ssh/ssh_host_rsa_key";
|
|
type = "rsa";
|
|
bits = 4096;
|
|
}
|
|
];
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"z /persist 0755 root root -"
|
|
"z /persist-local 0755 root root -"
|
|
"d /persist/etc/ssh 0700 root root -"
|
|
"d /persist-local/srv 0755 root root -"
|
|
"d /persist-local/srv/quixos 0755 quixos quixos -"
|
|
"d /persist-local/srv/quixos/packages 0755 quixos quixos -"
|
|
"d /persist-local/var 0755 root root -"
|
|
"d /persist-local/var/lib 0755 root root -"
|
|
"d /persist/var 0755 root root -"
|
|
"d /persist/var/lib 0755 root root -"
|
|
"d /persist/var/lib/quixos 0750 quixos quixos -"
|
|
"d ${quixosCodexDir} 0750 quixos quixos -"
|
|
"d ${quixosCodexDir}/rules 0750 quixos quixos -"
|
|
"d ${cfg.dataRoot} 0750 quixos quixos -"
|
|
"L+ ${cfg.dataRoot}/.codex - - - - ${quixosCodexDir}"
|
|
"f+ ${quixosCodexDir}/rules/quixos.rules 0644 quixos quixos - ${quixosCodexRule}"
|
|
"d /persist/var/lib/caddy 0750 caddy caddy -"
|
|
];
|
|
|
|
systemd.services.quixos-persist-layout = {
|
|
description = "Reconcile Quixos persistent directory layout";
|
|
requiredBy = [
|
|
"quixos-secrets.service"
|
|
"quixos-package-tree.service"
|
|
"quixos-orchestrator.service"
|
|
"quixos-codex-app-server.service"
|
|
"caddy.service"
|
|
];
|
|
before = [
|
|
"quixos-secrets.service"
|
|
"quixos-package-tree.service"
|
|
"quixos-orchestrator.service"
|
|
"quixos-codex-app-server.service"
|
|
"caddy.service"
|
|
];
|
|
after = [ localPersistMount sharedPersistMount ];
|
|
requires = [ localPersistMount sharedPersistMount ];
|
|
path = [ config.systemd.package ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
script = ''
|
|
set -eu
|
|
systemd-tmpfiles --create --prefix=/persist-local --prefix=/persist
|
|
'';
|
|
};
|
|
|
|
systemd.services.quixos-secrets = {
|
|
description = "Materialize Quixos runtime secrets";
|
|
wantedBy = [ "multi-user.target" ];
|
|
before = [ "quixos-package-tree.service" "quixos-orchestrator.service" ];
|
|
after = [ "network-online.target" "quixos-persist-layout.service" ];
|
|
wants = [ "network-online.target" ];
|
|
requires = [ "quixos-persist-layout.service" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
};
|
|
path = [ pkgs.awscli2 pkgs.coreutils ];
|
|
script =
|
|
''
|
|
set -eu
|
|
install -d -m 0750 -o root -g quixos-control ${lib.escapeShellArg secretsCfg.runtimeDirectory}
|
|
''
|
|
+ (
|
|
if secretsCfg.provider == "aws-ssm" then ''
|
|
${fetchAwsSsmParameter secretsCfg.awsSsm.controlSecretParameter secretFiles.controlSecret}
|
|
${fetchAwsSsmParameter secretsCfg.awsSsm.stateContextSecretParameter secretFiles.stateContextSecret}
|
|
${fetchAwsSsmParameter secretsCfg.awsSsm.packageTreeGitUsernameParameter secretFiles.packageTreeGitUsername}
|
|
${fetchAwsSsmParameter secretsCfg.awsSsm.packageTreeGitPasswordParameter secretFiles.packageTreeGitPassword}
|
|
'' else ''
|
|
${installFileSecret secretsCfg.files.controlSecretPath secretFiles.controlSecret}
|
|
${installFileSecret secretsCfg.files.stateContextSecretPath secretFiles.stateContextSecret}
|
|
${installFileSecret secretsCfg.files.packageTreeGitUsernamePath secretFiles.packageTreeGitUsername}
|
|
${installFileSecret secretsCfg.files.packageTreeGitPasswordPath secretFiles.packageTreeGitPassword}
|
|
''
|
|
);
|
|
};
|
|
|
|
systemd.services.quixos-package-tree = {
|
|
description = "Prepare Quixos package tree";
|
|
wantedBy = [ "multi-user.target" ];
|
|
before = [ "quixos-orchestrator.service" ];
|
|
after = [ "network-online.target" "quixos-persist-layout.service" "quixos-secrets.service" ];
|
|
wants = [ "network-online.target" "quixos-secrets.service" ];
|
|
requires = [ "quixos-persist-layout.service" "quixos-secrets.service" ];
|
|
path = [ pkgs.git ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "quixos";
|
|
Group = "quixos";
|
|
SupplementaryGroups = [ "quixos-control" ];
|
|
RemainAfterExit = true;
|
|
};
|
|
environment = {
|
|
GIT_ASKPASS = packageTreeGitAskPass;
|
|
GIT_TERMINAL_PROMPT = "0";
|
|
};
|
|
script = ''
|
|
set -eu
|
|
fetch_package_tree() {
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} fetch --prune origin \
|
|
'+refs/heads/*:refs/remotes/origin/*' \
|
|
'+refs/tags/*:refs/tags/*'
|
|
}
|
|
resolve_package_tree_ref() {
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} rev-parse --verify ${lib.escapeShellArg cfg.packageTreeRef}^{commit} 2>/dev/null \
|
|
|| git -C ${lib.escapeShellArg cfg.packagesPath} rev-parse --verify refs/remotes/origin/${lib.escapeShellArg cfg.packageTreeRef}^{commit} 2>/dev/null \
|
|
|| git -C ${lib.escapeShellArg cfg.packagesPath} rev-parse --verify refs/tags/${lib.escapeShellArg cfg.packageTreeRef}^{commit} 2>/dev/null \
|
|
|| true
|
|
}
|
|
|
|
if [ ! -d ${lib.escapeShellArg cfg.packagesPath}/.git ]; then
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} init
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} remote add origin ${lib.escapeShellArg cfg.packageTreeRepo}
|
|
fetch_package_tree
|
|
else
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} remote set-url origin ${lib.escapeShellArg cfg.packageTreeRepo}
|
|
fi
|
|
|
|
current_ref="$(git -C ${lib.escapeShellArg cfg.packagesPath} rev-parse HEAD 2>/dev/null || true)"
|
|
target_ref="$(resolve_package_tree_ref)"
|
|
|
|
if [ -z "$target_ref" ]; then
|
|
fetch_package_tree
|
|
target_ref="$(resolve_package_tree_ref)"
|
|
fi
|
|
|
|
if [ -z "$target_ref" ]; then
|
|
echo "Package-tree ref is not available from ${lib.escapeShellArg cfg.packageTreeRepo}: ${lib.escapeShellArg cfg.packageTreeRef}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$current_ref" != "$target_ref" ] || ${if cfg.forcePackageTreeRefOnBoot then "true" else "false"}; then
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} checkout --detach "$target_ref"
|
|
fi
|
|
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} submodule sync --recursive
|
|
git -C ${lib.escapeShellArg cfg.packagesPath} submodule update --init --recursive
|
|
'';
|
|
};
|
|
|
|
systemd.services.quixos-orchestrator = {
|
|
description = "Quixos orchestrator";
|
|
wantedBy = [ "multi-user.target" ];
|
|
upheldBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" "nix-daemon.socket" "quixos-persist-layout.service" "quixos-secrets.service" "quixos-package-tree.service" ];
|
|
wants = [ "network-online.target" "nix-daemon.socket" "quixos-secrets.service" "quixos-package-tree.service" ];
|
|
requires = [ "quixos-persist-layout.service" "quixos-secrets.service" "quixos-package-tree.service" ];
|
|
path = [ qxControl ];
|
|
serviceConfig = {
|
|
User = "quixos";
|
|
Group = "quixos-control";
|
|
SupplementaryGroups = [ "quixos" ];
|
|
RuntimeDirectory = "quixos";
|
|
RuntimeDirectoryMode = "0770";
|
|
WorkingDirectory = "/persist-local/srv/quixos";
|
|
Restart = "always";
|
|
RestartSec = "5s";
|
|
};
|
|
script = ''
|
|
set -eu
|
|
QUIXOS_CONTROL_SECRET="$(cat ${lib.escapeShellArg secretFiles.controlSecret})"
|
|
export QUIXOS_CONTROL_SECRET
|
|
QUIXOS_STATE_CONTEXT_SECRET="$(cat ${lib.escapeShellArg secretFiles.stateContextSecret})"
|
|
export QUIXOS_STATE_CONTEXT_SECRET
|
|
exec ${quixosPackages.quixos-orchestrator}/bin/quixos-orchestrator --packages-path ${lib.escapeShellArg cfg.packagesPath}
|
|
'';
|
|
environment = {
|
|
GIT_ASKPASS = packageTreeGitAskPass;
|
|
GIT_TERMINAL_PROMPT = "0";
|
|
GIT_AUTHOR_NAME = "Quixos Runtime";
|
|
GIT_AUTHOR_EMAIL = "quixos-runtime@localhost";
|
|
GIT_COMMITTER_NAME = "Quixos Runtime";
|
|
GIT_COMMITTER_EMAIL = "quixos-runtime@localhost";
|
|
XDG_DATA_HOME = cfg.dataRoot;
|
|
QUIXOS_WORKSPACE_ROOT = "/persist-local/srv/quixos";
|
|
QUIXOS_APP_ROOT = "${self}";
|
|
QUIXOS_PACKAGES_PATH = cfg.packagesPath;
|
|
QUIXOS_PACKAGE_TEMPLATES_DIR = "${self}/package-templates";
|
|
QUIXOS_CONTROL_SOCKET = "/run/quixos/control.sock";
|
|
QUIXOS_DEVICE_ASSIGNMENTS_FILE = "${cfg.packagesPath}/device-assignments.json";
|
|
QUIXOS_REMOTE_DOM_RELAY_SOCKET = "/run/quixos/relay.sock";
|
|
QUIXOS_REMOTE_DOM_RELAY_PUBLIC_WS_URL = "wss://${cfg.domain}/relay/";
|
|
QUIXOS_REMOTE_DOM_RELAY_PUBLIC_HTTP_ORIGIN = "https://${cfg.domain}/relay";
|
|
};
|
|
};
|
|
|
|
systemd.services.quixos-codex-app-server = {
|
|
description = "Codex app server";
|
|
wantedBy = [ "multi-user.target" ];
|
|
upheldBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" "quixos-persist-layout.service" "quixos-package-tree.service" ];
|
|
wants = [ "network-online.target" "quixos-package-tree.service" ];
|
|
requires = [ "quixos-persist-layout.service" "quixos-package-tree.service" ];
|
|
path = [
|
|
pkgs.codex
|
|
pkgs.git
|
|
pkgs.nix
|
|
pkgs.ripgrep
|
|
qxControl
|
|
];
|
|
serviceConfig = {
|
|
User = "quixos";
|
|
Group = "quixos";
|
|
SupplementaryGroups = [ "quixos-control" ];
|
|
WorkingDirectory = cfg.packagesPath;
|
|
Restart = "always";
|
|
RestartSec = "5s";
|
|
};
|
|
environment = {
|
|
HOME = cfg.dataRoot;
|
|
CODEX_HOME = quixosCodexDir;
|
|
XDG_DATA_HOME = cfg.dataRoot;
|
|
QUIXOS_APP_ROOT = "${self}";
|
|
QUIXOS_PACKAGES_PATH = cfg.packagesPath;
|
|
QUIXOS_PACKAGE_TEMPLATES_DIR = "${self}/package-templates";
|
|
QUIXOS_WORKSPACE_ROOT = "/persist-local/srv/quixos";
|
|
QUIXOS_CONTROL_SOCKET = "/run/quixos/control.sock";
|
|
};
|
|
script = ''
|
|
exec codex app-server --listen ws://127.0.0.1:4500
|
|
'';
|
|
};
|
|
|
|
services.caddy = {
|
|
enable = true;
|
|
dataDir = "/persist/var/lib/caddy";
|
|
virtualHosts.${cfg.domain}.extraConfig = ''
|
|
handle_path /control/* {
|
|
reverse_proxy unix//run/quixos/control.sock
|
|
}
|
|
|
|
handle_path /relay/* {
|
|
reverse_proxy unix//run/quixos/relay.sock {
|
|
header_up X-Forwarded-Prefix /relay
|
|
}
|
|
}
|
|
|
|
handle {
|
|
root * ${quixosPackages.project-visualizer-static}
|
|
file_server
|
|
}
|
|
'';
|
|
};
|
|
|
|
systemd.services.caddy = {
|
|
upheldBy = [ "multi-user.target" ];
|
|
after = [ "quixos-persist-layout.service" ];
|
|
requires = [ "quixos-persist-layout.service" ];
|
|
};
|
|
};
|
|
}
|