kamal-backup 0.3.0.beta8 → 0.3.0.beta10
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 +111 -3
- data/lib/kamal_backup/cli.rb +26 -3
- data/lib/kamal_backup/config.rb +4 -0
- data/lib/kamal_backup/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c534b081ef41f75a8d10e72ae0fcbaa7b81ad5a0f2cbb3a12c281353d74ab69b
|
|
4
|
+
data.tar.gz: 6bd3b48d5573ab6a1e19ec1452584b7d44fc62e852edb7c202e2191d3bac6743
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdef6636e464308da24e9bb11aa64d8e68535fc9a8fc261a4dce02dc7e0d0bd26f20320f9dfede598085d780764310b0b3a7ad71ba3fe8d48e61d5c756b3597a
|
|
7
|
+
data.tar.gz: de0667fae6cb0ae276f1b4511ea1c63ffe8655523648a0e3f1e40d6872e53ba2823b4d987d4e651ace4d47710d516d17dc32f9e269e9bd20115501a533f793be
|
data/README.md
CHANGED
|
@@ -112,6 +112,8 @@ bundle exec kamal-backup check
|
|
|
112
112
|
bundle exec kamal-backup evidence
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
`backup` respects `backup.schedule` and skips when the latest backup is still current. Use `bundle exec kamal-backup backup --force` when you deliberately want an immediate snapshot.
|
|
116
|
+
|
|
115
117
|
## What you get
|
|
116
118
|
|
|
117
119
|
- **Scheduled backups:** the accessory runs continuously and backs up on `backup.schedule`.
|
data/lib/kamal_backup/app.rb
CHANGED
|
@@ -16,6 +16,8 @@ require_relative "schema"
|
|
|
16
16
|
|
|
17
17
|
module KamalBackup
|
|
18
18
|
class App
|
|
19
|
+
FRESH_BACKUP_GRACE_SECONDS = 5
|
|
20
|
+
|
|
19
21
|
attr_reader :config, :redactor
|
|
20
22
|
|
|
21
23
|
def initialize(env: ENV, config: nil, redactor: nil, restic: nil, database: nil, evidence_class: Evidence, scheduler_class: Scheduler)
|
|
@@ -27,8 +29,11 @@ module KamalBackup
|
|
|
27
29
|
@scheduler_class = scheduler_class
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
def backup
|
|
32
|
+
def backup(force: false)
|
|
33
|
+
started_at = Time.now.utc
|
|
31
34
|
config.validate_backup
|
|
35
|
+
return skipped_backup_result(started_at) unless force || backup_due?(started_at)
|
|
36
|
+
|
|
32
37
|
require_restic!
|
|
33
38
|
|
|
34
39
|
restic.ensure_repository
|
|
@@ -43,7 +48,10 @@ module KamalBackup
|
|
|
43
48
|
restic.check
|
|
44
49
|
end
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
backup_summary(started_at: started_at, finished_at: Time.now.utc).tap do |summary|
|
|
52
|
+
validate_fresh_backup_summary!(summary, started_at: started_at)
|
|
53
|
+
write_last_backup(summary)
|
|
54
|
+
end
|
|
47
55
|
end
|
|
48
56
|
|
|
49
57
|
def validate(check_files: true)
|
|
@@ -130,10 +138,48 @@ module KamalBackup
|
|
|
130
138
|
|
|
131
139
|
def schedule
|
|
132
140
|
config.validate_backup
|
|
133
|
-
@scheduler_class.new(config) { backup }.run
|
|
141
|
+
@scheduler_class.new(config) { backup(force: true) }.run
|
|
134
142
|
end
|
|
135
143
|
|
|
136
144
|
private
|
|
145
|
+
def backup_due?(now)
|
|
146
|
+
due_at = next_backup_at
|
|
147
|
+
due_at.nil? || now >= due_at
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def skipped_backup_result(now)
|
|
151
|
+
{
|
|
152
|
+
kind: "backup_result",
|
|
153
|
+
status: "skipped",
|
|
154
|
+
reason: "not_due",
|
|
155
|
+
last_backup_at: last_backup_finished_at&.iso8601,
|
|
156
|
+
next_backup_at: next_backup_at&.iso8601,
|
|
157
|
+
force_command: "kamal-backup backup --force",
|
|
158
|
+
finished_at: now.iso8601
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def next_backup_at
|
|
163
|
+
last_backup_finished_at + config.backup_schedule_seconds if last_backup_finished_at
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def last_backup_finished_at
|
|
167
|
+
@last_backup_finished_at ||= begin
|
|
168
|
+
value = last_backup_record["finished_at"] || last_backup_record["last_backup_at"]
|
|
169
|
+
value ? Time.parse(value.to_s).utc : nil
|
|
170
|
+
rescue ArgumentError
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def last_backup_record
|
|
176
|
+
@last_backup_record ||= begin
|
|
177
|
+
JSON.parse(File.read(config.last_backup_path))
|
|
178
|
+
rescue JSON::ParserError, SystemCallError
|
|
179
|
+
{}
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
137
183
|
def build_restore_result(scope, snapshot)
|
|
138
184
|
started_at = Time.now.utc
|
|
139
185
|
result = Schema.record(
|
|
@@ -241,6 +287,68 @@ module KamalBackup
|
|
|
241
287
|
result[:files] = file_results.size == 1 ? file_results.first : file_results
|
|
242
288
|
end
|
|
243
289
|
|
|
290
|
+
def backup_summary(started_at:, finished_at:)
|
|
291
|
+
{
|
|
292
|
+
kind: "backup_result",
|
|
293
|
+
status: "ok",
|
|
294
|
+
started_at: started_at.iso8601,
|
|
295
|
+
finished_at: finished_at.iso8601,
|
|
296
|
+
databases: databases.map { |adapter| backup_snapshot_summary(adapter) },
|
|
297
|
+
files: backup_file_snapshot_summary
|
|
298
|
+
}
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def backup_snapshot_summary(adapter)
|
|
302
|
+
snapshot = restic.latest_snapshot(tags: database_snapshot_tags(adapter))
|
|
303
|
+
snapshot_summary(snapshot).merge(
|
|
304
|
+
database: database_config_name(adapter),
|
|
305
|
+
adapter: adapter.adapter_name
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def backup_file_snapshot_summary
|
|
310
|
+
return nil if config.backup_paths.empty?
|
|
311
|
+
|
|
312
|
+
snapshot_summary(restic.latest_snapshot(tags: ["type:files"]))
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def snapshot_summary(snapshot)
|
|
316
|
+
{
|
|
317
|
+
snapshot: snapshot && (snapshot["short_id"] || snapshot["id"]),
|
|
318
|
+
time: snapshot && snapshot["time"]
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def validate_fresh_backup_summary!(summary, started_at:)
|
|
323
|
+
stale_databases = summary.fetch(:databases).reject { |entry| fresh_snapshot?(entry, started_at) }
|
|
324
|
+
|
|
325
|
+
unless stale_databases.empty?
|
|
326
|
+
names = stale_databases.map { |entry| entry.fetch(:database) }.join(", ")
|
|
327
|
+
raise ConfigurationError, "backup did not create a fresh database snapshot for #{names}"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
files = summary[:files]
|
|
331
|
+
if files && !fresh_snapshot?(files, started_at)
|
|
332
|
+
raise ConfigurationError, "backup did not create a fresh file snapshot"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def fresh_snapshot?(entry, started_at)
|
|
337
|
+
snapshot_time = Time.parse(entry[:time].to_s)
|
|
338
|
+
snapshot_time >= started_at - FRESH_BACKUP_GRACE_SECONDS
|
|
339
|
+
rescue ArgumentError
|
|
340
|
+
false
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def write_last_backup(result)
|
|
344
|
+
FileUtils.mkdir_p(config.state_dir)
|
|
345
|
+
File.write(config.last_backup_path, JSON.pretty_generate(result))
|
|
346
|
+
@last_backup_record = result.transform_keys(&:to_s)
|
|
347
|
+
@last_backup_finished_at = Time.parse(result.fetch(:finished_at)).utc
|
|
348
|
+
rescue SystemCallError, ArgumentError
|
|
349
|
+
nil
|
|
350
|
+
end
|
|
351
|
+
|
|
244
352
|
def database_snapshot_tags(adapter)
|
|
245
353
|
["type:database", "database:#{database_config_name(adapter)}", "adapter:#{adapter.adapter_name}"]
|
|
246
354
|
end
|
data/lib/kamal_backup/cli.rb
CHANGED
|
@@ -138,6 +138,26 @@ module KamalBackup
|
|
|
138
138
|
puts("fix: #{status_output.decorate(accessory_reboot_command, :yellow, :bold)}") if status == "out of sync"
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
+
def print_backup_result(result)
|
|
142
|
+
return unless result.is_a?(Hash)
|
|
143
|
+
|
|
144
|
+
if result[:status] == "skipped"
|
|
145
|
+
puts("No backup due. Last backup finished at #{result.fetch(:last_backup_at)}.")
|
|
146
|
+
puts("Next backup is due at #{result.fetch(:next_backup_at)}.")
|
|
147
|
+
puts("Run `#{result.fetch(:force_command)}` to force a backup now.")
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
puts("Backup completed at #{result.fetch(:finished_at)}")
|
|
152
|
+
result.fetch(:databases).each do |database|
|
|
153
|
+
puts("database #{database.fetch(:database)}: #{database.fetch(:snapshot)} at #{database.fetch(:time)}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if files = result[:files]
|
|
157
|
+
puts("files: #{files.fetch(:snapshot)} at #{files.fetch(:time)}")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
141
161
|
def validate_deploy_config
|
|
142
162
|
config = Config.new(
|
|
143
163
|
env: bridge.accessory_environment(accessory_name: accessory_name),
|
|
@@ -419,12 +439,15 @@ module KamalBackup
|
|
|
419
439
|
|
|
420
440
|
include Helpers
|
|
421
441
|
|
|
422
|
-
|
|
442
|
+
method_option :force, type: :boolean, default: false, desc: "Run a backup even if the configured schedule is not due"
|
|
443
|
+
desc "backup", "Run a due database and Active Storage backup"
|
|
423
444
|
def backup
|
|
424
445
|
if remote_command_mode?
|
|
425
|
-
|
|
446
|
+
argv = ["kamal-backup", "backup"]
|
|
447
|
+
argv << "--force" if options[:force]
|
|
448
|
+
exec_remote(argv)
|
|
426
449
|
else
|
|
427
|
-
direct_app.backup
|
|
450
|
+
print_backup_result(direct_app.backup(force: options[:force]))
|
|
428
451
|
end
|
|
429
452
|
end
|
|
430
453
|
|
data/lib/kamal_backup/config.rb
CHANGED
|
@@ -215,6 +215,10 @@ module KamalBackup
|
|
|
215
215
|
File.join(state_dir, "last_check.json")
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
+
def last_backup_path
|
|
219
|
+
File.join(state_dir, "last_backup.json")
|
|
220
|
+
end
|
|
221
|
+
|
|
218
222
|
def last_restore_drill_path
|
|
219
223
|
File.join(state_dir, "last_restore_drill.json")
|
|
220
224
|
end
|
data/lib/kamal_backup/version.rb
CHANGED