8a9deb8ebc
- 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
114 lines
3.0 KiB
Go
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)
|
|
}
|