initial commit
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
{
|
||||
name,
|
||||
hostInfo,
|
||||
dsl,
|
||||
pkgs,
|
||||
lib,
|
||||
containers,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer;
|
||||
inherit (hostInfo.paths) data;
|
||||
|
||||
nodePort = 9100;
|
||||
cadvPort = 9200;
|
||||
|
||||
containerNetworkRulesFile =
|
||||
let
|
||||
mkRules =
|
||||
metric: record:
|
||||
lib.mapAttrsToList (cname: c: {
|
||||
inherit record;
|
||||
expr = "rate(${metric}{device=\"${c.iface}\"}[5m])";
|
||||
labels = {
|
||||
container = cname;
|
||||
};
|
||||
}) containers;
|
||||
in
|
||||
pkgs.writeText "container-network.yml" (
|
||||
builtins.toJSON {
|
||||
groups = [
|
||||
{
|
||||
name = "container-network";
|
||||
rules =
|
||||
mkRules "node_network_receive_bytes_total" "container_network_receive_rate"
|
||||
++ mkRules "node_network_transmit_bytes_total" "container_network_transmit_rate";
|
||||
}
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
containerDefinedRulesFile = pkgs.writeText "container-defined.yml" (
|
||||
builtins.toJSON {
|
||||
groups = [
|
||||
{
|
||||
name = "container-defined";
|
||||
rules = lib.mapAttrsToList (cname: _: {
|
||||
record = "container_defined";
|
||||
expr = "1";
|
||||
labels = {
|
||||
container = cname;
|
||||
};
|
||||
}) containers;
|
||||
}
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
alloyConfig = pkgs.writeText "config.alloy" ''
|
||||
loki.source.journal "journal" {
|
||||
max_age = "12h"
|
||||
path = "/var/log/journal"
|
||||
labels = { job = "systemd-journal" }
|
||||
relabel_rules = loki.relabel.journal.rules
|
||||
forward_to = [loki.write.local.receiver]
|
||||
}
|
||||
|
||||
loki.relabel "journal" {
|
||||
forward_to = []
|
||||
|
||||
rule {
|
||||
source_labels = ["__journal__systemd_unit"]
|
||||
target_label = "unit"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__journal__hostname"]
|
||||
target_label = "host"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__journal_syslog_identifier"]
|
||||
target_label = "syslog_id"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__journal_priority_keyword"]
|
||||
target_label = "level"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__journal__systemd_machine_name"]
|
||||
target_label = "container"
|
||||
}
|
||||
}
|
||||
|
||||
loki.write "local" {
|
||||
endpoint {
|
||||
url = "http://127.0.0.1:3100/loki/api/v1/push"
|
||||
}
|
||||
}
|
||||
'';
|
||||
|
||||
dashboards = lib.filterAttrs (name: _: lib.hasSuffix "-dashboard" name) (
|
||||
dsl.listMap ./overseer (_: path: import path { inherit pkgs lib; })
|
||||
);
|
||||
|
||||
dashboardDir = pkgs.linkFarm "grafana-dashboards" (
|
||||
lib.mapAttrsToList (name: dashboard: {
|
||||
name = "${lib.removeSuffix "-dashboard" name}.json";
|
||||
path = dashboard;
|
||||
}) dashboards
|
||||
);
|
||||
|
||||
landingRoot = pkgs.writeTextDir "index.html" ''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Overseer</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; max-width: 480px; margin: 4rem auto; }
|
||||
ul { padding: 0; list-style: none; }
|
||||
li { margin: .5rem 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Overseer</h1>
|
||||
<ul>
|
||||
<li><a data-host="stats.localhost">stats.localhost</a> — Grafana</li>
|
||||
<li><a data-host="prometheus.localhost">prometheus.localhost</a> — Prometheus</li>
|
||||
</ul>
|
||||
<script>
|
||||
const p = location.port && location.port !== "80" ? ":" + location.port : "";
|
||||
document.querySelectorAll("a[data-host]").forEach(a => {
|
||||
const h = a.dataset.host;
|
||||
a.href = "http://" + h + p;
|
||||
a.textContent = h + p;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'';
|
||||
in
|
||||
let
|
||||
c =
|
||||
mkContainer {
|
||||
inherit name;
|
||||
net.access = [
|
||||
{
|
||||
to = "@host";
|
||||
tcp = [
|
||||
nodePort
|
||||
cadvPort
|
||||
];
|
||||
}
|
||||
];
|
||||
svc.autoStart = true;
|
||||
resources.fs = {
|
||||
"/var/lib/loki" = {
|
||||
hostPath = "${data}/overseer/loki/data";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"/var/lib/prometheus2" = {
|
||||
hostPath = "${data}/overseer/prometheus/data";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"/var/lib/grafana" = {
|
||||
hostPath = "${data}/overseer/grafana";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"/var/log/journal" = {
|
||||
hostPath = "/var/log/journal";
|
||||
isReadOnly = true;
|
||||
};
|
||||
};
|
||||
containerConfig = _: {
|
||||
services.loki = {
|
||||
enable = true;
|
||||
configuration = {
|
||||
auth_enabled = false;
|
||||
server = {
|
||||
http_listen_port = 3100;
|
||||
grpc_listen_port = 9096;
|
||||
};
|
||||
common = {
|
||||
instance_addr = "127.0.0.1";
|
||||
path_prefix = "/var/lib/loki";
|
||||
storage.filesystem = {
|
||||
chunks_directory = "/var/lib/loki/chunks";
|
||||
rules_directory = "/var/lib/loki/rules";
|
||||
};
|
||||
replication_factor = 1;
|
||||
ring.kvstore.store = "inmemory";
|
||||
};
|
||||
schema_config.configs = [
|
||||
{
|
||||
from = "2020-10-24";
|
||||
store = "tsdb";
|
||||
object_store = "filesystem";
|
||||
schema = "v13";
|
||||
index = {
|
||||
prefix = "index_";
|
||||
period = "24h";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
services.alloy = {
|
||||
enable = true;
|
||||
configPath = alloyConfig;
|
||||
};
|
||||
|
||||
services.prometheus = {
|
||||
enable = true;
|
||||
port = 9090;
|
||||
globalConfig.scrape_interval = "15s";
|
||||
scrapeConfigs = [
|
||||
{
|
||||
job_name = "node";
|
||||
static_configs = [ { targets = [ "${hostAddr}:${toString nodePort}" ]; } ];
|
||||
}
|
||||
{
|
||||
job_name = "cadvisor";
|
||||
static_configs = [ { targets = [ "${hostAddr}:${toString cadvPort}" ]; } ];
|
||||
}
|
||||
{
|
||||
job_name = "prometheus";
|
||||
static_configs = [ { targets = [ "localhost:9090" ]; } ];
|
||||
}
|
||||
];
|
||||
ruleFiles = [
|
||||
containerNetworkRulesFile
|
||||
containerDefinedRulesFile
|
||||
(dsl.mkContainerLimitsRuleFile pkgs containers)
|
||||
];
|
||||
rules = [
|
||||
''
|
||||
groups:
|
||||
- name: containers
|
||||
rules:
|
||||
- alert: ContainerFailed
|
||||
expr: node_systemd_unit_state{name=~"container@.*",state="failed"} == 1
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "{{ $labels.name }} entered failed state"
|
||||
- alert: ContainerOOMKill
|
||||
expr: increase(container_oom_events_total{id=~"/machine.slice/container@.+\\.service.*"}[5m]) > 0
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "OOM kill in {{ $labels.id }}"
|
||||
''
|
||||
];
|
||||
};
|
||||
|
||||
services.grafana = {
|
||||
enable = true;
|
||||
dataDir = "/var/lib/grafana";
|
||||
settings = {
|
||||
server.http_port = 3000;
|
||||
server.http_addr = "127.0.0.1";
|
||||
# WORKAROUND: Grafana 10+ requires an explicit secret_key; omitting it
|
||||
# causes auth session tokens to be regenerated on every restart
|
||||
security.secret_key = "SW2YcwTIb9zpOOhoPsMm";
|
||||
"auth".disable_login_form = true;
|
||||
"auth.anonymous" = {
|
||||
enabled = true;
|
||||
org_role = "Admin";
|
||||
};
|
||||
};
|
||||
provision = {
|
||||
enable = true;
|
||||
datasources.settings = {
|
||||
apiVersion = 1;
|
||||
datasources = [
|
||||
{
|
||||
name = "Prometheus";
|
||||
type = "prometheus";
|
||||
uid = "prometheus";
|
||||
access = "proxy";
|
||||
url = "http://localhost:9090";
|
||||
isDefault = true;
|
||||
jsonData.timeInterval = "15s";
|
||||
}
|
||||
{
|
||||
name = "Loki";
|
||||
type = "loki";
|
||||
uid = "loki";
|
||||
access = "proxy";
|
||||
url = "http://127.0.0.1:3100";
|
||||
}
|
||||
];
|
||||
};
|
||||
dashboards.settings = {
|
||||
apiVersion = 1;
|
||||
providers = [
|
||||
{
|
||||
name = "default";
|
||||
type = "file";
|
||||
disableDeletion = true;
|
||||
options.path = toString dashboardDir;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."_" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = 80;
|
||||
ssl = false;
|
||||
extraParameters = [ "default_server" ];
|
||||
}
|
||||
];
|
||||
root = "${landingRoot}";
|
||||
};
|
||||
virtualHosts."stats.localhost" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = 80;
|
||||
ssl = false;
|
||||
}
|
||||
];
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:3000";
|
||||
proxyWebsockets = true;
|
||||
extraConfig = "proxy_set_header Host $host;";
|
||||
};
|
||||
};
|
||||
virtualHosts."prometheus.localhost" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = 80;
|
||||
ssl = false;
|
||||
}
|
||||
];
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:9090";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services = lib.genAttrs [ "loki" "alloy" "prometheus" "grafana" "nginx" ] (_: {
|
||||
serviceConfig.FailureAction = "exit-force";
|
||||
});
|
||||
};
|
||||
net.expose.tcp = [ 80 ];
|
||||
resources.mem = "2G";
|
||||
}
|
||||
// {
|
||||
hostConfig = {
|
||||
services.prometheus.exporters.node = {
|
||||
enable = true;
|
||||
listenAddress = "0.0.0.0";
|
||||
port = nodePort;
|
||||
enabledCollectors = [
|
||||
"systemd"
|
||||
"processes"
|
||||
];
|
||||
disabledCollectors = [
|
||||
"netstat"
|
||||
"softnet"
|
||||
];
|
||||
# radeon sometimes breaks on kexec
|
||||
extraFlags = [
|
||||
"--collector.systemd.enable-task-metrics"
|
||||
"--collector.hwmon.chip-exclude=radeon"
|
||||
];
|
||||
};
|
||||
services.cadvisor = {
|
||||
enable = true;
|
||||
listenAddress = "0.0.0.0";
|
||||
port = cadvPort;
|
||||
};
|
||||
};
|
||||
};
|
||||
hostAddr = c.hostAddr;
|
||||
in
|
||||
c
|
||||
@@ -0,0 +1,61 @@
|
||||
{ pkgs, lib }:
|
||||
let
|
||||
inherit (import ./helpers.nix { inherit pkgs lib; }) mkRow loki mkDashboard;
|
||||
|
||||
mkLogs =
|
||||
{
|
||||
title,
|
||||
expr,
|
||||
w ? 24,
|
||||
h ? 10,
|
||||
}:
|
||||
{
|
||||
inherit title;
|
||||
type = "logs";
|
||||
datasource = loki;
|
||||
gridPos = { inherit w h; };
|
||||
options = {
|
||||
showTime = true;
|
||||
showLabels = true;
|
||||
sortOrder = "Descending";
|
||||
wrapLogMessage = true;
|
||||
enableLogDetails = true;
|
||||
};
|
||||
targets = [
|
||||
{
|
||||
datasource = loki;
|
||||
inherit expr;
|
||||
}
|
||||
];
|
||||
};
|
||||
in
|
||||
mkDashboard {
|
||||
name = "access-logs.json";
|
||||
title = "Access Logs";
|
||||
uid = "access-logs";
|
||||
from = "now-6h";
|
||||
panels = [
|
||||
(mkRow "HTTP")
|
||||
(mkLogs {
|
||||
title = "nginx (access + error)";
|
||||
expr = ''{syslog_id="nginx"}'';
|
||||
})
|
||||
(mkRow "SSH")
|
||||
(mkLogs {
|
||||
title = "sshpiper (all connections)";
|
||||
expr = ''{container="sshpiper"}'';
|
||||
})
|
||||
(mkLogs {
|
||||
title = "Failed / invalid auth attempts (all backends, incl. host)";
|
||||
expr = ''{syslog_id=~"sshd.*"} |~ "Failed|Invalid|error:"'';
|
||||
})
|
||||
(mkLogs {
|
||||
title = "sshd auth events (all backends, incl. host)";
|
||||
expr = ''{syslog_id=~"sshd.*"}'';
|
||||
})
|
||||
(mkLogs {
|
||||
title = "Host sshd (port 5952)";
|
||||
expr = ''{syslog_id=~"sshd.*", container=""}'';
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
{ pkgs, lib }:
|
||||
let
|
||||
inherit (import ./helpers.nix { inherit pkgs lib; }) prom mkDashboard;
|
||||
in
|
||||
mkDashboard {
|
||||
name = "containers.json";
|
||||
title = "Containers";
|
||||
uid = "containers";
|
||||
from = "now-3h";
|
||||
panels = [
|
||||
{
|
||||
title = "Container States";
|
||||
type = "piechart";
|
||||
gridPos = {
|
||||
h = 8;
|
||||
w = 8;
|
||||
};
|
||||
datasource = prom;
|
||||
options = {
|
||||
pieType = "donut";
|
||||
legend = {
|
||||
displayMode = "table";
|
||||
placement = "right";
|
||||
values = [
|
||||
"value"
|
||||
"percent"
|
||||
];
|
||||
};
|
||||
tooltip.mode = "single";
|
||||
};
|
||||
fieldConfig.overrides = [
|
||||
{
|
||||
matcher = {
|
||||
id = "byName";
|
||||
options = "active";
|
||||
};
|
||||
properties = [
|
||||
{
|
||||
id = "color";
|
||||
value = {
|
||||
mode = "fixed";
|
||||
fixedColor = "green";
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
matcher = {
|
||||
id = "byName";
|
||||
options = "failed";
|
||||
};
|
||||
properties = [
|
||||
{
|
||||
id = "color";
|
||||
value = {
|
||||
mode = "fixed";
|
||||
fixedColor = "red";
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
matcher = {
|
||||
id = "byName";
|
||||
options = "inactive";
|
||||
};
|
||||
properties = [
|
||||
{
|
||||
id = "color";
|
||||
value = {
|
||||
mode = "fixed";
|
||||
fixedColor = "blue";
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
matcher = {
|
||||
id = "byName";
|
||||
options = "activating";
|
||||
};
|
||||
properties = [
|
||||
{
|
||||
id = "color";
|
||||
value = {
|
||||
mode = "fixed";
|
||||
fixedColor = "yellow";
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
matcher = {
|
||||
id = "byName";
|
||||
options = "unknown";
|
||||
};
|
||||
properties = [
|
||||
{
|
||||
id = "color";
|
||||
value = {
|
||||
mode = "fixed";
|
||||
fixedColor = "gray";
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
targets = [
|
||||
{
|
||||
datasource = prom;
|
||||
expr = ''count by (state) ((label_replace(node_systemd_unit_state{name=~"container@.*"} == 1, "container", "$1", "name", "container@(.+)\\.service")) or on(container) label_replace(container_defined, "state", "unknown", "container", ".+"))'';
|
||||
instant = true;
|
||||
legendFormat = "{{state}}";
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
title = "Container States";
|
||||
type = "table";
|
||||
gridPos = {
|
||||
h = 16;
|
||||
w = 24;
|
||||
};
|
||||
datasource = prom;
|
||||
options.footer.show = false;
|
||||
fieldConfig = {
|
||||
defaults = { };
|
||||
overrides = [
|
||||
{
|
||||
matcher = {
|
||||
id = "byName";
|
||||
options = "State";
|
||||
};
|
||||
properties = [
|
||||
{
|
||||
id = "custom.displayMode";
|
||||
value = "color-text";
|
||||
}
|
||||
{
|
||||
id = "mappings";
|
||||
value = [
|
||||
{
|
||||
type = "value";
|
||||
options.active = {
|
||||
text = "active";
|
||||
color = "green";
|
||||
index = 0;
|
||||
};
|
||||
}
|
||||
{
|
||||
type = "value";
|
||||
options.failed = {
|
||||
text = "failed";
|
||||
color = "red";
|
||||
index = 1;
|
||||
};
|
||||
}
|
||||
{
|
||||
type = "value";
|
||||
options.activating = {
|
||||
text = "activating";
|
||||
color = "yellow";
|
||||
index = 2;
|
||||
};
|
||||
}
|
||||
{
|
||||
type = "value";
|
||||
options.inactive = {
|
||||
text = "inactive";
|
||||
color = "blue";
|
||||
index = 3;
|
||||
};
|
||||
}
|
||||
{
|
||||
type = "value";
|
||||
options.unknown = {
|
||||
text = "unknown";
|
||||
color = "gray";
|
||||
index = 4;
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
targets = [
|
||||
{
|
||||
datasource = prom;
|
||||
expr = ''
|
||||
(label_replace(node_systemd_unit_state{name=~"container@.*"} == 1, "container", "$1", "name", "container@(.+)\\.service"))
|
||||
or on(container)
|
||||
label_replace(container_defined, "state", "unknown", "container", ".+")
|
||||
'';
|
||||
instant = true;
|
||||
format = "table";
|
||||
legendFormat = "__auto";
|
||||
}
|
||||
];
|
||||
transformations = [
|
||||
{
|
||||
id = "organize";
|
||||
options = {
|
||||
excludeByName = {
|
||||
Time = true;
|
||||
Value = true;
|
||||
"__name__" = true;
|
||||
instance = true;
|
||||
job = true;
|
||||
name = true;
|
||||
};
|
||||
renameByName = {
|
||||
container = "Container";
|
||||
state = "State";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
{
|
||||
title = "Container State (1 = running, 0 = stopped/failed)";
|
||||
type = "timeseries";
|
||||
gridPos = {
|
||||
h = 8;
|
||||
w = 24;
|
||||
};
|
||||
datasource = prom;
|
||||
fieldConfig.defaults = {
|
||||
custom.lineWidth = 2;
|
||||
color.mode = "palette-classic";
|
||||
min = 0;
|
||||
max = 1;
|
||||
};
|
||||
options = {
|
||||
tooltip.mode = "multi";
|
||||
legend = {
|
||||
displayMode = "table";
|
||||
placement = "bottom";
|
||||
};
|
||||
};
|
||||
targets = [
|
||||
{
|
||||
datasource = prom;
|
||||
expr = ''label_replace(node_systemd_unit_state{name=~"container@.*",state="active"}, "container", "$1", "name", "container@(.+).service")'';
|
||||
legendFormat = "{{container}}";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
{ pkgs, lib }:
|
||||
let
|
||||
prom = {
|
||||
type = "prometheus";
|
||||
uid = "prometheus";
|
||||
};
|
||||
loki = {
|
||||
type = "loki";
|
||||
uid = "loki";
|
||||
};
|
||||
|
||||
autoLayout =
|
||||
panels:
|
||||
let
|
||||
step =
|
||||
acc: panel:
|
||||
let
|
||||
w = panel.gridPos.w;
|
||||
h = panel.gridPos.h;
|
||||
wrap = acc.x + w > 24;
|
||||
x = if wrap then 0 else acc.x;
|
||||
y = if wrap then acc.y + acc.rowH else acc.y;
|
||||
rowH = if wrap then h else lib.max acc.rowH h;
|
||||
in
|
||||
{
|
||||
x = x + w;
|
||||
inherit y rowH;
|
||||
result = acc.result ++ [
|
||||
(
|
||||
panel
|
||||
// {
|
||||
gridPos = panel.gridPos // {
|
||||
inherit x y;
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
in
|
||||
(lib.foldl' step {
|
||||
x = 0;
|
||||
y = 0;
|
||||
rowH = 0;
|
||||
result = [ ];
|
||||
} panels).result;
|
||||
|
||||
T = expr: legendFormat: {
|
||||
datasource = prom;
|
||||
inherit expr legendFormat;
|
||||
};
|
||||
|
||||
mkRow = title: {
|
||||
inherit title;
|
||||
type = "row";
|
||||
collapsed = false;
|
||||
panels = [ ];
|
||||
gridPos = {
|
||||
w = 24;
|
||||
h = 1;
|
||||
};
|
||||
};
|
||||
|
||||
mkStat =
|
||||
{
|
||||
title,
|
||||
w ? 4,
|
||||
h ? 4,
|
||||
expr,
|
||||
unit ? "short",
|
||||
decimals ? 2,
|
||||
}:
|
||||
{
|
||||
inherit title;
|
||||
type = "stat";
|
||||
datasource = prom;
|
||||
gridPos = { inherit w h; };
|
||||
fieldConfig.defaults = {
|
||||
inherit unit decimals;
|
||||
color.mode = "thresholds";
|
||||
thresholds = {
|
||||
mode = "absolute";
|
||||
steps = [
|
||||
{
|
||||
color = "blue";
|
||||
value = null;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
options = {
|
||||
reduceOptions.calcs = [ "lastNotNull" ];
|
||||
colorMode = "value";
|
||||
textMode = "value";
|
||||
};
|
||||
targets = [
|
||||
{
|
||||
datasource = prom;
|
||||
inherit expr;
|
||||
instant = true;
|
||||
legendFormat = "";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
mkGauge =
|
||||
{
|
||||
title,
|
||||
w ? 4,
|
||||
h ? 4,
|
||||
expr,
|
||||
unit ? "percent",
|
||||
min ? 0,
|
||||
max ? 100,
|
||||
}:
|
||||
{
|
||||
inherit title;
|
||||
type = "gauge";
|
||||
datasource = prom;
|
||||
gridPos = { inherit w h; };
|
||||
fieldConfig.defaults = {
|
||||
inherit unit min max;
|
||||
thresholds = {
|
||||
mode = "absolute";
|
||||
steps = [
|
||||
{
|
||||
color = "green";
|
||||
value = null;
|
||||
}
|
||||
{
|
||||
color = "yellow";
|
||||
value = 70;
|
||||
}
|
||||
{
|
||||
color = "red";
|
||||
value = 90;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
options = {
|
||||
reduceOptions.calcs = [ "lastNotNull" ];
|
||||
orientation = "auto";
|
||||
showThresholdLabels = false;
|
||||
showThresholdMarkers = true;
|
||||
};
|
||||
targets = [
|
||||
{
|
||||
datasource = prom;
|
||||
inherit expr;
|
||||
instant = true;
|
||||
legendFormat = "";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
mkTs =
|
||||
{
|
||||
title,
|
||||
w ? 24,
|
||||
h ? 8,
|
||||
unit ? "short",
|
||||
targets,
|
||||
}:
|
||||
{
|
||||
inherit title targets;
|
||||
type = "timeseries";
|
||||
datasource = prom;
|
||||
gridPos = { inherit w h; };
|
||||
fieldConfig.defaults = {
|
||||
inherit unit;
|
||||
custom = {
|
||||
lineWidth = 1;
|
||||
fillOpacity = 5;
|
||||
};
|
||||
color.mode = "palette-classic";
|
||||
};
|
||||
options = {
|
||||
tooltip.mode = "multi";
|
||||
legend = {
|
||||
displayMode = "table";
|
||||
placement = "bottom";
|
||||
calcs = [
|
||||
"mean"
|
||||
"max"
|
||||
"lastNotNull"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkDashboard =
|
||||
{
|
||||
name,
|
||||
title,
|
||||
uid,
|
||||
from ? "now-1h",
|
||||
panels,
|
||||
templating ? [ ],
|
||||
}:
|
||||
pkgs.writeText name (
|
||||
builtins.toJSON (
|
||||
{
|
||||
inherit title uid;
|
||||
schemaVersion = 37;
|
||||
refresh = "30s";
|
||||
time = {
|
||||
inherit from;
|
||||
to = "now";
|
||||
};
|
||||
panels = lib.imap1 (id: p: p // { inherit id; }) (autoLayout panels);
|
||||
}
|
||||
// lib.optionalAttrs (templating != [ ]) { templating.list = templating; }
|
||||
)
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit
|
||||
prom
|
||||
loki
|
||||
autoLayout
|
||||
T
|
||||
mkRow
|
||||
mkStat
|
||||
mkGauge
|
||||
mkTs
|
||||
mkDashboard
|
||||
;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
{ pkgs, lib }:
|
||||
let
|
||||
inherit (import ./helpers.nix { inherit pkgs lib; })
|
||||
T
|
||||
mkRow
|
||||
mkStat
|
||||
mkGauge
|
||||
mkTs
|
||||
mkDashboard
|
||||
;
|
||||
in
|
||||
mkDashboard {
|
||||
name = "host.json";
|
||||
title = "Host";
|
||||
uid = "host";
|
||||
panels = [
|
||||
|
||||
(mkRow "Overview")
|
||||
(mkGauge {
|
||||
title = "CPU";
|
||||
expr = ''100 * (1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])))'';
|
||||
})
|
||||
(mkGauge {
|
||||
title = "Memory used";
|
||||
expr = "100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)";
|
||||
})
|
||||
(mkStat {
|
||||
title = "Load (1m)";
|
||||
unit = "short";
|
||||
decimals = 2;
|
||||
expr = "node_load1";
|
||||
})
|
||||
(mkStat {
|
||||
title = "Load (5m)";
|
||||
unit = "short";
|
||||
decimals = 2;
|
||||
expr = "node_load5";
|
||||
})
|
||||
(mkStat {
|
||||
title = "Uptime";
|
||||
unit = "dtdurations";
|
||||
decimals = 0;
|
||||
expr = "node_time_seconds - node_boot_time_seconds";
|
||||
})
|
||||
|
||||
(mkRow "CPU")
|
||||
(mkTs {
|
||||
title = "CPU usage (%)";
|
||||
unit = "percent";
|
||||
targets = [
|
||||
(T ''avg(rate(node_cpu_seconds_total{mode="user"}[5m])) * 100'' "user")
|
||||
(T ''avg(rate(node_cpu_seconds_total{mode="system"}[5m])) * 100'' "system")
|
||||
(T ''avg(rate(node_cpu_seconds_total{mode="iowait"}[5m])) * 100'' "iowait")
|
||||
(T ''avg(rate(node_cpu_seconds_total{mode="steal"}[5m])) * 100'' "steal")
|
||||
(T ''avg(rate(node_cpu_seconds_total{mode="softirq"}[5m])) * 100'' "softirq")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Per-core usage (%)";
|
||||
unit = "percent";
|
||||
targets = [
|
||||
(T ''(1 - rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100'' "cpu{{cpu}}")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "CPU temperatures";
|
||||
unit = "celsius";
|
||||
targets = [
|
||||
(T "node_hwmon_temp_celsius" "{{chip}} {{sensor}}")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Load average";
|
||||
targets = [
|
||||
(T "node_load1" "1m")
|
||||
(T "node_load5" "5m")
|
||||
(T "node_load15" "15m")
|
||||
];
|
||||
})
|
||||
|
||||
(mkRow "Memory")
|
||||
(mkTs {
|
||||
title = "Memory";
|
||||
unit = "bytes";
|
||||
targets = [
|
||||
(T "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes" "used")
|
||||
(T "node_memory_Cached_bytes + node_memory_Buffers_bytes" "cache + buffers")
|
||||
(T "node_memory_MemAvailable_bytes" "available")
|
||||
(T "node_memory_MemTotal_bytes" "total")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Swap";
|
||||
unit = "bytes";
|
||||
targets = [
|
||||
(T "node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes" "used")
|
||||
(T "node_memory_SwapTotal_bytes" "total")
|
||||
];
|
||||
})
|
||||
|
||||
(mkRow "Disk")
|
||||
(mkTs {
|
||||
title = "Filesystem usage (%)";
|
||||
unit = "percent";
|
||||
targets = [
|
||||
(T ''100 * (1 - node_filesystem_avail_bytes{fstype=~"btrfs|ext4|xfs|vfat|f2fs"} / node_filesystem_size_bytes{fstype=~"btrfs|ext4|xfs|vfat|f2fs"})'' "{{mountpoint}}")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Disk IO (bytes/s)";
|
||||
unit = "Bps";
|
||||
targets = [
|
||||
(T ''rate(node_disk_read_bytes_total{device!~"loop.*"}[5m])'' "{{device}} read")
|
||||
(T ''rate(node_disk_written_bytes_total{device!~"loop.*"}[5m])'' "{{device}} write")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Disk IO utilisation (%)";
|
||||
unit = "percent";
|
||||
targets = [
|
||||
(T ''rate(node_disk_io_time_seconds_total{device!~"loop.*"}[5m]) * 100'' "{{device}}")
|
||||
];
|
||||
})
|
||||
|
||||
(mkRow "Network")
|
||||
(mkTs {
|
||||
title = "Network (bytes/s)";
|
||||
unit = "Bps";
|
||||
targets = [
|
||||
(T ''rate(node_network_receive_bytes_total{device!~"lo|br-.*|vh-.*|vc-.*"}[5m])'' "{{device}} rx")
|
||||
(T ''rate(node_network_transmit_bytes_total{device!~"lo|br-.*|vh-.*|vc-.*"}[5m])'' "{{device}} tx")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Network errors + drops (pps)";
|
||||
targets = [
|
||||
(T ''rate(node_network_receive_errs_total{device!~"lo|br-.*|vh-.*|vc-.*"}[5m])'' "{{device}} rx err")
|
||||
(T ''rate(node_network_transmit_errs_total{device!~"lo|br-.*|vh-.*|vc-.*"}[5m])'' "{{device}} tx err")
|
||||
(T ''rate(node_network_receive_drop_total{device!~"lo|br-.*|vh-.*|vc-.*"}[5m])'' "{{device}} rx drop")
|
||||
(T ''rate(node_network_transmit_drop_total{device!~"lo|br-.*|vh-.*|vc-.*"}[5m])'' "{{device}} tx drop")
|
||||
];
|
||||
})
|
||||
|
||||
(mkRow "System")
|
||||
(mkTs {
|
||||
title = "Context switches + forks (ops/s)";
|
||||
targets = [
|
||||
(T "rate(node_context_switches_total[5m])" "context switches")
|
||||
(T "rate(node_forks_total[5m])" "forks")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "File descriptors";
|
||||
targets = [
|
||||
(T "node_filefd_allocated" "allocated")
|
||||
(T "node_filefd_maximum" "limit")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Processes";
|
||||
targets = [
|
||||
(T "node_procs_running" "running")
|
||||
(T "node_procs_blocked" "blocked on IO")
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
{ pkgs, lib }:
|
||||
let
|
||||
inherit (import ./helpers.nix { inherit pkgs lib; })
|
||||
T
|
||||
mkRow
|
||||
mkStat
|
||||
mkTs
|
||||
prom
|
||||
mkDashboard
|
||||
;
|
||||
|
||||
ctid = "id=~\"/machine.slice/container@.+\\\\.service/payload/.+\"";
|
||||
ctsel = "id=~\"/machine.slice/container@$container\\\\.service/payload/.+\"";
|
||||
|
||||
ctall = "id=~\"/machine.slice/container@.+\\\\.service.*\"";
|
||||
ctnet = "id=~\"/machine.slice/container@$container\\\\.service.*\"";
|
||||
|
||||
labeled =
|
||||
m:
|
||||
"label_replace(${m}, \"container\", \"$1\", \"id\", \"/machine.slice/container@(.+)\\\\.service/payload.+\")";
|
||||
byContainer = m: "sum by (container) (${labeled m})";
|
||||
|
||||
labeledAll =
|
||||
m:
|
||||
"label_replace(${m}, \"container\", \"$1\", \"id\", \"/machine.slice/container@(.+)\\\\.service.*\")";
|
||||
byContainerAll = m: "sum by (container) (${labeledAll m})";
|
||||
|
||||
nmSvc = m: "label_replace(${m}, \"container\", \"$1\", \"name\", \"container@(.+)\\\\.service\")";
|
||||
|
||||
orZero = m: fb: "(${m}) or on(container) (${fb} * 0)";
|
||||
|
||||
cpuAll =
|
||||
rate:
|
||||
orZero (byContainer "rate(container_cpu_usage_seconds_total{${ctid}}[${rate}])") "container_limit_cpu_cores";
|
||||
memAll = orZero (byContainerAll "container_memory_rss{${ctall}}") "container_limit_memory_bytes";
|
||||
taskAll = nmSvc "node_systemd_unit_tasks_current{name=~\"container@.+\"}";
|
||||
in
|
||||
mkDashboard {
|
||||
name = "resources.json";
|
||||
title = "Container Resources";
|
||||
uid = "container-resources";
|
||||
|
||||
templating = [
|
||||
{
|
||||
name = "container";
|
||||
type = "query";
|
||||
datasource = prom;
|
||||
query = "label_values(container_limit_cpu_cores, container)";
|
||||
refresh = 2;
|
||||
sort = 1;
|
||||
includeAll = true;
|
||||
multi = true;
|
||||
current = {
|
||||
selected = true;
|
||||
text = [ "All" ];
|
||||
value = [ "$__all" ];
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
panels = [
|
||||
(mkRow "Overview")
|
||||
(mkStat {
|
||||
title = "Running";
|
||||
decimals = 0;
|
||||
expr = "count(node_systemd_unit_state{name=~\"container@.*\",state=\"active\"} == 1) or vector(0)";
|
||||
})
|
||||
(mkStat {
|
||||
title = "CPU used";
|
||||
decimals = 2;
|
||||
expr = "sum(${cpuAll "5m"}) or vector(0)";
|
||||
})
|
||||
(mkStat {
|
||||
title = "CPU limit";
|
||||
decimals = 2;
|
||||
expr = "sum(container_limit_cpu_cores) or vector(0)";
|
||||
})
|
||||
(mkStat {
|
||||
title = "Mem RSS";
|
||||
decimals = 1;
|
||||
unit = "bytes";
|
||||
expr = "sum(${memAll}) or vector(0)";
|
||||
})
|
||||
(mkStat {
|
||||
title = "Mem limit";
|
||||
decimals = 1;
|
||||
unit = "bytes";
|
||||
expr = "sum(container_limit_memory_bytes) or vector(0)";
|
||||
})
|
||||
(mkStat {
|
||||
title = "Tasks";
|
||||
decimals = 0;
|
||||
expr = "sum(${taskAll}) or vector(0)";
|
||||
})
|
||||
|
||||
(mkRow "All Containers")
|
||||
(mkTs {
|
||||
title = "CPU (cores)";
|
||||
h = 24;
|
||||
targets = [ (T (cpuAll "5m") "{{container}}") ];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Memory RSS";
|
||||
unit = "bytes";
|
||||
h = 24;
|
||||
targets = [ (T memAll "{{container}}") ];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Tasks";
|
||||
h = 24;
|
||||
targets = [ (T taskAll "{{container}}") ];
|
||||
})
|
||||
(mkTs {
|
||||
title = "CPU throttled %";
|
||||
unit = "percent";
|
||||
h = 12;
|
||||
targets = [
|
||||
(T "(${byContainer "rate(container_cpu_cfs_throttled_periods_total{${ctid}}[5m])"} / ${byContainer "rate(container_cpu_cfs_periods_total{${ctid}}[5m])"}) * 100 or on(container) (container_limit_cpu_cores * 0)" "{{container}}")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Network egress (bytes/s)";
|
||||
unit = "Bps";
|
||||
h = 24;
|
||||
targets = [ (T "sum by (container) (container_network_receive_rate)" "{{container}}") ];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Network ingress (bytes/s)";
|
||||
unit = "Bps";
|
||||
h = 24;
|
||||
targets = [ (T "sum by (container) (container_network_transmit_rate)" "{{container}}") ];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Disk read (bytes/s)";
|
||||
unit = "Bps";
|
||||
h = 24;
|
||||
targets = [ (T (byContainer "rate(container_fs_reads_bytes_total{${ctid}}[5m])") "{{container}}") ];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Disk write (bytes/s)";
|
||||
unit = "Bps";
|
||||
h = 24;
|
||||
targets = [
|
||||
(T (byContainer "rate(container_fs_writes_bytes_total{${ctid}}[5m])") "{{container}}")
|
||||
];
|
||||
})
|
||||
|
||||
{
|
||||
type = "row";
|
||||
title = "$container";
|
||||
repeat = "container";
|
||||
collapsed = true;
|
||||
panels = [ ];
|
||||
gridPos = {
|
||||
w = 24;
|
||||
h = 1;
|
||||
};
|
||||
}
|
||||
(mkTs {
|
||||
title = "CPU (cores)";
|
||||
targets = [
|
||||
(T "sum(rate(container_cpu_usage_seconds_total{${ctsel}}[5m]))" "used")
|
||||
(T "container_limit_cpu_cores{container=\"$container\"}" "limit")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "CPU throttled %";
|
||||
unit = "percent";
|
||||
targets = [
|
||||
(T "(sum(rate(container_cpu_cfs_throttled_periods_total{${ctsel}}[5m])) / sum(rate(container_cpu_cfs_periods_total{${ctsel}}[5m]))) * 100 or vector(0)" "throttled %")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Memory";
|
||||
unit = "bytes";
|
||||
targets = [
|
||||
(T "sum(container_memory_rss{${ctnet}})" "RSS")
|
||||
(T "sum(container_memory_working_set_bytes{${ctnet}})" "working set")
|
||||
(T "container_limit_memory_bytes{container=\"$container\"}" "limit")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Network (bytes/s)";
|
||||
unit = "Bps";
|
||||
targets = [
|
||||
(T "container_network_receive_rate{container=\"$container\"}" "{{device}} ↑")
|
||||
(T "container_network_transmit_rate{container=\"$container\"}" "{{device}} ↓")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Disk IO (bytes/s)";
|
||||
unit = "Bps";
|
||||
targets = [
|
||||
(T "sum(rate(container_fs_reads_bytes_total{${ctsel}}[5m]))" "read")
|
||||
(T "sum(rate(container_fs_writes_bytes_total{${ctsel}}[5m]))" "write")
|
||||
];
|
||||
})
|
||||
(mkTs {
|
||||
title = "Tasks";
|
||||
targets = [ (T "node_systemd_unit_tasks_current{name=\"container@$container.service\"}" "tasks") ];
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
name,
|
||||
hostInfo,
|
||||
dsl,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkService;
|
||||
inherit (hostInfo.paths) data secrets;
|
||||
in
|
||||
mkContainer {
|
||||
inherit name;
|
||||
net.access = [ { to = "@internet"; } ];
|
||||
svc.autoStart = true;
|
||||
resources.fs = {
|
||||
"/config" = {
|
||||
hostPath = "${data}/web/devenv/config";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"/workspace" = {
|
||||
hostPath = "${data}/web/devenv/workspace";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"/run/secrets/code-server-env" = {
|
||||
hostPath = "${secrets}/code-server-env";
|
||||
isReadOnly = true;
|
||||
};
|
||||
};
|
||||
users = [
|
||||
{
|
||||
name = "coder";
|
||||
uid = 1000;
|
||||
home = "/config";
|
||||
}
|
||||
];
|
||||
service =
|
||||
{ pkgs, lib, ... }:
|
||||
mkService {
|
||||
description = "code-server";
|
||||
execStart = "${lib.getExe' pkgs.code-server "code-server"} --bind-addr 0.0.0.0:8080 --user-data-dir /config/data --extensions-dir /config/extensions --auth password /workspace";
|
||||
networkOnline = true;
|
||||
serviceConfig = {
|
||||
EnvironmentFile = "/run/secrets/code-server-env";
|
||||
User = "coder";
|
||||
ReadWritePaths = [
|
||||
"/config"
|
||||
"/workspace"
|
||||
];
|
||||
};
|
||||
};
|
||||
net.expose.tcp = [ 8080 ];
|
||||
resources.mem = "4G";
|
||||
resources.cpu = 2;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
{
|
||||
name,
|
||||
hostInfo,
|
||||
dsl,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkVm;
|
||||
inherit (hostInfo.paths) data secrets;
|
||||
|
||||
ciRunnerData = "${data}/web/git-ci-runner";
|
||||
ciRunnerSecrets = "${secrets}/ci-runner";
|
||||
in
|
||||
{
|
||||
${name} = mkContainer {
|
||||
inherit name;
|
||||
net.access = [ { to = "@internet"; } ];
|
||||
net.forward.tcp = [
|
||||
{
|
||||
host = 23;
|
||||
container = 8023;
|
||||
}
|
||||
];
|
||||
svc.autoStart = true;
|
||||
resources.fs."/var/lib/gitea" = {
|
||||
hostPath = "${data}/web/git";
|
||||
isReadOnly = false;
|
||||
};
|
||||
containerConfig = _: {
|
||||
services.gitea = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/gitea";
|
||||
settings = {
|
||||
server = {
|
||||
HTTP_ADDR = "0.0.0.0";
|
||||
HTTP_PORT = 3000;
|
||||
DOMAIN = "git.floweytf.com";
|
||||
ROOT_URL = "https://git.floweytf.com/";
|
||||
START_SSH_SERVER = true;
|
||||
BUILTIN_SSH_SERVER_USER = "git";
|
||||
SSH_PORT = 8023;
|
||||
};
|
||||
service.DISABLE_REGISTRATION = true;
|
||||
};
|
||||
};
|
||||
users.users.gitea.uid = 1000;
|
||||
users.groups.gitea.gid = 1000;
|
||||
time.timeZone = "UTC";
|
||||
systemd.services.gitea.serviceConfig.FailureAction = "exit-force";
|
||||
};
|
||||
net.expose.tcp = [ 3000 ];
|
||||
};
|
||||
|
||||
ci-runner = mkVm {
|
||||
name = "ci-runner";
|
||||
net.access = [
|
||||
{
|
||||
to = "git";
|
||||
tcp = [ 3000 ];
|
||||
}
|
||||
{ to = "@internet"; }
|
||||
];
|
||||
svc.autoStart = true;
|
||||
resources.cpu = 4;
|
||||
resources.mem = "8G";
|
||||
resources.fs = [
|
||||
{
|
||||
proto = "virtiofs";
|
||||
tag = "nix-rw-store";
|
||||
source = "${ciRunnerData}/nix-rw-store";
|
||||
mountPoint = "/nix/.rw-store";
|
||||
}
|
||||
{
|
||||
proto = "virtiofs";
|
||||
tag = "docker-data";
|
||||
source = "${ciRunnerData}/docker";
|
||||
mountPoint = "/var/lib/docker";
|
||||
}
|
||||
{
|
||||
proto = "virtiofs";
|
||||
tag = "runner-state";
|
||||
source = "${ciRunnerData}/runner-state";
|
||||
mountPoint = "/var/lib/gitea-runner";
|
||||
}
|
||||
{
|
||||
proto = "virtiofs";
|
||||
tag = "runner-token";
|
||||
source = ciRunnerSecrets;
|
||||
mountPoint = "/run/ci-runner-secrets";
|
||||
readOnly = true;
|
||||
}
|
||||
];
|
||||
guestConfig =
|
||||
{
|
||||
pkgs,
|
||||
utils,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
runnerInstanceName = "ci-runner";
|
||||
|
||||
ciJobImage = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "ci-job-image";
|
||||
tag = "latest";
|
||||
contents = with pkgs; [
|
||||
bashInteractive
|
||||
coreutils
|
||||
gitMinimal
|
||||
nixVersions.latest
|
||||
nodejs
|
||||
cacert
|
||||
];
|
||||
config = {
|
||||
Cmd = [ "/bin/sh" ];
|
||||
Env = [
|
||||
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
||||
"NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
||||
"NIX_REMOTE=daemon"
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
boot.kernelModules = [ "kvm-intel" ];
|
||||
boot.extraModprobeConfig = "options kvm_intel nested=1";
|
||||
|
||||
microvm.writableStoreOverlay = "/nix/.rw-store";
|
||||
|
||||
nix.settings = {
|
||||
experimental-features = [
|
||||
"nix-command"
|
||||
"flakes"
|
||||
];
|
||||
max-jobs = "auto";
|
||||
};
|
||||
|
||||
nix.gc = {
|
||||
automatic = true;
|
||||
dates = "daily";
|
||||
options = "--delete-older-than 14d";
|
||||
};
|
||||
# nix.optimise.automatic = true; - doesn't work with microvm.writableStoreOverlay
|
||||
|
||||
virtualisation.docker.enable = true;
|
||||
|
||||
systemd.services.load-ci-job-image = {
|
||||
description = "Load the CI job Docker image";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "docker.service" ];
|
||||
requires = [ "docker.service" ];
|
||||
before = [ "gitea-runner-${utils.escapeSystemdPath runnerInstanceName}.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = "${lib.getExe' pkgs.docker "docker"} load -i ${ciJobImage}";
|
||||
};
|
||||
};
|
||||
|
||||
services.gitea-actions-runner.instances.${runnerInstanceName} = {
|
||||
enable = true;
|
||||
name = runnerInstanceName;
|
||||
url = "http://git:3000/";
|
||||
tokenFile = "/run/ci-runner-secrets/token";
|
||||
labels = [
|
||||
"nix:docker://ci-job-image:latest"
|
||||
];
|
||||
settings.container = {
|
||||
options = "--device=/dev/kvm --volume=/nix/store:/nix/store:ro --volume=/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket";
|
||||
valid_volumes = [
|
||||
"/nix/store"
|
||||
"/nix/var/nix/daemon-socket"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
name,
|
||||
pkgs,
|
||||
lib,
|
||||
dsl,
|
||||
containers,
|
||||
customPkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkService collectContainers;
|
||||
|
||||
mkHopperConfig =
|
||||
{ routes }:
|
||||
let
|
||||
header = ''
|
||||
listen = "0.0.0.0:25565"
|
||||
|
||||
[routing.routes]
|
||||
'';
|
||||
mkRoute = { domain, ip }: ''
|
||||
"${domain}" = { ip = "${ip}" }
|
||||
'';
|
||||
in
|
||||
header + lib.concatMapStrings mkRoute routes;
|
||||
|
||||
backends = collectContainers "hopperRegister" containers;
|
||||
|
||||
mcRoutes = map ({ container, data }: {
|
||||
domain = "${data.hopperSubdomain}.mc.floweytf.com";
|
||||
ip = "${container.name}:${toString data.hopperPort}";
|
||||
}) backends;
|
||||
|
||||
hopperCfg = pkgs.writeText "Config.toml" (mkHopperConfig {
|
||||
routes = mcRoutes;
|
||||
});
|
||||
in
|
||||
{
|
||||
${name} = mkContainer {
|
||||
inherit name;
|
||||
net.access = map ({ container, data }: {
|
||||
to = container.name;
|
||||
tcp = [ data.hopperPort ];
|
||||
}) backends;
|
||||
svc.autoStart = true;
|
||||
net.expose.tcp = [ 25565 ];
|
||||
net.forward.tcp = [ 25565 ];
|
||||
resources.fs."/Config.toml" = {
|
||||
hostPath = toString hopperCfg;
|
||||
isReadOnly = true;
|
||||
};
|
||||
service =
|
||||
_:
|
||||
mkService {
|
||||
description = "Hopper Minecraft reverse proxy";
|
||||
execStart = lib.getExe' customPkgs.hopper-rs "hopper";
|
||||
workingDir = "/";
|
||||
networkOnline = true;
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
CapabilityBoundingSet = "";
|
||||
RestrictAddressFamilies = "AF_INET AF_INET6";
|
||||
};
|
||||
};
|
||||
};
|
||||
_hopperRegister =
|
||||
{
|
||||
hopperSubdomain ? null,
|
||||
hopperPort ? 25565,
|
||||
}:
|
||||
self: {
|
||||
hopperRegister = {
|
||||
hopperSubdomain = if hopperSubdomain == null then self.name else hopperSubdomain;
|
||||
inherit hopperPort;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
{
|
||||
name,
|
||||
pkgs,
|
||||
lib,
|
||||
hostInfo,
|
||||
dsl,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkService;
|
||||
inherit (hostInfo.paths) data secrets;
|
||||
|
||||
domain = "mail.floweytf.com";
|
||||
accountName = "flowey";
|
||||
stateDir = "/var/lib/stalwart";
|
||||
acmeDir = "/etc/mail-acme";
|
||||
|
||||
taggingScript = ''
|
||||
require ["envelope", "variables", "imap4flags", "fileinto"];
|
||||
|
||||
if envelope :localpart :matches "to" "*" {
|
||||
addflag "''${1}";
|
||||
}
|
||||
|
||||
fileinto "INBOX";
|
||||
'';
|
||||
|
||||
cli = lib.getExe' pkgs.stalwart-cli "stalwart-cli";
|
||||
jq = lib.getExe pkgs.jq;
|
||||
curl = lib.getExe pkgs.curl;
|
||||
grep = lib.getExe' pkgs.gnugrep "grep";
|
||||
paste = lib.getExe' pkgs.coreutils "paste";
|
||||
|
||||
# use sqlite b/c rocksdb segfaults
|
||||
configJsonTemplate = pkgs.writeText "stalwart-config.json" (
|
||||
builtins.toJSON {
|
||||
"@type" = "Sqlite";
|
||||
path = "${stateDir}/data.sqlite3";
|
||||
}
|
||||
);
|
||||
|
||||
staticPlan = pkgs.writeText "mail-static-plan.json" (
|
||||
builtins.concatStringsSep "\n" (
|
||||
map builtins.toJSON [
|
||||
{
|
||||
"@type" = "upsert";
|
||||
object = "Domain";
|
||||
matchOn = [ "name" ];
|
||||
value."domain-mail" = {
|
||||
name = domain;
|
||||
catchAllAddress = "${accountName}@${domain}";
|
||||
isEnabled = true;
|
||||
};
|
||||
}
|
||||
{
|
||||
"@type" = "upsert";
|
||||
object = "SieveSystemScript";
|
||||
matchOn = [ "name" ];
|
||||
value."sieve-tag" = {
|
||||
name = "tag-by-recipient";
|
||||
isActive = true;
|
||||
contents = taggingScript;
|
||||
};
|
||||
}
|
||||
]
|
||||
)
|
||||
+ "\n"
|
||||
);
|
||||
|
||||
stalwartAuthEnv = ''
|
||||
export STALWART_URL="http://127.0.0.1:8080"
|
||||
export STALWART_USER="admin"
|
||||
export STALWART_PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/admin_password")"
|
||||
'';
|
||||
|
||||
readyGate = pkgs.writeShellScript "mail-ready-gate" ''
|
||||
set -uo pipefail
|
||||
until ${curl} -sS -o /dev/null "http://127.0.0.1:8080/api/account"; do
|
||||
if ! systemctl -q is-active stalwart.service; then
|
||||
echo "mail: stalwart is not running" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
'';
|
||||
|
||||
bootstrapScript = pkgs.writeShellScript "mail-bootstrap" ''
|
||||
set -uo pipefail
|
||||
${stalwartAuthEnv}
|
||||
|
||||
out="$(${cli} update Bootstrap singleton \
|
||||
--field defaultDomain=${domain} \
|
||||
--field serverHostname=${domain} \
|
||||
--field generateDkimKeys=true \
|
||||
--field requestTlsCertificate=false \
|
||||
--field 'directory={"@type":"Internal"}' 2>&1)"
|
||||
status=$?
|
||||
|
||||
if [ "$status" -eq 0 ]; then
|
||||
echo "mail-bootstrap: first-time setup complete, restarting stalwart into normal mode" >&2
|
||||
systemctl restart stalwart.service
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ${grep} -qi 'bootstrap mode' <<<"$out"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "mail-bootstrap: $out" >&2
|
||||
exit 1
|
||||
'';
|
||||
|
||||
certCredCert = "/run/credentials/stalwart.service/mail_tls_cert";
|
||||
certCredKey = "/run/credentials/stalwart.service/mail_tls_key";
|
||||
|
||||
retryOnFailure = {
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
|
||||
retryLimit = {
|
||||
StartLimitBurst = 5;
|
||||
StartLimitIntervalSec = 120;
|
||||
};
|
||||
|
||||
provisionScript = pkgs.writeShellScript "mail-provision" ''
|
||||
set -euo pipefail
|
||||
${stalwartAuthEnv}
|
||||
export account_pass="$(cat "$CREDENTIALS_DIRECTORY/account_password")"
|
||||
|
||||
account_entry="$(${jq} -nc --arg name "${accountName}" \
|
||||
'{"@type":"upsert","object":"Account","matchOn":["name"],"value":{("account-"+$name):{"@type":"User","name":$name,"domainId":"#domain-mail","credentials":{"0":{"@type":"Password","secret":env.account_pass}}}}}')"
|
||||
|
||||
{ cat ${staticPlan}; echo "$account_entry"; } | ${cli} apply --stdin
|
||||
|
||||
existing_cert_ids="$(${cli} query Certificate --json --fields id | ${jq} -r .id | ${paste} -sd, -)"
|
||||
if [ -n "$existing_cert_ids" ]; then
|
||||
${cli} delete Certificate --ids "$existing_cert_ids"
|
||||
fi
|
||||
${cli} create Certificate \
|
||||
--field 'certificate={"@type":"File","filePath":"${certCredCert}"}' \
|
||||
--field 'privateKey={"@type":"File","filePath":"${certCredKey}"}'
|
||||
|
||||
systemctl restart stalwart.service
|
||||
'';
|
||||
in
|
||||
mkContainer {
|
||||
inherit name;
|
||||
net.access = [ { to = "@internet"; } ];
|
||||
svc.autoStart = true;
|
||||
net.forward.tcp = [
|
||||
25
|
||||
993
|
||||
];
|
||||
resources.fs = {
|
||||
"${stateDir}" = {
|
||||
hostPath = "${data}/core/mail";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"${acmeDir}" = {
|
||||
hostPath = "${data}/core/nginx/acme/${domain}";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"/run/secrets/mail-admin-password" = {
|
||||
hostPath = "${secrets}/mail-admin-password";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"/run/secrets/mail-account-password" = {
|
||||
hostPath = "${secrets}/mail-account-password";
|
||||
isReadOnly = true;
|
||||
};
|
||||
};
|
||||
users = [
|
||||
{
|
||||
name = "stalwart";
|
||||
uid = 1000;
|
||||
home = stateDir;
|
||||
}
|
||||
];
|
||||
containerConfig = _: {
|
||||
systemd.tmpfiles.rules = [
|
||||
"Z ${stateDir} 0750 stalwart stalwart -"
|
||||
"C ${stateDir}/config.json 0640 stalwart stalwart - ${configJsonTemplate}"
|
||||
];
|
||||
|
||||
systemd.services.stalwart = mkService {
|
||||
description = "Stalwart Mail Server";
|
||||
execStart = pkgs.writeShellScript "stalwart-run" ''
|
||||
set -euo pipefail
|
||||
export STALWART_RECOVERY_ADMIN="admin:$(cat "$CREDENTIALS_DIRECTORY/admin_password")"
|
||||
exec ${lib.getExe' pkgs.stalwart_0_16 "stalwart"} -c ${stateDir}/config.json
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = "stalwart";
|
||||
Group = "stalwart";
|
||||
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
||||
ReadWritePaths = [ stateDir ];
|
||||
LoadCredential = [
|
||||
"admin_password:/run/secrets/mail-admin-password"
|
||||
"mail_tls_cert:${acmeDir}/fullchain.pem"
|
||||
"mail_tls_key:${acmeDir}/key.pem"
|
||||
];
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.paths.mail-cert-renew = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
pathConfig = {
|
||||
PathChanged = "${acmeDir}/key.pem";
|
||||
Unit = "mail-provision.service";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.mail-ready = mkService {
|
||||
description = "Wait for the Stalwart management API";
|
||||
execStart = readyGate;
|
||||
after = [ "stalwart.service" ];
|
||||
wants = [ "stalwart.service" ];
|
||||
wantedBy = [ ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
TimeoutStartSec = 60;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.mail-bootstrap = mkService {
|
||||
description = "One-time Stalwart bootstrap (server identity + admin account)";
|
||||
execStart = bootstrapScript;
|
||||
after = [ "mail-ready.service" ];
|
||||
wants = [ "mail-ready.service" ];
|
||||
unitConfig = retryLimit;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
LoadCredential = [ "admin_password:/run/secrets/mail-admin-password" ];
|
||||
}
|
||||
// retryOnFailure;
|
||||
};
|
||||
|
||||
systemd.services.mail-provision = mkService {
|
||||
description = "Provision mail.floweytf.com domain + catch-all account in Stalwart";
|
||||
execStart = provisionScript;
|
||||
after = [
|
||||
"mail-ready.service"
|
||||
"mail-bootstrap.service"
|
||||
];
|
||||
wants = [
|
||||
"mail-ready.service"
|
||||
"mail-bootstrap.service"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
LoadCredential = [
|
||||
"admin_password:/run/secrets/mail-admin-password"
|
||||
"account_password:/run/secrets/mail-account-password"
|
||||
];
|
||||
}
|
||||
// retryOnFailure;
|
||||
unitConfig = retryLimit;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
name,
|
||||
hostInfo,
|
||||
dsl,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkService;
|
||||
inherit (hostInfo.paths) data;
|
||||
in
|
||||
mkContainer {
|
||||
inherit name;
|
||||
svc.autoStart = true;
|
||||
resources.fs."/app/data" = {
|
||||
hostPath = "${data}/web/reposilite";
|
||||
isReadOnly = false;
|
||||
};
|
||||
users = [
|
||||
{
|
||||
name = "reposilite";
|
||||
uid = 1000;
|
||||
home = "/app";
|
||||
}
|
||||
];
|
||||
service =
|
||||
{ pkgs, lib, ... }:
|
||||
mkService {
|
||||
description = "Reposilite";
|
||||
execStart = "${lib.getExe' pkgs.reposilite "reposilite"} --working-directory /app/data";
|
||||
workingDir = "/app/data";
|
||||
serviceConfig = {
|
||||
User = "reposilite";
|
||||
PrivateTmp = true;
|
||||
ReadWritePaths = [ "/app/data" ];
|
||||
};
|
||||
}
|
||||
// {
|
||||
environment.JAVA_OPTS = "-Xmx128M";
|
||||
};
|
||||
net.expose.tcp = [ 8080 ];
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
hostInfo,
|
||||
dsl,
|
||||
self,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkService;
|
||||
inherit (hostInfo.paths) data;
|
||||
|
||||
mkMcTrait =
|
||||
{
|
||||
jdk,
|
||||
dataPath,
|
||||
extraPackages ? [ ],
|
||||
}:
|
||||
{
|
||||
resources.fs."/data" = {
|
||||
hostPath = dataPath;
|
||||
isReadOnly = false;
|
||||
};
|
||||
users = [
|
||||
{
|
||||
name = "runner";
|
||||
uid = 1000;
|
||||
}
|
||||
];
|
||||
containerConfig =
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
jdk
|
||||
pkgs.bash
|
||||
]
|
||||
++ extraPackages;
|
||||
systemd.services.minecraft = mkService {
|
||||
description = "Minecraft server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [
|
||||
"local-fs.target"
|
||||
"container-ready.target"
|
||||
];
|
||||
wants = [ "container-ready.target" ];
|
||||
path = [
|
||||
jdk
|
||||
pkgs.bash
|
||||
];
|
||||
execStart = "${lib.getExe' pkgs.bash "bash"} -c ${
|
||||
lib.escapeShellArg (
|
||||
"exec ${lib.getExe' pkgs.dtach "dtach"} -N /run/ct-attach/attach.sock "
|
||||
+ "${lib.getExe' pkgs.util-linux "script"} -qfc /data/launch.sh "
|
||||
+ ">(exec ${lib.getExe' pkgs.systemd "systemd-cat"} -t minecraft)"
|
||||
)
|
||||
}";
|
||||
serviceConfig = {
|
||||
WorkingDirectory = "/data";
|
||||
User = "runner";
|
||||
RuntimeDirectory = "ct-attach";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
StandardInput = "null";
|
||||
StandardOutput = "journal";
|
||||
StandardError = "journal";
|
||||
ReadWritePaths = [ "/data" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
extractAttrs =
|
||||
l: s: lib.mergeAttrsList (map (f: lib.optionalAttrs (s ? ${f}) { ${f} = s.${f}; }) l);
|
||||
|
||||
mkMcSet =
|
||||
{ dataDir }:
|
||||
servers:
|
||||
lib.listToAttrs (
|
||||
map (
|
||||
args@{
|
||||
name,
|
||||
jdk,
|
||||
autoStart ? false,
|
||||
allowInternet ? true,
|
||||
dataPath ? "${dataDir}/${name}",
|
||||
extraPackages ? [ ],
|
||||
mem ? "6G",
|
||||
cpu ? 2,
|
||||
...
|
||||
}:
|
||||
lib.nameValuePair "mc-${name}" (mkContainer {
|
||||
name = "mc-${name}";
|
||||
net.access = lib.optional allowInternet { to = "@internet"; };
|
||||
net.expose.tcp = [ (args.hopperPort or 25565) ];
|
||||
svc.autoStart = autoStart;
|
||||
resources = { inherit cpu mem; };
|
||||
traits = [ (mkMcTrait { inherit jdk dataPath extraPackages; }) ];
|
||||
miscProperties = self._hopperRegister (extractAttrs [ "hopperSubdomain" "hopperPort" ] args);
|
||||
includedInTests = false;
|
||||
})
|
||||
) servers
|
||||
);
|
||||
|
||||
jdk8FontPkgs = with pkgs; [
|
||||
freetype
|
||||
fontconfig
|
||||
dejavu_fonts
|
||||
];
|
||||
|
||||
mcServers = [
|
||||
{
|
||||
name = "nomifactory";
|
||||
jdk = pkgs.jdk8;
|
||||
extraPackages = jdk8FontPkgs;
|
||||
hopperSubdomain = "nomi";
|
||||
hopperPort = 25555;
|
||||
}
|
||||
{
|
||||
name = "monifactory";
|
||||
jdk = pkgs.jdk21;
|
||||
hopperSubdomain = "moni";
|
||||
}
|
||||
{
|
||||
name = "rad2";
|
||||
jdk = pkgs.jdk8;
|
||||
extraPackages = jdk8FontPkgs;
|
||||
}
|
||||
{
|
||||
name = "smp";
|
||||
jdk = pkgs.jdk17;
|
||||
}
|
||||
{
|
||||
name = "rc4";
|
||||
jdk = pkgs.jdk17;
|
||||
}
|
||||
{
|
||||
name = "vh";
|
||||
jdk = pkgs.jdk17;
|
||||
hopperPort = 25555;
|
||||
}
|
||||
{
|
||||
name = "gregblock";
|
||||
jdk = pkgs.jdk8;
|
||||
extraPackages = jdk8FontPkgs;
|
||||
}
|
||||
{
|
||||
name = "tfg";
|
||||
jdk = pkgs.jdk17;
|
||||
}
|
||||
{
|
||||
name = "ratgame";
|
||||
jdk = pkgs.jdk21;
|
||||
}
|
||||
{
|
||||
name = "maidrpg";
|
||||
jdk = pkgs.jdk17;
|
||||
}
|
||||
{
|
||||
name = "maidsmp";
|
||||
jdk = pkgs.jdk21;
|
||||
}
|
||||
{
|
||||
name = "maid-event";
|
||||
jdk = pkgs.jdk21;
|
||||
hopperSubdomain = "maidevent";
|
||||
dataPath = "${data}/mc/maidevent/speedrun";
|
||||
}
|
||||
{
|
||||
name = "gtnh";
|
||||
jdk = pkgs.jdk21;
|
||||
}
|
||||
{
|
||||
name = "turing";
|
||||
jdk = pkgs.jdk21;
|
||||
}
|
||||
{
|
||||
name = "tfg2";
|
||||
jdk = pkgs.jdk21;
|
||||
autoStart = true;
|
||||
mem = "10G";
|
||||
cpu = 8;
|
||||
}
|
||||
{
|
||||
name = "tl";
|
||||
jdk = pkgs.jdk21;
|
||||
}
|
||||
{
|
||||
name = "botm";
|
||||
jdk = pkgs.jdk17;
|
||||
}
|
||||
{
|
||||
name = "rogule-build";
|
||||
jdk = pkgs.jdk21;
|
||||
}
|
||||
{
|
||||
name = "rogule-dev";
|
||||
jdk = pkgs.jdk21;
|
||||
}
|
||||
];
|
||||
in
|
||||
mkMcSet {
|
||||
dataDir = "${data}/mc";
|
||||
} mcServers
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
name,
|
||||
pkgs,
|
||||
lib,
|
||||
hostInfo,
|
||||
dsl,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkService;
|
||||
inherit (hostInfo.paths) data secrets;
|
||||
inherit (inputs) mtce;
|
||||
dataDir = "/var/lib/mtce-bot";
|
||||
|
||||
threads = 64;
|
||||
timeout = 1800;
|
||||
|
||||
configJson = pkgs.writeText "mtce-bot-config.json" (
|
||||
builtins.toJSON {
|
||||
inherit threads timeout;
|
||||
}
|
||||
);
|
||||
|
||||
mtce-engine = mtce.lib.mkMtce pkgs {
|
||||
cpuFlags = [
|
||||
"-O3"
|
||||
"-march=cascadelake"
|
||||
];
|
||||
vectorizedBitSize = 512;
|
||||
};
|
||||
mtce-bot = mtce.lib.mkBot pkgs;
|
||||
in
|
||||
mkContainer {
|
||||
inherit name;
|
||||
svc.autoStart = true;
|
||||
resources.fs = {
|
||||
"${dataDir}/data.json" = {
|
||||
hostPath = "${data}/misc/mtce-discord-bot/data.json";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"${dataDir}/audit" = {
|
||||
hostPath = "${data}/misc/mtce-discord-bot/audit";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"${dataDir}/config.json" = {
|
||||
hostPath = "${configJson}";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"${dataDir}/mtce" = {
|
||||
hostPath = lib.getExe' mtce-engine "mtce";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"/run/secrets/mtce-discord-bot-env" = {
|
||||
hostPath = "${secrets}/mtce-discord-bot-env";
|
||||
isReadOnly = true;
|
||||
};
|
||||
};
|
||||
users = [
|
||||
{
|
||||
name = "bot";
|
||||
uid = 1000;
|
||||
}
|
||||
];
|
||||
service =
|
||||
_:
|
||||
mkService {
|
||||
description = "Discord bot (monumenta-charm-eval)";
|
||||
execStart = lib.getExe' mtce-bot "mtce-bot";
|
||||
workingDir = dataDir;
|
||||
networkOnline = true;
|
||||
serviceConfig = {
|
||||
User = "bot";
|
||||
EnvironmentFile = "/run/secrets/mtce-discord-bot-env";
|
||||
PrivateTmp = true;
|
||||
CapabilityBoundingSet = "";
|
||||
ReadWritePaths = [ dataDir ];
|
||||
};
|
||||
};
|
||||
resources.mem = "2G";
|
||||
resources.cpu = 64;
|
||||
net.access = [ { to = "@internet"; } ];
|
||||
includedInTests = false;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
{
|
||||
name,
|
||||
hostInfo,
|
||||
dsl,
|
||||
lib,
|
||||
containers,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer;
|
||||
inherit (hostInfo.paths) data;
|
||||
|
||||
mkNginxConfig =
|
||||
{
|
||||
email,
|
||||
vhosts,
|
||||
resolver,
|
||||
}:
|
||||
let
|
||||
mkVhostEntries =
|
||||
{
|
||||
domain,
|
||||
upstream ? null,
|
||||
websockets ? true,
|
||||
redirect ? true,
|
||||
listenHttp ? false,
|
||||
extraLocations ? { },
|
||||
}:
|
||||
let
|
||||
httpsListen = {
|
||||
addr = "0.0.0.0";
|
||||
port = 443;
|
||||
ssl = true;
|
||||
};
|
||||
httpListen = {
|
||||
addr = "0.0.0.0";
|
||||
port = 80;
|
||||
ssl = false;
|
||||
};
|
||||
defaultLoc = lib.optionalAttrs (upstream != null) {
|
||||
"/" = {
|
||||
proxyWebsockets = websockets;
|
||||
# avoid startup error when containers aren't registered yet
|
||||
# proxyPass is left unset for that reason, so recommendedProxySettings
|
||||
# won't auto-include its headers here - set them manually to match
|
||||
extraConfig = ''
|
||||
set $backend "${upstream.container.name}:${toString upstream.port}";
|
||||
proxy_pass http://$backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Server $hostname;
|
||||
'';
|
||||
};
|
||||
};
|
||||
acmePrimaryOnMain = !redirect && listenHttp;
|
||||
mainVhost = lib.nameValuePair domain (
|
||||
{
|
||||
listen = [ httpsListen ] ++ lib.optional listenHttp httpListen;
|
||||
locations = defaultLoc // extraLocations;
|
||||
}
|
||||
// (
|
||||
if acmePrimaryOnMain then
|
||||
{
|
||||
addSSL = true;
|
||||
enableACME = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
onlySSL = true;
|
||||
useACMEHost = domain;
|
||||
}
|
||||
)
|
||||
);
|
||||
redirectVhost = lib.nameValuePair "${domain}-redirect" {
|
||||
serverName = domain;
|
||||
enableACME = true;
|
||||
listen = [ httpListen ];
|
||||
locations = {
|
||||
"/" = {
|
||||
return = "301 https://$host$request_uri";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
[ mainVhost ] ++ lib.optional redirect redirectVhost;
|
||||
in
|
||||
{
|
||||
access = map (v: {
|
||||
to = v.upstream.container.name;
|
||||
tcp = [ v.upstream.port ];
|
||||
}) (lib.filter (v: v.upstream or null != null) vhosts);
|
||||
|
||||
containerConfig =
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedTlsSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedGzipSettings = true;
|
||||
recommendedProxySettings = true;
|
||||
clientMaxBodySize = "100M";
|
||||
appendHttpConfig = ''
|
||||
access_log syslog:server=unix:/dev/log,tag=nginx combined;
|
||||
'';
|
||||
resolver = {
|
||||
addresses = [ resolver ];
|
||||
ipv6 = false;
|
||||
valid = "30s";
|
||||
};
|
||||
virtualHosts = lib.listToAttrs (lib.concatLists (map mkVhostEntries vhosts)) // {
|
||||
"_" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = 443;
|
||||
extraParameters = [
|
||||
"ssl"
|
||||
"default_server"
|
||||
];
|
||||
}
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = 80;
|
||||
extraParameters = [ "default_server" ];
|
||||
}
|
||||
];
|
||||
extraConfig = "ssl_reject_handshake on;";
|
||||
locations."/".return = "404";
|
||||
};
|
||||
};
|
||||
};
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults.email = email;
|
||||
};
|
||||
systemd.services =
|
||||
lib.mapAttrs' (
|
||||
cert: _:
|
||||
lib.nameValuePair "acme-${cert}" {
|
||||
after = [ "container-ready.target" ];
|
||||
wants = [ "container-ready.target" ];
|
||||
}
|
||||
) config.security.acme.certs
|
||||
// {
|
||||
nginx.serviceConfig.FailureAction = "exit-force";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
up = container: port: { inherit container port; };
|
||||
|
||||
nginxSetup =
|
||||
hostAddr:
|
||||
mkNginxConfig {
|
||||
email = "floweydev@gmail.com";
|
||||
resolver = hostAddr;
|
||||
vhosts = [
|
||||
{
|
||||
domain = "devenv.floweytf.com";
|
||||
upstream = up containers.devenv 8080;
|
||||
}
|
||||
{
|
||||
domain = "maven.floweytf.com";
|
||||
upstream = up containers.maven 8080;
|
||||
}
|
||||
{
|
||||
domain = "git.floweytf.com";
|
||||
upstream = up containers.git 3000;
|
||||
}
|
||||
{
|
||||
domain = "utils.floweytf.com";
|
||||
redirect = false;
|
||||
listenHttp = true;
|
||||
extraLocations = {
|
||||
"/static/" = {
|
||||
alias = "/usr/share/nginx/static/";
|
||||
extraConfig = "autoindex on;";
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
# no upstream, vhost only exists for certs
|
||||
domain = "mail.floweytf.com";
|
||||
}
|
||||
];
|
||||
};
|
||||
in
|
||||
let
|
||||
setup = nginxSetup hostAddr;
|
||||
c = mkContainer {
|
||||
inherit name;
|
||||
net.access = setup.access ++ [ { to = "@internet"; } ];
|
||||
svc.autoStart = true;
|
||||
svc.depends = lib.attrVals [ "devenv" "maven" "git" ] containers;
|
||||
net.forward.tcp = [
|
||||
80
|
||||
443
|
||||
];
|
||||
resources.fs."/var/lib/acme" = {
|
||||
hostPath = "${data}/core/nginx/acme";
|
||||
isReadOnly = false;
|
||||
};
|
||||
inherit (setup) containerConfig;
|
||||
};
|
||||
hostAddr = c.hostAddr;
|
||||
in
|
||||
c
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
name,
|
||||
hostInfo,
|
||||
dsl,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer;
|
||||
inherit (hostInfo.paths) data;
|
||||
in
|
||||
mkContainer {
|
||||
inherit name;
|
||||
net.access = [ { to = "@internet"; } ];
|
||||
svc.autoStart = false;
|
||||
net.expose.tcp = [ 53 ];
|
||||
# TODO: figure out how to have this be the external dns resolver only
|
||||
resources.fs."/var/lib/pihole" = {
|
||||
hostPath = "${data}/misc/pihole";
|
||||
isReadOnly = false;
|
||||
};
|
||||
containerConfig = _: {
|
||||
services.pihole-ftl = {
|
||||
enable = true;
|
||||
stateDirectory = "/var/lib/pihole";
|
||||
openFirewallDNS = true;
|
||||
settings.dns.upstreams = [
|
||||
"1.1.1.1"
|
||||
"8.8.8.8"
|
||||
];
|
||||
lists = [
|
||||
{
|
||||
url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts";
|
||||
type = "block";
|
||||
description = "StevenBlack unified hosts";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
url = "https://big.oisd.nl/domainswild";
|
||||
type = "block";
|
||||
description = "OISD Big";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/pro.txt";
|
||||
type = "block";
|
||||
description = "HaGeZi Multi Pro";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
url = "https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/domains/tif.txt";
|
||||
type = "block";
|
||||
description = "HaGeZi Threat Intelligence Feeds";
|
||||
enabled = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
services.pihole-web = {
|
||||
enable = true;
|
||||
ports = [ "443s" ];
|
||||
};
|
||||
systemd.services.pihole-ftl.serviceConfig.FailureAction = "exit-force";
|
||||
};
|
||||
resources.mem = "512M";
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
pkgs,
|
||||
hostInfo,
|
||||
dsl,
|
||||
self,
|
||||
lib,
|
||||
customPkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer;
|
||||
inherit (hostInfo.paths) server data;
|
||||
|
||||
sshTrait = {
|
||||
resources.fs."/etc/ssh/authorized_keys" = {
|
||||
hostPath = "${server}/keypair/id_ed25519.pub";
|
||||
isReadOnly = true;
|
||||
};
|
||||
users = [
|
||||
{
|
||||
name = "runner";
|
||||
uid = 1000;
|
||||
}
|
||||
];
|
||||
containerConfig =
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
lib.mkMerge [
|
||||
{
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
authorizedKeysFiles = [ "/etc/ssh/authorized_keys" ];
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
UseDns = false;
|
||||
PrintMotd = false;
|
||||
};
|
||||
};
|
||||
systemd.services.sshd.serviceConfig.FailureAction = "exit-force";
|
||||
}
|
||||
{
|
||||
environment.systemPackages = [
|
||||
customPkgs.kitty-terminfo
|
||||
pkgs.wezterm.terminfo
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
mkSsh =
|
||||
{
|
||||
name,
|
||||
packages ? [ ],
|
||||
nixDaemon ? true,
|
||||
routingInfo ? {
|
||||
username = lib.strings.removePrefix "ssh-" name;
|
||||
},
|
||||
net ? { },
|
||||
resources ? { },
|
||||
traits ? [ ],
|
||||
}:
|
||||
mkContainer {
|
||||
inherit name;
|
||||
inherit net resources;
|
||||
svc.autoStart = true;
|
||||
isolation.nixDaemon = nixDaemon;
|
||||
traits = [ sshTrait ] ++ traits;
|
||||
packages = with pkgs; [ tmux ] ++ packages;
|
||||
miscProperties = self._sshpiperRegister routingInfo;
|
||||
};
|
||||
in
|
||||
{
|
||||
ssh-admin = mkSsh {
|
||||
name = "ssh-admin";
|
||||
net.access = [
|
||||
{
|
||||
to = "overseer";
|
||||
tcp = [ 80 ];
|
||||
}
|
||||
];
|
||||
resources.fs = {
|
||||
"/home/runner/data" = {
|
||||
hostPath = server;
|
||||
isReadOnly = false;
|
||||
};
|
||||
"/run/dbus/system_bus_socket" = {
|
||||
hostPath = "/run/dbus/system_bus_socket";
|
||||
isReadOnly = false;
|
||||
};
|
||||
};
|
||||
packages = with pkgs; [
|
||||
git
|
||||
systemd
|
||||
];
|
||||
};
|
||||
|
||||
ssh-botm = mkSsh {
|
||||
name = "ssh-botm";
|
||||
resources.fs."/home/runner/botm" = {
|
||||
hostPath = "${data}/mc/botm";
|
||||
isReadOnly = false;
|
||||
};
|
||||
packages = with pkgs; [
|
||||
python3
|
||||
jdk17
|
||||
tmux
|
||||
];
|
||||
};
|
||||
|
||||
ssh-generic = mkSsh {
|
||||
name = "ssh-generic";
|
||||
net.access = [ { to = "@internet"; } ];
|
||||
resources = {
|
||||
fs."/home/runner/data" = {
|
||||
hostPath = "${data}/ssh/generic";
|
||||
isReadOnly = false;
|
||||
};
|
||||
mem = "16G";
|
||||
cpu = 65;
|
||||
};
|
||||
routingInfo = [
|
||||
{
|
||||
username = "autumn";
|
||||
keysName = "generic";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
ssh-proxy = mkSsh {
|
||||
name = "ssh-proxy";
|
||||
resources.fs."/home/runner/data" = {
|
||||
hostPath = "${data}/ssh/nyaa";
|
||||
isReadOnly = true;
|
||||
};
|
||||
packages = with pkgs; [ python3 ];
|
||||
routingInfo = [
|
||||
{
|
||||
username = "nyaa";
|
||||
keysName = "nyaa";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
ssh-rogule = mkSsh {
|
||||
name = "ssh-rogule";
|
||||
resources.fs = {
|
||||
"/home/runner/rogule-dev" = {
|
||||
hostPath = "${data}/mc/rogule-dev";
|
||||
isReadOnly = false;
|
||||
};
|
||||
"/home/runner/rogule-build" = {
|
||||
hostPath = "${data}/mc/rogule-build";
|
||||
isReadOnly = false;
|
||||
};
|
||||
};
|
||||
packages = with pkgs; [
|
||||
python3
|
||||
jdk21
|
||||
];
|
||||
};
|
||||
|
||||
ssh-starwalker = mkSsh {
|
||||
name = "ssh-starwalker";
|
||||
resources.fs."/home/runner/starwalker" = {
|
||||
hostPath = "${data}/mc/starwalker";
|
||||
isReadOnly = false;
|
||||
};
|
||||
packages = with pkgs; [
|
||||
jdk21
|
||||
];
|
||||
};
|
||||
|
||||
ssh-tl = mkSsh {
|
||||
name = "ssh-tl";
|
||||
resources.fs."/home/runner/tl" = {
|
||||
hostPath = "${data}/mc/tl";
|
||||
isReadOnly = false;
|
||||
};
|
||||
packages = with pkgs; [
|
||||
jdk21
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
name,
|
||||
pkgs,
|
||||
hostInfo,
|
||||
dsl,
|
||||
lib,
|
||||
containers,
|
||||
customPkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (dsl) mkContainer mkService collectContainers;
|
||||
inherit (hostInfo.paths) server secrets;
|
||||
dataDir = hostInfo.paths.data;
|
||||
|
||||
mkSshpiperConfig =
|
||||
{
|
||||
pipes,
|
||||
privateKey ? "/common_id",
|
||||
}:
|
||||
let
|
||||
header = ''
|
||||
version: "1.0"
|
||||
pipes:
|
||||
'';
|
||||
mkPipe =
|
||||
{
|
||||
username,
|
||||
host,
|
||||
keysName ? username,
|
||||
}:
|
||||
''
|
||||
- from:
|
||||
- username: "${username}"
|
||||
authorized_keys: /etc/sshpiperd/keys/user_${keysName}/authorized_keys
|
||||
to:
|
||||
host: ${host}:22
|
||||
username: "runner"
|
||||
private_key: "${privateKey}"
|
||||
ignore_hostkey: true
|
||||
'';
|
||||
in
|
||||
header + lib.concatMapStrings mkPipe pipes;
|
||||
|
||||
sshPipes = lib.concatMap (
|
||||
{ container, data }: map (lib.mergeAttrs { host = container.name; }) data
|
||||
) (collectContainers "sshpiperRoutes" containers);
|
||||
|
||||
sshpiperYaml = pkgs.writeText "sshpiper.yaml" (mkSshpiperConfig {
|
||||
pipes = sshPipes;
|
||||
});
|
||||
in
|
||||
{
|
||||
${name} = mkContainer {
|
||||
inherit name;
|
||||
net.access = map (p: {
|
||||
to = p.host;
|
||||
tcp = [ 22 ];
|
||||
}) sshPipes;
|
||||
svc.autoStart = true;
|
||||
net.forward.tcp = [
|
||||
{
|
||||
host = 22;
|
||||
container = 2222;
|
||||
}
|
||||
];
|
||||
resources.fs = {
|
||||
"/etc/sshpiperd/keys" = {
|
||||
hostPath = "${dataDir}/core/sshpiper";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"/common_id" = {
|
||||
hostPath = "${server}/keypair/id_ed25519";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"/etc/ssh/ssh_host_ed25519_key" = {
|
||||
hostPath = "${secrets}/ssh_host_ed25519_key";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"/etc/ssh/ssh_host_rsa_key" = {
|
||||
hostPath = "${secrets}/ssh_host_rsa_key";
|
||||
isReadOnly = true;
|
||||
};
|
||||
"/etc/ssh/ssh_host_ecdsa_key" = {
|
||||
hostPath = "${secrets}/ssh_host_ecdsa_key";
|
||||
isReadOnly = true;
|
||||
};
|
||||
};
|
||||
service =
|
||||
{ pkgs, ... }:
|
||||
mkService {
|
||||
description = "sshpiper SSH multiplexer";
|
||||
execStart = "${lib.getExe' customPkgs.sshpiper "sshpiperd"} --log-level trace --server-key '/etc/ssh/ssh_host_*_key' ${lib.getExe' customPkgs.sshpiper "yaml"} --config /run/sshpiperd/config.yaml";
|
||||
networkOnline = true;
|
||||
serviceConfig = {
|
||||
# sshpiperd rejects world-readable configs; copy store (444) -> runtime (600)
|
||||
ExecStartPre = "${lib.getExe' pkgs.coreutils "install"} -m 600 ${sshpiperYaml} /run/sshpiperd/config.yaml";
|
||||
RuntimeDirectory = "sshpiperd";
|
||||
PrivateTmp = true;
|
||||
RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
_sshpiperRegister =
|
||||
let
|
||||
doTranslate =
|
||||
self:
|
||||
{
|
||||
username,
|
||||
keysName ? username,
|
||||
}:
|
||||
{
|
||||
inherit username keysName;
|
||||
host = self.name;
|
||||
};
|
||||
in
|
||||
args: self: {
|
||||
sshpiperRoutes =
|
||||
if lib.isAttrs args then [ (doTranslate self args) ] else lib.map (doTranslate self) args;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user