295 lines
8.5 KiB
Nix
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");
|
|
};
|
|
};
|
|
}
|