Skip to content

Commit 8750518

Browse files
Support connection service file and service names
The whole point of supporting this can best be said by directly quoting the PostgreSQL manual: > The connection service file allows libpq connection parameters to be > associated with a single service name. That service name can then be > specified by a libpq connection, and the associated settings will be > used. This allows connection parameters to be modified without > requiring a recompile of the libpq application. The service name can > also be specified using the PGSERVICE environment variable. source: https://www.postgresql.org/docs/current/libpq-pgservice.html Fixes #538
1 parent 9eb3fc8 commit 8750518

File tree

3 files changed

+144
-1
lines changed

3 files changed

+144
-1
lines changed

conn.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1876,7 +1876,9 @@ func parseEnviron(env []string) (out map[string]string) {
18761876
accrue("user")
18771877
case "PGPASSWORD":
18781878
accrue("password")
1879-
case "PGSERVICE", "PGSERVICEFILE", "PGREALM":
1879+
case "PGSERVICE":
1880+
accrue("service")
1881+
case "PGREALM":
18801882
unsupported()
18811883
case "PGOPTIONS":
18821884
accrue("options")
@@ -1914,6 +1916,62 @@ func parseEnviron(env []string) (out map[string]string) {
19141916
return out
19151917
}
19161918

1919+
// parseServiceFile parses the options from a service file and adds them to the values.
1920+
//
1921+
// The parsing code is based on parseServiceInfo from libpq's fe-connect.c
1922+
func parseServiceFile(service string, o values) error {
1923+
filename := os.Getenv("PGSERVICEFILE")
1924+
if filename == "" {
1925+
// XXX this code doesn't work on Windows where the default filename is
1926+
// XXX %APPDATA%\postgresql\.pg_service.conf
1927+
// Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470
1928+
userHome := os.Getenv("HOME")
1929+
if userHome == "" {
1930+
user, err := user.Current()
1931+
if err != nil {
1932+
return err
1933+
}
1934+
userHome = user.HomeDir
1935+
}
1936+
filename = filepath.Join(userHome, ".pg_service.conf")
1937+
}
1938+
1939+
file, err := os.Open(filename)
1940+
if err != nil {
1941+
return err
1942+
}
1943+
defer file.Close()
1944+
1945+
scanner := bufio.NewScanner(file)
1946+
for scanner.Scan() {
1947+
line := strings.TrimSpace(scanner.Text())
1948+
1949+
// once we find the header of our section, we can start reading
1950+
if line == fmt.Sprintf("[%s]", service) {
1951+
for scanner.Scan() {
1952+
line = strings.TrimSpace(scanner.Text())
1953+
// once we find the next section, we're done
1954+
if strings.HasPrefix(line, "[") {
1955+
return nil
1956+
} else if line != "" {
1957+
if err := parseOpts(line, o); err != nil {
1958+
return err
1959+
}
1960+
}
1961+
}
1962+
// EOF means we're done
1963+
return nil
1964+
}
1965+
}
1966+
1967+
if err := scanner.Err(); err != nil {
1968+
return err
1969+
}
1970+
1971+
// if we end up here, we didn't find the service that was explicitly provided
1972+
return fmt.Errorf(`definition of service "%s" not found`, service)
1973+
}
1974+
19171975
// isUTF8 returns whether name is a fuzzy variation of the string "UTF-8".
19181976
func isUTF8(name string) bool {
19191977
// Recognize all sorts of silly things as "UTF-8", like Postgres does

conn_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,72 @@ func TestOpenURL(t *testing.T) {
140140
testURL("postgresql://")
141141
}
142142

143+
func TestPgServiceFile(t *testing.T) {
144+
if os.Getenv("PGSERVICEFILE") == "" {
145+
if os.Getenv("TRAVIS") != "true" {
146+
t.Skip("PGSERVICEFILE not set, skipping service connection file tests")
147+
}
148+
os.Setenv("PGSERVICEFILE", "/tmp/pqgotest_pgservice")
149+
os.Remove(pgpassFile)
150+
pgservice, err := os.OpenFile(os.Getenv("PGSERVICEFILE"), os.O_RDWR|os.O_CREATE, 0644)
151+
if err != nil {
152+
t.Fatalf("Unexpected error writing pg service file %#v", err)
153+
}
154+
_, err = pgservice.WriteString(`
155+
[service1]
156+
host=localhost
157+
158+
[service2]
159+
dbname=template2
160+
161+
[service3]
162+
thistestshould=fail
163+
`)
164+
if err != nil {
165+
t.Fatalf("Unexpected error writing pg service file %#v", err)
166+
}
167+
pgservice.Close()
168+
}
169+
170+
testAssert := func(conninfo string, expected string, reason string) {
171+
conn, err := openTestConnConninfo(conninfo)
172+
if err != nil {
173+
t.Fatal(err)
174+
}
175+
defer conn.Close()
176+
177+
txn, err := conn.Begin()
178+
if err != nil {
179+
if expected != "fail" {
180+
t.Fatalf(reason, err)
181+
}
182+
return
183+
}
184+
rows, err := txn.Query("SELECT USER")
185+
if err != nil {
186+
txn.Rollback()
187+
if expected != "fail" {
188+
t.Fatalf(reason, err)
189+
}
190+
} else {
191+
rows.Close()
192+
if expected != "ok" {
193+
t.Fatalf(reason, err)
194+
}
195+
}
196+
txn.Rollback()
197+
}
198+
199+
testAssert("service=service1", "ok", "connect to defaults failed")
200+
testAssert("service=service2", "fail", "connect to template2 failed")
201+
testAssert("service=service3", "fail", "unrecognized parameter %#v")
202+
203+
os.Setenv("PGSERVICEFILE", "IdoNotExist")
204+
testAssert("service=pietje", "fail", "service file does not exist")
205+
206+
os.Setenv("PGSERVICEFILE", "")
207+
}
208+
143209
const pgpassFile = "/tmp/pqgotest_pgpass"
144210

145211
func TestPgpass(t *testing.T) {

connector.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func NewConnector(dsn string) (*Connector, error) {
4747
//
4848
// * Very low precedence defaults applied in every situation
4949
// * Environment variables
50+
// * Service name variables
5051
// * Explicitly passed connection information
5152
o["host"] = "localhost"
5253
o["port"] = "5432"
@@ -68,6 +69,24 @@ func NewConnector(dsn string) (*Connector, error) {
6869
return nil, err
6970
}
7071

72+
// whenever a service is specified, we will need to parse the connection service file
73+
// and override the defaults with the parameters specified for that service.
74+
// See https://www.postgresql.org/docs/current/libpq-pgservice.html
75+
if service, ok := o["service"]; ok {
76+
if err := parseServiceFile(service, o); err != nil {
77+
return nil, err
78+
}
79+
80+
// By overwriting the options with the service parameters we may have masked some
81+
// explicitly passed connection information, e.g. "service=staging user=read_only".
82+
// By repeating the parseOpts we overcome this issue.
83+
if err := parseOpts(dsn, o); err != nil {
84+
return nil, err
85+
}
86+
// "service" itself should not be passed down as a connection parameter
87+
delete(o, "service")
88+
}
89+
7190
// Use the "fallback" application name if necessary
7291
if fallback, ok := o["fallback_application_name"]; ok {
7392
if _, ok := o["application_name"]; !ok {

0 commit comments

Comments
 (0)