Compare commits
2 Commits
0662308bde
...
104da32e4b
| Author | SHA1 | Date | |
|---|---|---|---|
| 104da32e4b | |||
| 7be45ac44c |
@@ -50,7 +50,12 @@
|
||||
directories already exist inside the read-only source.
|
||||
For aggregate Copyparty volumes such as `/srv` with nested `/srv/inbox`, keep the guest bind mount
|
||||
writable and enforce read-only behavior through Copyparty access plus service `ReadWritePaths`.
|
||||
- Pikachu deploys from the monorepo checkout copied to `~/dev`.
|
||||
Do not use the old `~/wrk/dotfiles` path for remote rebuild commands.
|
||||
- `deploy-to` builds from the source checkout and activates an imported closure on the target.
|
||||
Do not rely on a target-local monorepo checkout for remote deployment.
|
||||
- On Pikachu, `nix copy --to ssh://pikachu` accepted a small `nix store add-file` probe,
|
||||
but rejected locally built system paths that lacked trusted signatures.
|
||||
Use a sudo `nix-store --import` path for unsigned deploy closures.
|
||||
- Public DNS entries are not managed through the NixOS configuration.
|
||||
Do not describe NixOS route choices as public-DNS management.
|
||||
- Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine.
|
||||
Generate it on the target system or live ISO, then stage it before evaluating the flake.
|
||||
|
||||
@@ -25,6 +25,14 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
registry.homepage.${config.networking.hostName}.actualbudget = {
|
||||
tab = "Apps";
|
||||
section = "Other";
|
||||
name = "Actual Budget";
|
||||
description = "Finance";
|
||||
icon = "actual-budget.png";
|
||||
};
|
||||
|
||||
guests.actualbudget = {
|
||||
interior = {
|
||||
modules.services.actualbudget = {
|
||||
|
||||
@@ -90,6 +90,15 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
registry.homepage.${config.networking.hostName}.copyparty = {
|
||||
tab = "Apps";
|
||||
section = "Files";
|
||||
name = "Copyparty";
|
||||
description = "Files";
|
||||
icon = "copyparty.svg";
|
||||
href = lib.mkIf cfg.reverseProxy.enable "https://${cfg.reverseProxy.route.domain}/?h";
|
||||
};
|
||||
|
||||
guests.copyparty = {
|
||||
interior.modules.services.copyparty = {
|
||||
enable = true;
|
||||
|
||||
32
configs/nixos/guests/homepage.nix
Normal file
32
configs/nixos/guests/homepage.nix
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.guests.homepage;
|
||||
in
|
||||
{
|
||||
options.guests.homepage = {
|
||||
allowedHosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "home" ];
|
||||
description = "Host headers accepted by Homepage.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
guests.homepage = {
|
||||
interior.modules.services.homepage = {
|
||||
enable = true;
|
||||
inherit (cfg) allowedHosts;
|
||||
};
|
||||
|
||||
limits = {
|
||||
memory = lib.mkDefault "1G";
|
||||
cpu = lib.mkDefault "100%";
|
||||
tasksMax = lib.mkDefault 256;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -48,6 +48,14 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
registry.homepage.${config.networking.hostName}.navidrome = {
|
||||
tab = "Apps";
|
||||
section = "Media";
|
||||
name = "Navidrome";
|
||||
description = "Music";
|
||||
icon = "navidrome.png";
|
||||
};
|
||||
|
||||
guests.navidrome = {
|
||||
interior.modules.services.navidrome.enable = true;
|
||||
|
||||
|
||||
@@ -143,6 +143,21 @@
|
||||
address = "10.23.20.109";
|
||||
};
|
||||
|
||||
guests.homepage = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
address = "10.23.20.46";
|
||||
reverseProxy = {
|
||||
enable = true;
|
||||
route = {
|
||||
domain = "home";
|
||||
forceSSL = false;
|
||||
enableACME = false;
|
||||
port = 8082;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
guests.copyparty = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
|
||||
135
configs/nixos/modules/services/homepage.nix
Normal file
135
configs/nixos/modules/services/homepage.nix
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
registry,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.modules.services.homepage;
|
||||
|
||||
mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}";
|
||||
|
||||
homepageEntries = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
host: entries:
|
||||
lib.mapAttrsToList (entry: homepage: {
|
||||
inherit host entry homepage;
|
||||
composite = registry.join host entry;
|
||||
}) entries
|
||||
) registry.homepage
|
||||
);
|
||||
|
||||
mkHref = item:
|
||||
if item.homepage.href != null then
|
||||
item.homepage.href
|
||||
else if (item.composite.route or null) != null && item.composite.route.port != null then
|
||||
let
|
||||
scheme = if item.composite.route.forceSSL then "https" else "http";
|
||||
in
|
||||
"${scheme}://${item.composite.route.domain}"
|
||||
else
|
||||
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching TCP route entry";
|
||||
|
||||
mkServiceAttrs = item:
|
||||
lib.filterAttrs (_: value: value != null) {
|
||||
href = mkHref item;
|
||||
inherit (item.homepage) description icon;
|
||||
};
|
||||
|
||||
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
|
||||
|
||||
insertEntry = acc: item:
|
||||
acc
|
||||
// {
|
||||
${item.homepage.tab} = (acc.${item.homepage.tab} or { })
|
||||
// {
|
||||
${item.homepage.section} = (acc.${item.homepage.tab}.${item.homepage.section} or [ ]) ++ [
|
||||
{ ${item.homepage.name} = mkServiceAttrs item; }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
|
||||
|
||||
tabOrder = [
|
||||
"Apps"
|
||||
"Download"
|
||||
"System"
|
||||
];
|
||||
|
||||
orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab);
|
||||
|
||||
mkTab = tab:
|
||||
let
|
||||
sections = entriesByTab.${tab};
|
||||
in
|
||||
{
|
||||
${tab} = map (section: { ${section} = sections.${section}; }) (lib.attrNames sections);
|
||||
};
|
||||
|
||||
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
|
||||
in
|
||||
{
|
||||
options.modules.services.homepage = {
|
||||
enable = lib.mkEnableOption "Homepage Dashboard";
|
||||
|
||||
listenPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8082;
|
||||
description = "Port for Homepage to bind to.";
|
||||
};
|
||||
|
||||
allowedHosts = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "home" ];
|
||||
description = "Host headers accepted by Homepage.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.homepage-dashboard = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
inherit (cfg) listenPort;
|
||||
allowedHosts = lib.concatStringsSep "," cfg.allowedHosts;
|
||||
services = homepageServices;
|
||||
settings = {
|
||||
title = "Tracion";
|
||||
theme = "dark";
|
||||
color = "slate";
|
||||
showStats = false;
|
||||
hideVersion = true;
|
||||
bookmarksStyle = "icons";
|
||||
useEqualHeights = true;
|
||||
layout = {
|
||||
Apps = {
|
||||
style = "row";
|
||||
columns = 1;
|
||||
tab = "Apps";
|
||||
Media = {
|
||||
style = "row";
|
||||
columns = 2;
|
||||
};
|
||||
Files = {
|
||||
style = "row";
|
||||
columns = 2;
|
||||
};
|
||||
Other = {
|
||||
style = "row";
|
||||
columns = 3;
|
||||
iconsOnly = true;
|
||||
};
|
||||
};
|
||||
Download = {
|
||||
style = "row";
|
||||
columns = 1;
|
||||
tab = "Download";
|
||||
};
|
||||
System = {
|
||||
tab = "System";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -21,8 +21,7 @@ let
|
||||
inherit host entry;
|
||||
name = route.domain;
|
||||
value = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
inherit (route) forceSSL enableACME;
|
||||
locations."/".proxyPass = mkBackend address route;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -99,7 +99,7 @@ in
|
||||
};
|
||||
|
||||
deploy-to = {
|
||||
description = "Deploy the current dotfiles branch to a host through target-local nixos-rebuild";
|
||||
description = "Build the current checkout locally, import it on a host, and activate it";
|
||||
body = builtins.readFile ./functions/deploy-to.fish;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
if test (count $argv) -lt 1; or test (count $argv) -gt 2
|
||||
echo "usage: deploy-to <host> [branch]" >&2
|
||||
if test (count $argv) -ne 1
|
||||
echo "usage: deploy-to <host>" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
set host $argv[1]
|
||||
set branch $argv[2]
|
||||
set branch_source explicit
|
||||
|
||||
if test -z "$branch"
|
||||
set branch_source current
|
||||
set branch (git branch --show-current)
|
||||
end
|
||||
|
||||
git rev-parse --show-toplevel >/dev/null 2>&1
|
||||
or begin
|
||||
@@ -18,69 +11,69 @@ or begin
|
||||
return 2
|
||||
end
|
||||
|
||||
git fetch origin
|
||||
set repository_root (git rev-parse --show-toplevel)
|
||||
or return $status
|
||||
|
||||
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
|
||||
if test "$branch_source" = explicit
|
||||
echo "deploy-to: origin/$branch does not exist" >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set branch_source upstream
|
||||
set upstream_ref (git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)
|
||||
set branch (string replace -r '^origin/' '' -- $upstream_ref)
|
||||
set untracked_nix_files (git -C $repository_root ls-files --others --exclude-standard -- '*.nix' flake.nix flake.lock)
|
||||
if test (count $untracked_nix_files) -gt 0
|
||||
echo "deploy-to: warning: untracked Nix files are not part of Git flake evaluation" >&2
|
||||
printf ' %s\n' $untracked_nix_files >&2
|
||||
end
|
||||
|
||||
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
|
||||
set branch_source default
|
||||
set origin_head (git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)
|
||||
set branch (string replace -r '^origin/' '' -- $origin_head)
|
||||
end
|
||||
set installable "$repository_root#nixosConfigurations.$host.config.system.build.toplevel"
|
||||
|
||||
if test -z "$branch"
|
||||
echo "deploy-to: could not determine a deploy branch" >&2
|
||||
return 2
|
||||
end
|
||||
|
||||
set remote_ref refs/remotes/origin/$branch
|
||||
git rev-parse --verify --quiet $remote_ref >/dev/null
|
||||
echo "deploy-to: building $installable" >&2
|
||||
set system_path (nix build --no-link --print-out-paths $installable)
|
||||
or begin
|
||||
echo "deploy-to: origin/$branch does not exist" >&2
|
||||
echo "deploy-to: build failed for $host" >&2
|
||||
return $status
|
||||
end
|
||||
|
||||
if test (count $system_path) -ne 1
|
||||
echo "deploy-to: expected one build output, got "(count $system_path) >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
set has_local_changes false
|
||||
if not git diff --quiet $remote_ref --
|
||||
set has_local_changes true
|
||||
set nar_file (mktemp --suffix=.nar)
|
||||
or return $status
|
||||
set remote_nar "/tmp/deploy-to-$host-"(date +%s)"-$fish_pid.nar"
|
||||
|
||||
set closure_paths (nix-store -qR $system_path)
|
||||
or begin
|
||||
set query_status $status
|
||||
rm -f $nar_file
|
||||
echo "deploy-to: closure query failed for $host" >&2
|
||||
return $query_status
|
||||
end
|
||||
|
||||
set escaped_branch (string escape -- $branch)
|
||||
set escaped_host (string escape -- $host)
|
||||
set remote_command "cd ~/dev && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && git clean -fd"
|
||||
|
||||
set patch_file ""
|
||||
set remote_patch_file ""
|
||||
if test "$has_local_changes" = true
|
||||
set patch_file (mktemp)
|
||||
set remote_patch_file "/tmp/deploy-to-$host.patch"
|
||||
git diff --binary $remote_ref -- >$patch_file
|
||||
or begin
|
||||
rm -f $patch_file
|
||||
return $status
|
||||
end
|
||||
|
||||
scp $patch_file $host:$remote_patch_file
|
||||
set scp_status $status
|
||||
rm -f $patch_file
|
||||
if test $scp_status -ne 0
|
||||
return $scp_status
|
||||
end
|
||||
|
||||
set escaped_remote_patch_file (string escape -- $remote_patch_file)
|
||||
set remote_command "$remote_command && git apply --index $escaped_remote_patch_file && rm -f $escaped_remote_patch_file"
|
||||
echo "deploy-to: exporting closure for $system_path" >&2
|
||||
nix-store --export $closure_paths >$nar_file
|
||||
or begin
|
||||
set export_status $status
|
||||
rm -f $nar_file
|
||||
echo "deploy-to: closure export failed for $host" >&2
|
||||
return $export_status
|
||||
end
|
||||
|
||||
set remote_command "$remote_command && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
|
||||
echo "deploy-to: copying closure archive to $host:$remote_nar" >&2
|
||||
scp $nar_file $host:$remote_nar
|
||||
set scp_status $status
|
||||
rm -f $nar_file
|
||||
if test $scp_status -ne 0
|
||||
echo "deploy-to: closure archive copy failed for $host" >&2
|
||||
return $scp_status
|
||||
end
|
||||
|
||||
ssh -t $host $remote_command
|
||||
echo "deploy-to: importing closure on $host" >&2
|
||||
ssh -t $host "sudo sh -c 'nix-store --import < $remote_nar' && rm -f $remote_nar"
|
||||
or begin
|
||||
echo "deploy-to: closure import failed for $host" >&2
|
||||
return $status
|
||||
end
|
||||
|
||||
echo "deploy-to: activating $system_path on $host" >&2
|
||||
ssh -t $host sudo "$system_path/bin/switch-to-configuration" switch
|
||||
or begin
|
||||
echo "deploy-to: activation failed for $host" >&2
|
||||
return $status
|
||||
end
|
||||
|
||||
45
configs/nixos/registry/homepage.nix
Normal file
45
configs/nixos/registry/homepage.nix
Normal file
@@ -0,0 +1,45 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
tab = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "Apps";
|
||||
description = "Homepage tab that contains this entry.";
|
||||
};
|
||||
|
||||
section = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "Media";
|
||||
description = "Homepage section that contains this entry.";
|
||||
};
|
||||
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "Navidrome";
|
||||
description = "Homepage display name for this entry.";
|
||||
};
|
||||
|
||||
description = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "Music";
|
||||
description = "Homepage description for this entry.";
|
||||
};
|
||||
|
||||
icon = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "navidrome.png";
|
||||
description = "Homepage icon for this entry.";
|
||||
};
|
||||
|
||||
href = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "https://music.alexion.dev";
|
||||
description = "Homepage link override for this entry.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,19 @@
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "files.alexion.dev";
|
||||
description = "Public domain served by this route entry.";
|
||||
description = "Domain served by this route entry.";
|
||||
};
|
||||
|
||||
forceSSL = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Redirect HTTP requests to HTTPS for this route.";
|
||||
};
|
||||
|
||||
enableACME = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Enable ACME certificate management for this route.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
|
||||
@@ -50,10 +50,8 @@
|
||||
- immich
|
||||
- romm
|
||||
- gitea
|
||||
- vikunja (?)
|
||||
- nginxproxymanager (gradual migration to nginx guest)
|
||||
- adguard
|
||||
- homepage
|
||||
|
||||
## raichu proxmox -> nixos
|
||||
- dwn torrent (vpn)
|
||||
|
||||
Reference in New Issue
Block a user