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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/lib/potluck/postgres.rb +178 -59
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2dc9b254c8fd0d0bd4c7fee8d03705c1fe054577addaacf874286bfb32d0a2a
|
4
|
+
data.tar.gz: b995b1b64aa3bca01392fcf874eaf71a7fae58bf0a5d0da54cc1e42269ed0499
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/potluck/postgres.rb
CHANGED
@@ -4,79 +4,122 @@ require('potluck')
|
|
4
4
|
require('sequel')
|
5
5
|
|
6
6
|
module Potluck
|
7
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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 = [
|
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
|
-
|
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.
|
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:
|
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.
|
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.
|
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.
|
122
|
+
rubygems_version: 3.2.32
|
123
123
|
signing_key:
|
124
124
|
specification_version: 4
|
125
125
|
summary: A Ruby manager for Postgres.
|