goで設定ファイルを読み込むときに上書きしたい
goで設定ファイルを読み込むときに設定の一部だけを上書きしたいことがある。それ用のコードのメモ。
mergo?
mergoというのはけっこう古くからあるライブラリみたい。
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements.
2つのデータをいい感じにマージしてくれる。
振る舞いについては以下を見ると良い
雑にまとめると mergo.Merge(<dst>, <src>)
は
- unexported fieldは無視
- zero valueは無視
という条件で再帰的に上書きしていく。<src>
自体は書き換わらずに<dst>
が書き換わる。
試しに使ってみる
実際に設定の上書きを試してみる(意外とコード例までたどり着くまでが長かったのでコード例の部分だけが知りたい場合にはgistを直接見たほうが早いかもしれない)。
やりたいことの整理
設定ファイルを上書きする例を無理やりひねり出す。
テキトーにネストした形状になっている設定ファイルを探してくる。docker composeの例などが便利かもしれない。
docker-compose.yml
version: "3.7" services: wordpress: image: wordpress ports: - "8080:80" networks: - overlay deploy: mode: replicated replicas: 2 endpoint_mode: vip mysql: image: mysql volumes: - db-data:/var/lib/mysql/data networks: - overlay deploy: mode: replicated replicas: 2 endpoint_mode: dnsrr volumes: db-data: networks: overlay:
これを以下の様に変更して利用したい。
--- 03config/output.txt 2020-02-19 20:07:00.000000000 +0900 +++ 04overwrite-config/output.txt 2020-02-19 20:07:05.000000000 +0900 @@ -3,7 +3,7 @@ wordpress: image: wordpress ports: - - '8080:80' + - '9090:80' networks: - overlay deploy: @@ -18,7 +18,7 @@ - overlay deploy: mode: replicated - replicas: 2 + replicas: 5 endpoint_mode: dnsrr volumes: db-data: null
goで読み込めるようにjsonに変換しておく(別にgo-yamlなどを使っても良い)
$ dictknife cat -o json docker-compose.yml > config.json
ふつうの読み込み
コードの一部分だけを抜粋という形式は好きではないのでまじめにふつうの読み込み部分のコードも書く。
変換しておいたJSONを以下のサービスにテキトーに貼り付けてgoの構造体のコードを得る。
conf/config.go
package conf // https://docs.docker.com/compose/compose-file/ // https://mholt.github.io/json-to-go/ type Config struct { Version string `json:"version"` Services Services `json:"services"` Volumes Volumes `json:"volumes"` Networks Networks `json:"networks"` } type Deploy struct { Mode string `json:"mode"` Replicas int `json:"replicas"` EndpointMode string `json:"endpoint_mode"` } type Wordpress struct { Image string `json:"image"` Ports []string `json:"ports"` Networks []string `json:"networks"` Deploy Deploy `json:"deploy"` } type Mysql struct { Image string `json:"image"` Volumes []string `json:"volumes"` Networks []string `json:"networks"` Deploy Deploy `json:"deploy"` } type Services struct { Wordpress Wordpress `json:"wordpress"` Mysql Mysql `json:"mysql"` } type Volumes struct { DbData interface{} `json:"db-data"` } type Networks struct { Overlay interface{} `json:"overlay"` }
読み込む関数は以下のようなもの。
// JSONLoadFile ... func JSONLoadFile(filename string, c interface{}) error { f, err := os.Open(filename) if err != nil { return err } defer f.Close() decoder := json.NewDecoder(f) return decoder.Decode(c) }
以下の様にして使う。
var c conf.Config if err := JSONLoadFile(config, &c); err != nil { return err } pp.ColoringEnabled = false pp.Println(c)
上書きの確認
上書きの確認は以下の様にすることにする
- 00config/main.go は上書きを行わないコード
- 01config/main.go は上書きを行うコード (overwrite.jsonを利用して上書きする)
- 00と01の実行結果のdiffを取りその差分を見る
00config ├── main.go └── output.txt 01overwrite-config ├── main.go └── output.txt config.json overwrite.json Makefile conf └── config.go
こんな感じで実行する想定
$ go run 00config/main.go --config config.json | tee 00config/output.txt OVERWRITE_CONFIG=overwrite.json $ go run 01overwrite-config/main.go --config config.json | tee 01overwrite-config/output.txt $ diff -u 00config/output.txt 01overwrite-config/output.txt > 02.diff
ちなみにgo.modは以下の様な状態。
go.mod
module m go 1.13 require ( github.com/imdario/mergo v0.3.8 // indirect github.com/k0kubun/pp v3.0.1+incompatible // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/spf13/pflag v1.0.5 // indirect )
実際の上書きのコード
ようやく本題。
OVERWRITE_CONFIG
という環境変数に値が入っていたらそれを使って上書きすることにする。
以下の様なファイルを渡す。
overwrite.json
{ "services": { "wordpress": { "ports": [ "9090:80" ] }, "mysql": { "deploy": { "replicas": 5 } } } }
設定の読み込みは以下の様なコードに変わった。
// JSONLoadFile ... (再掲) func JSONLoadFile(filename string, c interface{}) error { f, err := os.Open(filename) if err != nil { return err } defer f.Close() decoder := json.NewDecoder(f) return decoder.Decode(c) } // LoadConfig ... func LoadConfig(filename string) (*conf.Config, error) { var c conf.Config if err := JSONLoadFile(filename, &c); err != nil { return nil, err } overwritefile := os.Getenv("OVERWRITE_CONFIG") if overwritefile == "" { return &c, nil } fmt.Fprintf(os.Stderr, "***** OVERWRITE CONFIG by %q *****\n", overwritefile) var c2 conf.Config if err := JSONLoadFile(overwritefile, &c2); err != nil { return &c, err } if err := mergo.Merge(&c2, &c); err != nil { return &c, err } return &c2, nil }
以下の様な形で実行する。
$ go run 00config/main.go --config config.json | tee 00config/output.txt OVERWRITE_CONFIG=overwrite.json $ go run 01overwrite-config/main.go --config config.json | tee 01overwrite-config/output.txt $ diff -u 00config/output.txt 01overwrite-config/output.txt > 02.diff || exit 0
diff。良さそう。
02.diff
--- 00config/output.txt 2020-02-19 20:01:01.000000000 +0900 +++ 01overwrite-config/output.txt 2020-02-19 20:01:01.000000000 +0900 @@ -1,10 +1,10 @@ -conf.Config{ +&conf.Config{ Version: "3.7", Services: conf.Services{ Wordpress: conf.Wordpress{ Image: "wordpress", Ports: []string{ - "8080:80", + "9090:80", }, Networks: []string{ "overlay", @@ -25,7 +25,7 @@ }, Deploy: conf.Deploy{ Mode: "replicated", - Replicas: 2, + Replicas: 5, EndpointMode: "dnsrr", }, },
gist
全体のコードは以下。ただしgistにあげる過程でディレクトリ構造が壊れてしまっているのでそのままでは実行できない。
(最近はpflagの方を使い始めている(cobraやviperはどうなんだろう?))
ちなみに
ちなみにpythonで似たようなことをやる場合には以下の様な感じ(schemaを定義していないので同じ状態ではない。zero valueが存在していないので同様のマージは難しい)。
import os from handofcats import as_command from dictknife import loading from dictknife import deepmerge # pip install dictknife[yaml] handofcats # $ OVERWRITE_CONFIG=<overwrite file> python <file>.py --config <config file> @as_command def run(*, config: str) -> None: c = loading.loadfile(config) overwrite_file = os.environ.get("OVERWRITE_CONFIG") if overwrite_file is not None: c2 = loading.loadfile(overwrite_file) c = deepmerge(c, c2, method="replace") loading.dumpfile(c)
そして実はnestしたdictのマージはこれが良いというデフォルトが意外と決まらないという話。
ちなみに2
ちなみにpythonでhydraなどを使うと以下の様な形でコマンドライン引数として上書きする値を渡せたりする。
こういうかんじに。
$ python 06hydra/main.py db: driver: mysql pass: secret user: omry $ python 06hydra/main.py db.pass=oyoyo db: driver: mysql pass: oyoyo user: omry
このときのコードは以下。
この辺の説明を読めば雰囲気は分かる。
(追記: viperも似たような機能を提供していたような記憶)
ちなみにのまとめ
- (mergoを使うとそれなりに手軽に設定の上書きができる)
- schemaを定義するか否かどちらが良いかは考える必要がありそう
- コマンドライン引数での上書きをサポートするCLIも考えられる
- (viperなどを使って環境変数経由での設定への以降(12FactorApp的な文脈))
- (AWSのparameter storeなどを使う場合の対応)
(追記: validationの話がすっぽり抜け落ちているかも)