potluck-postgres 0.0.2 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/lib/potluck/postgres.rb +178 -59
  4. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48714348049e78a70647696184c95db2198c2de377c72934ab9b935be5b0dfc8
4
- data.tar.gz: fcaab8a6ecf0e62e39dee99863f769da0c7da33398887ac55edbe24a0634ff85
3
+ metadata.gz: e2dc9b254c8fd0d0bd4c7fee8d03705c1fe054577addaacf874286bfb32d0a2a
4
+ data.tar.gz: b995b1b64aa3bca01392fcf874eaf71a7fae58bf0a5d0da54cc1e42269ed0499
5
5
  SHA512:
6
- metadata.gz: 6b1acaa045587d18246278775619e472736069b73e8939fa4bf594327307a00f5ab3b2fa00f162b3e804412892bc1f3b8e652e8fd5c258c85f5bff3260df3ebd
7
- data.tar.gz: dc1e1845ca72d509624ecc0fd2d3886b2cfbcc105822a5591e7626a4ef5e7557ee567498583e1765d8a551b2e87ab5af76824a28eb0f8d6587b96319d76d2ee7
6
+ metadata.gz: 734fc97cfe75c7c0af39b2fb65db31dc6fe7d8355995fd339c7ec3808ffb2ee2f01de2b4852753b8ad45acde92a508126eef475725db2327c2482721c0a3b1d2
7
+ data.tar.gz: 108749ea87986e3af08ef7f392b381880a5b391f44a3e86c237620c52a60c67bd591c8784d59a27c8b75ac88e3016ebb4987d0bb33054b28d7712312d705f357
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2021 Nate Pickens
1
+ Copyright 2021-2022 Nate Pickens
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
4
  documentation files (the "Software"), to deal in the Software without restriction, including without
@@ -4,79 +4,122 @@ require('potluck')
4
4
  require('sequel')
5
5
 
6
6
  module Potluck
7
- class Postgres < Dish
7
+ ##
8
+ # Error class used to wrap errors encountered while connecting to or setting up a database.
9
+ #
10
+ class PostgresError < ServiceError
11
+ attr_reader(:wrapped_error)
12
+
13
+ ##
14
+ # Creates a new instance.
15
+ #
16
+ # * +message+ - Error message.
17
+ # * +wrapped_error+ - Original error that was rescued and is being wrapped by this one (optional).
18
+ #
19
+ def initialize(message, wrapped_error = nil)
20
+ super(message)
21
+
22
+ @wrapped_error = wrapped_error
23
+ end
24
+ end
25
+
26
+ ##
27
+ # A Ruby interface for controlling and connecting to Postgres. Uses
28
+ # [Sequel](https://github.com/jeremyevans/sequel) to connect and perform automatic role and database
29
+ # creation, as well as for utility methods such as database schema migration.
30
+ #
31
+ class Postgres < Service
32
+ ROLE_NOT_FOUND_REGEX = /role .* does not exist/.freeze
33
+ DATABASE_NOT_FOUND_REGEX = /database .* does not exist/.freeze
34
+
35
+ STARTING_UP_STRING = 'the database system is starting up'
36
+ STARTING_UP_TIMEOUT = 30
37
+
38
+ CONNECTION_REFUSED_STRING = 'connection refused'
39
+ CONNECTION_REFUSED_TIMEOUT = 3
40
+
8
41
  attr_reader(:database)
9
42
 
43
+ ##
44
+ # Creates a new instance.
45
+ #
46
+ # * +config+ - Configuration hash to pass to <tt>Sequel.connect</tt>.
47
+ # * +args+ - Arguments to pass to Potluck::Service.new (optional).
48
+ #
10
49
  def initialize(config, **args)
11
50
  super(**args)
12
51
 
13
52
  @config = config
14
53
  end
15
54
 
16
- def connect
17
- (tries ||= 0) && (tries += 1)
18
- @database = Sequel.connect(@config, logger: @logger)
19
- rescue Sequel::DatabaseConnectionError => e
20
- if (dud = Sequel::DATABASES.last)
21
- dud.disconnect
22
- Sequel.synchronize { Sequel::DATABASES.delete(dud) }
23
- end
24
-
25
- if e.message =~ /role .* does not exist/ && tries == 1
26
- create_database_role
27
- create_database
28
- retry
29
- elsif e.message =~ /database .* does not exist/ && tries == 1
30
- create_database
31
- retry
32
- elsif (@is_local && tries < 3) && (e.message.include?('could not connect') ||
33
- e.message.include?('the database system is starting up'))
34
- sleep(1)
35
- retry
36
- elsif e.message.include?('could not connect')
37
- abort("#{e.class}: #{e.message.strip}")
38
- else
39
- abort("#{e.class}: #{e.message.strip}\n #{e.backtrace.join("\n ")}")
40
- end
41
- end
42
-
43
- def disconnect
44
- @database&.disconnect
55
+ ##
56
+ # Disconnects and stops the Postgres process.
57
+ #
58
+ def stop
59
+ disconnect
60
+ super
45
61
  end
46
62
 
47
- def create_database_role
48
- tmp_config = @config.dup
49
- tmp_config[:database] = 'postgres'
50
- tmp_config[:username] = ENV['USER']
51
- tmp_config[:password] = nil
63
+ ##
64
+ # Connects to the configured Postgres database.
65
+ #
66
+ def connect
67
+ role_created = false
68
+ database_created = false
52
69
 
53
70
  begin
54
- Sequel.connect(tmp_config, logger: @logger) do |database|
55
- database.execute("CREATE ROLE #{@config[:username]} WITH LOGIN CREATEDB REPLICATION PASSWORD "\
56
- "'#{@config[:password]}'")
71
+ (tries ||= 0) && (tries += 1)
72
+ @database = Sequel.connect(@config, logger: @logger)
73
+ rescue Sequel::DatabaseConnectionError => e
74
+ if (dud = Sequel::DATABASES.last)
75
+ dud.disconnect
76
+ Sequel.synchronize { Sequel::DATABASES.delete(dud) }
57
77
  end
58
- rescue => e
59
- @logger.error("#{e.class}: #{e.message.strip}\n #{e.backtrace.join("\n ")}\n")
60
- abort("Could not create role '#{@config[:username]}'. Make sure database user '#{ENV['USER']}' "\
61
- 'has permission to do so, or create it manually.')
62
- end
63
- end
64
78
 
65
- def create_database
66
- tmp_config = @config.dup
67
- tmp_config[:database] = 'postgres'
68
-
69
- begin
70
- Sequel.connect(tmp_config, logger: @logger) do |database|
71
- database.execute("CREATE DATABASE #{@config[:database]}")
79
+ message = e.message.downcase
80
+
81
+ if message =~ ROLE_NOT_FOUND_REGEX && !role_created && manage?
82
+ role_created = true
83
+ create_role
84
+ retry
85
+ elsif message =~ DATABASE_NOT_FOUND_REGEX && !database_created && manage?
86
+ database_created = true
87
+ create_database
88
+ retry
89
+ elsif message.include?(STARTING_UP_STRING) && tries < STARTING_UP_TIMEOUT
90
+ sleep(1)
91
+ retry
92
+ elsif message.include?(CONNECTION_REFUSED_STRING) && tries < CONNECTION_REFUSED_TIMEOUT
93
+ sleep(1)
94
+ retry
95
+ elsif message.include?(CONNECTION_REFUSED_STRING)
96
+ raise(PostgresError.new(e.message.strip, e))
97
+ else
98
+ raise
72
99
  end
73
- rescue => e
74
- @logger.error("#{e.class}: #{e.message.strip}\n #{e.backtrace.join("\n ")}\n")
75
- abort("Could not create database '#{@config[:database]}'. Make sure database user "\
76
- "'#{@config[:username]}' has permission to do so, or create it manually.")
77
100
  end
101
+
102
+ # Only grant permissions if the database already existed but the role did not. Automatic database
103
+ # creation (via #create_database) is performed as the configured role, which means explicit permission
104
+ # granting is not necessary.
105
+ grant_permissions if role_created && !database_created
106
+ end
107
+
108
+ ##
109
+ # Disconnects from the database if a connection was made.
110
+ #
111
+ def disconnect
112
+ @database&.disconnect
78
113
  end
79
114
 
115
+ ##
116
+ # Runs database migrations by way of Sequel's migration extension. Migration files must use the
117
+ # timestamp naming strategy as opposed to integers.
118
+ #
119
+ # * +dir+ - Directory where migration files are located.
120
+ # * +steps+ - Number of steps forward or backward to migrate from the current migration, otherwise will
121
+ # migrate to latest (optional).
122
+ #
80
123
  def migrate(dir, steps = nil)
81
124
  return unless File.directory?(dir)
82
125
 
@@ -84,9 +127,9 @@ module Potluck
84
127
 
85
128
  # Suppress Sequel schema migration table queries.
86
129
  original_level = @logger.level
87
- @logger.level = Logger::WARN
130
+ @logger.level = Logger::WARN if @logger.level == Logger::INFO
88
131
 
89
- args = [Sequel::Model.db, dir, {allow_missing_migration_files: true}]
132
+ args = [@database, dir, {allow_missing_migration_files: true}]
90
133
  migrator = Sequel::TimestampMigrator.new(*args)
91
134
 
92
135
  return if migrator.files.empty?
@@ -107,10 +150,13 @@ module Potluck
107
150
  migrator = Sequel::TimestampMigrator.new(*args)
108
151
  @logger.level = original_level
109
152
  migrator.run
153
+ ensure
154
+ @logger.level = original_level if original_level
110
155
  end
111
156
 
112
- private
113
-
157
+ ##
158
+ # Content of the launchctl plist file.
159
+ #
114
160
  def self.plist
115
161
  super(
116
162
  <<~EOS
@@ -129,5 +175,78 @@ module Potluck
129
175
  EOS
130
176
  )
131
177
  end
178
+
179
+ private
180
+
181
+ ##
182
+ # Attempts to connect to the 'postgres' database as the system user with no password and create the
183
+ # configured role. Useful in development.
184
+ #
185
+ def create_role
186
+ tmp_config = admin_database_config
187
+ tmp_config[:database] = 'postgres'
188
+
189
+ begin
190
+ Sequel.connect(tmp_config, logger: @logger) do |database|
191
+ database.execute("CREATE ROLE \"#{@config[:username]}\" WITH LOGIN CREATEDB REPLICATION"\
192
+ "#{" PASSWORD '#{@config[:password]}'" if @config[:password]}")
193
+ end
194
+ rescue => e
195
+ raise(PostgresError.new("Failed to create database role #{@config[:username].inspect} by "\
196
+ "connecting to database #{tmp_config[:database].inspect} as role "\
197
+ "#{tmp_config[:username].inspect}. Please create the role manually.", e))
198
+ end
199
+ end
200
+
201
+ ##
202
+ # Attempts to connect to the 'postgres' database with the configured user and password and create the
203
+ # configured database. Useful in development.
204
+ #
205
+ def create_database
206
+ tmp_config = @config.dup
207
+ tmp_config[:database] = 'postgres'
208
+
209
+ begin
210
+ Sequel.connect(tmp_config, logger: @logger) do |database|
211
+ database.execute("CREATE DATABASE \"#{@config[:database]}\"")
212
+ end
213
+ rescue => e
214
+ raise(PostgresError.new("Failed to create database #{@config[:database].inspect} by connecting to "\
215
+ "database #{tmp_config[:database].inspect} as role #{tmp_config[:username].inspect}. "\
216
+ 'Please create the database manually.', e))
217
+ end
218
+ end
219
+
220
+ ##
221
+ # Grants appropriate permissions for the configured database role.
222
+ #
223
+ def grant_permissions
224
+ tmp_config = admin_database_config
225
+
226
+ begin
227
+ Sequel.connect(tmp_config, logger: @logger) do |db|
228
+ db.execute("GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"#{@config[:username]}\"")
229
+ db.execute("GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"#{@config[:username]}\"")
230
+ db.execute("ALTER DEFAULT PRIVILEGES FOR ROLE \"#{@config[:username]}\" IN SCHEMA public GRANT "\
231
+ "ALL PRIVILEGES ON TABLES TO \"#{@config[:username]}\"")
232
+ end
233
+ rescue => e
234
+ raise(PostgresError.new("Failed to grant database permissions for role "\
235
+ "#{@config[:username].inspect} by connecting as role #{tmp_config[:username].inspect}. Please "\
236
+ 'grant appropriate permissions manually.', e))
237
+ end
238
+ end
239
+
240
+ ##
241
+ # Returns a configuration hash for connecting to Postgres to perform administrative tasks (i.e. role and
242
+ # database creation). Uses the system user as the username and no password.
243
+ #
244
+ def admin_database_config
245
+ config = @config.dup
246
+ config[:username] = ENV['USER']
247
+ config[:password] = nil
248
+
249
+ config
250
+ end
132
251
  end
133
252
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: potluck-postgres
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Pickens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-13 00:00:00.000000000 Z
11
+ date: 2022-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: potluck
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.0.2
19
+ version: 0.0.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.0.2
26
+ version: 0.0.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pg
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -119,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
119
  - !ruby/object:Gem::Version
120
120
  version: '0'
121
121
  requirements: []
122
- rubygems_version: 3.2.3
122
+ rubygems_version: 3.2.32
123
123
  signing_key:
124
124
  specification_version: 4
125
125
  summary: A Ruby manager for Postgres.