[client] Stop the last config write that skipped normalization

Every path that creates or updates a profile config goes through apply(),
which resolves an optional field to its default — except RenameProfile,
which read the file with a bare json.Unmarshal, set the name, and wrote it
straight back. That copied whatever the file held, so a config written by a
client that stored these fields as null kept them null. It could not
introduce a null, only carry one forward, but renaming a profile is a poor
place to leave a half-resolved config behind. It now reads through
GetExistingConfig, which normalizes what it hands out.

The tests state the invariant the fix completes, over the *bool fields of
Config listed by reflection so a field added later is covered without
touching them: none may come out of apply() unset, and no write may store
one as null. An optional bool that can be nil, true or false forces every
reader to invent the meaning of nil, and makes a diff of the config compare
presence rather than value — which is exactly what refused `netbird up` for
a client restating its own defaults.

SyncMessageVersion stays a genuine three-state field and is not covered: it
is an *int whose absence means the client pins no version, and it travels to
management that way.
This commit is contained in:
riccardom
2026-09-07 17:12:12 +02:00
parent e998887977
commit 70167821bb
2 changed files with 138 additions and 7 deletions
+7 -7
View File
@@ -356,17 +356,17 @@ func (s *ServiceManager) RenameProfile(id ID, username string, newName string) e
return ErrProfileNotFound
}
data, err := os.ReadFile(target.Path)
// Through the reader, not a bare Unmarshal: this was the one write that
// skipped apply(), so it copied back whatever the file held — including an
// optional field left unset, which every other write resolves to its
// default. Renaming a profile is a poor place to leave that behind.
cfg, err := GetExistingConfig(target.Path)
if err != nil {
return err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return err
return fmt.Errorf("read profile config: %w", err)
}
cfg.Name = displayName
if err := util.WriteJson(context.Background(), target.Path, cfg); err != nil {
if err := WriteOutConfig(target.Path, cfg); err != nil {
return fmt.Errorf("failed to write profile name: %w", err)
}
return nil