pg_easy_replicate 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3e58d75f6e22242f3b833838eba69bfd72cb930ad5931ef77a5c0c4f49aded8
4
- data.tar.gz: 8ff3c66b8ee6b37bcadb801f57fa668019fc7c36c5cabf5639c65d3b80813eae
3
+ metadata.gz: c253c57988226f7d90116cfc4276a51ac6a2753daea88169ba244fe4ce069e76
4
+ data.tar.gz: cadca2e9e430f06be91ce4e1623fdf333fdc96edded99e83729f7b56e9356191
5
5
  SHA512:
6
- metadata.gz: d337088ee7ccf6855c45328502c587172cd96fb4b57bb386a6774665277d5753533f7876fadc28d6088ab0ecfb4cc9e201245d8a05f83ca178f9dd6376df4a00
7
- data.tar.gz: 8d44166f5ad25773462e952109b315195229e1127639296e3f4f56341823af60fd7feba3bef1ffcbd32cbb864cab28117bbedda3ceb9949aa96451296bc63a78
6
+ metadata.gz: dee2a8f5c96289076276b6d2f92e5e0623ac1c2ed7d885bfa7ab472bbc80fc525665fa72ed54fb772d63e9fd564acecea5c0a3d107dcb5e46370e539c20ab2ac
7
+ data.tar.gz: b7c9c94cf5c5b9288a81e6d2a144f71cc387096b464f732f38755f293c1d7344120cf4107f1803981524773edd95cabfa34c56b973004dfd0efec69e9f066f8a
data/Gemfile.lock CHANGED
@@ -1,8 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg_easy_replicate (0.1.3)
5
- lockbox (~> 1.2.0)
4
+ pg_easy_replicate (0.1.4)
6
5
  ougai (~> 2.0.0)
7
6
  pg (~> 1.5.3)
8
7
  sequel (~> 5.69.0)
@@ -19,7 +18,6 @@ GEM
19
18
  thor
20
19
  tilt
21
20
  json (2.6.3)
22
- lockbox (1.2.0)
23
21
  method_source (1.0.0)
24
22
  oj (3.14.3)
25
23
  ougai (2.0.0)
data/README.md CHANGED
@@ -15,6 +15,9 @@ Battle tested in production at [Tines](https://www.tines.com/) 🚀
15
15
  - [Replicating all tables with a single group](#replicating-all-tables-with-a-single-group)
16
16
  - [Config check](#config-check)
17
17
  - [Bootstrap](#bootstrap)
18
+ - [Bootstrap and Config Check with special user role (AWS/GCP/Custom)](#bootstrap-and-config-check-with-special-user-role--aws-gcp-custom-)
19
+ - [Config Check](#config-check)
20
+ - [Bootstrap](#bootstrap-1)
18
21
  - [Start sync](#start-sync)
19
22
  - [Stats](#stats)
20
23
  - [Performing switchover](#performing-switchover)
@@ -22,6 +25,7 @@ Battle tested in production at [Tines](https://www.tines.com/) 🚀
22
25
  - [Switchover strategies with minimal downtime](#switchover-strategies-with-minimal-downtime)
23
26
  - [Rolling restart strategy](#rolling-restart-strategy)
24
27
  - [DNS Failover strategy](#dns-failover-strategy)
28
+ - [Contributing](#contributing)
25
29
 
26
30
  ## Installation
27
31
 
@@ -51,7 +55,7 @@ https://hub.docker.com/r/shayonj/pg_easy_replicate
51
55
 
52
56
  - PostgreSQL 10 and later
53
57
  - Ruby 2.7 and later
54
- - Database user should have permissions for `SUPERUSER`
58
+ - Database user should have permissions for `SUPERUSER` or pass in the special user role that has the privileges to create role, schema, publication and subscription on both databases. More on `--special-user-role` section below.
55
59
  - Both databases should have the same schema
56
60
 
57
61
  ## Limits
@@ -116,6 +120,31 @@ $ pg_easy_replicate bootstrap --group-name database-cluster-1
116
120
  ...
117
121
  ```
118
122
 
123
+ ### Bootstrap and Config Check with special user role (AWS/GCP/Custom)
124
+
125
+ If you don't want your primary login user to have `superuser` privileges or you are on AWS or GCP, you will need to pass in the special user role that has the privileges to create role, schema, publication and subscription. This is required so `pg_easy_replicate` can create a dedicated user for replication which is granted the respective special user role to carry out its functionalities.
126
+
127
+ For AWS the special user role is `rds_superuser`, and for GCP it is `cloudsqlsuperuser`. Please refer to docs for the most up to date information.
128
+
129
+ **Note**: The user in the connection url must be part of the special user role being supplied.
130
+
131
+ #### Config Check
132
+
133
+ ```bash
134
+ $ pg_easy_replicate config_check --special-user-role="rds_superuser"
135
+
136
+ ✅ Config is looking good.
137
+ ```
138
+
139
+ #### Bootstrap
140
+
141
+ ```bash
142
+ $ pg_easy_replicate bootstrap --group-name database-cluster-1 --special-user-role="rds_superuser"
143
+
144
+ {"name":"pg_easy_replicate","hostname":"PKHXQVK6DW","pid":21485,"level":30,"time":"2023-06-19T15:51:11.015-04:00","v":0,"msg":"Setting up schema","version":"0.1.0"}
145
+ ...
146
+ ```
147
+
119
148
  ### Start sync
120
149
 
121
150
  Once the bootstrap is complete, you can start the sync. Starting the sync sets up the publication, subscription and performs other minor housekeeping things.
data/bin/release.sh CHANGED
@@ -7,12 +7,12 @@ gem build pg_easy_replicate.gemspec
7
7
  echo "=== Pushing gem ===="
8
8
  gem push pg_easy_replicate-"$VERSION".gem
9
9
 
10
- echo "=== Sleeping for 5s ===="
11
- sleep 5
10
+ echo "=== Sleeping for 15s ===="
11
+ sleep 15
12
12
 
13
- # echo "=== Pushing tags to github ===="
14
- # git tag v"$VERSION"
15
- # git push origin --tags
13
+ echo "=== Pushing tags to github ===="
14
+ git tag v"$VERSION"
15
+ git push origin --tags
16
16
 
17
- # echo "=== Cleaning up ===="
18
- # rm pg_easy_replicate-"$VERSION".gem
17
+ echo "=== Cleaning up ===="
18
+ rm pg_easy_replicate-"$VERSION".gem
data/docker-compose.yml CHANGED
@@ -6,7 +6,7 @@ services:
6
6
  - "5432:5432"
7
7
  environment:
8
8
  POSTGRES_USER: jamesbond
9
- POSTGRES_PASSWORD: jamesbond
9
+ POSTGRES_PASSWORD: jamesbond123@7!'3aaR
10
10
  POSTGRES_DB: postgres
11
11
  command: >
12
12
  -c wal_level=logical
@@ -22,7 +22,7 @@ services:
22
22
  - "5433:5432"
23
23
  environment:
24
24
  POSTGRES_USER: jamesbond
25
- POSTGRES_PASSWORD: jamesbond
25
+ POSTGRES_PASSWORD: jamesbond123@7!'3aaR
26
26
  POSTGRES_DB: postgres
27
27
  command: >
28
28
  -c wal_level=logical
@@ -8,8 +8,14 @@ module PgEasyReplicate
8
8
 
9
9
  desc "config_check",
10
10
  "Prints if source and target database have the required config"
11
- def config_check
12
- PgEasyReplicate.assert_config
11
+ method_option :special_user_role,
12
+ aliases: "-s",
13
+ desc:
14
+ "Name of the role that has superuser permissions. Usually useful for AWS (rds_superuser) or GCP (cloudsqlsuperuser)."
15
+ def config_check(options)
16
+ PgEasyReplicate.assert_config(
17
+ special_user_role: options[:special_user_role],
18
+ )
13
19
 
14
20
  puts "✅ Config is looking good."
15
21
  end
@@ -18,6 +24,10 @@ module PgEasyReplicate
18
24
  aliases: "-g",
19
25
  required: true,
20
26
  desc: "Name of the group to provision"
27
+ method_option :special_user_role,
28
+ aliases: "-s",
29
+ desc:
30
+ "Name of the role that has superuser permissions. Usually useful with AWS (rds_superuser) or GCP (cloudsqlsuperuser)."
21
31
  desc "bootstrap",
22
32
  "Sets up temporary tables for information required during runtime"
23
33
  def bootstrap
@@ -60,6 +60,10 @@ module PgEasyReplicate
60
60
  connection_info(url)[:user]
61
61
  end
62
62
 
63
+ def db_name(url)
64
+ connection_info(url)[:dbname]
65
+ end
66
+
63
67
  def abort_with(msg)
64
68
  raise(msg) if test_env?
65
69
  abort(msg)
@@ -9,8 +9,6 @@ module PgEasyReplicate
9
9
  DEFAULT_WAIT = 5 # seconds
10
10
 
11
11
  def start_sync(options)
12
- PgEasyReplicate.assert_config
13
-
14
12
  create_publication(
15
13
  group_name: options[:group_name],
16
14
  conn_string: source_db_url,
@@ -62,10 +60,14 @@ module PgEasyReplicate
62
60
  "Setting up publication",
63
61
  { publication_name: publication_name(group_name) },
64
62
  )
63
+
65
64
  Query.run(
66
65
  query: "create publication #{publication_name(group_name)}",
67
66
  connection_url: conn_string,
67
+ user: db_user(conn_string),
68
68
  )
69
+ rescue => e
70
+ raise "Unable to create publication: #{e.message}"
69
71
  end
70
72
 
71
73
  def add_tables_to_publication(
@@ -91,6 +93,8 @@ module PgEasyReplicate
91
93
  schema: schema,
92
94
  )
93
95
  end
96
+ rescue => e
97
+ raise "Unable to add tables to publication: #{e.message}"
94
98
  end
95
99
 
96
100
  def list_all_tables(schema:, conn_string:)
@@ -112,7 +116,10 @@ module PgEasyReplicate
112
116
  Query.run(
113
117
  query: "DROP PUBLICATION IF EXISTS #{publication_name(group_name)}",
114
118
  connection_url: conn_string,
119
+ user: db_user(conn_string),
115
120
  )
121
+ rescue => e
122
+ raise "Unable to drop publication: #{e.message}"
116
123
  end
117
124
 
118
125
  def create_subscription(
@@ -132,6 +139,7 @@ module PgEasyReplicate
132
139
  query:
133
140
  "CREATE SUBSCRIPTION #{subscription_name(group_name)} CONNECTION '#{source_conn_string}' PUBLICATION #{publication_name(group_name)}",
134
141
  connection_url: target_conn_string,
142
+ user: db_user(target_conn_string),
135
143
  transaction: false,
136
144
  )
137
145
  rescue Sequel::DatabaseError => e
@@ -141,7 +149,7 @@ module PgEasyReplicate
141
149
  )
142
150
  end
143
151
 
144
- raise
152
+ raise "Unable to create subscription: #{e.message}"
145
153
  end
146
154
 
147
155
  def drop_subscription(group_name:, target_conn_string:)
@@ -157,11 +165,11 @@ module PgEasyReplicate
157
165
  connection_url: target_conn_string,
158
166
  transaction: false,
159
167
  )
168
+ rescue => e
169
+ raise "Unable to drop subscription: #{e.message}"
160
170
  end
161
171
 
162
172
  def stop_sync(target_conn_string:, source_conn_string:, group_name:)
163
- PgEasyReplicate.assert_config
164
-
165
173
  logger.info(
166
174
  "Stopping sync",
167
175
  {
@@ -177,6 +185,8 @@ module PgEasyReplicate
177
185
  group_name: group_name,
178
186
  target_conn_string: target_conn_string,
179
187
  )
188
+ rescue => e
189
+ raise "Unable to stop sync user: #{e.message}"
180
190
  end
181
191
 
182
192
  def switchover(
@@ -185,7 +195,6 @@ module PgEasyReplicate
185
195
  target_conn_string: target_db_url,
186
196
  lag_delta_size: DEFAULT_LAG
187
197
  )
188
- PgEasyReplicate.assert_config
189
198
  group = Group.find(group_name)
190
199
 
191
200
  watch_lag(group_name: group_name, lag: lag_delta_size)
@@ -258,6 +267,8 @@ module PgEasyReplicate
258
267
  "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE usename = '#{db_user(source_db_url)}';"
259
268
 
260
269
  Query.run(query: kill_sql, connection_url: source_db_url)
270
+ rescue => e
271
+ raise "Unable to revoke connections on source db: #{e.message}"
261
272
  end
262
273
 
263
274
  def restore_connections_on_source_db(group_name)
@@ -298,6 +309,8 @@ module PgEasyReplicate
298
309
  SQL
299
310
 
300
311
  Query.run(query: sql, connection_url: conn_string, schema: schema)
312
+ rescue => e
313
+ raise "Unable to refresh sequences: #{e.message}"
301
314
  end
302
315
 
303
316
  def mark_switchover_complete(group_name)
@@ -13,7 +13,6 @@ module PgEasyReplicate
13
13
 
14
14
  class << self
15
15
  def object(group_name)
16
- PgEasyReplicate.assert_config
17
16
  stats = replication_stats(group_name)
18
17
  group = Group.find(group_name)
19
18
  {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgEasyReplicate
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "json"
4
4
  require "ougai"
5
- require "lockbox"
6
5
  require "pg"
7
6
  require "sequel"
8
7
 
@@ -22,17 +21,20 @@ module PgEasyReplicate
22
21
  extend Helper
23
22
 
24
23
  class << self
25
- def config
24
+ def config(special_user_role: nil)
26
25
  abort_with("SOURCE_DB_URL is missing") if source_db_url.nil?
27
26
  abort_with("TARGET_DB_URL is missing") if target_db_url.nil?
27
+
28
28
  @config ||=
29
29
  begin
30
30
  q =
31
31
  "select name, setting from pg_settings where name in ('max_wal_senders', 'max_worker_processes', 'wal_level', 'max_replication_slots', 'max_logical_replication_workers');"
32
32
 
33
33
  {
34
- source_db_is_superuser: is_super_user?(source_db_url),
35
- target_db_is_superuser: is_super_user?(target_db_url),
34
+ source_db_is_super_user:
35
+ is_super_user?(source_db_url, special_user_role),
36
+ target_db_is_super_user:
37
+ is_super_user?(target_db_url, special_user_role),
36
38
  source_db:
37
39
  Query.run(
38
40
  query: q,
@@ -51,33 +53,43 @@ module PgEasyReplicate
51
53
  end
52
54
  end
53
55
 
54
- def assert_config
55
- unless assert_wal_level_logical(config.dig(:source_db))
56
+ def assert_config(special_user_role: nil)
57
+ config_hash = config(special_user_role: special_user_role)
58
+
59
+ unless assert_wal_level_logical(config_hash.dig(:source_db))
56
60
  abort_with("WAL_LEVEL should be LOGICAL on source DB")
57
61
  end
58
62
 
59
- unless assert_wal_level_logical(config.dig(:target_db))
63
+ unless assert_wal_level_logical(config_hash.dig(:target_db))
60
64
  abort_with("WAL_LEVEL should be LOGICAL on target DB")
61
65
  end
62
66
 
63
- unless config.dig(:source_db_is_superuser)
64
- abort_with("User on source database should be a superuser")
67
+ unless config_hash.dig(:source_db_is_super_user)
68
+ abort_with("User on source database does not have super user privilege")
65
69
  end
66
70
 
67
- return if config.dig(:target_db_is_superuser)
68
- abort_with("User on target database should be a superuser")
71
+ return if config_hash.dig(:target_db_is_super_user)
72
+ abort_with("User on target database does not have super user privilege")
69
73
  end
70
74
 
71
75
  def bootstrap(options)
72
- assert_config
73
76
  logger.info("Setting up schema")
74
77
  setup_schema
75
78
 
76
79
  logger.info("Setting up replication user on source database")
77
- create_user(conn_string: source_db_url, group_name: options[:group_name])
80
+ create_user(
81
+ conn_string: source_db_url,
82
+ group_name: options[:group_name],
83
+ special_user_role: options[:special_user_role],
84
+ grant_permissions_on_schema: true,
85
+ )
78
86
 
79
87
  logger.info("Setting up replication user on target database")
80
- create_user(conn_string: target_db_url, group_name: options[:group_name])
88
+ create_user(
89
+ conn_string: target_db_url,
90
+ group_name: options[:group_name],
91
+ special_user_role: options[:special_user_role],
92
+ )
81
93
 
82
94
  logger.info("Setting up groups tables")
83
95
  Group.setup
@@ -123,7 +135,10 @@ module PgEasyReplicate
123
135
  query: "DROP SCHEMA IF EXISTS #{internal_schema_name} CASCADE",
124
136
  connection_url: source_db_url,
125
137
  schema: internal_schema_name,
138
+ user: db_user(target_db_url),
126
139
  )
140
+ rescue => e
141
+ raise "Unable to drop schema: #{e.message}"
127
142
  end
128
143
 
129
144
  def setup_schema
@@ -139,6 +154,8 @@ module PgEasyReplicate
139
154
  schema: internal_schema_name,
140
155
  user: db_user(target_db_url),
141
156
  )
157
+ rescue => e
158
+ raise "Unable to setup schema: #{e.message}"
142
159
  end
143
160
 
144
161
  def logger
@@ -160,38 +177,109 @@ module PgEasyReplicate
160
177
  end
161
178
  end
162
179
 
163
- def is_super_user?(url)
164
- Query.run(
165
- query:
166
- "select usesuper from pg_user where usename = '#{db_user(url)}';",
167
- connection_url: url,
168
- user: db_user(target_db_url),
169
- ).first[
170
- :usesuper
171
- ]
180
+ def is_super_user?(url, special_user_role = nil)
181
+ if special_user_role
182
+ sql = <<~SQL
183
+ SELECT r.rolname AS username,
184
+ r1.rolname AS "role"
185
+ FROM pg_catalog.pg_roles r
186
+ LEFT JOIN pg_catalog.pg_auth_members m ON (m.member = r.oid)
187
+ LEFT JOIN pg_roles r1 ON (m.roleid=r1.oid)
188
+ WHERE r.rolname = '#{db_user(url)}'
189
+ ORDER BY 1;
190
+ SQL
191
+
192
+ r =
193
+ Query.run(
194
+ query: sql,
195
+ connection_url: url,
196
+ user: db_user(target_db_url),
197
+ )
198
+ # If special_user_role is passed just ensure the url in conn_string has been granted
199
+ # the special_user_role
200
+ r.any? { |q| q[:role] == special_user_role }
201
+ else
202
+ r =
203
+ Query.run(
204
+ query:
205
+ "SELECT rolname, rolsuper FROM pg_roles where rolname = '#{db_user(url)}';",
206
+ connection_url: url,
207
+ user: db_user(target_db_url),
208
+ )
209
+ r.any? { |q| q[:rolsuper] }
210
+ end
211
+ rescue => e
212
+ raise "Unable to check superuser conditions: #{e.message}"
172
213
  end
173
214
 
174
- def create_user(conn_string:, group_name:)
175
- password = connection_info(conn_string)[:user]
215
+ def create_user(
216
+ conn_string:,
217
+ group_name:,
218
+ special_user_role: nil,
219
+ grant_permissions_on_schema: false
220
+ )
221
+ password = connection_info(conn_string)[:password].gsub("'") { "''" }
222
+
176
223
  sql = <<~SQL
177
224
  drop role if exists #{internal_user_name};
178
- create role #{internal_user_name} with password '#{password}' login superuser createdb createrole;
225
+ create role #{internal_user_name} with password '#{password}' login createdb createrole;
226
+ grant all privileges on database #{db_name(conn_string)} TO #{internal_user_name};
179
227
  SQL
180
228
 
181
229
  Query.run(
182
230
  query: sql,
183
231
  connection_url: conn_string,
184
- user: db_user(target_db_url),
232
+ user: db_user(conn_string),
233
+ transaction: false,
234
+ )
235
+
236
+ sql =
237
+ if special_user_role
238
+ "grant #{special_user_role} to #{internal_user_name};"
239
+ else
240
+ "alter user #{internal_user_name} with superuser;"
241
+ end
242
+
243
+ Query.run(
244
+ query: sql,
245
+ connection_url: conn_string,
246
+ user: db_user(conn_string),
247
+ transaction: false,
185
248
  )
249
+
250
+ return unless grant_permissions_on_schema
251
+ Query.run(
252
+ query:
253
+ "grant all on schema #{internal_schema_name} to #{internal_user_name}",
254
+ connection_url: conn_string,
255
+ user: db_user(conn_string),
256
+ transaction: false,
257
+ )
258
+ rescue => e
259
+ raise "Unable to create user: #{e.message}"
186
260
  end
187
261
 
188
262
  def drop_user(conn_string:, group_name:)
189
- sql = "drop role if exists #{internal_user_name};"
263
+ sql = <<~SQL
264
+ revoke all privileges on database #{db_name(conn_string)} from #{internal_user_name};
265
+ SQL
190
266
  Query.run(
191
267
  query: sql,
192
268
  connection_url: conn_string,
193
269
  user: db_user(conn_string),
194
270
  )
271
+
272
+ sql = <<~SQL
273
+ drop role if exists #{internal_user_name};
274
+ SQL
275
+
276
+ Query.run(
277
+ query: sql,
278
+ connection_url: conn_string,
279
+ user: db_user(conn_string),
280
+ )
281
+ rescue => e
282
+ raise "Unable to drop user: #{e.message}"
195
283
  end
196
284
  end
197
285
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_easy_replicate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shayon Mukherjee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-23 00:00:00.000000000 Z
11
+ date: 2023-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: lockbox
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: 1.2.0
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: 1.2.0
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: ougai
29
15
  requirement: !ruby/object:Gem::Requirement