init
This commit is contained in:
commit
d05799fe65
60 changed files with 7078 additions and 0 deletions
53
agent/cmd/main.go
Normal file
53
agent/cmd/main.go
Normal 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
11
agent/go.mod
Normal 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
|
||||
)
|
157
agent/internal/agent/agent.go
Normal file
157
agent/internal/agent/agent.go
Normal 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
|
||||
}
|
121
agent/internal/agent/containers.go
Normal file
121
agent/internal/agent/containers.go
Normal 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
|
||||
}
|
120
agent/internal/agent/logs.go
Normal file
120
agent/internal/agent/logs.go
Normal 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
|
||||
}
|
40
agent/internal/config/config.go
Normal file
40
agent/internal/config/config.go
Normal 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
|
||||
}
|
8
agent/internal/config/errors.go
Normal file
8
agent/internal/config/errors.go
Normal 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")
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue