Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,4 +44,7 @@ PGAPPNAME=pgschema
#PGSCHEMA_PLAN_USER=postgres

# Plan database password
#PGSCHEMA_PLAN_PASSWORD=your_plan_db_password
#PGSCHEMA_PLAN_PASSWORD=your_plan_db_password

# Plan database SSL mode (default: prefer)
#PGSCHEMA_PLAN_SSLMODE=prefer
51 changes: 46 additions & 5 deletions cmd/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ var (
applyPlanDBDatabase string
applyPlanDBUser string
applyPlanDBPassword string

applySSLMode string
applyPlanDBSSLMode string
)

var ApplyCmd = &cobra.Command{
Expand Down Expand Up @@ -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")
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
Expand All @@ -277,6 +304,7 @@ func RunApply(cmd *cobra.Command, args []string) error {
NoColor: applyNoColor,
LockTimeout: applyLockTimeout,
ApplicationName: applyApplicationName,
SSLMode: finalSSLMode,
}

var provider postgres.DesiredStateProvider
Expand Down Expand Up @@ -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 == "" {
Expand All @@ -337,29 +372,35 @@ 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
return ApplyMigration(config, provider)
}

// 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)
}
Expand Down
19 changes: 18 additions & 1 deletion cmd/dump/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
multiFile bool
file string
noComments bool
sslmode string
)

// DumpConfig holds configuration for dump execution
Expand All @@ -34,6 +35,7 @@ type DumpConfig struct {
MultiFile bool
File string
NoComments bool
SSLMode string
}

var DumpCmd = &cobra.Command{
Expand All @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
Expand All @@ -121,6 +137,7 @@ func runDump(cmd *cobra.Command, args []string) error {
MultiFile: multiFile,
File: file,
NoComments: noComments,
SSLMode: finalSSLMode,
}

// Execute dump
Expand Down
1 change: 1 addition & 0 deletions cmd/plan/external_db_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
48 changes: 45 additions & 3 deletions cmd/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ var (
planDBDatabase string
planDBUser string
planDBPassword string

planSSLMode string
planDBSSLMode string
)

var PlanCmd = &cobra.Command{
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand All @@ -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 == "" {
Expand All @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -692,4 +732,6 @@ func ResetFlags() {
planDBDatabase = ""
planDBUser = ""
planDBPassword = ""
planSSLMode = "prefer"
planDBSSLMode = "prefer"
}
Loading