Skip to content

Commit a30c0ae

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 9927457 commit a30c0ae

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

conn.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1867,7 +1867,9 @@ func parseEnviron(env []string) (out map[string]string) {
18671867
accrue("user")
18681868
case "PGPASSWORD":
18691869
accrue("password")
1870-
case "PGSERVICE", "PGSERVICEFILE", "PGREALM":
1870+
case "PGSERVICE":
1871+
accrue("service")
1872+
case "PGREALM":
18711873
unsupported()
18721874
case "PGOPTIONS":
18731875
accrue("options")
@@ -1905,6 +1907,62 @@ func parseEnviron(env []string) (out map[string]string) {
19051907
return out
19061908
}
19071909

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