-
Notifications
You must be signed in to change notification settings - Fork 29
Description
⚠️ Statut : Bug potentiel identifié dans le code source mais non confirmé en production
📦 Version analysée : v0.9.0-rc.2
📅 Date d'analyse : 10 novembre 2025
🐛 Résumé du problème
Lorsqu'un TerraformRepository et TerraformLayer sont déployés simultanément (via kubectl apply -f), le TerraformRepository pourrait ne jamais se synchroniser avec Git, et le TerraformLayer ne pourrait pas créer de run (plan/apply) car il manquerait l'annotation webhook.terraform.padok.cloud/relevant-commit.
Le repository pourrait rester bloqué dans l'état SyncNeeded indéfiniment, sauf si :
- Le TerraformLayer est supprimé puis recréé
- Le timer
repositorySync(par défaut 30m) expire - L'annotation
api.terraform.padok.cloud/sync-nowest ajoutée manuellement
📊 Symptômes
- Le TerraformRepository reste dans l'état
SyncNeededindéfiniment (jusqu'à 30 minutes) - Le TerraformLayer reste dans l'état
IdleouPlanNeededsans créer de pod - Message dans les logs du contrôleur :
"layer <name> has no last relevant commit annotation, run not created" - Event Kubernetes sur le TerraformLayer :
"Layer has no last relevant commit annotation, Plan run not created" - Aucun TerraformRun n'est créé même si le repository est accessible
⚠️ Affecte principalement les repositories PRIVÉS avec credentials (GitLab/GitHub avec token)- ✅ Les repositories PUBLICS (sans credentials) semblent fonctionner correctement
� Analyse Approfondie
Pourquoi les Repositories PUBLICS fonctionnent ?
Test de validation :
# Repository public GitHub sans credentials
kubectl apply -f terraform-layer-demo.yaml
# → TerraformRepository sync immédiatement après création du layer ✅Explication technique :
Dans internal/repository/repository.go (lignes 15-20) :
func GetGitProviderFromRepository(store *credentials.CredentialStore, repo *configv1alpha1.TerraformRepository) (types.GitProvider, error) {
creds, err := store.GetCredentials(repo)
// If no credentials, it may be a standard public repository
if err != nil {
return getStandardGitNoAuth(repo.Spec.Repository.Url), nil // ✅ PAS D'ERREUR !
}
// ...
}Pour les repositories publics :
GetCredentials()échoue (pas de secret trouvé)- La fonction retourne
getStandardGitNoAuth()SANS erreur - Le code continue et arrive à la vérification
isThereANewLayer() - Lors de la recréation d'un layer,
isThereANewLayer()retournetrue(layer sans annotationLastBranchCommit) - La condition de skip devient
false→ sync exécuté ✅
Pourquoi les Repositories PRIVÉS échouent ?
Pour les repositories privés (GitLab/GitHub avec token) :
GetCredentials()échoue à cause du bug du cache (credentials dansburrito-systemmais recherchés dansburrito-project)- Comme c'est un repo privé qui DEVRAIT avoir des credentials, cette erreur remonte
- Dans
internal/controllers/terraformrepository/states.golignes 52-56 :gitProvider, err := repo.GetGitProviderFromRepository(r.Credentials, repository) if err != nil { log.Errorf("failed to get git provider...") return ctrl.Result{}, branchStates // ⚠️ RETOUR IMMÉDIAT ! }
- Le code ne va jamais jusqu'à la vérification
isThereANewLayer() - Aucun sync n'est tenté → layer reste en
Idle❌
Conclusion
Le bug affecte UNIQUEMENT les repositories privés nécessitant des credentials, car :
- Le bug du cache empêche la récupération des credentials
- L'échec de récupération bloque toute tentative de sync
- Les repositories publics contournent le problème en utilisant
getStandardGitNoAuth()sans erreur
Root Cause
Le contrôleur TerraformRepository a une logique de skip sync qui empêche la synchronisation si :
- Un layer existe déjà (
len(layers) > 0) - ET tous les layers ont l'annotation
LastBranchCommit(!isThereANewLayer(layersForRef)) - ET le dernier sync est trop récent (
!nextSyncTime.Before(now))
Quand le TerraformRepository et TerraformLayer sont créés en même temps :
- Le repository fait une première tentative de sync avant que le layer existe
- Cette tentative échoue ou est skippée (aucun layer trouvé)
- Mais le
branchState.LastSyncDateest quand même mis à jour - Quand le layer apparaît quelques millisecondes plus tard, le repository considère que le sync est "trop récent" et skip indéfiniment
- Le layer n'obtient jamais l'annotation
LastRelevantCommitnécessaire pour créer un run
Cercle vicieux :
- Layer ne peut pas créer de run → manque annotation
LastRelevantCommit - Repository ne se synchronise pas → layer existe sans annotation
LastBranchCommitMAISbranchState.LastSyncDateest récent - La vérification
isThereANewLayer()retournetrue(layer sans annotation) - MAIS la vérification
!nextSyncTime.Before(now)retournefalse(sync trop récent) - → Skip sync pendant 30 minutes (timer
repositorySync)
Code concerné
Fichier: internal/controllers/terraformrepository/controller.go
Lignes 114-122 : Le repository watch les créations de TerraformLayer
Watches(&configv1alpha1.TerraformLayer{}, handler.EnqueueRequestsFromMapFunc(
func(ctx context.Context, obj client.Object) []reconcile.Request {
log.Infof("repository controller has detected the following layer creation: %s/%s", obj.GetNamespace(), obj.GetName())
layer := obj.(*configv1alpha1.TerraformLayer)
return []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: layer.Spec.Repository.Namespace, Name: layer.Spec.Repository.Name}},
} // ⚠️ Déclenche une réconciliation du repository à chaque création de layer
},
))Lignes 126-130 : Le repository ignore les updates de layer (watch uniquement les créations)
UpdateFunc: func(e event.UpdateEvent) bool {
// Ignore updates on TerraformLayer objects, we only watch their creation
if _, ok := e.ObjectNew.(*configv1alpha1.TerraformLayer); ok {
return false // ❌ Ignore les updates, watch UNIQUEMENT les créations
}Conséquence : Quand un layer est créé, le repository passe en état SyncNeeded mais refuse de sync si LastSyncDate est récent.
Fichier: internal/controllers/terraformrepository/states.go
Lignes 61-65 : Le repository refuse de se synchroniser si aucun layer n'existe
if len(layers) == 0 {
log.Warningf("no managed layers found for repository %s/%s, have you created TerraformLayer resources?", repository.Namespace, repository.Name)
return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.WaitAction}, []configv1alpha1.BranchState{}
}Lignes 73-82 : Skip sync si le dernier sync est trop récent ET qu'il n'y a pas de nouveau layer
// Skip sync if last sync is too recent and no new layer
if !syncNow && !nextSyncTime.Before(now) && branch.LastSyncStatus == SyncStatusSuccess && !isThereANewLayer(layersForRef) {
log.Infof("skipping sync for repository %s/%s ref %s: last sync was at %s and no new layer for this branch",
repository.Namespace, repository.Name, branch.Name, lastSync.Format(time.UnixDate))
continue // ⚠️ Skip la synchronisation !
}Fichier: internal/controllers/terraformrepository/polling.go
Lignes 50-57 : Détection d'un "nouveau layer" (sans annotation LastBranchCommit)
func isThereANewLayer(layers []configv1alpha1.TerraformLayer) bool {
for _, layer := range layers {
if _, ok := layer.Annotations[annotations.LastBranchCommit]; !ok {
return true // ✅ Layer sans annotation = nouveau layer
}
}
return false
}Problème : Même si isThereANewLayer() retourne true, le sync est skip si !nextSyncTime.Before(now) (dernier sync < 30 minutes).
Fichier: internal/controllers/terraformlayer/states.go
Lignes 61-67 : Le layer ne peut pas créer de run sans l'annotation
func (s *PlanNeeded) getHandler() Handler {
return func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) (ctrl.Result, *configv1alpha1.TerraformRun) {
log := log.WithContext(ctx)
// Check for sync windows that would block the apply action
if isActionBlocked(r, layer, repository, syncwindow.PlanAction) {
return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.WaitAction}, nil
}
revision, ok := layer.Annotations[annotations.LastRelevantCommit] // ⚠️ Bloque si l'annotation n'existe pas
if !ok {
r.Recorder.Event(layer, corev1.EventTypeWarning, "Reconciliation", "Layer has no last relevant commit annotation, Plan run not created")
log.Errorf("layer %s has no last relevant commit annotation, run not created", layer.Name)
return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}, nil // ⚠️ Requeue avec délai d'erreur (10s par défaut)
}
run := r.getRun(layer, revision, PlanAction)
// ...Lignes 90-96 : Même vérification pour Apply
func (s *ApplyNeeded) getHandler() Handler {
return func(ctx context.Context, r *Reconciler, layer *configv1alpha1.TerraformLayer, repository *configv1alpha1.TerraformRepository) (ctrl.Result, *configv1alpha1.TerraformRun) {
log := log.WithContext(ctx)
autoApply := configv1alpha1.GetAutoApplyEnabled(repository, layer)
if !autoApply {
log.Infof("autoApply is disabled for layer %s, no apply action taken", layer.Name)
return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.DriftDetection}, nil
}
// Check for sync windows that would block the apply action
if isActionBlocked(r, layer, repository, syncwindow.ApplyAction) {
return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.WaitAction}, nil
}
revision, ok := layer.Annotations[annotations.LastRelevantCommit] // ⚠️ Bloque également pour Apply
if !ok {
r.Recorder.Event(layer, corev1.EventTypeWarning, "Reconciliation", "Layer has no last relevant commit annotation, Apply run not created")
log.Errorf("layer %s has no last relevant commit annotation, run not created", layer.Name)
return ctrl.Result{RequeueAfter: r.Config.Controller.Timers.OnError}, nil
}
// ...Fichier: internal/annotations/annotations.go
Lignes 19-20 : Définition des annotations attendues
const (
// ...
LastRelevantCommit string = "webhook.terraform.padok.cloud/relevant-commit"
LastRelevantCommitDate string = "webhook.terraform.padok.cloud/relevant-commit-date"
// ...
)Séquence de création problématique
Cas 1 : Déploiement simultané (repository et layer créés en même temps)
- T0 :
kubectl apply -f layer.yamlcrée simultanément Secret, TerraformRepository, TerraformLayer - T0+50ms : TerraformRepository contrôleur démarre sa réconciliation
- T0+60ms : TerraformRepository vérifie
len(layers)→0(layer pas encore créé) - T0+70ms : TerraformRepository skip sync et met à jour
branchState.LastSyncDate = T0+70ms - T0+100ms : TerraformLayer est créé dans Kubernetes
- T0+110ms : TerraformRepository détecte la création du layer (via Watch) et se met en état
SyncNeeded - T0+120ms : TerraformRepository vérifie
len(layers)→1✅ - T0+130ms : TerraformRepository vérifie
isThereANewLayer()→true(layer sans annotation) ✅ - T0+140ms : TerraformRepository vérifie
!nextSyncTime.Before(now)→false❌nextSyncTime = T0+70ms + 30m = T0+30m+70msnow = T0+140msT0+30m+70msn'est PAS avantT0+140ms→ SKIP SYNC ❌
- T0+1m : TerraformRepository reste en
SyncNeededmais skip sync (même vérification) - T0+2m : TerraformRepository skip sync (même vérification)
- ... : Repository reste en
SyncNeededet skip sync indéfiniment pendant 30 minutes - T0+30m : TerraformRepository se synchronise ENFIN avec Git ✅
- T0+30m+1s : TerraformRepository ajoute l'annotation
LastRelevantCommitau layer - T0+30m+10s : TerraformLayer crée le run ✅
Cas 2 : Layer créé APRÈS synchronisation du repository (workaround qui échoue aussi !)
- T0 :
kubectl apply -f repository.yamlcrée le TerraformRepository - T0+500ms : TerraformRepository se synchronise avec Git et passe en état
Synced - T0+600ms :
branchState.LastSyncDate = T0+600ms - T0+5s :
kubectl apply -f layer.yamlcrée le TerraformLayer - T0+5.1s : TerraformRepository détecte la création du layer (via Watch) et se met en état
SyncNeeded✅ - T0+5.2s : TerraformRepository vérifie
isThereANewLayer()→true(nouveau layer sans annotation) ✅ - T0+5.3s : TerraformRepository vérifie
!nextSyncTime.Before(now)→false❌nextSyncTime = T0+600ms + 30m = T0+30m+600msnow = T0+5.3sT0+30m+600msn'est PAS avantT0+5.3s→ SKIP SYNC ❌
- T0+1m : TerraformRepository reste en
SyncNeededmais skip sync - ... : Repository reste bloqué en
SyncNeededpendant ~30 minutes - T0+30m : TerraformRepository se synchronise ENFIN ✅
Problème aggravant : Même le workaround "déployer le repository en premier" ne fonctionne pas si vous créez le layer dans les 30 minutes suivantes !
🧪 Reproduction
Scénario 1 : Repository PUBLIC (✅ FONCTIONNE - pas de bug)
Fichier : terraform-layer-demo.yaml
---
apiVersion: config.terraform.padok.cloud/v1alpha1
kind: TerraformRepository
metadata:
name: my-repository
namespace: burrito-project
spec:
repository:
url: https://github.com/padok-team/burrito-examples # Repository PUBLIC
terraform:
enabled: true
---
apiVersion: config.terraform.padok.cloud/v1alpha1
kind: TerraformLayer
metadata:
name: my-layer
namespace: burrito-project
spec:
branch: main
path: terraform
remediationStrategy:
autoApply: false
repository:
name: my-repository
namespace: burrito-projectTest - Déploiement simultané :
# Déployer tout en une fois
kubectl apply -f terraform-layer-demo.yaml
# Vérifier immédiatement l'état (< 10 secondes après apply)
watch -n 1 'kubectl get tfr,tfl -n burrito-project'Résultat attendu :
NAME STATE AGE
terraformrepository.config.terraform.padok.cloud/my-repository Synced 5s
NAME STATE LAST RESULT
terraformlayer.config.terraform.padok.cloud/my-layer PlanNeeded Layer has never been plannedLogs attendus :
time="..." level=info msg="repository my-repository is in sync with remote"
time="..." level=info msg="layer my-layer has an outdated plan, creating a new run"
time="..." level=info msg="Created TerraformRun for Plan action"
Pourquoi ça fonctionne ? Repository public → pas de credentials → utilise getStandardGitNoAuth() → pas d'erreur → sync immédiat ✅
Scénario 2 : Repository PRIVÉ avec credentials (❌ ÉCHOUE POTENTIELLEMENT pendant ~30 minutes)
Fichier : terraform-layer-drift-simple.yaml
---
apiVersion: v1
kind: Secret
metadata:
name: gitlab-credentials-simple
namespace: burrito-system
type: credentials.burrito.tf/repository
stringData:
provider: gitlab
url: https://gitlab.example.com/my-org/terraform-modules/my-module
gitlabToken: "glpat-xxxxxxxxxxxxxxxxxxxx"
webhookSecret: "my-webhook-secret"
---
apiVersion: config.terraform.padok.cloud/v1alpha1
kind: TerraformRepository
metadata:
name: my-terraform-module-simple
namespace: burrito-system
spec:
repository:
url: https://gitlab.example.com/my-org/terraform-modules/my-module
terraform:
enabled: true
---
apiVersion: config.terraform.padok.cloud/v1alpha1
kind: TerraformLayer
metadata:
name: my-layer-drift-simple
namespace: burrito-system
spec:
branch: main
remediationStrategy:
autoApply: false
repository:
name: my-terraform-module-simple
namespace: burrito-systemTest - Déploiement simultané (reproduit le bug potentiel) :
# Supprimer les ressources existantes si présentes
kubectl delete -f terraform-layer-drift-simple.yaml --ignore-not-found
# Attendre 2 secondes
sleep 2
# Déployer tout en une fois
kubectl apply -f terraform-layer-drift-simple.yaml
# Observer immédiatement (< 5 secondes)
kubectl get tfr,tfl -n burrito-system -wRésultat attendu (si bug présent) :
# Pendant les premières 30 minutes :
NAME STATE AGE
terraformrepository.../my-terraform-module-simple SyncNeeded 10s
NAME STATE LAST RESULT
terraformlayer.../my-layer-drift-simple Idle Layer has never been planned
# Après ~30 minutes (timer repositorySync expire) :
NAME STATE AGE
terraformrepository.../my-terraform-module-simple Synced 30m5s
NAME STATE LAST RESULT
terraformlayer.../my-layer-drift-simple PlanNeeded Layer has an outdated planLogs attendus (si bug présent) :
# Vérifier les logs pendant les 30 premières secondes
kubectl logs -n burrito-system deployment/burrito-controllers --tail=100 | grep -A2 -B2 "my-layer-drift-simple\|my-terraform-module-simple"time="..." level=info msg="no managed layers found for repository burrito-system/my-terraform-module-simple, have you created TerraformLayer resources?"
time="..." level=info msg="repository burrito-system/my-terraform-module-simple is in state SyncNeeded"
time="..." level=info msg="layer my-layer-drift-simple has no last relevant commit annotation, run not created"
time="..." level=info msg="skipping sync for repository burrito-system/my-terraform-module-simple ref main: last sync was at ... and no new layer for this branch"
Vérifier les annotations du layer :
kubectl get tfl my-layer-drift-simple -n burrito-system -o jsonpath='{.metadata.annotations}' | jqRésultat attendu (si bug présent) :
{} // Aucune annotation pendant ~30 minutesRésumé des scénarios de reproduction
| Scénario | Type de repo | Déploiement | Résultat attendu | Délai avant sync |
|---|---|---|---|---|
| 1 - Demo | Public GitHub | Simultané | ✅ Fonctionne | ~5 secondes |
| 2 - Drift Simple | Privé GitLab | Simultané | ❌ Bug potentiel | ~30 minutes |
🔍 Preuve du bug
Timeline des événements (déploiement simultané repository privé)
Test de reproduction :
# Supprimer les ressources existantes
kubectl delete -f terraform-layer-drift-simple.yaml --ignore-not-found
# Attendre 2 secondes
sleep 2
# Déployer tout en une fois avec horodatage
kubectl apply -f terraform-layer-drift-simple.yaml && date
# Observer les events en temps réel
kubectl get events -n burrito-system -w --field-selector involvedObject.kind=TerraformLayerLogs attendus (si bug présent) :
T+0.1s Normal Reconciliation Repository my-terraform-module-simple: no managed layers found
T+0.2s Warning Reconciliation Layer my-layer-drift-simple: has no last relevant commit annotation
T+0.5s Normal Reconciliation Repository my-terraform-module-simple: needs to be synced
T+1.0s Normal Reconciliation Repository: skipping sync (last sync too recent and no new layer)
T+10.0s Warning Reconciliation Layer my-layer-drift-simple: has no last relevant commit annotation
T+20.0s Normal Reconciliation Repository: skipping sync (last sync too recent)
... (continue pendant ~30 minutes)
T+30m Normal Reconciliation Repository my-terraform-module-simple: is in sync with remote
T+30m+5s Normal Reconciliation Layer my-layer-drift-simple: Created TerraformRun for Plan ✅
⏱️ Impact sur le déploiement
Délai observé
| Scénario | Délai avant création du run | État |
|---|---|---|
| Repository public (demo) | ~5 secondes | ✅ Fonctionne toujours |
| Repository privé - Déploiement simultané | ~30 minutes | ❌ Bug potentiel - Bloqué jusqu'au timer |
Configuration du timer
Fichier: install-values.yaml
config:
burrito:
controller:
timers:
onError: 10s # ⚠️ Délai de retry quand l'annotation est manquante
waitAction: 1m
driftDetection: 10m🔗 Références
- Controller TerraformLayer :
internal/controllers/terraformlayer/states.go - Controller TerraformRepository :
internal/controllers/terraformrepository/controller.go - Annotations :
internal/annotations/annotations.go
Metadata
Metadata
Assignees
Labels
Type
Projects
Status