package agent import ( "context" "crypto/sha256" "os" "path/filepath" "time" llmgwv1 "gitea.mynodes.xyz/ilker/llmgw/api/gen/llmgw/v1" ) // The cluster's settings for this agent (BACKLOG E3): what agent.yaml can // say but the agent's id, control planes, address and certificate, kept by // the control planes and sent over the stream. Kept here beside agent.yaml, // which then holds only what the agent needs to start; at start the two are // read together, these over the file. // SettingsFile keeps the settings the control plane last sent. var SettingsFile = "/var/lib/llmgw-agent/agent-settings.yaml" // ReadSettings is what SettingsFile holds, nil when nothing. func ReadSettings() []byte { b, err := os.ReadFile(SettingsFile) if err != nil { return nil } return b } // SetSettingsCheck says how to tell whether settings would load beside this // agent's own file; settings that would not are not taken. func (a *Agent) SetSettingsCheck(f func([]byte) error) { a.settingsCheck = f } // applySettings takes the settings the control plane sent: nothing when they // are what this agent runs on, and otherwise kept and started again on. func (a *Agent) applySettings(m *llmgwv1.AgentSettings) { next := m.GetYaml() if next == a.cfg.Settings { return } if a.settingsCheck != nil { if err := a.settingsCheck([]byte(next)); err != nil { // Kept running on what it has: settings that will not load // here must not take the node out of service. a.log.Error("the cluster's settings for this agent do not load beside its agent.yaml; running on what it has", "err", err) return } } if err := os.MkdirAll(filepath.Dir(SettingsFile), 0o755); err != nil { a.log.Error("could not keep the cluster's settings for this agent", "err", err) return } tmp := SettingsFile + ".new" if err := os.WriteFile(tmp, []byte(next), 0o600); err != nil { a.log.Error("could not keep the cluster's settings for this agent", "err", err) return } if err := os.Rename(tmp, SettingsFile); err != nil { a.log.Error("could not keep the cluster's settings for this agent", "err", err) return } a.log.Info("the cluster's settings for this agent changed; starting again on them, the engines keep serving") time.Sleep(300 * time.Millisecond) exitForUpgrade() } // WatchFile starts the agent again when its configuration file changes and // still loads: agent.yaml is read only at start, and a change to it that // waited for somebody to restart the agent was a change nobody saw take // effect. func (a *Agent) WatchFile(ctx context.Context, path string, loads func() error) { if path == "" { return } sum := func() [32]byte { b, _ := os.ReadFile(path) //nolint:gosec // the agent's own configuration return sha256.Sum256(b) } was := sum() t := time.NewTicker(watchEvery) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: } now := sum() if now == was { continue } if err := loads(); err != nil { // Once per change: an editor saving halfway must not stop the // agent, and a broken file is reported, not restarted into. a.log.Error("agent.yaml changed and does not load; running on what it had", "path", path, "err", err) was = now continue } a.log.Info("agent.yaml changed; starting again on it, the engines keep serving", "path", path) time.Sleep(300 * time.Millisecond) exitForUpgrade() return } } // watchEvery is how often agent.yaml is looked at. var watchEvery = 10 * time.Second