[reverse-proxy] Fix gw.viktorbarzin.me — point at 192.168.1.1 via EndpointSlice
The TP-Link gateway was wired via ExternalName `gw.viktorbarzin.lan`, but Technitium has no record for that name (the router isn't a DHCP client and Kea DDNS never registers it), so the ingress backend returned NXDOMAIN and the `[External] gw` Uptime Kuma monitor was permanently failing. Factory now accepts `backend_ip` as an alternative to `external_name`: it creates a selector-less ClusterIP Service + manual EndpointSlice pointing at the given IP, bypassing cluster DNS entirely. Used for gw (192.168.1.1); the old ExternalName path is retained for every other service. Also add a direct `port` monitor for the router in uptime-kuma's internal_monitors list so we can tell a Cloudflare/tunnel outage apart from the router itself being down. Extended the internal-monitor-sync script to handle non-DB monitor types (hostname + port fields).
This commit is contained in:
parent
4b39fbb717
commit
a86a97deb7
3 changed files with 112 additions and 25 deletions
|
|
@ -14,7 +14,16 @@ variable "name" {}
|
|||
variable "namespace" {
|
||||
default = "reverse-proxy"
|
||||
}
|
||||
variable "external_name" {}
|
||||
variable "external_name" {
|
||||
type = string
|
||||
default = null
|
||||
description = "DNS name for ExternalName Service. Mutually exclusive with backend_ip."
|
||||
}
|
||||
variable "backend_ip" {
|
||||
type = string
|
||||
default = null
|
||||
description = "IP address backend. When set, creates a selector-less Service + EndpointSlice pointing at this IP. Mutually exclusive with external_name — use for hosts that aren't in Technitium (e.g. upstream gateways)."
|
||||
}
|
||||
variable "port" {
|
||||
default = "80"
|
||||
}
|
||||
|
|
@ -95,7 +104,14 @@ variable "public_ipv6" {
|
|||
}
|
||||
|
||||
|
||||
locals {
|
||||
use_backend_ip = var.backend_ip != null
|
||||
port_name = var.backend_protocol == "HTTPS" ? "https-${var.name}" : "${var.name}-web"
|
||||
}
|
||||
|
||||
# ExternalName flavor — used when the backend is addressable by DNS.
|
||||
resource "kubernetes_service" "proxied-service" {
|
||||
count = local.use_backend_ip ? 0 : 1
|
||||
metadata {
|
||||
name = var.name
|
||||
namespace = var.namespace
|
||||
|
|
@ -109,7 +125,7 @@ resource "kubernetes_service" "proxied-service" {
|
|||
external_name = var.external_name
|
||||
|
||||
port {
|
||||
name = var.backend_protocol == "HTTPS" ? "https-${var.name}" : "${var.name}-web"
|
||||
name = local.port_name
|
||||
port = var.port
|
||||
protocol = "TCP"
|
||||
target_port = var.port
|
||||
|
|
@ -117,6 +133,58 @@ resource "kubernetes_service" "proxied-service" {
|
|||
}
|
||||
}
|
||||
|
||||
# IP-backend flavor — selector-less Service + manually-managed EndpointSlice.
|
||||
# Used for upstreams that have no DNS entry in Technitium (e.g. 192.168.1.1).
|
||||
resource "kubernetes_service" "ip-backend-service" {
|
||||
count = local.use_backend_ip ? 1 : 0
|
||||
metadata {
|
||||
name = var.name
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app" = var.name
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
type = "ClusterIP"
|
||||
port {
|
||||
name = local.port_name
|
||||
port = var.port
|
||||
protocol = "TCP"
|
||||
target_port = var.port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_manifest" "ip_backend_endpointslice" {
|
||||
count = local.use_backend_ip ? 1 : 0
|
||||
manifest = {
|
||||
apiVersion = "discovery.k8s.io/v1"
|
||||
kind = "EndpointSlice"
|
||||
metadata = {
|
||||
name = var.name
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"kubernetes.io/service-name" = var.name
|
||||
"app" = var.name
|
||||
}
|
||||
}
|
||||
addressType = "IPv4"
|
||||
ports = [{
|
||||
name = local.port_name
|
||||
port = tonumber(var.port)
|
||||
protocol = "TCP"
|
||||
}]
|
||||
endpoints = [{
|
||||
addresses = [var.backend_ip]
|
||||
conditions = {
|
||||
ready = true
|
||||
}
|
||||
}]
|
||||
}
|
||||
depends_on = [kubernetes_service.ip-backend-service]
|
||||
}
|
||||
|
||||
locals {
|
||||
# External monitor defaults: on when proxied, off otherwise. Explicit bool overrides.
|
||||
effective_external_monitor = var.external_monitor != null ? var.external_monitor : (var.dns_type == "proxied")
|
||||
|
|
|
|||
|
|
@ -112,13 +112,11 @@ module "idrac" {
|
|||
depends_on = [kubernetes_namespace.reverse-proxy]
|
||||
}
|
||||
|
||||
# Can either listen on https or http; can't do both :/
|
||||
# TODO: Not working yet
|
||||
module "tp-link-gateway" {
|
||||
source = "./factory"
|
||||
dns_type = "proxied"
|
||||
name = "gw"
|
||||
external_name = "gw.viktorbarzin.lan"
|
||||
backend_ip = "192.168.1.1"
|
||||
port = 443
|
||||
tls_secret_name = var.tls_secret_name
|
||||
backend_protocol = "HTTPS"
|
||||
|
|
|
|||
|
|
@ -552,6 +552,8 @@ locals {
|
|||
type = "mysql"
|
||||
database_connection_string = "mysql://uptimekuma@mysql.dbaas.svc.cluster.local:3306"
|
||||
database_password_vault_key = "uptimekuma_db_password"
|
||||
hostname = null
|
||||
port = null
|
||||
interval = 60
|
||||
retry_interval = 60
|
||||
max_retries = 2
|
||||
|
|
@ -565,6 +567,23 @@ locals {
|
|||
type = "redis"
|
||||
database_connection_string = "redis://redis-master.redis.svc.cluster.local:6379"
|
||||
database_password_vault_key = null
|
||||
hostname = null
|
||||
port = null
|
||||
interval = 60
|
||||
retry_interval = 30
|
||||
max_retries = 3
|
||||
},
|
||||
{
|
||||
# TP-Link home router upstream of pfSense. Complements the
|
||||
# `[External] gw` HTTPS monitor: this one checks the router
|
||||
# directly on 443, so we can tell a Cloudflare/tunnel outage
|
||||
# apart from the router itself being unreachable.
|
||||
name = "TP-Link Gateway (192.168.1.1)"
|
||||
type = "port"
|
||||
database_connection_string = null
|
||||
database_password_vault_key = null
|
||||
hostname = "192.168.1.1"
|
||||
port = 443
|
||||
interval = 60
|
||||
retry_interval = 30
|
||||
max_retries = 3
|
||||
|
|
@ -599,6 +618,8 @@ resource "kubernetes_config_map_v1" "internal_monitor_targets" {
|
|||
name = m.name
|
||||
type = m.type
|
||||
database_connection_string = m.database_connection_string
|
||||
hostname = m.hostname
|
||||
port = m.port
|
||||
password_env = m.database_password_vault_key != null ? "DB_PASSWORD_${upper(replace(m.name, "/[^A-Za-z0-9]/", "_"))}" : null
|
||||
interval = m.interval
|
||||
retry_interval = m.retry_interval
|
||||
|
|
@ -648,41 +669,41 @@ existing = {m["name"]: m for m in api.get_monitors()}
|
|||
|
||||
for t in targets:
|
||||
name = t["name"]
|
||||
mtype = MonitorType(t["type"])
|
||||
# MYSQL uses `databaseConnectionString` + `radiusPassword` (UK v2 re-uses
|
||||
# radiusPassword for mysql auth — backwards compat). Redis has auth
|
||||
# disabled on the cluster, so password_env is null.
|
||||
# disabled on the cluster, so password_env is null. PORT monitors use
|
||||
# hostname + port directly.
|
||||
desired = {
|
||||
"type": MonitorType(t["type"]),
|
||||
"type": mtype,
|
||||
"name": name,
|
||||
"databaseConnectionString": t["database_connection_string"],
|
||||
"interval": t["interval"],
|
||||
"retryInterval": t["retry_interval"],
|
||||
"maxretries": t["max_retries"],
|
||||
}
|
||||
if t.get("password_env"):
|
||||
desired["radiusPassword"] = os.environ[t["password_env"]]
|
||||
if mtype == MonitorType.PORT:
|
||||
desired["hostname"] = t["hostname"]
|
||||
desired["port"] = t["port"]
|
||||
else:
|
||||
desired["databaseConnectionString"] = t["database_connection_string"]
|
||||
if t.get("password_env"):
|
||||
desired["radiusPassword"] = os.environ[t["password_env"]]
|
||||
if name not in existing:
|
||||
print(f"Creating monitor: {name}")
|
||||
api.add_monitor(**desired)
|
||||
continue
|
||||
m = existing[name]
|
||||
drifted = (
|
||||
m.get("databaseConnectionString") != desired["databaseConnectionString"]
|
||||
or m.get("interval") != desired["interval"]
|
||||
or m.get("retryInterval") != desired["retryInterval"]
|
||||
or m.get("maxretries") != desired["maxretries"]
|
||||
or ("radiusPassword" in desired and m.get("radiusPassword") != desired["radiusPassword"])
|
||||
)
|
||||
drift_fields = ["interval", "retryInterval", "maxretries"]
|
||||
if mtype == MonitorType.PORT:
|
||||
drift_fields += ["hostname", "port"]
|
||||
else:
|
||||
drift_fields += ["databaseConnectionString"]
|
||||
if "radiusPassword" in desired:
|
||||
drift_fields += ["radiusPassword"]
|
||||
drifted = any(m.get(f) != desired.get(f) for f in drift_fields)
|
||||
if drifted:
|
||||
print(f"Updating monitor {name} (id={m['id']})")
|
||||
edit_kwargs = {
|
||||
"databaseConnectionString": desired["databaseConnectionString"],
|
||||
"interval": desired["interval"],
|
||||
"retryInterval": desired["retryInterval"],
|
||||
"maxretries": desired["maxretries"],
|
||||
}
|
||||
if "radiusPassword" in desired:
|
||||
edit_kwargs["radiusPassword"] = desired["radiusPassword"]
|
||||
edit_kwargs = {f: desired[f] for f in drift_fields if f in desired}
|
||||
api.edit_monitor(m["id"], **edit_kwargs)
|
||||
else:
|
||||
print(f"Monitor {name} (id={m['id']}) already in desired state")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue