Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
184
plugins/common/postgresql/config.go
Normal file
184
plugins/common/postgresql/config.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
package postgresql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/stdlib"
|
||||
|
||||
"github.com/influxdata/telegraf/config"
|
||||
)
|
||||
|
||||
var socketRegexp = regexp.MustCompile(`/\.s\.PGSQL\.\d+$`)
|
||||
var sanitizer = regexp.MustCompile(`(\s|^)((?:password|sslcert|sslkey|sslmode|sslrootcert)\s?=\s?(?:(?:'(?:[^'\\]|\\.)*')|(?:\S+)))`)
|
||||
|
||||
type Config struct {
|
||||
Address config.Secret `toml:"address"`
|
||||
OutputAddress string `toml:"outputaddress"`
|
||||
MaxIdle int `toml:"max_idle"`
|
||||
MaxOpen int `toml:"max_open"`
|
||||
MaxLifetime config.Duration `toml:"max_lifetime"`
|
||||
IsPgBouncer bool `toml:"-"`
|
||||
}
|
||||
|
||||
func (c *Config) CreateService() (*Service, error) {
|
||||
addrSecret, err := c.Address.Get()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting address failed: %w", err)
|
||||
}
|
||||
addr := addrSecret.String()
|
||||
defer addrSecret.Destroy()
|
||||
|
||||
if c.Address.Empty() || addr == "localhost" {
|
||||
addr = "host=localhost sslmode=disable"
|
||||
if err := c.Address.Set([]byte(addr)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
connConfig, err := pgx.ParseConfig(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Remove the socket name from the path
|
||||
connConfig.Host = socketRegexp.ReplaceAllLiteralString(connConfig.Host, "")
|
||||
|
||||
// Specific support to make it work with PgBouncer too
|
||||
// See https://github.com/influxdata/telegraf/issues/3253#issuecomment-357505343
|
||||
if c.IsPgBouncer {
|
||||
// Remove DriveConfig and revert it by the ParseConfig method
|
||||
// See https://github.com/influxdata/telegraf/issues/9134
|
||||
connConfig.PreferSimpleProtocol = true
|
||||
}
|
||||
|
||||
// Provide the connection string without sensitive information for use as
|
||||
// tag or other output properties
|
||||
sanitizedAddr, err := c.sanitizedAddress()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
SanitizedAddress: sanitizedAddr,
|
||||
ConnectionDatabase: connectionDatabase(sanitizedAddr),
|
||||
maxIdle: c.MaxIdle,
|
||||
maxOpen: c.MaxOpen,
|
||||
maxLifetime: time.Duration(c.MaxLifetime),
|
||||
dsn: stdlib.RegisterConnConfig(connConfig),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// connectionDatabase determines the database to which the connection was made
|
||||
func connectionDatabase(sanitizedAddr string) string {
|
||||
connConfig, err := pgx.ParseConfig(sanitizedAddr)
|
||||
if err != nil || connConfig.Database == "" {
|
||||
return "postgres"
|
||||
}
|
||||
|
||||
return connConfig.Database
|
||||
}
|
||||
|
||||
// sanitizedAddress strips sensitive information from the connection string.
|
||||
// If the user set the output address use that before parsing anything else.
|
||||
func (c *Config) sanitizedAddress() (string, error) {
|
||||
if c.OutputAddress != "" {
|
||||
return c.OutputAddress, nil
|
||||
}
|
||||
|
||||
// Get the address
|
||||
addrSecret, err := c.Address.Get()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting address for sanitization failed: %w", err)
|
||||
}
|
||||
defer addrSecret.Destroy()
|
||||
|
||||
// Make sure we convert URI-formatted strings into key-values
|
||||
addr := addrSecret.TemporaryString()
|
||||
if strings.HasPrefix(addr, "postgres://") || strings.HasPrefix(addr, "postgresql://") {
|
||||
if addr, err = toKeyValue(addr); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize the string using a regular expression
|
||||
sanitized := sanitizer.ReplaceAllString(addr, "")
|
||||
return strings.TrimSpace(sanitized), nil
|
||||
}
|
||||
|
||||
// Based on parseURLSettings() at https://github.com/jackc/pgx/blob/master/pgconn/config.go
|
||||
func toKeyValue(uri string) (string, error) {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing URI failed: %w", err)
|
||||
}
|
||||
|
||||
// Check the protocol
|
||||
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
|
||||
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
|
||||
}
|
||||
|
||||
quoteIfNecessary := func(v string) string {
|
||||
if !strings.ContainsAny(v, ` ='\`) {
|
||||
return v
|
||||
}
|
||||
r := strings.ReplaceAll(v, `\`, `\\`)
|
||||
r = strings.ReplaceAll(r, `'`, `\'`)
|
||||
return "'" + r + "'"
|
||||
}
|
||||
|
||||
// Extract the parameters
|
||||
parts := make([]string, 0, len(u.Query())+5)
|
||||
if u.User != nil {
|
||||
parts = append(parts, "user="+quoteIfNecessary(u.User.Username()))
|
||||
if password, found := u.User.Password(); found {
|
||||
parts = append(parts, "password="+quoteIfNecessary(password))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle multiple host:port's in url.Host by splitting them into host,host,host and port,port,port.
|
||||
hostParts := strings.Split(u.Host, ",")
|
||||
hosts := make([]string, 0, len(hostParts))
|
||||
ports := make([]string, 0, len(hostParts))
|
||||
var anyPortSet bool
|
||||
for _, host := range hostParts {
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
h, p, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "missing port") {
|
||||
return "", fmt.Errorf("failed to process host %q: %w", host, err)
|
||||
}
|
||||
h = host
|
||||
}
|
||||
anyPortSet = anyPortSet || err == nil
|
||||
hosts = append(hosts, h)
|
||||
ports = append(ports, p)
|
||||
}
|
||||
if len(hosts) > 0 {
|
||||
parts = append(parts, "host="+strings.Join(hosts, ","))
|
||||
}
|
||||
if anyPortSet {
|
||||
parts = append(parts, "port="+strings.Join(ports, ","))
|
||||
}
|
||||
|
||||
database := strings.TrimLeft(u.Path, "/")
|
||||
if database != "" {
|
||||
parts = append(parts, "dbname="+quoteIfNecessary(database))
|
||||
}
|
||||
|
||||
for k, v := range u.Query() {
|
||||
parts = append(parts, k+"="+quoteIfNecessary(strings.Join(v, ",")))
|
||||
}
|
||||
|
||||
// Required to produce a repeatable output e.g. for tags or testing
|
||||
sort.Strings(parts)
|
||||
return strings.Join(parts, " "), nil
|
||||
}
|
240
plugins/common/postgresql/config_test.go
Normal file
240
plugins/common/postgresql/config_test.go
Normal file
|
@ -0,0 +1,240 @@
|
|||
package postgresql
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf/config"
|
||||
)
|
||||
|
||||
func TestURIParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uri string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "short",
|
||||
uri: `postgres://localhost`,
|
||||
expected: "host=localhost",
|
||||
},
|
||||
{
|
||||
name: "with port",
|
||||
uri: `postgres://localhost:5432`,
|
||||
expected: "host=localhost port=5432",
|
||||
},
|
||||
{
|
||||
name: "with database",
|
||||
uri: `postgres://localhost/mydb`,
|
||||
expected: "dbname=mydb host=localhost",
|
||||
},
|
||||
{
|
||||
name: "with additional parameters",
|
||||
uri: `postgres://localhost/mydb?application_name=pgxtest&search_path=myschema&connect_timeout=5`,
|
||||
expected: "application_name=pgxtest connect_timeout=5 dbname=mydb host=localhost search_path=myschema",
|
||||
},
|
||||
{
|
||||
name: "with database setting in params",
|
||||
uri: `postgres://localhost:5432/?database=mydb`,
|
||||
expected: "database=mydb host=localhost port=5432",
|
||||
},
|
||||
{
|
||||
name: "with authentication",
|
||||
uri: `postgres://jack:secret@localhost:5432/mydb?sslmode=prefer`,
|
||||
expected: "dbname=mydb host=localhost password=secret port=5432 sslmode=prefer user=jack",
|
||||
},
|
||||
{
|
||||
name: "with spaces",
|
||||
uri: `postgres://jack%20hunter:secret@localhost/mydb?application_name=pgx%20test`,
|
||||
expected: "application_name='pgx test' dbname=mydb host=localhost password=secret user='jack hunter'",
|
||||
},
|
||||
{
|
||||
name: "with equal signs",
|
||||
uri: `postgres://jack%20hunter:secret@localhost/mydb?application_name=pgx%3Dtest`,
|
||||
expected: "application_name='pgx=test' dbname=mydb host=localhost password=secret user='jack hunter'",
|
||||
},
|
||||
{
|
||||
name: "multiple hosts",
|
||||
uri: `postgres://jack:secret@foo:1,bar:2,baz:3/mydb?sslmode=disable`,
|
||||
expected: "dbname=mydb host=foo,bar,baz password=secret port=1,2,3 sslmode=disable user=jack",
|
||||
},
|
||||
{
|
||||
name: "multiple hosts without ports",
|
||||
uri: `postgres://jack:secret@foo,bar,baz/mydb?sslmode=disable`,
|
||||
expected: "dbname=mydb host=foo,bar,baz password=secret sslmode=disable user=jack",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
// Key value without spaces around equal sign
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual, err := toKeyValue(tt.uri)
|
||||
require.NoError(t, err)
|
||||
require.Equalf(t, tt.expected, actual, "initial: %s", tt.uri)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeAddressKeyValue(t *testing.T) {
|
||||
keys := []string{"password", "sslcert", "sslkey", "sslmode", "sslrootcert"}
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{
|
||||
name: "simple text",
|
||||
value: `foo`,
|
||||
},
|
||||
{
|
||||
name: "empty values",
|
||||
value: `''`,
|
||||
},
|
||||
{
|
||||
name: "space in value",
|
||||
value: `'foo bar'`,
|
||||
},
|
||||
{
|
||||
name: "equal sign in value",
|
||||
value: `'foo=bar'`,
|
||||
},
|
||||
{
|
||||
name: "escaped quote",
|
||||
value: `'foo\'s bar'`,
|
||||
},
|
||||
{
|
||||
name: "escaped quote no space",
|
||||
value: `\'foobar\'s\'`,
|
||||
},
|
||||
{
|
||||
name: "escaped backslash",
|
||||
value: `'foo bar\\'`,
|
||||
},
|
||||
{
|
||||
name: "escaped quote and backslash",
|
||||
value: `'foo\\\'s bar'`,
|
||||
},
|
||||
{
|
||||
name: "two escaped backslashes",
|
||||
value: `'foo bar\\\\'`,
|
||||
},
|
||||
{
|
||||
name: "multiple inline spaces",
|
||||
value: "'foo \t bar'",
|
||||
},
|
||||
{
|
||||
name: "leading space",
|
||||
value: `' foo bar'`,
|
||||
},
|
||||
{
|
||||
name: "trailing space",
|
||||
value: `'foo bar '`,
|
||||
},
|
||||
{
|
||||
name: "multiple equal signs",
|
||||
value: `'foo===bar'`,
|
||||
},
|
||||
{
|
||||
name: "leading equal sign",
|
||||
value: `'=foo bar'`,
|
||||
},
|
||||
{
|
||||
name: "trailing equal sign",
|
||||
value: `'foo bar='`,
|
||||
},
|
||||
{
|
||||
name: "mix of equal signs and spaces",
|
||||
value: "'foo = a\t===\tbar'",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
// Key value without spaces around equal sign
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Generate the DSN from the given keys and value
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+"="+tt.value)
|
||||
}
|
||||
dsn := strings.Join(parts, " canary=ok ")
|
||||
|
||||
cfg := &Config{
|
||||
Address: config.NewSecret([]byte(dsn)),
|
||||
}
|
||||
|
||||
expected := strings.Join(make([]string, len(keys)), "canary=ok ")
|
||||
expected = strings.TrimSpace(expected)
|
||||
actual, err := cfg.sanitizedAddress()
|
||||
require.NoError(t, err)
|
||||
require.Equalf(t, expected, actual, "initial: %s", dsn)
|
||||
})
|
||||
|
||||
// Key value with spaces around equal sign
|
||||
t.Run("spaced "+tt.name, func(t *testing.T) {
|
||||
// Generate the DSN from the given keys and value
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+" = "+tt.value)
|
||||
}
|
||||
dsn := strings.Join(parts, " canary=ok ")
|
||||
|
||||
cfg := &Config{
|
||||
Address: config.NewSecret([]byte(dsn)),
|
||||
}
|
||||
|
||||
expected := strings.Join(make([]string, len(keys)), "canary=ok ")
|
||||
expected = strings.TrimSpace(expected)
|
||||
actual, err := cfg.sanitizedAddress()
|
||||
require.NoError(t, err)
|
||||
require.Equalf(t, expected, actual, "initial: %s", dsn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeAddressURI(t *testing.T) {
|
||||
keys := []string{"password", "sslcert", "sslkey", "sslmode", "sslrootcert"}
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{
|
||||
name: "simple text",
|
||||
value: `foo`,
|
||||
},
|
||||
{
|
||||
name: "empty values",
|
||||
value: ``,
|
||||
},
|
||||
{
|
||||
name: "space in value",
|
||||
value: `foo bar`,
|
||||
},
|
||||
{
|
||||
name: "equal sign in value",
|
||||
value: `foo=bar`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Generate the DSN from the given keys and value
|
||||
value := strings.ReplaceAll(tt.value, "=", "%3D")
|
||||
value = strings.ReplaceAll(value, " ", "%20")
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
parts = append(parts, k+"="+value)
|
||||
}
|
||||
dsn := "postgresql://user:passwd@localhost:5432/db?" + strings.Join(parts, "&")
|
||||
|
||||
cfg := &Config{
|
||||
Address: config.NewSecret([]byte(dsn)),
|
||||
}
|
||||
|
||||
expected := "dbname=db host=localhost port=5432 user=user"
|
||||
actual, err := cfg.sanitizedAddress()
|
||||
require.NoError(t, err)
|
||||
require.Equalf(t, expected, actual, "initial: %s", dsn)
|
||||
})
|
||||
}
|
||||
}
|
42
plugins/common/postgresql/service.go
Normal file
42
plugins/common/postgresql/service.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package postgresql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
// Blank import required to register driver
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
// Service common functionality shared between the postgresql and postgresql_extensible
|
||||
// packages.
|
||||
type Service struct {
|
||||
DB *sql.DB
|
||||
SanitizedAddress string
|
||||
ConnectionDatabase string
|
||||
|
||||
dsn string
|
||||
maxIdle int
|
||||
maxOpen int
|
||||
maxLifetime time.Duration
|
||||
}
|
||||
|
||||
func (p *Service) Start() error {
|
||||
db, err := sql.Open("pgx", p.dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.DB = db
|
||||
|
||||
p.DB.SetMaxOpenConns(p.maxOpen)
|
||||
p.DB.SetMaxIdleConns(p.maxIdle)
|
||||
p.DB.SetConnMaxLifetime(p.maxLifetime)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Service) Stop() {
|
||||
if p.DB != nil {
|
||||
p.DB.Close()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue