kamal-backup 0.5.0 → 0.5.2

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: aa8276219db8c5d8aa820e85e0f289f87715889cb5ea18ae2c99e3e12bbfe6b9
4
- data.tar.gz: 626824ed56029bf3b713676d3376ab38d571942ee3809e7cf18abbfde9c24a45
3
+ metadata.gz: 341913561478a45fc1dd88b376daae6977a180eba43739963c6e9055d5c064ab
4
+ data.tar.gz: 69a572c55cc9b33fcd0ab0a3918111cf7c3e26f3519d01de855c2fa73de08b2d
5
5
  SHA512:
6
- metadata.gz: 75d362d35409c0447c29994a7bf4d23ac47d287fc88621eb46f32a56296537217cee76a28799983f45154361ecd61e4f553b609144969bed2b10016bf0ecc09c
7
- data.tar.gz: e5f664a15123066441b940d945caed2335d8b50356acaa64d9ae6f49d87a7705894234e55db42ec6508aa188f727b01d4e4d4c71688ecd93b4dbf4c12d53ef5d
6
+ metadata.gz: af86080b3f6524990cd0df83bed996370e4cff186aaea4338e8ab9e9ddfb595313d58b96dd4f9fc0a17b758ff8869a7fd836840370bfe76b94dcad5d195b7455
7
+ data.tar.gz: 257939e10db7512ea4f12d0bd23d8ba472a41c6987ef974cc69c978653491b0e4b107a7e312fb34431745a5b855dae6919a58e8cdd626e8277c9276030a5a6d6
data/README.md CHANGED
@@ -125,7 +125,7 @@ bundle exec kamal-backup evidence
125
125
 
126
126
  - **Scheduled backups:** the accessory runs continuously and backs up on `backup.schedule`.
127
127
  - **Database and Active Storage coverage:** database dumps plus file-backed Active Storage files from mounted volumes.
128
- - **Restic underneath:** encrypted, deduplicated snapshots in S3-compatible storage, a restic REST server, or a filesystem repository.
128
+ - **Restic underneath:** encrypted, deduplicated snapshots in S3-compatible storage, over SFTP, in a restic REST server, or in a filesystem repository.
129
129
  - **Local restores:** inspect production data safely in your local Rails app.
130
130
  - **Restore drills:** restore into scratch production-side targets, run verification commands, and record the result.
131
131
  - **Security review evidence:** `kamal-backup evidence` prints redacted JSON with latest snapshots, `kamal-backup check` results, drills, retention, and tool versions.
@@ -480,6 +480,10 @@ module KamalBackup
480
480
  config.validate_restic
481
481
  config.validate_database_backup
482
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) }
483
487
  end
484
488
 
485
489
  def validate_production_drill(file_target, database_name, sqlite_path)
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KamalBackup
4
- VERSION = '0.5.0'
4
+ VERSION = '0.5.2'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - crmne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-10 00:00:00.000000000 Z
11
+ date: 2026-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor