fix: бесконечный цикл сборки — idempotency guard через last-built-s3key аннотацию
Проблема: upload handler сбрасывал phase в Pending ПОСЛЕ того как контроллер уже выставил Building → бесконечный цикл (94 job'а). Решение: - controllers/function_controller.go: startBuild сначала ставит аннотацию last-built-s3key = spec.S3Key (idempotency guard), потом status. Reconcile стартует сборку только если spec.S3Key != last-built-s3key. - handler/upload.go: убран Status().Update() — контроллер сам управляет фазой. - builder.go: IsAlreadyExists при создании job не ошибка (parallel reconcile).
This commit is contained in:
@@ -69,24 +69,23 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
||||
return ctrl.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
// Определяем что делать в зависимости от текущей фазы
|
||||
switch fn.Status.Phase {
|
||||
case "", slessv1alpha1.FunctionPhasePending:
|
||||
// Новая функция или сброшена в Pending — запускаем сборку образа
|
||||
// Есть новый код (s3Key изменился по сравнению с последним запуском сборки)
|
||||
// и сборка сейчас не идёт — запускаем. Это единственное место где решается "нужна ли сборка".
|
||||
// Почему аннотация а не phase: phase может быть Failed/Ready от прошлого кода;
|
||||
// новый upload меняет Spec.S3Key → контроллер сам понимает что нужно пересобрать.
|
||||
builtKey := fn.Annotations["sless.kube5s.ru/last-built-s3key"]
|
||||
needsBuild := fn.Spec.S3Key != "" && builtKey != fn.Spec.S3Key
|
||||
|
||||
if needsBuild && fn.Status.Phase != slessv1alpha1.FunctionPhaseBuilding {
|
||||
logger.Info("starting build", "function", fn.Name)
|
||||
return r.startBuild(ctx, fn)
|
||||
}
|
||||
|
||||
switch fn.Status.Phase {
|
||||
case slessv1alpha1.FunctionPhaseBuilding:
|
||||
// Сборка уже запущена — проверяем статус Job'а
|
||||
return r.checkBuild(ctx, fn)
|
||||
|
||||
case slessv1alpha1.FunctionPhaseReady:
|
||||
// Функция готова — проверяем что Deployment существует и актуален
|
||||
return r.ensureDeployment(ctx, fn)
|
||||
|
||||
case slessv1alpha1.FunctionPhaseFailed:
|
||||
// Ничего не делаем — пользователь должен исправить spec и обновить объект
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
@@ -94,29 +93,35 @@ func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
||||
|
||||
const finalizerName = "sless.kube5s.ru/finalizer"
|
||||
|
||||
// startBuild переводит функцию в фазу Building и запускает kaniko Job.
|
||||
// startBuild запускает kaniko Job и помечает функцию как Building.
|
||||
// Критически важно: СНАЧАЛА сохраняем last-built-s3key аннотацию, ПОТОМ status.
|
||||
// Это предотвращает повторный запуск сборки при параллельных reconcile —
|
||||
// следующий reconcile увидит last-built-s3key == spec.S3Key и не войдёт в startBuild.
|
||||
func (r *FunctionReconciler) startBuild(ctx context.Context, fn *slessv1alpha1.Function) (ctrl.Result, error) {
|
||||
jobName, err := r.Builder.Build(ctx, fn.Namespace, fn.Name, fn.Spec.S3Key)
|
||||
if err != nil {
|
||||
return r.setFailed(ctx, fn, fmt.Sprintf("failed to start build: %v", err))
|
||||
}
|
||||
|
||||
// Сохраняем имя Job'а в аннотации чтобы отслеживать в следующем reconcile
|
||||
// Обновляем аннотации ПЕРВЫМИ — это idempotency guard.
|
||||
// Как только last-built-s3key == spec.S3Key, дальнейшие reconcile не будут
|
||||
// вызывать startBuild снова, даже если status ещё не обновился.
|
||||
if fn.Annotations == nil {
|
||||
fn.Annotations = map[string]string{}
|
||||
}
|
||||
fn.Annotations["sless.kube5s.ru/build-job"] = jobName
|
||||
fn.Annotations["sless.kube5s.ru/last-built-s3key"] = fn.Spec.S3Key
|
||||
if err := r.Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update build annotations: %w", err)
|
||||
}
|
||||
|
||||
// Обновляем статус после аннотаций
|
||||
fn.Status.Phase = slessv1alpha1.FunctionPhaseBuilding
|
||||
fn.Status.Message = "Building image: " + jobName
|
||||
if err := r.Status().Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update status to building: %w", err)
|
||||
}
|
||||
if err := r.Update(ctx, fn); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("update annotations: %w", err)
|
||||
}
|
||||
|
||||
// Перепроверяем через 10 секунд
|
||||
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user