another refactor partly done
This commit is contained in:
parent
3362c47211
commit
a955528e44
31 changed files with 3790 additions and 1930 deletions
55
modules/homelab/lib/aggregators/base.nix
Normal file
55
modules/homelab/lib/aggregators/base.nix
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{lib}: let
|
||||
inherit (lib) flatten mapAttrs attrValues filterAttrs mapAttrsToList filter groupBy length unique attrByPath splitString;
|
||||
|
||||
# Generic function to aggregate any attribute across nodes
|
||||
aggregateFromNodes = {
|
||||
nodes,
|
||||
attributePath, # e.g. "homelab.monitoring.metrics" or "homelab.backups.jobs"
|
||||
enhancer ? null, # optional function to enhance each item with node context
|
||||
}: let
|
||||
# Extract the attribute from each node using the path
|
||||
getNestedAttr = path: config: let
|
||||
pathList = splitString "." path;
|
||||
in
|
||||
attrByPath pathList [] config;
|
||||
|
||||
# Get all items from all nodes
|
||||
allItems = flatten (mapAttrsToList
|
||||
(nodeName: nodeConfig: let
|
||||
items = getNestedAttr attributePath nodeConfig.config;
|
||||
baseEnhancer = item:
|
||||
item
|
||||
// {
|
||||
_nodeName = nodeName;
|
||||
_nodeConfig = nodeConfig;
|
||||
_nodeAddress = nodeConfig.config.networking.hostName or nodeName;
|
||||
};
|
||||
finalEnhancer =
|
||||
if enhancer != null
|
||||
then (item: enhancer (baseEnhancer item))
|
||||
else baseEnhancer;
|
||||
in
|
||||
map finalEnhancer items)
|
||||
nodes);
|
||||
in {
|
||||
# Raw aggregated data
|
||||
all = allItems;
|
||||
|
||||
# Common grouping patterns
|
||||
byNode = groupBy (item: item._nodeName) allItems;
|
||||
byType = groupBy (item: item.type or "unknown") allItems;
|
||||
byService = groupBy (item: item.service or "unknown") allItems;
|
||||
|
||||
# Utility functions for filtering
|
||||
filterBy = predicate: filter predicate allItems;
|
||||
ofType = type: filter (item: (item.type or "") == type) allItems;
|
||||
ofNode = nodeName: filter (item: item._nodeName == nodeName) allItems;
|
||||
enabled = filter (item: item.enabled or true) allItems;
|
||||
|
||||
# Counting utilities
|
||||
count = length allItems;
|
||||
countBy = fn: mapAttrs (key: items: length items) (groupBy fn allItems);
|
||||
};
|
||||
in {
|
||||
inherit aggregateFromNodes;
|
||||
}
|
||||
87
modules/homelab/lib/features/logging.nix
Normal file
87
modules/homelab/lib/features/logging.nix
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
serviceName: {
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.homelab.services.${serviceName};
|
||||
homelabCfg = config.homelab;
|
||||
in {
|
||||
options.homelab.services.${serviceName}.logging = {
|
||||
enable = mkEnableOption "logging for ${serviceName}";
|
||||
|
||||
files = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
};
|
||||
|
||||
parsing = {
|
||||
regex = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
};
|
||||
|
||||
extractFields = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
|
||||
multiline = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
firstLineRegex = mkOption {type = types.str;};
|
||||
maxWaitTime = mkOption {
|
||||
type = types.str;
|
||||
default = "3s";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
};
|
||||
|
||||
extraLabels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
};
|
||||
|
||||
extraSources = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (cfg.enable && cfg.logging.enable) {
|
||||
homelab.logging.sources =
|
||||
[
|
||||
{
|
||||
name = "${serviceName}-logs";
|
||||
type = "file";
|
||||
files = {
|
||||
paths = cfg.logging.files;
|
||||
multiline = cfg.logging.multiline;
|
||||
};
|
||||
labels =
|
||||
cfg.logging.extraLabels
|
||||
// {
|
||||
service = serviceName;
|
||||
node = homelabCfg.hostname;
|
||||
environment = homelabCfg.environment;
|
||||
};
|
||||
pipelineStages =
|
||||
mkIf (cfg.logging.parsing.regex != null) [
|
||||
{
|
||||
regex.expression = cfg.logging.parsing.regex;
|
||||
}
|
||||
]
|
||||
++ [
|
||||
{
|
||||
labels = listToAttrs (map (field: nameValuePair field null) cfg.logging.parsing.extractFields);
|
||||
}
|
||||
];
|
||||
enabled = true;
|
||||
}
|
||||
]
|
||||
++ cfg.logging.extraSources;
|
||||
};
|
||||
}
|
||||
108
modules/homelab/lib/features/monitoring.nix
Normal file
108
modules/homelab/lib/features/monitoring.nix
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
serviceName: {
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.homelab.services.${serviceName};
|
||||
homelabCfg = config.homelab;
|
||||
in {
|
||||
# Define the service-specific monitoring options
|
||||
options.homelab.services.${serviceName}.monitoring = {
|
||||
enable = mkEnableOption "monitoring for ${serviceName}";
|
||||
|
||||
metrics = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/metrics";
|
||||
};
|
||||
|
||||
extraEndpoints = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
|
||||
healthCheck = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/health";
|
||||
};
|
||||
|
||||
conditions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = ["[STATUS] == 200"];
|
||||
};
|
||||
|
||||
extraChecks = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
|
||||
extraLabels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
};
|
||||
};
|
||||
|
||||
# Generate the homelab config automatically when service is enabled
|
||||
config = mkIf (cfg.enable && cfg.monitoring.enable) {
|
||||
homelab.monitoring = {
|
||||
metrics =
|
||||
[
|
||||
{
|
||||
name = "${serviceName}-main";
|
||||
host = homelabCfg.hostname;
|
||||
port = cfg.port;
|
||||
path = cfg.monitoring.metrics.path;
|
||||
jobName = serviceName;
|
||||
scrapeInterval = "30s";
|
||||
labels =
|
||||
cfg.monitoring.extraLabels
|
||||
// {
|
||||
service = serviceName;
|
||||
node = homelabCfg.hostname;
|
||||
environment = homelabCfg.environment;
|
||||
};
|
||||
}
|
||||
]
|
||||
++ cfg.monitoring.metrics.extraEndpoints;
|
||||
|
||||
healthChecks =
|
||||
[
|
||||
{
|
||||
name = "${serviceName}-health";
|
||||
host = homelabCfg.hostname;
|
||||
port = cfg.port;
|
||||
path = cfg.monitoring.healthCheck.path;
|
||||
protocol = "http";
|
||||
method = "GET";
|
||||
interval = "30s";
|
||||
timeout = "10s";
|
||||
conditions = cfg.monitoring.healthCheck.conditions;
|
||||
group = "services";
|
||||
labels =
|
||||
cfg.monitoring.extraLabels
|
||||
// {
|
||||
service = serviceName;
|
||||
node = homelabCfg.hostname;
|
||||
environment = homelabCfg.environment;
|
||||
};
|
||||
enabled = true;
|
||||
}
|
||||
]
|
||||
++ cfg.monitoring.healthCheck.extraChecks;
|
||||
};
|
||||
};
|
||||
}
|
||||
64
modules/homelab/lib/features/proxy.nix
Normal file
64
modules/homelab/lib/features/proxy.nix
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
serviceName: {
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.homelab.services.${serviceName};
|
||||
homelabCfg = config.homelab;
|
||||
in {
|
||||
options.homelab.services.${serviceName}.proxy = {
|
||||
enable = mkEnableOption "reverse proxy for ${serviceName}";
|
||||
|
||||
subdomain = mkOption {
|
||||
type = types.str;
|
||||
default = serviceName;
|
||||
};
|
||||
|
||||
enableAuth = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
};
|
||||
|
||||
additionalSubdomains = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
subdomain = mkOption {type = types.str;};
|
||||
port = mkOption {type = types.port;};
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/";
|
||||
};
|
||||
enableAuth = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (cfg.enable && cfg.proxy.enable) {
|
||||
homelab.reverseProxy.entries =
|
||||
[
|
||||
{
|
||||
subdomain = cfg.proxy.subdomain;
|
||||
host = homelabCfg.hostname;
|
||||
port = cfg.port;
|
||||
path = "/";
|
||||
enableAuth = cfg.proxy.enableAuth;
|
||||
enableSSL = true;
|
||||
}
|
||||
]
|
||||
++ map (sub: {
|
||||
subdomain = sub.subdomain;
|
||||
host = homelabCfg.hostname;
|
||||
port = sub.port;
|
||||
path = sub.path;
|
||||
enableAuth = sub.enableAuth;
|
||||
enableSSL = true;
|
||||
})
|
||||
cfg.proxy.additionalSubdomains;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
{lib}: let
|
||||
inherit (lib) flatten mapAttrs mapAttrsToList filter groupBy length unique attrByPath splitString;
|
||||
|
||||
# Generic function to aggregate any attribute across nodes
|
||||
aggregateFromNodes = {
|
||||
nodes,
|
||||
attributePath, # e.g. "homelab.monitoring.endpoints" or "homelab.backups.jobs"
|
||||
enhancer ? null, # optional function to enhance each item with node context
|
||||
}: let
|
||||
# Extract the attribute from each node using the path
|
||||
getNestedAttr = path: config: let
|
||||
pathList = splitString "." path;
|
||||
in
|
||||
attrByPath pathList [] config;
|
||||
|
||||
# Get all items from all nodes
|
||||
allItems = flatten (mapAttrsToList
|
||||
(nodeName: nodeConfig: let
|
||||
items = getNestedAttr attributePath nodeConfig.config;
|
||||
baseEnhancer = item:
|
||||
item
|
||||
// {
|
||||
_nodeName = nodeName;
|
||||
_nodeConfig = nodeConfig;
|
||||
_nodeAddress = nodeConfig.config.networking.hostName or nodeName;
|
||||
};
|
||||
finalEnhancer =
|
||||
if enhancer != null
|
||||
then (item: enhancer (baseEnhancer item))
|
||||
else baseEnhancer;
|
||||
in
|
||||
map finalEnhancer items)
|
||||
nodes);
|
||||
in {
|
||||
# Raw aggregated data
|
||||
all = allItems;
|
||||
|
||||
# Common grouping patterns
|
||||
byNode = groupBy (item: item._nodeName) allItems;
|
||||
byType = groupBy (item: item.type or "unknown") allItems;
|
||||
byService = groupBy (item: item.service or "unknown") allItems;
|
||||
|
||||
# Utility functions for filtering
|
||||
filterBy = predicate: filter predicate allItems;
|
||||
ofType = type: filter (item: (item.type or "") == type) allItems;
|
||||
|
||||
count = length allItems;
|
||||
countBy = fn: mapAttrs (key: items: length items) (groupBy fn allItems);
|
||||
};
|
||||
|
||||
# Specialized aggregators for common use cases
|
||||
aggregators = {
|
||||
monitoring = nodes: let
|
||||
# Aggregate metrics endpoints
|
||||
metricsAgg = aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.monitoring.metrics";
|
||||
enhancer = endpoint:
|
||||
endpoint
|
||||
// {
|
||||
_fullAddress = "${endpoint.host or endpoint._nodeAddress}:${toString endpoint.port}";
|
||||
_metricsUrl = "http://${endpoint.host or endpoint._nodeAddress}:${toString endpoint.port}${endpoint.path or "/metrics"}";
|
||||
_type = "metrics";
|
||||
};
|
||||
};
|
||||
# Aggregate health checks
|
||||
healthChecksAgg = aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.monitoring.healthChecks";
|
||||
enhancer = check: let
|
||||
# Compute the actual host and URL
|
||||
actualHost =
|
||||
if check.useExternalDomain or false
|
||||
then "${check.subdomain}.${check._nodeConfig.config.homelab.externalDomain or "example.com"}"
|
||||
else check.host or check._nodeAddress;
|
||||
portPart =
|
||||
if check.port != null
|
||||
then ":${toString check.port}"
|
||||
else "";
|
||||
url = "${check.protocol or "http"}://${actualHost}${portPart}${check.path or "/"}";
|
||||
in
|
||||
check
|
||||
// {
|
||||
_actualHost = actualHost;
|
||||
_url = url;
|
||||
_type = "health-check";
|
||||
# Merge default labels with node context
|
||||
labels =
|
||||
(check.labels or {})
|
||||
// {
|
||||
node = check._nodeName;
|
||||
environment = check._nodeConfig.config.homelab.environment or "unknown";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
metricsAgg
|
||||
// healthChecksAgg
|
||||
// {
|
||||
# Metrics-specific aggregations
|
||||
allMetrics = metricsAgg.all;
|
||||
metricsByNode = metricsAgg.byNode;
|
||||
metricsByJobName = groupBy (m: m.jobName or "unknown") metricsAgg.all;
|
||||
|
||||
# Health checks-specific aggregations
|
||||
allHealthChecks = healthChecksAgg.all;
|
||||
healthChecksByNode = healthChecksAgg.byNode;
|
||||
healthChecksByGroup = groupBy (hc: hc.group or "default") healthChecksAgg.all;
|
||||
healthChecksByProtocol = groupBy (hc: hc.protocol or "http") healthChecksAgg.all;
|
||||
|
||||
# Filtered health checks
|
||||
externalHealthChecks = filter (hc: hc.useExternalDomain or false) healthChecksAgg.all;
|
||||
internalHealthChecks = filter (hc: !(hc.useExternalDomain or false)) healthChecksAgg.all;
|
||||
enabledHealthChecks = filter (hc: hc.enabled or true) healthChecksAgg.all;
|
||||
|
||||
# Summary statistics
|
||||
summary = {
|
||||
totalMetrics = length metricsAgg.all;
|
||||
totalHealthChecks = length healthChecksAgg.all;
|
||||
healthChecksByGroup =
|
||||
mapAttrs (group: checks: length checks)
|
||||
(groupBy (hc: hc.group or "default") healthChecksAgg.all);
|
||||
healthChecksByProtocol =
|
||||
mapAttrs (protocol: checks: length checks)
|
||||
(groupBy (hc: hc.protocol or "http") healthChecksAgg.all);
|
||||
externalChecksCount = length (filter (hc: hc.useExternalDomain or false) healthChecksAgg.all);
|
||||
internalChecksCount = length (filter (hc: !(hc.useExternalDomain or false)) healthChecksAgg.all);
|
||||
};
|
||||
};
|
||||
|
||||
# Promtail log configurations
|
||||
# logs = nodes:
|
||||
# aggregateFromNodes {
|
||||
# inherit nodes;
|
||||
# attributePath = "homelab.logging.sources";
|
||||
# enhancer = logSource:
|
||||
# logSource
|
||||
# // {
|
||||
# # Add log-specific computed fields
|
||||
# _logPath = logSource.path or "/var/log/${logSource.service}.log";
|
||||
# _labels =
|
||||
# (logSource.labels or {})
|
||||
# // {
|
||||
# node = logSource._nodeName;
|
||||
# service = logSource.service or "unknown";
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
|
||||
# Reverse proxy configurations
|
||||
reverseProxy = nodes:
|
||||
aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.reverseProxy.entries";
|
||||
enhancer = entry:
|
||||
entry
|
||||
// {
|
||||
# Add proxy-specific computed fields
|
||||
_upstream = "http://${entry.host or entry._nodeAddress}:${toString entry.port}";
|
||||
_fqdn = "${entry.subdomain or entry.service}.${entry.domain or "local"}";
|
||||
};
|
||||
};
|
||||
|
||||
# Backup jobs with enhanced aggregation
|
||||
backups = nodes: let
|
||||
baseAgg = aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.backups.jobs";
|
||||
enhancer = backup:
|
||||
backup
|
||||
// {
|
||||
_sourceNode = backup._nodeName;
|
||||
_backupId = "${backup._nodeName}-${backup.name}";
|
||||
_jobFqdn = "${backup.name}.${backup._nodeName}";
|
||||
};
|
||||
};
|
||||
|
||||
# Get all unique backends across all nodes
|
||||
allBackends = let
|
||||
allBackendConfigs =
|
||||
mapAttrsToList
|
||||
(nodeName: nodeConfig:
|
||||
attrByPath ["homelab" "backups" "backends"] {} nodeConfig.config)
|
||||
nodes;
|
||||
enabledBackends = flatten (map (backends:
|
||||
filter (name: backends.${name} != null) (lib.attrNames backends))
|
||||
allBackendConfigs);
|
||||
in
|
||||
unique enabledBackends;
|
||||
in
|
||||
baseAgg
|
||||
// {
|
||||
# Backup-specific aggregations
|
||||
byBackend = groupBy (job: job.backend) baseAgg.all;
|
||||
allBackends = allBackends;
|
||||
|
||||
# Enhanced summary
|
||||
summary = {
|
||||
totalJobs = length baseAgg.all;
|
||||
jobsByBackend =
|
||||
mapAttrs (backend: jobs: length jobs)
|
||||
(groupBy (job: job.backend) baseAgg.all);
|
||||
jobsByNode = baseAgg.countBy (job: job._nodeName);
|
||||
availableBackends = allBackends;
|
||||
backendsInUse = unique (map (job: job.backend) baseAgg.all);
|
||||
};
|
||||
};
|
||||
};
|
||||
in {
|
||||
inherit aggregateFromNodes aggregators;
|
||||
|
||||
# Convenience function to create a module that provides global aggregations
|
||||
mkGlobalModule = attributeName: aggregatorFn: {
|
||||
lib,
|
||||
nodes,
|
||||
...
|
||||
}: {
|
||||
options.homelab.global.${attributeName} = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
readOnly = true;
|
||||
description = "Globally aggregated ${attributeName} from all nodes";
|
||||
};
|
||||
|
||||
config.homelab.global.${attributeName} = aggregatorFn nodes;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,295 +0,0 @@
|
|||
# Standard service interface for homelab services
|
||||
# This provides a consistent contract that all services should follow
|
||||
{lib}: let
|
||||
inherit (lib) mkOption mkEnableOption types;
|
||||
|
||||
# Define the standard service interface
|
||||
mkServiceInterface = {
|
||||
serviceName,
|
||||
defaultPort ? null,
|
||||
defaultSubdomain ? serviceName,
|
||||
defaultDescription ? "Homelab ${serviceName} service",
|
||||
monitoringPath ? "/metrics",
|
||||
healthCheckPath ? "/health",
|
||||
healthCheckConditions ? ["[STATUS] == 200"],
|
||||
# Custom options that the service wants to expose
|
||||
serviceOptions ? {},
|
||||
}:
|
||||
{
|
||||
# Standard interface options that all services must have
|
||||
enable = mkEnableOption defaultDescription;
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default =
|
||||
if defaultPort != null
|
||||
then defaultPort
|
||||
else throw "Service ${serviceName} must specify a default port";
|
||||
description = "Port for ${serviceName} service";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether to automatically open firewall ports";
|
||||
};
|
||||
|
||||
proxy = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable reverse proxy for this service";
|
||||
};
|
||||
|
||||
subdomain = mkOption {
|
||||
type = types.str;
|
||||
default = defaultSubdomain;
|
||||
description = "Subdomain for reverse proxy (${defaultSubdomain}.yourdomain.com)";
|
||||
};
|
||||
|
||||
enableAuth = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable authentication for reverse proxy";
|
||||
};
|
||||
|
||||
enableSSL = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable SSL for reverse proxy";
|
||||
};
|
||||
};
|
||||
|
||||
monitoring = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable monitoring (metrics and health checks)";
|
||||
};
|
||||
|
||||
metricsPath = mkOption {
|
||||
type = types.str;
|
||||
default = monitoringPath;
|
||||
description = "Path for metrics endpoint";
|
||||
};
|
||||
|
||||
jobName = mkOption {
|
||||
type = types.str;
|
||||
default = serviceName;
|
||||
description = "Prometheus job name";
|
||||
};
|
||||
|
||||
scrapeInterval = mkOption {
|
||||
type = types.str;
|
||||
default = "30s";
|
||||
description = "Prometheus scrape interval";
|
||||
};
|
||||
|
||||
healthCheck = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable health check monitoring";
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = healthCheckPath;
|
||||
description = "Path for health check endpoint";
|
||||
};
|
||||
|
||||
interval = mkOption {
|
||||
type = types.str;
|
||||
default = "30s";
|
||||
description = "Health check interval";
|
||||
};
|
||||
|
||||
timeout = mkOption {
|
||||
type = types.str;
|
||||
default = "10s";
|
||||
description = "Health check timeout";
|
||||
};
|
||||
|
||||
conditions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = healthCheckConditions;
|
||||
description = "Health check conditions";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "services";
|
||||
description = "Health check group name";
|
||||
};
|
||||
};
|
||||
|
||||
extraLabels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
description = "Additional labels for monitoring";
|
||||
};
|
||||
};
|
||||
|
||||
description = mkOption {
|
||||
type = types.str;
|
||||
default = defaultDescription;
|
||||
description = "Service description";
|
||||
};
|
||||
|
||||
extraOptions = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
description = "Additional service-specific configuration options";
|
||||
};
|
||||
|
||||
# Merge in service-specific options
|
||||
}
|
||||
// serviceOptions;
|
||||
|
||||
# Helper function to implement the standard service behavior
|
||||
mkServiceConfig = {
|
||||
config,
|
||||
cfg,
|
||||
homelabCfg,
|
||||
serviceName,
|
||||
# Function that returns the actual service configuration
|
||||
serviceConfig,
|
||||
# Optional: custom monitoring labels
|
||||
extraMonitoringLabels ? {},
|
||||
# Optional: custom health check configuration
|
||||
customHealthChecks ? [],
|
||||
# Optional: custom reverse proxy configuration
|
||||
customProxyConfig ? {},
|
||||
}: let
|
||||
# Standard monitoring labels
|
||||
standardLabels =
|
||||
{
|
||||
service = serviceName;
|
||||
component = "main";
|
||||
instance = "${homelabCfg.hostname}.${homelabCfg.domain}";
|
||||
}
|
||||
// extraMonitoringLabels // cfg.monitoring.extraLabels;
|
||||
|
||||
# Standard reverse proxy entry
|
||||
standardProxyEntry =
|
||||
{
|
||||
subdomain = cfg.proxy.subdomain;
|
||||
host = homelabCfg.hostname;
|
||||
port = cfg.port;
|
||||
enableAuth = cfg.proxy.enableAuth;
|
||||
enableSSL = cfg.proxy.enableSSL;
|
||||
}
|
||||
// customProxyConfig;
|
||||
|
||||
# Standard metrics configuration
|
||||
standardMetrics = lib.optional cfg.monitoring.enable {
|
||||
name = "${serviceName}-metrics";
|
||||
port = cfg.port;
|
||||
path = cfg.monitoring.metricsPath;
|
||||
jobName = cfg.monitoring.jobName;
|
||||
scrapeInterval = cfg.monitoring.scrapeInterval;
|
||||
labels = standardLabels;
|
||||
};
|
||||
|
||||
# Standard health check configuration
|
||||
standardHealthCheck = lib.optional (cfg.monitoring.enable && cfg.monitoring.healthCheck.enable) {
|
||||
name = "${serviceName}-health";
|
||||
port = cfg.port;
|
||||
path = cfg.monitoring.healthCheck.path;
|
||||
interval = cfg.monitoring.healthCheck.interval;
|
||||
timeout = cfg.monitoring.healthCheck.timeout;
|
||||
conditions = cfg.monitoring.healthCheck.conditions;
|
||||
group = cfg.monitoring.healthCheck.group;
|
||||
labels = standardLabels;
|
||||
};
|
||||
|
||||
# Merge service config with standard behaviors
|
||||
baseConfig = lib.mkMerge [
|
||||
# Service-specific configuration
|
||||
serviceConfig
|
||||
|
||||
# Standard firewall configuration
|
||||
(lib.mkIf cfg.openFirewall {
|
||||
networking.firewall.allowedTCPPorts = [cfg.port];
|
||||
})
|
||||
|
||||
# Standard monitoring configuration
|
||||
(lib.mkIf cfg.monitoring.enable {
|
||||
homelab.monitoring.metrics = standardMetrics;
|
||||
homelab.monitoring.healthChecks = standardHealthCheck ++ customHealthChecks;
|
||||
})
|
||||
|
||||
# Standard reverse proxy configuration
|
||||
(lib.mkIf cfg.proxy.enable {
|
||||
homelab.reverseProxy.entries = [standardProxyEntry];
|
||||
})
|
||||
];
|
||||
in
|
||||
lib.mkIf cfg.enable baseConfig;
|
||||
|
||||
# Validation helper to ensure required options are set
|
||||
validateServiceConfig = cfg: serviceName: [
|
||||
# Validate that if proxy is enabled, subdomain is set
|
||||
(lib.mkIf (cfg.proxy.enable && cfg.proxy.subdomain == "")
|
||||
(throw "Service ${serviceName}: proxy.subdomain is required when proxy.enable is true"))
|
||||
|
||||
# Validate that if monitoring is enabled, required paths are set
|
||||
(lib.mkIf (cfg.monitoring.enable && cfg.monitoring.metricsPath == "")
|
||||
(throw "Service ${serviceName}: monitoring.metricsPath cannot be empty when monitoring is enabled"))
|
||||
];
|
||||
in {
|
||||
inherit mkServiceInterface mkServiceConfig validateServiceConfig;
|
||||
|
||||
# Common service option patterns
|
||||
commonOptions = {
|
||||
# Log level option
|
||||
logLevel = mkOption {
|
||||
type = types.enum ["debug" "info" "warn" "error"];
|
||||
default = "info";
|
||||
description = "Log level";
|
||||
};
|
||||
|
||||
# Environment file option (for secrets)
|
||||
environmentFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = "Environment file for secrets";
|
||||
};
|
||||
|
||||
# External URL option
|
||||
externalUrl = serviceName: homelabCfg:
|
||||
mkOption {
|
||||
type = types.str;
|
||||
default = "https://${serviceName}.${homelabCfg.externalDomain}";
|
||||
description = "External URL for ${serviceName}";
|
||||
};
|
||||
};
|
||||
|
||||
# Helper for creating service modules with the interface
|
||||
mkServiceModule = {
|
||||
serviceName,
|
||||
defaultPort,
|
||||
defaultSubdomain ? serviceName,
|
||||
serviceOptions ? {},
|
||||
...
|
||||
} @ args: {
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
cfg = config.homelab.services.${serviceName};
|
||||
homelabCfg = config.homelab;
|
||||
|
||||
serviceInterface = mkServiceInterface {
|
||||
inherit serviceName defaultPort defaultSubdomain serviceOptions;
|
||||
};
|
||||
in {
|
||||
options.homelab.services.${serviceName} = serviceInterface;
|
||||
|
||||
config = mkServiceConfig {
|
||||
inherit config cfg homelabCfg serviceName;
|
||||
# Service implementor must provide this function
|
||||
serviceConfig = args.serviceConfig or (throw "mkServiceModule requires serviceConfig function");
|
||||
};
|
||||
};
|
||||
}
|
||||
163
modules/homelab/lib/systems/backups.nix
Normal file
163
modules/homelab/lib/systems/backups.nix
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
nodes,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.homelab.backups;
|
||||
homelabCfg = config.homelab;
|
||||
hasNodes = length (attrNames nodes) > 0;
|
||||
|
||||
# Get all defined backend names dynamically
|
||||
backendNames = attrNames cfg.backends or {};
|
||||
|
||||
backupJobType = types.submodule {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "Name of the backup job";
|
||||
};
|
||||
backend = mkOption {
|
||||
type = types.enum backendNames;
|
||||
description = "Backend to use for this backup job";
|
||||
};
|
||||
backendOptions = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
description = "Backend-specific options to override or extend the backend configuration";
|
||||
};
|
||||
labels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
description = "Additional labels for this backup job";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Local aggregation
|
||||
localAggregation = {
|
||||
allJobs = cfg.jobs;
|
||||
allBackends = backendNames;
|
||||
};
|
||||
|
||||
# Global aggregation
|
||||
globalAggregation = let
|
||||
baseAgg = import ../aggregators/base.nix {inherit lib;};
|
||||
|
||||
jobsAgg = baseAgg.aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.backups.allJobs";
|
||||
enhancer = job:
|
||||
job
|
||||
// {
|
||||
_sourceNode = job._nodeName;
|
||||
_backupId = "${job._nodeName}-${job.name}";
|
||||
_jobFqdn = "${job.name}.${job._nodeName}";
|
||||
};
|
||||
};
|
||||
|
||||
# Get all backends from all nodes
|
||||
allBackendsFromNodes = let
|
||||
backendConfigs =
|
||||
mapAttrsToList (
|
||||
nodeName: nodeConfig:
|
||||
attrByPath ["homelab" "backups" "backends"] {} nodeConfig.config
|
||||
)
|
||||
nodes;
|
||||
enabledBackends = flatten (map (
|
||||
backends:
|
||||
filter (name: backends.${name} != null) (attrNames backends)
|
||||
)
|
||||
backendConfigs);
|
||||
in
|
||||
unique enabledBackends;
|
||||
in {
|
||||
allJobs = jobsAgg.all;
|
||||
allBackends = allBackendsFromNodes;
|
||||
jobsByBackend = groupBy (j: j.backend) jobsAgg.all;
|
||||
summary = {
|
||||
total = length jobsAgg.all;
|
||||
byBackend = jobsAgg.countBy (j: j.backend);
|
||||
byNode = jobsAgg.countBy (j: j._nodeName);
|
||||
uniqueBackends = unique (map (j: j.backend) jobsAgg.all);
|
||||
};
|
||||
};
|
||||
in {
|
||||
imports = [
|
||||
../../backup/restic.nix
|
||||
# ./backup/borgbackup.nix
|
||||
];
|
||||
|
||||
options.homelab.backups = {
|
||||
enable = mkEnableOption "backup system";
|
||||
|
||||
jobs = mkOption {
|
||||
type = types.listOf backupJobType;
|
||||
default = [];
|
||||
description = "Backup jobs to execute on this system";
|
||||
};
|
||||
|
||||
# Backend configurations (like your existing setup)
|
||||
# backends = mkOption {
|
||||
# type = types.attrs;
|
||||
# default = {};
|
||||
# description = "Backup backend configurations";
|
||||
# };
|
||||
|
||||
defaultLabels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {
|
||||
hostname = homelabCfg.hostname;
|
||||
environment = homelabCfg.environment;
|
||||
location = homelabCfg.location;
|
||||
};
|
||||
description = "Default labels applied to all backup jobs";
|
||||
};
|
||||
|
||||
monitoring = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable backup monitoring and metrics";
|
||||
};
|
||||
|
||||
# Always exposed aggregated data
|
||||
allJobs = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = [];
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
allBackends = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
global = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
readOnly = true;
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Validate that all job backends exist
|
||||
assertions = [
|
||||
{
|
||||
assertion = all (job: cfg.backends.${job.backend} != null) cfg.jobs;
|
||||
message = "All backup jobs must reference backends that are defined and not null in homelab.backups.backends";
|
||||
}
|
||||
];
|
||||
|
||||
# Always expose both local and global
|
||||
homelab.backups = {
|
||||
allJobs = localAggregation.allJobs;
|
||||
allBackends = localAggregation.allBackends;
|
||||
global =
|
||||
if hasNodes
|
||||
then globalAggregation
|
||||
else {};
|
||||
};
|
||||
};
|
||||
}
|
||||
209
modules/homelab/lib/systems/logging.nix
Normal file
209
modules/homelab/lib/systems/logging.nix
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
nodes,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.homelab.logging;
|
||||
homelabCfg = config.homelab;
|
||||
hasNodes = length (attrNames nodes) > 0;
|
||||
|
||||
# Local aggregation
|
||||
localAggregation = {
|
||||
allSources =
|
||||
cfg.sources
|
||||
++ (optional cfg.promtail.enable {
|
||||
name = "system-journal";
|
||||
type = "journal";
|
||||
journal.path = "/var/log/journal";
|
||||
labels =
|
||||
cfg.defaultLabels
|
||||
// {
|
||||
component = "system";
|
||||
log_source = "journald";
|
||||
};
|
||||
enabled = true;
|
||||
});
|
||||
};
|
||||
|
||||
# Global aggregation
|
||||
globalAggregation = let
|
||||
baseAgg = import ../aggregators/base.nix {inherit lib;};
|
||||
|
||||
sourcesAgg = baseAgg.aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.logging.allSources";
|
||||
enhancer = source:
|
||||
source
|
||||
// {
|
||||
_sourceNode = source._nodeName;
|
||||
_logId = "${source._nodeName}-${source.name}";
|
||||
};
|
||||
};
|
||||
in {
|
||||
allSources = sourcesAgg.all;
|
||||
sourcesByType = groupBy (s: s.type) sourcesAgg.all;
|
||||
summary = {
|
||||
total = length sourcesAgg.all;
|
||||
byType = sourcesAgg.countBy (s: s.type);
|
||||
byNode = sourcesAgg.countBy (s: s._nodeName);
|
||||
};
|
||||
};
|
||||
in {
|
||||
options.homelab.logging = {
|
||||
enable = mkEnableOption "logging system";
|
||||
|
||||
promtail = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 9080;
|
||||
};
|
||||
clients = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
url = mkOption {type = types.str;};
|
||||
tenant_id = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [{url = "http://monitor.${homelabCfg.domain}:3100/loki/api/v1/push";}];
|
||||
};
|
||||
};
|
||||
|
||||
sources = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
name = mkOption {type = types.str;};
|
||||
type = mkOption {
|
||||
type = types.enum ["journal" "file" "syslog" "docker"];
|
||||
default = "file";
|
||||
};
|
||||
files = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
paths = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
};
|
||||
multiline = mkOption {
|
||||
type = types.nullOr types.attrs;
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
};
|
||||
journal = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/log/journal";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
};
|
||||
labels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
};
|
||||
pipelineStages = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = [];
|
||||
};
|
||||
enabled = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
};
|
||||
|
||||
defaultLabels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {
|
||||
hostname = homelabCfg.hostname;
|
||||
environment = homelabCfg.environment;
|
||||
location = homelabCfg.location;
|
||||
};
|
||||
};
|
||||
|
||||
# Always exposed aggregated data
|
||||
allSources = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = [];
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
global = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
readOnly = true;
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Local setup
|
||||
services.promtail = mkIf cfg.promtail.enable {
|
||||
enable = true;
|
||||
configuration = {
|
||||
server = {
|
||||
http_listen_port = cfg.promtail.port;
|
||||
grpc_listen_port = 0;
|
||||
};
|
||||
positions.filename = "/var/lib/promtail/positions.yaml";
|
||||
clients = cfg.promtail.clients;
|
||||
scrape_configs = map (source:
|
||||
{
|
||||
job_name = source.name;
|
||||
static_configs = [
|
||||
{
|
||||
targets = ["localhost"];
|
||||
labels =
|
||||
cfg.defaultLabels
|
||||
// source.labels
|
||||
// (
|
||||
if source.type == "file"
|
||||
then {
|
||||
__path__ = concatStringsSep "," source.files.paths;
|
||||
}
|
||||
else {}
|
||||
);
|
||||
}
|
||||
];
|
||||
# pipeline_stages = source.pipelineStages;
|
||||
}
|
||||
// (
|
||||
if source.type == "journal"
|
||||
then {
|
||||
journal = {
|
||||
path = source.journal.path;
|
||||
labels = cfg.defaultLabels // source.labels;
|
||||
};
|
||||
}
|
||||
else {}
|
||||
))
|
||||
localAggregation.allSources;
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = optionals cfg.promtail.enable [cfg.promtail.port];
|
||||
|
||||
homelab.logging = {
|
||||
allSources = localAggregation.allSources;
|
||||
global =
|
||||
if hasNodes
|
||||
then globalAggregation
|
||||
else {};
|
||||
};
|
||||
};
|
||||
}
|
||||
222
modules/homelab/lib/systems/monitoring.nix
Normal file
222
modules/homelab/lib/systems/monitoring.nix
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
nodes,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.homelab.monitoring;
|
||||
homelabCfg = config.homelab;
|
||||
hasNodes = length (attrNames nodes) > 0;
|
||||
|
||||
# Local aggregation from this instance
|
||||
localAggregation = {
|
||||
# Metrics from manually configured + automatic node exporter
|
||||
allMetrics =
|
||||
cfg.metrics
|
||||
++ (optional cfg.nodeExporter.enable {
|
||||
name = "node-exporter";
|
||||
host = homelabCfg.hostname;
|
||||
port = cfg.nodeExporter.port;
|
||||
path = "/metrics";
|
||||
jobName = "node";
|
||||
scrapeInterval = "30s";
|
||||
labels = {
|
||||
instance = "${homelabCfg.hostname}.${homelabCfg.domain}";
|
||||
environment = homelabCfg.environment;
|
||||
location = homelabCfg.location;
|
||||
};
|
||||
});
|
||||
|
||||
allHealthChecks = cfg.healthChecks;
|
||||
};
|
||||
|
||||
# Global aggregation from all nodes (when nodes available)
|
||||
globalAggregation = let
|
||||
baseAgg = import ../aggregators/base.nix {inherit lib;};
|
||||
|
||||
# Aggregate metrics from all nodes
|
||||
metricsAgg = baseAgg.aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.monitoring.allMetrics";
|
||||
enhancer = endpoint:
|
||||
endpoint
|
||||
// {
|
||||
_fullAddress = "${endpoint.host}:${toString endpoint.port}";
|
||||
_metricsUrl = "http://${endpoint.host}:${toString endpoint.port}${endpoint.path}";
|
||||
};
|
||||
};
|
||||
|
||||
# Aggregate health checks from all nodes
|
||||
healthChecksAgg = baseAgg.aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.monitoring.allHealthChecks";
|
||||
enhancer = check: let
|
||||
actualHost = check.host;
|
||||
portPart =
|
||||
if check.port != null
|
||||
then ":${toString check.port}"
|
||||
else "";
|
||||
url = "${check.protocol or "http"}://${actualHost}${portPart}${check.path}";
|
||||
in
|
||||
check
|
||||
// {
|
||||
_actualHost = actualHost;
|
||||
_url = url;
|
||||
};
|
||||
};
|
||||
in {
|
||||
allMetrics = metricsAgg.all;
|
||||
allHealthChecks = healthChecksAgg.all;
|
||||
|
||||
# Useful groupings for services
|
||||
metricsByJobName = groupBy (m: m.jobName) metricsAgg.all;
|
||||
healthChecksByGroup = groupBy (hc: hc.group or "default") healthChecksAgg.all;
|
||||
|
||||
summary = {
|
||||
totalMetrics = length metricsAgg.all;
|
||||
totalHealthChecks = length healthChecksAgg.all;
|
||||
nodesCovered = unique (map (m: m._nodeName or m.host) metricsAgg.all);
|
||||
};
|
||||
};
|
||||
in {
|
||||
# Instance-level monitoring options
|
||||
options.homelab.monitoring = {
|
||||
enable = mkEnableOption "monitoring system";
|
||||
|
||||
# Node exporter (automatically enabled)
|
||||
nodeExporter = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 9100;
|
||||
};
|
||||
};
|
||||
|
||||
# Manual metrics (in addition to service auto-registration)
|
||||
metrics = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
name = mkOption {type = types.str;};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = homelabCfg.hostname;
|
||||
};
|
||||
port = mkOption {type = types.port;};
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/metrics";
|
||||
};
|
||||
jobName = mkOption {type = types.str;};
|
||||
scrapeInterval = mkOption {
|
||||
type = types.str;
|
||||
default = "30s";
|
||||
};
|
||||
labels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
};
|
||||
|
||||
# Manual health checks (in addition to service auto-registration)
|
||||
healthChecks = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
name = mkOption {type = types.str;};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = homelabCfg.hostname;
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
};
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/";
|
||||
};
|
||||
protocol = mkOption {
|
||||
type = types.enum ["http" "https" "tcp" "icmp"];
|
||||
default = "http";
|
||||
};
|
||||
method = mkOption {
|
||||
type = types.str;
|
||||
default = "GET";
|
||||
};
|
||||
interval = mkOption {
|
||||
type = types.str;
|
||||
default = "30s";
|
||||
};
|
||||
timeout = mkOption {
|
||||
type = types.str;
|
||||
default = "10s";
|
||||
};
|
||||
conditions = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = ["[STATUS] == 200"];
|
||||
};
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "manual";
|
||||
};
|
||||
labels = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
};
|
||||
enabled = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
};
|
||||
|
||||
# Read-only aggregated data (always exposed)
|
||||
allMetrics = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = localAggregation.allMetrics;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
allHealthChecks = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = localAggregation.allHealthChecks;
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
# Global aggregation (always available, empty if no nodes)
|
||||
global = mkOption {
|
||||
type = types.attrs;
|
||||
default = globalAggregation;
|
||||
readOnly = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Configuration - always includes both local and global
|
||||
config = mkIf cfg.enable {
|
||||
# Basic instance setup
|
||||
services.prometheus.exporters.node = mkIf cfg.nodeExporter.enable {
|
||||
enable = true;
|
||||
port = cfg.nodeExporter.port;
|
||||
enabledCollectors = ["systemd" "textfile" "filesystem" "loadavg" "meminfo" "netdev" "stat"];
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = optionals cfg.nodeExporter.enable [cfg.nodeExporter.port];
|
||||
|
||||
# homelab.monitoring = {
|
||||
# allMetrics = localAggregation.allMetrics;
|
||||
# allHealthChecks = localAggregation.allHealthChecks;
|
||||
# global =
|
||||
# if hasNodes
|
||||
# then globalAggregation
|
||||
# else {};
|
||||
# };
|
||||
};
|
||||
}
|
||||
98
modules/homelab/lib/systems/proxy.nix
Normal file
98
modules/homelab/lib/systems/proxy.nix
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
nodes,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.homelab.reverseProxy;
|
||||
homelabCfg = config.homelab;
|
||||
hasNodes = length (attrNames nodes) > 0;
|
||||
|
||||
# Local aggregation
|
||||
localAggregation = {
|
||||
allEntries = cfg.entries;
|
||||
};
|
||||
|
||||
# Global aggregation
|
||||
globalAggregation = let
|
||||
baseAgg = import ../aggregators/base.nix {inherit lib;};
|
||||
|
||||
entriesAgg = baseAgg.aggregateFromNodes {
|
||||
inherit nodes;
|
||||
attributePath = "homelab.reverseProxy.allEntries";
|
||||
enhancer = entry:
|
||||
entry
|
||||
// {
|
||||
_upstream = "http://${entry.host}:${toString entry.port}${entry.path or ""}";
|
||||
_fqdn = "${entry.subdomain}.${entry._nodeConfig.config.homelab.externalDomain or homelabCfg.externalDomain}";
|
||||
_internal = "${entry.host}:${toString entry.port}";
|
||||
};
|
||||
};
|
||||
in {
|
||||
allEntries = entriesAgg.all;
|
||||
entriesBySubdomain = groupBy (e: e.subdomain) entriesAgg.all;
|
||||
entriesWithAuth = entriesAgg.filterBy (e: e.enableAuth or false);
|
||||
entriesWithoutAuth = entriesAgg.filterBy (e: !(e.enableAuth or false));
|
||||
summary = {
|
||||
total = length entriesAgg.all;
|
||||
byNode = entriesAgg.countBy (e: e._nodeName);
|
||||
withAuth = length (entriesAgg.filterBy (e: e.enableAuth or false));
|
||||
withoutAuth = length (entriesAgg.filterBy (e: !(e.enableAuth or false)));
|
||||
};
|
||||
};
|
||||
in {
|
||||
options.homelab.reverseProxy = {
|
||||
enable = mkEnableOption "reverse proxy system";
|
||||
|
||||
entries = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
subdomain = mkOption {type = types.str;};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = homelabCfg.hostname;
|
||||
};
|
||||
port = mkOption {type = types.port;};
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
default = "/";
|
||||
};
|
||||
enableAuth = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
};
|
||||
enableSSL = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [];
|
||||
};
|
||||
|
||||
# Always exposed aggregated data
|
||||
allEntries = mkOption {
|
||||
type = types.listOf types.attrs;
|
||||
default = [];
|
||||
readOnly = true;
|
||||
};
|
||||
|
||||
global = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
readOnly = true;
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# Always expose both local and global
|
||||
homelab.reverseProxy = {
|
||||
allEntries = localAggregation.allEntries;
|
||||
global =
|
||||
if hasNodes
|
||||
then globalAggregation
|
||||
else {};
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue