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
This commit is contained in:
+51
-6
@@ -1,7 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
@@ -17,17 +20,55 @@ type AgentConfig struct {
|
||||
|
||||
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: "ws://localhost:3001",
|
||||
Server: defaultServerURL,
|
||||
HeadscaleURL: "",
|
||||
HeadscaleAuthKey: "",
|
||||
NodeID: defaultNodeID(),
|
||||
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)
|
||||
@@ -43,11 +84,12 @@ func loadOrCreateConfig(dataDir string) (*AgentConfig, bool, error) {
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var cfg AgentConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
cfg := &AgentConfig{}
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &cfg, false, nil
|
||||
cfg = mergeWithDefaults(cfg, dataDir)
|
||||
return cfg, false, nil
|
||||
}
|
||||
|
||||
cfg := defaultConfig(dataDir)
|
||||
@@ -60,9 +102,12 @@ func loadOrCreateConfig(dataDir string) (*AgentConfig, bool, error) {
|
||||
// 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, 0644)
|
||||
return os.WriteFile(cp, data, 0600)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user