fix: immediate fixes from Opus review (v0.1.22)

- trigger: CronJob moved to deployNS (sless-fn-{userNS}), was tr.Namespace
  Reason: with NetworkPolicy default-deny, pod in user-ns can't reach
  Service in sless-fn-ns. Co-locating CronJob with Service guarantees
  connectivity regardless of NetworkPolicy configuration.
  handleTriggerDeletion updated consistently.

- trigger: pin curlimages/curl to 8.5.0 (was :latest)
  Reason: reproducibility, no unexpected behavior changes from image updates.

- function: sort env vars in buildDeployment (was non-deterministic map range)
  Reason: non-deterministic order caused k8s to detect container spec 'change'
  on every reconcile → unnecessary pod restarts. Sorted order is stable.

- function: cleanup kaniko Job in handleDeletion
  Reason: if Function deleted during Building phase, kaniko Job continued
  running, wasting CPU/memory and pushing an unused image.

- invoke: filter hop-by-hop headers in proxy response (RFC 2616 §13.5.1)
  Reason: Transfer-Encoding especially dangerous — forwarding it corrupts
  response body framing for the client.

- config: SLESS_API_TOKEN no longer required
  Reason: dead code — field loaded but never passed to any component.
  Auth uses validateJWT() middleware, not static token.

Namespace lifecycle: user namespaces preserved on destroy (not changed).
E2E: apply 4 resources + destroy clean. Operator v0.1.22 deployed.
This commit is contained in:
“Naeel”
2026-03-11 09:20:09 +04:00
parent bca889d355
commit e761439546
5 changed files with 600 additions and 19 deletions
+19 -4
View File
@@ -9,6 +9,7 @@ package controllers
import (
"context"
"fmt"
"sort"
"time"
appsv1 "k8s.io/api/apps/v1"
@@ -221,8 +222,7 @@ func (r *FunctionReconciler) ensureDeployment(ctx context.Context, fn *slessv1al
}
return ctrl.Result{}, nil
}
// buildDeployment формирует Deployment манифест для функции.
// Изменено: 2026-03-11// buildDeployment формирует Deployment манифест для функции.
func (r *FunctionReconciler) buildDeployment(fn *slessv1alpha1.Function, namespace string) *appsv1.Deployment {
replicas := int32(1)
envVars := []corev1.EnvVar{
@@ -230,8 +230,16 @@ func (r *FunctionReconciler) buildDeployment(fn *slessv1alpha1.Function, namespa
// Формат: "module-name.funcName" (например: handler-http.handle)
{Name: "SLESS_ENTRYPOINT", Value: fn.Spec.Entrypoint},
}
for k, v := range fn.Spec.Env {
envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
// Сортируем ключи env vars для стабильного порядка в Pod spec.
// map range в Go — недетерминирован: разный порядок при каждом вызове.
// Нестабильный порядок → k8s видит изменение контейнера → лишние rollout'ы.
keys := make([]string, 0, len(fn.Spec.Env))
for k := range fn.Spec.Env {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
envVars = append(envVars, corev1.EnvVar{Name: k, Value: fn.Spec.Env[k]})
}
return &appsv1.Deployment{
@@ -304,6 +312,7 @@ func (r *FunctionReconciler) ensureRegistrySecret(ctx context.Context, targetNS
}
// handleDeletion обрабатывает удаление Function: удаляет Deployment, Service, Ingress и убирает finalizer.
// ВАЖНО: Namespace sless-fn-{userNS} НЕ удаляется — он принадлежит пользователю на всё время его существования.
func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
deployNS := "sless-fn-" + fn.Namespace
dep := &appsv1.Deployment{}
@@ -311,6 +320,12 @@ func (r *FunctionReconciler) handleDeletion(ctx context.Context, fn *slessv1alph
_ = r.Delete(ctx, dep)
}
// Если функция удалена в процессе сборки — убиваем kaniko Job.
// Без этого Job продолжит работу, займёт CPU/память и запушит образ которым никто не воспользуется.
if jobName := fn.Annotations["sless.kube5s.ru/build-job"]; jobName != "" {
_ = r.Builder.Cleanup(ctx, jobName)
}
// Удаляем Service и Ingress — созданы HTTP триггером, но именованы по функции.
// Если function_controller не удалит их, Ingress остаётся после destroy → 502.
svc := &corev1.Service{}
+12 -6
View File
@@ -1,4 +1,4 @@
// Изменено: 2026-03-10
// Изменено: 2026-03-11
// TriggerReconciler — контроллер триггеров.
// HTTP триггер: создаёт Service + Ingress в namespace функции.
// Cron триггер: создаёт k8s CronJob который периодически вызывает функцию по внутреннему URL.
@@ -218,6 +218,9 @@ func (r *TriggerReconciler) reconcileHTTP(ctx context.Context, tr *slessv1alpha1
// reconcileCron создаёт CronJob который вызывает функцию по HTTP внутри кластера.
// curl делает POST на внутренний Service функции — это исключает внешний round-trip.
// CronJob размещается в deployNS (sless-fn-{userNS}), НЕ в user-namespace:
// при NetworkPolicy default-deny под в user-ns не может достучаться до Service в sless-fn-ns.
// Размещение CronJob в том же namespace что и Service — гарантирует работу при любой политике.
func (r *TriggerReconciler) reconcileCron(ctx context.Context, tr *slessv1alpha1.Trigger, fn *slessv1alpha1.Function) (ctrl.Result, error) {
deployNS := "sless-fn-" + tr.Namespace
// Внутренний URL: Service должен быть создан HTTP триггером или заранее
@@ -226,7 +229,7 @@ func (r *TriggerReconciler) reconcileCron(ctx context.Context, tr *slessv1alpha1
wantCJ := &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: tr.Name,
Namespace: tr.Namespace,
Namespace: deployNS, // размещаем там же где Service функции
Labels: map[string]string{"managed-by": "sless", "trigger": tr.Name},
},
Spec: batchv1.CronJobSpec{
@@ -238,9 +241,10 @@ func (r *TriggerReconciler) reconcileCron(ctx context.Context, tr *slessv1alpha1
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{
{
// curlimages/curl вызывает функцию по внутреннему адресу
// curlimages/curl вызывает функцию по внутреннему адресу.
// Версия зафиксирована для воспроизводимости — не latest.
Name: "invoker",
Image: "curlimages/curl:latest",
Image: "curlimages/curl:8.5.0",
Command: []string{"curl", "-sf", "-X", "POST", funcURL},
},
},
@@ -252,7 +256,7 @@ func (r *TriggerReconciler) reconcileCron(ctx context.Context, tr *slessv1alpha1
}
existing := &batchv1.CronJob{}
if err := r.Get(ctx, client.ObjectKey{Name: tr.Name, Namespace: tr.Namespace}, existing); err != nil {
if err := r.Get(ctx, client.ObjectKey{Name: tr.Name, Namespace: deployNS}, existing); err != nil {
if errors.IsNotFound(err) {
if err := r.Create(ctx, wantCJ); err != nil {
return ctrl.Result{}, fmt.Errorf("create cronjob: %w", err)
@@ -277,10 +281,12 @@ func (r *TriggerReconciler) reconcileCron(ctx context.Context, tr *slessv1alpha1
}
// handleTriggerDeletion удаляет ресурсы триггера и убирает finalizer.
// CronJob ищется в deployNS — туда же куда reconcileCron его создаёт.
func (r *TriggerReconciler) handleTriggerDeletion(ctx context.Context, tr *slessv1alpha1.Trigger) (ctrl.Result, error) {
if tr.Spec.Type == slessv1alpha1.TriggerTypeCron {
deployNS := "sless-fn-" + tr.Namespace
cj := &batchv1.CronJob{}
if err := r.Get(ctx, client.ObjectKey{Name: tr.Name, Namespace: tr.Namespace}, cj); err == nil {
if err := r.Get(ctx, client.ObjectKey{Name: tr.Name, Namespace: deployNS}, cj); err == nil {
_ = r.Delete(ctx, cj)
}
}