From 565428b8de826cbacf8112da1697315537f8fe26 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 12 Apr 2026 17:29:14 +0200 Subject: [PATCH 01/44] f-21: api: add openapi file Signed-off-by: GnomeZworc --- api/openapi.yaml | 296 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 api/openapi.yaml diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..be328d0 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,296 @@ +openapi: "3.1.0" +info: + title: Two API + version: "0.1.0" + description: REST API for managing VPCs and Subnets in the Two orchestrator. + +servers: + - url: http://localhost:8080 + description: Local development server + +paths: + + # ── VPC ──────────────────────────────────────────────────────────────────── + + /vpcs: + get: + summary: List all VPCs + operationId: listVPCs + responses: + "200": + description: List of VPCs + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/VPC" + "500": + $ref: "#/components/responses/InternalError" + + post: + summary: Create a VPC + operationId: createVPC + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/VPCCreateRequest" + responses: + "202": + description: VPC creation accepted + content: + application/json: + schema: + $ref: "#/components/schemas/VPC" + "409": + description: VPC already exists + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + $ref: "#/components/responses/InternalError" + + /vpcs/{name}: + parameters: + - $ref: "#/components/parameters/ResourceName" + + get: + summary: Get VPC status and info + operationId: getVPC + responses: + "200": + description: VPC found + content: + application/json: + schema: + $ref: "#/components/schemas/VPC" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalError" + + delete: + summary: Delete a VPC + operationId: deleteVPC + responses: + "202": + description: VPC deletion accepted + content: + application/json: + schema: + $ref: "#/components/schemas/VPC" + "404": + $ref: "#/components/responses/NotFound" + "409": + description: VPC not in a deletable state + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + $ref: "#/components/responses/InternalError" + + # ── Subnet ───────────────────────────────────────────────────────────────── + + /subnets: + get: + summary: List all subnets + operationId: listSubnets + responses: + "200": + description: List of subnets + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Subnet" + "500": + $ref: "#/components/responses/InternalError" + + post: + summary: Create a subnet + operationId: createSubnet + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SubnetCreateRequest" + responses: + "202": + description: Subnet creation accepted + content: + application/json: + schema: + $ref: "#/components/schemas/Subnet" + "409": + description: Subnet already exists + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "422": + description: Parent VPC does not exist or is not ready + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + $ref: "#/components/responses/InternalError" + + /subnets/{name}: + parameters: + - $ref: "#/components/parameters/ResourceName" + + get: + summary: Get subnet status and info + operationId: getSubnet + responses: + "200": + description: Subnet found + content: + application/json: + schema: + $ref: "#/components/schemas/Subnet" + "404": + $ref: "#/components/responses/NotFound" + "500": + $ref: "#/components/responses/InternalError" + + delete: + summary: Delete a subnet + operationId: deleteSubnet + responses: + "202": + description: Subnet deletion accepted + content: + application/json: + schema: + $ref: "#/components/schemas/Subnet" + "404": + $ref: "#/components/responses/NotFound" + "409": + description: Subnet not in a deletable state + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "500": + $ref: "#/components/responses/InternalError" + +# ── Components ────────────────────────────────────────────────────────────── + +components: + + parameters: + ResourceName: + name: name + in: path + required: true + schema: + type: string + description: Resource name + + schemas: + + VPCCreateRequest: + type: object + required: [name] + properties: + name: + type: string + description: Unique name for the VPC + example: vpc1 + + VPC: + type: object + properties: + name: + type: string + example: vpc1 + state: + type: string + enum: [creating, created, deleting, deleted] + example: created + + SubnetCreateRequest: + type: object + required: [name, vpc, vxlan_id, local_ip, gateway_ip, cidr] + properties: + name: + type: string + description: Unique name for the subnet + example: sn-00001 + vpc: + type: string + description: Parent VPC name + example: vpc1 + vxlan_id: + type: integer + description: VXLAN VNI identifier + example: 100 + local_ip: + type: string + format: ipv4 + description: Local VTEP IP address + example: "10.0.0.5" + gateway_ip: + type: string + format: ipv4 + description: Gateway IP for the subnet + example: "10.10.10.1" + cidr: + type: string + description: Subnet CIDR block + example: "10.10.10.0/24" + + Subnet: + type: object + properties: + name: + type: string + example: sn-00001 + state: + type: string + enum: [creating, created, deleting, deleted] + example: created + vpc: + type: string + example: vpc1 + vxlan_id: + type: integer + example: 100 + local_ip: + type: string + example: "10.0.0.5" + gateway_ip: + type: string + example: "10.10.10.1" + cidr: + type: string + example: "10.10.10.0/24" + + Error: + type: object + properties: + error: + type: string + example: "resource not found" + + responses: + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + InternalError: + description: Internal server error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" From bcedeece18402140681352f96c94aab823ec0491 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 12 Apr 2026 17:29:37 +0200 Subject: [PATCH 02/44] f-21: add first api file Signed-off-by: GnomeZworc --- internal/api/agent/server.go | 16 ++++++++++++++++ internal/api/agent/subnet.go | 25 +++++++++++++++++++++++++ internal/api/agent/subnets.go | 19 +++++++++++++++++++ internal/api/agent/vpc.go | 25 +++++++++++++++++++++++++ internal/api/agent/vpcs.go | 19 +++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 internal/api/agent/server.go create mode 100644 internal/api/agent/subnet.go create mode 100644 internal/api/agent/subnets.go create mode 100644 internal/api/agent/vpc.go create mode 100644 internal/api/agent/vpcs.go diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go new file mode 100644 index 0000000..05653a4 --- /dev/null +++ b/internal/api/agent/server.go @@ -0,0 +1,16 @@ +package agentapi + +import ( + "log" + "net/http" +) + +func Start(address string) { + mux := http.NewServeMux() + mux.HandleFunc("/vpcs", VpcsHandler) + mux.HandleFunc("/vpcs/", VpcByNameHandler) + mux.HandleFunc("/subnets", SubnetsHandler) + mux.HandleFunc("/subnets/", SubnetByNameHandler) + log.Printf("API server listening on %s", address) + log.Fatal(http.ListenAndServe(address, mux)) +} diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go new file mode 100644 index 0000000..e46e4f4 --- /dev/null +++ b/internal/api/agent/subnet.go @@ -0,0 +1,25 @@ +package agentapi + +import ( + "encoding/json" + "net/http" + "strings" +) + +func SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { + name := strings.TrimPrefix(r.URL.Path, "/subnets/") + if name == "" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"name": name}) + case http.MethodDelete: + w.WriteHeader(http.StatusAccepted) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go new file mode 100644 index 0000000..53dce2e --- /dev/null +++ b/internal/api/agent/subnets.go @@ -0,0 +1,19 @@ +package agentapi + +import ( + "encoding/json" + "net/http" +) + +func SubnetsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode([]interface{}{}) + case http.MethodPost: + w.WriteHeader(http.StatusAccepted) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go new file mode 100644 index 0000000..aecfaa1 --- /dev/null +++ b/internal/api/agent/vpc.go @@ -0,0 +1,25 @@ +package agentapi + +import ( + "encoding/json" + "net/http" + "strings" +) + +func VpcByNameHandler(w http.ResponseWriter, r *http.Request) { + name := strings.TrimPrefix(r.URL.Path, "/vpcs/") + if name == "" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"name": name}) + case http.MethodDelete: + w.WriteHeader(http.StatusAccepted) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go new file mode 100644 index 0000000..c4e9b27 --- /dev/null +++ b/internal/api/agent/vpcs.go @@ -0,0 +1,19 @@ +package agentapi + +import ( + "encoding/json" + "net/http" +) + +func VpcsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode([]interface{}{}) + case http.MethodPost: + w.WriteHeader(http.StatusAccepted) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} From d393b647ac82714ade4f1202ed5b74c119d9e1d2 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 12 Apr 2026 17:30:07 +0200 Subject: [PATCH 03/44] f-21: mon: add prometheus data Signed-off-by: GnomeZworc --- internal/prometheus/agent/collector.go | 67 ++++++++++++++++++++++++++ pkg/prometheus/server.go | 20 ++++++++ 2 files changed, 87 insertions(+) create mode 100644 internal/prometheus/agent/collector.go create mode 100644 pkg/prometheus/server.go diff --git a/internal/prometheus/agent/collector.go b/internal/prometheus/agent/collector.go new file mode 100644 index 0000000..04712ad --- /dev/null +++ b/internal/prometheus/agent/collector.go @@ -0,0 +1,67 @@ +package agentmetrics + +import ( + "strings" + + "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/dgraph-io/badger/v4" + "github.com/prometheus/client_golang/prometheus" +) + +var allStates = []string{"creating", "created", "deleting", "deleted"} + +// AgentCollector implements prometheus.Collector and exposes agent metrics +// by querying the BadgerDB on each scrape. +type AgentCollector struct { + db *badger.DB + vpcsTotal *prometheus.Desc + subnetsTotal *prometheus.Desc +} + +func NewAgentCollector(db *badger.DB) *AgentCollector { + return &AgentCollector{ + db: db, + vpcsTotal: prometheus.NewDesc( + "syonad_vpcs_total", + "Number of VPCs by state.", + []string{"state"}, nil, + ), + subnetsTotal: prometheus.NewDesc( + "syonad_subnets_total", + "Number of subnets by state.", + []string{"state"}, nil, + ), + } +} + +func (c *AgentCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.vpcsTotal + ch <- c.subnetsTotal +} + +func (c *AgentCollector) Collect(ch chan<- prometheus.Metric) { + c.collectStates(ch, "vpc/", c.vpcsTotal) + c.collectStates(ch, "subnet/", c.subnetsTotal) +} + +// collectStates counts resources under the given DB prefix by their state value +// and emits one gauge per state label. +func (c *AgentCollector) collectStates(ch chan<- prometheus.Metric, prefix string, desc *prometheus.Desc) { + counts := make(map[string]float64, len(allStates)) + for _, s := range allStates { + counts[s] = 0 + } + + items, err := kv.ListByPrefix(c.db, prefix) + if err == nil { + for key, val := range items { + if strings.HasSuffix(key, "/state") { + counts[val]++ + } + } + } + + for _, state := range allStates { + ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, counts[state], state) + } +} diff --git a/pkg/prometheus/server.go b/pkg/prometheus/server.go new file mode 100644 index 0000000..9f8e557 --- /dev/null +++ b/pkg/prometheus/server.go @@ -0,0 +1,20 @@ +package promserver + +import ( + "log" + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// Start launches the Prometheus metrics HTTP server on the given address. +// The provided registry is used to expose metrics at /metrics. +func Start(address string, registry *prometheus.Registry) { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{ + EnableOpenMetrics: true, + })) + log.Printf("Prometheus server listening on %s", address) + log.Fatal(http.ListenAndServe(address, mux)) +} From 906201a1b6cdbb097ba49082596b48ad431f62d7 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 12 Apr 2026 17:30:36 +0200 Subject: [PATCH 04/44] f-21: code: add list prefix Signed-off-by: GnomeZworc --- pkg/db/kv/listByPrefix.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 pkg/db/kv/listByPrefix.go diff --git a/pkg/db/kv/listByPrefix.go b/pkg/db/kv/listByPrefix.go new file mode 100644 index 0000000..e6c095e --- /dev/null +++ b/pkg/db/kv/listByPrefix.go @@ -0,0 +1,33 @@ +package kv + +import ( + "github.com/dgraph-io/badger/v4" +) + +// ListByPrefix returns all key-value pairs whose key starts with prefix. +func ListByPrefix(db *badger.DB, prefix string) (map[string]string, error) { + result := make(map[string]string) + p := []byte(prefix) + + err := db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchSize = 10 + + it := txn.NewIterator(opts) + defer it.Close() + + for it.Seek(p); it.ValidForPrefix(p); it.Next() { + item := it.Item() + key := string(item.Key()) + + val, err := item.ValueCopy(nil) + if err != nil { + return err + } + result[key] = string(val) + } + return nil + }) + + return result, err +} From cc6c08b522b41ddad19e071fd934a5a6be133caa Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 12 Apr 2026 17:30:58 +0200 Subject: [PATCH 05/44] f-21: config: add config data Signed-off-by: GnomeZworc --- internal/config/agent/struct.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index c9537bf..0cca127 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -8,6 +8,14 @@ type Config struct { Database struct { Path string `mapstructure:"path"` } `mapstructure:"database"` + Api struct { + Address string `mapstructure:"address"` + Port int `mapstructure:"port"` + } `mapstructure:"api"` + Prometheus struct { + Address string `mapstructure:"address"` + Port int `mapstructure:"port"` + } `mapstructure:"prometheus"` } func LoadConfig(path string) (*Config, error) { @@ -16,6 +24,10 @@ func LoadConfig(path string) (*Config, error) { v.SetConfigType("yaml") v.SetDefault("database.path", "/var/lib/two/data/") + v.SetDefault("api.address", "") + v.SetDefault("api.port", 8080) + v.SetDefault("prometheus.address", "") + v.SetDefault("prometheus.port", 9090) v.ReadInConfig() From 04e8adb9b026a21bdcc944042fc538f877374d6a Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 12 Apr 2026 17:31:24 +0200 Subject: [PATCH 06/44] f-21: agent: implement api first Signed-off-by: GnomeZworc --- cmd/agent/main.go | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 0f82e93..ce3df4e 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -1,17 +1,38 @@ package main import ( + "flag" "fmt" - "os" -) + "log" -var ( - bin_name = os.Args[0] + agentapi "git.g3e.fr/syonad/two/internal/api/agent" + agentmetrics "git.g3e.fr/syonad/two/internal/prometheus/agent" + configuration "git.g3e.fr/syonad/two/internal/config/agent" + promserver "git.g3e.fr/syonad/two/pkg/prometheus" + "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/prometheus/client_golang/prometheus" ) func main() { + confFile := flag.String("config", "/etc/two/agent.yml", "config file path") + flag.Parse() - fmt.Printf("%s: Start process\n", bin_name) + cfg, err := configuration.LoadConfig(*confFile) + if err != nil { + log.Fatalf("failed to load config: %v", err) + } - os.Exit(0) + db := kv.InitDB(kv.Config{Path: cfg.Database.Path}, true) + defer db.Close() + + apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) + promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) + + registry := prometheus.NewRegistry() + registry.MustRegister(agentmetrics.NewAgentCollector(db)) + + go agentapi.Start(apiAddr) + go promserver.Start(promAddr, registry) + + select {} } From dc701886eb726ca76cf0d407607597219eb97ece Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 12 Apr 2026 17:31:42 +0200 Subject: [PATCH 07/44] f-21: go: add import lib Signed-off-by: GnomeZworc --- go.mod | 11 +++++++++-- go.sum | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 7430e72..a4b0b38 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 toolchain go1.24.11 require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/dgraph-io/badger/v4 v4.8.0 // indirect @@ -17,7 +18,12 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -31,9 +37,10 @@ require ( go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.28.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.8 // indirect ) diff --git a/go.sum b/go.sum index 73d16f3..402452d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= @@ -23,8 +25,18 @@ github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6F github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -51,10 +63,14 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= @@ -65,4 +81,6 @@ golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 2fbde24e8928d36de6dcddeb941e68d709f77ca0 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 14 Apr 2026 21:24:13 +0200 Subject: [PATCH 08/44] f-21: code: ajout d'un system de worker Signed-off-by: GnomeZworc --- cmd/agent/main.go | 14 +++++++++----- internal/api/agent/server.go | 20 +++++++++++++++----- internal/api/agent/subnet.go | 7 ++++++- internal/api/agent/subnets.go | 7 ++++++- internal/api/agent/vpc.go | 7 ++++++- internal/api/agent/vpcs.go | 7 ++++++- internal/config/agent/struct.go | 6 ++++++ pkg/worker/queue.go | 33 +++++++++++++++++++++++++++++++++ 8 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 pkg/worker/queue.go diff --git a/cmd/agent/main.go b/cmd/agent/main.go index ce3df4e..56011bf 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -6,10 +6,11 @@ import ( "log" agentapi "git.g3e.fr/syonad/two/internal/api/agent" - agentmetrics "git.g3e.fr/syonad/two/internal/prometheus/agent" configuration "git.g3e.fr/syonad/two/internal/config/agent" - promserver "git.g3e.fr/syonad/two/pkg/prometheus" + agentmetrics "git.g3e.fr/syonad/two/internal/prometheus/agent" "git.g3e.fr/syonad/two/pkg/db/kv" + promserver "git.g3e.fr/syonad/two/pkg/prometheus" + "git.g3e.fr/syonad/two/pkg/worker" "github.com/prometheus/client_golang/prometheus" ) @@ -25,13 +26,16 @@ func main() { db := kv.InitDB(kv.Config{Path: cfg.Database.Path}, true) defer db.Close() - apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) - promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) + q := worker.New(cfg.Worker.BufferSize) + q.Start(cfg.Worker.Count) registry := prometheus.NewRegistry() registry.MustRegister(agentmetrics.NewAgentCollector(db)) - go agentapi.Start(apiAddr) + apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) + promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) + + go agentapi.New(q).Start(apiAddr) go promserver.Start(promAddr, registry) select {} diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go index 05653a4..aebe2c3 100644 --- a/internal/api/agent/server.go +++ b/internal/api/agent/server.go @@ -3,14 +3,24 @@ package agentapi import ( "log" "net/http" + + "git.g3e.fr/syonad/two/pkg/worker" ) -func Start(address string) { +type Server struct { + queue *worker.Queue +} + +func New(queue *worker.Queue) *Server { + return &Server{queue: queue} +} + +func (s *Server) Start(address string) { mux := http.NewServeMux() - mux.HandleFunc("/vpcs", VpcsHandler) - mux.HandleFunc("/vpcs/", VpcByNameHandler) - mux.HandleFunc("/subnets", SubnetsHandler) - mux.HandleFunc("/subnets/", SubnetByNameHandler) + mux.HandleFunc("/vpcs", s.VpcsHandler) + mux.HandleFunc("/vpcs/", s.VpcByNameHandler) + mux.HandleFunc("/subnets", s.SubnetsHandler) + mux.HandleFunc("/subnets/", s.SubnetByNameHandler) log.Printf("API server listening on %s", address) log.Fatal(http.ListenAndServe(address, mux)) } diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go index e46e4f4..52b27d0 100644 --- a/internal/api/agent/subnet.go +++ b/internal/api/agent/subnet.go @@ -6,7 +6,7 @@ import ( "strings" ) -func SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { +func (s *Server) SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/subnets/") if name == "" { http.NotFound(w, r) @@ -18,8 +18,13 @@ func SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"name": name}) case http.MethodDelete: + s.queue.Submit(func() { + deleteSubnet(name) + }) w.WriteHeader(http.StatusAccepted) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } + +func deleteSubnet(name string) {} diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index 53dce2e..b3d587b 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -5,15 +5,20 @@ import ( "net/http" ) -func SubnetsHandler(w http.ResponseWriter, r *http.Request) { +func (s *Server) SubnetsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode([]interface{}{}) case http.MethodPost: + s.queue.Submit(func() { + createSubnet() + }) w.WriteHeader(http.StatusAccepted) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } + +func createSubnet() {} diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index aecfaa1..73cecd9 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -6,7 +6,7 @@ import ( "strings" ) -func VpcByNameHandler(w http.ResponseWriter, r *http.Request) { +func (s *Server) VpcByNameHandler(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/vpcs/") if name == "" { http.NotFound(w, r) @@ -18,8 +18,13 @@ func VpcByNameHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"name": name}) case http.MethodDelete: + s.queue.Submit(func() { + deleteVpc(name) + }) w.WriteHeader(http.StatusAccepted) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } + +func deleteVpc(name string) {} diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index c4e9b27..57386b6 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -5,15 +5,20 @@ import ( "net/http" ) -func VpcsHandler(w http.ResponseWriter, r *http.Request) { +func (s *Server) VpcsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode([]interface{}{}) case http.MethodPost: + s.queue.Submit(func() { + createVpc() + }) w.WriteHeader(http.StatusAccepted) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } + +func createVpc() {} diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 0cca127..24ad0f5 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -16,6 +16,10 @@ type Config struct { Address string `mapstructure:"address"` Port int `mapstructure:"port"` } `mapstructure:"prometheus"` + Worker struct { + Count int `mapstructure:"count"` + BufferSize int `mapstructure:"buffer_size"` + } `mapstructure:"worker"` } func LoadConfig(path string) (*Config, error) { @@ -28,6 +32,8 @@ func LoadConfig(path string) (*Config, error) { v.SetDefault("api.port", 8080) v.SetDefault("prometheus.address", "") v.SetDefault("prometheus.port", 9090) + v.SetDefault("worker.count", 4) + v.SetDefault("worker.buffer_size", 100) v.ReadInConfig() diff --git a/pkg/worker/queue.go b/pkg/worker/queue.go new file mode 100644 index 0000000..109c726 --- /dev/null +++ b/pkg/worker/queue.go @@ -0,0 +1,33 @@ +package worker + +import "log" + +// Task is a function to be executed asynchronously by a worker. +type Task func() + +// Queue is a FIFO channel-backed task queue consumed by worker goroutines. +type Queue struct { + tasks chan Task +} + +// New creates a Queue with the given channel buffer size. +func New(bufferSize int) *Queue { + return &Queue{tasks: make(chan Task, bufferSize)} +} + +// Submit enqueues a task. Blocks if the queue is full. +func (q *Queue) Submit(t Task) { + q.tasks <- t +} + +// Start launches n worker goroutines that consume and execute tasks. +func (q *Queue) Start(n int) { + log.Printf("worker: starting %d workers", n) + for i := range n { + go func(id int) { + for task := range q.tasks { + task() + } + }(i) + } +} From ac43979b38cb43909a111c53f9a63bb8b0e47644 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 16 Apr 2026 22:40:27 +0200 Subject: [PATCH 09/44] f-21: code: set db tu readwrite Signed-off-by: GnomeZworc --- cmd/agent/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 56011bf..5ebf09f 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -23,7 +23,7 @@ func main() { log.Fatalf("failed to load config: %v", err) } - db := kv.InitDB(kv.Config{Path: cfg.Database.Path}, true) + db := kv.InitDB(kv.Config{Path: cfg.Database.Path}, false) defer db.Close() q := worker.New(cfg.Worker.BufferSize) From 327a3590073fa8613a11007210e8459b71952c9b Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 16 Apr 2026 22:57:20 +0200 Subject: [PATCH 10/44] f-21: code: separate and create route fonction Signed-off-by: GnomeZworc --- internal/api/agent/models.go | 33 ++++++++++++++++++++++++++ internal/api/agent/server.go | 9 ++++++- internal/api/agent/subnet.go | 29 ++++++++++++++++------- internal/api/agent/subnets.go | 44 ++++++++++++++++++++++++++++------- internal/api/agent/vpc.go | 29 ++++++++++++++++------- internal/api/agent/vpcs.go | 36 +++++++++++++++++++++------- 6 files changed, 145 insertions(+), 35 deletions(-) create mode 100644 internal/api/agent/models.go diff --git a/internal/api/agent/models.go b/internal/api/agent/models.go new file mode 100644 index 0000000..bd09c76 --- /dev/null +++ b/internal/api/agent/models.go @@ -0,0 +1,33 @@ +package agentapi + +type VPCCreateRequest struct { + Name string `json:"name"` +} + +type VPC struct { + Name string `json:"name"` + State string `json:"state"` +} + +type SubnetCreateRequest struct { + Name string `json:"name"` + VPC string `json:"vpc"` + VxlanID int `json:"vxlan_id"` + LocalIP string `json:"local_ip"` + GatewayIP string `json:"gateway_ip"` + CIDR string `json:"cidr"` +} + +type Subnet struct { + Name string `json:"name"` + State string `json:"state"` + VPC string `json:"vpc"` + VxlanID int `json:"vxlan_id"` + LocalIP string `json:"local_ip"` + GatewayIP string `json:"gateway_ip"` + CIDR string `json:"cidr"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go index aebe2c3..75833c9 100644 --- a/internal/api/agent/server.go +++ b/internal/api/agent/server.go @@ -22,5 +22,12 @@ func (s *Server) Start(address string) { mux.HandleFunc("/subnets", s.SubnetsHandler) mux.HandleFunc("/subnets/", s.SubnetByNameHandler) log.Printf("API server listening on %s", address) - log.Fatal(http.ListenAndServe(address, mux)) + log.Fatal(http.ListenAndServe(address, logMiddleware(mux))) +} + +func logMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path) + next.ServeHTTP(w, r) + }) } diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go index 52b27d0..289667f 100644 --- a/internal/api/agent/subnet.go +++ b/internal/api/agent/subnet.go @@ -9,22 +9,33 @@ import ( func (s *Server) SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/subnets/") if name == "" { - http.NotFound(w, r) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: "resource not found"}) return } w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"name": name}) + s.getSubnet(w, r, name) case http.MethodDelete: - s.queue.Submit(func() { - deleteSubnet(name) - }) - w.WriteHeader(http.StatusAccepted) + s.deleteSubnet(w, r, name) default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } } -func deleteSubnet(name string) {} +func (s *Server) getSubnet(w http.ResponseWriter, r *http.Request, name string) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(Subnet{Name: name, State: "created"}) +} + +func (s *Server) deleteSubnet(w http.ResponseWriter, r *http.Request, name string) { + s.queue.Submit(func() { + destroySubnet(name) + }) + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(Subnet{Name: name, State: "deleting"}) +} + +func destroySubnet(name string) {} diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index b3d587b..5e0ebdc 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -9,16 +9,44 @@ func (s *Server) SubnetsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode([]interface{}{}) + s.listSubnets(w, r) case http.MethodPost: - s.queue.Submit(func() { - createSubnet() - }) - w.WriteHeader(http.StatusAccepted) + s.postSubnet(w, r) default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } } -func createSubnet() {} +func (s *Server) listSubnets(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode([]Subnet{}) +} + +func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { + var req SubnetCreateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"}) + return + } + if req.Name == "" || req.VPC == "" || req.LocalIP == "" || req.GatewayIP == "" || req.CIDR == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, local_ip, gateway_ip and cidr are required"}) + return + } + s.queue.Submit(func() { + createSubnet(req) + }) + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(Subnet{ + Name: req.Name, + State: "creating", + VPC: req.VPC, + VxlanID: req.VxlanID, + LocalIP: req.LocalIP, + GatewayIP: req.GatewayIP, + CIDR: req.CIDR, + }) +} + +func createSubnet(req SubnetCreateRequest) {} diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index 73cecd9..f1d34c1 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -9,22 +9,33 @@ import ( func (s *Server) VpcByNameHandler(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/vpcs/") if name == "" { - http.NotFound(w, r) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: "resource not found"}) return } w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"name": name}) + s.getVpc(w, r, name) case http.MethodDelete: - s.queue.Submit(func() { - deleteVpc(name) - }) - w.WriteHeader(http.StatusAccepted) + s.deleteVpc(w, r, name) default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } } -func deleteVpc(name string) {} +func (s *Server) getVpc(w http.ResponseWriter, r *http.Request, name string) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(VPC{Name: name, State: "created"}) +} + +func (s *Server) deleteVpc(w http.ResponseWriter, r *http.Request, name string) { + s.queue.Submit(func() { + destroyVpc(name) + }) + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(VPC{Name: name, State: "deleting"}) +} + +func destroyVpc(name string) {} diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index 57386b6..aba4e7a 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -9,16 +9,36 @@ func (s *Server) VpcsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode([]interface{}{}) + s.listVpcs(w, r) case http.MethodPost: - s.queue.Submit(func() { - createVpc() - }) - w.WriteHeader(http.StatusAccepted) + s.postVpc(w, r) default: - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) } } -func createVpc() {} +func (s *Server) listVpcs(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode([]VPC{}) +} + +func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) { + var req VPCCreateRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"}) + return + } + if req.Name == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(ErrorResponse{Error: "name is required"}) + return + } + s.queue.Submit(func() { + createVpc(req.Name) + }) + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(VPC{Name: req.Name, State: "creating"}) +} + +func createVpc(name string) {} From 82fa401680c15fa5588ce6c1ffa00dc63a422e5d Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 16 Apr 2026 22:57:53 +0200 Subject: [PATCH 11/44] f-21: doc: rename api doc Signed-off-by: GnomeZworc --- api/{openapi.yaml => agent.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/{openapi.yaml => agent.yaml} (100%) diff --git a/api/openapi.yaml b/api/agent.yaml similarity index 100% rename from api/openapi.yaml rename to api/agent.yaml From 59e83da1d6820e89123a6409db29812bf3416be0 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Fri, 17 Apr 2026 23:25:35 +0200 Subject: [PATCH 12/44] f-21: code: add database in api Signed-off-by: GnomeZworc --- cmd/agent/main.go | 2 +- internal/api/agent/server.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 5ebf09f..2f5e084 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -35,7 +35,7 @@ func main() { apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) - go agentapi.New(q).Start(apiAddr) + go agentapi.New(q, db).Start(apiAddr) go promserver.Start(promAddr, registry) select {} diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go index 75833c9..7e4eb21 100644 --- a/internal/api/agent/server.go +++ b/internal/api/agent/server.go @@ -5,14 +5,16 @@ import ( "net/http" "git.g3e.fr/syonad/two/pkg/worker" + "github.com/dgraph-io/badger/v4" ) type Server struct { queue *worker.Queue + db *badger.DB } -func New(queue *worker.Queue) *Server { - return &Server{queue: queue} +func New(queue *worker.Queue, db *badger.DB) *Server { + return &Server{queue: queue, db: db} } func (s *Server) Start(address string) { From 49aa9a7ab6a3ba81f78711436a40ed07eda908fd Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Fri, 17 Apr 2026 23:26:04 +0200 Subject: [PATCH 13/44] f-21: code: add vpc gestion in api Signed-off-by: GnomeZworc --- internal/api/agent/vpc.go | 20 +++++++++++++++++--- internal/api/agent/vpcs.go | 13 +++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index f1d34c1..3396d23 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -2,8 +2,13 @@ package agentapi import ( "encoding/json" + "fmt" "net/http" + "os" "strings" + + "git.g3e.fr/syonad/two/internal/vpc" + "git.g3e.fr/syonad/two/pkg/db/kv" ) func (s *Server) VpcByNameHandler(w http.ResponseWriter, r *http.Request) { @@ -25,14 +30,23 @@ func (s *Server) VpcByNameHandler(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) getVpc(w http.ResponseWriter, r *http.Request, name string) { +func (s *Server) getVpc(w http.ResponseWriter, _ *http.Request, name string) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(VPC{Name: name, State: "created"}) } -func (s *Server) deleteVpc(w http.ResponseWriter, r *http.Request, name string) { +func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) { s.queue.Submit(func() { - destroyVpc(name) + kv.AddInDB(s.db, "vpc/"+name+"/state", "deleting") + if err := vpc.DeleteVPC(s.db, name); err != nil { + fmt.Println(err) + } + if state, err := kv.GetFromDB(s.db, "vpc/"+name+"/state"); err != nil { + fmt.Println(err) + os.Exit(1) + } else if state == "deleted" { + kv.DeleteInDB(s.db, "vpc/"+name) + } }) w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(VPC{Name: name, State: "deleting"}) diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index aba4e7a..2dddd42 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -2,7 +2,11 @@ package agentapi import ( "encoding/json" + "fmt" "net/http" + + "git.g3e.fr/syonad/two/internal/vpc" + "git.g3e.fr/syonad/two/pkg/db/kv" ) func (s *Server) VpcsHandler(w http.ResponseWriter, r *http.Request) { @@ -17,7 +21,7 @@ func (s *Server) VpcsHandler(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) listVpcs(w http.ResponseWriter, r *http.Request) { +func (s *Server) listVpcs(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode([]VPC{}) } @@ -35,10 +39,11 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) { return } s.queue.Submit(func() { - createVpc(req.Name) + kv.AddInDB(s.db, "vpc/"+req.Name+"/state", "creating") + if err := vpc.CreateVPC(s.db, req.Name); err != nil { + fmt.Println(err) + } }) w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(VPC{Name: req.Name, State: "creating"}) } - -func createVpc(name string) {} From 7ee85275829818eb5151b47689ceaa056efe2aab Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 19 Apr 2026 00:04:46 +0200 Subject: [PATCH 14/44] f-21: git: add claude to gitignore Signed-off-by: GnomeZworc --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3a0b507..68ae417 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# Output of the go coverage tool, specifically when used with LiteIDE +.claude + # Dependency directories (remove the comment below to include it) # vendor/ @@ -26,4 +29,4 @@ go.work.sum .env # ignore local info -data/ \ No newline at end of file +data/ From 63a288f69eafc0f41ca6550fa06543cc816af801 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 19 Apr 2026 00:06:50 +0200 Subject: [PATCH 15/44] f-21: refactor: add dispatcher layer for MQTT migration Introduce internal/dispatcher package with a Command interface and typed commands (CreateVPC, DeleteVPC, CreateSubnet, DeleteSubnet). The API handlers now call dispatcher.Dispatch() instead of enqueuing closures directly, decoupling transport (HTTP today, MQTT tomorrow) from execution. Signed-off-by: GnomeZworc --- cmd/agent/main.go | 4 ++- internal/api/agent/server.go | 10 ++++---- internal/api/agent/subnet.go | 7 +++--- internal/api/agent/subnets.go | 12 ++++++--- internal/api/agent/vpc.go | 19 ++------------ internal/api/agent/vpcs.go | 11 ++------ internal/dispatcher/dispatcher.go | 29 +++++++++++++++++++++ internal/dispatcher/subnet_commands.go | 26 +++++++++++++++++++ internal/dispatcher/vpc_commands.go | 35 ++++++++++++++++++++++++++ 9 files changed, 114 insertions(+), 39 deletions(-) create mode 100644 internal/dispatcher/dispatcher.go create mode 100644 internal/dispatcher/subnet_commands.go create mode 100644 internal/dispatcher/vpc_commands.go diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 2f5e084..b61bb6f 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -7,6 +7,7 @@ import ( agentapi "git.g3e.fr/syonad/two/internal/api/agent" configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/dispatcher" agentmetrics "git.g3e.fr/syonad/two/internal/prometheus/agent" "git.g3e.fr/syonad/two/pkg/db/kv" promserver "git.g3e.fr/syonad/two/pkg/prometheus" @@ -35,7 +36,8 @@ func main() { apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) - go agentapi.New(q, db).Start(apiAddr) + d := dispatcher.New(q, db) + go agentapi.New(d, db).Start(apiAddr) go promserver.Start(promAddr, registry) select {} diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go index 7e4eb21..7f3247c 100644 --- a/internal/api/agent/server.go +++ b/internal/api/agent/server.go @@ -4,17 +4,17 @@ import ( "log" "net/http" - "git.g3e.fr/syonad/two/pkg/worker" + "git.g3e.fr/syonad/two/internal/dispatcher" "github.com/dgraph-io/badger/v4" ) type Server struct { - queue *worker.Queue - db *badger.DB + dispatcher *dispatcher.Dispatcher + db *badger.DB } -func New(queue *worker.Queue, db *badger.DB) *Server { - return &Server{queue: queue, db: db} +func New(d *dispatcher.Dispatcher, db *badger.DB) *Server { + return &Server{dispatcher: d, db: db} } func (s *Server) Start(address string) { diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go index 289667f..b20c65a 100644 --- a/internal/api/agent/subnet.go +++ b/internal/api/agent/subnet.go @@ -4,6 +4,8 @@ import ( "encoding/json" "net/http" "strings" + + "git.g3e.fr/syonad/two/internal/dispatcher" ) func (s *Server) SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { @@ -31,11 +33,8 @@ func (s *Server) getSubnet(w http.ResponseWriter, r *http.Request, name string) } func (s *Server) deleteSubnet(w http.ResponseWriter, r *http.Request, name string) { - s.queue.Submit(func() { - destroySubnet(name) - }) + s.dispatcher.Dispatch(dispatcher.DeleteSubnetCommand{Name: name}) w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(Subnet{Name: name, State: "deleting"}) } -func destroySubnet(name string) {} diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index 5e0ebdc..b251c3c 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -3,6 +3,8 @@ package agentapi import ( "encoding/json" "net/http" + + "git.g3e.fr/syonad/two/internal/dispatcher" ) func (s *Server) SubnetsHandler(w http.ResponseWriter, r *http.Request) { @@ -34,8 +36,13 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, local_ip, gateway_ip and cidr are required"}) return } - s.queue.Submit(func() { - createSubnet(req) + s.dispatcher.Dispatch(dispatcher.CreateSubnetCommand{ + Name: req.Name, + VPC: req.VPC, + VxlanID: req.VxlanID, + LocalIP: req.LocalIP, + GatewayIP: req.GatewayIP, + CIDR: req.CIDR, }) w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(Subnet{ @@ -49,4 +56,3 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { }) } -func createSubnet(req SubnetCreateRequest) {} diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index 3396d23..60d614e 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -2,13 +2,10 @@ package agentapi import ( "encoding/json" - "fmt" "net/http" - "os" "strings" - "git.g3e.fr/syonad/two/internal/vpc" - "git.g3e.fr/syonad/two/pkg/db/kv" + "git.g3e.fr/syonad/two/internal/dispatcher" ) func (s *Server) VpcByNameHandler(w http.ResponseWriter, r *http.Request) { @@ -36,20 +33,8 @@ func (s *Server) getVpc(w http.ResponseWriter, _ *http.Request, name string) { } func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) { - s.queue.Submit(func() { - kv.AddInDB(s.db, "vpc/"+name+"/state", "deleting") - if err := vpc.DeleteVPC(s.db, name); err != nil { - fmt.Println(err) - } - if state, err := kv.GetFromDB(s.db, "vpc/"+name+"/state"); err != nil { - fmt.Println(err) - os.Exit(1) - } else if state == "deleted" { - kv.DeleteInDB(s.db, "vpc/"+name) - } - }) + s.dispatcher.Dispatch(dispatcher.DeleteVPCCommand{Name: name}) w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(VPC{Name: name, State: "deleting"}) } -func destroyVpc(name string) {} diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index 2dddd42..eb633e3 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -2,11 +2,9 @@ package agentapi import ( "encoding/json" - "fmt" "net/http" - "git.g3e.fr/syonad/two/internal/vpc" - "git.g3e.fr/syonad/two/pkg/db/kv" + "git.g3e.fr/syonad/two/internal/dispatcher" ) func (s *Server) VpcsHandler(w http.ResponseWriter, r *http.Request) { @@ -38,12 +36,7 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ErrorResponse{Error: "name is required"}) return } - s.queue.Submit(func() { - kv.AddInDB(s.db, "vpc/"+req.Name+"/state", "creating") - if err := vpc.CreateVPC(s.db, req.Name); err != nil { - fmt.Println(err) - } - }) + s.dispatcher.Dispatch(dispatcher.CreateVPCCommand{Name: req.Name}) w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(VPC{Name: req.Name, State: "creating"}) } diff --git a/internal/dispatcher/dispatcher.go b/internal/dispatcher/dispatcher.go new file mode 100644 index 0000000..937adf2 --- /dev/null +++ b/internal/dispatcher/dispatcher.go @@ -0,0 +1,29 @@ +package dispatcher + +import ( + "log" + + "git.g3e.fr/syonad/two/pkg/worker" + "github.com/dgraph-io/badger/v4" +) + +type Command interface { + Execute(db *badger.DB) error +} + +type Dispatcher struct { + queue *worker.Queue + db *badger.DB +} + +func New(queue *worker.Queue, db *badger.DB) *Dispatcher { + return &Dispatcher{queue: queue, db: db} +} + +func (d *Dispatcher) Dispatch(cmd Command) { + d.queue.Submit(func() { + if err := cmd.Execute(d.db); err != nil { + log.Printf("command error (%T): %v", cmd, err) + } + }) +} diff --git a/internal/dispatcher/subnet_commands.go b/internal/dispatcher/subnet_commands.go new file mode 100644 index 0000000..d8ef8e3 --- /dev/null +++ b/internal/dispatcher/subnet_commands.go @@ -0,0 +1,26 @@ +package dispatcher + +import "github.com/dgraph-io/badger/v4" + +type CreateSubnetCommand struct { + Name string + VPC string + VxlanID int + LocalIP string + GatewayIP string + CIDR string +} + +func (c CreateSubnetCommand) Execute(db *badger.DB) error { + // TODO: brancher internal/subnet/create.go + return nil +} + +type DeleteSubnetCommand struct { + Name string +} + +func (c DeleteSubnetCommand) Execute(db *badger.DB) error { + // TODO: brancher internal/subnet/delete.go + return nil +} diff --git a/internal/dispatcher/vpc_commands.go b/internal/dispatcher/vpc_commands.go new file mode 100644 index 0000000..195b014 --- /dev/null +++ b/internal/dispatcher/vpc_commands.go @@ -0,0 +1,35 @@ +package dispatcher + +import ( + "git.g3e.fr/syonad/two/internal/vpc" + "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/dgraph-io/badger/v4" +) + +type CreateVPCCommand struct { + Name string +} + +func (c CreateVPCCommand) Execute(db *badger.DB) error { + kv.AddInDB(db, "vpc/"+c.Name+"/state", "creating") + return vpc.CreateVPC(db, c.Name) +} + +type DeleteVPCCommand struct { + Name string +} + +func (c DeleteVPCCommand) Execute(db *badger.DB) error { + kv.AddInDB(db, "vpc/"+c.Name+"/state", "deleting") + if err := vpc.DeleteVPC(db, c.Name); err != nil { + return err + } + state, err := kv.GetFromDB(db, "vpc/"+c.Name+"/state") + if err != nil { + return err + } + if state == "deleted" { + kv.DeleteInDB(db, "vpc/"+c.Name) + } + return nil +} From 3127c052160c9d19f5f17d68e9a9c6c5ff56f92c Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 19 Apr 2026 20:49:50 +0200 Subject: [PATCH 16/44] f-21: clean: delete vpc notions Signed-off-by: GnomeZworc --- .forgejo/workflows/prerelease.yml | 1 - cmd/vpc/main.go | 65 ------------------------------- 2 files changed, 66 deletions(-) delete mode 100644 cmd/vpc/main.go diff --git a/.forgejo/workflows/prerelease.yml b/.forgejo/workflows/prerelease.yml index 8003e82..ed635e9 100644 --- a/.forgejo/workflows/prerelease.yml +++ b/.forgejo/workflows/prerelease.yml @@ -37,7 +37,6 @@ jobs: - metadata - metacli - agent - - vpc - dhcp - subnet uses: ./.forgejo/workflows/build.yml diff --git a/cmd/vpc/main.go b/cmd/vpc/main.go deleted file mode 100644 index e73f7e9..0000000 --- a/cmd/vpc/main.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - configuration "git.g3e.fr/syonad/two/internal/config/agent" - "git.g3e.fr/syonad/two/internal/vpc" - "git.g3e.fr/syonad/two/pkg/db/kv" - "github.com/dgraph-io/badger/v4" -) - -var ( - netns = flag.String("netns", "", "Network namespace à faire") - name = flag.String("name", "", "interface name") - action = flag.String("action", "", "Action a faire") - conf_file = flag.String("conf", "/etc/two/agent.yml", "configuration file") -) - -var DB *badger.DB - -func main() { - flag.Parse() - - conf, err := configuration.LoadConfig(*conf_file) - if err != nil { - fmt.Println(err) - return - } - - DB = kv.InitDB(kv.Config{ - Path: conf.Database.Path, - }, false) - defer DB.Close() - - switch *action { - case "create": - kv.AddInDB(DB, "vpc/"+*name+"/state", "creating") - if err := vpc.CreateVPC(DB, *name); err != nil { - fmt.Println(err) - } - case "delete": - kv.AddInDB(DB, "vpc/"+*name+"/state", "deleting") - if err := vpc.DeleteVPC(DB, *name); err != nil { - fmt.Println(err) - } - if state, err := kv.GetFromDB(DB, "vpc/"+*name+"/state"); err != nil { - fmt.Println(err) - os.Exit(1) - } else if state == "deleted" { - kv.DeleteInDB(DB, "vpc/"+*name) - } - case "check": - if state, err := kv.GetFromDB(DB, "vpc/"+*name+"/state"); err != nil { - os.Exit(1) - } else if state != "created" { - os.Exit(1) - } - default: - fmt.Printf("Available commande:\n - create\n - delete\n - check\n") - os.Exit(1) - } - os.Exit(0) -} From 6a5a646eaad1928431336ab7c0a82a5c2d73d8b4 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 19 Apr 2026 21:02:54 +0200 Subject: [PATCH 17/44] f-21: code: add dispatch command for subnet Signed-off-by: GnomeZworc --- internal/dispatcher/subnet_commands.go | 31 ++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/internal/dispatcher/subnet_commands.go b/internal/dispatcher/subnet_commands.go index d8ef8e3..b23949e 100644 --- a/internal/dispatcher/subnet_commands.go +++ b/internal/dispatcher/subnet_commands.go @@ -1,6 +1,14 @@ package dispatcher -import "github.com/dgraph-io/badger/v4" +import ( + "fmt" + "os" + "strconv" + + "git.g3e.fr/syonad/two/internal/subnet" + "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/dgraph-io/badger/v4" +) type CreateSubnetCommand struct { Name string @@ -12,8 +20,13 @@ type CreateSubnetCommand struct { } func (c CreateSubnetCommand) Execute(db *badger.DB) error { - // TODO: brancher internal/subnet/create.go - return nil + kv.AddInDB(db, "subnet/"+c.Name+"/state", "creating") + kv.AddInDB(db, "subnet/"+c.Name+"/vpc", c.VPC) + kv.AddInDB(db, "subnet/"+c.Name+"/vxlan_id", strconv.Itoa(c.VxlanID)) + kv.AddInDB(db, "subnet/"+c.Name+"/local_ip", c.LocalIP) + kv.AddInDB(db, "subnet/"+c.Name+"/gateway_ip", c.GatewayIP) + kv.AddInDB(db, "subnet/"+c.Name+"/cidr", c.CIDR) + return subnet.CreateSubnet(db, c.Name) } type DeleteSubnetCommand struct { @@ -21,6 +34,16 @@ type DeleteSubnetCommand struct { } func (c DeleteSubnetCommand) Execute(db *badger.DB) error { - // TODO: brancher internal/subnet/delete.go + kv.AddInDB(db, "subnet/"+c.Name+"/state", "deleting") + if err := subnet.DeleteSubnet(db, c.Name); err != nil { + fmt.Println(err) + os.Exit(1) + } + if state, err := kv.GetFromDB(db, "subnet/"+c.Name+"/state"); err != nil { + fmt.Println(err) + os.Exit(1) + } else if state == "deleted" { + kv.DeleteInDB(db, "subnet/"+c.Name) + } return nil } From 066b90dff45a1e4db560799def3dc7e1251eb4a5 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 21 Apr 2026 21:27:19 +0200 Subject: [PATCH 18/44] f-21: code: add interface name in config Signed-off-by: GnomeZworc --- conf/agent/config.exemple.yml | 7 ++++++- internal/config/agent/struct.go | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/conf/agent/config.exemple.yml b/conf/agent/config.exemple.yml index a2b9f1b..5bd1dfd 100644 --- a/conf/agent/config.exemple.yml +++ b/conf/agent/config.exemple.yml @@ -1,2 +1,7 @@ database: - path: "/var/lib/two/data/" \ No newline at end of file + path: "/var/lib/two/data/" + +interfaces: + vms: br-000000 + internet: br-000000 + admin: br-000000 \ No newline at end of file diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 24ad0f5..21dde0d 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -20,6 +20,7 @@ type Config struct { Count int `mapstructure:"count"` BufferSize int `mapstructure:"buffer_size"` } `mapstructure:"worker"` + Interfaces map[string]string `mapstructure:"interfaces"` } func LoadConfig(path string) (*Config, error) { From 9ec6e64327a2c63ad9cb970a59934682c185edb1 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 21 Apr 2026 21:28:20 +0200 Subject: [PATCH 19/44] f-21: code: change from ip to interface type Signed-off-by: GnomeZworc --- cmd/agent/main.go | 2 +- internal/api/agent/models.go | 16 ++++++++-------- internal/api/agent/subnets.go | 8 +++----- internal/dispatcher/dispatcher.go | 13 +++++++------ internal/dispatcher/subnet_commands.go | 12 ++++++++---- internal/dispatcher/vpc_commands.go | 4 ++-- internal/netif/vxlan.go | 16 +++++++++------- internal/subnet/create.go | 10 +++------- 8 files changed, 41 insertions(+), 40 deletions(-) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index b61bb6f..faa6139 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -36,7 +36,7 @@ func main() { apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) - d := dispatcher.New(q, db) + d := dispatcher.New(q, db, cfg.Interfaces) go agentapi.New(d, db).Start(apiAddr) go promserver.Start(promAddr, registry) diff --git a/internal/api/agent/models.go b/internal/api/agent/models.go index bd09c76..e826082 100644 --- a/internal/api/agent/models.go +++ b/internal/api/agent/models.go @@ -13,19 +13,19 @@ type SubnetCreateRequest struct { Name string `json:"name"` VPC string `json:"vpc"` VxlanID int `json:"vxlan_id"` - LocalIP string `json:"local_ip"` + IfaceType string `json:"iface_type"` GatewayIP string `json:"gateway_ip"` CIDR string `json:"cidr"` } type Subnet struct { - Name string `json:"name"` - State string `json:"state"` - VPC string `json:"vpc"` - VxlanID int `json:"vxlan_id"` - LocalIP string `json:"local_ip"` - GatewayIP string `json:"gateway_ip"` - CIDR string `json:"cidr"` + Name string `json:"name"` + State string `json:"state"` + VPC string `json:"vpc"` + VxlanID int `json:"vxlan_id"` + LocalIface string `json:"local_iface"` + GatewayIP string `json:"gateway_ip"` + CIDR string `json:"cidr"` } type ErrorResponse struct { diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index b251c3c..263c9ce 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -31,16 +31,16 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"}) return } - if req.Name == "" || req.VPC == "" || req.LocalIP == "" || req.GatewayIP == "" || req.CIDR == "" { + if req.Name == "" || req.VPC == "" || req.IfaceType == "" || req.GatewayIP == "" || req.CIDR == "" { w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, local_ip, gateway_ip and cidr are required"}) + json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, iface_type, gateway_ip and cidr are required"}) return } s.dispatcher.Dispatch(dispatcher.CreateSubnetCommand{ Name: req.Name, VPC: req.VPC, VxlanID: req.VxlanID, - LocalIP: req.LocalIP, + IfaceType: req.IfaceType, GatewayIP: req.GatewayIP, CIDR: req.CIDR, }) @@ -50,9 +50,7 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { State: "creating", VPC: req.VPC, VxlanID: req.VxlanID, - LocalIP: req.LocalIP, GatewayIP: req.GatewayIP, CIDR: req.CIDR, }) } - diff --git a/internal/dispatcher/dispatcher.go b/internal/dispatcher/dispatcher.go index 937adf2..f240245 100644 --- a/internal/dispatcher/dispatcher.go +++ b/internal/dispatcher/dispatcher.go @@ -8,21 +8,22 @@ import ( ) type Command interface { - Execute(db *badger.DB) error + Execute(db *badger.DB, interfaces map[string]string) error } type Dispatcher struct { - queue *worker.Queue - db *badger.DB + queue *worker.Queue + db *badger.DB + interfaces map[string]string } -func New(queue *worker.Queue, db *badger.DB) *Dispatcher { - return &Dispatcher{queue: queue, db: db} +func New(queue *worker.Queue, db *badger.DB, interfaces map[string]string) *Dispatcher { + return &Dispatcher{queue: queue, db: db, interfaces: interfaces} } func (d *Dispatcher) Dispatch(cmd Command) { d.queue.Submit(func() { - if err := cmd.Execute(d.db); err != nil { + if err := cmd.Execute(d.db, d.interfaces); err != nil { log.Printf("command error (%T): %v", cmd, err) } }) diff --git a/internal/dispatcher/subnet_commands.go b/internal/dispatcher/subnet_commands.go index b23949e..dcb0398 100644 --- a/internal/dispatcher/subnet_commands.go +++ b/internal/dispatcher/subnet_commands.go @@ -14,16 +14,20 @@ type CreateSubnetCommand struct { Name string VPC string VxlanID int - LocalIP string + IfaceType string GatewayIP string CIDR string } -func (c CreateSubnetCommand) Execute(db *badger.DB) error { +func (c CreateSubnetCommand) Execute(db *badger.DB, interfaces map[string]string) error { + localIface, ok := interfaces[c.IfaceType] + if !ok { + return fmt.Errorf("unknown iface_type %q: not found in config", c.IfaceType) + } kv.AddInDB(db, "subnet/"+c.Name+"/state", "creating") kv.AddInDB(db, "subnet/"+c.Name+"/vpc", c.VPC) kv.AddInDB(db, "subnet/"+c.Name+"/vxlan_id", strconv.Itoa(c.VxlanID)) - kv.AddInDB(db, "subnet/"+c.Name+"/local_ip", c.LocalIP) + kv.AddInDB(db, "subnet/"+c.Name+"/local_iface", localIface) kv.AddInDB(db, "subnet/"+c.Name+"/gateway_ip", c.GatewayIP) kv.AddInDB(db, "subnet/"+c.Name+"/cidr", c.CIDR) return subnet.CreateSubnet(db, c.Name) @@ -33,7 +37,7 @@ type DeleteSubnetCommand struct { Name string } -func (c DeleteSubnetCommand) Execute(db *badger.DB) error { +func (c DeleteSubnetCommand) Execute(db *badger.DB, _ map[string]string) error { kv.AddInDB(db, "subnet/"+c.Name+"/state", "deleting") if err := subnet.DeleteSubnet(db, c.Name); err != nil { fmt.Println(err) diff --git a/internal/dispatcher/vpc_commands.go b/internal/dispatcher/vpc_commands.go index 195b014..2c316a3 100644 --- a/internal/dispatcher/vpc_commands.go +++ b/internal/dispatcher/vpc_commands.go @@ -10,7 +10,7 @@ type CreateVPCCommand struct { Name string } -func (c CreateVPCCommand) Execute(db *badger.DB) error { +func (c CreateVPCCommand) Execute(db *badger.DB, _ map[string]string) error { kv.AddInDB(db, "vpc/"+c.Name+"/state", "creating") return vpc.CreateVPC(db, c.Name) } @@ -19,7 +19,7 @@ type DeleteVPCCommand struct { Name string } -func (c DeleteVPCCommand) Execute(db *badger.DB) error { +func (c DeleteVPCCommand) Execute(db *badger.DB, _ map[string]string) error { kv.AddInDB(db, "vpc/"+c.Name+"/state", "deleting") if err := vpc.DeleteVPC(db, c.Name); err != nil { return err diff --git a/internal/netif/vxlan.go b/internal/netif/vxlan.go index eacae7c..6523f37 100644 --- a/internal/netif/vxlan.go +++ b/internal/netif/vxlan.go @@ -1,20 +1,22 @@ package netif import ( - "net" - "github.com/vishvananda/netlink" ) -func CreateVxlan(name string, vxlanID int, localIP net.IP) error { +func CreateVxlan(name string, vxlanID int, localIface string) error { + link, err := netlink.LinkByName(localIface) + if err != nil { + return err + } vxlan := &netlink.Vxlan{ LinkAttrs: netlink.LinkAttrs{ Name: name, }, - VxlanId: vxlanID, - Port: 4789, - SrcAddr: localIP, - Learning: false, + VxlanId: vxlanID, + Port: 4789, + VtepDevIndex: link.Attrs().Index, + Learning: false, } return netlink.LinkAdd(vxlan) } diff --git a/internal/subnet/create.go b/internal/subnet/create.go index 9bfe02a..1ed514e 100644 --- a/internal/subnet/create.go +++ b/internal/subnet/create.go @@ -40,13 +40,9 @@ func CreateSubnet(db *badger.DB, subnetName string) error { return fmt.Errorf("parse vxlan_id: %w", err) } - localIPStr, err := kv.GetFromDB(db, "subnet/"+subnetName+"/local_ip") + localIface, err := kv.GetFromDB(db, "subnet/"+subnetName+"/local_iface") if err != nil { - return fmt.Errorf("get local_ip: %w", err) - } - localIP := net.ParseIP(localIPStr) - if localIP == nil { - return fmt.Errorf("invalid local_ip: %s", localIPStr) + return fmt.Errorf("get local_iface: %w", err) } gatewayIPStr, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip") @@ -90,7 +86,7 @@ func CreateSubnet(db *badger.DB, subnetName string) error { } // vxlan - if err := netif.CreateVxlan(vxlanIface, vxlanID, localIP); err != nil { + if err := netif.CreateVxlan(vxlanIface, vxlanID, localIface); err != nil { return fmt.Errorf("create vxlan: %w", err) } From 8a68d3818fd3b76923a86810fc0ab95028d94101 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 21 Apr 2026 21:32:59 +0200 Subject: [PATCH 20/44] f-21: code: swap from interface only to full conf Signed-off-by: GnomeZworc --- cmd/agent/main.go | 2 +- internal/dispatcher/dispatcher.go | 15 ++++++++------- internal/dispatcher/subnet_commands.go | 7 ++++--- internal/dispatcher/vpc_commands.go | 5 +++-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index faa6139..dd6fd6d 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -36,7 +36,7 @@ func main() { apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) - d := dispatcher.New(q, db, cfg.Interfaces) + d := dispatcher.New(q, db, cfg) go agentapi.New(d, db).Start(apiAddr) go promserver.Start(promAddr, registry) diff --git a/internal/dispatcher/dispatcher.go b/internal/dispatcher/dispatcher.go index f240245..8e94827 100644 --- a/internal/dispatcher/dispatcher.go +++ b/internal/dispatcher/dispatcher.go @@ -3,27 +3,28 @@ package dispatcher import ( "log" + configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/pkg/worker" "github.com/dgraph-io/badger/v4" ) type Command interface { - Execute(db *badger.DB, interfaces map[string]string) error + Execute(db *badger.DB, cfg *configuration.Config) error } type Dispatcher struct { - queue *worker.Queue - db *badger.DB - interfaces map[string]string + queue *worker.Queue + db *badger.DB + cfg *configuration.Config } -func New(queue *worker.Queue, db *badger.DB, interfaces map[string]string) *Dispatcher { - return &Dispatcher{queue: queue, db: db, interfaces: interfaces} +func New(queue *worker.Queue, db *badger.DB, cfg *configuration.Config) *Dispatcher { + return &Dispatcher{queue: queue, db: db, cfg: cfg} } func (d *Dispatcher) Dispatch(cmd Command) { d.queue.Submit(func() { - if err := cmd.Execute(d.db, d.interfaces); err != nil { + if err := cmd.Execute(d.db, d.cfg); err != nil { log.Printf("command error (%T): %v", cmd, err) } }) diff --git a/internal/dispatcher/subnet_commands.go b/internal/dispatcher/subnet_commands.go index dcb0398..c2a0cff 100644 --- a/internal/dispatcher/subnet_commands.go +++ b/internal/dispatcher/subnet_commands.go @@ -5,6 +5,7 @@ import ( "os" "strconv" + configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/internal/subnet" "git.g3e.fr/syonad/two/pkg/db/kv" "github.com/dgraph-io/badger/v4" @@ -19,8 +20,8 @@ type CreateSubnetCommand struct { CIDR string } -func (c CreateSubnetCommand) Execute(db *badger.DB, interfaces map[string]string) error { - localIface, ok := interfaces[c.IfaceType] +func (c CreateSubnetCommand) Execute(db *badger.DB, cfg *configuration.Config) error { + localIface, ok := cfg.Interfaces[c.IfaceType] if !ok { return fmt.Errorf("unknown iface_type %q: not found in config", c.IfaceType) } @@ -37,7 +38,7 @@ type DeleteSubnetCommand struct { Name string } -func (c DeleteSubnetCommand) Execute(db *badger.DB, _ map[string]string) error { +func (c DeleteSubnetCommand) Execute(db *badger.DB, _ *configuration.Config) error { kv.AddInDB(db, "subnet/"+c.Name+"/state", "deleting") if err := subnet.DeleteSubnet(db, c.Name); err != nil { fmt.Println(err) diff --git a/internal/dispatcher/vpc_commands.go b/internal/dispatcher/vpc_commands.go index 2c316a3..b2687fc 100644 --- a/internal/dispatcher/vpc_commands.go +++ b/internal/dispatcher/vpc_commands.go @@ -1,6 +1,7 @@ package dispatcher import ( + configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/internal/vpc" "git.g3e.fr/syonad/two/pkg/db/kv" "github.com/dgraph-io/badger/v4" @@ -10,7 +11,7 @@ type CreateVPCCommand struct { Name string } -func (c CreateVPCCommand) Execute(db *badger.DB, _ map[string]string) error { +func (c CreateVPCCommand) Execute(db *badger.DB, _ *configuration.Config) error { kv.AddInDB(db, "vpc/"+c.Name+"/state", "creating") return vpc.CreateVPC(db, c.Name) } @@ -19,7 +20,7 @@ type DeleteVPCCommand struct { Name string } -func (c DeleteVPCCommand) Execute(db *badger.DB, _ map[string]string) error { +func (c DeleteVPCCommand) Execute(db *badger.DB, _ *configuration.Config) error { kv.AddInDB(db, "vpc/"+c.Name+"/state", "deleting") if err := vpc.DeleteVPC(db, c.Name); err != nil { return err From 7dd795f4aad8234e5d9e6fa6d4fad69d9a7854d7 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 21 Apr 2026 21:38:46 +0200 Subject: [PATCH 21/44] f-21: code: add default interface Signed-off-by: GnomeZworc --- conf/agent/config.exemple.yml | 4 +++- internal/config/agent/struct.go | 4 +++- internal/dispatcher/subnet_commands.go | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/conf/agent/config.exemple.yml b/conf/agent/config.exemple.yml index 5bd1dfd..bc1d437 100644 --- a/conf/agent/config.exemple.yml +++ b/conf/agent/config.exemple.yml @@ -1,7 +1,9 @@ database: path: "/var/lib/two/data/" +default_interface: br-000000 + interfaces: vms: br-000000 internet: br-000000 - admin: br-000000 \ No newline at end of file + admin: br-000000 diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 21dde0d..1c9fc9f 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -20,7 +20,8 @@ type Config struct { Count int `mapstructure:"count"` BufferSize int `mapstructure:"buffer_size"` } `mapstructure:"worker"` - Interfaces map[string]string `mapstructure:"interfaces"` + DefaultInterface string `mapstructure:"default_interface"` + Interfaces map[string]string `mapstructure:"interfaces"` } func LoadConfig(path string) (*Config, error) { @@ -35,6 +36,7 @@ func LoadConfig(path string) (*Config, error) { v.SetDefault("prometheus.port", 9090) v.SetDefault("worker.count", 4) v.SetDefault("worker.buffer_size", 100) + v.SetDefault("default_interface", "br-000000") v.ReadInConfig() diff --git a/internal/dispatcher/subnet_commands.go b/internal/dispatcher/subnet_commands.go index c2a0cff..0d471c3 100644 --- a/internal/dispatcher/subnet_commands.go +++ b/internal/dispatcher/subnet_commands.go @@ -23,7 +23,7 @@ type CreateSubnetCommand struct { func (c CreateSubnetCommand) Execute(db *badger.DB, cfg *configuration.Config) error { localIface, ok := cfg.Interfaces[c.IfaceType] if !ok { - return fmt.Errorf("unknown iface_type %q: not found in config", c.IfaceType) + localIface = cfg.DefaultInterface } kv.AddInDB(db, "subnet/"+c.Name+"/state", "creating") kv.AddInDB(db, "subnet/"+c.Name+"/vpc", c.VPC) From b70fdb66f096db5cc389196c4deb2e3f7a011557 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 21 Apr 2026 21:43:17 +0200 Subject: [PATCH 22/44] f-21: api: update api Signed-off-by: GnomeZworc --- api/agent.yaml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/api/agent.yaml b/api/agent.yaml index be328d0..2b3acc7 100644 --- a/api/agent.yaml +++ b/api/agent.yaml @@ -127,6 +127,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Subnet" + "400": + description: Missing required field or unknown iface_type + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "409": description: Subnet already exists content: @@ -219,7 +225,7 @@ components: SubnetCreateRequest: type: object - required: [name, vpc, vxlan_id, local_ip, gateway_ip, cidr] + required: [name, vpc, vxlan_id, gateway_ip, cidr] properties: name: type: string @@ -233,11 +239,10 @@ components: type: integer description: VXLAN VNI identifier example: 100 - local_ip: + iface_type: type: string - format: ipv4 - description: Local VTEP IP address - example: "10.0.0.5" + description: Interface type key defined in the agent config (e.g. vms, internet, admin). Falls back to default_interface if omitted or unknown. + example: vms gateway_ip: type: string format: ipv4 @@ -264,9 +269,10 @@ components: vxlan_id: type: integer example: 100 - local_ip: + local_iface: type: string - example: "10.0.0.5" + description: Resolved interface name + example: br-000000 gateway_ip: type: string example: "10.10.10.1" From 2779002c634fcc212696120cb69ee01b6c269865 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 21 Apr 2026 21:52:03 +0200 Subject: [PATCH 23/44] f-21: vpc: change to be ok in the name Signed-off-by: GnomeZworc --- api/agent.yaml | 7 ++++--- internal/vpc/create.go | 25 +++++++++---------------- internal/vpc/delete.go | 6 +++++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/api/agent.yaml b/api/agent.yaml index 2b3acc7..1d3a77f 100644 --- a/api/agent.yaml +++ b/api/agent.yaml @@ -209,15 +209,16 @@ components: properties: name: type: string - description: Unique name for the VPC - example: vpc1 + description: Unique name for the VPC, must follow the format vp-[id] + pattern: '^vp-.+' + example: vp-00001 VPC: type: object properties: name: type: string - example: vpc1 + example: vp-00001 state: type: string enum: [creating, created, deleting, deleted] diff --git a/internal/vpc/create.go b/internal/vpc/create.go index a6e6aaa..148f70c 100644 --- a/internal/vpc/create.go +++ b/internal/vpc/create.go @@ -1,6 +1,8 @@ package vpc import ( + "strings" + "git.g3e.fr/syonad/two/internal/netif" "git.g3e.fr/syonad/two/internal/netns" "git.g3e.fr/syonad/two/pkg/db/kv" @@ -9,49 +11,40 @@ import ( ) func CreateVPC(db *badger.DB, name string) error { - // missing - // search data in db - // change state in db - - // create netns if state, err := kv.GetFromDB(db, "vpc/"+name+"/state"); err != nil { return err } else if state == "creating" { + vpcID := strings.SplitN(name, "-", 2)[1] + if err := netns.Create(name); err != nil { return err } - // create veth public for this netns - if err := netif.CreateVethToNetns("vp-"+name+"-e", "vp-public-i", "/var/run/netns/"+name, 9000); err != nil { + if err := netif.CreateVethToNetns("vp-"+vpcID+"-e", "vp-"+vpcID+"-i", "/var/run/netns/"+name, 9000); err != nil { return err } - // create public bridge in netns if err := netns.Call(name, func() error { return netif.CreateBridge("br-public", 1500) }); err != nil { return err } - // set veth to ext public bridge - if err := netif.BridgeSetMaster("vp-"+name+"-e", "br-public"); err != nil { + if err := netif.BridgeSetMaster("vp-"+vpcID+"-e", "br-public"); err != nil { return err } - // set veth to int public bridge if err := netns.Call(name, func() error { - return netif.BridgeSetMaster("vp-public-i", "br-public") + return netif.BridgeSetMaster("vp-"+vpcID+"-i", "br-public") }); err != nil { return err } - // set set ext veth up - if err := netif.LinkSetUp("vp-" + name + "-e"); err != nil { + if err := netif.LinkSetUp("vp-" + vpcID + "-e"); err != nil { return err } - // set set int veth up if err := netns.Call(name, func() error { - return netif.LinkSetUp("vp-public-i") + return netif.LinkSetUp("vp-" + vpcID + "-i") }); err != nil { return err } diff --git a/internal/vpc/delete.go b/internal/vpc/delete.go index 10c863d..dbd3a59 100644 --- a/internal/vpc/delete.go +++ b/internal/vpc/delete.go @@ -1,6 +1,8 @@ package vpc import ( + "strings" + "git.g3e.fr/syonad/two/internal/netif" "git.g3e.fr/syonad/two/internal/netns" "git.g3e.fr/syonad/two/pkg/db/kv" @@ -12,7 +14,9 @@ func DeleteVPC(db *badger.DB, name string) error { if state, err := kv.GetFromDB(db, "vpc/"+name+"/state"); err != nil { return err } else if state == "deleting" { - if err := netif.DeleteLink("vp-" + name + "-e"); err != nil { + vpcID := strings.SplitN(name, "-", 2)[1] + + if err := netif.DeleteLink("vp-" + vpcID + "-e"); err != nil { return err } From 6c0e5a0509c1f787bd2ddba05461cb48c08e0245 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Wed, 22 Apr 2026 17:27:06 +0200 Subject: [PATCH 24/44] f-21: clean: delete dhcp binari Signed-off-by: GnomeZworc --- .forgejo/workflows/prerelease.yml | 1 - cmd/dhcp/main.go | 64 ------------------------------- 2 files changed, 65 deletions(-) delete mode 100644 cmd/dhcp/main.go diff --git a/.forgejo/workflows/prerelease.yml b/.forgejo/workflows/prerelease.yml index ed635e9..2393b44 100644 --- a/.forgejo/workflows/prerelease.yml +++ b/.forgejo/workflows/prerelease.yml @@ -37,7 +37,6 @@ jobs: - metadata - metacli - agent - - dhcp - subnet uses: ./.forgejo/workflows/build.yml with: diff --git a/cmd/dhcp/main.go b/cmd/dhcp/main.go deleted file mode 100644 index b2ce08d..0000000 --- a/cmd/dhcp/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "net" - "os" - - "git.g3e.fr/syonad/two/internal/dhcp" - "git.g3e.fr/syonad/two/pkg/systemd" -) - -func main() { - subnet := flag.String("subnet", "", "Subnet CIDR (e.g. 10.10.10.0/24)") - name := flag.String("name", "", "Config name (e.g. vpc1_br-00002)") - gateway := flag.String("gateway", "", "Gateway IP (e.g. 10.10.10.1)") - confDir := flag.String("confdir", "/etc/dnsmasq.d", "dnsmasq config directory") - flag.Parse() - - if *subnet == "" || *name == "" || *gateway == "" { - flag.Usage() - os.Exit(1) - } - - _, network, err := net.ParseCIDR(*subnet) - if err != nil { - fmt.Fprintf(os.Stderr, "invalid subnet: %v\n", err) - os.Exit(1) - } - - gw := net.ParseIP(*gateway) - if gw == nil { - fmt.Fprintf(os.Stderr, "invalid gateway IP: %q\n", *gateway) - os.Exit(1) - } - - conf := dhcp.Config{ - Network: network, - Gateway: gw, - Name: *name, - ConfDir: *confDir, - } - - confPath, err := dhcp.GenerateConfig(conf) - if err != nil { - fmt.Fprintf(os.Stderr, "error generating config: %v\n", err) - os.Exit(1) - } - fmt.Printf("dnsmasq config written to %s\n", confPath) - - svc, err := systemd.New() - if err != nil { - fmt.Fprintf(os.Stderr, "error connecting to systemd: %v\n", err) - os.Exit(1) - } - defer svc.Close() - - unit := "dnsmasq@" + *name + ".service" - if err := svc.Start(unit); err != nil { - fmt.Fprintf(os.Stderr, "error starting %s: %v\n", unit, err) - os.Exit(1) - } - fmt.Printf("started %s\n", unit) -} From 15a7f50d01183ea61773fd5df1f431319ce15ad2 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Wed, 22 Apr 2026 17:27:42 +0200 Subject: [PATCH 25/44] f-21: clean: delete subnet binari Signed-off-by: GnomeZworc --- .forgejo/workflows/prerelease.yml | 1 - cmd/subnet/main.go | 93 ------------------------------- 2 files changed, 94 deletions(-) delete mode 100644 cmd/subnet/main.go diff --git a/.forgejo/workflows/prerelease.yml b/.forgejo/workflows/prerelease.yml index 2393b44..2440fbd 100644 --- a/.forgejo/workflows/prerelease.yml +++ b/.forgejo/workflows/prerelease.yml @@ -37,7 +37,6 @@ jobs: - metadata - metacli - agent - - subnet uses: ./.forgejo/workflows/build.yml with: tag: ${{ needs.set-release-target.outputs.release_cible }} diff --git a/cmd/subnet/main.go b/cmd/subnet/main.go deleted file mode 100644 index 05ecd47..0000000 --- a/cmd/subnet/main.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - configuration "git.g3e.fr/syonad/two/internal/config/agent" - "git.g3e.fr/syonad/two/internal/subnet" - "git.g3e.fr/syonad/two/pkg/db/kv" - - "github.com/dgraph-io/badger/v4" -) - -var ( - name = flag.String("name", "", "Subnet name (ex: sn-00001)") - vpcName = flag.String("vpc", "", "VPC name") - vxlanID = flag.String("vxlan-id", "", "VXLAN ID") - localIP = flag.String("local-ip", "", "Local VTEP IP") - gatewayIP = flag.String("gateway-ip", "", "Gateway IP") - cidr = flag.String("cidr", "", "Subnet CIDR (ex: 10.10.10.0/24)") - action = flag.String("action", "", "Action à effectuer") - conf_file = flag.String("conf", "/etc/two/agent.yml", "Configuration file") -) - -var DB *badger.DB - -func main() { - flag.Parse() - - conf, err := configuration.LoadConfig(*conf_file) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - DB = kv.InitDB(kv.Config{ - Path: conf.Database.Path, - }, false) - defer DB.Close() - - switch *action { - case "create": - if *name == "" || *vpcName == "" || *vxlanID == "" || *localIP == "" || *gatewayIP == "" || *cidr == "" { - fmt.Println("create requires: -name -vpc -vxlan-id -local-ip -gateway-ip -cidr") - os.Exit(1) - } - kv.AddInDB(DB, "subnet/"+*name+"/state", "creating") - kv.AddInDB(DB, "subnet/"+*name+"/vpc", *vpcName) - kv.AddInDB(DB, "subnet/"+*name+"/vxlan_id", *vxlanID) - kv.AddInDB(DB, "subnet/"+*name+"/local_ip", *localIP) - kv.AddInDB(DB, "subnet/"+*name+"/gateway_ip", *gatewayIP) - kv.AddInDB(DB, "subnet/"+*name+"/cidr", *cidr) - if err := subnet.CreateSubnet(DB, *name); err != nil { - fmt.Println(err) - os.Exit(1) - } - - case "delete": - if *name == "" { - fmt.Println("delete requires: -name") - os.Exit(1) - } - kv.AddInDB(DB, "subnet/"+*name+"/state", "deleting") - if err := subnet.DeleteSubnet(DB, *name); err != nil { - fmt.Println(err) - os.Exit(1) - } - if state, err := kv.GetFromDB(DB, "subnet/"+*name+"/state"); err != nil { - fmt.Println(err) - os.Exit(1) - } else if state == "deleted" { - kv.DeleteInDB(DB, "subnet/"+*name) - } - - case "check": - if *name == "" { - fmt.Println("check requires: -name") - os.Exit(1) - } - if state, err := kv.GetFromDB(DB, "subnet/"+*name+"/state"); err != nil { - os.Exit(1) - } else if state != "created" { - os.Exit(1) - } - - default: - fmt.Printf("Available commands:\n - create\n - delete\n - check\n") - os.Exit(1) - } - - os.Exit(0) -} From 0f70b32076c12aba795248debca01275fb14dfee Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Wed, 22 Apr 2026 17:28:02 +0200 Subject: [PATCH 26/44] f-21: systemd: add agent systemd Signed-off-by: GnomeZworc --- systemd/agent.service | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 systemd/agent.service diff --git a/systemd/agent.service b/systemd/agent.service new file mode 100644 index 0000000..37715c4 --- /dev/null +++ b/systemd/agent.service @@ -0,0 +1,10 @@ +[Unit] +Description=Agent service +After=network.target + +[Service] +Type=simple +ExecStart=/opt/two/bin/agent + +[Install] +WantedBy=multi-user.target From 77cf180b103a664d2e44ac013600fcf02f41f1e3 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 23 Apr 2026 22:29:43 +0200 Subject: [PATCH 27/44] f-21: api: make vpc get Signed-off-by: GnomeZworc --- internal/api/agent/vpc.go | 9 ++++++++- internal/api/agent/vpcs.go | 28 +++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index 60d614e..8151e1d 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -6,6 +6,7 @@ import ( "strings" "git.g3e.fr/syonad/two/internal/dispatcher" + "git.g3e.fr/syonad/two/pkg/db/kv" ) func (s *Server) VpcByNameHandler(w http.ResponseWriter, r *http.Request) { @@ -28,8 +29,14 @@ func (s *Server) VpcByNameHandler(w http.ResponseWriter, r *http.Request) { } func (s *Server) getVpc(w http.ResponseWriter, _ *http.Request, name string) { + state, err := kv.GetFromDB(s.db, "vpc/"+name+"/state") + if err != nil { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: "vpc not found"}) + return + } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(VPC{Name: name, State: "created"}) + json.NewEncoder(w).Encode(VPC{Name: name, State: state}) } func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) { diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index eb633e3..a6ee70e 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -3,8 +3,10 @@ package agentapi import ( "encoding/json" "net/http" + "strings" "git.g3e.fr/syonad/two/internal/dispatcher" + "git.g3e.fr/syonad/two/pkg/db/kv" ) func (s *Server) VpcsHandler(w http.ResponseWriter, r *http.Request) { @@ -20,8 +22,32 @@ func (s *Server) VpcsHandler(w http.ResponseWriter, r *http.Request) { } func (s *Server) listVpcs(w http.ResponseWriter, _ *http.Request) { + entries, err := kv.ListByPrefix(s.db, "vpc/") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to list vpcs"}) + return + } + vpcs := make(map[string]*VPC) + for key, value := range entries { + parts := strings.Split(key, "/") + if len(parts) != 3 { + continue + } + name := parts[1] + if _, ok := vpcs[name]; !ok { + vpcs[name] = &VPC{Name: name} + } + if parts[2] == "state" { + vpcs[name].State = value + } + } + result := make([]VPC, 0, len(vpcs)) + for _, v := range vpcs { + result = append(result, *v) + } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode([]VPC{}) + json.NewEncoder(w).Encode(result) } func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) { From 0c2dbdb525913be2de953d87b2668d81b9ffe5b9 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 23 Apr 2026 22:30:04 +0200 Subject: [PATCH 28/44] f-21: api: make subnet get Signed-off-by: GnomeZworc --- internal/api/agent/subnet.go | 33 +++++++++++++++++++++++++-- internal/api/agent/subnets.go | 42 +++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go index b20c65a..8ea3d19 100644 --- a/internal/api/agent/subnet.go +++ b/internal/api/agent/subnet.go @@ -3,9 +3,11 @@ package agentapi import ( "encoding/json" "net/http" + "strconv" "strings" "git.g3e.fr/syonad/two/internal/dispatcher" + "git.g3e.fr/syonad/two/pkg/db/kv" ) func (s *Server) SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { @@ -27,9 +29,36 @@ func (s *Server) SubnetByNameHandler(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) getSubnet(w http.ResponseWriter, r *http.Request, name string) { +func (s *Server) getSubnet(w http.ResponseWriter, _ *http.Request, name string) { + entries, err := kv.ListByPrefix(s.db, "subnet/"+name+"/") + if err != nil || len(entries) == 0 { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: "subnet not found"}) + return + } + sub := Subnet{Name: name} + for key, value := range entries { + parts := strings.Split(key, "/") + if len(parts) != 3 { + continue + } + switch parts[2] { + case "state": + sub.State = value + case "vpc": + sub.VPC = value + case "vxlan_id": + sub.VxlanID, _ = strconv.Atoi(value) + case "local_iface": + sub.LocalIface = value + case "gateway_ip": + sub.GatewayIP = value + case "cidr": + sub.CIDR = value + } + } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(Subnet{Name: name, State: "created"}) + json.NewEncoder(w).Encode(sub) } func (s *Server) deleteSubnet(w http.ResponseWriter, r *http.Request, name string) { diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index 263c9ce..90faaff 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -3,8 +3,11 @@ package agentapi import ( "encoding/json" "net/http" + "strconv" + "strings" "git.g3e.fr/syonad/two/internal/dispatcher" + "git.g3e.fr/syonad/two/pkg/db/kv" ) func (s *Server) SubnetsHandler(w http.ResponseWriter, r *http.Request) { @@ -19,9 +22,44 @@ func (s *Server) SubnetsHandler(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) listSubnets(w http.ResponseWriter, r *http.Request) { +func (s *Server) listSubnets(w http.ResponseWriter, _ *http.Request) { + entries, err := kv.ListByPrefix(s.db, "subnet/") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to list subnets"}) + return + } + subnets := make(map[string]*Subnet) + for key, value := range entries { + parts := strings.Split(key, "/") + if len(parts) != 3 { + continue + } + name := parts[1] + if _, ok := subnets[name]; !ok { + subnets[name] = &Subnet{Name: name} + } + switch parts[2] { + case "state": + subnets[name].State = value + case "vpc": + subnets[name].VPC = value + case "vxlan_id": + subnets[name].VxlanID, _ = strconv.Atoi(value) + case "local_iface": + subnets[name].LocalIface = value + case "gateway_ip": + subnets[name].GatewayIP = value + case "cidr": + subnets[name].CIDR = value + } + } + result := make([]Subnet, 0, len(subnets)) + for _, sub := range subnets { + result = append(result, *sub) + } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode([]Subnet{}) + json.NewEncoder(w).Encode(result) } func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { From 19434b1848f95b86eece111183ad07113025f6cf Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 23 Apr 2026 22:38:20 +0200 Subject: [PATCH 29/44] f-21: refacto: move dispatcher to agent specific files Signed-off-by: GnomeZworc --- cmd/agent/main.go | 2 +- internal/api/agent/server.go | 2 +- internal/api/agent/subnet.go | 3 +-- internal/api/agent/subnets.go | 2 +- internal/api/agent/vpc.go | 3 +-- internal/api/agent/vpcs.go | 2 +- internal/dispatcher/{ => agent}/dispatcher.go | 0 internal/dispatcher/{ => agent}/subnet_commands.go | 0 internal/dispatcher/{ => agent}/vpc_commands.go | 0 9 files changed, 6 insertions(+), 8 deletions(-) rename internal/dispatcher/{ => agent}/dispatcher.go (100%) rename internal/dispatcher/{ => agent}/subnet_commands.go (100%) rename internal/dispatcher/{ => agent}/vpc_commands.go (100%) diff --git a/cmd/agent/main.go b/cmd/agent/main.go index dd6fd6d..bb56aba 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -7,7 +7,7 @@ import ( agentapi "git.g3e.fr/syonad/two/internal/api/agent" configuration "git.g3e.fr/syonad/two/internal/config/agent" - "git.g3e.fr/syonad/two/internal/dispatcher" + dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" agentmetrics "git.g3e.fr/syonad/two/internal/prometheus/agent" "git.g3e.fr/syonad/two/pkg/db/kv" promserver "git.g3e.fr/syonad/two/pkg/prometheus" diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go index 7f3247c..276b416 100644 --- a/internal/api/agent/server.go +++ b/internal/api/agent/server.go @@ -4,7 +4,7 @@ import ( "log" "net/http" - "git.g3e.fr/syonad/two/internal/dispatcher" + dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" "github.com/dgraph-io/badger/v4" ) diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go index 8ea3d19..61cbd6f 100644 --- a/internal/api/agent/subnet.go +++ b/internal/api/agent/subnet.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "git.g3e.fr/syonad/two/internal/dispatcher" + dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" "git.g3e.fr/syonad/two/pkg/db/kv" ) @@ -66,4 +66,3 @@ func (s *Server) deleteSubnet(w http.ResponseWriter, r *http.Request, name strin w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(Subnet{Name: name, State: "deleting"}) } - diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index 90faaff..18fb81f 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "git.g3e.fr/syonad/two/internal/dispatcher" + dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" "git.g3e.fr/syonad/two/pkg/db/kv" ) diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index 8151e1d..fd1bfdb 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "git.g3e.fr/syonad/two/internal/dispatcher" + dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" "git.g3e.fr/syonad/two/pkg/db/kv" ) @@ -44,4 +44,3 @@ func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(VPC{Name: name, State: "deleting"}) } - diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index a6ee70e..735259c 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "git.g3e.fr/syonad/two/internal/dispatcher" + dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" "git.g3e.fr/syonad/two/pkg/db/kv" ) diff --git a/internal/dispatcher/dispatcher.go b/internal/dispatcher/agent/dispatcher.go similarity index 100% rename from internal/dispatcher/dispatcher.go rename to internal/dispatcher/agent/dispatcher.go diff --git a/internal/dispatcher/subnet_commands.go b/internal/dispatcher/agent/subnet_commands.go similarity index 100% rename from internal/dispatcher/subnet_commands.go rename to internal/dispatcher/agent/subnet_commands.go diff --git a/internal/dispatcher/vpc_commands.go b/internal/dispatcher/agent/vpc_commands.go similarity index 100% rename from internal/dispatcher/vpc_commands.go rename to internal/dispatcher/agent/vpc_commands.go From 1d86c45ba4e4bbcf48d5222dfcb628c617e9b210 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 23 Apr 2026 23:11:10 +0200 Subject: [PATCH 30/44] f-21: dispatch: add a prepare step to dispatch Signed-off-by: GnomeZworc --- internal/api/agent/subnet.go | 13 +++++-- internal/api/agent/subnets.go | 41 +++++++++++++++----- internal/api/agent/vpc.go | 11 +++++- internal/api/agent/vpcs.go | 11 +++++- internal/dispatcher/agent/dispatcher.go | 5 +++ internal/dispatcher/agent/subnet_commands.go | 24 +++++++++++- internal/dispatcher/agent/vpc_commands.go | 18 ++++++++- 7 files changed, 102 insertions(+), 21 deletions(-) diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go index 61cbd6f..556b7c3 100644 --- a/internal/api/agent/subnet.go +++ b/internal/api/agent/subnet.go @@ -61,8 +61,15 @@ func (s *Server) getSubnet(w http.ResponseWriter, _ *http.Request, name string) json.NewEncoder(w).Encode(sub) } -func (s *Server) deleteSubnet(w http.ResponseWriter, r *http.Request, name string) { - s.dispatcher.Dispatch(dispatcher.DeleteSubnetCommand{Name: name}) +func (s *Server) deleteSubnet(w http.ResponseWriter, _ *http.Request, name string) { + cmd := dispatcher.DeleteSubnetCommand{Name: name} + if err := s.dispatcher.Prepare(cmd); err != nil { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()}) + return + } + s.dispatcher.Dispatch(cmd) + state, _ := kv.GetFromDB(s.db, "subnet/"+name+"/state") w.WriteHeader(http.StatusAccepted) - json.NewEncoder(w).Encode(Subnet{Name: name, State: "deleting"}) + json.NewEncoder(w).Encode(Subnet{Name: name, State: state}) } diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index 18fb81f..9e99a45 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -74,21 +74,42 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, iface_type, gateway_ip and cidr are required"}) return } - s.dispatcher.Dispatch(dispatcher.CreateSubnetCommand{ + cmd := dispatcher.CreateSubnetCommand{ Name: req.Name, VPC: req.VPC, VxlanID: req.VxlanID, IfaceType: req.IfaceType, GatewayIP: req.GatewayIP, CIDR: req.CIDR, - }) + } + if err := s.dispatcher.Prepare(cmd); err != nil { + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()}) + return + } + s.dispatcher.Dispatch(cmd) + entries, _ := kv.ListByPrefix(s.db, "subnet/"+req.Name+"/") + sub := Subnet{Name: req.Name} + for key, value := range entries { + parts := strings.Split(key, "/") + if len(parts) != 3 { + continue + } + switch parts[2] { + case "state": + sub.State = value + case "vpc": + sub.VPC = value + case "vxlan_id": + sub.VxlanID, _ = strconv.Atoi(value) + case "local_iface": + sub.LocalIface = value + case "gateway_ip": + sub.GatewayIP = value + case "cidr": + sub.CIDR = value + } + } w.WriteHeader(http.StatusAccepted) - json.NewEncoder(w).Encode(Subnet{ - Name: req.Name, - State: "creating", - VPC: req.VPC, - VxlanID: req.VxlanID, - GatewayIP: req.GatewayIP, - CIDR: req.CIDR, - }) + json.NewEncoder(w).Encode(sub) } diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index fd1bfdb..be0724e 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -40,7 +40,14 @@ func (s *Server) getVpc(w http.ResponseWriter, _ *http.Request, name string) { } func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) { - s.dispatcher.Dispatch(dispatcher.DeleteVPCCommand{Name: name}) + cmd := dispatcher.DeleteVPCCommand{Name: name} + if err := s.dispatcher.Prepare(cmd); err != nil { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()}) + return + } + s.dispatcher.Dispatch(cmd) + state, _ := kv.GetFromDB(s.db, "vpc/"+name+"/state") w.WriteHeader(http.StatusAccepted) - json.NewEncoder(w).Encode(VPC{Name: name, State: "deleting"}) + json.NewEncoder(w).Encode(VPC{Name: name, State: state}) } diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index 735259c..3087456 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -62,7 +62,14 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ErrorResponse{Error: "name is required"}) return } - s.dispatcher.Dispatch(dispatcher.CreateVPCCommand{Name: req.Name}) + cmd := dispatcher.CreateVPCCommand{Name: req.Name} + if err := s.dispatcher.Prepare(cmd); err != nil { + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()}) + return + } + s.dispatcher.Dispatch(cmd) + state, _ := kv.GetFromDB(s.db, "vpc/"+req.Name+"/state") w.WriteHeader(http.StatusAccepted) - json.NewEncoder(w).Encode(VPC{Name: req.Name, State: "creating"}) + json.NewEncoder(w).Encode(VPC{Name: req.Name, State: state}) } diff --git a/internal/dispatcher/agent/dispatcher.go b/internal/dispatcher/agent/dispatcher.go index 8e94827..6b1ca67 100644 --- a/internal/dispatcher/agent/dispatcher.go +++ b/internal/dispatcher/agent/dispatcher.go @@ -9,6 +9,7 @@ import ( ) type Command interface { + Prepare(db *badger.DB, cfg *configuration.Config) error Execute(db *badger.DB, cfg *configuration.Config) error } @@ -22,6 +23,10 @@ func New(queue *worker.Queue, db *badger.DB, cfg *configuration.Config) *Dispatc return &Dispatcher{queue: queue, db: db, cfg: cfg} } +func (d *Dispatcher) Prepare(cmd Command) error { + return cmd.Prepare(d.db, d.cfg) +} + func (d *Dispatcher) Dispatch(cmd Command) { d.queue.Submit(func() { if err := cmd.Execute(d.db, d.cfg); err != nil { diff --git a/internal/dispatcher/agent/subnet_commands.go b/internal/dispatcher/agent/subnet_commands.go index 0d471c3..c86cc77 100644 --- a/internal/dispatcher/agent/subnet_commands.go +++ b/internal/dispatcher/agent/subnet_commands.go @@ -20,7 +20,17 @@ type CreateSubnetCommand struct { CIDR string } -func (c CreateSubnetCommand) Execute(db *badger.DB, cfg *configuration.Config) error { +func (c CreateSubnetCommand) Prepare(db *badger.DB, cfg *configuration.Config) error { + if _, err := kv.GetFromDB(db, "subnet/"+c.Name+"/state"); err == nil { + return fmt.Errorf("subnet %q already exists", c.Name) + } + vpcState, err := kv.GetFromDB(db, "vpc/"+c.VPC+"/state") + if err != nil { + return fmt.Errorf("vpc %q not found", c.VPC) + } + if vpcState == "deleting" || vpcState == "deleted" { + return fmt.Errorf("vpc %q is %s", c.VPC, vpcState) + } localIface, ok := cfg.Interfaces[c.IfaceType] if !ok { localIface = cfg.DefaultInterface @@ -31,6 +41,10 @@ func (c CreateSubnetCommand) Execute(db *badger.DB, cfg *configuration.Config) e kv.AddInDB(db, "subnet/"+c.Name+"/local_iface", localIface) kv.AddInDB(db, "subnet/"+c.Name+"/gateway_ip", c.GatewayIP) kv.AddInDB(db, "subnet/"+c.Name+"/cidr", c.CIDR) + return nil +} + +func (c CreateSubnetCommand) Execute(db *badger.DB, _ *configuration.Config) error { return subnet.CreateSubnet(db, c.Name) } @@ -38,8 +52,14 @@ type DeleteSubnetCommand struct { Name string } +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) + } + return kv.AddInDB(db, "subnet/"+c.Name+"/state", "deleting") +} + func (c DeleteSubnetCommand) Execute(db *badger.DB, _ *configuration.Config) error { - kv.AddInDB(db, "subnet/"+c.Name+"/state", "deleting") if err := subnet.DeleteSubnet(db, c.Name); err != nil { fmt.Println(err) os.Exit(1) diff --git a/internal/dispatcher/agent/vpc_commands.go b/internal/dispatcher/agent/vpc_commands.go index b2687fc..a1313e1 100644 --- a/internal/dispatcher/agent/vpc_commands.go +++ b/internal/dispatcher/agent/vpc_commands.go @@ -1,6 +1,8 @@ package dispatcher import ( + "fmt" + configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/internal/vpc" "git.g3e.fr/syonad/two/pkg/db/kv" @@ -11,8 +13,14 @@ type CreateVPCCommand struct { Name string } +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) + } + return kv.AddInDB(db, "vpc/"+c.Name+"/state", "creating") +} + func (c CreateVPCCommand) Execute(db *badger.DB, _ *configuration.Config) error { - kv.AddInDB(db, "vpc/"+c.Name+"/state", "creating") return vpc.CreateVPC(db, c.Name) } @@ -20,8 +28,14 @@ type DeleteVPCCommand struct { Name string } +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) + } + return kv.AddInDB(db, "vpc/"+c.Name+"/state", "deleting") +} + func (c DeleteVPCCommand) Execute(db *badger.DB, _ *configuration.Config) error { - kv.AddInDB(db, "vpc/"+c.Name+"/state", "deleting") if err := vpc.DeleteVPC(db, c.Name); err != nil { return err } From 9ee361ae285f048d5a8387164d3ba84cb6380612 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 23 Apr 2026 23:29:38 +0200 Subject: [PATCH 31/44] f-21: dispatch: add check and verif before and during action Signed-off-by: GnomeZworc --- internal/config/agent/struct.go | 6 +++ internal/dispatcher/agent/subnet_commands.go | 18 +++++++- internal/dispatcher/agent/vpc_commands.go | 44 +++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 1c9fc9f..8818541 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -20,6 +20,10 @@ type Config struct { Count int `mapstructure:"count"` BufferSize int `mapstructure:"buffer_size"` } `mapstructure:"worker"` + Dispatcher struct { + TimeoutSeconds int `mapstructure:"timeout_seconds"` + PollSeconds int `mapstructure:"poll_seconds"` + } `mapstructure:"dispatcher"` DefaultInterface string `mapstructure:"default_interface"` Interfaces map[string]string `mapstructure:"interfaces"` } @@ -36,6 +40,8 @@ func LoadConfig(path string) (*Config, error) { v.SetDefault("prometheus.port", 9090) v.SetDefault("worker.count", 4) v.SetDefault("worker.buffer_size", 100) + v.SetDefault("dispatcher.timeout_seconds", 300) + v.SetDefault("dispatcher.poll_seconds", 2) v.SetDefault("default_interface", "br-000000") v.ReadInConfig() diff --git a/internal/dispatcher/agent/subnet_commands.go b/internal/dispatcher/agent/subnet_commands.go index c86cc77..7bc214d 100644 --- a/internal/dispatcher/agent/subnet_commands.go +++ b/internal/dispatcher/agent/subnet_commands.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "time" configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/internal/subnet" @@ -44,7 +45,22 @@ func (c CreateSubnetCommand) Prepare(db *badger.DB, cfg *configuration.Config) e return nil } -func (c CreateSubnetCommand) Execute(db *badger.DB, _ *configuration.Config) error { +func (c CreateSubnetCommand) Execute(db *badger.DB, cfg *configuration.Config) error { + timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second) + for { + state, err := kv.GetFromDB(db, "vpc/"+c.VPC+"/state") + if err != nil { + return fmt.Errorf("vpc %q not found while waiting", c.VPC) + } + if state == "created" { + break + } + select { + case <-timeout: + return fmt.Errorf("timed out waiting for vpc %q to be created", c.VPC) + case <-time.After(time.Duration(cfg.Dispatcher.PollSeconds) * time.Second): + } + } return subnet.CreateSubnet(db, c.Name) } diff --git a/internal/dispatcher/agent/vpc_commands.go b/internal/dispatcher/agent/vpc_commands.go index a1313e1..c03dd77 100644 --- a/internal/dispatcher/agent/vpc_commands.go +++ b/internal/dispatcher/agent/vpc_commands.go @@ -2,6 +2,8 @@ package dispatcher import ( "fmt" + "strings" + "time" configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/internal/vpc" @@ -32,10 +34,50 @@ 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) } + entries, err := kv.ListByPrefix(db, "subnet/") + if err != nil { + return fmt.Errorf("failed to list subnets: %w", err) + } + for key, value := range entries { + if !strings.HasSuffix(key, "/vpc") || value != c.Name { + continue + } + subnetName := strings.Split(key, "/")[1] + state, err := kv.GetFromDB(db, "subnet/"+subnetName+"/state") + if err != nil || (state != "deleting" && state != "deleted") { + return fmt.Errorf("subnet %q must be deleted before deleting vpc %q", subnetName, c.Name) + } + } return kv.AddInDB(db, "vpc/"+c.Name+"/state", "deleting") } -func (c DeleteVPCCommand) Execute(db *badger.DB, _ *configuration.Config) error { +func (c DeleteVPCCommand) Execute(db *badger.DB, cfg *configuration.Config) error { + timeout := time.After(time.Duration(cfg.Dispatcher.TimeoutSeconds) * time.Second) + for { + entries, err := kv.ListByPrefix(db, "subnet/") + if err != nil { + return fmt.Errorf("failed to list subnets: %w", err) + } + pending := false + for key, value := range entries { + if strings.HasSuffix(key, "/vpc") && value == c.Name { + subnetName := strings.Split(key, "/")[1] + state, _ := kv.GetFromDB(db, "subnet/"+subnetName+"/state") + if state == "deleting" { + pending = true + break + } + } + } + if !pending { + break + } + select { + case <-timeout: + return fmt.Errorf("timed out waiting for subnets of vpc %q to be deleted", c.Name) + case <-time.After(time.Duration(cfg.Dispatcher.PollSeconds) * time.Second): + } + } if err := vpc.DeleteVPC(db, c.Name); err != nil { return err } From 648a64782c4e941edd488ccb2b4be4b710ff1545 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Thu, 23 Apr 2026 23:30:05 +0200 Subject: [PATCH 32/44] f-21: conf: add full conf exemple Signed-off-by: GnomeZworc --- conf/agent/config.exemple.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/conf/agent/config.exemple.yml b/conf/agent/config.exemple.yml index bc1d437..0a966dd 100644 --- a/conf/agent/config.exemple.yml +++ b/conf/agent/config.exemple.yml @@ -1,8 +1,35 @@ +# Path to the Badger key-value database directory database: path: "/var/lib/two/data/" +# REST API server +api: + address: "0.0.0.0" + port: 8080 + +# Prometheus metrics server +prometheus: + address: "0.0.0.0" + port: 9090 + +# Worker pool that executes dispatched commands +worker: + # Number of concurrent worker goroutines + count: 4 + # Maximum number of commands queued before Dispatch blocks + buffer_size: 100 + +# Timing for commands that wait on resource state transitions +dispatcher: + # How long (in seconds) to wait before giving up + timeout_seconds: 300 + # Interval (in seconds) between each state check + poll_seconds: 2 + +# Bridge interface used when the requested iface_type is not found in interfaces default_interface: br-000000 +# Map of logical interface types to physical bridge names on this host interfaces: vms: br-000000 internet: br-000000 From fe6792be0d4829c25c2755ae28763f946da15bf4 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Fri, 24 Apr 2026 00:10:25 +0200 Subject: [PATCH 33/44] f-21: fix: use of exit become return Signed-off-by: GnomeZworc --- internal/dispatcher/agent/subnet_commands.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/dispatcher/agent/subnet_commands.go b/internal/dispatcher/agent/subnet_commands.go index 7bc214d..18f2cd3 100644 --- a/internal/dispatcher/agent/subnet_commands.go +++ b/internal/dispatcher/agent/subnet_commands.go @@ -2,7 +2,6 @@ package dispatcher import ( "fmt" - "os" "strconv" "time" @@ -77,13 +76,13 @@ func (c DeleteSubnetCommand) Prepare(db *badger.DB, _ *configuration.Config) err func (c DeleteSubnetCommand) Execute(db *badger.DB, _ *configuration.Config) error { if err := subnet.DeleteSubnet(db, c.Name); err != nil { - fmt.Println(err) - os.Exit(1) + return err } - if state, err := kv.GetFromDB(db, "subnet/"+c.Name+"/state"); err != nil { - fmt.Println(err) - os.Exit(1) - } else if state == "deleted" { + state, err := kv.GetFromDB(db, "subnet/"+c.Name+"/state") + if err != nil { + return err + } + if state == "deleted" { kv.DeleteInDB(db, "subnet/"+c.Name) } return nil From 921a5ca96e8f48a24c76e8174b2bbc45f306609c Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Fri, 24 Apr 2026 00:31:34 +0200 Subject: [PATCH 34/44] f-21: log: add full log info Signed-off-by: GnomeZworc --- cmd/agent/main.go | 20 ++++++++--- conf/agent/config.exemple.yml | 7 ++++ internal/api/agent/server.go | 46 ++++++++++++++++++++----- internal/config/agent/struct.go | 10 ++++-- internal/dispatcher/agent/dispatcher.go | 30 +++++++++++----- pkg/logger/logger.go | 25 ++++++++++++++ 6 files changed, 116 insertions(+), 22 deletions(-) create mode 100644 pkg/logger/logger.go diff --git a/cmd/agent/main.go b/cmd/agent/main.go index bb56aba..7b87bde 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -3,13 +3,14 @@ package main import ( "flag" "fmt" - "log" + "log/slog" agentapi "git.g3e.fr/syonad/two/internal/api/agent" configuration "git.g3e.fr/syonad/two/internal/config/agent" dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" agentmetrics "git.g3e.fr/syonad/two/internal/prometheus/agent" "git.g3e.fr/syonad/two/pkg/db/kv" + "git.g3e.fr/syonad/two/pkg/logger" promserver "git.g3e.fr/syonad/two/pkg/prometheus" "git.g3e.fr/syonad/two/pkg/worker" "github.com/prometheus/client_golang/prometheus" @@ -21,9 +22,12 @@ func main() { cfg, err := configuration.LoadConfig(*confFile) if err != nil { - log.Fatalf("failed to load config: %v", err) + slog.Error("failed to load config", "error", err) + return } + log := logger.New(cfg.Logger.Level, cfg.Logger.Debug) + db := kv.InitDB(kv.Config{Path: cfg.Database.Path}, false) defer db.Close() @@ -36,8 +40,16 @@ func main() { apiAddr := fmt.Sprintf("%s:%d", cfg.Api.Address, cfg.Api.Port) promAddr := fmt.Sprintf("%s:%d", cfg.Prometheus.Address, cfg.Prometheus.Port) - d := dispatcher.New(q, db, cfg) - go agentapi.New(d, db).Start(apiAddr) + log.Info("starting agent", + "api", apiAddr, + "prometheus", promAddr, + "workers", cfg.Worker.Count, + "log_level", cfg.Logger.Level, + "debug", cfg.Logger.Debug, + ) + + d := dispatcher.New(q, db, cfg, log.With(slog.String("component", "dispatcher"))) + go agentapi.New(d, db, log.With(slog.String("component", "api"))).Start(apiAddr) go promserver.Start(promAddr, registry) select {} diff --git a/conf/agent/config.exemple.yml b/conf/agent/config.exemple.yml index 0a966dd..fb8e604 100644 --- a/conf/agent/config.exemple.yml +++ b/conf/agent/config.exemple.yml @@ -34,3 +34,10 @@ interfaces: vms: br-000000 internet: br-000000 admin: br-000000 + +# Logging configuration +logger: + # Log level: debug, info, warn, error (default: info) + level: info + # Force debug level regardless of level setting (default: false) + debug: false diff --git a/internal/api/agent/server.go b/internal/api/agent/server.go index 276b416..8e3c4e3 100644 --- a/internal/api/agent/server.go +++ b/internal/api/agent/server.go @@ -1,8 +1,11 @@ package agentapi import ( - "log" + "crypto/rand" + "encoding/hex" + "log/slog" "net/http" + "time" dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" "github.com/dgraph-io/badger/v4" @@ -11,10 +14,11 @@ import ( type Server struct { dispatcher *dispatcher.Dispatcher db *badger.DB + logger *slog.Logger } -func New(d *dispatcher.Dispatcher, db *badger.DB) *Server { - return &Server{dispatcher: d, db: db} +func New(d *dispatcher.Dispatcher, db *badger.DB, logger *slog.Logger) *Server { + return &Server{dispatcher: d, db: db, logger: logger} } func (s *Server) Start(address string) { @@ -23,13 +27,39 @@ func (s *Server) Start(address string) { mux.HandleFunc("/vpcs/", s.VpcByNameHandler) mux.HandleFunc("/subnets", s.SubnetsHandler) mux.HandleFunc("/subnets/", s.SubnetByNameHandler) - log.Printf("API server listening on %s", address) - log.Fatal(http.ListenAndServe(address, logMiddleware(mux))) + s.logger.Info("API server listening", "address", address) + if err := http.ListenAndServe(address, s.logMiddleware(mux)); err != nil { + s.logger.Error("API server stopped", "error", err) + } } -func logMiddleware(next http.Handler) http.Handler { +type statusWriter struct { + http.ResponseWriter + status int +} + +func (sw *statusWriter) WriteHeader(code int) { + sw.status = code + sw.ResponseWriter.WriteHeader(code) +} + +func (s *Server) logMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path) - next.ServeHTTP(w, r) + var b [4]byte + rand.Read(b[:]) + reqID := hex.EncodeToString(b[:]) + + sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} + start := time.Now() + next.ServeHTTP(sw, r) + + s.logger.Info("request", + "request_id", reqID, + "method", r.Method, + "path", r.URL.Path, + "status", sw.status, + "duration_ms", time.Since(start).Milliseconds(), + "remote", r.RemoteAddr, + ) }) } diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 8818541..d8e4ee5 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -21,9 +21,13 @@ type Config struct { BufferSize int `mapstructure:"buffer_size"` } `mapstructure:"worker"` Dispatcher struct { - TimeoutSeconds int `mapstructure:"timeout_seconds"` - PollSeconds int `mapstructure:"poll_seconds"` + TimeoutSeconds int `mapstructure:"timeout_seconds"` + PollSeconds int `mapstructure:"poll_seconds"` } `mapstructure:"dispatcher"` + Logger struct { + Level string `mapstructure:"level"` + Debug bool `mapstructure:"debug"` + } `mapstructure:"logger"` DefaultInterface string `mapstructure:"default_interface"` Interfaces map[string]string `mapstructure:"interfaces"` } @@ -43,6 +47,8 @@ func LoadConfig(path string) (*Config, error) { v.SetDefault("dispatcher.timeout_seconds", 300) v.SetDefault("dispatcher.poll_seconds", 2) v.SetDefault("default_interface", "br-000000") + v.SetDefault("logger.level", "info") + v.SetDefault("logger.debug", false) v.ReadInConfig() diff --git a/internal/dispatcher/agent/dispatcher.go b/internal/dispatcher/agent/dispatcher.go index 6b1ca67..2897c8a 100644 --- a/internal/dispatcher/agent/dispatcher.go +++ b/internal/dispatcher/agent/dispatcher.go @@ -1,7 +1,9 @@ package dispatcher import ( - "log" + "fmt" + "log/slog" + "time" configuration "git.g3e.fr/syonad/two/internal/config/agent" "git.g3e.fr/syonad/two/pkg/worker" @@ -14,23 +16,35 @@ type Command interface { } type Dispatcher struct { - queue *worker.Queue - db *badger.DB - cfg *configuration.Config + queue *worker.Queue + db *badger.DB + cfg *configuration.Config + logger *slog.Logger } -func New(queue *worker.Queue, db *badger.DB, cfg *configuration.Config) *Dispatcher { - return &Dispatcher{queue: queue, db: db, cfg: cfg} +func New(queue *worker.Queue, db *badger.DB, cfg *configuration.Config, logger *slog.Logger) *Dispatcher { + return &Dispatcher{queue: queue, db: db, cfg: cfg, logger: logger} } func (d *Dispatcher) Prepare(cmd Command) error { + d.logger.Debug("prepare", "command", fmt.Sprintf("%T", cmd)) return cmd.Prepare(d.db, d.cfg) } func (d *Dispatcher) Dispatch(cmd Command) { + cmdType := fmt.Sprintf("%T", cmd) + d.logger.Debug("dispatch", "command", cmdType) d.queue.Submit(func() { - if err := cmd.Execute(d.db, d.cfg); err != nil { - log.Printf("command error (%T): %v", cmd, err) + start := time.Now() + err := cmd.Execute(d.db, d.cfg) + attrs := []any{ + "command", cmdType, + "duration_ms", time.Since(start).Milliseconds(), + } + if err != nil { + d.logger.Error("command failed", append(attrs, "error", err)...) + } else { + d.logger.Info("command done", attrs...) } }) } diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..0d6fa47 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,25 @@ +package logger + +import ( + "log/slog" + "os" +) + +var Level = new(slog.LevelVar) + +func New(level string, debug bool) *slog.Logger { + switch level { + case "debug": + Level.Set(slog.LevelDebug) + case "warn": + Level.Set(slog.LevelWarn) + case "error": + Level.Set(slog.LevelError) + default: + Level.Set(slog.LevelInfo) + } + if debug { + Level.Set(slog.LevelDebug) + } + return slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: Level})) +} From 6b104c4784b49219f68fc9af8ee613cb180d1eaf Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 14:52:14 +0200 Subject: [PATCH 35/44] f-21: debug: use err correct returne for kv db Signed-off-by: GnomeZworc --- internal/api/agent/subnet.go | 7 ++++++- internal/api/agent/subnets.go | 7 ++++++- internal/api/agent/vpc.go | 7 ++++++- internal/api/agent/vpcs.go | 7 ++++++- pkg/db/kv/deleteInDB.go | 4 +--- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/internal/api/agent/subnet.go b/internal/api/agent/subnet.go index 556b7c3..31eddc8 100644 --- a/internal/api/agent/subnet.go +++ b/internal/api/agent/subnet.go @@ -69,7 +69,12 @@ func (s *Server) deleteSubnet(w http.ResponseWriter, _ *http.Request, name strin return } s.dispatcher.Dispatch(cmd) - state, _ := kv.GetFromDB(s.db, "subnet/"+name+"/state") + state, err := kv.GetFromDB(s.db, "subnet/"+name+"/state") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read subnet state"}) + return + } w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(Subnet{Name: name, State: state}) } diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index 9e99a45..b0fb2fb 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -88,7 +88,12 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { return } s.dispatcher.Dispatch(cmd) - entries, _ := kv.ListByPrefix(s.db, "subnet/"+req.Name+"/") + entries, err := kv.ListByPrefix(s.db, "subnet/"+req.Name+"/") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read subnet state"}) + return + } sub := Subnet{Name: req.Name} for key, value := range entries { parts := strings.Split(key, "/") diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index be0724e..df2d47c 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -47,7 +47,12 @@ func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) return } s.dispatcher.Dispatch(cmd) - state, _ := kv.GetFromDB(s.db, "vpc/"+name+"/state") + state, err := kv.GetFromDB(s.db, "vpc/"+name+"/state") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read vpc state"}) + return + } w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(VPC{Name: name, State: state}) } diff --git a/internal/api/agent/vpcs.go b/internal/api/agent/vpcs.go index 3087456..b062d1c 100644 --- a/internal/api/agent/vpcs.go +++ b/internal/api/agent/vpcs.go @@ -69,7 +69,12 @@ func (s *Server) postVpc(w http.ResponseWriter, r *http.Request) { return } s.dispatcher.Dispatch(cmd) - state, _ := kv.GetFromDB(s.db, "vpc/"+req.Name+"/state") + state, err := kv.GetFromDB(s.db, "vpc/"+req.Name+"/state") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(ErrorResponse{Error: "failed to read vpc state"}) + return + } w.WriteHeader(http.StatusAccepted) json.NewEncoder(w).Encode(VPC{Name: req.Name, State: state}) } diff --git a/pkg/db/kv/deleteInDB.go b/pkg/db/kv/deleteInDB.go index 1943c81..e1335de 100644 --- a/pkg/db/kv/deleteInDB.go +++ b/pkg/db/kv/deleteInDB.go @@ -1,8 +1,6 @@ package kv import ( - "log" - "github.com/dgraph-io/badger/v4" ) @@ -35,7 +33,7 @@ func DeleteInDB(db *badger.DB, key string) error { return nil }) if err != nil { - log.Fatal(err) + return err } return deleteKey(db, key) From 71aaaacf7ba61fa1c84ce887b7d411be82723d9b Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 14:56:17 +0200 Subject: [PATCH 36/44] f-21: debug: make mtu parametrable in full netif Signed-off-by: GnomeZworc --- internal/netif/vxlan.go | 3 ++- internal/subnet/create.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/netif/vxlan.go b/internal/netif/vxlan.go index 6523f37..70740bc 100644 --- a/internal/netif/vxlan.go +++ b/internal/netif/vxlan.go @@ -4,7 +4,7 @@ import ( "github.com/vishvananda/netlink" ) -func CreateVxlan(name string, vxlanID int, localIface string) error { +func CreateVxlan(name string, vxlanID int, localIface string, mtu int) error { link, err := netlink.LinkByName(localIface) if err != nil { return err @@ -12,6 +12,7 @@ func CreateVxlan(name string, vxlanID int, localIface string) error { vxlan := &netlink.Vxlan{ LinkAttrs: netlink.LinkAttrs{ Name: name, + MTU: mtu, }, VxlanId: vxlanID, Port: 4789, diff --git a/internal/subnet/create.go b/internal/subnet/create.go index 1ed514e..d376d7f 100644 --- a/internal/subnet/create.go +++ b/internal/subnet/create.go @@ -86,7 +86,7 @@ func CreateSubnet(db *badger.DB, subnetName string) error { } // vxlan - if err := netif.CreateVxlan(vxlanIface, vxlanID, localIface); err != nil { + if err := netif.CreateVxlan(vxlanIface, vxlanID, localIface, 1500); err != nil { return fmt.Errorf("create vxlan: %w", err) } From 1791196c87f7dae5a9d84e0c648c83c1c366cb90 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 15:04:24 +0200 Subject: [PATCH 37/44] f-21: lib: add ebatable wrapper Signed-off-by: GnomeZworc --- internal/ebtables/ebtables.go | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 internal/ebtables/ebtables.go diff --git a/internal/ebtables/ebtables.go b/internal/ebtables/ebtables.go new file mode 100644 index 0000000..31e276b --- /dev/null +++ b/internal/ebtables/ebtables.go @@ -0,0 +1,58 @@ +package ebtables + +import ( + "fmt" + "os/exec" +) + +func addRule(args ...string) error { + return exec.Command("ebtables", append([]string{"-A"}, args...)...).Run() +} + +func deleteRule(args ...string) error { + return exec.Command("ebtables", append([]string{"-D"}, args...)...).Run() +} + +func DropARPToGateway(bridge, gatewayIP string) error { + if err := addRule("FORWARD", + "--out-interface", bridge, + "-p", "arp", + "--arp-op", "Request", + "--arp-ip-dst", gatewayIP, + "-j", "DROP"); err != nil { + return fmt.Errorf("ebtables arp rule: %w", err) + } + return nil +} + +func DropDHCP(bridge string) error { + if err := addRule("FORWARD", + "--out-interface", bridge, + "-p", "IPv4", + "--ip-protocol", "udp", + "--ip-source-port", "67:68", + "--ip-destination-port", "67:68", + "-j", "DROP"); err != nil { + return fmt.Errorf("ebtables dhcp rule: %w", err) + } + return nil +} + +func DeleteARPToGateway(bridge, gatewayIP string) error { + return deleteRule("FORWARD", + "--out-interface", bridge, + "-p", "arp", + "--arp-op", "Request", + "--arp-ip-dst", gatewayIP, + "-j", "DROP") +} + +func DeleteDHCP(bridge string) error { + return deleteRule("FORWARD", + "--out-interface", bridge, + "-p", "IPv4", + "--ip-protocol", "udp", + "--ip-source-port", "67:68", + "--ip-destination-port", "67:68", + "-j", "DROP") +} From 396f2842e5f805646cdf5a777c609737cb85e4f7 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 15:04:45 +0200 Subject: [PATCH 38/44] f-21: code: use ebtable parse Signed-off-by: GnomeZworc --- internal/subnet/create.go | 24 +++++------------------- internal/subnet/delete.go | 26 ++++++++++++-------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/internal/subnet/create.go b/internal/subnet/create.go index d376d7f..496c257 100644 --- a/internal/subnet/create.go +++ b/internal/subnet/create.go @@ -3,11 +3,11 @@ package subnet import ( "fmt" "net" - "os/exec" "strconv" "strings" "git.g3e.fr/syonad/two/internal/dhcp" + "git.g3e.fr/syonad/two/internal/ebtables" "git.g3e.fr/syonad/two/internal/netif" "git.g3e.fr/syonad/two/internal/netns" "git.g3e.fr/syonad/two/pkg/db/kv" @@ -136,25 +136,11 @@ func CreateSubnet(db *badger.DB, subnetName string) error { return fmt.Errorf("add route in netns: %w", err) } - // ebtables : drop ARP Request vers la gateway sur ce bridge - if err := exec.Command("ebtables", "-A", "FORWARD", - "--out-interface", bridge, - "-p", "arp", - "--arp-op", "Request", - "--arp-ip-dst", gatewayIP.String(), - "-j", "DROP").Run(); err != nil { - return fmt.Errorf("ebtables arp rule: %w", err) + if err := ebtables.DropARPToGateway(bridge, gatewayIP.String()); err != nil { + return err } - - // ebtables : drop trafic DHCP sur ce bridge - if err := exec.Command("ebtables", "-A", "FORWARD", - "--out-interface", bridge, - "-p", "IPv4", - "--ip-protocol", "udp", - "--ip-source-port", "67:68", - "--ip-destination-port", "67:68", - "-j", "DROP").Run(); err != nil { - return fmt.Errorf("ebtables dhcp rule: %w", err) + if err := ebtables.DropDHCP(bridge); err != nil { + return err } // génération de la config dnsmasq et démarrage du service diff --git a/internal/subnet/delete.go b/internal/subnet/delete.go index 650aa6f..8ce91ff 100644 --- a/internal/subnet/delete.go +++ b/internal/subnet/delete.go @@ -3,9 +3,9 @@ package subnet import ( "fmt" "os" - "os/exec" "strings" + "git.g3e.fr/syonad/two/internal/ebtables" "git.g3e.fr/syonad/two/internal/netif" "git.g3e.fr/syonad/two/internal/netns" "git.g3e.fr/syonad/two/pkg/db/kv" @@ -33,6 +33,11 @@ func DeleteSubnet(db *badger.DB, subnetName string) error { return fmt.Errorf("get vxlan_id: %w", err) } + gatewayIP, err := kv.GetFromDB(db, "subnet/"+subnetName+"/gateway_ip") + if err != nil { + return fmt.Errorf("get gateway_ip: %w", err) + } + subnetID := strings.SplitN(subnetName, "-", 2)[1] bridge := "br-" + subnetID vxlanIface := "vxlan-" + vxlanIDStr @@ -55,19 +60,12 @@ func DeleteSubnet(db *badger.DB, subnetName string) error { } // suppression des règles ebtables - exec.Command("ebtables", "-D", "FORWARD", - "--out-interface", bridge, - "-p", "arp", - "--arp-op", "Request", - "-j", "DROP").Run() - - exec.Command("ebtables", "-D", "FORWARD", - "--out-interface", bridge, - "-p", "IPv4", - "--ip-protocol", "udp", - "--ip-source-port", "67:68", - "--ip-destination-port", "67:68", - "-j", "DROP").Run() + if err := ebtables.DeleteARPToGateway(bridge, gatewayIP); err != nil { + return fmt.Errorf("delete ebtables arp rule: %w", err) + } + if err := ebtables.DeleteDHCP(bridge); err != nil { + return fmt.Errorf("delete ebtables dhcp rule: %w", err) + } // suppression du bridge dans le netns VPC if err := netns.Call(vpcName, func() error { From b420217f2b91cd87de8e788ebcc2a75ae3193cb8 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 15:12:34 +0200 Subject: [PATCH 39/44] f-21: test: increase coverage test for db Signed-off-by: GnomeZworc --- pkg/db/kv/kv_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/pkg/db/kv/kv_test.go b/pkg/db/kv/kv_test.go index de4fd68..b45422b 100644 --- a/pkg/db/kv/kv_test.go +++ b/pkg/db/kv/kv_test.go @@ -151,3 +151,69 @@ func TestDeleteInDB_MissingKey(t *testing.T) { t.Logf("DeleteInDB clé inexistante retourne : %v (non bloquant)", err) } } + +// --- ListByPrefix --- + +func TestListByPrefix_MatchingKeys(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "subnet/sn1/state", "created") + AddInDB(db, "subnet/sn1/vpc", "vpc-1") + AddInDB(db, "subnet/sn1/cidr", "10.0.0.0/24") + + entries, err := ListByPrefix(db, "subnet/sn1/") + if err != nil { + t.Fatalf("ListByPrefix a échoué : %v", err) + } + if len(entries) != 3 { + t.Fatalf("attendu 3 entrées, obtenu %d", len(entries)) + } + if entries["subnet/sn1/state"] != "created" { + t.Errorf("valeur inattendue pour state : %q", entries["subnet/sn1/state"]) + } + if entries["subnet/sn1/vpc"] != "vpc-1" { + t.Errorf("valeur inattendue pour vpc : %q", entries["subnet/sn1/vpc"]) + } +} + +func TestListByPrefix_NoMatch(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/v1/state", "created") + + entries, err := ListByPrefix(db, "subnet/") + if err != nil { + t.Fatalf("ListByPrefix a échoué : %v", err) + } + if len(entries) != 0 { + t.Errorf("attendu 0 entrées, obtenu %d", len(entries)) + } +} + +func TestListByPrefix_IsolatesPrefix(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "subnet/sn1/state", "created") + AddInDB(db, "subnet/sn2/state", "creating") + AddInDB(db, "vpc/v1/state", "created") + + entries, err := ListByPrefix(db, "subnet/sn1/") + if err != nil { + t.Fatalf("ListByPrefix a échoué : %v", err) + } + if len(entries) != 1 { + t.Errorf("attendu 1 entrée, obtenu %d : %v", len(entries), entries) + } + if _, ok := entries["subnet/sn1/state"]; !ok { + t.Error("subnet/sn1/state devrait être présent") + } +} + +func TestListByPrefix_EmptyDB(t *testing.T) { + db := newTestDB(t) + + entries, err := ListByPrefix(db, "subnet/") + if err != nil { + t.Fatalf("ListByPrefix a échoué : %v", err) + } + if len(entries) != 0 { + t.Errorf("attendu 0 entrées sur DB vide, obtenu %d", len(entries)) + } +} From 9950e0e24af0e99f94b5addbfe52ee529a12d59c Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 15:32:46 +0200 Subject: [PATCH 40/44] f-21: test: add test for agent api Signed-off-by: GnomeZworc --- internal/api/agent/helpers_test.go | 27 ++++ internal/api/agent/subnet_test.go | 234 +++++++++++++++++++++++++++++ internal/api/agent/vpc_test.go | 188 +++++++++++++++++++++++ 3 files changed, 449 insertions(+) create mode 100644 internal/api/agent/helpers_test.go create mode 100644 internal/api/agent/subnet_test.go create mode 100644 internal/api/agent/vpc_test.go diff --git a/internal/api/agent/helpers_test.go b/internal/api/agent/helpers_test.go new file mode 100644 index 0000000..206874c --- /dev/null +++ b/internal/api/agent/helpers_test.go @@ -0,0 +1,27 @@ +package agentapi + +import ( + "io" + "log/slog" + "testing" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + dispatcher "git.g3e.fr/syonad/two/internal/dispatcher/agent" + "git.g3e.fr/syonad/two/pkg/db/kv" + "git.g3e.fr/syonad/two/pkg/worker" + "github.com/dgraph-io/badger/v4" +) + +// newTestServer builds a Server backed by an in-memory Badger DB. +// The worker queue is buffered but has no running goroutines: Dispatch enqueues +// without blocking and Execute never runs, so DB state reflects only Prepare writes. +func newTestServer(t *testing.T) (*Server, *badger.DB) { + t.Helper() + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + q := worker.New(100) + cfg := &configuration.Config{DefaultInterface: "br-test"} + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + d := dispatcher.New(q, db, cfg, logger) + return New(d, db, logger), db +} diff --git a/internal/api/agent/subnet_test.go b/internal/api/agent/subnet_test.go new file mode 100644 index 0000000..0fdd18b --- /dev/null +++ b/internal/api/agent/subnet_test.go @@ -0,0 +1,234 @@ +package agentapi + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "git.g3e.fr/syonad/two/pkg/db/kv" +) + +// --- SubnetsHandler --- + +func TestListSubnets_Empty(t *testing.T) { + s, _ := newTestServer(t) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodGet, "/subnets", nil)) + if w.Code != http.StatusOK { + t.Fatalf("attendu 200, obtenu %d", w.Code) + } + var result []Subnet + json.NewDecoder(w.Body).Decode(&result) + if len(result) != 0 { + t.Errorf("attendu liste vide, obtenu %v", result) + } +} + +func TestListSubnets_WithData(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "subnet/sn-1/state", "created") + kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1") + kv.AddInDB(db, "subnet/sn-2/state", "creating") + kv.AddInDB(db, "subnet/sn-2/vpc", "vpc-1") + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodGet, "/subnets", nil)) + if w.Code != http.StatusOK { + t.Fatalf("attendu 200, obtenu %d", w.Code) + } + var result []Subnet + json.NewDecoder(w.Body).Decode(&result) + if len(result) != 2 { + t.Errorf("attendu 2 subnets, obtenu %d", len(result)) + } +} + +func TestListSubnets_InvalidMethod(t *testing.T) { + s, _ := newTestServer(t) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPut, "/subnets", nil)) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("attendu 405, obtenu %d", w.Code) + } +} + +func TestPostSubnet_Created(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + req := SubnetCreateRequest{ + Name: "sn-new", + VPC: "vpc-1", + IfaceType: "vms", + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) + if w.Code != http.StatusAccepted { + t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String()) + } + var result Subnet + json.NewDecoder(w.Body).Decode(&result) + if result.Name != "sn-new" { + t.Errorf("name attendu sn-new, obtenu %q", result.Name) + } + if result.State != "creating" { + t.Errorf("state attendu creating, obtenu %q", result.State) + } +} + +func TestPostSubnet_MissingFields(t *testing.T) { + s, _ := newTestServer(t) + body, _ := json.Marshal(SubnetCreateRequest{Name: "sn-1"}) // vpc, iface_type, gateway_ip, cidr manquants + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) + if w.Code != http.StatusBadRequest { + t.Errorf("attendu 400, obtenu %d", w.Code) + } +} + +func TestPostSubnet_VPCNotFound(t *testing.T) { + s, _ := newTestServer(t) + req := SubnetCreateRequest{ + Name: "sn-1", + VPC: "vpc-inexistant", + IfaceType: "vms", + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) + if w.Code != http.StatusConflict { + t.Errorf("attendu 409, obtenu %d", w.Code) + } +} + +func TestPostSubnet_Duplicate(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + kv.AddInDB(db, "subnet/sn-exist/state", "created") + req := SubnetCreateRequest{ + Name: "sn-exist", + VPC: "vpc-1", + IfaceType: "vms", + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) + if w.Code != http.StatusConflict { + t.Errorf("attendu 409, obtenu %d", w.Code) + } +} + +func TestPostSubnet_VPCDeleting(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-dying/state", "deleting") + req := SubnetCreateRequest{ + Name: "sn-1", + VPC: "vpc-dying", + IfaceType: "vms", + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) + if w.Code != http.StatusConflict { + t.Errorf("attendu 409, obtenu %d", w.Code) + } +} + +func TestPostSubnet_InvalidBody(t *testing.T) { + s, _ := newTestServer(t) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader([]byte("not json")))) + if w.Code != http.StatusBadRequest { + t.Errorf("attendu 400, obtenu %d", w.Code) + } +} + +// --- SubnetByNameHandler --- + +func TestGetSubnet_Found(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "subnet/sn-1/state", "created") + kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-1") + kv.AddInDB(db, "subnet/sn-1/cidr", "10.0.0.0/24") + kv.AddInDB(db, "subnet/sn-1/gateway_ip", "10.0.0.1") + req := httptest.NewRequest(http.MethodGet, "/subnets/sn-1", nil) + w := httptest.NewRecorder() + s.SubnetByNameHandler(w, req) + if w.Code != http.StatusOK { + t.Fatalf("attendu 200, obtenu %d", w.Code) + } + var result Subnet + json.NewDecoder(w.Body).Decode(&result) + if result.Name != "sn-1" || result.State != "created" { + t.Errorf("résultat inattendu : %+v", result) + } + if result.VPC != "vpc-1" { + t.Errorf("vpc attendu vpc-1, obtenu %q", result.VPC) + } +} + +func TestGetSubnet_NotFound(t *testing.T) { + s, _ := newTestServer(t) + req := httptest.NewRequest(http.MethodGet, "/subnets/inexistant", nil) + w := httptest.NewRecorder() + s.SubnetByNameHandler(w, req) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404, obtenu %d", w.Code) + } +} + +func TestGetSubnet_EmptyName(t *testing.T) { + s, _ := newTestServer(t) + req := httptest.NewRequest(http.MethodGet, "/subnets/", nil) + w := httptest.NewRecorder() + s.SubnetByNameHandler(w, req) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404, obtenu %d", w.Code) + } +} + +func TestDeleteSubnet_Success(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "subnet/sn-del/state", "created") + req := httptest.NewRequest(http.MethodDelete, "/subnets/sn-del", nil) + w := httptest.NewRecorder() + s.SubnetByNameHandler(w, req) + if w.Code != http.StatusAccepted { + t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String()) + } + var result Subnet + json.NewDecoder(w.Body).Decode(&result) + if result.State != "deleting" { + t.Errorf("state attendu deleting, obtenu %q", result.State) + } +} + +func TestDeleteSubnet_NotFound(t *testing.T) { + s, _ := newTestServer(t) + req := httptest.NewRequest(http.MethodDelete, "/subnets/inexistant", nil) + w := httptest.NewRecorder() + s.SubnetByNameHandler(w, req) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404, obtenu %d", w.Code) + } +} + +func TestSubnetByName_InvalidMethod(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "subnet/sn-1/state", "created") + req := httptest.NewRequest(http.MethodPut, "/subnets/sn-1", nil) + w := httptest.NewRecorder() + s.SubnetByNameHandler(w, req) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("attendu 405, obtenu %d", w.Code) + } +} diff --git a/internal/api/agent/vpc_test.go b/internal/api/agent/vpc_test.go new file mode 100644 index 0000000..0edcd5b --- /dev/null +++ b/internal/api/agent/vpc_test.go @@ -0,0 +1,188 @@ +package agentapi + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "git.g3e.fr/syonad/two/pkg/db/kv" +) + +// --- VpcsHandler --- + +func TestListVpcs_Empty(t *testing.T) { + s, _ := newTestServer(t) + w := httptest.NewRecorder() + s.VpcsHandler(w, httptest.NewRequest(http.MethodGet, "/vpcs", nil)) + if w.Code != http.StatusOK { + t.Fatalf("attendu 200, obtenu %d", w.Code) + } + var result []VPC + json.NewDecoder(w.Body).Decode(&result) + if len(result) != 0 { + t.Errorf("attendu liste vide, obtenu %v", result) + } +} + +func TestListVpcs_WithData(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/v1/state", "created") + kv.AddInDB(db, "vpc/v2/state", "creating") + w := httptest.NewRecorder() + s.VpcsHandler(w, httptest.NewRequest(http.MethodGet, "/vpcs", nil)) + if w.Code != http.StatusOK { + t.Fatalf("attendu 200, obtenu %d", w.Code) + } + var result []VPC + json.NewDecoder(w.Body).Decode(&result) + if len(result) != 2 { + t.Errorf("attendu 2 VPCs, obtenu %d", len(result)) + } +} + +func TestListVpcs_InvalidMethod(t *testing.T) { + s, _ := newTestServer(t) + w := httptest.NewRecorder() + s.VpcsHandler(w, httptest.NewRequest(http.MethodPut, "/vpcs", nil)) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("attendu 405, obtenu %d", w.Code) + } +} + +func TestPostVpc_Created(t *testing.T) { + s, _ := newTestServer(t) + body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-new"}) + w := httptest.NewRecorder() + s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body))) + if w.Code != http.StatusAccepted { + t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String()) + } + var result VPC + json.NewDecoder(w.Body).Decode(&result) + if result.Name != "vpc-new" { + t.Errorf("name attendu vpc-new, obtenu %q", result.Name) + } + if result.State != "creating" { + t.Errorf("state attendu creating, obtenu %q", result.State) + } +} + +func TestPostVpc_MissingName(t *testing.T) { + s, _ := newTestServer(t) + body, _ := json.Marshal(VPCCreateRequest{}) + w := httptest.NewRecorder() + s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body))) + if w.Code != http.StatusBadRequest { + t.Errorf("attendu 400, obtenu %d", w.Code) + } +} + +func TestPostVpc_Duplicate(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-exist/state", "created") + body, _ := json.Marshal(VPCCreateRequest{Name: "vpc-exist"}) + w := httptest.NewRecorder() + s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader(body))) + if w.Code != http.StatusConflict { + t.Errorf("attendu 409, obtenu %d", w.Code) + } +} + +func TestPostVpc_InvalidBody(t *testing.T) { + s, _ := newTestServer(t) + w := httptest.NewRecorder() + s.VpcsHandler(w, httptest.NewRequest(http.MethodPost, "/vpcs", bytes.NewReader([]byte("not json")))) + if w.Code != http.StatusBadRequest { + t.Errorf("attendu 400, obtenu %d", w.Code) + } +} + +// --- VpcByNameHandler --- + +func TestGetVpc_Found(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + req := httptest.NewRequest(http.MethodGet, "/vpcs/vpc-1", nil) + w := httptest.NewRecorder() + s.VpcByNameHandler(w, req) + if w.Code != http.StatusOK { + t.Fatalf("attendu 200, obtenu %d", w.Code) + } + var result VPC + json.NewDecoder(w.Body).Decode(&result) + if result.Name != "vpc-1" || result.State != "created" { + t.Errorf("résultat inattendu : %+v", result) + } +} + +func TestGetVpc_NotFound(t *testing.T) { + s, _ := newTestServer(t) + req := httptest.NewRequest(http.MethodGet, "/vpcs/inexistant", nil) + w := httptest.NewRecorder() + s.VpcByNameHandler(w, req) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404, obtenu %d", w.Code) + } +} + +func TestGetVpc_EmptyName(t *testing.T) { + s, _ := newTestServer(t) + req := httptest.NewRequest(http.MethodGet, "/vpcs/", nil) + w := httptest.NewRecorder() + s.VpcByNameHandler(w, req) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404, obtenu %d", w.Code) + } +} + +func TestDeleteVpc_Success(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-del/state", "created") + req := httptest.NewRequest(http.MethodDelete, "/vpcs/vpc-del", nil) + w := httptest.NewRecorder() + s.VpcByNameHandler(w, req) + if w.Code != http.StatusAccepted { + t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String()) + } + var result VPC + json.NewDecoder(w.Body).Decode(&result) + if result.State != "deleting" { + t.Errorf("state attendu deleting, obtenu %q", result.State) + } +} + +func TestDeleteVpc_NotFound(t *testing.T) { + s, _ := newTestServer(t) + req := httptest.NewRequest(http.MethodDelete, "/vpcs/inexistant", nil) + w := httptest.NewRecorder() + s.VpcByNameHandler(w, req) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404, obtenu %d", w.Code) + } +} + +func TestDeleteVpc_BlockedByActiveSubnet(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-busy/state", "created") + kv.AddInDB(db, "subnet/sn-1/state", "created") + kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-busy") + req := httptest.NewRequest(http.MethodDelete, "/vpcs/vpc-busy", nil) + w := httptest.NewRecorder() + s.VpcByNameHandler(w, req) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404 (Prepare échoue), obtenu %d: %s", w.Code, w.Body.String()) + } +} + +func TestVpcByName_InvalidMethod(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + req := httptest.NewRequest(http.MethodPut, "/vpcs/vpc-1", nil) + w := httptest.NewRecorder() + s.VpcByNameHandler(w, req) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("attendu 405, obtenu %d", w.Code) + } +} From 712692d414218666f02b3a1dacfa3e58b468babf Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 15:34:40 +0200 Subject: [PATCH 41/44] f-21: test: add test for worker pkg Signed-off-by: GnomeZworc --- pkg/worker/queue_test.go | 103 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 pkg/worker/queue_test.go diff --git a/pkg/worker/queue_test.go b/pkg/worker/queue_test.go new file mode 100644 index 0000000..5353b06 --- /dev/null +++ b/pkg/worker/queue_test.go @@ -0,0 +1,103 @@ +package worker + +import ( + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestNew_ReturnsQueue(t *testing.T) { + q := New(10) + if q == nil { + t.Fatal("New devrait retourner une queue non-nil") + } +} + +func TestQueue_SingleTaskExecuted(t *testing.T) { + q := New(1) + q.Start(1) + + var done atomic.Bool + var wg sync.WaitGroup + wg.Add(1) + q.Submit(func() { + done.Store(true) + wg.Done() + }) + + wg.Wait() + if !done.Load() { + t.Error("la tâche n'a pas été exécutée") + } +} + +func TestQueue_AllTasksExecuted(t *testing.T) { + const n = 50 + q := New(n) + q.Start(1) + + var count atomic.Int32 + var wg sync.WaitGroup + wg.Add(n) + for range n { + q.Submit(func() { + count.Add(1) + wg.Done() + }) + } + + wg.Wait() + if count.Load() != n { + t.Errorf("attendu %d exécutions, obtenu %d", n, count.Load()) + } +} + +func TestQueue_MultipleWorkers(t *testing.T) { + const n = 100 + q := New(n) + q.Start(4) + + var count atomic.Int32 + var wg sync.WaitGroup + wg.Add(n) + for range n { + q.Submit(func() { + count.Add(1) + wg.Done() + }) + } + + wg.Wait() + if count.Load() != n { + t.Errorf("attendu %d exécutions, obtenu %d", n, count.Load()) + } +} + +func TestQueue_SubmitBlocksWhenFull(t *testing.T) { + q := New(1) + // Remplit le buffer sans worker + q.Submit(func() {}) + + submitted := make(chan struct{}) + go func() { + q.Submit(func() {}) // doit bloquer jusqu'à ce qu'un worker consomme + close(submitted) + }() + + select { + case <-submitted: + t.Error("Submit aurait dû bloquer sur une queue pleine") + case <-time.After(50 * time.Millisecond): + // comportement attendu : goroutine bloquée + } + + // Démarre un worker pour débloquer + q.Start(1) + select { + case <-submitted: + // Submit a pu avancer + case <-time.After(time.Second): + t.Error("Submit aurait dû se débloquer après démarrage d'un worker") + } +} From ba2f8080be4b669b5e76c36ced2a11f096959a2b Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 15:39:00 +0200 Subject: [PATCH 42/44] f-21: test: add test for agent dispatcher Signed-off-by: GnomeZworc --- internal/dispatcher/agent/dispatcher_test.go | 63 ++++++++ internal/dispatcher/agent/helpers_test.go | 38 +++++ .../dispatcher/agent/subnet_commands_test.go | 135 ++++++++++++++++++ .../dispatcher/agent/vpc_commands_test.go | 89 ++++++++++++ 4 files changed, 325 insertions(+) create mode 100644 internal/dispatcher/agent/dispatcher_test.go create mode 100644 internal/dispatcher/agent/helpers_test.go create mode 100644 internal/dispatcher/agent/subnet_commands_test.go create mode 100644 internal/dispatcher/agent/vpc_commands_test.go diff --git a/internal/dispatcher/agent/dispatcher_test.go b/internal/dispatcher/agent/dispatcher_test.go new file mode 100644 index 0000000..b4881d9 --- /dev/null +++ b/internal/dispatcher/agent/dispatcher_test.go @@ -0,0 +1,63 @@ +package dispatcher + +import ( + "errors" + "sync" + "testing" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "github.com/dgraph-io/badger/v4" +) + +func TestDispatcher_Prepare_Success(t *testing.T) { + d, _ := newTestDispatcher(t) + cmd := mockCmd{ + prepareFn: func(*badger.DB, *configuration.Config) error { return nil }, + executeFn: func(*badger.DB, *configuration.Config) error { return nil }, + } + if err := d.Prepare(cmd); err != nil { + t.Errorf("Prepare devrait retourner nil, obtenu : %v", err) + } +} + +func TestDispatcher_Prepare_PropagatesError(t *testing.T) { + d, _ := newTestDispatcher(t) + want := errors.New("prepare failed") + cmd := mockCmd{ + prepareFn: func(*badger.DB, *configuration.Config) error { return want }, + executeFn: func(*badger.DB, *configuration.Config) error { return nil }, + } + if err := d.Prepare(cmd); !errors.Is(err, want) { + t.Errorf("attendu %v, obtenu %v", want, err) + } +} + +func TestDispatcher_Dispatch_ExecutesCommand(t *testing.T) { + d, _ := newTestDispatcher(t) + var wg sync.WaitGroup + wg.Add(1) + cmd := mockCmd{ + prepareFn: func(*badger.DB, *configuration.Config) error { return nil }, + executeFn: func(*badger.DB, *configuration.Config) error { + wg.Done() + return nil + }, + } + d.Dispatch(cmd) + wg.Wait() +} + +func TestDispatcher_Dispatch_ExecuteErrorLogged(t *testing.T) { + d, _ := newTestDispatcher(t) + var wg sync.WaitGroup + wg.Add(1) + cmd := mockCmd{ + prepareFn: func(*badger.DB, *configuration.Config) error { return nil }, + executeFn: func(*badger.DB, *configuration.Config) error { + defer wg.Done() + return errors.New("execute failed") + }, + } + d.Dispatch(cmd) + wg.Wait() // Execute s'est terminé — l'erreur est loggée, pas propagée +} diff --git a/internal/dispatcher/agent/helpers_test.go b/internal/dispatcher/agent/helpers_test.go new file mode 100644 index 0000000..2cee6fc --- /dev/null +++ b/internal/dispatcher/agent/helpers_test.go @@ -0,0 +1,38 @@ +package dispatcher + +import ( + "io" + "log/slog" + "testing" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/pkg/db/kv" + "git.g3e.fr/syonad/two/pkg/worker" + "github.com/dgraph-io/badger/v4" +) + +func newTestDispatcher(t *testing.T) (*Dispatcher, *badger.DB) { + t.Helper() + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + q := worker.New(100) + q.Start(2) + cfg := &configuration.Config{DefaultInterface: "br-default"} + cfg.Interfaces = map[string]string{"vms": "br-vms"} + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + return New(q, db, cfg, logger), db +} + +// mockCmd implémente Command sans aucune dépendance système. +type mockCmd struct { + prepareFn func(*badger.DB, *configuration.Config) error + executeFn func(*badger.DB, *configuration.Config) error +} + +func (m mockCmd) Prepare(db *badger.DB, cfg *configuration.Config) error { + return m.prepareFn(db, cfg) +} + +func (m mockCmd) Execute(db *badger.DB, cfg *configuration.Config) error { + return m.executeFn(db, cfg) +} diff --git a/internal/dispatcher/agent/subnet_commands_test.go b/internal/dispatcher/agent/subnet_commands_test.go new file mode 100644 index 0000000..3b118bc --- /dev/null +++ b/internal/dispatcher/agent/subnet_commands_test.go @@ -0,0 +1,135 @@ +package dispatcher + +import ( + "testing" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/pkg/db/kv" +) + +func testCfg() *configuration.Config { + cfg := &configuration.Config{DefaultInterface: "br-default"} + cfg.Interfaces = map[string]string{"vms": "br-vms"} + return cfg +} + +// --- CreateSubnetCommand.Prepare --- + +func TestCreateSubnetCommand_Prepare_Success(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + cmd := CreateSubnetCommand{ + Name: "sn-1", VPC: "vpc-1", VxlanID: 100, + IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24", + } + if err := cmd.Prepare(db, testCfg()); err != nil { + t.Fatalf("Prepare a échoué : %v", err) + } + state, _ := kv.GetFromDB(db, "subnet/sn-1/state") + if state != "creating" { + t.Errorf("state attendu creating, obtenu %q", state) + } + vpc, _ := kv.GetFromDB(db, "subnet/sn-1/vpc") + if vpc != "vpc-1" { + t.Errorf("vpc attendu vpc-1, obtenu %q", vpc) + } +} + +func TestCreateSubnetCommand_Prepare_UsesIfaceTypeMapping(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + cmd := CreateSubnetCommand{ + Name: "sn-1", VPC: "vpc-1", VxlanID: 100, + IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24", + } + cmd.Prepare(db, testCfg()) + iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface") + if iface != "br-vms" { + t.Errorf("local_iface attendu br-vms, obtenu %q", iface) + } +} + +func TestCreateSubnetCommand_Prepare_UsesDefaultIfaceWhenTypeUnknown(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + cmd := CreateSubnetCommand{ + Name: "sn-1", VPC: "vpc-1", VxlanID: 100, + IfaceType: "inconnu", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24", + } + cmd.Prepare(db, testCfg()) + iface, _ := kv.GetFromDB(db, "subnet/sn-1/local_iface") + if iface != "br-default" { + t.Errorf("local_iface attendu br-default, obtenu %q", iface) + } +} + +func TestCreateSubnetCommand_Prepare_Duplicate(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + kv.AddInDB(db, "subnet/sn-exist/state", "created") + cmd := CreateSubnetCommand{ + Name: "sn-exist", VPC: "vpc-1", VxlanID: 100, + IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24", + } + if err := cmd.Prepare(db, testCfg()); err == nil { + t.Error("Prepare devrait échouer sur un subnet déjà existant") + } +} + +func TestCreateSubnetCommand_Prepare_VPCNotFound(t *testing.T) { + _, db := newTestDispatcher(t) + cmd := CreateSubnetCommand{ + Name: "sn-1", VPC: "vpc-inexistant", VxlanID: 100, + IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24", + } + if err := cmd.Prepare(db, testCfg()); err == nil { + t.Error("Prepare devrait échouer si le VPC n'existe pas") + } +} + +func TestCreateSubnetCommand_Prepare_VPCDeleting(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-dying/state", "deleting") + cmd := CreateSubnetCommand{ + Name: "sn-1", VPC: "vpc-dying", VxlanID: 100, + IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24", + } + if err := cmd.Prepare(db, testCfg()); err == nil { + t.Error("Prepare devrait échouer si le VPC est en cours de suppression") + } +} + +func TestCreateSubnetCommand_Prepare_VPCDeleted(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-gone/state", "deleted") + cmd := CreateSubnetCommand{ + Name: "sn-1", VPC: "vpc-gone", VxlanID: 100, + IfaceType: "vms", GatewayIP: "10.0.0.1", CIDR: "10.0.0.0/24", + } + if err := cmd.Prepare(db, testCfg()); err == nil { + t.Error("Prepare devrait échouer si le VPC est supprimé") + } +} + +// --- DeleteSubnetCommand.Prepare --- + +func TestDeleteSubnetCommand_Prepare_Success(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "subnet/sn-del/state", "created") + cmd := DeleteSubnetCommand{Name: "sn-del"} + if err := cmd.Prepare(db, nil); err != nil { + t.Fatalf("Prepare a échoué : %v", err) + } + state, _ := kv.GetFromDB(db, "subnet/sn-del/state") + if state != "deleting" { + t.Errorf("state attendu deleting, obtenu %q", state) + } +} + +func TestDeleteSubnetCommand_Prepare_NotFound(t *testing.T) { + _, db := newTestDispatcher(t) + cmd := DeleteSubnetCommand{Name: "sn-inexistant"} + if err := cmd.Prepare(db, nil); err == nil { + t.Error("Prepare devrait échouer si le subnet n'existe pas") + } +} diff --git a/internal/dispatcher/agent/vpc_commands_test.go b/internal/dispatcher/agent/vpc_commands_test.go new file mode 100644 index 0000000..e2f2162 --- /dev/null +++ b/internal/dispatcher/agent/vpc_commands_test.go @@ -0,0 +1,89 @@ +package dispatcher + +import ( + "testing" + + "git.g3e.fr/syonad/two/pkg/db/kv" +) + +// --- CreateVPCCommand.Prepare --- + +func TestCreateVPCCommand_Prepare_NewVPC(t *testing.T) { + _, db := newTestDispatcher(t) + cmd := CreateVPCCommand{Name: "vpc-1"} + if err := cmd.Prepare(db, nil); err != nil { + t.Fatalf("Prepare a échoué : %v", err) + } + state, err := kv.GetFromDB(db, "vpc/vpc-1/state") + if err != nil { + t.Fatalf("état non écrit en DB : %v", err) + } + if state != "creating" { + t.Errorf("state attendu creating, obtenu %q", state) + } +} + +func TestCreateVPCCommand_Prepare_Duplicate(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-exist/state", "created") + cmd := CreateVPCCommand{Name: "vpc-exist"} + if err := cmd.Prepare(db, nil); err == nil { + t.Error("Prepare devrait échouer sur un VPC déjà existant") + } +} + +// --- DeleteVPCCommand.Prepare --- + +func TestDeleteVPCCommand_Prepare_Success(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-del/state", "created") + cmd := DeleteVPCCommand{Name: "vpc-del"} + if err := cmd.Prepare(db, nil); err != nil { + t.Fatalf("Prepare a échoué : %v", err) + } + state, _ := kv.GetFromDB(db, "vpc/vpc-del/state") + if state != "deleting" { + t.Errorf("state attendu deleting, obtenu %q", state) + } +} + +func TestDeleteVPCCommand_Prepare_NotFound(t *testing.T) { + _, db := newTestDispatcher(t) + cmd := DeleteVPCCommand{Name: "vpc-inexistant"} + if err := cmd.Prepare(db, nil); err == nil { + t.Error("Prepare devrait échouer si le VPC n'existe pas") + } +} + +func TestDeleteVPCCommand_Prepare_BlockedByActiveSubnet(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-busy/state", "created") + kv.AddInDB(db, "subnet/sn-1/state", "created") + kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-busy") + cmd := DeleteVPCCommand{Name: "vpc-busy"} + if err := cmd.Prepare(db, nil); err == nil { + t.Error("Prepare devrait échouer si un subnet actif existe") + } +} + +func TestDeleteVPCCommand_Prepare_AllowedWhenSubnetDeleted(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-ok/state", "created") + kv.AddInDB(db, "subnet/sn-1/state", "deleted") + kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-ok") + cmd := DeleteVPCCommand{Name: "vpc-ok"} + if err := cmd.Prepare(db, nil); err != nil { + t.Fatalf("Prepare devrait réussir si le subnet est deleted : %v", err) + } +} + +func TestDeleteVPCCommand_Prepare_AllowedWhenSubnetDeleting(t *testing.T) { + _, db := newTestDispatcher(t) + kv.AddInDB(db, "vpc/vpc-ok/state", "created") + kv.AddInDB(db, "subnet/sn-1/state", "deleting") + kv.AddInDB(db, "subnet/sn-1/vpc", "vpc-ok") + cmd := DeleteVPCCommand{Name: "vpc-ok"} + if err := cmd.Prepare(db, nil); err != nil { + t.Fatalf("Prepare devrait réussir si le subnet est deleting : %v", err) + } +} From 649ca66bf94f608e273e2ff6a675a24fc7ab204f Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 15:51:22 +0200 Subject: [PATCH 43/44] f-21: test: add test for agent metadata Signed-off-by: GnomeZworc --- internal/metadata/metadata_test.go | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/internal/metadata/metadata_test.go b/internal/metadata/metadata_test.go index 62a2830..5884151 100644 --- a/internal/metadata/metadata_test.go +++ b/internal/metadata/metadata_test.go @@ -1,6 +1,8 @@ package metadata import ( + "net/http" + "net/http/httptest" "strings" "testing" @@ -181,6 +183,103 @@ func TestUnLoadNoCloudInDB_RemovesAllKeys(t *testing.T) { } } +// --- getIP --- + +func TestGetIP_ValidHostPort(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.RemoteAddr = "10.0.0.1:4567" + if ip := getIP(req); ip != "10.0.0.1" { + t.Errorf("attendu 10.0.0.1, obtenu %q", ip) + } +} + +func TestGetIP_IPv6(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.RemoteAddr = "[::1]:8080" + if ip := getIP(req); ip != "::1" { + t.Errorf("attendu ::1, obtenu %q", ip) + } +} + +func TestGetIP_NoPort(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.RemoteAddr = "10.0.0.1" + if ip := getIP(req); ip != "10.0.0.1" { + t.Errorf("attendu RemoteAddr brut, obtenu %q", ip) + } +} + +// --- rootHandler --- + +func TestRootHandler_UserData(t *testing.T) { + data = NoCloudData{UserData: "userdata-content"} + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/user-data", nil) + rootHandler(w, req) + if w.Code != http.StatusOK { + t.Errorf("attendu 200, obtenu %d", w.Code) + } + if body := w.Body.String(); body != "userdata-content" { + t.Errorf("body inattendu : %q", body) + } +} + +func TestRootHandler_MetaData(t *testing.T) { + data = NoCloudData{MetaData: "metadata-content"} + w := httptest.NewRecorder() + rootHandler(w, httptest.NewRequest(http.MethodGet, "/meta-data", nil)) + if w.Code != http.StatusOK { + t.Errorf("attendu 200, obtenu %d", w.Code) + } + if body := w.Body.String(); body != "metadata-content" { + t.Errorf("body inattendu : %q", body) + } +} + +func TestRootHandler_NetworkConfig(t *testing.T) { + data = NoCloudData{NetworkConfig: "network-content"} + w := httptest.NewRecorder() + rootHandler(w, httptest.NewRequest(http.MethodGet, "/network-config", nil)) + if w.Code != http.StatusOK { + t.Errorf("attendu 200, obtenu %d", w.Code) + } + if body := w.Body.String(); body != "network-content" { + t.Errorf("body inattendu : %q", body) + } +} + +func TestRootHandler_VendorData(t *testing.T) { + data = NoCloudData{VendorData: "vendor-content"} + w := httptest.NewRecorder() + rootHandler(w, httptest.NewRequest(http.MethodGet, "/vendor-data", nil)) + if w.Code != http.StatusOK { + t.Errorf("attendu 200, obtenu %d", w.Code) + } + if body := w.Body.String(); body != "vendor-content" { + t.Errorf("body inattendu : %q", body) + } +} + +func TestRootHandler_UnknownPath(t *testing.T) { + data = NoCloudData{} + w := httptest.NewRecorder() + rootHandler(w, httptest.NewRequest(http.MethodGet, "/unknown", nil)) + if w.Code != http.StatusNotFound { + t.Errorf("attendu 404, obtenu %d", w.Code) + } +} + +func TestRootHandler_ContentType(t *testing.T) { + data = NoCloudData{MetaData: "x"} + w := httptest.NewRecorder() + rootHandler(w, httptest.NewRequest(http.MethodGet, "/meta-data", nil)) + if ct := w.Header().Get("Content-Type"); ct != "text/yaml" { + t.Errorf("Content-Type attendu text/yaml, obtenu %q", ct) + } +} + +// --- UnLoadNoCloudInDB_DoesNotAffectOtherVMs --- + func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) { db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) t.Cleanup(func() { db.Close() }) From e939467abfee96ef7eaf3b79a1840c603fbf8953 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Sun, 26 Apr 2026 16:14:18 +0200 Subject: [PATCH 44/44] f-21: api coherence Signed-off-by: GnomeZworc --- api/agent.yaml | 12 ++++++------ internal/api/agent/subnet_test.go | 28 +++++++++++++++++++++++----- internal/api/agent/subnets.go | 10 +++++++--- internal/api/agent/vpc.go | 6 +++++- internal/api/agent/vpc_test.go | 4 ++-- 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/api/agent.yaml b/api/agent.yaml index 1d3a77f..da20853 100644 --- a/api/agent.yaml +++ b/api/agent.yaml @@ -44,6 +44,12 @@ paths: application/json: schema: $ref: "#/components/schemas/VPC" + "400": + description: Missing required field or invalid request body + content: + application/json: + schema: + $ref: "#/components/schemas/Error" "409": description: VPC already exists content: @@ -179,12 +185,6 @@ paths: $ref: "#/components/schemas/Subnet" "404": $ref: "#/components/responses/NotFound" - "409": - description: Subnet not in a deletable state - content: - application/json: - schema: - $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" diff --git a/internal/api/agent/subnet_test.go b/internal/api/agent/subnet_test.go index 0fdd18b..23745a0 100644 --- a/internal/api/agent/subnet_test.go +++ b/internal/api/agent/subnet_test.go @@ -81,7 +81,7 @@ func TestPostSubnet_Created(t *testing.T) { func TestPostSubnet_MissingFields(t *testing.T) { s, _ := newTestServer(t) - body, _ := json.Marshal(SubnetCreateRequest{Name: "sn-1"}) // vpc, iface_type, gateway_ip, cidr manquants + body, _ := json.Marshal(SubnetCreateRequest{Name: "sn-1"}) // vpc, gateway_ip, cidr manquants w := httptest.NewRecorder() s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) if w.Code != http.StatusBadRequest { @@ -89,6 +89,24 @@ func TestPostSubnet_MissingFields(t *testing.T) { } } +func TestPostSubnet_IfaceTypeOptional(t *testing.T) { + s, db := newTestServer(t) + kv.AddInDB(db, "vpc/vpc-1/state", "created") + req := SubnetCreateRequest{ + Name: "sn-opt", + VPC: "vpc-1", + GatewayIP: "10.0.0.1", + CIDR: "10.0.0.0/24", + // IfaceType omis — doit utiliser default_interface + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) + if w.Code != http.StatusAccepted { + t.Fatalf("attendu 202, obtenu %d: %s", w.Code, w.Body.String()) + } +} + func TestPostSubnet_VPCNotFound(t *testing.T) { s, _ := newTestServer(t) req := SubnetCreateRequest{ @@ -101,8 +119,8 @@ func TestPostSubnet_VPCNotFound(t *testing.T) { body, _ := json.Marshal(req) w := httptest.NewRecorder() s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) - if w.Code != http.StatusConflict { - t.Errorf("attendu 409, obtenu %d", w.Code) + if w.Code != http.StatusUnprocessableEntity { + t.Errorf("attendu 422, obtenu %d", w.Code) } } @@ -138,8 +156,8 @@ func TestPostSubnet_VPCDeleting(t *testing.T) { body, _ := json.Marshal(req) w := httptest.NewRecorder() s.SubnetsHandler(w, httptest.NewRequest(http.MethodPost, "/subnets", bytes.NewReader(body))) - if w.Code != http.StatusConflict { - t.Errorf("attendu 409, obtenu %d", w.Code) + if w.Code != http.StatusUnprocessableEntity { + t.Errorf("attendu 422, obtenu %d", w.Code) } } diff --git a/internal/api/agent/subnets.go b/internal/api/agent/subnets.go index b0fb2fb..60467aa 100644 --- a/internal/api/agent/subnets.go +++ b/internal/api/agent/subnets.go @@ -69,9 +69,9 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ErrorResponse{Error: "invalid request body"}) return } - if req.Name == "" || req.VPC == "" || req.IfaceType == "" || req.GatewayIP == "" || req.CIDR == "" { + if req.Name == "" || req.VPC == "" || req.GatewayIP == "" || req.CIDR == "" { w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, iface_type, gateway_ip and cidr are required"}) + json.NewEncoder(w).Encode(ErrorResponse{Error: "name, vpc, gateway_ip and cidr are required"}) return } cmd := dispatcher.CreateSubnetCommand{ @@ -83,7 +83,11 @@ func (s *Server) postSubnet(w http.ResponseWriter, r *http.Request) { CIDR: req.CIDR, } if err := s.dispatcher.Prepare(cmd); err != nil { - w.WriteHeader(http.StatusConflict) + if _, dbErr := kv.GetFromDB(s.db, "subnet/"+req.Name+"/state"); dbErr == nil { + w.WriteHeader(http.StatusConflict) + } else { + w.WriteHeader(http.StatusUnprocessableEntity) + } json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()}) return } diff --git a/internal/api/agent/vpc.go b/internal/api/agent/vpc.go index df2d47c..43cc33c 100644 --- a/internal/api/agent/vpc.go +++ b/internal/api/agent/vpc.go @@ -42,7 +42,11 @@ func (s *Server) getVpc(w http.ResponseWriter, _ *http.Request, name string) { func (s *Server) deleteVpc(w http.ResponseWriter, _ *http.Request, name string) { cmd := dispatcher.DeleteVPCCommand{Name: name} if err := s.dispatcher.Prepare(cmd); err != nil { - w.WriteHeader(http.StatusNotFound) + if _, dbErr := kv.GetFromDB(s.db, "vpc/"+name+"/state"); dbErr != nil { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusConflict) + } json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()}) return } diff --git a/internal/api/agent/vpc_test.go b/internal/api/agent/vpc_test.go index 0edcd5b..01308a5 100644 --- a/internal/api/agent/vpc_test.go +++ b/internal/api/agent/vpc_test.go @@ -171,8 +171,8 @@ func TestDeleteVpc_BlockedByActiveSubnet(t *testing.T) { req := httptest.NewRequest(http.MethodDelete, "/vpcs/vpc-busy", nil) w := httptest.NewRecorder() s.VpcByNameHandler(w, req) - if w.Code != http.StatusNotFound { - t.Errorf("attendu 404 (Prepare échoue), obtenu %d: %s", w.Code, w.Body.String()) + if w.Code != http.StatusConflict { + t.Errorf("attendu 409, obtenu %d: %s", w.Code, w.Body.String()) } }