Files
edubox/agent/config.go
T
EduBox Dev 8a9deb8ebc feat(agent): activation zéro-config – config Headscale envoyée par le serveur
- Agent: URL serveur par défaut, node_id auto-généré, config Headscale vide par défaut
- Serveur: lors de l’activation, renvoie headscaleUrl + headscaleAuthKey
- Agent: sauvegarde la config reçue et démarre Tailscale automatiquement
- docker-compose.yml: passe HEADSCALE_URL et HEADSCALE_AUTH_KEY au service server
- Mise à jour du suivi avec le flow zéro-config
2026-06-23 10:30:19 +00:00

114 lines
3.0 KiB
Go

package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// AgentConfig holds user-editable settings for the agent.
type AgentConfig struct {
Server string `json:"server"`
HeadscaleURL string `json:"headscale_url"`
HeadscaleAuthKey string `json:"headscale_auth_key"`
NodeID string `json:"node_id"`
DataDir string `json:"data_dir"`
}
const configFileName = "studioE5-config.json"
// defaultServerURL is the production WebSocket endpoint baked into the agent.
// It can be overridden by the config file for self-hosted or test setups.
const defaultServerURL = "wss://studioe5.edudeploy.com/api/websocket"
// uniqueNodeID returns a stable-ish unique identifier for this machine.
// It combines the hostname with a short random suffix so every install is distinct.
func uniqueNodeID() string {
h, err := os.Hostname()
if err != nil || h == "" {
h = "node"
}
b := make([]byte, 4)
if _, err := rand.Read(b); err == nil {
return fmt.Sprintf("%s-%s", h, hex.EncodeToString(b))
}
return fmt.Sprintf("%s-%d", h, os.Getpid())
}
// defaultConfig returns sensible defaults for a first run.
// The user only needs to provide an activation code; Headscale credentials are
// delivered by the server during activation.
func defaultConfig(dataDir string) *AgentConfig {
return &AgentConfig{
Server: defaultServerURL,
HeadscaleURL: "",
HeadscaleAuthKey: "",
NodeID: uniqueNodeID(),
DataDir: dataDir,
}
}
// mergeWithDefaults fills missing fields from disk with sensible defaults.
func mergeWithDefaults(cfg *AgentConfig, dataDir string) *AgentConfig {
defaults := defaultConfig(dataDir)
if cfg == nil {
return defaults
}
if cfg.Server == "" {
cfg.Server = defaults.Server
}
if cfg.NodeID == "" {
cfg.NodeID = defaults.NodeID
}
if cfg.DataDir == "" {
cfg.DataDir = defaults.DataDir
}
return cfg
}
// configPath returns the absolute path to the config file.
func configPath(dataDir string) string {
return filepath.Join(dataDir, configFileName)
}
// loadOrCreateConfig loads the config file. If it does not exist, it creates
// one with default values and returns it (the caller can then open the settings UI).
func loadOrCreateConfig(dataDir string) (*AgentConfig, bool, error) {
cp := configPath(dataDir)
if _, err := os.Stat(cp); err == nil {
data, err := os.ReadFile(cp)
if err != nil {
return nil, false, err
}
cfg := &AgentConfig{}
if err := json.Unmarshal(data, cfg); err != nil {
return nil, false, err
}
cfg = mergeWithDefaults(cfg, dataDir)
return cfg, false, nil
}
cfg := defaultConfig(dataDir)
if err := saveConfig(dataDir, cfg); err != nil {
return nil, true, err
}
return cfg, true, nil
}
// saveConfig writes the config file to disk.
func saveConfig(dataDir string, cfg *AgentConfig) error {
cp := configPath(dataDir)
if err := os.MkdirAll(dataDir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(cp, data, 0600)
}