kamal-backup 1.1.0 → 1.1.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 +4 -4
- data/lib/kamal_backup/app.rb +13 -1
- data/lib/kamal_backup/command_output.rb +4 -2
- data/lib/kamal_backup/restic.rb +64 -32
- data/lib/kamal_backup/version.rb +1 -1
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ffce7c9bfda0a3f7bf1431000f0b0d9f517d532b0d0b37378879e53a320d622
|
|
4
|
+
data.tar.gz: f927f0c24744d0c62797831a9839aa557fee677ac04c0956a4cf5fc196702b3d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b9e6b7a3c2386a7e09c199236bafaf57849a27dfe4dba95b3a0a8114bed4cfb134184e9cf4adcfd7536300add5df525742af032d8e8eb410f9355dea626b731
|
|
7
|
+
data.tar.gz: d1d747ccd9b1a86e13d43d812b0551eb6601780632f5f0556caa9cced544b3db75eca34ed825cb02d0922ac87b6604403faf0c3f1617e732343101f827ed02dd
|
data/lib/kamal_backup/app.rb
CHANGED
|
@@ -36,7 +36,7 @@ module KamalBackup
|
|
|
36
36
|
return skipped_backup_result(started_at) unless force || backup_due?(started_at)
|
|
37
37
|
|
|
38
38
|
restic.ensure_repository
|
|
39
|
-
databases.each { |database| database
|
|
39
|
+
databases.each { |database| backup_database(database) }
|
|
40
40
|
restic.backup_paths(config.backup_paths, tags: ['type:files'])
|
|
41
41
|
|
|
42
42
|
restic.prune if config.forget_after_backup?
|
|
@@ -210,6 +210,18 @@ module KamalBackup
|
|
|
210
210
|
|
|
211
211
|
private
|
|
212
212
|
|
|
213
|
+
def backup_database(database)
|
|
214
|
+
database.backup(restic)
|
|
215
|
+
rescue CommandError => e
|
|
216
|
+
raise CommandError.new(
|
|
217
|
+
"database backup failed for #{database_config_name(database)}: #{e.message}",
|
|
218
|
+
command: e.command,
|
|
219
|
+
status: e.status,
|
|
220
|
+
stdout: e.stdout,
|
|
221
|
+
stderr: e.stderr
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
213
225
|
def backup_due?(now)
|
|
214
226
|
due_at = next_backup_at
|
|
215
227
|
due_at.nil? || now >= due_at
|
|
@@ -82,8 +82,10 @@ module KamalBackup
|
|
|
82
82
|
|
|
83
83
|
def command_exit(context, status)
|
|
84
84
|
runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - context.fetch(:started_at)
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
# A command killed by a signal has no exit status, so nil is a failure.
|
|
86
|
+
successful = status&.zero?
|
|
87
|
+
result = successful ? 'successful' : 'failed'
|
|
88
|
+
result_color = successful ? :green : :red
|
|
87
89
|
|
|
88
90
|
synchronize do
|
|
89
91
|
flush_output_buffers(context)
|
data/lib/kamal_backup/restic.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative 'private_tempfile'
|
|
|
11
11
|
module KamalBackup
|
|
12
12
|
class Restic
|
|
13
13
|
RESTIC_ENV_PATTERN = /\A(?:RESTIC_|AWS_|B2_|AZURE_|GOOGLE_|RCLONE_|OS_|ST_|HP_|HTTP_|HTTPS_|NO_PROXY)/i
|
|
14
|
+
INCOMPLETE_SNAPSHOT_STATUS = 3
|
|
14
15
|
|
|
15
16
|
attr_reader :config, :redactor
|
|
16
17
|
|
|
@@ -31,11 +32,13 @@ module KamalBackup
|
|
|
31
32
|
|
|
32
33
|
def backup_stream(command, filename:, tags:)
|
|
33
34
|
log("backing up stream as #{filename}")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
['--stdin-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
without_incomplete_snapshot do
|
|
36
|
+
run(
|
|
37
|
+
['backup'] + host_args + ['--stdin-filename', filename] + tag_args(common_tags + tags) +
|
|
38
|
+
['--stdin-from-command', '--'] + command.argv,
|
|
39
|
+
env: restic_env.merge(command.env)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def backup_file(path, filename:, tags:)
|
|
@@ -46,34 +49,36 @@ module KamalBackup
|
|
|
46
49
|
)
|
|
47
50
|
log("backing up file content as #{filename}")
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
without_incomplete_snapshot do
|
|
53
|
+
File.open(path, 'rb') do |file|
|
|
54
|
+
output = Command.output
|
|
55
|
+
context = output&.command_start(command, redactor: redactor)
|
|
56
|
+
Open3.popen3(command.env, *command.argv) do |stdin, stdout, stderr, wait_thread|
|
|
57
|
+
stdout_reader = Thread.new do
|
|
58
|
+
Command.collect_stream(stdout, command_output: output, context: context, stream: :stdout,
|
|
59
|
+
redactor: redactor)
|
|
60
|
+
end
|
|
61
|
+
stderr_reader = Thread.new do
|
|
62
|
+
Command.collect_stream(stderr, command_output: output, context: context, stream: :stderr,
|
|
63
|
+
redactor: redactor)
|
|
64
|
+
end
|
|
65
|
+
copy_error = nil
|
|
66
|
+
begin
|
|
67
|
+
IO.copy_stream(file, stdin)
|
|
68
|
+
rescue Errno::EPIPE => e
|
|
69
|
+
copy_error = e
|
|
70
|
+
ensure
|
|
71
|
+
stdin.close unless stdin.closed?
|
|
72
|
+
end
|
|
73
|
+
out = stdout_reader.value
|
|
74
|
+
err = stderr_reader.value
|
|
75
|
+
status = wait_thread.value
|
|
76
|
+
output&.command_exit(context, status.exitstatus)
|
|
77
|
+
raise_command_error(command, status, out, err) unless status.success?
|
|
78
|
+
raise_stream_error(command, copy_error, out, err) if copy_error
|
|
79
|
+
|
|
80
|
+
CommandResult.new(stdout: out, stderr: err, status: status.exitstatus)
|
|
60
81
|
end
|
|
61
|
-
copy_error = nil
|
|
62
|
-
begin
|
|
63
|
-
IO.copy_stream(file, stdin)
|
|
64
|
-
rescue Errno::EPIPE => e
|
|
65
|
-
copy_error = e
|
|
66
|
-
ensure
|
|
67
|
-
stdin.close unless stdin.closed?
|
|
68
|
-
end
|
|
69
|
-
out = stdout_reader.value
|
|
70
|
-
err = stderr_reader.value
|
|
71
|
-
status = wait_thread.value
|
|
72
|
-
output&.command_exit(context, status.exitstatus)
|
|
73
|
-
raise_command_error(command, status, out, err) unless status.success?
|
|
74
|
-
raise_stream_error(command, copy_error, out, err) if copy_error
|
|
75
|
-
|
|
76
|
-
CommandResult.new(stdout: out, stderr: err, status: status.exitstatus)
|
|
77
82
|
end
|
|
78
83
|
end
|
|
79
84
|
rescue Errno::ENOENT => e
|
|
@@ -381,6 +386,33 @@ module KamalBackup
|
|
|
381
386
|
stderr: e.message)
|
|
382
387
|
end
|
|
383
388
|
|
|
389
|
+
# restic exits 3 when it could not read a source but saved a snapshot anyway.
|
|
390
|
+
# A dump that exits 0 without writing anything ends up here: the snapshot has
|
|
391
|
+
# no dump in it, yet it would become the latest one and use a retention slot.
|
|
392
|
+
def without_incomplete_snapshot
|
|
393
|
+
yield
|
|
394
|
+
rescue CommandError => e
|
|
395
|
+
raise unless e.status == INCOMPLETE_SNAPSHOT_STATUS
|
|
396
|
+
|
|
397
|
+
raise CommandError.new(
|
|
398
|
+
"#{e.message}\n#{discard_incomplete_snapshot(e.stdout)}",
|
|
399
|
+
command: e.command,
|
|
400
|
+
status: e.status,
|
|
401
|
+
stdout: e.stdout,
|
|
402
|
+
stderr: e.stderr
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def discard_incomplete_snapshot(stdout)
|
|
407
|
+
snapshot = stdout[/snapshot (\h+) saved/, 1]
|
|
408
|
+
return 'restic may have saved an incomplete snapshot; check `kamal-backup list`.' unless snapshot
|
|
409
|
+
|
|
410
|
+
run(['forget', snapshot])
|
|
411
|
+
"Removed incomplete snapshot #{snapshot}."
|
|
412
|
+
rescue CommandError => e
|
|
413
|
+
"Could not remove incomplete snapshot #{snapshot}; run `restic forget #{snapshot}`: #{e.message}"
|
|
414
|
+
end
|
|
415
|
+
|
|
384
416
|
def raise_command_error(command, status, stdout, stderr)
|
|
385
417
|
raise CommandError.new(
|
|
386
418
|
"command failed (#{status.exitstatus}): #{command.display(redactor)}\n#{redactor.redact_string(stderr)}",
|
data/lib/kamal_backup/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kamal-backup
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crmne
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: thor
|
|
@@ -110,7 +109,6 @@ dependencies:
|
|
|
110
109
|
version: '0'
|
|
111
110
|
description: Back up PostgreSQL, MySQL, SQLite, and file-backed Active Storage files
|
|
112
111
|
into restic on a schedule, then run restore drills and produce review evidence.
|
|
113
|
-
email:
|
|
114
112
|
executables:
|
|
115
113
|
- kamal-backup
|
|
116
114
|
extensions: []
|
|
@@ -149,7 +147,6 @@ metadata:
|
|
|
149
147
|
source_code_uri: https://github.com/crmne/kamal-backup
|
|
150
148
|
changelog_uri: https://github.com/crmne/kamal-backup/releases
|
|
151
149
|
rubygems_mfa_required: 'true'
|
|
152
|
-
post_install_message:
|
|
153
150
|
rdoc_options: []
|
|
154
151
|
require_paths:
|
|
155
152
|
- lib
|
|
@@ -164,8 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
164
161
|
- !ruby/object:Gem::Version
|
|
165
162
|
version: '0'
|
|
166
163
|
requirements: []
|
|
167
|
-
rubygems_version:
|
|
168
|
-
signing_key:
|
|
164
|
+
rubygems_version: 4.0.20
|
|
169
165
|
specification_version: 4
|
|
170
166
|
summary: Scheduled backups, restore drills, and evidence for Rails apps deployed with
|
|
171
167
|
Kamal
|