From fadf2ac8d3e5b278fd58ab4d4d804f1c4d7b4eb2 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Sat, 14 Mar 2026 22:47:47 +0100 Subject: [PATCH] feat(db): split cli args for postgres Add support of `COCOON_DB_HOST`, `COCOON_DB_USER`, `COCOON_DB_PASSWORD`, `COCOON_DB_PORT` and `COCOON_DB_NAME` for postgres. Do not remove `COCOON_DATABASE_URL` to avoid creating a breaking change. Remove volume mouting scripts in docker-compose*.yaml following 0756641e36a8fb7075fc8376ccfa3ad7e5c7071a. --- README.md | 12 ++++-- cmd/cocoon/main.go | 66 ++++++++++++++++++++----------- docker-compose.noproxy.yaml | 2 - docker-compose.postgres.yaml | 3 +- docker-compose.yaml | 2 - server/db.go | 75 ++++++++++++++++++++++++++++++++++++ server/server.go | 38 +++--------------- 7 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 server/db.go diff --git a/README.md b/README.md index 5cfe1ce..60dd641 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Cocoon is a PDS implementation in Go. It is highly experimental, and is not read **For PostgreSQL deployment:** ```bash - # Add POSTGRES_PASSWORD to your .env file first! + # See database configuration first! docker-compose -f docker-compose.postgres.yaml up -d ``` @@ -114,8 +114,14 @@ COCOON_DB_TYPE="postgres" # Format: postgres://user:password@host:port/database?sslmode=disable COCOON_DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable" -# Or use the standard DATABASE_URL environment variable -DATABASE_URL="postgres://cocoon:password@localhost:5432/cocoon?sslmode=disable" +# You can also specify them with: +COCOON_DB_HOST="localhost" +COCOON_DB_USER="cocoon" +COCOON_DB_PASSWORD="password" +COCOON_DB_PORT=5432 +COCOON_DB_NAME="cocoon" +# if you want to set custom DSN option like timezone or sslmode (if nothing is set, sslmode is disabled) +COCOON_DB_CUSTOM="" ``` For SQLite (default): diff --git a/cmd/cocoon/main.go b/cmd/cocoon/main.go index e10606f..d05a9b7 100644 --- a/cmd/cocoon/main.go +++ b/cmd/cocoon/main.go @@ -20,8 +20,6 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/urfave/cli/v2" "golang.org/x/crypto/bcrypt" - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -54,6 +52,33 @@ func main() { Usage: "PostgreSQL connection string (required if db-type is postgres)", EnvVars: []string{"COCOON_DATABASE_URL", "DATABASE_URL"}, }, + &cli.StringFlag{ + Name: "db-host", + Usage: "PostgreSQL host (required if db-type is postgres and if database-url is not set)", + EnvVars: []string{"COCOON_DB_HOST"}, + }, + &cli.StringFlag{ + Name: "db-user", + Usage: "PostgreSQL user (required if db-type is postgres and if database-url is not set)", + EnvVars: []string{"COCOON_DB_USER"}, + }, + &cli.StringFlag{ + Name: "db-password", + Usage: "PostgreSQL password (required if db-type is postgres and if database-url is not set)", + EnvVars: []string{"COCOON_DB_PASSWORD"}, + }, + &cli.UintFlag{ + Name: "db-port", + Value: 5432, + Usage: "PostgreSQL port (default to 5432, only has effect if db-type is postgres)", + EnvVars: []string{"COCOON_DB_PORT"}, + }, + &cli.StringFlag{ + Name: "db-custom", + Value: "sslmode=disable", + Usage: "PostgreSQL custom DSN (default to sslmode=disable, only has effect if db-type is postgres)", + EnvVars: []string{"COCOON_DB_CUSTOM"}, + }, &cli.StringFlag{ Name: "did", EnvVars: []string{"COCOON_DID"}, @@ -215,9 +240,6 @@ var runServe = &cli.Command{ Logger: logger, LogLevel: level, Addr: cmd.String("addr"), - DbName: cmd.String("db-name"), - DbType: cmd.String("db-type"), - DatabaseURL: cmd.String("database-url"), Did: cmd.String("did"), Hostname: cmd.String("hostname"), RotationKeyPath: cmd.String("rotation-key-path"), @@ -233,6 +255,7 @@ var runServe = &cli.Command{ SmtpPort: cmd.String("smtp-port"), SmtpEmail: cmd.String("smtp-email"), SmtpName: cmd.String("smtp-name"), + DBConfig: newDbArgs(cmd), S3Config: &server.S3Config{ BackupsEnabled: cmd.Bool("s3-backups-enabled"), BlobstoreEnabled: cmd.Bool("s3-blobstore-enabled"), @@ -409,24 +432,23 @@ var runResetPassword = &cli.Command{ }, } -func newDb(cmd *cli.Context) (*gorm.DB, error) { - dbType := cmd.String("db-type") - if dbType == "" { - dbType = "sqlite" +func newDbArgs(cmd *cli.Context) *server.DBConfig { + return &server.DBConfig{ + Name: cmd.String("db-name"), + Type: cmd.String("db-type"), + URL: cmd.String("database-url"), + Host: cmd.String("db-host"), + User: cmd.String("db-user"), + Password: cmd.String("db-password"), + Port: cmd.Uint("db-port"), + Custom: cmd.String("db-custom"), } +} - switch dbType { - case "postgres": - databaseURL := cmd.String("database-url") - if databaseURL == "" { - return nil, fmt.Errorf("COCOON_DATABASE_URL or DATABASE_URL must be set when using postgres") - } - return gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) - default: - dbName := cmd.String("db-name") - if dbName == "" { - dbName = "cocoon.db" - } - return gorm.Open(sqlite.Open(dbName), &gorm.Config{}) +func newDb(cmd *cli.Context) (*gorm.DB, error) { + db := newDbArgs(cmd) + if db.Type == "" { + db.Type = "sqlite" } + return db.Connect(nil) } diff --git a/docker-compose.noproxy.yaml b/docker-compose.noproxy.yaml index da0e7a6..ab61245 100644 --- a/docker-compose.noproxy.yaml +++ b/docker-compose.noproxy.yaml @@ -17,7 +17,6 @@ services: volumes: - ./keys:/keys - ./data:/data/cocoon - - ./init-keys.sh:/init-keys.sh:ro environment: COCOON_DID: ${COCOON_DID} COCOON_HOSTNAME: ${COCOON_HOSTNAME} @@ -100,7 +99,6 @@ services: volumes: - ./keys:/keys - ./data:/data/cocoon - - ./create-initial-invite.sh:/create-initial-invite.sh:ro environment: COCOON_DID: ${COCOON_DID} COCOON_HOSTNAME: ${COCOON_HOSTNAME} diff --git a/docker-compose.postgres.yaml b/docker-compose.postgres.yaml index eafc809..c85ca93 100644 --- a/docker-compose.postgres.yaml +++ b/docker-compose.postgres.yaml @@ -37,7 +37,6 @@ services: volumes: - ./keys:/keys - ./data:/data/cocoon - - ./init-keys.sh:/init-keys.sh:ro environment: COCOON_DID: ${COCOON_DID} COCOON_HOSTNAME: ${COCOON_HOSTNAME} @@ -118,7 +117,7 @@ services: container_name: cocoon-create-invite volumes: - ./keys:/keys - - ./create-initial-invite.sh:/create-initial-invite.sh:ro + - ./data:/data/cocoon environment: COCOON_DID: ${COCOON_DID} COCOON_HOSTNAME: ${COCOON_HOSTNAME} diff --git a/docker-compose.yaml b/docker-compose.yaml index c5eb5e3..bba9744 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,6 @@ services: volumes: - ./keys:/keys - ./data:/data/cocoon - - ./init-keys.sh:/init-keys.sh:ro environment: COCOON_DID: ${COCOON_DID} COCOON_HOSTNAME: ${COCOON_HOSTNAME} @@ -92,7 +91,6 @@ services: volumes: - ./keys:/keys - ./data:/data/cocoon - - ./create-initial-invite.sh:/create-initial-invite.sh:ro environment: COCOON_DID: ${COCOON_DID} COCOON_HOSTNAME: ${COCOON_HOSTNAME} diff --git a/server/db.go b/server/db.go new file mode 100644 index 0000000..2003d92 --- /dev/null +++ b/server/db.go @@ -0,0 +1,75 @@ +package server + +import ( + "fmt" + "log/slog" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type DBConfig struct { + // Name is the sqlite file or the name of the postgres database + Name string + Type string + URL string + User string + Password string + Host string + Port uint + Custom string +} + +func (cfg *DBConfig) Connect(logger *slog.Logger) (*gorm.DB, error) { + dbType := cfg.Type + if dbType == "" { + dbType = "sqlite" + } + + var gdb *gorm.DB + var err error + switch dbType { + case "postgres": + var dsn string + if cfg.URL != "" { + dsn = cfg.URL + } else if cfg.Host != "" && + cfg.User != "" && + cfg.Password != "" && + cfg.Name != "" { + dsn = fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s %s", + cfg.Host, + cfg.Port, + cfg.User, + cfg.Password, + cfg.Name, + cfg.Custom, + ) + } else { + return nil, fmt.Errorf("database config must be set when using postgres") + } + gdb, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to postgres: %w", err) + } + if logger != nil { + logger.Info("connected to PostgreSQL database", "host", cfg.Host, "dbname", cfg.Name) + } + case "sqlite": + gdb, err = gorm.Open(sqlite.Open(cfg.Name), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to open sqlite database: %w", err) + } + gdb.Exec("PRAGMA journal_mode=WAL") + gdb.Exec("PRAGMA synchronous=NORMAL") + + if logger != nil { + logger.Info("connected to SQLite database", "path", cfg.Name) + } + default: + panic("UNSUPPORTED DB TYPE") + } + return gdb, nil +} diff --git a/server/server.go b/server/server.go index f92b15b..ac509a9 100644 --- a/server/server.go +++ b/server/server.go @@ -44,9 +44,6 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" slogecho "github.com/samber/slog-echo" - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" ) const ( @@ -94,9 +91,6 @@ type Args struct { LogLevel slog.Level Addr string - DbName string - DbType string - DatabaseURL string Version string Did string Hostname string @@ -114,6 +108,7 @@ type Args struct { SmtpEmail string SmtpName string + DBConfig *DBConfig S3Config *S3Config SessionSecret string @@ -253,7 +248,7 @@ func New(args *Args) (*Server, error) { return nil, fmt.Errorf("addr must be set") } - if args.DbName == "" { + if args.DBConfig.Name == "" { return nil, fmt.Errorf("db name must be set") } @@ -332,32 +327,9 @@ func New(args *Args) (*Server, error) { IdleTimeout: 5 * time.Minute, } - dbType := args.DbType - if dbType == "" { - dbType = "sqlite" - } - - var gdb *gorm.DB - var err error - switch dbType { - case "postgres": - if args.DatabaseURL == "" { - return nil, fmt.Errorf("database-url must be set when using postgres") - } - gdb, err = gorm.Open(postgres.Open(args.DatabaseURL), &gorm.Config{}) - if err != nil { - return nil, fmt.Errorf("failed to connect to postgres: %w", err) - } - logger.Info("connected to PostgreSQL database") - default: - gdb, err = gorm.Open(sqlite.Open(args.DbName), &gorm.Config{}) - if err != nil { - return nil, fmt.Errorf("failed to open sqlite database: %w", err) - } - gdb.Exec("PRAGMA journal_mode=WAL") - gdb.Exec("PRAGMA synchronous=NORMAL") - - logger.Info("connected to SQLite database", "path", args.DbName) + gdb, err := args.DBConfig.Connect(logger) + if err != nil { + return nil, err } dbw := db.NewDB(gdb)