fix(backends): Backend-Löschen verweigern wenn Routing-Regeln darauf zeigen (v1.1.142)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -97,6 +97,15 @@ async function listDomains(): Promise<DomainFull[]> {
|
||||
return (r.data.data as { domains?: DomainFull[] }).domains ?? []
|
||||
}
|
||||
|
||||
interface RoutingRule { id: number; backend_id: number }
|
||||
async function listRoutingRules(): Promise<RoutingRule[]> {
|
||||
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<BackendFormValues>()
|
||||
|
||||
@@ -296,13 +309,18 @@ export default function BackendsPage() {
|
||||
},
|
||||
{
|
||||
title: t('common.actions'), key: 'actions',
|
||||
render: (_, row) => (
|
||||
<ActionButtons
|
||||
onEdit={() => navigate(`/backends/${row.id}`)}
|
||||
onDelete={() => del.mutate(row.id)}
|
||||
deleteConfirm={t('backends.deleteConfirm', { name: row.name })}
|
||||
/>
|
||||
),
|
||||
render: (_, row) => {
|
||||
const n = ruleCountForBackend(row.id)
|
||||
return (
|
||||
<ActionButtons
|
||||
onEdit={() => navigate(`/backends/${row.id}`)}
|
||||
onDelete={() => del.mutate(row.id)}
|
||||
deleteConfirm={t('backends.deleteConfirm', { name: row.name })}
|
||||
deleteDisabled={n > 0}
|
||||
deleteDisabledReason={t('backends.deleteInUse', { n })}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user