84 lines
2.7 KiB
Go
84 lines
2.7 KiB
Go
// Package migration met la DB au format attendu par la version courante de
|
|
// l'agent. Les migrations sont idempotentes et jouées au démarrage.
|
|
package migration
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"git.g3e.fr/syonad/two/internal/state"
|
|
"git.g3e.fr/syonad/two/pkg/db/kv"
|
|
|
|
"github.com/dgraph-io/badger/v4"
|
|
)
|
|
|
|
// prefixes énumère les familles de ressources portant une clé /state.
|
|
var prefixes = []string{"vpc/", "subnet/", "vm/"}
|
|
|
|
// legacyStates traduit l'ancien vocabulaire (états VPC/subnet d'avant
|
|
// l'unification, et états VM start/stop) vers l'enum courant.
|
|
var legacyStates = map[string]state.State{
|
|
"created": state.Running,
|
|
"started": state.Running,
|
|
"starting": state.Creating,
|
|
"stopping": state.Deleting,
|
|
"stopped": state.Deleted,
|
|
}
|
|
|
|
// MigrateStates convertit les valeurs de state héritées vers l'enum courant,
|
|
// puis marque en erreur les ressources restées dans un état transitoire.
|
|
//
|
|
// La réconciliation est sûre au démarrage : la queue worker est en mémoire et
|
|
// vide à ce moment, donc aucune commande n'est en cours. Une ressource en
|
|
// creating/deleting est nécessairement orpheline d'un arrêt de l'agent — sans
|
|
// ce passage en error, elle resterait indéfiniment non supprimable.
|
|
func MigrateStates(db *badger.DB, log *slog.Logger) error {
|
|
for _, prefix := range prefixes {
|
|
entries, err := kv.ListByPrefix(db, prefix)
|
|
if err != nil {
|
|
return fmt.Errorf("list %s: %w", prefix, err)
|
|
}
|
|
for key, value := range entries {
|
|
if !strings.HasSuffix(key, "/state") {
|
|
continue
|
|
}
|
|
resource := strings.TrimSuffix(key, "/state")
|
|
target, reason := targetState(value)
|
|
if target == "" {
|
|
continue
|
|
}
|
|
if err := state.Set(db, resource, target); err != nil {
|
|
return fmt.Errorf("migrate %s: %w", key, err)
|
|
}
|
|
log.Info("state migrated",
|
|
"resource", resource, "from", value, "to", string(target), "reason", reason)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// targetState retourne l'état vers lequel migrer une valeur brute, ou "" si
|
|
// elle doit rester inchangée.
|
|
func targetState(value string) (state.State, string) {
|
|
if legacy, ok := legacyStates[value]; ok {
|
|
// Un état hérité transitoire est orphelin au même titre qu'un état
|
|
// transitoire courant : on applique la réconciliation directement.
|
|
if state.IsTransient(legacy) {
|
|
return state.Error, "orphaned legacy state"
|
|
}
|
|
return legacy, "legacy state"
|
|
}
|
|
|
|
current, err := state.Parse(value)
|
|
if err != nil {
|
|
// Valeur inconnue : ni l'ancien vocabulaire, ni le nouveau. On la
|
|
// bascule en error plutôt que de la laisser bloquer l'agent — la
|
|
// ressource reste visible et supprimable.
|
|
return state.Error, "unknown state"
|
|
}
|
|
if state.IsTransient(current) {
|
|
return state.Error, "orphaned transient state"
|
|
}
|
|
return "", ""
|
|
}
|