This commit is contained in:
el 2025-04-01 16:33:54 +02:00
commit d05799fe65
60 changed files with 7078 additions and 0 deletions

53
agent/cmd/main.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"github.com/etoilepolaire/agent/internal/agent"
"github.com/etoilepolaire/agent/internal/config"
)
func main() {
// Configuration des flags
serverURL := flag.String("server", "http://localhost:8000", "URL du serveur central")
agentName := flag.String("name", "", "Nom de l'agent")
flag.Parse()
if *agentName == "" {
log.Fatal("Le nom de l'agent est requis")
}
// Chargement de la configuration
cfg := config.Config{
ServerURL: *serverURL,
Name: *agentName,
}
// Création de l'agent
a, err := agent.New(cfg)
if err != nil {
log.Fatalf("Erreur lors de la création de l'agent: %v", err)
}
// Gestion des signaux pour un arrêt propre
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Démarrage de l'agent
go func() {
if err := a.Start(); err != nil {
log.Printf("Erreur lors du démarrage de l'agent: %v", err)
}
}()
// Attente du signal d'arrêt
<-sigChan
log.Println("Arrêt de l'agent...")
if err := a.Stop(); err != nil {
log.Printf("Erreur lors de l'arrêt de l'agent: %v", err)
}
}

11
agent/go.mod Normal file
View file

@ -0,0 +1,11 @@
module github.com/etoilepolaire/agent
go 1.21
require (
github.com/docker/docker v24.0.7+incompatible
github.com/gorilla/websocket v1.5.1
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
golang.org/x/crypto v0.14.0
)

View file

@ -0,0 +1,157 @@
package agent
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/docker/docker/client"
"github.com/etoilepolaire/agent/internal/config"
)
// Agent représente l'agent Docker
type Agent struct {
config config.Config
client *client.Client
httpClient *http.Client
agentID string
ctx context.Context
cancel context.CancelFunc
}
// New crée une nouvelle instance de l'agent
func New(cfg config.Config) (*Agent, error) {
if err := cfg.Validate(); err != nil {
return nil, err
}
// Création du client Docker
dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("erreur lors de la création du client Docker: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
return &Agent{
config: cfg,
client: dockerClient,
httpClient: &http.Client{Timeout: 10 * time.Second},
ctx: ctx,
cancel: cancel,
}, nil
}
// Start démarre l'agent
func (a *Agent) Start() error {
// Enregistrement de l'agent
if err := a.register(); err != nil {
return fmt.Errorf("erreur lors de l'enregistrement: %v", err)
}
// Démarrage des routines
go a.heartbeat()
go a.watchContainers()
go a.watchLogs()
return nil
}
// Stop arrête l'agent
func (a *Agent) Stop() error {
a.cancel()
return nil
}
// register enregistre l'agent auprès du serveur
func (a *Agent) register() error {
registration := struct {
Name string `json:"name"`
Hostname string `json:"hostname"`
IPAddress string `json:"ip_address"`
DockerVersion string `json:"docker_version"`
}{
Name: a.config.Name,
Hostname: a.config.Hostname,
IPAddress: a.config.IPAddress,
DockerVersion: a.config.DockerVersion,
}
data, err := json.Marshal(registration)
if err != nil {
return err
}
resp, err := a.httpClient.Post(
fmt.Sprintf("%s/api/agents/register", a.config.ServerURL),
"application/json",
bytes.NewBuffer(data),
)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("erreur lors de l'enregistrement: %s", resp.Status)
}
var result struct {
ID string `json:"id"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
a.agentID = result.ID
return nil
}
// heartbeat envoie régulièrement un heartbeat au serveur
func (a *Agent) heartbeat() {
ticker := time.NewTicker(a.config.HeartbeatInterval)
defer ticker.Stop()
for {
select {
case <-a.ctx.Done():
return
case <-ticker.C:
if err := a.sendHeartbeat(); err != nil {
fmt.Printf("Erreur lors de l'envoi du heartbeat: %v\n", err)
}
}
}
}
// sendHeartbeat envoie un heartbeat au serveur
func (a *Agent) sendHeartbeat() error {
resp, err := a.httpClient.Post(
fmt.Sprintf("%s/api/agents/%s/heartbeat", a.config.ServerURL, a.agentID),
"application/json",
nil,
)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("erreur lors de l'envoi du heartbeat: %s", resp.Status)
}
return nil
}
// watchContainers surveille les changements dans les conteneurs
func (a *Agent) watchContainers() {
// TODO: Implémenter la surveillance des conteneurs
}
// watchLogs surveille les logs des conteneurs
func (a *Agent) watchLogs() {
// TODO: Implémenter la surveillance des logs
}

View file

@ -0,0 +1,121 @@
package agent
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
)
// Container représente un conteneur Docker
type Container struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Status string `json:"status"`
Created time.Time `json:"created"`
Ports map[string][]Port `json:"ports"`
Labels map[string]string `json:"labels"`
}
// Port représente un port exposé
type Port struct {
HostIP string `json:"host_ip"`
HostPort string `json:"host_port"`
}
// watchContainers surveille les changements dans les conteneurs
func (a *Agent) watchContainers() {
events, errs := a.client.Events(a.ctx, types.EventsOptions{})
for {
select {
case <-a.ctx.Done():
return
case err := <-errs:
fmt.Printf("Erreur lors de la surveillance des conteneurs: %v\n", err)
case event := <-events:
if event.Type == events.ContainerEventType {
if err := a.handleContainerEvent(event); err != nil {
fmt.Printf("Erreur lors du traitement de l'événement: %v\n", err)
}
}
}
}
}
// handleContainerEvent traite un événement de conteneur
func (a *Agent) handleContainerEvent(event events.Message) error {
// Mise à jour de la liste des conteneurs
containers, err := a.listContainers()
if err != nil {
return err
}
// Envoi de la mise à jour au serveur
data, err := json.Marshal(containers)
if err != nil {
return err
}
resp, err := a.httpClient.Post(
fmt.Sprintf("%s/api/agents/%s/containers/update", a.config.ServerURL, a.agentID),
"application/json",
bytes.NewBuffer(data),
)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("erreur lors de la mise à jour des conteneurs: %s", resp.Status)
}
return nil
}
// listContainers liste tous les conteneurs
func (a *Agent) listContainers() ([]Container, error) {
containers, err := a.client.ContainerList(a.ctx, types.ContainerListOptions{All: true})
if err != nil {
return nil, err
}
var result []Container
for _, c := range containers {
container, err := a.client.ContainerInspect(a.ctx, c.ID)
if err != nil {
continue
}
ports := make(map[string][]Port)
for containerPort, bindings := range container.NetworkSettings.Ports {
var portBindings []Port
for _, binding := range bindings {
portBindings = append(portBindings, Port{
HostIP: binding.HostIP,
HostPort: binding.HostPort,
})
}
ports[string(containerPort)] = portBindings
}
result = append(result, Container{
ID: container.ID,
Name: container.Name,
Image: container.Config.Image,
Status: container.State.Status,
Created: time.Unix(container.Created, 0),
Ports: ports,
Labels: container.Config.Labels,
})
}
return result, nil
}

View file

@ -0,0 +1,120 @@
package agent
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/docker/docker/api/types"
)
// LogEntry représente une entrée de log
type LogEntry struct {
ContainerID string `json:"container_id"`
Timestamp time.Time `json:"timestamp"`
Message string `json:"message"`
Stream string `json:"stream"`
}
// watchLogs surveille les logs des conteneurs
func (a *Agent) watchLogs() {
containers, err := a.client.ContainerList(a.ctx, types.ContainerListOptions{})
if err != nil {
fmt.Printf("Erreur lors de la récupération des conteneurs: %v\n", err)
return
}
for _, container := range containers {
go a.watchContainerLogs(container.ID)
}
}
// watchContainerLogs surveille les logs d'un conteneur spécifique
func (a *Agent) watchContainerLogs(containerID string) {
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Timestamps: true,
}
reader, err := a.client.ContainerLogs(a.ctx, containerID, options)
if err != nil {
fmt.Printf("Erreur lors de la récupération des logs: %v\n", err)
return
}
defer reader.Close()
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
select {
case <-a.ctx.Done():
return
default:
line := scanner.Text()
if err := a.handleLogLine(containerID, line); err != nil {
fmt.Printf("Erreur lors du traitement du log: %v\n", err)
}
}
}
}
// handleLogLine traite une ligne de log
func (a *Agent) handleLogLine(containerID, line string) error {
// Le format des logs Docker est : [timestamp] stream message
// Exemple : [2024-03-31T22:30:00.000000000Z] stdout Hello World
var stream string
var message string
var timestamp time.Time
// Parse le timestamp
if len(line) >= 32 {
timestampStr := line[1:31]
var err error
timestamp, err = time.Parse(time.RFC3339Nano, timestampStr)
if err != nil {
return err
}
// Parse le stream et le message
if len(line) >= 35 {
stream = line[33:35]
message = line[36:]
}
}
logEntry := LogEntry{
ContainerID: containerID,
Timestamp: timestamp,
Message: message,
Stream: stream,
}
// Envoi du log au serveur
data, err := json.Marshal(logEntry)
if err != nil {
return err
}
resp, err := a.httpClient.Post(
fmt.Sprintf("%s/api/containers/%s/logs", a.config.ServerURL, containerID),
"application/json",
bytes.NewBuffer(data),
)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("erreur lors de l'envoi du log: %s", resp.Status)
}
return nil
}

View file

@ -0,0 +1,40 @@
package config
import (
"os"
"time"
)
// Config représente la configuration de l'agent
type Config struct {
ServerURL string
Name string
Hostname string
IPAddress string
DockerVersion string
HeartbeatInterval time.Duration
}
// DefaultConfig retourne une configuration par défaut
func DefaultConfig() Config {
hostname, _ := os.Hostname()
return Config{
ServerURL: "http://localhost:8000",
Name: "",
Hostname: hostname,
IPAddress: "127.0.0.1", // À remplacer par l'adresse IP réelle
DockerVersion: "unknown",
HeartbeatInterval: 30 * time.Second,
}
}
// Validate vérifie que la configuration est valide
func (c *Config) Validate() error {
if c.Name == "" {
return ErrNameRequired
}
if c.ServerURL == "" {
return ErrServerURLRequired
}
return nil
}

View file

@ -0,0 +1,8 @@
package config
import "errors"
var (
ErrNameRequired = errors.New("le nom de l'agent est requis")
ErrServerURLRequired = errors.New("l'URL du serveur est requise")
)