kamal-backup 0.4.2 → 0.5.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0442e12748359ee182d6fc91e5573375440c31a5a65ccf1b46079547b8e4bc21
4
- data.tar.gz: 9af03ddf3ad23b99b77c717711dc66cde34bc17500e66e776aff50ca2678e653
3
+ metadata.gz: 0ba0b661680e0b8885935b4697cc40052f1584a2c39b0646d2e939d5874f043a
4
+ data.tar.gz: 3ac2499e70f17ac20b30eaba0bc507e6558fbf1406d8423e32fb1f44c642e3b8
5
5
  SHA512:
6
- metadata.gz: ccd324da250d8a36f623a6c65c9cf000c80e4fbcc0308944c1074c67ce9422e1e637c55e672160399078944c83c281ccec25ff17a63d1181ed28045244a2d674
7
- data.tar.gz: 80f64be478843d1695cdfa34c899ba614ee7ed45a10853f521c08ee568f003f3b0045809ef1ff0c1217e5ec834d2a607888963998af64f9643886048f0e65bf1
6
+ metadata.gz: 61ff158f87e67167f446b540af94a4193657bf02f8a435d7ffb9465092c32cde6a9b82b4a616fb8571454ddde085915a051be312748ef0d1e2c8647d80f159ee
7
+ data.tar.gz: e872f59bb07fa089fcf4477c6b2d1f315d8a6d1fed164366d52a8475753c90a951caf46b929ff71ec4b5056160b3f8f8661a833ce0c8ecfaa2a0269d2c03b253
@@ -32,6 +32,7 @@ module KamalBackup
32
32
  def backup(force: false)
33
33
  started_at = Time.now.utc
34
34
  config.validate_backup
35
+ return disabled_backup_result(started_at) unless config.backup_enabled? || force
35
36
  return skipped_backup_result(started_at) unless force || backup_due?(started_at)
36
37
 
37
38
  restic.ensure_repository
@@ -136,6 +137,18 @@ module KamalBackup
136
137
  due_at.nil? || now >= due_at
137
138
  end
138
139
 
140
+ def disabled_backup_result(now)
141
+ Schema.record(
142
+ kind: 'backup_result',
143
+ status: 'skipped',
144
+ reason: 'disabled',
145
+ last_backup_at: last_backup_finished_at&.iso8601,
146
+ next_backup_at: nil,
147
+ force_command: 'kamal-backup backup --force',
148
+ finished_at: now.iso8601
149
+ )
150
+ end
151
+
139
152
  def skipped_backup_result(now)
140
153
  Schema.record(
141
154
  kind: 'backup_result',
@@ -467,6 +480,10 @@ module KamalBackup
467
480
  config.validate_restic
468
481
  config.validate_database_backup
469
482
  config.validate_backup_paths
483
+ # Check writability up front. This used to be discovered only after
484
+ # restic had pulled the entire file snapshot, so a read-only mount cost a
485
+ # full download before failing — the wrong moment to learn about it.
486
+ config.backup_paths.each { |path| ensure_writable_restore_target(path) }
470
487
  end
471
488
 
472
489
  def validate_production_drill(file_target, database_name, sqlite_path)
@@ -140,6 +140,13 @@ module KamalBackup
140
140
  end
141
141
 
142
142
  def print_backup_result(result)
143
+ if result[:status] == 'skipped' && result[:reason] == 'disabled'
144
+ puts('Backups are disabled for this app (backup.enabled is false).')
145
+ puts('Set backup.enabled to true and redeploy the accessory to resume scheduled backups.')
146
+ puts("Run `#{result.fetch(:force_command)}` to take one anyway.")
147
+ return
148
+ end
149
+
143
150
  if result[:status] == 'skipped'
144
151
  puts("No backup due. Last backup finished at #{result.fetch(:last_backup_at)}.")
145
152
  puts("Next backup is due at #{result.fetch(:next_backup_at)}.")
@@ -168,6 +168,10 @@ module KamalBackup
168
168
  truthy?('KAMAL_BACKUP_ALLOW_SUSPICIOUS_PATHS')
169
169
  end
170
170
 
171
+ def backup_enabled?
172
+ !falsey?('BACKUP_ENABLED')
173
+ end
174
+
171
175
  def backup_schedule_seconds
172
176
  integer('BACKUP_SCHEDULE_SECONDS', 86_400, minimum: 1)
173
177
  end
@@ -271,6 +271,7 @@ module KamalBackup
271
271
  hash = require_mapping(raw_value, "#{@path} backup")
272
272
  env = {}
273
273
  env['BACKUP_SCHEDULE_SECONDS'] = normalize_duration(hash['schedule'], "#{@path} backup.schedule") if hash.key?('schedule')
274
+ env['BACKUP_ENABLED'] = normalize_value(hash['enabled']) if hash.key?('enabled')
274
275
  env.compact
275
276
  end
276
277
 
@@ -34,6 +34,28 @@ module KamalBackup
34
34
  CommandSpec.new(argv: argv, env: current_connection)
35
35
  end
36
36
 
37
+ # Replace the target schema before restoring.
38
+ #
39
+ # pg_restore --clean emits DROP TABLE for each object in the dump, but
40
+ # PostgreSQL refuses to drop a table another table still references, and
41
+ # the dump's ordering cannot account for constraints that only exist in
42
+ # the target. Restoring over a schema Rails already created — which is
43
+ # every restore onto a freshly deployed host, since Kamal runs db:prepare
44
+ # on boot — leaves the drops failing, the CREATEs failing as "already
45
+ # exists", the COPYs never running, and the foreign keys rejected because
46
+ # the tables they point at are still empty.
47
+ #
48
+ # Dropping the schema first sidesteps the ordering problem entirely. This
49
+ # only runs for restore-to-current, which already means "replace this
50
+ # database"; scratch restores target a separate database and are
51
+ # untouched.
52
+ def restore_to_current(restic, snapshot, filename)
53
+ reset_current_schema
54
+ result = super
55
+ ensure_restore_reported_no_errors(result)
56
+ result
57
+ end
58
+
37
59
  def current_restore_command
38
60
  connection = current_connection
39
61
  database = connection.fetch('PGDATABASE')
@@ -61,6 +83,38 @@ module KamalBackup
61
83
 
62
84
  private
63
85
 
86
+ # pg_restore exits 0 even when individual objects fail, reporting only
87
+ # "errors ignored on restore: N" on stderr. A restore that half-applied
88
+ # and claimed success is worse than one that failed, because the damage
89
+ # is only discovered later, from the data.
90
+ def ensure_restore_reported_no_errors(result)
91
+ stderr = result.respond_to?(:stderr) ? result.stderr.to_s : ''
92
+ match = stderr.match(/errors ignored on restore:\s*(\d+)/i)
93
+ return if match.nil? || match[1].to_i.zero?
94
+
95
+ raise CommandError.new(
96
+ "pg_restore reported #{match[1]} ignored error(s); the database is only partially restored. " \
97
+ 'Inspect the output above, resolve the cause, and restore again.',
98
+ command: current_restore_command,
99
+ stderr: stderr
100
+ )
101
+ end
102
+
103
+ def reset_current_schema
104
+ connection = current_connection
105
+ argv = ['psql', '--quiet', '--no-psqlrc', '--set', 'ON_ERROR_STOP=1', '--command', RESET_SCHEMA_SQL]
106
+ Command.capture(CommandSpec.new(argv: argv, env: connection), redactor: redactor)
107
+ rescue CommandError => e
108
+ raise CommandError.new(
109
+ "failed to reset the public schema before restoring: #{e.message}",
110
+ command: e.command,
111
+ stderr: e.stderr
112
+ )
113
+ end
114
+
115
+ RESET_SCHEMA_SQL = 'DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;'
116
+ private_constant :RESET_SCHEMA_SQL
117
+
64
118
  def validate_scratch_restore_target(target)
65
119
  raise ConfigurationError, 'scratch database must differ from the current PostgreSQL database' if current_connection.fetch('PGDATABASE') == target
66
120
 
@@ -15,6 +15,17 @@ module KamalBackup
15
15
  $stderr.sync = true
16
16
 
17
17
  install_signal_handlers
18
+
19
+ unless @config.backup_enabled?
20
+ # Stay up rather than exit: the accessory is still how restore, list, check and
21
+ # evidence reach the repository, and exiting would restart-loop under Kamal's
22
+ # `--restart unless-stopped`. Set backup.enabled to true and redeploy to resume.
23
+ log('scheduled backups are disabled (backup.enabled is false); no backups will be taken')
24
+ idle_until_stopped
25
+ log("scheduler stopped at #{Time.now.utc.iso8601}")
26
+ return
27
+ end
28
+
18
29
  sleep_interruptibly(@config.backup_start_delay_seconds)
19
30
 
20
31
  until @stop
@@ -43,6 +54,10 @@ module KamalBackup
43
54
  end
44
55
  end
45
56
 
57
+ def idle_until_stopped
58
+ sleep 1 until @stop
59
+ end
60
+
46
61
  def sleep_interruptibly(seconds)
47
62
  deadline = Time.now + seconds
48
63
  sleep([deadline - Time.now, 1].min) while !@stop && Time.now < deadline
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KamalBackup
4
- VERSION = '0.4.2'
4
+ VERSION = '0.5.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - crmne