-
Notifications
You must be signed in to change notification settings - Fork 10
fix(config): boot with duplicate site meters demoted instead of crash-looping #991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ftw": patch | ||
| --- | ||
|
|
||
| A config with two `is_site_meter: true` drivers no longer stops the box from starting. The box boots with the first declared driver as the site meter — the same one older versions silently used — ignores the flag on the rest, and logs a clear error naming both drivers so the mistake is visible in the log and the help report. Saving such a config from Settings is still rejected. A driver install that accidentally added a second site meter used to crash-loop the box before the web UI came up, leaving SSH as the only way back in. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,13 @@ type Config struct { | |
| FleetPing *FleetPing `yaml:"fleet_ping,omitempty" json:"fleet_ping,omitempty"` | ||
| Nova *Nova `yaml:"nova,omitempty" json:"nova,omitempty"` | ||
| DeviceRepository *DeviceRepository `yaml:"device_repository,omitempty" json:"device_repository,omitempty"` | ||
|
|
||
| // LoadWarnings collects recoverable problems Parse repaired instead of | ||
| // refusing the file: an on-disk config an older version accepted must | ||
| // still boot, or the operator loses the UI they would fix it with. The | ||
| // write path (Settings save, bootstrap) never populates this — it calls | ||
| // Validate directly and stays strict. Never serialized. | ||
| LoadWarnings []string `yaml:"-" json:"-"` | ||
| } | ||
|
|
||
| // AppLink controls the outbound connection the FTW app reaches this box | ||
|
|
@@ -1312,13 +1319,45 @@ func Parse(data []byte, baseDir string) (*Config, error) { | |
| c.AppLink = &AppLink{Enabled: false} | ||
| } | ||
| applyDefaults(&c) | ||
| c.demoteExtraSiteMeters() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a duplicate is introduced while the service is already running, Useful? React with 👍 / 👎. |
||
| if err := c.Validate(); err != nil { | ||
| return nil, err | ||
| } | ||
| c.ResolveDriverPaths(baseDir) | ||
| return &c, nil | ||
| } | ||
|
|
||
| // demoteExtraSiteMeters keeps the first declared is_site_meter driver and | ||
| // clears the flag on the rest, recording a LoadWarning per demotion. | ||
| // | ||
| // Load-time only. Duplicate site meters are an operator mistake Validate | ||
| // rejects on the write path, but a file already on disk was often written | ||
| // by an older version that silently used the first match | ||
| // (SiteMeterDriver's order) — refusing it at boot crash-loops the process | ||
| // before the HTTP listener binds, and the operator loses the very UI they | ||
| // would fix the config with (field incident 2026-08-29: a driver install | ||
| // on v1.15.0 left two site meters; the box updated to v2.3.0 and went | ||
| // dark until SSH). Demoting reproduces exactly what the older version | ||
| // dispatched against, and the warning makes the ambiguity visible where | ||
| // silence caused it to be missed. | ||
| func (c *Config) demoteExtraSiteMeters() { | ||
| kept := "" | ||
| for i := range c.Drivers { | ||
| d := &c.Drivers[i] | ||
| if !d.IsSiteMeter { | ||
| continue | ||
| } | ||
| if kept == "" { | ||
| kept = d.Name | ||
| continue | ||
| } | ||
| d.IsSiteMeter = false | ||
| c.LoadWarnings = append(c.LoadWarnings, fmt.Sprintf( | ||
| "config: drivers %q and %q both set is_site_meter: true; keeping %q as the site meter and ignoring the flag on %q — fix config.yaml so exactly one driver has it", | ||
| kept, d.Name, kept, d.Name)) | ||
| } | ||
| } | ||
|
|
||
| func topLevelYAMLNull(doc *yaml.Node, key string) bool { | ||
| if doc == nil || doc.Kind != yaml.DocumentNode || len(doc.Content) != 1 { | ||
| return false | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hot reload swallows site-meter warnings
Medium Severity
LoadWarningsare only logged inmainafter the initialconfig.Load.Watcher.reloadalso callsconfig.Load, which demotes extrais_site_meterflags, then applies the result with no ERROR. A liveconfig.yamledit that leaves two flags now hot-reloads the first driver silently, so the log ring and help report never see the ambiguity after boot.Reviewed by Cursor Bugbot for commit d33fb18. Configure here.