[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" {
|
variable "namespace" {
|
||||||
default = "reverse-proxy"
|
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" {
|
variable "port" {
|
||||||
default = "80"
|
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" {
|
resource "kubernetes_service" "proxied-service" {
|
||||||
|
count = local.use_backend_ip ? 0 : 1
|
||||||
metadata {
|
metadata {
|
||||||
name = var.name
|
name = var.name
|
||||||
namespace = var.namespace
|
namespace = var.namespace
|
||||||
|
|
@ -109,7 +125,7 @@ resource "kubernetes_service" "proxied-service" {
|
||||||
external_name = var.external_name
|
external_name = var.external_name
|
||||||
|
|
||||||
port {
|
port {
|
||||||
name = var.backend_protocol == "HTTPS" ? "https-${var.name}" : "${var.name}-web"
|
name = local.port_name
|
||||||
port = var.port
|
port = var.port
|
||||||
protocol = "TCP"
|
protocol = "TCP"
|
||||||
target_port = var.port
|
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 {
|
locals {
|
||||||
# External monitor defaults: on when proxied, off otherwise. Explicit bool overrides.
|
# 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")
|
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]
|
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" {
|
module "tp-link-gateway" {
|
||||||
source = "./factory"
|
source = "./factory"
|
||||||
dns_type = "proxied"
|
dns_type = "proxied"
|
||||||
name = "gw"
|
name = "gw"
|
||||||
external_name = "gw.viktorbarzin.lan"
|
backend_ip = "192.168.1.1"
|
||||||
port = 443
|
port = 443
|
||||||
tls_secret_name = var.tls_secret_name
|
tls_secret_name = var.tls_secret_name
|
||||||
backend_protocol = "HTTPS"
|
backend_protocol = "HTTPS"
|
||||||
|
|
|
||||||
|
|
@ -552,6 +552,8 @@ locals {
|
||||||
type = "mysql"
|
type = "mysql"
|
||||||
database_connection_string = "mysql://uptimekuma@mysql.dbaas.svc.cluster.local:3306"
|
database_connection_string = "mysql://uptimekuma@mysql.dbaas.svc.cluster.local:3306"
|
||||||
database_password_vault_key = "uptimekuma_db_password"
|
database_password_vault_key = "uptimekuma_db_password"
|
||||||
|
hostname = null
|
||||||
|
port = null
|
||||||
interval = 60
|
interval = 60
|
||||||
retry_interval = 60
|
retry_interval = 60
|
||||||
max_retries = 2
|
max_retries = 2
|
||||||
|
|
@ -565,6 +567,23 @@ locals {
|
||||||
type = "redis"
|
type = "redis"
|
||||||
database_connection_string = "redis://redis-master.redis.svc.cluster.local:6379"
|
database_connection_string = "redis://redis-master.redis.svc.cluster.local:6379"
|
||||||
database_password_vault_key = null
|
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
|
interval = 60
|
||||||
retry_interval = 30
|
retry_interval = 30
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
|
|
@ -599,6 +618,8 @@ resource "kubernetes_config_map_v1" "internal_monitor_targets" {
|
||||||
name = m.name
|
name = m.name
|
||||||
type = m.type
|
type = m.type
|
||||||
database_connection_string = m.database_connection_string
|
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
|
password_env = m.database_password_vault_key != null ? "DB_PASSWORD_${upper(replace(m.name, "/[^A-Za-z0-9]/", "_"))}" : null
|
||||||
interval = m.interval
|
interval = m.interval
|
||||||
retry_interval = m.retry_interval
|
retry_interval = m.retry_interval
|
||||||
|
|
@ -648,41 +669,41 @@ existing = {m["name"]: m for m in api.get_monitors()}
|
||||||
|
|
||||||
for t in targets:
|
for t in targets:
|
||||||
name = t["name"]
|
name = t["name"]
|
||||||
|
mtype = MonitorType(t["type"])
|
||||||
# MYSQL uses `databaseConnectionString` + `radiusPassword` (UK v2 re-uses
|
# MYSQL uses `databaseConnectionString` + `radiusPassword` (UK v2 re-uses
|
||||||
# radiusPassword for mysql auth — backwards compat). Redis has auth
|
# 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 = {
|
desired = {
|
||||||
"type": MonitorType(t["type"]),
|
"type": mtype,
|
||||||
"name": name,
|
"name": name,
|
||||||
"databaseConnectionString": t["database_connection_string"],
|
|
||||||
"interval": t["interval"],
|
"interval": t["interval"],
|
||||||
"retryInterval": t["retry_interval"],
|
"retryInterval": t["retry_interval"],
|
||||||
"maxretries": t["max_retries"],
|
"maxretries": t["max_retries"],
|
||||||
}
|
}
|
||||||
if t.get("password_env"):
|
if mtype == MonitorType.PORT:
|
||||||
desired["radiusPassword"] = os.environ[t["password_env"]]
|
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:
|
if name not in existing:
|
||||||
print(f"Creating monitor: {name}")
|
print(f"Creating monitor: {name}")
|
||||||
api.add_monitor(**desired)
|
api.add_monitor(**desired)
|
||||||
continue
|
continue
|
||||||
m = existing[name]
|
m = existing[name]
|
||||||
drifted = (
|
drift_fields = ["interval", "retryInterval", "maxretries"]
|
||||||
m.get("databaseConnectionString") != desired["databaseConnectionString"]
|
if mtype == MonitorType.PORT:
|
||||||
or m.get("interval") != desired["interval"]
|
drift_fields += ["hostname", "port"]
|
||||||
or m.get("retryInterval") != desired["retryInterval"]
|
else:
|
||||||
or m.get("maxretries") != desired["maxretries"]
|
drift_fields += ["databaseConnectionString"]
|
||||||
or ("radiusPassword" in desired and m.get("radiusPassword") != desired["radiusPassword"])
|
if "radiusPassword" in desired:
|
||||||
)
|
drift_fields += ["radiusPassword"]
|
||||||
|
drifted = any(m.get(f) != desired.get(f) for f in drift_fields)
|
||||||
if drifted:
|
if drifted:
|
||||||
print(f"Updating monitor {name} (id={m['id']})")
|
print(f"Updating monitor {name} (id={m['id']})")
|
||||||
edit_kwargs = {
|
edit_kwargs = {f: desired[f] for f in drift_fields if f in desired}
|
||||||
"databaseConnectionString": desired["databaseConnectionString"],
|
|
||||||
"interval": desired["interval"],
|
|
||||||
"retryInterval": desired["retryInterval"],
|
|
||||||
"maxretries": desired["maxretries"],
|
|
||||||
}
|
|
||||||
if "radiusPassword" in desired:
|
|
||||||
edit_kwargs["radiusPassword"] = desired["radiusPassword"]
|
|
||||||
api.edit_monitor(m["id"], **edit_kwargs)
|
api.edit_monitor(m["id"], **edit_kwargs)
|
||||||
else:
|
else:
|
||||||
print(f"Monitor {name} (id={m['id']}) already in desired state")
|
print(f"Monitor {name} (id={m['id']}) already in desired state")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue