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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7c67946461c23a767a12d31d82389bb4e0fb3c75a5494f102c4dad8facecb2e
4
- data.tar.gz: f0265c16c92bdf291bf19f0586589af08938962c5a6b79635f782559dee75d39
3
+ metadata.gz: a292aa17220ba3ef9cf77c566d5530ece93450aeaa3b01e8f3eaf234ca00859a
4
+ data.tar.gz: 19a798a19c52f3467fabac4bd5699d0a0b717fe2e9c0da238083aea1e99766cc
5
5
  SHA512:
6
- metadata.gz: f919b9df92d55407c57745e61cfa4fd011c15060ab6edd5d93b75516fef13b22f0490e7010531bb6c464578df6a25faf8a485d6a35f5431f9a6e53331cef90a5
7
- data.tar.gz: d7243e236effb7c4da158c70993af8896e39764952f9dc2155a123655ddac49931804c67dd10424d10a5ddcdd0208542a28fe798256691a4cb2287ef35bba242
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`.
@@ -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
@@ -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
- desc "backup", "Run one database and Active Storage backup immediately"
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
- exec_remote(["kamal-backup", "backup"])
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
 
@@ -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
@@ -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
- IO.copy_stream(file, stdin)
52
- stdin.close
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) }))
@@ -1,3 +1,3 @@
1
1
  module KamalBackup
2
- VERSION = "0.3.0.beta9"
2
+ VERSION = "0.3.0.beta11"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.beta9
4
+ version: 0.3.0.beta11
5
5
  platform: ruby
6
6
  authors:
7
7
  - crmne