diff --git a/.env.example b/.env.example index 6c4adec6..5eeaf6bb 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,10 @@ PGUSER=your_username # Database password PGPASSWORD=your_password_here +# SSL mode for database connection (default: prefer) +# Valid values: disable, allow, prefer, require, verify-ca, verify-full +#PGSSLMODE=prefer + # Application name for database connection (default: pgschema) # This appears in pg_stat_activity and can help identify connections PGAPPNAME=pgschema @@ -40,4 +44,7 @@ PGAPPNAME=pgschema #PGSCHEMA_PLAN_USER=postgres # Plan database password -#PGSCHEMA_PLAN_PASSWORD=your_plan_db_password \ No newline at end of file +#PGSCHEMA_PLAN_PASSWORD=your_plan_db_password + +# Plan database SSL mode (default: prefer) +#PGSCHEMA_PLAN_SSLMODE=prefer \ No newline at end of file diff --git a/cmd/apply/apply.go b/cmd/apply/apply.go index 657cb3af..a699cb9d 100644 --- a/cmd/apply/apply.go +++ b/cmd/apply/apply.go @@ -38,6 +38,9 @@ var ( applyPlanDBDatabase string applyPlanDBUser string applyPlanDBPassword string + + applySSLMode string + applyPlanDBSSLMode string ) var ApplyCmd = &cobra.Command{ @@ -76,6 +79,10 @@ func init() { ApplyCmd.Flags().StringVar(&applyPlanDBDatabase, "plan-db", "", "Plan database name (env: PGSCHEMA_PLAN_DB)") ApplyCmd.Flags().StringVar(&applyPlanDBUser, "plan-user", "", "Plan database user (env: PGSCHEMA_PLAN_USER)") ApplyCmd.Flags().StringVar(&applyPlanDBPassword, "plan-password", "", "Plan database password (env: PGSCHEMA_PLAN_PASSWORD)") + ApplyCmd.Flags().StringVar(&applyPlanDBSSLMode, "plan-sslmode", "prefer", "Plan database SSL mode (env: PGSCHEMA_PLAN_SSLMODE)") + + // SSL mode flag + ApplyCmd.Flags().StringVar(&applySSLMode, "sslmode", "prefer", "SSL mode for database connection (disable, allow, prefer, require, verify-ca, verify-full) (env: PGSSLMODE)") // Mark file and plan as mutually exclusive ApplyCmd.MarkFlagsMutuallyExclusive("file", "plan") @@ -96,6 +103,10 @@ type ApplyConfig struct { Quiet bool // Suppress plan display and progress messages (useful for tests) LockTimeout string ApplicationName string + SSLMode string + // Plan database configuration (needed when GeneratePlan checks provider SSL mode) + PlanDBHost string + PlanDBSSLMode string } // ApplyMigration applies a migration plan to update a database schema. @@ -127,6 +138,9 @@ func ApplyMigration(config *ApplyConfig, provider postgres.DesiredStateProvider) Schema: config.Schema, File: config.File, ApplicationName: config.ApplicationName, + SSLMode: config.SSLMode, + PlanDBHost: config.PlanDBHost, + PlanDBSSLMode: config.PlanDBSSLMode, } // Generate plan using shared logic @@ -146,7 +160,7 @@ func ApplyMigration(config *ApplyConfig, provider postgres.DesiredStateProvider) // Validate schema fingerprint if plan has one if migrationPlan.SourceFingerprint != nil { - err := validateSchemaFingerprint(migrationPlan, config.Host, config.Port, config.DB, config.User, config.Password, config.Schema, config.ApplicationName, ignoreConfig) + err := validateSchemaFingerprint(migrationPlan, config.Host, config.Port, config.DB, config.User, config.Password, config.SSLMode, config.Schema, config.ApplicationName, ignoreConfig) if err != nil { return err } @@ -191,7 +205,7 @@ func ApplyMigration(config *ApplyConfig, provider postgres.DesiredStateProvider) Database: config.DB, User: config.User, Password: config.Password, - SSLMode: "prefer", + SSLMode: config.SSLMode, ApplicationName: config.ApplicationName, } @@ -265,6 +279,19 @@ func RunApply(cmd *cobra.Command, args []string) error { } } + // Derive final sslmode: use flag if explicitly set, otherwise check environment variable + finalSSLMode := applySSLMode + if cmd == nil || !cmd.Flags().Changed("sslmode") { + if envSSLMode := os.Getenv("PGSSLMODE"); envSSLMode != "" { + finalSSLMode = envSSLMode + } + } + + // Validate sslmode + if err := util.ValidateSSLMode(finalSSLMode); err != nil { + return err + } + // Build configuration config := &ApplyConfig{ Host: applyHost, @@ -277,6 +304,7 @@ func RunApply(cmd *cobra.Command, args []string) error { NoColor: applyNoColor, LockTimeout: applyLockTimeout, ApplicationName: applyApplicationName, + SSLMode: finalSSLMode, } var provider postgres.DesiredStateProvider @@ -312,13 +340,20 @@ func RunApply(cmd *cobra.Command, args []string) error { config.File = applyFile // Apply environment variables to plan database flags (only needed for File Mode) - util.ApplyPlanDBEnvVars(cmd, &applyPlanDBHost, &applyPlanDBDatabase, &applyPlanDBUser, &applyPlanDBPassword, &applyPlanDBPort) + util.ApplyPlanDBEnvVars(cmd, &applyPlanDBHost, &applyPlanDBDatabase, &applyPlanDBUser, &applyPlanDBPassword, &applyPlanDBPort, &applyPlanDBSSLMode) // Validate plan database flags if plan-host is provided if err := util.ValidatePlanDBFlags(applyPlanDBHost, applyPlanDBDatabase, applyPlanDBUser); err != nil { return err } + // Validate plan database sslmode if plan-host is provided + if applyPlanDBHost != "" { + if err := util.ValidateSSLMode(applyPlanDBSSLMode); err != nil { + return fmt.Errorf("plan database: %w", err) + } + } + // Derive final plan database password finalPlanPassword := applyPlanDBPassword if finalPlanPassword == "" { @@ -337,18 +372,24 @@ func RunApply(cmd *cobra.Command, args []string) error { Schema: applySchema, File: applyFile, ApplicationName: applyApplicationName, + SSLMode: finalSSLMode, // Plan database configuration PlanDBHost: applyPlanDBHost, PlanDBPort: applyPlanDBPort, PlanDBDatabase: applyPlanDBDatabase, PlanDBUser: applyPlanDBUser, PlanDBPassword: finalPlanPassword, + PlanDBSSLMode: applyPlanDBSSLMode, } provider, err = planCmd.CreateDesiredStateProvider(planConfig) if err != nil { return err } defer provider.Stop() + + // Propagate plan DB fields so ApplyMigration -> GeneratePlan knows the provider type + config.PlanDBHost = applyPlanDBHost + config.PlanDBSSLMode = applyPlanDBSSLMode } // Apply the migration @@ -356,10 +397,10 @@ func RunApply(cmd *cobra.Command, args []string) error { } // validateSchemaFingerprint validates that the current database schema matches the expected fingerprint -func validateSchemaFingerprint(migrationPlan *plan.Plan, host string, port int, db, user, password, schema, applicationName string, ignoreConfig *ir.IgnoreConfig) error { +func validateSchemaFingerprint(migrationPlan *plan.Plan, host string, port int, db, user, password, sslmode, schema, applicationName string, ignoreConfig *ir.IgnoreConfig) error { // Get current state from target database with ignore config // This ensures ignored objects are excluded from fingerprint calculation - currentStateIR, err := util.GetIRFromDatabase(host, port, db, user, password, schema, applicationName, ignoreConfig) + currentStateIR, err := util.GetIRFromDatabase(host, port, db, user, password, sslmode, schema, applicationName, ignoreConfig) if err != nil { return fmt.Errorf("failed to get current database state for fingerprint validation: %w", err) } diff --git a/cmd/dump/dump.go b/cmd/dump/dump.go index 7fcd89ec..59fc167e 100644 --- a/cmd/dump/dump.go +++ b/cmd/dump/dump.go @@ -21,6 +21,7 @@ var ( multiFile bool file string noComments bool + sslmode string ) // DumpConfig holds configuration for dump execution @@ -34,6 +35,7 @@ type DumpConfig struct { MultiFile bool File string NoComments bool + SSLMode string } var DumpCmd = &cobra.Command{ @@ -55,6 +57,7 @@ func init() { DumpCmd.Flags().BoolVar(&multiFile, "multi-file", false, "Output schema to multiple files organized by object type") DumpCmd.Flags().StringVar(&file, "file", "", "Output file path (required when --multi-file is used)") DumpCmd.Flags().BoolVar(&noComments, "no-comments", false, "Do not output object comment headers") + DumpCmd.Flags().StringVar(&sslmode, "sslmode", "prefer", "SSL mode for database connection (disable, allow, prefer, require, verify-ca, verify-full) (env: PGSSLMODE)") } // ExecuteDump executes the dump operation with the given configuration @@ -73,7 +76,7 @@ func ExecuteDump(config *DumpConfig) (string, error) { } // Get IR from database using the shared utility - schemaIR, err := util.GetIRFromDatabase(config.Host, config.Port, config.DB, config.User, config.Password, config.Schema, "pgschema", ignoreConfig) + schemaIR, err := util.GetIRFromDatabase(config.Host, config.Port, config.DB, config.User, config.Password, config.SSLMode, config.Schema, "pgschema", ignoreConfig) if err != nil { return "", fmt.Errorf("failed to get database schema: %w", err) } @@ -110,6 +113,19 @@ func runDump(cmd *cobra.Command, args []string) error { } } + // Derive final sslmode: use flag if explicitly set, otherwise check environment variable + finalSSLMode := sslmode + if cmd == nil || !cmd.Flags().Changed("sslmode") { + if envSSLMode := os.Getenv("PGSSLMODE"); envSSLMode != "" { + finalSSLMode = envSSLMode + } + } + + // Validate sslmode + if err := util.ValidateSSLMode(finalSSLMode); err != nil { + return err + } + // Create config from command-line flags config := &DumpConfig{ Host: host, @@ -121,6 +137,7 @@ func runDump(cmd *cobra.Command, args []string) error { MultiFile: multiFile, File: file, NoComments: noComments, + SSLMode: finalSSLMode, } // Execute dump diff --git a/cmd/plan/external_db_integration_test.go b/cmd/plan/external_db_integration_test.go index b1357ec5..4aa19972 100644 --- a/cmd/plan/external_db_integration_test.go +++ b/cmd/plan/external_db_integration_test.go @@ -109,6 +109,7 @@ func TestExternalDatabase_VersionMismatch(t *testing.T) { targetDatabase, targetUser, targetPassword, + "prefer", ) require.NoError(t, err, "should detect PostgreSQL version") assert.NotEmpty(t, pgVersion, "version should not be empty") diff --git a/cmd/plan/plan.go b/cmd/plan/plan.go index b2623fdb..afd5e3bd 100644 --- a/cmd/plan/plan.go +++ b/cmd/plan/plan.go @@ -37,6 +37,9 @@ var ( planDBDatabase string planDBUser string planDBPassword string + + planSSLMode string + planDBSSLMode string ) var PlanCmd = &cobra.Command{ @@ -66,6 +69,10 @@ func init() { PlanCmd.Flags().StringVar(&planDBDatabase, "plan-db", "", "Plan database name (env: PGSCHEMA_PLAN_DB)") PlanCmd.Flags().StringVar(&planDBUser, "plan-user", "", "Plan database user (env: PGSCHEMA_PLAN_USER)") PlanCmd.Flags().StringVar(&planDBPassword, "plan-password", "", "Plan database password (env: PGSCHEMA_PLAN_PASSWORD)") + PlanCmd.Flags().StringVar(&planDBSSLMode, "plan-sslmode", "prefer", "Plan database SSL mode (env: PGSCHEMA_PLAN_SSLMODE)") + + // SSL mode flag + PlanCmd.Flags().StringVar(&planSSLMode, "sslmode", "prefer", "SSL mode for database connection (disable, allow, prefer, require, verify-ca, verify-full) (env: PGSSLMODE)") // Output flags PlanCmd.Flags().StringVar(&outputHuman, "output-human", "", "Output human-readable format to stdout or file path") @@ -78,7 +85,7 @@ func init() { func runPlan(cmd *cobra.Command, args []string) error { // Apply environment variables to plan database flags - util.ApplyPlanDBEnvVars(cmd, &planDBHost, &planDBDatabase, &planDBUser, &planDBPassword, &planDBPort) + util.ApplyPlanDBEnvVars(cmd, &planDBHost, &planDBDatabase, &planDBUser, &planDBPassword, &planDBPort, &planDBSSLMode) // Validate plan database flags if plan-host is provided if err := util.ValidatePlanDBFlags(planDBHost, planDBDatabase, planDBUser); err != nil { @@ -93,6 +100,14 @@ func runPlan(cmd *cobra.Command, args []string) error { } } + // Derive final sslmode: use flag if explicitly set, otherwise check environment variable + finalSSLMode := planSSLMode + if cmd == nil || !cmd.Flags().Changed("sslmode") { + if envSSLMode := os.Getenv("PGSSLMODE"); envSSLMode != "" { + finalSSLMode = envSSLMode + } + } + // Derive final plan database password finalPlanPassword := planDBPassword if finalPlanPassword == "" { @@ -101,6 +116,16 @@ func runPlan(cmd *cobra.Command, args []string) error { } } + // Validate sslmode values + if err := util.ValidateSSLMode(finalSSLMode); err != nil { + return err + } + if planDBHost != "" { + if err := util.ValidateSSLMode(planDBSSLMode); err != nil { + return fmt.Errorf("plan database: %w", err) + } + } + // Create plan configuration config := &PlanConfig{ Host: planHost, @@ -111,12 +136,14 @@ func runPlan(cmd *cobra.Command, args []string) error { Schema: planSchema, File: planFile, ApplicationName: "pgschema", + SSLMode: finalSSLMode, // Plan database configuration PlanDBHost: planDBHost, PlanDBPort: planDBPort, PlanDBDatabase: planDBDatabase, PlanDBUser: planDBUser, PlanDBPassword: finalPlanPassword, + PlanDBSSLMode: planDBSSLMode, } // Create desired state provider (embedded postgres or external database) @@ -164,6 +191,8 @@ type PlanConfig struct { PlanDBDatabase string PlanDBUser string PlanDBPassword string + SSLMode string + PlanDBSSLMode string } // CreateDesiredStateProvider creates either an embedded PostgreSQL instance or connects to an external database @@ -176,6 +205,7 @@ func CreateDesiredStateProvider(config *PlanConfig) (postgres.DesiredStateProvid config.DB, config.User, config.Password, + config.SSLMode, ) if err != nil { return nil, fmt.Errorf("failed to detect PostgreSQL version: %w", err) @@ -197,6 +227,7 @@ func CreateDesiredStateProvider(config *PlanConfig) (postgres.DesiredStateProvid Database: config.PlanDBDatabase, Username: config.PlanDBUser, Password: config.PlanDBPassword, + SSLMode: config.PlanDBSSLMode, TargetMajorVersion: targetMajorVersion, } return postgres.NewExternalDatabase(externalConfig) @@ -250,7 +281,7 @@ func GeneratePlan(config *PlanConfig, provider postgres.DesiredStateProvider) (* } // Get current state from target database - currentStateIR, err := util.GetIRFromDatabase(config.Host, config.Port, config.DB, config.User, config.Password, config.Schema, config.ApplicationName, ignoreConfig) + currentStateIR, err := util.GetIRFromDatabase(config.Host, config.Port, config.DB, config.User, config.Password, config.SSLMode, config.Schema, config.ApplicationName, ignoreConfig) if err != nil { return nil, fmt.Errorf("failed to get current state from database: %w", err) } @@ -279,7 +310,16 @@ func GeneratePlan(config *PlanConfig, provider postgres.DesiredStateProvider) (* schemaToInspect = config.Schema } - desiredStateIR, err := util.GetIRFromDatabase(providerHost, providerPort, providerDB, providerUsername, providerPassword, schemaToInspect, config.ApplicationName, ignoreConfig) + // For embedded postgres, always use "disable" since it starts without SSL configured. + // For external plan databases, use the configured PlanDBSSLMode (defaulting to "prefer"). + providerSSLMode := "disable" + if config.PlanDBHost != "" { + providerSSLMode = config.PlanDBSSLMode + if providerSSLMode == "" { + providerSSLMode = "prefer" + } + } + desiredStateIR, err := util.GetIRFromDatabase(providerHost, providerPort, providerDB, providerUsername, providerPassword, providerSSLMode, schemaToInspect, config.ApplicationName, ignoreConfig) if err != nil { return nil, fmt.Errorf("failed to get desired state: %w", err) } @@ -692,4 +732,6 @@ func ResetFlags() { planDBDatabase = "" planDBUser = "" planDBPassword = "" + planSSLMode = "prefer" + planDBSSLMode = "prefer" } diff --git a/cmd/util/connection.go b/cmd/util/connection.go index 6ff42f9c..9eba0422 100644 --- a/cmd/util/connection.go +++ b/cmd/util/connection.go @@ -84,8 +84,22 @@ func buildDSN(config *ConnectionConfig) string { return strings.Join(parts, " ") } +// ValidateSSLMode validates that the given sslmode is a valid PostgreSQL SSL mode. +func ValidateSSLMode(mode string) error { + switch mode { + case "disable", "allow", "prefer", "require", "verify-ca", "verify-full": + return nil + default: + return fmt.Errorf("invalid sslmode %q: must be one of disable, allow, prefer, require, verify-ca, verify-full", mode) + } +} + // GetIRFromDatabase gets the IR from a database with ignore configuration -func GetIRFromDatabase(host string, port int, db, user, password, schemaName, applicationName string, ignoreConfig *ir.IgnoreConfig) (*ir.IR, error) { +func GetIRFromDatabase(host string, port int, db, user, password, sslmode, schemaName, applicationName string, ignoreConfig *ir.IgnoreConfig) (*ir.IR, error) { + if sslmode == "" { + sslmode = "prefer" + } + // Build database connection config := &ConnectionConfig{ Host: host, @@ -93,7 +107,7 @@ func GetIRFromDatabase(host string, port int, db, user, password, schemaName, ap Database: db, User: user, Password: password, - SSLMode: "prefer", + SSLMode: sslmode, ApplicationName: applicationName, } diff --git a/cmd/util/connection_test.go b/cmd/util/connection_test.go new file mode 100644 index 00000000..ea26dffe --- /dev/null +++ b/cmd/util/connection_test.go @@ -0,0 +1,21 @@ +package util + +import "testing" + +func TestValidateSSLMode(t *testing.T) { + // Valid modes + validModes := []string{"disable", "allow", "prefer", "require", "verify-ca", "verify-full"} + for _, mode := range validModes { + if err := ValidateSSLMode(mode); err != nil { + t.Errorf("ValidateSSLMode(%q) returned error: %v", mode, err) + } + } + + // Invalid modes + invalidModes := []string{"", "reqiure", "DISABLE", "ssl", "none"} + for _, mode := range invalidModes { + if err := ValidateSSLMode(mode); err == nil { + t.Errorf("ValidateSSLMode(%q) should have returned error", mode) + } + } +} diff --git a/cmd/util/env.go b/cmd/util/env.go index 32c96c9d..d3f74d25 100644 --- a/cmd/util/env.go +++ b/cmd/util/env.go @@ -79,7 +79,7 @@ func PreRunEWithEnvVarsAndConnectionAndApp(dbPtr, userPtr *string, hostPtr *stri // ApplyPlanDBEnvVars applies environment variables to plan database connection parameters // This is used in the plan command to populate plan-* flags from PGSCHEMA_PLAN_* environment variables -func ApplyPlanDBEnvVars(cmd *cobra.Command, hostPtr, dbPtr, userPtr, passwordPtr *string, portPtr *int) { +func ApplyPlanDBEnvVars(cmd *cobra.Command, hostPtr, dbPtr, userPtr, passwordPtr *string, portPtr *int, sslmodePtr *string) { // Apply environment variables if flags were not explicitly set if GetEnvWithDefault("PGSCHEMA_PLAN_HOST", "") != "" && !cmd.Flags().Changed("plan-host") { *hostPtr = GetEnvWithDefault("PGSCHEMA_PLAN_HOST", "") @@ -96,6 +96,9 @@ func ApplyPlanDBEnvVars(cmd *cobra.Command, hostPtr, dbPtr, userPtr, passwordPtr if GetEnvWithDefault("PGSCHEMA_PLAN_PASSWORD", "") != "" && !cmd.Flags().Changed("plan-password") { *passwordPtr = GetEnvWithDefault("PGSCHEMA_PLAN_PASSWORD", "") } + if GetEnvWithDefault("PGSCHEMA_PLAN_SSLMODE", "") != "" && !cmd.Flags().Changed("plan-sslmode") { + *sslmodePtr = GetEnvWithDefault("PGSCHEMA_PLAN_SSLMODE", "") + } } // ValidatePlanDBFlags validates plan database flags when plan-host is provided diff --git a/docs/cli/apply.mdx b/docs/cli/apply.mdx index 21ff17d1..05e682be 100644 --- a/docs/cli/apply.mdx +++ b/docs/cli/apply.mdx @@ -114,6 +114,14 @@ pgschema apply --host localhost --db myapp --user postgres --password mypassword See [dotenv (.env)](/cli/dotenv) for detailed configuration options. + + SSL mode for database connection (env: PGSSLMODE) + + Valid values: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` + + For `verify-ca` and `verify-full` modes, you can configure certificate paths using standard PostgreSQL environment variables (`PGSSLROOTCERT`, `PGSSLCERT`, `PGSSLKEY`). + + Schema name to apply changes to diff --git a/docs/cli/dotenv.mdx b/docs/cli/dotenv.mdx index 63a688a9..4e431007 100644 --- a/docs/cli/dotenv.mdx +++ b/docs/cli/dotenv.mdx @@ -37,6 +37,10 @@ pgschema supports all standard PostgreSQL environment variables: Database password + + SSL mode for database connection. Valid values: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` + + Application name visible in `pg_stat_activity` @@ -53,6 +57,9 @@ PGDATABASE=myapp PGUSER=postgres PGPASSWORD=secretpassword +# Optional: SSL mode (disable, allow, prefer, require, verify-ca, verify-full) +PGSSLMODE=prefer + # Optional: Custom application name PGAPPNAME=pgschema ``` diff --git a/docs/cli/dump.mdx b/docs/cli/dump.mdx index ca3c68f4..4684d41a 100644 --- a/docs/cli/dump.mdx +++ b/docs/cli/dump.mdx @@ -118,6 +118,14 @@ pgschema apply --host staging-host --db myapp --user postgres --file current.sql See [dotenv (.env)](/cli/dotenv) for detailed configuration options. + + SSL mode for database connection (env: PGSSLMODE) + + Valid values: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` + + For `verify-ca` and `verify-full` modes, you can configure certificate paths using standard PostgreSQL environment variables (`PGSSLROOTCERT`, `PGSSLCERT`, `PGSSLKEY`). + + Schema name to dump diff --git a/docs/cli/plan-db.mdx b/docs/cli/plan-db.mdx index 7858d131..2153d8fa 100644 --- a/docs/cli/plan-db.mdx +++ b/docs/cli/plan-db.mdx @@ -190,6 +190,12 @@ CREATE TABLE orders ( Environment variable: `PGSCHEMA_PLAN_PASSWORD` + + Plan database SSL mode. Valid values: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` + + Environment variable: `PGSCHEMA_PLAN_SSLMODE` + + ### Using Environment Variables @@ -207,6 +213,7 @@ PGSCHEMA_PLAN_PORT=5432 PGSCHEMA_PLAN_DB=pgschema_plan PGSCHEMA_PLAN_USER=postgres PGSCHEMA_PLAN_PASSWORD=planpassword +PGSCHEMA_PLAN_SSLMODE=prefer # Run plan with external database pgschema plan --file schema.sql @@ -226,6 +233,7 @@ export PGSCHEMA_PLAN_HOST=localhost export PGSCHEMA_PLAN_DB=pgschema_plan export PGSCHEMA_PLAN_USER=postgres export PGSCHEMA_PLAN_PASSWORD=planpassword +export PGSCHEMA_PLAN_SSLMODE=prefer # Run plan pgschema plan --file schema.sql diff --git a/docs/cli/plan.mdx b/docs/cli/plan.mdx index 5db20f15..1c331519 100644 --- a/docs/cli/plan.mdx +++ b/docs/cli/plan.mdx @@ -116,6 +116,14 @@ pgschema plan --host localhost --db myapp --user postgres --password mypassword See [dotenv (.env)](/cli/dotenv) for detailed configuration options. + + SSL mode for database connection (env: PGSSLMODE) + + Valid values: `disable`, `allow`, `prefer`, `require`, `verify-ca`, `verify-full` + + For `verify-ca` and `verify-full` modes, you can configure certificate paths using standard PostgreSQL environment variables (`PGSSLROOTCERT`, `PGSSLCERT`, `PGSSLKEY`). + + Schema name to target for comparison diff --git a/docs/coding-agent.mdx b/docs/coding-agent.mdx index 39e5e253..247e95f5 100644 --- a/docs/coding-agent.mdx +++ b/docs/coding-agent.mdx @@ -83,6 +83,7 @@ Use these connection parameters for all commands: - `--db` - Database name (required) - `--user` - Database user (required) - `--password` - Database password (or use PGPASSWORD environment variable) +- `--sslmode` - SSL mode (default: prefer, or use PGSSLMODE environment variable) - `--schema` - Target schema (default: public) ### Multi-File Schema Management diff --git a/internal/postgres/embedded.go b/internal/postgres/embedded.go index 551ac012..8a26f9b9 100644 --- a/internal/postgres/embedded.go +++ b/internal/postgres/embedded.go @@ -45,15 +45,19 @@ type EmbeddedPostgresConfig struct { // DetectPostgresVersionFromDB connects to a database and detects its version // This is a convenience function that opens a connection, detects the version, and closes it -func DetectPostgresVersionFromDB(host string, port int, database, user, password string) (PostgresVersion, error) { +func DetectPostgresVersionFromDB(host string, port int, database, user, password, sslmode string) (PostgresVersion, error) { // Build connection config + finalSSLMode := sslmode + if finalSSLMode == "" { + finalSSLMode = "prefer" + } config := &util.ConnectionConfig{ Host: host, Port: port, Database: database, User: user, Password: password, - SSLMode: "prefer", + SSLMode: finalSSLMode, } // Connect to database diff --git a/internal/postgres/external.go b/internal/postgres/external.go index 52268bc1..0837a3f7 100644 --- a/internal/postgres/external.go +++ b/internal/postgres/external.go @@ -30,9 +30,18 @@ type ExternalDatabaseConfig struct { Database string Username string Password string + SSLMode string TargetMajorVersion int // Expected major version to match } +// sslModeOrDefault returns the configured SSL mode, defaulting to "prefer" if empty +func (c *ExternalDatabaseConfig) sslModeOrDefault() string { + if c.SSLMode == "" { + return "prefer" + } + return c.SSLMode +} + // NewExternalDatabase creates a new external database connection for desired state validation. // It validates the connection, checks version compatibility, and generates a temporary schema name. func NewExternalDatabase(config *ExternalDatabaseConfig) (*ExternalDatabase, error) { @@ -43,7 +52,7 @@ func NewExternalDatabase(config *ExternalDatabaseConfig) (*ExternalDatabase, err Database: config.Database, User: config.Username, Password: config.Password, - SSLMode: "prefer", + SSLMode: config.sslModeOrDefault(), } // Connect to database