{ config, lib, pkgs, ... }: with lib; let jsonFormat = pkgs.formats.json {}; lbBackendType = types.submodule { options = { name = mkOption { type = types.str; description = "Backend name"; }; address = mkOption { type = types.str; description = "Backend IP or hostname"; }; port = mkOption { type = types.port; description = "Backend port"; }; weight = mkOption { type = types.nullOr types.int; default = null; description = "Backend weight (default: 1)"; }; admin_state = mkOption { type = types.nullOr types.str; default = null; description = "Backend admin state (enabled, disabled, drain)"; }; }; }; lbSessionPersistenceType = types.submodule { options = { type = mkOption { type = types.enum [ "source_ip" "cookie" "app_cookie" ]; description = "Persistence strategy"; }; cookie_name = mkOption { type = types.nullOr types.str; default = null; description = "Cookie name for cookie-based persistence"; }; timeout_seconds = mkOption { type = types.nullOr types.int; default = null; description = "Persistence timeout in seconds"; }; }; }; lbHttpHealthType = types.submodule { options = { method = mkOption { type = types.nullOr types.str; default = null; description = "HTTP method for health checks"; }; path = mkOption { type = types.nullOr types.str; default = null; description = "HTTP path for health checks"; }; expected_codes = mkOption { type = types.listOf types.int; default = []; description = "Expected HTTP status codes"; }; host = mkOption { type = types.nullOr types.str; default = null; description = "Host header for HTTP health checks"; }; }; }; lbHealthCheckType = types.submodule { options = { name = mkOption { type = types.str; description = "Health check name"; }; type = mkOption { type = types.enum [ "tcp" "http" "https" "udp" "ping" ]; description = "Health check type"; }; interval_seconds = mkOption { type = types.nullOr types.int; default = null; description = "Interval in seconds"; }; timeout_seconds = mkOption { type = types.nullOr types.int; default = null; description = "Timeout in seconds"; }; healthy_threshold = mkOption { type = types.nullOr types.int; default = null; description = "Healthy threshold"; }; unhealthy_threshold = mkOption { type = types.nullOr types.int; default = null; description = "Unhealthy threshold"; }; http = mkOption { type = types.nullOr lbHttpHealthType; default = null; description = "HTTP-specific health check configuration"; }; enabled = mkOption { type = types.nullOr types.bool; default = null; description = "Enable or disable health check"; }; }; }; lbPoolType = types.submodule { options = { name = mkOption { type = types.str; description = "Pool name"; }; algorithm = mkOption { type = types.nullOr types.str; default = null; description = "Pool algorithm (round_robin, least_connections, ip_hash, weighted_round_robin, random, maglev)"; }; protocol = mkOption { type = types.nullOr types.str; default = null; description = "Pool protocol (tcp, udp, http, https)"; }; session_persistence = mkOption { type = types.nullOr lbSessionPersistenceType; default = null; description = "Session persistence configuration"; }; backends = mkOption { type = types.listOf lbBackendType; default = []; description = "Pool backends"; }; health_checks = mkOption { type = types.listOf lbHealthCheckType; default = []; description = "Pool health checks"; }; }; }; lbTlsType = types.submodule { options = { certificate_id = mkOption { type = types.str; description = "Certificate ID (FiberLB)"; }; min_version = mkOption { type = types.nullOr types.str; default = null; description = "TLS minimum version (tls_1_2, tls_1_3)"; }; cipher_suites = mkOption { type = types.listOf types.str; default = []; description = "TLS cipher suites"; }; }; }; lbL7RuleType = types.submodule { options = { type = mkOption { type = types.enum [ "host_name" "path" "file_type" "header" "cookie" "ssl_conn_has_sni" ]; description = "L7 rule type"; }; compare_type = mkOption { type = types.nullOr (types.enum [ "equal_to" "regex" "starts_with" "ends_with" "contains" ]); default = null; description = "L7 compare type"; }; value = mkOption { type = types.str; description = "L7 rule match value"; }; key = mkOption { type = types.nullOr types.str; default = null; description = "Header/cookie key for L7 rules"; }; invert = mkOption { type = types.nullOr types.bool; default = null; description = "Invert the rule match"; }; }; }; lbL7PolicyType = types.submodule { options = { name = mkOption { type = types.str; description = "L7 policy name"; }; position = mkOption { type = types.nullOr types.int; default = null; description = "L7 policy position (lower evaluates first)"; }; action = mkOption { type = types.enum [ "redirect_to_pool" "redirect_to_url" "reject" ]; description = "L7 policy action"; }; redirect_url = mkOption { type = types.nullOr types.str; default = null; description = "Redirect URL for redirect_to_url action"; }; redirect_pool = mkOption { type = types.nullOr types.str; default = null; description = "Target pool name for redirect_to_pool action"; }; redirect_http_status_code = mkOption { type = types.nullOr types.int; default = null; description = "Redirect or reject HTTP status code"; }; enabled = mkOption { type = types.nullOr types.bool; default = null; description = "Enable or disable the policy"; }; rules = mkOption { type = types.listOf lbL7RuleType; default = []; description = "L7 rules for this policy"; }; }; }; lbListenerType = types.submodule { options = { name = mkOption { type = types.str; description = "Listener name"; }; protocol = mkOption { type = types.nullOr types.str; default = null; description = "Listener protocol (tcp, udp, http, https, terminated_https)"; }; port = mkOption { type = types.port; description = "Listener port"; }; default_pool = mkOption { type = types.str; description = "Default pool name"; }; tls = mkOption { type = types.nullOr lbTlsType; default = null; description = "TLS configuration"; }; connection_limit = mkOption { type = types.nullOr types.int; default = null; description = "Connection limit"; }; enabled = mkOption { type = types.nullOr types.bool; default = null; description = "Enable or disable listener"; }; l7_policies = mkOption { type = types.listOf lbL7PolicyType; default = []; description = "L7 policies for this listener"; }; }; }; lbType = types.submodule { options = { name = mkOption { type = types.str; description = "Load balancer name"; }; org_id = mkOption { type = types.str; description = "Organization ID"; }; project_id = mkOption { type = types.nullOr types.str; default = null; description = "Project ID (null for org-level)"; }; description = mkOption { type = types.nullOr types.str; default = null; description = "Load balancer description"; }; pools = mkOption { type = types.listOf lbPoolType; default = []; description = "Pools for this load balancer"; }; listeners = mkOption { type = types.listOf lbListenerType; default = []; description = "Listeners for this load balancer"; }; }; }; dnsRecordType = types.submodule { options = { name = mkOption { type = types.str; description = "Record name"; }; record_type = mkOption { type = types.str; description = "Record type (A, AAAA, CNAME, MX, TXT, SRV, NS, PTR, CAA)"; }; ttl = mkOption { type = types.nullOr types.int; default = null; description = "Record TTL (default: 300)"; }; data = mkOption { type = types.attrsOf types.anything; description = "Record data (type-specific)"; }; enabled = mkOption { type = types.nullOr types.bool; default = null; description = "Enable or disable record"; }; }; }; dnsZoneType = types.submodule { options = { name = mkOption { type = types.str; description = "Zone name (example.com)"; }; org_id = mkOption { type = types.str; description = "Organization ID"; }; project_id = mkOption { type = types.nullOr types.str; default = null; description = "Project ID (null for org-level)"; }; primary_ns = mkOption { type = types.nullOr types.str; default = null; description = "Primary NS hostname"; }; admin_email = mkOption { type = types.nullOr types.str; default = null; description = "Admin email for SOA"; }; refresh = mkOption { type = types.nullOr types.int; default = null; description = "SOA refresh"; }; retry = mkOption { type = types.nullOr types.int; default = null; description = "SOA retry"; }; expire = mkOption { type = types.nullOr types.int; default = null; description = "SOA expire"; }; minimum = mkOption { type = types.nullOr types.int; default = null; description = "SOA minimum"; }; records = mkOption { type = types.listOf dnsRecordType; default = []; description = "Zone records"; }; }; }; dnsReverseZoneType = types.submodule { options = { org_id = mkOption { type = types.str; description = "Organization ID"; }; project_id = mkOption { type = types.nullOr types.str; default = null; description = "Project ID (null for org-level)"; }; cidr = mkOption { type = types.str; description = "CIDR for the reverse zone"; }; ptr_pattern = mkOption { type = types.str; description = "PTR pattern (e.g., {4}-{3}-{2}-{1}.hosts.example.com.)"; }; ttl = mkOption { type = types.nullOr types.int; default = null; description = "PTR TTL"; }; }; }; lbCfg = config.plasmacloud.lb; dnsCfg = config.plasmacloud.dns; lbConfigFile = jsonFormat.generate "plasmacloud-lb.json" { load_balancers = lbCfg.loadBalancers; }; lbConfigPath = lbCfg.configPath; lbConfigRelative = removePrefix "/etc/" lbConfigPath; dnsConfigFile = jsonFormat.generate "plasmacloud-dns.json" { zones = dnsCfg.zones; reverse_zones = dnsCfg.reverseZones; }; dnsConfigPath = dnsCfg.configPath; dnsConfigRelative = removePrefix "/etc/" dnsConfigPath; in { options.plasmacloud.lb = { enable = mkEnableOption "PlasmaCloud load balancer declarations"; endpoint = mkOption { type = types.str; default = "http://127.0.0.1:7000"; description = "FiberLB gRPC endpoint"; }; loadBalancers = mkOption { type = types.listOf lbType; default = []; description = "Load balancer declarations"; }; configPath = mkOption { type = types.str; default = "/etc/plasmacloud/lb.json"; description = "Path for rendered load balancer config"; }; applyOnBoot = mkOption { type = types.bool; default = true; description = "Apply declarations at boot"; }; applyOnChange = mkOption { type = types.bool; default = true; description = "Apply declarations when the config file changes"; }; prune = mkOption { type = types.bool; default = false; description = "Delete load balancer resources not declared in config"; }; package = mkOption { type = types.package; default = pkgs.plasmacloud-reconciler or (throw "plasmacloud-reconciler package not found"); description = "Reconciler package for load balancer declarations"; }; }; options.plasmacloud.dns = { enable = mkEnableOption "PlasmaCloud DNS declarations"; endpoint = mkOption { type = types.str; default = "http://127.0.0.1:6000"; description = "FlashDNS gRPC endpoint"; }; zones = mkOption { type = types.listOf dnsZoneType; default = []; description = "DNS zone declarations"; }; reverseZones = mkOption { type = types.listOf dnsReverseZoneType; default = []; description = "Reverse DNS zone declarations"; }; configPath = mkOption { type = types.str; default = "/etc/plasmacloud/dns.json"; description = "Path for rendered DNS config"; }; applyOnBoot = mkOption { type = types.bool; default = true; description = "Apply declarations at boot"; }; applyOnChange = mkOption { type = types.bool; default = true; description = "Apply declarations when the config file changes"; }; prune = mkOption { type = types.bool; default = false; description = "Delete DNS resources not declared in config"; }; package = mkOption { type = types.package; default = pkgs.plasmacloud-reconciler or (throw "plasmacloud-reconciler package not found"); description = "Reconciler package for DNS declarations"; }; }; config = mkMerge [ (mkIf lbCfg.enable { assertions = [ { assertion = hasPrefix "/etc/" lbConfigPath; message = "plasmacloud.lb.configPath must be under /etc"; } ]; environment.etc."${lbConfigRelative}".source = lbConfigFile; systemd.services.plasmacloud-lb-apply = { description = "Apply PlasmaCloud load balancer declarations"; after = [ "network-online.target" ] ++ (optional config.services.fiberlb.enable "fiberlb.service"); wants = [ "network-online.target" ] ++ (optional config.services.fiberlb.enable "fiberlb.service"); wantedBy = optional lbCfg.applyOnBoot "multi-user.target"; serviceConfig = { Type = "oneshot"; ExecStart = "${lbCfg.package}/bin/plasmacloud-reconciler lb --config ${lbConfigPath} --endpoint ${lbCfg.endpoint}${optionalString lbCfg.prune " --prune"}"; }; }; systemd.paths.plasmacloud-lb-apply = mkIf lbCfg.applyOnChange { wantedBy = [ "multi-user.target" ]; pathConfig = { PathChanged = lbConfigPath; }; }; }) (mkIf dnsCfg.enable { assertions = [ { assertion = hasPrefix "/etc/" dnsConfigPath; message = "plasmacloud.dns.configPath must be under /etc"; } ]; environment.etc."${dnsConfigRelative}".source = dnsConfigFile; systemd.services.plasmacloud-dns-apply = { description = "Apply PlasmaCloud DNS declarations"; after = [ "network-online.target" ] ++ (optional config.services.flashdns.enable "flashdns.service"); wants = [ "network-online.target" ] ++ (optional config.services.flashdns.enable "flashdns.service"); wantedBy = optional dnsCfg.applyOnBoot "multi-user.target"; serviceConfig = { Type = "oneshot"; ExecStart = "${dnsCfg.package}/bin/plasmacloud-reconciler dns --config ${dnsConfigPath} --endpoint ${dnsCfg.endpoint}${optionalString dnsCfg.prune " --prune"}"; }; }; systemd.paths.plasmacloud-dns-apply = mkIf dnsCfg.applyOnChange { wantedBy = [ "multi-user.target" ]; pathConfig = { PathChanged = dnsConfigPath; }; }; }) ]; }