kamal-backup 0.3.0.beta9 → 0.3.0.beta11
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 +52 -2
- data/lib/kamal_backup/cli.rb +17 -3
- data/lib/kamal_backup/config.rb +4 -0
- data/lib/kamal_backup/restic.rb +18 -2
- 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: a292aa17220ba3ef9cf77c566d5530ece93450aeaa3b01e8f3eaf234ca00859a
|
|
4
|
+
data.tar.gz: 19a798a19c52f3467fabac4bd5699d0a0b717fe2e9c0da238083aea1e99766cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa21d79ea00396dcd2a4f0496ef79c29e9db626a6f31e9969e99ff6560149ddb8aa3cdd0819490d896e770e39a655f4d0212c6b9a02fe0be46bbe1bc8a5441ac
|
|
7
|
+
data.tar.gz: 622747a659f8d7dc0c02b65888d51693d7388772a99f6459ba919ddaa8603be18dd90299c7f7c03fbb40906154b1c9c3aeace84986b22637381e8762a37bbb5b
|
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
|
@@ -29,9 +29,11 @@ module KamalBackup
|
|
|
29
29
|
@scheduler_class = scheduler_class
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def backup
|
|
32
|
+
def backup(force: false)
|
|
33
33
|
started_at = Time.now.utc
|
|
34
34
|
config.validate_backup
|
|
35
|
+
return skipped_backup_result(started_at) unless force || backup_due?(started_at)
|
|
36
|
+
|
|
35
37
|
require_restic!
|
|
36
38
|
|
|
37
39
|
restic.ensure_repository
|
|
@@ -48,6 +50,7 @@ module KamalBackup
|
|
|
48
50
|
|
|
49
51
|
backup_summary(started_at: started_at, finished_at: Time.now.utc).tap do |summary|
|
|
50
52
|
validate_fresh_backup_summary!(summary, started_at: started_at)
|
|
53
|
+
write_last_backup(summary)
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
|
|
@@ -135,10 +138,48 @@ module KamalBackup
|
|
|
135
138
|
|
|
136
139
|
def schedule
|
|
137
140
|
config.validate_backup
|
|
138
|
-
@scheduler_class.new(config) { backup }.run
|
|
141
|
+
@scheduler_class.new(config) { backup(force: true) }.run
|
|
139
142
|
end
|
|
140
143
|
|
|
141
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
|
+
|
|
142
183
|
def build_restore_result(scope, snapshot)
|
|
143
184
|
started_at = Time.now.utc
|
|
144
185
|
result = Schema.record(
|
|
@@ -299,6 +340,15 @@ module KamalBackup
|
|
|
299
340
|
false
|
|
300
341
|
end
|
|
301
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
|
+
|
|
302
352
|
def database_snapshot_tags(adapter)
|
|
303
353
|
["type:database", "database:#{database_config_name(adapter)}", "adapter:#{adapter.adapter_name}"]
|
|
304
354
|
end
|
data/lib/kamal_backup/cli.rb
CHANGED
|
@@ -141,6 +141,13 @@ module KamalBackup
|
|
|
141
141
|
def print_backup_result(result)
|
|
142
142
|
return unless result.is_a?(Hash)
|
|
143
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
|
+
|
|
144
151
|
puts("Backup completed at #{result.fetch(:finished_at)}")
|
|
145
152
|
result.fetch(:databases).each do |database|
|
|
146
153
|
puts("database #{database.fetch(:database)}: #{database.fetch(:snapshot)} at #{database.fetch(:time)}")
|
|
@@ -422,6 +429,10 @@ module KamalBackup
|
|
|
422
429
|
output ||= CommandOutput.new(io: $stderr, env: env)
|
|
423
430
|
output.error("(#{e.class}): #{e.message}", redactor: Redactor.new(env: env))
|
|
424
431
|
exit(1)
|
|
432
|
+
rescue StandardError => e
|
|
433
|
+
output ||= CommandOutput.new(io: $stderr, env: env)
|
|
434
|
+
output.error("(#{e.class}): #{e.message}", redactor: Redactor.new(env: env))
|
|
435
|
+
exit(1)
|
|
425
436
|
rescue Interrupt
|
|
426
437
|
output ||= CommandOutput.new(io: $stderr, env: env)
|
|
427
438
|
output.error("(Interrupt): interrupted", redactor: Redactor.new(env: env))
|
|
@@ -432,12 +443,15 @@ module KamalBackup
|
|
|
432
443
|
|
|
433
444
|
include Helpers
|
|
434
445
|
|
|
435
|
-
|
|
446
|
+
method_option :force, type: :boolean, default: false, desc: "Run a backup even if the configured schedule is not due"
|
|
447
|
+
desc "backup", "Run a due database and Active Storage backup"
|
|
436
448
|
def backup
|
|
437
449
|
if remote_command_mode?
|
|
438
|
-
|
|
450
|
+
argv = ["kamal-backup", "backup"]
|
|
451
|
+
argv << "--force" if options[:force]
|
|
452
|
+
exec_remote(argv)
|
|
439
453
|
else
|
|
440
|
-
print_backup_result(direct_app.backup)
|
|
454
|
+
print_backup_result(direct_app.backup(force: options[:force]))
|
|
441
455
|
end
|
|
442
456
|
end
|
|
443
457
|
|
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/restic.rb
CHANGED
|
@@ -48,13 +48,20 @@ module KamalBackup
|
|
|
48
48
|
Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
|
|
49
49
|
stdout_reader = Thread.new { Command.collect_stream(stdout, command_output: output, context: context, stream: :stdout, redactor: redactor) }
|
|
50
50
|
stderr_reader = Thread.new { Command.collect_stream(stderr, command_output: output, context: context, stream: :stderr, redactor: redactor) }
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
copy_error = nil
|
|
52
|
+
begin
|
|
53
|
+
IO.copy_stream(file, stdin)
|
|
54
|
+
rescue Errno::EPIPE => e
|
|
55
|
+
copy_error = e
|
|
56
|
+
ensure
|
|
57
|
+
stdin.close unless stdin.closed?
|
|
58
|
+
end
|
|
53
59
|
out = stdout_reader.value
|
|
54
60
|
err = stderr_reader.value
|
|
55
61
|
status = wait_thread.value
|
|
56
62
|
output&.command_exit(context, status.exitstatus)
|
|
57
63
|
raise_command_error(command, status, out, err) unless status.success?
|
|
64
|
+
raise_stream_error(command, copy_error, out, err) if copy_error
|
|
58
65
|
|
|
59
66
|
CommandResult.new(stdout: out, stderr: err, status: status.exitstatus)
|
|
60
67
|
end
|
|
@@ -309,6 +316,15 @@ module KamalBackup
|
|
|
309
316
|
)
|
|
310
317
|
end
|
|
311
318
|
|
|
319
|
+
def raise_stream_error(command, error, stdout, stderr)
|
|
320
|
+
raise CommandError.new(
|
|
321
|
+
"failed to stream file to #{command.display(redactor)}: #{error.message}\n#{redactor.redact_string(stderr)}",
|
|
322
|
+
command: command,
|
|
323
|
+
stdout: redactor.redact_string(stdout),
|
|
324
|
+
stderr: redactor.redact_string(stderr)
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
312
328
|
def write_last_check(payload)
|
|
313
329
|
FileUtils.mkdir_p(config.state_dir)
|
|
314
330
|
File.write(config.last_check_path, JSON.pretty_generate(payload.transform_values { |value| value.respond_to?(:iso8601) ? value.iso8601 : redactor.redact_string(value.to_s) }))
|
data/lib/kamal_backup/version.rb
CHANGED