mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(database): connect postgres via socket and read config from env _FILE secrets (#2061)
* feat(database): connect postgres via socket * feat(config): read env var secrets from file * docs: explain env var secrets * refactor: generate postgres dsn
This commit is contained in:
parent
24648e45f7
commit
fe4f385a22
9 changed files with 345 additions and 76 deletions
|
@ -6,8 +6,8 @@ package database
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/domain"
|
||||
|
@ -35,13 +35,33 @@ type DB struct {
|
|||
|
||||
func NewDB(cfg *domain.Config, log logger.Logger) (*DB, error) {
|
||||
db := &DB{
|
||||
// set default placeholder for squirrel to support both sqlite and postgres
|
||||
// set a default placeholder for squirrel to support both sqlite and postgres
|
||||
squirrel: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
|
||||
log: log.With().Str("module", "database").Str("type", cfg.DatabaseType).Logger(),
|
||||
cfg: cfg,
|
||||
}
|
||||
db.ctx, db.cancel = context.WithCancel(context.Background())
|
||||
|
||||
// Check for directly configured DSN in config
|
||||
if cfg.DatabaseDSN != "" {
|
||||
if strings.HasPrefix(cfg.DatabaseDSN, "postgres://") || strings.HasPrefix(cfg.DatabaseDSN, "postgresql://") {
|
||||
db.Driver = "postgres"
|
||||
db.DSN = cfg.DatabaseDSN
|
||||
return db, nil
|
||||
} else if strings.HasPrefix(cfg.DatabaseDSN, "file:") || cfg.DatabaseDSN == ":memory:" || strings.HasSuffix(cfg.DatabaseDSN, ".db") {
|
||||
db.Driver = "sqlite"
|
||||
if strings.HasPrefix(cfg.DatabaseDSN, "file:") && strings.HasSuffix(cfg.DatabaseDSN, ".db") {
|
||||
db.DSN = strings.TrimPrefix(cfg.DatabaseDSN, "file:")
|
||||
} else {
|
||||
db.DSN = cfg.DatabaseDSN
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported database DSN: %s", cfg.DatabaseDSN)
|
||||
}
|
||||
|
||||
// If no direct DSN is provided, build it from individual settings
|
||||
switch cfg.DatabaseType {
|
||||
case "sqlite":
|
||||
db.Driver = "sqlite"
|
||||
|
@ -51,14 +71,19 @@ func NewDB(cfg *domain.Config, log logger.Logger) (*DB, error) {
|
|||
db.DSN = dataSourceName(cfg.ConfigPath, "autobrr.db")
|
||||
}
|
||||
case "postgres":
|
||||
if cfg.PostgresHost == "" || cfg.PostgresPort == 0 || cfg.PostgresDatabase == "" {
|
||||
return nil, errors.New("postgres: bad variables")
|
||||
}
|
||||
db.DSN = fmt.Sprintf("postgres://%v:%v@%v:%d/%v?sslmode=%v", cfg.PostgresUser, cfg.PostgresPass, cfg.PostgresHost, cfg.PostgresPort, cfg.PostgresDatabase, cfg.PostgresSSLMode)
|
||||
if cfg.PostgresExtraParams != "" {
|
||||
db.DSN = fmt.Sprintf("%s&%s", db.DSN, cfg.PostgresExtraParams)
|
||||
}
|
||||
db.Driver = "postgres"
|
||||
|
||||
// If no database-specific settings are provided, return an error
|
||||
if cfg.PostgresDatabase == "" && cfg.DatabaseDSN == "" {
|
||||
return nil, errors.New("postgres: database name is required")
|
||||
}
|
||||
|
||||
pgDsn, err := PostgresDSN(cfg.PostgresHost, cfg.PostgresPort, cfg.PostgresUser, cfg.PostgresPass, cfg.PostgresDatabase, cfg.PostgresSocket, cfg.PostgresSSLMode, cfg.PostgresExtraParams)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "postgres: failed to build DSN")
|
||||
}
|
||||
db.DSN = pgDsn
|
||||
|
||||
default:
|
||||
return nil, errors.New("unsupported database: %v", cfg.DatabaseType)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ package database
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/autobrr/autobrr/pkg/errors"
|
||||
|
||||
|
@ -87,3 +90,56 @@ func (db *DB) migratePostgres() error {
|
|||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// PostgresDSN build postgres dsn connect string
|
||||
func PostgresDSN(host string, port int, user, pass, database, socket, sslMode, extraParams string) (string, error) {
|
||||
// If no database is provided, return an error
|
||||
if database == "" {
|
||||
return "", errors.New("postgres: database name is required")
|
||||
}
|
||||
|
||||
pgDsn, err := url.Parse("postgres://")
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not parse postgres DSN")
|
||||
}
|
||||
|
||||
pgDsn.Path = database
|
||||
if user != "" {
|
||||
pgDsn.User = url.UserPassword(user, pass)
|
||||
}
|
||||
queryParams := pgDsn.Query()
|
||||
|
||||
// Build DSN based on the connection type (TCP vs. Unix socket)
|
||||
if socket != "" {
|
||||
// Unix socket connection via the host param
|
||||
queryParams.Add("host", socket)
|
||||
} else {
|
||||
// TCP connection
|
||||
if host == "" && port == 0 {
|
||||
return "", errors.New("postgres: host and port are required for TCP connection")
|
||||
}
|
||||
if port > 0 {
|
||||
pgDsn.Host = net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
} else {
|
||||
pgDsn.Host = database
|
||||
}
|
||||
}
|
||||
|
||||
// Add SSL mode if provided
|
||||
if sslMode != "" {
|
||||
queryParams.Add("sslmode", sslMode)
|
||||
}
|
||||
|
||||
pgDsn.RawQuery = queryParams.Encode()
|
||||
|
||||
// Add any extra parameters
|
||||
if extraParams != "" {
|
||||
values, err := url.ParseQuery(extraParams)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not parse extra params")
|
||||
}
|
||||
pgDsn.RawQuery = fmt.Sprintf("%s&%s", pgDsn.RawQuery, values.Encode())
|
||||
}
|
||||
|
||||
return pgDsn.String(), nil
|
||||
}
|
||||
|
|
67
internal/database/postgres_test.go
Normal file
67
internal/database/postgres_test.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPostgresDSN(t *testing.T) {
|
||||
type args struct {
|
||||
host string
|
||||
port int
|
||||
user string
|
||||
pass string
|
||||
database string
|
||||
socket string
|
||||
sslMode string
|
||||
extraParams string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
args: args{
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
user: "postgres",
|
||||
pass: "PASSWORD",
|
||||
database: "postgres",
|
||||
sslMode: "disable",
|
||||
socket: "",
|
||||
},
|
||||
want: "postgres://postgres:PASSWORD@localhost:5432/postgres?sslmode=disable",
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
args: args{
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
user: "postgres",
|
||||
pass: "PASSWORD",
|
||||
database: "postgres",
|
||||
sslMode: "disable",
|
||||
extraParams: "connect_timeout=10",
|
||||
socket: "",
|
||||
},
|
||||
want: "postgres://postgres:PASSWORD@localhost:5432/postgres?sslmode=disable&connect_timeout=10",
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
args: args{
|
||||
database: "postgres",
|
||||
socket: "/path/to/socket",
|
||||
},
|
||||
want: "postgres://postgres?host=%2Fpath%2Fto%2Fsocket",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, _ := PostgresDSN(tt.args.host, tt.args.port, tt.args.user, tt.args.pass, tt.args.database, tt.args.socket, tt.args.sslMode, tt.args.extraParams)
|
||||
assert.Equalf(t, tt.want, got, "PostgresDSN(%v, %v, %v, %v, %v, %v, %v, %v)", tt.args.host, tt.args.port, tt.args.user, tt.args.pass, tt.args.database, tt.args.socket, tt.args.sslMode, tt.args.extraParams)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue