Skip to content

AGE-182: only write explicit changes to config file#36

Merged
murrayju merged 5 commits intomainfrom
murrayju/config-mgmt
Oct 3, 2025
Merged

AGE-182: only write explicit changes to config file#36
murrayju merged 5 commits intomainfrom
murrayju/config-mgmt

Conversation

@murrayju
Copy link
Member

@murrayju murrayju commented Oct 2, 2025

The config system had a problem where the effective config (after resolving CLI args, env vars, and defaults) would be written to the config file when any config was intended to be saved.

This caused several problems, most annoying of which being that temporarily passed CLI flags would get persisted (like -o json).

The solution is to never use the global viper instance to write the config file. Instead, a new instance is created, and only populated with the file content before being updated as needed. Then this can be written without carrying in unwanted values.

@linear
Copy link

linear bot commented Oct 2, 2025

AGE-182 Tiger CLI: Make sure config updates don't accidentally save global flag/env vars

See Slack thread.

Currently, if you run tiger service create --output json, it saves output: json to your config file (which you can verify with tiger config show).

I think what's probably happening is that when it saves the default service ID to the config file, it's also saving any config values that were provided as env vars or flags, which is NOT what we want. I imagine the same is true for any other commands that save to the config file (e.g. tiger config set).

I think the problem is that this call to Save() is saving the config with all global flags and env vars applied (because of the way viper is configured here and here). We need to fix that logic (as well as all calls to Set() and Unset(), which call Save() internally), so that it first loads the config without env vars/flags applied, updates the relevant value, and then saves it back to the filesystem. Might require a decent refactor of how the logic for loading and saving the config currently works.

Copy link
Contributor

@cevian cevian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

brilliant solution! bravo. One nit: I'd really like to see unit tests for this before approving. This is the kind of thing claude can generate tests for really well and may catch some edge cases.

@murrayju murrayju force-pushed the murrayju/config-mgmt branch from ef05f14 to 9ee3ee1 Compare October 3, 2025 16:21
@nathanjcochran nathanjcochran self-requested a review October 3, 2025 17:28
Copy link
Member

@nathanjcochran nathanjcochran left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall approach looks great to me. Tested (very) briefly and it seems to do what I'd expect.

My one minor suggestion is that I'd prefer we didn't use reflection to set config values in updateField. See my comment below on that.

Comment on lines +246 to +267
// updateField uses reflection to update the field in the Config struct corresponding to the given key.
// This is used to keep the struct in sync with the viper state when setting values.
func (c *Config) updateField(key string, value any) error {
v := reflect.ValueOf(c).Elem()
t := v.Type()

// Find field by json tag
for i := range t.NumField() {
field := t.Field(i)
tag := field.Tag.Get("mapstructure")

if tag == key {
fieldValue := v.Field(i)
if fieldValue.CanSet() {
fieldValue.Set(reflect.ValueOf(value))
viper.Set(key, value)
return nil
}
}
}
return fmt.Errorf("field not found: %s", key)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this reflection approach, tbh. Generally best to avoid reflection if possible imo ("Reflection is never clear" is one of the Go proverbs). I'd honestly prefer a big switch statement (even if it looks redundant) like before. Can the switch just be extracted from the Set call and used for this purpose too? I guess not, because Set takes a string value? But maybe things could be restructured somehow? 🤔

I could probably be convinced that reflection is fine here, if you have strong feelings. But it did stand out to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a vacuum, I might have argued for reflection for this function. But, thinking about it more, we already had a switch-case for Set that does some validation, and we might as well re-use that here.

d0e098d

@murrayju murrayju merged commit 8f193d9 into main Oct 3, 2025
1 check passed
@murrayju murrayju deleted the murrayju/config-mgmt branch October 3, 2025 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants