diff --git a/lib/gat/configs/pgbouncer/config.go b/lib/gat/configs/pgbouncer/config.go new file mode 100644 index 0000000000000000000000000000000000000000..a2b5f91fa381a88d1c12e19d7617bde23e0f750e --- /dev/null +++ b/lib/gat/configs/pgbouncer/config.go @@ -0,0 +1,10 @@ +package pgbouncer + +type Config struct { + Databases map[string]string `ini:"databases"` + Users map[string]string `ini:"users"` +} + +func Test() { + +} diff --git a/lib/util/ini/unmarshal.go b/lib/util/ini/unmarshal.go new file mode 100644 index 0000000000000000000000000000000000000000..aca4458e3c42039ad95e715259298f139994cc51 --- /dev/null +++ b/lib/util/ini/unmarshal.go @@ -0,0 +1,159 @@ +package ini + +import ( + "bytes" + "errors" + "reflect" + "strings" +) + +func get(rv reflect.Value, key string, fn func(rv reflect.Value) error) error { +outer: + for { + switch rv.Kind() { + case reflect.Pointer: + if rv.IsNil() { + rv.Set(reflect.New(rv.Type().Elem())) + } + rv = rv.Elem() + case reflect.Struct, reflect.Map: + break outer + default: + return nil + } + } + + switch rv.Kind() { + case reflect.Struct: + rt := rv.Type() + numFields := rt.NumField() + for i := 0; i < numFields; i++ { + field := rt.Field(i) + if !field.IsExported() { + continue + } + name, ok := field.Tag.Lookup("ini") + if !ok { + name = field.Name + } + if name == key { + return fn(rv.Field(i)) + } + } + return nil + case reflect.Map: + rt := rv.Type() + rtKey := rt.Key() + if rtKey.Kind() != reflect.String { + return nil + } + if rv.IsNil() { + rv.Set(reflect.MakeMap(rt)) + } + k := reflect.New(rtKey).Elem() + k.SetString(key) + v := reflect.New(rt.Elem()).Elem() + if err := fn(v); err != nil { + return err + } + rv.SetMapIndex(k, v) + return nil + default: + panic("unreachable") + } +} + +func set(rv reflect.Value, value string) error { +outer: + for { + switch rv.Kind() { + case reflect.Pointer: + if rv.IsNil() { + rv.Set(reflect.New(rv.Type().Elem())) + } + rv = rv.Elem() + case reflect.Struct, reflect.Map, reflect.String: + break outer + default: + return errors.New("cannot set value of this type") + } + } + + switch rv.Kind() { + case reflect.Struct, reflect.Map: + fields := strings.Fields(value) + for _, field := range fields { + k, v, ok := strings.Cut(field, "=") + if !ok { + return errors.New("expected key=value") + } + if err := get(rv, k, func(rvValue reflect.Value) error { + return set(rvValue, v) + }); err != nil { + return err + } + } + return nil + case reflect.String: + rv.SetString(value) + return nil + default: + panic("unreachable") + } +} + +func setpath(rv reflect.Value, section, key, value string) error { + return get(rv, section, func(sec reflect.Value) error { + return get(sec, key, func(entry reflect.Value) error { + return set(entry, value) + }) + }) +} + +func Unmarshal(data []byte, v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return errors.New("expected pointer to non nil") + } + rv = rv.Elem() + + var section string + + var line []byte + for { + line, data, _ = bytes.Cut(data, []byte{'\n'}) + if len(line) == 0 { + if len(data) == 0 { + break + } + continue + } + + line = bytes.TrimSpace(line) + + // comment + if bytes.HasPrefix(line, []byte{';'}) || bytes.HasPrefix(line, []byte{'#'}) { + continue + } + + // section + if bytes.HasPrefix(line, []byte{'['}) && bytes.HasSuffix(line, []byte{']'}) { + section = string(line[1 : len(line)-1]) + continue + } + + // kv pair + key, value, ok := bytes.Cut(line, []byte{'='}) + if !ok { + return errors.New("expected key = value") + } + key = bytes.TrimSpace(key) + value = bytes.TrimSpace(value) + + if err := setpath(rv, section, string(key), string(value)); err != nil { + return err + } + } + + return nil +} diff --git a/lib/util/ini/unmarshal_test.go b/lib/util/ini/unmarshal_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9a0af8a9ecb6d6a1ec682307278ad715264e2262 --- /dev/null +++ b/lib/util/ini/unmarshal_test.go @@ -0,0 +1,176 @@ +package ini + +import ( + "reflect" + "testing" +) + +type Database struct { + Host string `ini:"host"` + Port string `ini:"port"` + User string `ini:"user"` + Password string `ini:"password"` + ClientEncoding string `ini:"client_encoding"` + Datestyle string `ini:"datestyle"` + DBName string `ini:"dbname"` + AuthUser string `ini:"auth_user"` +} + +type Peer struct { + Host string `ini:"host"` +} + +type PoolMode string + +const ( + PoolModeSession PoolMode = "session" + PoolModeTransaction PoolMode = "transaction" +) + +type PgBouncer struct { + PoolMode PoolMode `ini:"pool_mode"` + ListenPort string `ini:"listen_port"` + ListenAddr string `ini:"listen_addr"` + AuthType string `ini:"auth_type"` + AuthFile string `ini:"auth_file"` + Logfile string `ini:"logfile"` + Pidfile string `ini:"pidfile"` + AdminUsers string `ini:"admin_users"` + StatsUsers string `ini:"stats_users"` + SoReuseport string `ini:"so_reuseport"` + UnixSocketDir string `ini:"unix_socket_dir"` + PeerId string `ini:"peer_id"` +} + +type Root struct { + Databases map[string]Database `ini:"databases"` + Peers map[string]Peer `ini:"peers"` + PgBouncer PgBouncer `ini:"pgbouncer"` +} + +type Case struct { + Value string + Expected Root +} + +var Cases = []Case{ + { + Value: `[databases] +postgres = host=localhost dbname=postgres + +[peers] +1 = host=/tmp/pgbouncer1 +2 = host=/tmp/pgbouncer2 + +[pgbouncer] +listen_addr=127.0.0.1 +auth_file=auth_file.conf +so_reuseport=1 +; only unix_socket_dir and peer_id are different +unix_socket_dir=/tmp/pgbouncer2 +peer_id=2 +`, + Expected: Root{ + Databases: map[string]Database{ + "postgres": { + Host: "localhost", + DBName: "postgres", + }, + }, + Peers: map[string]Peer{ + "1": { + Host: "/tmp/pgbouncer1", + }, + "2": { + Host: "/tmp/pgbouncer2", + }, + }, + PgBouncer: PgBouncer{ + ListenAddr: "127.0.0.1", + AuthFile: "auth_file.conf", + SoReuseport: "1", + UnixSocketDir: "/tmp/pgbouncer2", + PeerId: "2", + }, + }, + }, + { + Value: `[databases] +template1 = host=localhost dbname=template1 auth_user=someuser + +[pgbouncer] +pool_mode = session +listen_port = 6432 +listen_addr = localhost +auth_type = md5 +auth_file = users.txt +logfile = pgbouncer.log +pidfile = pgbouncer.pid +admin_users = someuser +stats_users = stat_collector`, + Expected: Root{ + Databases: map[string]Database{ + "template1": { + Host: "localhost", + DBName: "template1", + AuthUser: "someuser", + }, + }, + PgBouncer: PgBouncer{ + PoolMode: PoolModeSession, + ListenPort: "6432", + ListenAddr: "localhost", + AuthType: "md5", + AuthFile: "users.txt", + Logfile: "pgbouncer.log", + Pidfile: "pgbouncer.pid", + AdminUsers: "someuser", + StatsUsers: "stat_collector", + }, + }, + }, + { + Value: `[databases] + +; foodb over Unix socket +foodb = + +; redirect bardb to bazdb on localhost +bardb = host=localhost dbname=bazdb + +; access to destination database will go with single user +forcedb = host=localhost port=300 user=baz password=foo client_encoding=UNICODE datestyle=ISO`, + Expected: Root{ + Databases: map[string]Database{ + "foodb": {}, + "bardb": { + Host: "localhost", + DBName: "bazdb", + }, + "forcedb": { + Host: "localhost", + Port: "300", + User: "baz", + Password: "foo", + ClientEncoding: "UNICODE", + Datestyle: "ISO", + }, + }, + }, + }, +} + +func TestUnmarshal(t *testing.T) { + for _, cas := range Cases { + var result Root + err := Unmarshal([]byte(cas.Value), &result) + if err != nil { + t.Error(err) + continue + } + if !reflect.DeepEqual(result, cas.Expected) { + t.Error("result != expected") + continue + } + } +}