spf13 / viper

Go configuration with fangs
MIT License
27.03k stars 2.01k forks source link

Possibility to delete keys #632

Open dee-kryvenko opened 5 years ago

dee-kryvenko commented 5 years ago

Sometimes it might be necessary to delete/unset existing keys. For instance in my case, I want to use viper.WriteConfig to dump config to file, but I do want to filter out certain keys: I've tried the following:

viper.SetDefault("config", nil)
viper.Set("config", nil)

But it is interpreted as empty string in resulted config file. I also ran into https://stackoverflow.com/questions/52339336/removal-of-key-value-pair-from-viper-config-file but I wasn't able to make it work for a root level keys.

sagikazarmark commented 5 years ago

The reason why it is not working is that there is no such key as config, but something like config.key, config.key2, etc.

What you can do is fetching all keys from a viper instance using AllKeys and setting all keys starting with config. to null, but I think that wouldn't work either.

Another option is getting all configuration with AllSettings and writing it to a file manually.

Please note that neither options will work with AutomaticEnv and env vars as it only works with direct access (using Get* functions).

Unfortunately implementing the feature you want is not trivial, because "unsetting" would trigger the next configuration source.

dee-kryvenko commented 5 years ago

config is a valid root key in my case, something like:

config: /foo/bar
xxx: yyy

Basically I'm trying to maintain in-memory config based on inputs from viper and cobra, i.e. from CLI arguments, env variables and multiple config files. As part of CLI arguments or env variables you would be able to pass a path to the config file that later needs to be considered as part of the rest of the input.

When I want to change something in a config, such as similar to kubectl config use-context - I want to take what's in-memory, filter out some keys (such as why config path needs to be in the config itself?), and dump it to the file.

I were able to set the key to null, but when I try to save the file - the key ends up in the yaml with an empty value. Expected behavior would be to not to have that key at all.

Writing to file manually is the only option atm but it would be nice if I can reuse what's already in the library and not reinventing the wheel.

Theres two potential ways I can see this can be implemented. One is to go through every map in memory (config, override, defaults, etc...) and delete a key from there. Second is to improve AllSettings and interpret null value as "no key". It seems to break backward compatibility so might require a feature flag / option to be introduced.

sagikazarmark commented 5 years ago

Writing to file manually is the only option atm but it would be nice if I can reuse what's already in the library and not reinventing the wheel.

Personally I think the current writing mechanism is flawed in so many ways, so I avoid using it whenever I can. I can only suggest doing the same. 😕

dee-kryvenko commented 5 years ago

Could you explain please why do you think so? Any suggestions/alternatives? I'm pretty new to viper, well actually I'm pretty new to golang...

anjannath commented 5 years ago

Yay!! There's already a patch for this, see https://github.com/spf13/viper/pull/519

sagikazarmark commented 5 years ago

@llibicpep sorry, missed your answer.

Viper reads configuration from a number of sources. When you write the configuration, everything is written to a file. This poses several issues in itself.

For example, if you configure Viper to read secrets from a secret store and the rest from file, your secrets would be written too file as well. That's not what you usually want.

So using Viper for writing to file only makes sense, of you only use a file as source as well. Even then, you have limited access to the configuration itself (the issue itself proves that).

Based on what the use case is, I usually prefer using Viper when I need to configure an application, and use custom logic when I need to write files. Eg. use a separate Viper instance for file config. Or just read the config file manually, merge in viper, and write back the original config manually.

Take a look at my suggestions in my first comment as well. My advice: avoid writing with Viper. I'm actually going to propose removing config writing if a v2 Viper ever becomes a thing.

anjannath commented 5 years ago

@llibicpep As a hack (if you are only using a config file) if you really want to use viper.WriteConfig you could do something as following:

func Unset(key string) error {
    configMap := viper.AllSettings()
    delete(configMap, key)
    encodedConfig, _ := json.MarshalIndent(configMap, "", " ")
    err := viper.ReadConfig(encodedConfig)
    if err != nil {
        return err
    }
    viper.WriteConfig()
}

Note: do proper error handling

Jazun713 commented 4 years ago

@llibicpep As a hack (if you are only using a config file) if you really want to use viper.WriteConfig you could do something as following:

func Unset(key string) error {
    configMap := viper.AllSettings()
    delete(configMap, key)
    encodedConfig, _ := json.MarshalIndent(configMap, "", " ")
    err := viper.ReadConfig(encodedConfig)
    if err != nil {
        return err
    }
    viper.WriteConfig()
}

Note: do proper error handling

I had to convert the byte[] here to a reader, otherwise, you receive: cannot use (type []byte) as type io.Reader in argument to viper.ReadConfig Here's the modified hack:

func Unset(key string) error {
    configMap := viper.AllSettings()
    delete(configMap, key)
    encodedConfig, _ := json.MarshalIndent(configMap, "", " ")
    err := viper.ReadConfig(bytes.NewReader(encodedConfig))
    if err != nil {
        return err
    }
    viper.WriteConfig()
}
stevenh commented 3 years ago

A more complete version of Unset which deals with more than just root level items.

It's still not perfect as it will save default's and has no concept of where values came from so could save values from ENV to the config including secure vars, which isn't desireable.

func Unset(vars ...string) error {
        cfg := viper.AllSettings()
        vals := cfg

        for _, v := range vars {
                parts := strings.Split(v, ".")
                for i, k := range parts {
                        v, ok := vals[k]
                        if !ok {
                                // Doesn't exist no action needed
                                break
                        }

                        switch len(parts) {
                        case i + 1:
                                // Last part so delete.
                                delete(vals, k)
                        default:
                                m, ok := v.(map[string]interface{})
                                if !ok {
                                        return fmt.Errorf("unsupported type: %T for %q", v, strings.Join(parts[0:i], "."))
                                }
                                vals = m
                        }
                }
        }

        b, err := json.MarshalIndent(cfg, "", " ")
        if err != nil {
                return err
        }

        if err = viper.ReadConfig(bytes.NewReader(b)); err != nil {
                return err
        }

        return viper.WriteConfig()
}
gusega commented 1 year ago

this is a really annoying thing, say I have a key which is a map, I want to remove it. How do I do that? What worked for me is when deleting to write map[string]string{} value and also create a custom getValue function that does this check to see if the value is there !viper.IsSet(key) || len(maps.Keys(viper.GetStringMapString(key))) == 0

tangxinfa commented 5 months ago

A hack method:

    delABC := viper.New()
    delABC.MergeConfigMap(source.AllSettings())
    delABC.Set("a.b.c", struct{}{})
    noABC := viper.New()
    noABC.MergeConfigMap(delABC.AllSettings())