Skip to content

Commit

Permalink
Merge pull request #9 from cased/db-uri
Browse files Browse the repository at this point in the history
Refactor db option
  • Loading branch information
tnm authored Feb 12, 2025
2 parents 9ee6cc5 + 5a51ee7 commit 76f2fc6
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 125 deletions.
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ HubProxy uses [SQLC](https://sqlc.dev/) to generate type-safe Go code from SQL q
SQLite is used by default for development, but PostgreSQL or MySQL are recommended for production:
```bash
# SQLite (default for development)
hubproxy --db sqlite --db-dsn ".cache/hubproxy.db"
hubproxy --db sqlite:.cache/hubproxy.db

# PostgreSQL
hubproxy --db postgres --db-dsn "postgres://user:password@localhost:5432/hubproxy?sslmode=disable"
hubproxy --db "postgres://user:password@localhost:5432/hubproxy?sslmode=disable"

# MySQL
hubproxy --db mysql --db-dsn "user:password@tcp(localhost:3306)/hubproxy"
hubproxy --db "mysql://user:password@tcp(localhost:3306)/hubproxy"
```

### Schema
Expand Down Expand Up @@ -518,8 +518,9 @@ ts-authkey: ""
ts-hostname: hubproxy

# Database configuration
db-type: sqlite # sqlite, mysql, or postgres
db-dsn: hubproxy.db
db: sqlite:hubproxy.db
# db: mysql://user:pass@host/db
# db: postgres://user:pass@host/db
```

To use a configuration file, specify its path with the `--config` flag:
Expand All @@ -539,11 +540,7 @@ Most configuration options can also be set via command-line flags:
- `--validate-ip`: Validate that requests come from GitHub IPs
- `--ts-authkey`: Tailscale auth key for tsnet
- `--ts-hostname`: Tailscale hostname
- `--db`: Database type (sqlite, mysql, postgres)
- `--db-dsn`: Database connection string
- SQLite: path to file
- MySQL: user:pass@tcp(host:port)/dbname
- Postgres: postgres://user:pass@host:port/dbname
- `--db`: Database URI (e.g., sqlite:file.db, mysql://user:pass@host/db, postgresql://user:pass@host/db)

Command-line flags take precedence over values in the configuration file.

Expand Down
124 changes: 13 additions & 111 deletions cmd/hubproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import (

"hubproxy/internal/api"
"hubproxy/internal/storage"
"hubproxy/internal/storage/sql/mysql"
"hubproxy/internal/storage/sql/postgres"
"hubproxy/internal/storage/sql/sqlite"
"hubproxy/internal/storage/factory"
"hubproxy/internal/webhook"
"log/slog"

Expand Down Expand Up @@ -68,8 +66,7 @@ and your target services.`,
flags.Bool("validate-ip", true, "Validate that requests come from GitHub IPs")
flags.String("ts-authkey", "", "Tailscale auth key for tsnet")
flags.String("ts-hostname", "hubproxy", "Tailscale hostname (will be <hostname>.<tailnet>.ts.net)")
flags.String("db-type", "sqlite", "Database type (sqlite, mysql, postgres)")
flags.String("db-dsn", "hubproxy.db", "Database DSN (connection string)")
flags.String("db", "", "Database URI (e.g., sqlite:hubproxy.db, mysql://user:pass@host/db, postgres://user:pass@host/db)")
flags.Bool("test-mode", false, "Skip server startup for testing")

return cmd
Expand Down Expand Up @@ -115,42 +112,20 @@ func run() error {
logger.Info("running in log-only mode (no target URL specified)")
}

// Initialize storage
// Initialize storage if DB URI is provided
var store storage.Storage
var storageErr error
switch viper.GetString("db-type") {
case "sqlite":
store, storageErr = sqlite.NewStorage(viper.GetString("db-dsn"))
case "mysql":
var mysqlCfg storage.Config
mysqlCfg, storageErr = parseMySQLDSN(viper.GetString("db-dsn"))
if storageErr != nil {
return fmt.Errorf("invalid MySQL DSN: %w", storageErr)
}
store, storageErr = mysql.NewStorage(mysqlCfg)
if storageErr != nil {
return fmt.Errorf("failed to initialize MySQL storage: %w", storageErr)
}
case "postgres":
var pgCfg storage.Config
pgCfg, storageErr = parsePostgresDSN(viper.GetString("db-dsn"))
if storageErr != nil {
return fmt.Errorf("invalid Postgres DSN: %w", storageErr)
}
store, storageErr = postgres.NewStorage(pgCfg)
if storageErr != nil {
return fmt.Errorf("failed to initialize Postgres storage: %w", storageErr)
dbURI := viper.GetString("db")
if dbURI != "" {
var err error
store, err = factory.NewStorageFromURI(dbURI)
if err != nil {
return fmt.Errorf("failed to initialize storage: %w", err)
}
default:
return fmt.Errorf("unsupported database type: %s", viper.GetString("db-type"))
}
if storageErr != nil {
return fmt.Errorf("failed to initialize storage: %w", storageErr)
}
defer store.Close()
defer store.Close()

if err := store.CreateSchema(context.Background()); err != nil {
return fmt.Errorf("failed to create schema: %w", err)
if err := store.CreateSchema(context.Background()); err != nil {
return fmt.Errorf("failed to create schema: %w", err)
}
}

// Create webhook handler
Expand Down Expand Up @@ -234,76 +209,3 @@ func main() {
os.Exit(1)
}
}

// parseMySQLDSN parses MySQL DSN into Config
// Format: user:pass@tcp(host:port)/dbname
func parseMySQLDSN(dsn string) (storage.Config, error) {
// Extract username and password
parts := strings.Split(dsn, "@")
if len(parts) != 2 {
return storage.Config{}, fmt.Errorf("invalid MySQL DSN format")
}
userPass := parts[0]
credentials := strings.Split(userPass, ":")
if len(credentials) != 2 {
return storage.Config{}, fmt.Errorf("invalid MySQL DSN format")
}
username := credentials[0]
password := credentials[1]

// Extract host and port from tcp(host:port)
remainder := parts[1]
tcpParts := strings.Split(remainder, ")/")
if len(tcpParts) != 2 {
return storage.Config{}, fmt.Errorf("invalid MySQL DSN format")
}

hostPort := strings.TrimPrefix(tcpParts[0], "tcp(")
hostPortParts := strings.Split(hostPort, ":")
if len(hostPortParts) != 2 {
return storage.Config{}, fmt.Errorf("invalid MySQL DSN format")
}
host := hostPortParts[0]
var port int
if _, err := fmt.Sscanf(hostPortParts[1], "%d", &port); err != nil {
return storage.Config{}, fmt.Errorf("parsing port: %w", err)
}

// Extract database name
database := strings.Split(tcpParts[1], "?")[0]

return storage.Config{
Host: host,
Port: port,
Database: database,
Username: username,
Password: password,
}, nil
}

// parsePostgresDSN parses Postgres DSN into Config
// Format: postgres://user:pass@host:port/dbname
func parsePostgresDSN(dsn string) (storage.Config, error) {
u, err := url.Parse(dsn)
if err != nil {
return storage.Config{}, fmt.Errorf("parsing PostgreSQL DSN: %w", err)
}

password, _ := u.User.Password()
var port int
if u.Port() != "" {
if _, err := fmt.Sscanf(u.Port(), "%d", &port); err != nil {
return storage.Config{}, fmt.Errorf("parsing port: %w", err)
}
} else {
port = 5432 // default postgres port
}

return storage.Config{
Host: u.Hostname(),
Port: port,
Database: strings.TrimPrefix(u.Path, "/"),
Username: u.User.Username(),
Password: password,
}, nil
}
5 changes: 3 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ ts-authkey: ""
ts-hostname: hubproxy

# Database configuration
db-type: sqlite # sqlite, mysql, or postgres
db-dsn: hubproxy.db
db: sqlite:hubproxy.db
# db: mysql://user:pass@host/db
# db: postgres://user:pass@host/db
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ require (
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/dburl v0.23.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/dburl v0.23.3 h1:s9tUyKAkcgRfNQ7ut5gaDWC9s5ROafY3hmNOrGbNXtE=
github.com/xo/dburl v0.23.3/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
Expand Down
56 changes: 56 additions & 0 deletions internal/storage/factory/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package factory

import (
"fmt"
"strings"

"github.com/xo/dburl"

"hubproxy/internal/storage"
"hubproxy/internal/storage/sql/mysql"
"hubproxy/internal/storage/sql/postgres"
"hubproxy/internal/storage/sql/sqlite"
)

// NewStorageFromURI creates a new storage instance from a database URI.
// The URI format follows the dburl package conventions:
// - SQLite: sqlite:/path/to/file.db or sqlite:file.db
// - MySQL: mysql://user:pass@host/dbname
// - PostgreSQL: postgres://user:pass@host/dbname
func NewStorageFromURI(uri string) (storage.Storage, error) {
u, err := dburl.Parse(uri)
if err != nil {
return nil, fmt.Errorf("invalid database URL: %w", err)
}

cfg := storage.Config{
Host: u.Hostname(),
Database: strings.TrimPrefix(u.Path, "/"),
Username: u.User.Username(),
}
if password, ok := u.User.Password(); ok {
cfg.Password = password
}
if u.Port() != "" {
if _, err := fmt.Sscanf(u.Port(), "%d", &cfg.Port); err != nil {
return nil, fmt.Errorf("parsing port: %w", err)
}
}

switch u.Driver {
case "sqlite3", "sqlite":
return sqlite.NewStorage(strings.TrimPrefix(u.DSN, "file:"))
case "mysql":
if cfg.Port == 0 {
cfg.Port = 3306 // default MySQL port
}
return mysql.NewStorage(cfg)
case "postgres", "postgresql":
if cfg.Port == 0 {
cfg.Port = 5432 // default PostgreSQL port
}
return postgres.NewStorage(cfg)
default:
return nil, fmt.Errorf("unsupported database type: %s", u.Driver)
}
}
3 changes: 1 addition & 2 deletions tools/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ fi

go run cmd/hubproxy/main.go \
--target-url "$TARGET_URL" \
--db-type sqlite \
--db-dsn "$DB_PATH" \
--db "sqlite:$DB_PATH" \
--validate-ip=false \
--log-level debug

0 comments on commit 76f2fc6

Please sign in to comment.