kamal-backup 0.3.0.beta21 → 0.3.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.
@@ -1,12 +1,14 @@
1
- require "fileutils"
2
- require "json"
3
- require "shellwords"
4
- require "thor"
5
- require_relative "app"
6
- require_relative "config"
7
- require_relative "kamal_bridge"
8
- require_relative "redactor"
9
- require_relative "version"
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'shellwords'
6
+ require 'thor'
7
+ require_relative 'app'
8
+ require_relative 'config'
9
+ require_relative 'kamal_bridge'
10
+ require_relative 'redactor'
11
+ require_relative 'version'
10
12
 
11
13
  module KamalBackup
12
14
  class CLI < Thor
@@ -38,17 +40,15 @@ module KamalBackup
38
40
  end
39
41
 
40
42
  def local_command_config
41
- @local_command_config ||= begin
42
- if deployment_mode?
43
- Config.new(
44
- env: command_env,
45
- defaults: production_source_defaults,
46
- config_paths: [Config::LOCAL_CONFIG_PATH]
47
- )
48
- else
49
- Config.new(env: command_env)
50
- end
51
- end
43
+ @local_command_config ||= if deployment_mode?
44
+ Config.new(
45
+ env: command_env,
46
+ defaults: production_source_defaults,
47
+ config_paths: [Config::LOCAL_CONFIG_PATH]
48
+ )
49
+ else
50
+ Config.new(env: command_env)
51
+ end
52
52
  end
53
53
 
54
54
  def production_source_defaults
@@ -59,10 +59,10 @@ module KamalBackup
59
59
  config = Config.new(env: {}, config_paths: [Config::SHARED_CONFIG_PATH], load_project_defaults: false)
60
60
 
61
61
  {}.tap do |defaults|
62
- defaults["APP_NAME"] = config.app_name if config.app_name
63
- defaults["DATABASE_ADAPTER"] = config.database_adapter if config.database_adapter
64
- defaults["RESTIC_REPOSITORY"] = config.restic_repository if config.restic_repository
65
- defaults["LOCAL_RESTORE_SOURCE_PATHS"] = config.backup_paths.join("\n") if config.backup_paths.any?
62
+ defaults['APP_NAME'] = config.app_name if config.app_name
63
+ defaults['DATABASE_ADAPTER'] = config.database_adapter if config.database_adapter
64
+ defaults['RESTIC_REPOSITORY'] = config.restic_repository if config.restic_repository
65
+ defaults['LOCAL_RESTORE_SOURCE_PATHS'] = config.backup_paths.join("\n") if config.backup_paths.any?
66
66
  end
67
67
  end
68
68
 
@@ -121,27 +121,27 @@ module KamalBackup
121
121
  end
122
122
 
123
123
  def accessory_reboot_command
124
- argv = ["bin/kamal", "accessory", "reboot", accessory_name]
125
- argv.concat(["-c", options[:config_file]]) if options[:config_file]
126
- argv.concat(["-d", options[:destination]]) if options[:destination]
124
+ argv = ['bin/kamal', 'accessory', 'reboot', accessory_name]
125
+ argv.concat(['-c', options[:config_file]]) if options[:config_file]
126
+ argv.concat(['-d', options[:destination]]) if options[:destination]
127
127
  Shellwords.join(argv)
128
128
  end
129
129
 
130
130
  def print_remote_version_status
131
- status = remote_version == VERSION ? "in sync" : "out of sync"
132
- status_color = status == "in sync" ? :green : :red
131
+ status = remote_version == VERSION ? 'in sync' : 'out of sync'
132
+ status_color = status == 'in sync' ? :green : :red
133
133
  status_output = CommandOutput.new(io: $stdout, env: command_env)
134
134
 
135
135
  puts("local: #{VERSION}")
136
136
  puts("remote: #{remote_version}")
137
137
  puts("status: #{status_output.decorate(status, status_color, :bold)}")
138
- puts("fix: #{status_output.decorate(accessory_reboot_command, :yellow, :bold)}") if status == "out of sync"
138
+ puts("fix: #{status_output.decorate(accessory_reboot_command, :yellow, :bold)}") if status == 'out of sync'
139
139
  end
140
140
 
141
141
  def print_backup_result(result)
142
142
  return unless result.is_a?(Hash)
143
143
 
144
- if result[:status] == "skipped"
144
+ if result[:status] == 'skipped'
145
145
  puts("No backup due. Last backup finished at #{result.fetch(:last_backup_at)}.")
146
146
  puts("Next backup is due at #{result.fetch(:next_backup_at)}.")
147
147
  puts("Run `#{result.fetch(:force_command)}` to force a backup now.")
@@ -153,7 +153,7 @@ module KamalBackup
153
153
  puts("database #{database.fetch(:database)}: #{database.fetch(:snapshot)} at #{database.fetch(:time)}")
154
154
  end
155
155
 
156
- if files = result[:files]
156
+ if (files = result[:files])
157
157
  puts("files: #{files.fetch(:snapshot)} at #{files.fetch(:time)}")
158
158
  end
159
159
  end
@@ -162,7 +162,7 @@ module KamalBackup
162
162
  output = Array(results).map(&:stdout).join
163
163
 
164
164
  if output.empty?
165
- puts("Prune completed")
165
+ puts('Prune completed')
166
166
  else
167
167
  print(output)
168
168
  puts unless output.end_with?("\n")
@@ -181,30 +181,28 @@ module KamalBackup
181
181
  def confirm!(message)
182
182
  return if options[:yes]
183
183
 
184
- unless $stdin.tty?
185
- raise ConfigurationError, "confirmation required; rerun with --yes"
186
- end
184
+ raise ConfigurationError, 'confirmation required; rerun with --yes' unless $stdin.tty?
187
185
 
188
- unless yes?("#{message} [y/N]")
189
- raise ConfigurationError, "aborted"
190
- end
186
+ raise ConfigurationError, 'aborted' unless yes?("#{message} [y/N]")
191
187
  end
192
188
 
193
189
  def confirm_production_restore!(snapshot)
194
190
  return if options[:"confirm-production-restore"]
195
191
 
196
192
  if options[:yes]
197
- raise ConfigurationError, "--yes does not bypass restore production; use --confirm-production-restore only for deliberate automation"
193
+ raise ConfigurationError,
194
+ '--yes does not bypass restore production; use --confirm-production-restore only for deliberate automation'
198
195
  end
199
196
 
200
197
  unless $stdin.tty?
201
- raise ConfigurationError, "production restore confirmation required; rerun interactively or pass --confirm-production-restore only for deliberate automation"
198
+ raise ConfigurationError,
199
+ 'production restore confirmation required; rerun interactively or pass --confirm-production-restore only for deliberate automation'
202
200
  end
203
201
 
204
202
  app_name = production_restore_confirmation_config.required_app_name
205
203
  say "This will overwrite the production database and file paths for #{app_name} from backup #{snapshot}.", :red
206
- require_typed_confirmation("Type the app name to continue", app_name)
207
- require_typed_confirmation("Type RESTORE PRODUCTION to continue", "RESTORE PRODUCTION")
204
+ require_typed_confirmation('Type the app name to continue', app_name)
205
+ require_typed_confirmation('Type RESTORE PRODUCTION to continue', 'RESTORE PRODUCTION')
208
206
  confirm!("Restore #{snapshot} into production now? This will overwrite production data.")
209
207
  end
210
208
 
@@ -212,7 +210,7 @@ module KamalBackup
212
210
  answer = ask("#{prompt}:").to_s.strip
213
211
  return if answer == expected
214
212
 
215
- raise ConfigurationError, "aborted"
213
+ raise ConfigurationError, 'aborted'
216
214
  end
217
215
 
218
216
  def production_restore_confirmation_config
@@ -228,16 +226,12 @@ module KamalBackup
228
226
  end
229
227
 
230
228
  def prompt_required(label)
231
- unless $stdin.tty?
232
- raise ConfigurationError, "#{label.downcase} is required; pass it on the command line"
233
- end
229
+ raise ConfigurationError, "#{label.downcase} is required; pass it on the command line" unless $stdin.tty?
234
230
 
235
231
  value = ask("#{label}:").to_s.strip
236
- if value.empty?
237
- raise ConfigurationError, "#{label.downcase} is required"
238
- else
239
- value
240
- end
232
+ raise ConfigurationError, "#{label.downcase} is required" if value.empty?
233
+
234
+ value
241
235
  end
242
236
 
243
237
  def init_config_root
@@ -246,7 +240,7 @@ module KamalBackup
246
240
  end
247
241
 
248
242
  def shared_config_path
249
- File.join(init_config_root, "kamal-backup.yml")
243
+ File.join(init_config_root, 'kamal-backup.yml')
250
244
  end
251
245
 
252
246
  def write_init_file(path, contents)
@@ -305,9 +299,9 @@ module KamalBackup
305
299
  class CommandBase < Thor
306
300
  include Helpers
307
301
 
308
- class_option :yes, aliases: "-y", type: :boolean, default: false, desc: "Skip confirmation prompt"
309
- class_option :config_file, aliases: "-c", type: :string, desc: "Path to Kamal deploy config file"
310
- class_option :destination, aliases: "-d", type: :string, desc: "Kamal destination to use"
302
+ class_option :yes, aliases: '-y', type: :boolean, default: false, desc: 'Skip confirmation prompt'
303
+ class_option :config_file, aliases: '-c', type: :string, desc: 'Path to Kamal deploy config file'
304
+ class_option :destination, aliases: '-d', type: :string, desc: 'Kamal destination to use'
311
305
  remove_command :tree
312
306
  end
313
307
 
@@ -316,19 +310,20 @@ module KamalBackup
316
310
  CLI.basename
317
311
  end
318
312
 
319
- desc "local [SNAPSHOT]", "Restore the backup into the local database and Active Storage path"
320
- def local(snapshot = "latest")
313
+ desc 'local [SNAPSHOT]', 'Restore the backup into the local database and Active Storage path'
314
+ def local(snapshot = 'latest')
321
315
  confirm!("Restore #{snapshot} into the local database and Active Storage path? This will overwrite local data.")
322
316
  puts(JSON.pretty_generate(local_restore_app.restore_to_local_machine(snapshot)))
323
317
  end
324
318
 
325
- method_option :"confirm-production-restore", type: :boolean, default: false, desc: "Confirm production restore without interactive prompts"
326
- desc "production [SNAPSHOT]", "Restore the backup into the production database and Active Storage path"
327
- def production(snapshot = "latest")
319
+ method_option :"confirm-production-restore", type: :boolean, default: false,
320
+ desc: 'Confirm production restore without interactive prompts'
321
+ desc 'production [SNAPSHOT]', 'Restore the backup into the production database and Active Storage path'
322
+ def production(snapshot = 'latest')
328
323
  confirm_production_restore!(snapshot)
329
324
 
330
325
  if deployment_mode?
331
- exec_remote(["kamal-backup", "restore", "production", snapshot, "--confirm-production-restore"])
326
+ exec_remote(['kamal-backup', 'restore', 'production', snapshot, '--confirm-production-restore'])
332
327
  else
333
328
  puts(JSON.pretty_generate(direct_app.restore_to_production(snapshot)))
334
329
  end
@@ -340,28 +335,29 @@ module KamalBackup
340
335
  CLI.basename
341
336
  end
342
337
 
343
- method_option :check, type: :string, desc: "Run a verification command after the restore"
344
- desc "local [SNAPSHOT]", "Run a restore drill on the local machine"
345
- def local(snapshot = "latest")
338
+ method_option :check, type: :string, desc: 'Run a verification command after the restore'
339
+ desc 'local [SNAPSHOT]', 'Run a restore drill on the local machine'
340
+ def local(snapshot = 'latest')
346
341
  confirm!("Run a local restore drill for #{snapshot}? This will overwrite local data.")
347
342
  result = local_restore_app.drill_on_local_machine(snapshot, check_command: options[:check])
348
343
  puts(JSON.pretty_generate(result))
349
344
  exit(1) if local_restore_app.drill_failed?(result)
350
345
  end
351
346
 
352
- method_option :database, type: :string, desc: "Scratch database name for PostgreSQL or MySQL"
353
- method_option :"sqlite-path", type: :string, desc: "Scratch SQLite path for production-side drills"
354
- method_option :files, type: :string, default: "/restore/files", desc: "Scratch Active Storage target for the drill"
355
- method_option :check, type: :string, desc: "Run a verification command after the restore"
356
- desc "production [SNAPSHOT]", "Run a restore drill on production infrastructure using scratch targets"
357
- def production(snapshot = "latest")
347
+ method_option :database, type: :string, desc: 'Scratch database name for PostgreSQL or MySQL'
348
+ method_option :"sqlite-path", type: :string, desc: 'Scratch SQLite path for production-side drills'
349
+ method_option :files, type: :string, default: '/restore/files',
350
+ desc: 'Scratch Active Storage target for the drill'
351
+ method_option :check, type: :string, desc: 'Run a verification command after the restore'
352
+ desc 'production [SNAPSHOT]', 'Run a restore drill on production infrastructure using scratch targets'
353
+ def production(snapshot = 'latest')
358
354
  confirm!("Run a production-side restore drill for #{snapshot}? This will restore into scratch targets on production infrastructure.")
359
355
 
360
356
  if deployment_mode?
361
- argv = ["kamal-backup", "drill", "production", snapshot, "--files", options[:files], "--yes"]
362
- argv.concat(["--database", production_database_name]) if production_database_name
363
- argv.concat(["--sqlite-path", options[:"sqlite-path"]]) if options[:"sqlite-path"]
364
- argv.concat(["--check", options[:check]]) if options[:check]
357
+ argv = ['kamal-backup', 'drill', 'production', snapshot, '--files', options[:files], '--yes']
358
+ argv.concat(['--database', production_database_name]) if production_database_name
359
+ argv.concat(['--sqlite-path', options[:"sqlite-path"]]) if options[:"sqlite-path"]
360
+ argv.concat(['--check', options[:check]]) if options[:check]
365
361
  exec_remote(argv)
366
362
  else
367
363
  result = direct_app.drill_on_production(
@@ -378,10 +374,10 @@ module KamalBackup
378
374
 
379
375
  no_commands do
380
376
  def production_database_name
381
- if local_command_config.database_adapter == "sqlite"
377
+ if local_command_config.database_adapter == 'sqlite'
382
378
  nil
383
379
  else
384
- options[:database] || prompt_required("Scratch database name")
380
+ options[:database] || prompt_required('Scratch database name')
385
381
  end
386
382
  end
387
383
  end
@@ -398,7 +394,7 @@ module KamalBackup
398
394
  token = tokens.first
399
395
 
400
396
  case token
401
- when "-d", "--destination", "-c", "--config-file"
397
+ when '-d', '--destination', '-c', '--config-file'
402
398
  leading << tokens.shift
403
399
  leading << tokens.shift if tokens.any?
404
400
  when /\A--destination=.+\z/, /\A--config-file=.+\z/
@@ -416,18 +412,18 @@ module KamalBackup
416
412
  end
417
413
  end
418
414
 
419
- package_name "kamal-backup"
415
+ package_name 'kamal-backup'
420
416
  map %w[-v --version] => :version
421
- class_option :config_file, aliases: "-c", type: :string, desc: "Path to Kamal deploy config file"
422
- class_option :destination, aliases: "-d", type: :string, desc: "Kamal destination to use"
417
+ class_option :config_file, aliases: '-c', type: :string, desc: 'Path to Kamal deploy config file'
418
+ class_option :destination, aliases: '-d', type: :string, desc: 'Kamal destination to use'
423
419
  remove_command :tree
424
- desc "restore SUBCOMMAND ...ARGS", "Restore a database and Active Storage backup locally or into production"
425
- subcommand "restore", RestoreCLI
426
- desc "drill SUBCOMMAND ...ARGS", "Run a restore drill on the local machine or on production infrastructure"
427
- subcommand "drill", DrillCLI
420
+ desc 'restore SUBCOMMAND ...ARGS', 'Restore a database and Active Storage backup locally or into production'
421
+ subcommand 'restore', RestoreCLI
422
+ desc 'drill SUBCOMMAND ...ARGS', 'Run a restore drill on the local machine or on production infrastructure'
423
+ subcommand 'drill', DrillCLI
428
424
 
429
425
  def self.basename
430
- "kamal-backup"
426
+ 'kamal-backup'
431
427
  end
432
428
 
433
429
  def self.start(argv = ARGV, env: ENV)
@@ -446,7 +442,7 @@ module KamalBackup
446
442
  exit(1)
447
443
  rescue Interrupt
448
444
  output ||= CommandOutput.new(io: $stderr, env: env)
449
- output.error("(Interrupt): interrupted", redactor: Redactor.new(env: env))
445
+ output.error('(Interrupt): interrupted', redactor: Redactor.new(env: env))
450
446
  exit(130)
451
447
  ensure
452
448
  self.command_env = nil
@@ -454,55 +450,56 @@ module KamalBackup
454
450
 
455
451
  include Helpers
456
452
 
457
- method_option :force, type: :boolean, default: false, desc: "Run a backup even if the configured schedule is not due"
458
- desc "backup", "Run a due database and Active Storage backup"
453
+ method_option :force, type: :boolean, default: false,
454
+ desc: 'Run a backup even if the configured schedule is not due'
455
+ desc 'backup', 'Run a due database and Active Storage backup'
459
456
  def backup
460
457
  if remote_command_mode?
461
- argv = ["kamal-backup", "backup"]
462
- argv << "--force" if options[:force]
458
+ argv = %w[kamal-backup backup]
459
+ argv << '--force' if options[:force]
463
460
  exec_remote(argv)
464
461
  else
465
462
  print_backup_result(direct_app.backup(force: options[:force]))
466
463
  end
467
464
  end
468
465
 
469
- desc "list", "List matching restic snapshots"
466
+ desc 'list', 'List matching restic snapshots'
470
467
  def list
471
468
  if remote_command_mode?
472
- exec_remote(["kamal-backup", "list"])
469
+ exec_remote(%w[kamal-backup list])
473
470
  else
474
471
  puts(direct_app.snapshots)
475
472
  end
476
473
  end
477
474
 
478
- desc "check", "Run restic check and record the latest result"
475
+ desc 'check', 'Run restic check and record the latest result'
479
476
  def check
480
477
  if remote_command_mode?
481
- exec_remote(["kamal-backup", "check"])
478
+ exec_remote(%w[kamal-backup check])
482
479
  else
483
480
  puts(direct_app.check)
484
481
  end
485
482
  end
486
483
 
487
- desc "prune", "Apply the configured restic retention policy and prune unneeded data"
484
+ desc 'prune', 'Apply the configured restic retention policy and prune unneeded data'
488
485
  def prune
489
486
  if remote_command_mode?
490
- exec_remote(["kamal-backup", "prune"])
487
+ exec_remote(%w[kamal-backup prune])
491
488
  else
492
489
  print_prune_result(direct_app.prune)
493
490
  end
494
491
  end
495
492
 
496
- desc "evidence", "Print redacted backup, check, and restore-drill evidence as JSON"
493
+ desc 'evidence', 'Print redacted backup, check, and restore-drill evidence as JSON'
497
494
  def evidence
498
495
  if remote_command_mode?
499
- exec_remote(["kamal-backup", "evidence"])
496
+ exec_remote(%w[kamal-backup evidence])
500
497
  else
501
498
  puts(direct_app.evidence)
502
499
  end
503
500
  end
504
501
 
505
- desc "validate", "Validate backup configuration without running a backup"
502
+ desc 'validate', 'Validate backup configuration without running a backup'
506
503
  def validate
507
504
  if remote_command_mode?
508
505
  validate_deploy_config
@@ -510,34 +507,34 @@ module KamalBackup
510
507
  direct_app.validate
511
508
  end
512
509
 
513
- puts("ok")
510
+ puts('ok')
514
511
  end
515
512
 
516
- desc "init", "Create config and print the scheduled backup accessory snippet"
513
+ desc 'init', 'Create config and print the scheduled backup accessory snippet'
517
514
  def init
518
515
  write_init_file(shared_config_path, shared_config_template)
519
516
 
520
517
  puts
521
- puts "Add this accessory block to your Kamal deploy config:"
518
+ puts 'Add this accessory block to your Kamal deploy config:'
522
519
  puts
523
520
  puts deploy_snippet
524
521
  puts
525
- puts "The accessory runs scheduled database and file backups with backup.schedule."
526
- puts "For most Rails apps, restore local and drill local can infer the development database, Active Storage path, and tmp state directory."
527
- puts "Local restore and drill also require the restic binary on your machine."
528
- puts "Create config/kamal-backup.local.yml only if you need to override those local defaults."
522
+ puts 'The accessory runs scheduled database and file backups with backup.schedule.'
523
+ puts 'For most Rails apps, restore local and drill local can infer the development database, Active Storage path, and tmp state directory.'
524
+ puts 'Local restore and drill also require the restic binary on your machine.'
525
+ puts 'Create config/kamal-backup.local.yml only if you need to override those local defaults.'
529
526
  end
530
527
 
531
- desc "schedule", "Run the foreground scheduler loop"
528
+ desc 'schedule', 'Run the foreground scheduler loop'
532
529
  def schedule
533
530
  if deployment_mode?
534
- exec_remote(["kamal-backup", "schedule"])
531
+ exec_remote(%w[kamal-backup schedule])
535
532
  else
536
533
  direct_app.schedule
537
534
  end
538
535
  end
539
536
 
540
- desc "version", "Print the running kamal-backup version"
537
+ desc 'version', 'Print the running kamal-backup version'
541
538
  def version
542
539
  if remote_command_mode?
543
540
  print_remote_version_status
@@ -545,6 +542,5 @@ module KamalBackup
545
542
  puts(VERSION)
546
543
  end
547
544
  end
548
-
549
545
  end
550
546
  end