homelab/modules/homelab/lib/node-aggregation.nix
plasmagoat bcbcc8b17b
Some checks failed
Test / tests (push) Has been cancelled
/ OpenTofu (push) Has been cancelled
homelab framework module init (everything is a mess)
2025-07-28 02:05:13 +02:00

226 lines
7.8 KiB
Nix

{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;
};
}