Compare commits
5 Commits
479a8de858
...
2dc9ba7b55
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dc9ba7b55 | |||
| c9ca22bafc | |||
| 47ca55adbb | |||
| 6485019785 | |||
| 7ecc2abe0a |
@@ -3,6 +3,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:80 {
|
:80 {
|
||||||
|
route /edubox-agent* {
|
||||||
|
file_server {
|
||||||
|
root /usr/share/caddy/agent
|
||||||
|
}
|
||||||
|
}
|
||||||
reverse_proxy /api/websocket* server:3001
|
reverse_proxy /api/websocket* server:3001
|
||||||
reverse_proxy /gitea* gitea:3000
|
reverse_proxy /gitea* gitea:3000
|
||||||
reverse_proxy server:3000
|
reverse_proxy server:3000
|
||||||
|
|||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION="0.2.1"
|
||||||
|
LDFLAGS="-X main.version=${VERSION}"
|
||||||
|
|
||||||
|
echo "Building EduBox Agent v${VERSION}..."
|
||||||
|
|
||||||
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
|
|
||||||
|
GOOS=windows GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent.exe .
|
||||||
|
echo " edubox-agent.exe (Windows amd64)"
|
||||||
|
|
||||||
|
GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent .
|
||||||
|
echo " edubox-agent (Linux amd64)"
|
||||||
|
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o edubox-agent-mac .
|
||||||
|
echo " edubox-agent-mac (macOS amd64)"
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstanceInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
TemplateName string `json:"templateName,omitempty"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func instancesFile(dataDir string) string {
|
||||||
|
return filepath.Join(dataDir, "instances.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadInstances(dataDir string) (map[string]*InstanceInfo, error) {
|
||||||
|
f := instancesFile(dataDir)
|
||||||
|
data, err := os.ReadFile(f)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return make(map[string]*InstanceInfo), nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var list []*InstanceInfo
|
||||||
|
if err := json.Unmarshal(data, &list); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := make(map[string]*InstanceInfo)
|
||||||
|
for _, inst := range list {
|
||||||
|
m[inst.ID] = inst
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveInstances(dataDir string, instances map[string]*InstanceInfo) error {
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
list := make([]*InstanceInfo, 0, len(instances))
|
||||||
|
for _, inst := range instances {
|
||||||
|
list = append(list, inst)
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(list, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(instancesFile(dataDir), data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertInstance(dataDir string, inst *InstanceInfo) error {
|
||||||
|
instances, err := loadInstances(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
instances[inst.ID] = inst
|
||||||
|
return saveInstances(dataDir, instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeInstance(dataDir, instanceID string) error {
|
||||||
|
instances, err := loadInstances(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(instances, instanceID)
|
||||||
|
return saveInstances(dataDir, instances)
|
||||||
|
}
|
||||||
|
|
||||||
|
func instanceURL(inst *InstanceInfo) string {
|
||||||
|
return fmt.Sprintf("http://localhost:%d", inst.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInstanceStatus(dataDir, instanceID string) string {
|
||||||
|
dir := instanceDir(dataDir, instanceID)
|
||||||
|
composeFile := filepath.Join(dir, "docker-compose.yml")
|
||||||
|
if _, err := os.Stat(composeFile); err != nil {
|
||||||
|
return "stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := getContainerEngine()
|
||||||
|
|
||||||
|
// Try modern JSON format first
|
||||||
|
cmd := exec.Command(engine, "compose", "-f", composeFile, "ps", "--format", "json")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
outStr := strings.TrimSpace(string(out))
|
||||||
|
if outStr == "" || outStr == "[]" {
|
||||||
|
return "stopped"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(outStr, "[") {
|
||||||
|
var containers []map[string]interface{}
|
||||||
|
if err := json.Unmarshal(out, &containers); err == nil {
|
||||||
|
for _, c := range containers {
|
||||||
|
state, _ := c["State"].(string)
|
||||||
|
if state == "running" {
|
||||||
|
return "running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "stopped"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var c map[string]interface{}
|
||||||
|
if err := json.Unmarshal(out, &c); err == nil {
|
||||||
|
state, _ := c["State"].(string)
|
||||||
|
if state == "running" {
|
||||||
|
return "running"
|
||||||
|
}
|
||||||
|
return "stopped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use "ps -q" which is supported by all docker-compose versions
|
||||||
|
cmd = exec.Command(engine, "compose", "-f", composeFile, "ps", "-q")
|
||||||
|
out, err = cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(out)) != "" {
|
||||||
|
return "running"
|
||||||
|
}
|
||||||
|
return "stopped"
|
||||||
|
}
|
||||||
+4
-1
@@ -8,6 +8,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// version is injected at build time via -ldflags "-X main.version=X.Y.Z"
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
serverAddr = flag.String("server", "ws://localhost:3001", "Adresse WebSocket du serveur")
|
serverAddr = flag.String("server", "ws://localhost:3001", "Adresse WebSocket du serveur")
|
||||||
nodeID = flag.String("node-id", defaultNodeID(), "ID du nœud (défaut: hostname)")
|
nodeID = flag.String("node-id", defaultNodeID(), "ID du nœud (défaut: hostname)")
|
||||||
@@ -42,7 +45,7 @@ func main() {
|
|||||||
log.Fatalf("Cannot create data-dir: %v", err)
|
log.Fatalf("Cannot create data-dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("EduBox Agent - node: %s - data-dir: %s\n", *nodeID, *dataDir)
|
fmt.Printf("EduBox Agent v%s - node: %s - data-dir: %s\n", version, *nodeID, *dataDir)
|
||||||
|
|
||||||
if *uiEnabled {
|
if *uiEnabled {
|
||||||
go startUI(*dataDir, *nodeID, *serverAddr)
|
go startUI(*dataDir, *nodeID, *serverAddr)
|
||||||
|
|||||||
+18
-9
@@ -5,8 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
@@ -81,17 +79,28 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listInstances(dataDir string, conn *websocket.Conn) {
|
func listInstances(dataDir string, conn *websocket.Conn) {
|
||||||
dir := filepath.Join(dataDir, "instances")
|
instances, err := loadInstances(dataDir)
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("loadInstances error: %v", err)
|
||||||
conn.WriteJSON(map[string]interface{}{"action": "instances_list", "instances": []interface{}{}})
|
conn.WriteJSON(map[string]interface{}{"action": "instances_list", "instances": []interface{}{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var instances []map[string]interface{}
|
|
||||||
for _, e := range entries {
|
var list []map[string]interface{}
|
||||||
if e.IsDir() {
|
for _, inst := range instances {
|
||||||
instances = append(instances, map[string]interface{}{"id": e.Name()})
|
status := getInstanceStatus(dataDir, inst.ID)
|
||||||
|
if status != inst.Status {
|
||||||
|
inst.Status = status
|
||||||
|
_ = upsertInstance(dataDir, inst)
|
||||||
}
|
}
|
||||||
|
list = append(list, map[string]interface{}{
|
||||||
|
"id": inst.ID,
|
||||||
|
"templateName": inst.TemplateName,
|
||||||
|
"port": inst.Port,
|
||||||
|
"status": inst.Status,
|
||||||
|
"url": instanceURL(inst),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
conn.WriteJSON(map[string]interface{}{"action": "instances_list", "instances": instances})
|
|
||||||
|
conn.WriteJSON(map[string]interface{}{"action": "instances_list", "instances": list})
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-27
@@ -3,32 +3,54 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>EduBox Agent</title>
|
<title>EduBox Agent</title>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, sans-serif; background: #f8fafc; margin: 0; padding: 2rem; }
|
* { box-sizing: border-box; }
|
||||||
.card { background: white; border-radius: 8px; padding: 1.5rem; max-width: 400px; margin: 2rem auto; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: #f1f5f9; margin: 0; padding: 2rem; color: #1e293b; }
|
||||||
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #1e293b; }
|
.container { max-width: 640px; margin: 0 auto; }
|
||||||
input { width: 100%; padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 4px; margin-bottom: 0.75rem; box-sizing: border-box; }
|
.card { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1rem; }
|
||||||
button { width: 100%; padding: 0.6rem; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; }
|
h1 { font-size: 1.5rem; margin: 0 0 1rem; }
|
||||||
|
h2 { font-size: 1.125rem; margin: 0 0 1rem; }
|
||||||
|
input { width: 100%; padding: 0.6rem; border: 1px solid #cbd5e1; border-radius: 8px; margin-bottom: 0.75rem; font-size: 1rem; }
|
||||||
|
button { width: 100%; padding: 0.7rem; background: #2563eb; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 1rem; }
|
||||||
button:hover { background: #1d4ed8; }
|
button:hover { background: #1d4ed8; }
|
||||||
.status { margin-top: 1rem; font-size: 0.875rem; }
|
.status { margin-top: 0.75rem; font-size: 0.9rem; min-height: 1.2rem; }
|
||||||
.success { color: #16a34a; }
|
.success { color: #16a34a; }
|
||||||
.error { color: #dc2626; }
|
.error { color: #dc2626; }
|
||||||
.instance-list { margin-top: 1rem; }
|
.info { color: #64748b; }
|
||||||
.instance-item { padding: 0.5rem; border: 1px solid #e2e8f0; border-radius: 4px; margin-bottom: 0.5rem; }
|
.instance-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||||
|
.instance-item { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
||||||
|
.instance-meta { flex: 1; }
|
||||||
|
.instance-name { font-weight: 600; margin-bottom: 0.25rem; }
|
||||||
|
.instance-port { font-size: 0.85rem; color: #64748b; }
|
||||||
|
.badge { display: inline-block; padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
|
||||||
|
.badge-running { background: #dcfce7; color: #166534; }
|
||||||
|
.badge-starting { background: #fef9c3; color: #854d0e; }
|
||||||
|
.badge-stopped { background: #f1f5f9; color: #475569; }
|
||||||
|
.badge-error { background: #fee2e2; color: #991b1b; }
|
||||||
|
.instance-link { font-size: 0.875rem; color: #2563eb; text-decoration: none; font-weight: 500; }
|
||||||
|
.instance-link:hover { text-decoration: underline; }
|
||||||
|
.empty { text-align: center; color: #64748b; padding: 1rem 0; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="container">
|
||||||
<h1>EduBox Agent</h1>
|
<div class="card">
|
||||||
<div id="main">
|
<h1>EduBox Agent</h1>
|
||||||
<p>Chargement...</p>
|
<div id="main">
|
||||||
|
<p class="info">Connexion en cours...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="instances-card" class="card" style="display:none;">
|
||||||
|
<h2>Mes instances</h2>
|
||||||
|
<div id="instances" class="instance-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ws = new WebSocket('ws://' + location.host + '/ws');
|
const ws = new WebSocket('ws://' + location.host + '/ws');
|
||||||
const main = document.getElementById('main');
|
const main = document.getElementById('main');
|
||||||
|
const instancesCard = document.getElementById('instances-card');
|
||||||
|
const instancesContainer = document.getElementById('instances');
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
ws.send(JSON.stringify({action: 'check'}));
|
ws.send(JSON.stringify({action: 'check'}));
|
||||||
@@ -36,35 +58,42 @@
|
|||||||
|
|
||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
const msg = JSON.parse(ev.data);
|
const msg = JSON.parse(ev.data);
|
||||||
|
|
||||||
if (msg.action === 'not_activated') {
|
if (msg.action === 'not_activated') {
|
||||||
main.innerHTML = `
|
main.innerHTML = `
|
||||||
<p>Veuillez entrer votre code d'activation (6 caractères) :</p>
|
<p>Entre ton code d'activation (6 caractères) :</p>
|
||||||
<input type="text" id="code" maxlength="6" placeholder="XXXXXX">
|
<input type="text" id="code" maxlength="6" placeholder="XXXXXX" onkeydown="if(event.key==='Enter')activate()">
|
||||||
<button onclick="activate()">Activer</button>
|
<button onclick="activate()">Activer</button>
|
||||||
<div id="status" class="status"></div>
|
<div id="status" class="status"></div>
|
||||||
`;
|
`;
|
||||||
} else if (msg.action === 'activated') {
|
} else if (msg.action === 'activated') {
|
||||||
main.innerHTML = `
|
main.innerHTML = `
|
||||||
<p class="success">✅ Activé : <strong>${msg.studentName}</strong></p>
|
<p class="success">✅ Activé : <strong>${escapeHtml(msg.studentName || '')}</strong></p>
|
||||||
<div class="instance-list" id="instances"></div>
|
<p class="info">Tes instances apparaissent ci-dessous.</p>
|
||||||
`;
|
`;
|
||||||
|
instancesCard.style.display = 'block';
|
||||||
ws.send(JSON.stringify({action: 'instances'}));
|
ws.send(JSON.stringify({action: 'instances'}));
|
||||||
} else if (msg.action === 'activation_failed') {
|
} else if (msg.action === 'activation_failed') {
|
||||||
document.getElementById('status').innerHTML = `<span class="error">❌ ${msg.error || 'Code invalide'}</span>`;
|
const status = document.getElementById('status');
|
||||||
|
if (status) status.innerHTML = `<span class="error">❌ ${escapeHtml(msg.error || 'Code invalide')}</span>`;
|
||||||
} else if (msg.action === 'instances_list') {
|
} else if (msg.action === 'instances_list') {
|
||||||
const container = document.getElementById('instances');
|
renderInstances(msg.instances);
|
||||||
if (!container) return;
|
} else if (msg.action === 'instances_updated') {
|
||||||
if (msg.instances.length === 0) {
|
ws.send(JSON.stringify({action: 'instances'}));
|
||||||
container.innerHTML = '<p class="status">Aucune instance assignée.</p>';
|
|
||||||
} else {
|
|
||||||
container.innerHTML = '<p class="status"><strong>Instances :</strong></p>' +
|
|
||||||
msg.instances.map(i => `<div class="instance-item">${i.id}</div>`).join('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
main.innerHTML = `<p class="error">Déconnecté. Recharge la page.</p>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
main.innerHTML = `<p class="error">Erreur de connexion.</p>`;
|
||||||
|
};
|
||||||
|
|
||||||
function activate() {
|
function activate() {
|
||||||
const code = document.getElementById('code').value.trim().toUpperCase();
|
const input = document.getElementById('code');
|
||||||
|
const code = input.value.trim().toUpperCase();
|
||||||
if (code.length !== 6) {
|
if (code.length !== 6) {
|
||||||
document.getElementById('status').innerHTML = '<span class="error">Le code doit faire 6 caractères.</span>';
|
document.getElementById('status').innerHTML = '<span class="error">Le code doit faire 6 caractères.</span>';
|
||||||
return;
|
return;
|
||||||
@@ -72,6 +101,44 @@
|
|||||||
document.getElementById('status').innerHTML = 'Activation en cours...';
|
document.getElementById('status').innerHTML = 'Activation en cours...';
|
||||||
ws.send(JSON.stringify({action: 'activate', code}));
|
ws.send(JSON.stringify({action: 'activate', code}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderInstances(instances) {
|
||||||
|
if (!instances || instances.length === 0) {
|
||||||
|
instancesContainer.innerHTML = '<p class="empty">Aucune instance assignée.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instancesContainer.innerHTML = instances.map(i => {
|
||||||
|
const badgeClass = i.status === 'running' ? 'badge-running' :
|
||||||
|
i.status === 'starting' ? 'badge-starting' :
|
||||||
|
i.status === 'error' ? 'badge-error' : 'badge-stopped';
|
||||||
|
const link = i.status === 'running' && i.url
|
||||||
|
? `<a class="instance-link" href="${escapeHtml(i.url)}" target="_blank" rel="noopener">Ouvrir le site →</a>`
|
||||||
|
: '';
|
||||||
|
const name = escapeHtml(i.templateName || i.id);
|
||||||
|
return `
|
||||||
|
<div class="instance-item">
|
||||||
|
<div class="instance-meta">
|
||||||
|
<div class="instance-name">${name}</div>
|
||||||
|
<div class="instance-port">Port ${i.port || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right;">
|
||||||
|
<span class="badge ${badgeClass}">${i.status || 'stopped'}</span>
|
||||||
|
${link ? '<div style="margin-top:0.4rem;">' + link + '</div>' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (text == null) return '';
|
||||||
|
return String(text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+30
-1
@@ -33,7 +33,9 @@ func sendMessage(msg WSMessage) error {
|
|||||||
if mainConn == nil {
|
if mainConn == nil {
|
||||||
return fmt.Errorf("not connected to server")
|
return fmt.Errorf("not connected to server")
|
||||||
}
|
}
|
||||||
log.Printf("sendMessage: sending %+v", msg)
|
if msg.Action != "heartbeat" {
|
||||||
|
log.Printf("sendMessage: sending %+v", msg)
|
||||||
|
}
|
||||||
return mainConn.WriteJSON(msg)
|
return mainConn.WriteJSON(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +166,9 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
|||||||
"action": "activated",
|
"action": "activated",
|
||||||
"studentName": msg.StudentName,
|
"studentName": msg.StudentName,
|
||||||
})
|
})
|
||||||
|
case "registered":
|
||||||
|
// Server acknowledged our register message; nothing to do.
|
||||||
|
return
|
||||||
case "activation_failed":
|
case "activation_failed":
|
||||||
log.Printf("handleMessage: activation_failed received, error=%s", msg.Error)
|
log.Printf("handleMessage: activation_failed received, error=%s", msg.Error)
|
||||||
notifyUI(map[string]interface{}{
|
notifyUI(map[string]interface{}{
|
||||||
@@ -172,35 +177,59 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
|||||||
})
|
})
|
||||||
case "start":
|
case "start":
|
||||||
log.Printf("Start instance %s on port %d", msg.InstanceID, msg.Port)
|
log.Printf("Start instance %s on port %d", msg.InstanceID, msg.Port)
|
||||||
|
if err := upsertInstance(dataDir, &InstanceInfo{
|
||||||
|
ID: msg.InstanceID,
|
||||||
|
TemplateName: msg.Type,
|
||||||
|
Port: msg.Port,
|
||||||
|
Status: "starting",
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("upsertInstance error: %v", err)
|
||||||
|
}
|
||||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
||||||
log.Printf("writeCompose error: %v", err)
|
log.Printf("writeCompose error: %v", err)
|
||||||
|
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := dockerComposeUp(dataDir, msg.InstanceID); err != nil {
|
if err := dockerComposeUp(dataDir, msg.InstanceID); err != nil {
|
||||||
log.Printf("dockerComposeUp error: %v", err)
|
log.Printf("dockerComposeUp error: %v", err)
|
||||||
|
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||||
|
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||||
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||||
|
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||||
case "stop":
|
case "stop":
|
||||||
log.Printf("Stop instance %s", msg.InstanceID)
|
log.Printf("Stop instance %s", msg.InstanceID)
|
||||||
if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil {
|
if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil {
|
||||||
log.Printf("dockerComposeDown error: %v", err)
|
log.Printf("dockerComposeDown error: %v", err)
|
||||||
}
|
}
|
||||||
|
if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil {
|
||||||
|
inst[msg.InstanceID].Status = "stopped"
|
||||||
|
_ = saveInstances(dataDir, inst)
|
||||||
|
}
|
||||||
|
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||||
case "reset":
|
case "reset":
|
||||||
log.Printf("Reset instance %s", msg.InstanceID)
|
log.Printf("Reset instance %s", msg.InstanceID)
|
||||||
dockerComposeRm(dataDir, msg.InstanceID)
|
dockerComposeRm(dataDir, msg.InstanceID)
|
||||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
||||||
log.Printf("writeCompose error: %v", err)
|
log.Printf("writeCompose error: %v", err)
|
||||||
|
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||||
|
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := dockerComposeUp(dataDir, msg.InstanceID); err != nil {
|
if err := dockerComposeUp(dataDir, msg.InstanceID); err != nil {
|
||||||
log.Printf("dockerComposeUp error: %v", err)
|
log.Printf("dockerComposeUp error: %v", err)
|
||||||
|
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"})
|
||||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
status := getInstanceStatus(dataDir, msg.InstanceID)
|
||||||
|
_ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status})
|
||||||
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||||
|
notifyUI(map[string]interface{}{"action": "instances_updated"})
|
||||||
default:
|
default:
|
||||||
log.Printf("Unknown action: %s", msg.Action)
|
log.Printf("Unknown action: %s", msg.Action)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ services:
|
|||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- ./agent:/usr/share/caddy/agent:ro
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
+92
-6
@@ -20,26 +20,112 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const templates = [
|
const templates = [
|
||||||
{ name: "WordPress latest vierge", type: "wordpress", dockerImage: "wordpress:latest" },
|
{
|
||||||
{ name: "WordPress 6.7 vierge", type: "wordpress", dockerImage: "wordpress:6.7" },
|
name: "WordPress latest vierge",
|
||||||
{ name: "WordPress 6.4 vierge", type: "wordpress", dockerImage: "wordpress:6.4" },
|
type: "wordpress",
|
||||||
{ name: "PrestaShop latest vierge", type: "prestashop", dockerImage: "prestashop/prestashop:latest" },
|
dockerImage: "wordpress:latest",
|
||||||
{ name: "PrestaShop 8.1 vierge", type: "prestashop", dockerImage: "prestashop/prestashop:8.1" },
|
dbImage: "mariadb:10.11",
|
||||||
|
dbName: "wordpress",
|
||||||
|
dbUser: "wordpress",
|
||||||
|
dbPassword: "wordpress",
|
||||||
|
dbRootPassword: "rootpassword",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WordPress 6.7 vierge",
|
||||||
|
type: "wordpress",
|
||||||
|
dockerImage: "wordpress:6.7",
|
||||||
|
dbImage: "mariadb:10.11",
|
||||||
|
dbName: "wordpress",
|
||||||
|
dbUser: "wordpress",
|
||||||
|
dbPassword: "wordpress",
|
||||||
|
dbRootPassword: "rootpassword",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WordPress 6.4 vierge",
|
||||||
|
type: "wordpress",
|
||||||
|
dockerImage: "wordpress:6.4",
|
||||||
|
dbImage: "mariadb:10.11",
|
||||||
|
dbName: "wordpress",
|
||||||
|
dbUser: "wordpress",
|
||||||
|
dbPassword: "wordpress",
|
||||||
|
dbRootPassword: "rootpassword",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrestaShop latest vierge",
|
||||||
|
type: "prestashop",
|
||||||
|
dockerImage: "prestashop/prestashop:latest",
|
||||||
|
dbImage: "mariadb:10.11",
|
||||||
|
dbName: "prestashop",
|
||||||
|
dbUser: "prestashop",
|
||||||
|
dbPassword: "prestashop",
|
||||||
|
dbRootPassword: "rootpassword",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrestaShop 8.1 vierge",
|
||||||
|
type: "prestashop",
|
||||||
|
dockerImage: "prestashop/prestashop:8.1",
|
||||||
|
dbImage: "mariadb:10.11",
|
||||||
|
dbName: "prestashop",
|
||||||
|
dbUser: "prestashop",
|
||||||
|
dbPassword: "prestashop",
|
||||||
|
dbRootPassword: "rootpassword",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const t of templates) {
|
for (const t of templates) {
|
||||||
|
const dbHost = "db";
|
||||||
|
const dbPort = "3306";
|
||||||
const composeConfig = `services:
|
const composeConfig = `services:
|
||||||
|
db:
|
||||||
|
image: ${t.dbImage}
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: ${t.dbRootPassword}
|
||||||
|
MYSQL_DATABASE: ${t.dbName}
|
||||||
|
MYSQL_USER: ${t.dbUser}
|
||||||
|
MYSQL_PASSWORD: ${t.dbPassword}
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
app:
|
app:
|
||||||
image: ${t.dockerImage}
|
image: ${t.dockerImage}
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:{PORT}:80"
|
- "127.0.0.1:{PORT}:80"
|
||||||
environment:
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: ${dbHost}:${dbPort}
|
||||||
|
WORDPRESS_DB_NAME: ${t.dbName}
|
||||||
|
WORDPRESS_DB_USER: ${t.dbUser}
|
||||||
|
WORDPRESS_DB_PASSWORD: ${t.dbPassword}
|
||||||
|
WORDPRESS_DB_PREFIX: wp_
|
||||||
|
WORDPRESS_CONFIG_EXTRA: |
|
||||||
|
define('WP_HOME', 'http://localhost:{PORT}');
|
||||||
|
define('WP_SITEURL', 'http://localhost:{PORT}');
|
||||||
|
PS_DB_HOST: ${dbHost}:${dbPort}
|
||||||
|
PS_DB_NAME: ${t.dbName}
|
||||||
|
PS_DB_USER: ${t.dbUser}
|
||||||
|
PS_DB_PASSWORD: ${t.dbPassword}
|
||||||
|
PS_DB_PREFIX: ps_
|
||||||
|
PS_DOMAIN: localhost:{PORT}
|
||||||
|
PS_SHOP_NAME: ${t.name}
|
||||||
|
PS_INSTALL_AUTO: "0"
|
||||||
INSTANCE_ID: {INSTANCE_ID}
|
INSTANCE_ID: {INSTANCE_ID}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- app_data:/var/www/html
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
app_data:
|
||||||
`;
|
`;
|
||||||
await prisma.template.upsert({
|
await prisma.template.upsert({
|
||||||
where: { id: `${t.type}-${t.dockerImage.replace(/[:\/]/g, "-")}` },
|
where: { id: `${t.type}-${t.dockerImage.replace(/[:\/]/g, "-")}` },
|
||||||
update: {},
|
update: { composeConfig },
|
||||||
create: {
|
create: {
|
||||||
id: `${t.type}-${t.dockerImage.replace(/[:\/]/g, "-")}`,
|
id: `${t.type}-${t.dockerImage.replace(/[:\/]/g, "-")}`,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
|
|||||||
Reference in New Issue
Block a user