kamal-backup 0.3.0 → 0.4.0
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/README.md +2 -0
- data/lib/kamal_backup/app.rb +150 -202
- data/lib/kamal_backup/cli/helpers.rb +298 -0
- data/lib/kamal_backup/cli.rb +13 -294
- data/lib/kamal_backup/command.rb +11 -187
- data/lib/kamal_backup/command_output.rb +189 -0
- data/lib/kamal_backup/config.rb +77 -481
- data/lib/kamal_backup/config_file.rb +376 -0
- data/lib/kamal_backup/databases/base.rb +11 -0
- data/lib/kamal_backup/databases/postgres.rb +3 -3
- data/lib/kamal_backup/evidence.rb +0 -3
- data/lib/kamal_backup/kamal_bridge.rb +39 -27
- data/lib/kamal_backup/rails_app.rb +6 -17
- data/lib/kamal_backup/restic.rb +46 -45
- data/lib/kamal_backup/version.rb +1 -1
- data/lib/kamal_backup/yaml_access.rb +13 -0
- data/lib/kamal_backup.rb +3 -0
- metadata +48 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b93c203599481320559eb006ccf07a07c010c5bc4d5dfcae3e11b7a9a832fa3
|
|
4
|
+
data.tar.gz: 705fc119fcacdd54ee431e9cfb5b88220a0faae02540cba77e38c45b94044905
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd7b30595fe843980d39979b8b7e241fbd54c7fd814c55bccb4bfcdf6b175b5767e84910929050f4e3aa40f150aec5b01e15015b196e522e3c48566f9527020f
|
|
7
|
+
data.tar.gz: e81f5d21997f6f072b9556a73b7e751accbe99098adaebaec3435ca15600e8d13f204b9d343cc3ae7f6905a86efd3abea3c5dd0d64ea602bd2f2617d8bca8fef
|
data/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://rubygems.org/gems/kamal-backup)
|
|
10
10
|
[](https://github.com/crmne/kamal-backup/actions/workflows/ci.yml)
|
|
11
|
+
[](https://codecov.io/gh/crmne/kamal-backup)
|
|
11
12
|
[](https://github.com/crmne/kamal-backup/pkgs/container/kamal-backup)
|
|
12
13
|
[](https://kamal-backup.dev)
|
|
13
14
|
[](LICENSE)
|
|
@@ -110,6 +111,7 @@ Run the first backup, check the repository, and print evidence. From an app chec
|
|
|
110
111
|
bundle exec kamal-backup backup
|
|
111
112
|
bundle exec kamal-backup list
|
|
112
113
|
bundle exec kamal-backup check
|
|
114
|
+
bundle exec kamal-backup unlock
|
|
113
115
|
bundle exec kamal-backup evidence
|
|
114
116
|
```
|
|
115
117
|
|
data/lib/kamal_backup/app.rb
CHANGED
|
@@ -22,14 +22,11 @@ module KamalBackup
|
|
|
22
22
|
|
|
23
23
|
attr_reader :config, :redactor
|
|
24
24
|
|
|
25
|
-
def initialize(env: ENV, config: nil, redactor: nil, restic: nil, database: nil
|
|
26
|
-
scheduler_class: Scheduler)
|
|
25
|
+
def initialize(env: ENV, config: nil, redactor: nil, restic: nil, database: nil)
|
|
27
26
|
@config = config || Config.new(env: env)
|
|
28
27
|
@redactor = redactor || Redactor.new(env: @config.env)
|
|
29
28
|
@restic = restic
|
|
30
29
|
@database = database
|
|
31
|
-
@evidence_class = evidence_class
|
|
32
|
-
@scheduler_class = scheduler_class
|
|
33
30
|
end
|
|
34
31
|
|
|
35
32
|
def backup(force: false)
|
|
@@ -37,13 +34,11 @@ module KamalBackup
|
|
|
37
34
|
config.validate_backup
|
|
38
35
|
return skipped_backup_result(started_at) unless force || backup_due?(started_at)
|
|
39
36
|
|
|
40
|
-
require_restic!
|
|
41
|
-
|
|
42
37
|
restic.ensure_repository
|
|
43
38
|
databases.each { |database| database.backup(restic) }
|
|
44
39
|
restic.backup_paths(config.backup_paths, tags: ['type:files'])
|
|
45
40
|
|
|
46
|
-
restic.
|
|
41
|
+
restic.prune if config.forget_after_backup?
|
|
47
42
|
|
|
48
43
|
restic.check if config.check_after_backup?
|
|
49
44
|
|
|
@@ -60,46 +55,39 @@ module KamalBackup
|
|
|
60
55
|
|
|
61
56
|
def restore_to_local_machine(snapshot = 'latest')
|
|
62
57
|
validate_local_machine_restore
|
|
63
|
-
require_restic!
|
|
64
58
|
|
|
65
59
|
build_restore_result('local', snapshot) do |result|
|
|
66
60
|
databases.each { |adapter| validate_local_machine_database_target(adapter) }
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
assign_restore_results(result, database_results, file_results)
|
|
61
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
62
|
+
result[:files] = perform_replacement_file_restore(snapshot, production_source: false)
|
|
70
63
|
end
|
|
71
64
|
end
|
|
72
65
|
|
|
73
66
|
def restore_to_production(snapshot = 'latest')
|
|
74
67
|
validate_production_restore
|
|
75
|
-
require_restic!
|
|
76
68
|
|
|
77
69
|
build_restore_result('production', snapshot) do |result|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
assign_restore_results(result, database_results, file_results)
|
|
70
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
71
|
+
result[:files] = perform_replacement_file_restore(snapshot, production_source: true)
|
|
81
72
|
end
|
|
82
73
|
end
|
|
83
74
|
|
|
84
75
|
def drill_on_local_machine(snapshot = 'latest', check_command: nil)
|
|
85
76
|
validate_local_machine_restore
|
|
86
|
-
require_restic!
|
|
87
77
|
|
|
88
78
|
run_drill('local', snapshot, check_command: check_command) do |result|
|
|
89
79
|
databases.each { |adapter| validate_local_machine_database_target(adapter) }
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
assign_restore_results(result, database_results, file_results)
|
|
80
|
+
result[:databases] = perform_database_restores_to_current(snapshot)
|
|
81
|
+
result[:files] = perform_replacement_file_restore(snapshot, production_source: false)
|
|
93
82
|
end
|
|
94
83
|
end
|
|
95
84
|
|
|
96
85
|
def drill_on_production(snapshot = 'latest', database_name: nil, sqlite_path: nil, file_target: '/restore/files',
|
|
97
86
|
check_command: nil)
|
|
98
87
|
validate_production_drill(file_target, database_name, sqlite_path)
|
|
99
|
-
require_restic!
|
|
100
88
|
|
|
101
89
|
run_drill('production', snapshot, check_command: check_command) do |result|
|
|
102
|
-
|
|
90
|
+
result[:databases] = databases.map do |adapter|
|
|
103
91
|
perform_database_restore_to_scratch(
|
|
104
92
|
snapshot,
|
|
105
93
|
adapter: adapter,
|
|
@@ -107,44 +95,38 @@ module KamalBackup
|
|
|
107
95
|
sqlite_path: sqlite_path
|
|
108
96
|
)
|
|
109
97
|
end
|
|
110
|
-
|
|
111
|
-
assign_restore_results(result, database_results, file_results)
|
|
98
|
+
result[:files] = perform_file_restore(snapshot, target: file_target)
|
|
112
99
|
end
|
|
113
100
|
end
|
|
114
101
|
|
|
115
|
-
def drill_failed?(result)
|
|
116
|
-
result.fetch(:status) != 'ok'
|
|
117
|
-
rescue KeyError
|
|
118
|
-
true
|
|
119
|
-
end
|
|
120
|
-
|
|
121
102
|
def snapshots
|
|
122
103
|
config.validate_restic
|
|
123
|
-
require_restic!
|
|
124
104
|
restic.snapshots.stdout
|
|
125
105
|
end
|
|
126
106
|
|
|
127
107
|
def check
|
|
128
108
|
config.validate_restic
|
|
129
|
-
require_restic!
|
|
130
109
|
restic.check.stdout
|
|
131
110
|
end
|
|
132
111
|
|
|
112
|
+
def unlock
|
|
113
|
+
config.validate_restic
|
|
114
|
+
restic.unlock.stdout
|
|
115
|
+
end
|
|
116
|
+
|
|
133
117
|
def prune
|
|
134
118
|
config.validate_backup(check_files: false)
|
|
135
|
-
require_restic!
|
|
136
119
|
restic.prune
|
|
137
120
|
end
|
|
138
121
|
|
|
139
122
|
def evidence
|
|
140
123
|
config.validate_restic
|
|
141
|
-
|
|
142
|
-
@evidence_class.new(config, restic: restic, redactor: redactor).to_json
|
|
124
|
+
Evidence.new(config, restic: restic, redactor: redactor).to_json
|
|
143
125
|
end
|
|
144
126
|
|
|
145
127
|
def schedule
|
|
146
128
|
config.validate_backup
|
|
147
|
-
|
|
129
|
+
Scheduler.new(config) { backup(force: true) }.run
|
|
148
130
|
end
|
|
149
131
|
|
|
150
132
|
private
|
|
@@ -155,7 +137,7 @@ module KamalBackup
|
|
|
155
137
|
end
|
|
156
138
|
|
|
157
139
|
def skipped_backup_result(now)
|
|
158
|
-
|
|
140
|
+
Schema.record(
|
|
159
141
|
kind: 'backup_result',
|
|
160
142
|
status: 'skipped',
|
|
161
143
|
reason: 'not_due',
|
|
@@ -163,7 +145,7 @@ module KamalBackup
|
|
|
163
145
|
next_backup_at: next_backup_at&.iso8601,
|
|
164
146
|
force_command: 'kamal-backup backup --force',
|
|
165
147
|
finished_at: now.iso8601
|
|
166
|
-
|
|
148
|
+
)
|
|
167
149
|
end
|
|
168
150
|
|
|
169
151
|
def next_backup_at
|
|
@@ -172,7 +154,7 @@ module KamalBackup
|
|
|
172
154
|
|
|
173
155
|
def last_backup_finished_at
|
|
174
156
|
@last_backup_finished_at ||= begin
|
|
175
|
-
value = last_backup_record['finished_at']
|
|
157
|
+
value = last_backup_record['finished_at']
|
|
176
158
|
value ? Time.parse(value.to_s).utc : nil
|
|
177
159
|
rescue ArgumentError
|
|
178
160
|
nil
|
|
@@ -187,6 +169,68 @@ module KamalBackup
|
|
|
187
169
|
end
|
|
188
170
|
end
|
|
189
171
|
|
|
172
|
+
def backup_summary(started_at:, finished_at:)
|
|
173
|
+
Schema.record(
|
|
174
|
+
kind: 'backup_result',
|
|
175
|
+
status: 'ok',
|
|
176
|
+
started_at: started_at.iso8601,
|
|
177
|
+
finished_at: finished_at.iso8601,
|
|
178
|
+
databases: databases.map { |adapter| backup_snapshot_summary(adapter) },
|
|
179
|
+
files: backup_file_snapshot_summary
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def backup_snapshot_summary(adapter)
|
|
184
|
+
snapshot = restic.latest_snapshot(tags: database_snapshot_tags(adapter))
|
|
185
|
+
snapshot_summary(snapshot).merge(
|
|
186
|
+
database: database_config_name(adapter),
|
|
187
|
+
adapter: adapter.adapter_name
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def backup_file_snapshot_summary
|
|
192
|
+
return nil if config.backup_paths.empty?
|
|
193
|
+
|
|
194
|
+
snapshot_summary(restic.latest_snapshot(tags: ['type:files']))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def snapshot_summary(snapshot)
|
|
198
|
+
{
|
|
199
|
+
snapshot: snapshot && (snapshot['short_id'] || snapshot['id']),
|
|
200
|
+
time: snapshot && snapshot['time']
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def validate_fresh_backup_summary!(summary, started_at:)
|
|
205
|
+
stale_databases = summary.fetch(:databases).reject { |entry| fresh_snapshot?(entry, started_at) }
|
|
206
|
+
|
|
207
|
+
unless stale_databases.empty?
|
|
208
|
+
names = stale_databases.map { |entry| entry.fetch(:database) }.join(', ')
|
|
209
|
+
raise ConfigurationError, "backup did not create a fresh database snapshot for #{names}"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
files = summary[:files]
|
|
213
|
+
return unless files && !fresh_snapshot?(files, started_at)
|
|
214
|
+
|
|
215
|
+
raise ConfigurationError, 'backup did not create a fresh file snapshot'
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def fresh_snapshot?(entry, started_at)
|
|
219
|
+
snapshot_time = Time.parse(entry[:time].to_s)
|
|
220
|
+
snapshot_time >= started_at - FRESH_BACKUP_GRACE_SECONDS
|
|
221
|
+
rescue ArgumentError
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def write_last_backup(result)
|
|
226
|
+
FileUtils.mkdir_p(config.state_dir)
|
|
227
|
+
File.write(config.last_backup_path, JSON.pretty_generate(result))
|
|
228
|
+
@last_backup_record = result.transform_keys(&:to_s)
|
|
229
|
+
@last_backup_finished_at = Time.parse(result.fetch(:finished_at)).utc
|
|
230
|
+
rescue SystemCallError, ArgumentError
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
190
234
|
def build_restore_result(scope, snapshot)
|
|
191
235
|
started_at = Time.now.utc
|
|
192
236
|
result = Schema.record(
|
|
@@ -197,10 +241,8 @@ module KamalBackup
|
|
|
197
241
|
started_at: started_at.iso8601,
|
|
198
242
|
finished_at: nil,
|
|
199
243
|
error: nil,
|
|
200
|
-
database: nil,
|
|
201
244
|
databases: [],
|
|
202
|
-
files: nil
|
|
203
|
-
paths: nil
|
|
245
|
+
files: nil
|
|
204
246
|
)
|
|
205
247
|
yield(result)
|
|
206
248
|
result[:finished_at] = Time.now.utc.iso8601
|
|
@@ -218,10 +260,8 @@ module KamalBackup
|
|
|
218
260
|
started_at: started_at.iso8601,
|
|
219
261
|
finished_at: nil,
|
|
220
262
|
error: nil,
|
|
221
|
-
database: nil,
|
|
222
263
|
databases: [],
|
|
223
264
|
files: nil,
|
|
224
|
-
paths: nil,
|
|
225
265
|
check: nil
|
|
226
266
|
)
|
|
227
267
|
|
|
@@ -247,6 +287,41 @@ module KamalBackup
|
|
|
247
287
|
result
|
|
248
288
|
end
|
|
249
289
|
|
|
290
|
+
def drill_operator
|
|
291
|
+
config.value('USER') || config.value('USERNAME')
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def run_drill_check(command)
|
|
295
|
+
result = Command.capture(
|
|
296
|
+
CommandSpec.new(argv: ['sh', '-lc', command]),
|
|
297
|
+
redactor: redactor
|
|
298
|
+
)
|
|
299
|
+
output = result.stdout.empty? ? result.stderr : result.stdout
|
|
300
|
+
|
|
301
|
+
{
|
|
302
|
+
status: 'ok',
|
|
303
|
+
command: redactor.redact_string(command),
|
|
304
|
+
output: redactor.redact_string(output.strip)
|
|
305
|
+
}
|
|
306
|
+
rescue CommandError => e
|
|
307
|
+
{
|
|
308
|
+
status: 'failed',
|
|
309
|
+
command: redactor.redact_string(command),
|
|
310
|
+
error: redactor.redact_string(e.message)
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def write_last_restore_drill(payload)
|
|
315
|
+
FileUtils.mkdir_p(config.state_dir)
|
|
316
|
+
File.write(config.last_restore_drill_path, JSON.pretty_generate(payload))
|
|
317
|
+
rescue SystemCallError
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def perform_database_restores_to_current(snapshot)
|
|
322
|
+
databases.map { |adapter| perform_database_restore_to_current(snapshot, adapter: adapter) }
|
|
323
|
+
end
|
|
324
|
+
|
|
250
325
|
def perform_database_restore_to_current(snapshot, adapter:)
|
|
251
326
|
resolved_snapshot = resolve_snapshot(snapshot, tags: database_snapshot_tags(adapter))
|
|
252
327
|
filename = restic.database_file(resolved_snapshot, adapter.adapter_name,
|
|
@@ -270,114 +345,37 @@ module KamalBackup
|
|
|
270
345
|
summarize_database_restore(adapter, resolved_snapshot, filename, adapter.scratch_target_identifier(target))
|
|
271
346
|
end
|
|
272
347
|
|
|
273
|
-
def
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
source_paths: source_paths,
|
|
283
|
-
target_paths: config.backup_paths
|
|
284
|
-
)
|
|
285
|
-
]
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def assign_restore_results(result, database_results, file_results)
|
|
289
|
-
result[:databases] = database_results
|
|
290
|
-
result[:database] = database_results.first
|
|
291
|
-
result[:paths] = file_results.first
|
|
292
|
-
result[:files] = file_results.size == 1 ? file_results.first : file_results
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def backup_summary(started_at:, finished_at:)
|
|
296
|
-
{
|
|
297
|
-
kind: 'backup_result',
|
|
298
|
-
status: 'ok',
|
|
299
|
-
started_at: started_at.iso8601,
|
|
300
|
-
finished_at: finished_at.iso8601,
|
|
301
|
-
databases: databases.map { |adapter| backup_snapshot_summary(adapter) },
|
|
302
|
-
files: backup_file_snapshot_summary
|
|
303
|
-
}
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def backup_snapshot_summary(adapter)
|
|
307
|
-
snapshot = restic.latest_snapshot(tags: database_snapshot_tags(adapter))
|
|
308
|
-
snapshot_summary(snapshot).merge(
|
|
309
|
-
database: database_config_name(adapter),
|
|
310
|
-
adapter: adapter.adapter_name
|
|
311
|
-
)
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def backup_file_snapshot_summary
|
|
315
|
-
return nil if config.backup_paths.empty?
|
|
316
|
-
|
|
317
|
-
snapshot_summary(restic.latest_snapshot(tags: ['type:files']))
|
|
348
|
+
def scratch_database_target(adapter, database_name, sqlite_path)
|
|
349
|
+
case adapter.adapter_name
|
|
350
|
+
when 'sqlite'
|
|
351
|
+
target = sqlite_path || raise(ConfigurationError, 'scratch SQLite path is required')
|
|
352
|
+
databases.size > 1 ? File.join(target, "#{database_config_name(adapter)}.sqlite3") : target
|
|
353
|
+
else
|
|
354
|
+
target = database_name || raise(ConfigurationError, 'scratch database name is required')
|
|
355
|
+
databases.size > 1 ? "#{target}_#{database_config_name(adapter)}" : target
|
|
356
|
+
end
|
|
318
357
|
end
|
|
319
358
|
|
|
320
|
-
def
|
|
359
|
+
def summarize_database_restore(adapter, snapshot, filename, target)
|
|
321
360
|
{
|
|
322
|
-
snapshot: snapshot
|
|
323
|
-
|
|
361
|
+
snapshot: snapshot,
|
|
362
|
+
adapter: adapter.adapter_name,
|
|
363
|
+
filename: filename,
|
|
364
|
+
target: redactor.redact_string(target.to_s)
|
|
324
365
|
}
|
|
325
366
|
end
|
|
326
367
|
|
|
327
|
-
def validate_fresh_backup_summary!(summary, started_at:)
|
|
328
|
-
stale_databases = summary.fetch(:databases).reject { |entry| fresh_snapshot?(entry, started_at) }
|
|
329
|
-
|
|
330
|
-
unless stale_databases.empty?
|
|
331
|
-
names = stale_databases.map { |entry| entry.fetch(:database) }.join(', ')
|
|
332
|
-
raise ConfigurationError, "backup did not create a fresh database snapshot for #{names}"
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
files = summary[:files]
|
|
336
|
-
return unless files && !fresh_snapshot?(files, started_at)
|
|
337
|
-
|
|
338
|
-
raise ConfigurationError, 'backup did not create a fresh file snapshot'
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def fresh_snapshot?(entry, started_at)
|
|
342
|
-
snapshot_time = Time.parse(entry[:time].to_s)
|
|
343
|
-
snapshot_time >= started_at - FRESH_BACKUP_GRACE_SECONDS
|
|
344
|
-
rescue ArgumentError
|
|
345
|
-
false
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
def write_last_backup(result)
|
|
349
|
-
FileUtils.mkdir_p(config.state_dir)
|
|
350
|
-
File.write(config.last_backup_path, JSON.pretty_generate(result))
|
|
351
|
-
@last_backup_record = result.transform_keys(&:to_s)
|
|
352
|
-
@last_backup_finished_at = Time.parse(result.fetch(:finished_at)).utc
|
|
353
|
-
rescue SystemCallError, ArgumentError
|
|
354
|
-
nil
|
|
355
|
-
end
|
|
356
|
-
|
|
357
368
|
def database_snapshot_tags(adapter)
|
|
358
369
|
['type:database', "database:#{database_config_name(adapter)}", "adapter:#{adapter.adapter_name}"]
|
|
359
370
|
end
|
|
360
371
|
|
|
361
372
|
def database_config_name(adapter)
|
|
362
|
-
|
|
363
|
-
adapter.config.database_name
|
|
364
|
-
else
|
|
365
|
-
config.database_name
|
|
366
|
-
end
|
|
373
|
+
adapter.config.database_name
|
|
367
374
|
end
|
|
368
375
|
|
|
369
|
-
def
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
restic.restore_snapshot(resolved_snapshot, validated_target)
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
snapshot: resolved_snapshot,
|
|
376
|
-
target: validated_target
|
|
377
|
-
}
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
def perform_replacement_file_restore(snapshot, source_paths:, target_paths:)
|
|
376
|
+
def perform_replacement_file_restore(snapshot, production_source:)
|
|
377
|
+
source_paths = production_source ? config.backup_paths : config.local_restore_source_paths
|
|
378
|
+
target_paths = config.backup_paths
|
|
381
379
|
resolved_snapshot = resolve_snapshot(snapshot, tags: ['type:files'])
|
|
382
380
|
Dir.mktmpdir('kamal-backup-restore-') do |stage_dir|
|
|
383
381
|
restic.restore_snapshot(resolved_snapshot, stage_dir)
|
|
@@ -412,26 +410,17 @@ module KamalBackup
|
|
|
412
410
|
File.join(stage_dir, path.to_s.sub(%r{\A/+}, ''))
|
|
413
411
|
end
|
|
414
412
|
|
|
415
|
-
def
|
|
413
|
+
def perform_file_restore(snapshot, target:)
|
|
414
|
+
resolved_snapshot = resolve_snapshot(snapshot, tags: ['type:files'])
|
|
415
|
+
validated_target = config.validate_file_restore_target(target)
|
|
416
|
+
restic.restore_snapshot(resolved_snapshot, validated_target)
|
|
417
|
+
|
|
416
418
|
{
|
|
417
|
-
snapshot:
|
|
418
|
-
|
|
419
|
-
filename: filename,
|
|
420
|
-
target: redactor.redact_string(target.to_s)
|
|
419
|
+
snapshot: resolved_snapshot,
|
|
420
|
+
target: validated_target
|
|
421
421
|
}
|
|
422
422
|
end
|
|
423
423
|
|
|
424
|
-
def scratch_database_target(adapter, database_name, sqlite_path)
|
|
425
|
-
case adapter.adapter_name
|
|
426
|
-
when 'sqlite'
|
|
427
|
-
target = sqlite_path || raise(ConfigurationError, 'scratch SQLite path is required')
|
|
428
|
-
databases.size > 1 ? File.join(target, "#{database_config_name(adapter)}.sqlite3") : target
|
|
429
|
-
else
|
|
430
|
-
target = database_name || raise(ConfigurationError, 'scratch database name is required')
|
|
431
|
-
databases.size > 1 ? "#{target}_#{database_config_name(adapter)}" : target
|
|
432
|
-
end
|
|
433
|
-
end
|
|
434
|
-
|
|
435
424
|
def validate_local_machine_restore
|
|
436
425
|
config.validate_restic
|
|
437
426
|
config.validate_database_backup
|
|
@@ -463,55 +452,15 @@ module KamalBackup
|
|
|
463
452
|
config.validate_local_database_restore_target(adapter.current_target_identifier)
|
|
464
453
|
end
|
|
465
454
|
|
|
466
|
-
def require_restic!
|
|
467
|
-
return unless using_builtin_restic?
|
|
468
|
-
return if Command.available?('restic')
|
|
469
|
-
|
|
470
|
-
raise ConfigurationError,
|
|
471
|
-
'restic is required on PATH for commands that run on this machine. Install restic locally and try again.'
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
def run_drill_check(command)
|
|
475
|
-
result = Command.capture(
|
|
476
|
-
CommandSpec.new(argv: ['sh', '-lc', command]),
|
|
477
|
-
redactor: redactor
|
|
478
|
-
)
|
|
479
|
-
output = result.stdout.empty? ? result.stderr : result.stdout
|
|
480
|
-
|
|
481
|
-
{
|
|
482
|
-
status: 'ok',
|
|
483
|
-
command: redactor.redact_string(command),
|
|
484
|
-
output: redactor.redact_string(output.strip)
|
|
485
|
-
}
|
|
486
|
-
rescue CommandError => e
|
|
487
|
-
{
|
|
488
|
-
status: 'failed',
|
|
489
|
-
command: redactor.redact_string(command),
|
|
490
|
-
error: redactor.redact_string(e.message)
|
|
491
|
-
}
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def write_last_restore_drill(payload)
|
|
495
|
-
FileUtils.mkdir_p(config.state_dir)
|
|
496
|
-
File.write(config.last_restore_drill_path, JSON.pretty_generate(payload))
|
|
497
|
-
rescue SystemCallError
|
|
498
|
-
nil
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
def drill_operator
|
|
502
|
-
config.value('USER') || config.value('USERNAME')
|
|
503
|
-
end
|
|
504
|
-
|
|
505
455
|
def restic
|
|
506
|
-
@restic ||=
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
end
|
|
456
|
+
@restic ||= begin
|
|
457
|
+
unless Command.available?('restic')
|
|
458
|
+
raise ConfigurationError,
|
|
459
|
+
'restic is required on PATH for commands that run on this machine. Install restic locally and try again.'
|
|
460
|
+
end
|
|
512
461
|
|
|
513
|
-
|
|
514
|
-
|
|
462
|
+
Restic.new(config, redactor: redactor)
|
|
463
|
+
end
|
|
515
464
|
end
|
|
516
465
|
|
|
517
466
|
def databases
|
|
@@ -529,7 +478,6 @@ module KamalBackup
|
|
|
529
478
|
raise ConfigurationError, "no restic snapshot found for #{tags.join(', ')}" unless snapshot
|
|
530
479
|
|
|
531
480
|
snapshot['short_id'] || snapshot['id']
|
|
532
|
-
|
|
533
481
|
else
|
|
534
482
|
argument
|
|
535
483
|
end
|