Skip to content

🐛 [BUG] - Un projet dont la suppression externe échoue reste verrouillé en <slug>_<horodatage>_archived sans voie de récupération #2813

Description

@shikanime

Description

L'archivage d'un projet dont la suppression échoue sur un service externe laisse le projet dans un état irrécupérable : renommé <nom>_<horodatage>_archived, locked: true, sans aucune voie API pour relancer la suppression ou le déverrouiller.

La transaction en base et la suppression des ressources externes ne sont pas atomiques. La transaction — renommage, status: archived, locked: true — est committée avant l'émission de l'événement project.delete vers les listeners externes (Keycloak, GitLab, Nexus, Vault, Sonarqube, Registry, ArgoCD, observabilité). Un échec du listener ne peut plus annuler le verrouillage.

flowchart TD
  A[DELETE /projects/:id] --> B[Transaction: renommage _archived, locked=true]
  B --> C[commit]
  C --> D[emit project.delete vers les listeners externes]
  D -- OK --> E[statut archived]
  D -- KO --> F[statut failed ou erreur 500]
  F --> G[projet verrouillé, aucune voie de récupération]
Loading

Étapes de reproduction

  1. Disposer d'un projet dont une ressource existe sur un service externe en erreur — par exemple Keycloak injoignable.
  2. Archiver le projet via DELETE /projects/{projectId}.
  3. Constater que le projet est renommé <nom>_<horodatage>_archived, locked: true, statut archived ou failed selon la nature de l'échec.
  4. Tenter une récupération : la mise à jour du projet renvoie 404 — Projet introuvable, le rejeu des webhooks est refusé — statut bloqué par le garde si le projet reste archived, verrouillage bloqué par le garde s'il est passé à failed — un second archivage renvoie 400, et aucun endpoint ne réémet project.delete.

Comportement attendu

Un archivage dont la suppression externe a échoué doit rester récupérable : un administrateur doit pouvoir relancer la suppression des ressources externes ou déverrouiller le projet pour reprendre le nettoyage, sans intervention manuelle en base de données.

Logs ou journal

Aucune pièce jointe au signalement ; l'analyse de cause racine est postée en commentaire.

Environnement

server-nestjs, main courant.

Définition du fini

  • Le correctif est terminé
  • Un test simule l'échec d'un listener project.delete pendant l'archivage et vérifie que le projet est récupérable sans intervention manuelle en base
  • pnpm --filter server-nestjs test project passe avec ce test

Activity

  1. shikanime commented on Oct 5, 2026

    @shikanime
    MemberAuthor

    Cause racine

    L'archivage commute la transaction base avant d'appeler les services externes, et l'échec externe ne peut plus défaire le verrouillage.

    ProjectService.archive (apps/server-nestjs/src/modules/project/project.service.ts:187) :

    const project = await this.prisma.$transaction(async (tx) => {
      const loaded = await getProject(tx, projectId)
      if (!loaded) throw new NotFoundException('Projet introuvable')
      if (loaded.status === 'archived') throw new BadRequestException('Le projet est archivé')
    
      await deleteProjectDependencies(tx, projectId)
    
      const archivedSuffix = `${Date.now()}_archived`
      await updateProject(tx, projectId, {
        name: `${loaded.name}_${archivedSuffix}`,
        slug: `${loaded.slug}_${archivedSuffix}`,
        status: 'archived',
        locked: true,
        clusters: { set: [] },
      })
    
      return loaded
    })

    L'émission project.delete vers les huit listeners externes — keycloak, gitlab, nexus, vault, sonarqube, registry, argocd, observabilité — se fait après le commit (project.service.ts:212). Deux issues en découlent :

    1. Quand un listener échoue via capturePluginResult, AppEventsService.updateProjectStatus (apps/server-nestjs/src/modules/events/app-events.service.ts:149) bascule le statut à failed mais ne touche ni à locked ni au suffixe _archived déjà posé.
    2. Quand un listener lève hors capturePluginResult, emitProjectEvent propage l'exception, archive la re-jette, la requête finit en 500 — et la ligne reste archived + locked: true, le renommage étant déjà committé. C'est l'état décrit dans le signalement.
  2. shikanime commented on Oct 5, 2026

    @shikanime
    MemberAuthor

    Voies de récupération épuisées

    Chaque route API est fermée par un garde pour un projet en {slug}_{timestamp}_archived + locked: true :

    • PUT /api/v1/projects/:id : getNotArchivedProjectForUpdate exclut les projets archived → 404 Projet introuvable.
    • PUT /api/v1/projects/:id/hooks (rejeu) : @RequireProjectStatus('initializing', 'created', 'failed', 'warning') + @RequireProjectLocked(false) (apps/server-nestjs/src/modules/project-hooks/project-hooks.controller.ts:22-23), et ProjectHooksService.replay refuse en plus archived et locked (project-hooks.service.ts:29-37). Le rejeu réémet de toute façon project.upsert, pas project.delete — il re-provisionnerait au lieu de nettoyer.
    • Second DELETE : loaded.status === 'archived' → 400 Le projet est archivé.
    • Action en lot lock/unlock : passe par updateProjectLocked sans garde de statut, mais après déverrouillage rien ne réémet project.delete.

    Cas intermédiaire : quand le statut a basculé à failed, un administrateur peut théoriquement déverrouiller via PUT avec locked: false, mais le projet reste renommé _archived, clusters vidé, sans aucune route pour relancer la suppression externe.

    Conclusion : la seule récupération actuelle est un UPDATE manuel en base de données. Le correctif doit offrir une voie supportée — par exemple réémettre project.delete pour un projet archived en erreur, ou ne pas verrouiller avant le succès des listeners — la décision de conception appartient à la revue.

  3. self-assigned this
    on Oct 5, 2026
  4. added this to the 9.28.0 milestone on Oct 5, 2026
  5. added theissue type on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions