homelab/modules/homelab/lib/service-interface.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

295 lines
8.5 KiB
Nix

# 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");
};
};
}