fix: destroy route cleanup bug — delete Service+Ingress on trigger/function deletion

- controllers/trigger_controller.go: handleTriggerDeletion теперь удаляет Service
  и Ingress из sless-fn-{ns} при удалении HTTP-триггера
- controllers/function_controller.go: handleDeletion теперь удаляет Service и Ingress
  (имена совпадают с fn.Name); добавлен импорт netv1
- terraform/provider trigger_resource.go: Delete ждёт пока GetTrigger вернёт 404
  (до 90с) — провайдер не возвращает успех раньше чем cleanup завершён
- examples: версия провайдера обновлена до ~> 0.1.11

Operator: naeel/sless-operator:v0.1.17
Provider: v0.1.11

Fixes DESTROY_ROUTE_CLEANUP_BUG.md
This commit is contained in:
“Naeel”
2026-03-09 19:23:28 +04:00
parent 9b4ec729b5
commit 1676bf3c5a
7 changed files with 56 additions and 8 deletions
+13 -1
View File
@@ -13,6 +13,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -302,7 +303,7 @@ func (r *FunctionReconciler) ensureRegistrySecret(ctx context.Context, targetNS
return nil
}
// handleDeletion обрабатывает удаление Function: удаляет Deployment и убирает finalizer.
// handleDeletion обрабатывает удаление Function: удаляет Deployment, Service, Ingress и убирает finalizer.
func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
deployNS := "sless-fn-" + fn.Namespace
dep := &appsv1.Deployment{}
@@ -310,6 +311,17 @@ func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alph
_ = r.Delete(ctx, dep)
}
// Удаляем Service и Ingress — созданы HTTP триггером, но именованы по функции.
// Если function_controller не удалит их, Ingress остаётся после destroy → 502.
svc := &corev1.Service{}
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, svc); err == nil {
_ = r.Delete(ctx, svc)
}
ing := &netv1.Ingress{}
if err := r.Get(ctx, client.ObjectKey{Name: fn.Name, Namespace: deployNS}, ing); err == nil {
_ = r.Delete(ctx, ing)
}
fn.Finalizers = removeString(fn.Finalizers, finalizerName)
if err := r.Update(ctx, fn); err != nil {
return ctrl.Result{}, fmt.Errorf("remove finalizer: %w", err)
+19 -2
View File
@@ -282,8 +282,25 @@ func (r *TriggerReconciler) handleTriggerDeletion(ctx context.Context, tr *sless
_ = r.Delete(ctx, cj)
}
}
// Ingress/Service для HTTP триггеров оставляем — они могут быть нужны другим триггерам,
// полная очистка происходит при удалении Function через function_controller finalizer
// Для HTTP триггеров удаляем Service и Ingress из sless-fn-{namespace}.
// Имена ресурсов совпадают с именем функции (FunctionRef).
// Без этого Ingress остаётся висеть после destroy и endpoint возвращает 502.
if tr.Spec.Type == slessv1alpha1.TriggerTypeHTTP {
deployNS := "sless-fn-" + tr.Namespace
fnName := tr.Spec.FunctionRef
svc := &corev1.Service{}
if err := r.Get(ctx, client.ObjectKey{Name: fnName, Namespace: deployNS}, svc); err == nil {
_ = r.Delete(ctx, svc)
}
ing := &netv1.Ingress{}
if err := r.Get(ctx, client.ObjectKey{Name: fnName, Namespace: deployNS}, ing); err == nil {
_ = r.Delete(ctx, ing)
}
}
tr.Finalizers = removeString(tr.Finalizers, triggerFinalizer)
if err := r.Update(ctx, tr); err != nil {
return ctrl.Result{}, fmt.Errorf("remove trigger finalizer: %w", err)
+1 -1
View File
@@ -8,7 +8,7 @@ terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
version = "~> 0.1.11"
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ terraform {
# Провайдер для управления serverless функциями через sless API
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
version = "~> 0.1.11"
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
version = "~> 0.1.11"
}
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
version = "~> 0.1.11"
}
}
}
@@ -209,9 +209,28 @@ func (r *TriggerResource) Delete(ctx context.Context, req resource.DeleteRequest
return
}
if err := r.client.DeleteTrigger(ctx, r.client.Namespace, state.Name.ValueString()); err != nil {
name := state.Name.ValueString()
if err := r.client.DeleteTrigger(ctx, r.client.Namespace, name); err != nil {
resp.Diagnostics.AddError("delete trigger", err.Error())
return
}
// Ждём, пока триггер исчезнет из API.
// Финалайзер контроллера снимается ПОСЛЕ того, как Service+Ingress удалены,
// поэтому когда GetTrigger возвращает nil (404) — маршрут уже убран и endpoint мёртв.
deadline := time.Now().Add(90 * time.Second)
for time.Now().Before(deadline) {
tr, err := r.client.GetTrigger(ctx, r.client.Namespace, name)
if err != nil {
resp.Diagnostics.AddError("wait trigger gone", err.Error())
return
}
if tr == nil {
return // триггер исчез — cleanup завершён
}
time.Sleep(3 * time.Second)
}
resp.Diagnostics.AddError("wait trigger gone", fmt.Sprintf("trigger %s still exists after 90s", name))
}
// trToModel конвертирует API-ответ → state модель.