mirror of https://github.com/dexidp/dex.git
14 changed files with 401 additions and 20 deletions
@ -0,0 +1,155 @@
|
||||
package ent |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/sha256" |
||||
"database/sql" |
||||
"fmt" |
||||
"net" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
// Register postgres driver.
|
||||
_ "github.com/lib/pq" |
||||
|
||||
entSQL "entgo.io/ent/dialect/sql" |
||||
"github.com/dexidp/dex/pkg/log" |
||||
"github.com/dexidp/dex/storage" |
||||
"github.com/dexidp/dex/storage/ent/client" |
||||
"github.com/dexidp/dex/storage/ent/db" |
||||
) |
||||
|
||||
// nolint
|
||||
const ( |
||||
// postgres SSL modes
|
||||
pgSSLDisable = "disable" |
||||
pgSSLRequire = "require" |
||||
pgSSLVerifyCA = "verify-ca" |
||||
pgSSLVerifyFull = "verify-full" |
||||
) |
||||
|
||||
// Postgres options for creating an SQL db.
|
||||
type Postgres struct { |
||||
NetworkDB |
||||
|
||||
SSL SSL `json:"ssl"` |
||||
} |
||||
|
||||
// Open always returns a new in sqlite3 storage.
|
||||
func (p *Postgres) Open(logger log.Logger) (storage.Storage, error) { |
||||
logger.Debug("experimental ent-based storage driver is enabled") |
||||
drv, err := p.driver() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
databaseClient := client.NewDatabase( |
||||
client.WithClient(db.NewClient(db.Driver(drv))), |
||||
client.WithHasher(sha256.New), |
||||
// The default behavior for Postgres transactions is consistent reads, not consistent writes.
|
||||
// For each transaction opened, ensure it has the correct isolation level.
|
||||
//
|
||||
// See: https://www.postgresql.org/docs/9.3/static/sql-set-transaction.html
|
||||
client.WithTxIsolationLevel(sql.LevelSerializable), |
||||
) |
||||
|
||||
if err := databaseClient.Schema().Create(context.TODO()); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return databaseClient, nil |
||||
} |
||||
|
||||
func (p *Postgres) driver() (*entSQL.Driver, error) { |
||||
drv, err := entSQL.Open("postgres", p.dsn()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// set database/sql tunables if configured
|
||||
if p.ConnMaxLifetime != 0 { |
||||
drv.DB().SetConnMaxLifetime(time.Duration(p.ConnMaxLifetime) * time.Second) |
||||
} |
||||
|
||||
if p.MaxIdleConns == 0 { |
||||
drv.DB().SetMaxIdleConns(5) |
||||
} else { |
||||
drv.DB().SetMaxIdleConns(p.MaxIdleConns) |
||||
} |
||||
|
||||
if p.MaxOpenConns == 0 { |
||||
drv.DB().SetMaxOpenConns(5) |
||||
} else { |
||||
drv.DB().SetMaxOpenConns(p.MaxOpenConns) |
||||
} |
||||
|
||||
return drv, nil |
||||
} |
||||
|
||||
func (p *Postgres) dsn() string { |
||||
// detect host:port for backwards-compatibility
|
||||
host, port, err := net.SplitHostPort(p.Host) |
||||
if err != nil { |
||||
// not host:port, probably unix socket or bare address
|
||||
host = p.Host |
||||
if p.Port != 0 { |
||||
port = strconv.Itoa(int(p.Port)) |
||||
} |
||||
} |
||||
|
||||
var parameters []string |
||||
addParam := func(key, val string) { |
||||
parameters = append(parameters, fmt.Sprintf("%s=%s", key, val)) |
||||
} |
||||
|
||||
addParam("connect_timeout", strconv.Itoa(p.ConnectionTimeout)) |
||||
|
||||
if host != "" { |
||||
addParam("host", dataSourceStr(host)) |
||||
} |
||||
|
||||
if port != "" { |
||||
addParam("port", port) |
||||
} |
||||
|
||||
if p.User != "" { |
||||
addParam("user", dataSourceStr(p.User)) |
||||
} |
||||
|
||||
if p.Password != "" { |
||||
addParam("password", dataSourceStr(p.Password)) |
||||
} |
||||
|
||||
if p.Database != "" { |
||||
addParam("dbname", dataSourceStr(p.Database)) |
||||
} |
||||
|
||||
if p.SSL.Mode == "" { |
||||
// Assume the strictest mode if unspecified.
|
||||
addParam("sslmode", dataSourceStr(pgSSLVerifyFull)) |
||||
} else { |
||||
addParam("sslmode", dataSourceStr(p.SSL.Mode)) |
||||
} |
||||
|
||||
if p.SSL.CAFile != "" { |
||||
addParam("sslrootcert", dataSourceStr(p.SSL.CAFile)) |
||||
} |
||||
|
||||
if p.SSL.CertFile != "" { |
||||
addParam("sslcert", dataSourceStr(p.SSL.CertFile)) |
||||
} |
||||
|
||||
if p.SSL.KeyFile != "" { |
||||
addParam("sslkey", dataSourceStr(p.SSL.KeyFile)) |
||||
} |
||||
|
||||
return strings.Join(parameters, " ") |
||||
} |
||||
|
||||
var strEsc = regexp.MustCompile(`([\\'])`) |
||||
|
||||
func dataSourceStr(str string) string { |
||||
return "'" + strEsc.ReplaceAllString(str, `\$1`) + "'" |
||||
} |
||||
@ -0,0 +1,183 @@
|
||||
package ent |
||||
|
||||
import ( |
||||
"os" |
||||
"strconv" |
||||
"testing" |
||||
|
||||
"github.com/sirupsen/logrus" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/dexidp/dex/storage" |
||||
"github.com/dexidp/dex/storage/conformance" |
||||
) |
||||
|
||||
func getenv(key, defaultVal string) string { |
||||
if val := os.Getenv(key); val != "" { |
||||
return val |
||||
} |
||||
return defaultVal |
||||
} |
||||
|
||||
func postgresTestConfig(host string, port uint64) *Postgres { |
||||
return &Postgres{ |
||||
NetworkDB: NetworkDB{ |
||||
Database: getenv("DEX_POSTGRES_DATABASE", "postgres"), |
||||
User: getenv("DEX_POSTGRES_USER", "postgres"), |
||||
Password: getenv("DEX_POSTGRES_PASSWORD", "postgres"), |
||||
Host: host, |
||||
Port: uint16(port), |
||||
}, |
||||
SSL: SSL{ |
||||
Mode: pgSSLDisable, // Postgres container doesn't support SSL.
|
||||
}, |
||||
} |
||||
} |
||||
|
||||
func newPostgresStorage(host string, port uint64) storage.Storage { |
||||
logger := &logrus.Logger{ |
||||
Out: os.Stderr, |
||||
Formatter: &logrus.TextFormatter{DisableColors: true}, |
||||
Level: logrus.DebugLevel, |
||||
} |
||||
|
||||
cfg := postgresTestConfig(host, port) |
||||
s, err := cfg.Open(logger) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return s |
||||
} |
||||
|
||||
func TestPostgres(t *testing.T) { |
||||
host := os.Getenv("DEX_POSTGRES_HOST") |
||||
if host == "" { |
||||
t.Skipf("test environment variable DEX_POSTGRES_HOST not set, skipping") |
||||
} |
||||
|
||||
port := uint64(5432) |
||||
if rawPort := os.Getenv("DEX_POSTGRES_PORT"); rawPort != "" { |
||||
var err error |
||||
|
||||
port, err = strconv.ParseUint(rawPort, 10, 32) |
||||
require.NoError(t, err, "invalid postgres port %q: %s", rawPort, err) |
||||
} |
||||
|
||||
newStorage := func() storage.Storage { |
||||
return newPostgresStorage(host, port) |
||||
} |
||||
conformance.RunTests(t, newStorage) |
||||
conformance.RunTransactionTests(t, newStorage) |
||||
} |
||||
|
||||
func TestPostgresDSN(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
cfg *Postgres |
||||
desiredDSN string |
||||
}{ |
||||
{ |
||||
name: "Host port", |
||||
cfg: &Postgres{ |
||||
NetworkDB: NetworkDB{ |
||||
Host: "localhost", |
||||
Port: uint16(5432), |
||||
}, |
||||
}, |
||||
desiredDSN: "connect_timeout=0 host='localhost' port=5432 sslmode='verify-full'", |
||||
}, |
||||
{ |
||||
name: "Host with port", |
||||
cfg: &Postgres{ |
||||
NetworkDB: NetworkDB{ |
||||
Host: "localhost:5432", |
||||
}, |
||||
}, |
||||
desiredDSN: "connect_timeout=0 host='localhost' port=5432 sslmode='verify-full'", |
||||
}, |
||||
{ |
||||
name: "Host ipv6 with port", |
||||
cfg: &Postgres{ |
||||
NetworkDB: NetworkDB{ |
||||
Host: "[a:b:c:d]:5432", |
||||
}, |
||||
}, |
||||
desiredDSN: "connect_timeout=0 host='a:b:c:d' port=5432 sslmode='verify-full'", |
||||
}, |
||||
{ |
||||
name: "Credentials and timeout", |
||||
cfg: &Postgres{ |
||||
NetworkDB: NetworkDB{ |
||||
Database: "test", |
||||
User: "test", |
||||
Password: "test", |
||||
ConnectionTimeout: 5, |
||||
}, |
||||
}, |
||||
desiredDSN: "connect_timeout=5 user='test' password='test' dbname='test' sslmode='verify-full'", |
||||
}, |
||||
{ |
||||
name: "SSL", |
||||
cfg: &Postgres{ |
||||
SSL: SSL{ |
||||
Mode: pgSSLRequire, |
||||
CAFile: "/ca.crt", |
||||
KeyFile: "/cert.crt", |
||||
CertFile: "/cert.key", |
||||
}, |
||||
}, |
||||
desiredDSN: "connect_timeout=0 sslmode='require' sslrootcert='/ca.crt' sslcert='/cert.key' sslkey='/cert.crt'", |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
require.Equal(t, tt.desiredDSN, tt.cfg.dsn()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestPostgresDriver(t *testing.T) { |
||||
host := os.Getenv("DEX_POSTGRES_HOST") |
||||
if host == "" { |
||||
t.Skipf("test environment variable DEX_POSTGRES_HOST not set, skipping") |
||||
} |
||||
|
||||
port := uint64(5432) |
||||
if rawPort := os.Getenv("DEX_POSTGRES_PORT"); rawPort != "" { |
||||
var err error |
||||
|
||||
port, err = strconv.ParseUint(rawPort, 10, 32) |
||||
require.NoError(t, err, "invalid postgres port %q: %s", rawPort, err) |
||||
} |
||||
|
||||
tests := []struct { |
||||
name string |
||||
cfg func() *Postgres |
||||
desiredConns int |
||||
}{ |
||||
{ |
||||
name: "Defaults", |
||||
cfg: func() *Postgres { return postgresTestConfig(host, port) }, |
||||
desiredConns: 5, |
||||
}, |
||||
{ |
||||
name: "Tune", |
||||
cfg: func() *Postgres { |
||||
cfg := postgresTestConfig(host, port) |
||||
cfg.MaxOpenConns = 101 |
||||
return cfg |
||||
}, |
||||
desiredConns: 101, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
drv, err := tt.cfg().driver() |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, tt.desiredConns, drv.DB().Stats().MaxOpenConnections) |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
|
||||
package ent |
||||
|
||||
// NetworkDB contains options common to SQL databases accessed over network.
|
||||
type NetworkDB struct { |
||||
Database string |
||||
User string |
||||
Password string |
||||
Host string |
||||
Port uint16 |
||||
|
||||
ConnectionTimeout int // Seconds
|
||||
|
||||
MaxOpenConns int // default: 5
|
||||
MaxIdleConns int // default: 5
|
||||
ConnMaxLifetime int // Seconds, default: not set
|
||||
} |
||||
|
||||
// SSL represents SSL options for network databases.
|
||||
type SSL struct { |
||||
Mode string |
||||
CAFile string |
||||
// Files for client auth.
|
||||
KeyFile string |
||||
CertFile string |
||||
} |
||||
Loading…
Reference in new issue