From 7dd76d4b58f143f50476fcf35cc849b5709b7045 Mon Sep 17 00:00:00 2001 From: Debian Date: Fri, 29 May 2026 07:15:16 +0200 Subject: [PATCH] =?UTF-8?q?fix(backends):=20Backend-L=C3=B6schen=20verweig?= =?UTF-8?q?ern=20wenn=20Routing-Regeln=20darauf=20zeigen=20(v1.1.142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Service: Pre-Check auf routing_rules.backend_id → ErrInUse vor DELETE - Handler: ErrInUse → HTTP 409 Conflict statt 500 (response.Conflict helper) - UI: Delete-Button deaktiviert + Tooltip "Von N Regeln verwendet" wenn Referenzen existieren Co-Authored-By: Claude Sonnet 4.6 --- VERSION | 2 +- cmd/edgeguard-api/main.go | 2 +- cmd/edgeguard-ctl/main.go | 2 +- cmd/edgeguard-scheduler/main.go | 2 +- internal/handlers/backends.go | 4 +++ internal/handlers/response/response.go | 4 +++ internal/services/backends/backends.go | 14 +++++++- management-ui/src/i18n/locales/de/common.json | 1 + management-ui/src/i18n/locales/en/common.json | 1 + management-ui/src/pages/Backends/index.tsx | 32 +++++++++++++++---- 10 files changed, 52 insertions(+), 12 deletions(-) diff --git a/VERSION b/VERSION index 591b0f8..1b033d7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.141 +1.1.142 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 045cdea..8c564b3 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -60,7 +60,7 @@ import ( usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" ) -var version = "1.1.141" +var version = "1.1.142" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index 80e21ca..4db399a 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -11,7 +11,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) -var version = "1.1.141" +var version = "1.1.142" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index cf51fed..3f913c8 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -41,7 +41,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.141" +var version = "1.1.142" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/internal/handlers/backends.go b/internal/handlers/backends.go index b2a128c..fc3fb76 100644 --- a/internal/handlers/backends.go +++ b/internal/handlers/backends.go @@ -117,6 +117,10 @@ func (h *BackendsHandler) Delete(c *gin.Context) { response.NotFound(c, err) return } + if errors.Is(err, backends.ErrInUse) { + response.Conflict(c, err) + return + } response.Internal(c, err) return } diff --git a/internal/handlers/response/response.go b/internal/handlers/response/response.go index 62f9a7b..f2076f1 100644 --- a/internal/handlers/response/response.go +++ b/internal/handlers/response/response.go @@ -82,6 +82,10 @@ func Forbidden(c *gin.Context, err error) { Err(c, http.StatusForbidden, err) } +func Conflict(c *gin.Context, err error) { + Err(c, http.StatusConflict, err) +} + func Internal(c *gin.Context, err error) { Err(c, http.StatusInternalServerError, err) } diff --git a/internal/services/backends/backends.go b/internal/services/backends/backends.go index 9edc95b..cc49c05 100644 --- a/internal/services/backends/backends.go +++ b/internal/services/backends/backends.go @@ -14,7 +14,10 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/models" ) -var ErrNotFound = errors.New("backend not found") +var ( + ErrNotFound = errors.New("backend not found") + ErrInUse = errors.New("backend is referenced by one or more routing rules") +) type Repo struct { Pool *pgxpool.Pool @@ -99,6 +102,15 @@ RETURNING id, name, scheme, health_check_path, lb_algorithm, websocket, force_ht } func (r *Repo) Delete(ctx context.Context, id int64) error { + var n int + if err := r.Pool.QueryRow(ctx, + `SELECT COUNT(*) FROM routing_rules WHERE backend_id = $1`, id, + ).Scan(&n); err != nil { + return err + } + if n > 0 { + return ErrInUse + } tag, err := r.Pool.Exec(ctx, `DELETE FROM backends WHERE id = $1`, id) if err != nil { return err diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index cbfd347..729a6a2 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -387,6 +387,7 @@ "serverHintCreate": "Speichern legt nur den Pool an. Server kommen im nächsten Schritt — Pool öffnen → „Server hinzufügen\".", "actions": "Aktionen", "deleteConfirm": "Backend-Pool {{name}} wirklich löschen? Alle Server im Pool werden mitentfernt.", + "deleteInUse": "Von {{n}} Routing-Regel(n) verwendet — zuerst diese löschen", "backToList": "Zurück zur Übersicht", "settingsCard": "Pool-Einstellungen", "healthCheckHint": "HTTP-Pfad den HAProxy alle 5s pro Server prüft. Leer lassen = kein Health-Check.", diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index b28cf68..c8f118e 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -387,6 +387,7 @@ "serverHintCreate": "Saving creates the pool only. Add servers in the next step — open the pool and click “Add server”.", "actions": "Actions", "deleteConfirm": "Really delete backend pool {{name}}? All servers in the pool will be removed too.", + "deleteInUse": "Used by {{n}} routing rule(s) — remove those first", "backToList": "Back to backends", "settingsCard": "Pool settings", "healthCheckHint": "HTTP path HAProxy probes every 5s per server. Leave empty to disable health checks.", diff --git a/management-ui/src/pages/Backends/index.tsx b/management-ui/src/pages/Backends/index.tsx index f2f9837..0fb0432 100644 --- a/management-ui/src/pages/Backends/index.tsx +++ b/management-ui/src/pages/Backends/index.tsx @@ -97,6 +97,15 @@ async function listDomains(): Promise { return (r.data.data as { domains?: DomainFull[] }).domains ?? [] } +interface RoutingRule { id: number; backend_id: number } +async function listRoutingRules(): Promise { + try { + const r = await apiClient.get('/routing-rules') + if (!isEnvelope(r.data)) return [] + return (r.data.data as { routing_rules?: RoutingRule[] }).routing_rules ?? [] + } catch { return [] } +} + interface HAProxyStat { backend: string; server: string; status: string sessions: number; bytes_in: number; bytes_out: number @@ -124,6 +133,7 @@ export default function BackendsPage() { const { data, isLoading } = useQuery({ queryKey: ['backends'], queryFn: listBackends }) const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains }) + const { data: routingRules } = useQuery({ queryKey: ['routing-rules'], queryFn: listRoutingRules }) const { data: services } = useQuery({ queryKey: ['system', 'services'], queryFn: async () => { @@ -177,6 +187,9 @@ export default function BackendsPage() { const domainsForBackend = (id: number) => (domains ?? []).filter(d => d.primary_backend_id === id) + const ruleCountForBackend = (id: number) => + (routingRules ?? []).filter(r => r.backend_id === id).length + const [creating, setCreating] = useState(false) const [form] = Form.useForm() @@ -296,13 +309,18 @@ export default function BackendsPage() { }, { title: t('common.actions'), key: 'actions', - render: (_, row) => ( - navigate(`/backends/${row.id}`)} - onDelete={() => del.mutate(row.id)} - deleteConfirm={t('backends.deleteConfirm', { name: row.name })} - /> - ), + render: (_, row) => { + const n = ruleCountForBackend(row.id) + return ( + navigate(`/backends/${row.id}`)} + onDelete={() => del.mutate(row.id)} + deleteConfirm={t('backends.deleteConfirm', { name: row.name })} + deleteDisabled={n > 0} + deleteDisabledReason={t('backends.deleteInUse', { n })} + /> + ) + }, }, ]