From fa72b6cd47f50791ab7ff445591d84239f2eb4ae Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 13 Aug 2026 23:12:32 +0200 Subject: [PATCH 1/2] f-37: code: add state error in return nil #37 Signed-off-by: GnomeZworc --- internal/dispatcher/agent/dispatcher.go | 6 ++++++ internal/dispatcher/agent/subnet_commands.go | 4 ++++ internal/dispatcher/agent/vm_commands.go | 4 ++++ internal/dispatcher/agent/vpc_commands.go | 4 ++++ 4 files changed, 18 insertions(+) diff --git a/internal/dispatcher/agent/dispatcher.go b/internal/dispatcher/agent/dispatcher.go index 2897c8a..e4129a2 100644 --- a/internal/dispatcher/agent/dispatcher.go +++ b/internal/dispatcher/agent/dispatcher.go @@ -6,6 +6,7 @@ import ( "time" configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/state" "git.g3e.fr/syonad/two/pkg/worker" "github.com/dgraph-io/badger/v4" ) @@ -13,6 +14,7 @@ import ( type Command interface { Prepare(db *badger.DB, cfg *configuration.Config) error Execute(db *badger.DB, cfg *configuration.Config) error + Key() string } type Dispatcher struct { @@ -43,6 +45,10 @@ func (d *Dispatcher) Dispatch(cmd Command) { } if err != nil { d.logger.Error("command failed", append(attrs, "error", err)...) + if setErr := state.Set(d.db, cmd.Key(), state.Error); setErr != nil { + d.logger.Error("failed to mark resource as errored", + "command", cmdType, "key", cmd.Key(), "error", setErr) + } } else { d.logger.Info("command done", attrs...) } diff --git a/internal/dispatcher/agent/subnet_commands.go b/internal/dispatcher/agent/subnet_commands.go index e488842..a73ab2b 100644 --- a/internal/dispatcher/agent/subnet_commands.go +++ b/internal/dispatcher/agent/subnet_commands.go @@ -22,6 +22,8 @@ type CreateSubnetCommand struct { DefaultRoute bool } +func (c CreateSubnetCommand) Key() string { return "subnet/" + c.Name } + func (c CreateSubnetCommand) Prepare(db *badger.DB, cfg *configuration.Config) error { if c.Mode == "" { c.Mode = "vxlan" @@ -79,6 +81,8 @@ type DeleteSubnetCommand struct { Name string } +func (c DeleteSubnetCommand) Key() string { return "subnet/" + c.Name } + func (c DeleteSubnetCommand) Prepare(db *badger.DB, _ *configuration.Config) error { if _, err := kv.GetFromDB(db, "subnet/"+c.Name+"/state"); err != nil { return fmt.Errorf("subnet %q not found", c.Name) diff --git a/internal/dispatcher/agent/vm_commands.go b/internal/dispatcher/agent/vm_commands.go index bdfeaec..4190f85 100644 --- a/internal/dispatcher/agent/vm_commands.go +++ b/internal/dispatcher/agent/vm_commands.go @@ -30,6 +30,8 @@ type StartVMCommand struct { SSHKey string } +func (c StartVMCommand) Key() string { return "vm/" + c.Name } + func (c StartVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error { if _, err := kv.GetFromDB(db, "vm/"+c.Name+"/state"); err == nil { return fmt.Errorf("vm %q already exists", c.Name) @@ -111,6 +113,8 @@ type StopVMCommand struct { Name string } +func (c StopVMCommand) Key() string { return "vm/" + c.Name } + func (c StopVMCommand) Prepare(db *badger.DB, _ *configuration.Config) error { if _, err := kv.GetFromDB(db, "vm/"+c.Name+"/state"); err != nil { return fmt.Errorf("vm %q not found", c.Name) diff --git a/internal/dispatcher/agent/vpc_commands.go b/internal/dispatcher/agent/vpc_commands.go index a129d88..4a0a1dc 100644 --- a/internal/dispatcher/agent/vpc_commands.go +++ b/internal/dispatcher/agent/vpc_commands.go @@ -17,6 +17,8 @@ type CreateVPCCommand struct { CIDR string } +func (c CreateVPCCommand) Key() string { return "vpc/" + c.Name } + func (c CreateVPCCommand) Prepare(db *badger.DB, _ *configuration.Config) error { if _, err := kv.GetFromDB(db, "vpc/"+c.Name+"/state"); err == nil { return fmt.Errorf("vpc %q already exists", c.Name) @@ -38,6 +40,8 @@ type DeleteVPCCommand struct { Name string } +func (c DeleteVPCCommand) Key() string { return "vpc/" + c.Name } + func (c DeleteVPCCommand) Prepare(db *badger.DB, _ *configuration.Config) error { if _, err := kv.GetFromDB(db, "vpc/"+c.Name+"/state"); err != nil { return fmt.Errorf("vpc %q not found", c.Name) From 78363194bffa4c218ee4f875abbbf554a38a212b Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 13 Aug 2026 23:13:25 +0200 Subject: [PATCH 2/2] f-37: add test for error state #37 Signed-off-by: GnomeZworc --- internal/dispatcher/agent/dispatcher_test.go | 65 ++++++++++++++++++++ internal/dispatcher/agent/helpers_test.go | 8 +++ 2 files changed, 73 insertions(+) diff --git a/internal/dispatcher/agent/dispatcher_test.go b/internal/dispatcher/agent/dispatcher_test.go index b4881d9..4475a82 100644 --- a/internal/dispatcher/agent/dispatcher_test.go +++ b/internal/dispatcher/agent/dispatcher_test.go @@ -4,8 +4,10 @@ import ( "errors" "sync" "testing" + "time" configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/state" "github.com/dgraph-io/badger/v4" ) @@ -61,3 +63,66 @@ func TestDispatcher_Dispatch_ExecuteErrorLogged(t *testing.T) { d.Dispatch(cmd) wg.Wait() // Execute s'est terminé — l'erreur est loggée, pas propagée } + +func TestDispatcher_Dispatch_ExecuteErrorSetsErrorState(t *testing.T) { + d, db := newTestDispatcher(t) + if err := state.Set(db, "vpc/vpc-1", state.Creating); err != nil { + t.Fatalf("préparation du test : %v", err) + } + + done := make(chan struct{}) + cmd := mockCmd{ + key: "vpc/vpc-1", + prepareFn: func(*badger.DB, *configuration.Config) error { return nil }, + executeFn: func(*badger.DB, *configuration.Config) error { + return errors.New("execute failed") + }, + } + d.Dispatch(cmd) + + // L'état est écrit après le retour d'Execute : on scrute la DB. + go func() { + defer close(done) + for { + if s, err := state.Get(db, "vpc/vpc-1"); err == nil && s == state.Error { + return + } + time.Sleep(5 * time.Millisecond) + } + }() + select { + case <-done: + case <-time.After(2 * time.Second): + s, _ := state.Get(db, "vpc/vpc-1") + t.Fatalf("la ressource devrait être en %q, obtenu %q", state.Error, s) + } +} + +func TestDispatcher_Dispatch_ExecuteSuccessKeepsState(t *testing.T) { + d, db := newTestDispatcher(t) + if err := state.Set(db, "vpc/vpc-1", state.Running); err != nil { + t.Fatalf("préparation du test : %v", err) + } + + var wg sync.WaitGroup + wg.Add(1) + cmd := mockCmd{ + key: "vpc/vpc-1", + prepareFn: func(*badger.DB, *configuration.Config) error { return nil }, + executeFn: func(*badger.DB, *configuration.Config) error { + defer wg.Done() + return nil + }, + } + d.Dispatch(cmd) + wg.Wait() + time.Sleep(50 * time.Millisecond) // laisse le temps d'une écriture parasite + + s, err := state.Get(db, "vpc/vpc-1") + if err != nil { + t.Fatalf("Get a échoué : %v", err) + } + if s != state.Running { + t.Errorf("l'état devrait rester %q, obtenu %q", state.Running, s) + } +} diff --git a/internal/dispatcher/agent/helpers_test.go b/internal/dispatcher/agent/helpers_test.go index 2cee6fc..c14f6a3 100644 --- a/internal/dispatcher/agent/helpers_test.go +++ b/internal/dispatcher/agent/helpers_test.go @@ -25,10 +25,18 @@ func newTestDispatcher(t *testing.T) (*Dispatcher, *badger.DB) { // mockCmd implémente Command sans aucune dépendance système. type mockCmd struct { + key string prepareFn func(*badger.DB, *configuration.Config) error executeFn func(*badger.DB, *configuration.Config) error } +func (m mockCmd) Key() string { + if m.key == "" { + return "vpc/mock" + } + return m.key +} + func (m mockCmd) Prepare(db *badger.DB, cfg *configuration.Config) error { return m.prepareFn(db, cfg) }