kamal-backup 0.3.0.beta21 → 0.3.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/README.md +1 -0
- data/exe/kamal-backup +7 -6
- data/lib/kamal_backup/app.rb +330 -393
- data/lib/kamal_backup/cli/helpers.rb +298 -0
- data/lib/kamal_backup/cli.rb +73 -367
- data/lib/kamal_backup/command.rb +77 -258
- data/lib/kamal_backup/command_output.rb +189 -0
- data/lib/kamal_backup/config.rb +242 -624
- data/lib/kamal_backup/config_file.rb +376 -0
- data/lib/kamal_backup/databases/base.rb +28 -14
- data/lib/kamal_backup/databases/mysql.rb +68 -67
- data/lib/kamal_backup/databases/postgres.rb +59 -58
- data/lib/kamal_backup/databases/sqlite.rb +21 -20
- data/lib/kamal_backup/errors.rb +3 -1
- data/lib/kamal_backup/evidence.rb +61 -63
- data/lib/kamal_backup/kamal_bridge.rb +270 -254
- data/lib/kamal_backup/rails_app.rb +94 -104
- data/lib/kamal_backup/redactor.rb +18 -13
- data/lib/kamal_backup/restic.rb +207 -183
- data/lib/kamal_backup/scheduler.rb +17 -14
- data/lib/kamal_backup/schema.rb +2 -0
- data/lib/kamal_backup/version.rb +3 -1
- data/lib/kamal_backup/yaml_access.rb +13 -0
- data/lib/kamal_backup.rb +22 -17
- metadata +76 -2
data/lib/kamal_backup/cli.rb
CHANGED
|
@@ -1,313 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'thor'
|
|
5
|
+
require_relative 'app'
|
|
6
|
+
require_relative 'cli/helpers'
|
|
7
|
+
require_relative 'command_output'
|
|
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
|
|
13
|
-
module Helpers
|
|
14
|
-
def command_env
|
|
15
|
-
CLI.command_env || ENV
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def redactor
|
|
19
|
-
@redactor ||= Redactor.new(env: command_env)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def direct_app
|
|
23
|
-
@direct_app ||= App.new(
|
|
24
|
-
config: Config.new(env: command_env),
|
|
25
|
-
redactor: redactor
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def local_restore_app
|
|
30
|
-
@local_restore_app ||= App.new(
|
|
31
|
-
config: local_command_config,
|
|
32
|
-
redactor: redactor
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def local_preferences
|
|
37
|
-
@local_preferences ||= Config.new(env: command_env)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
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
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def production_source_defaults
|
|
55
|
-
shared_config_source_defaults.merge(bridge.local_restore_defaults(accessory_name: accessory_name))
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def shared_config_source_defaults
|
|
59
|
-
config = Config.new(env: {}, config_paths: [Config::SHARED_CONFIG_PATH], load_project_defaults: false)
|
|
60
|
-
|
|
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?
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def bridge
|
|
70
|
-
@bridge ||= KamalBridge.new(
|
|
71
|
-
redactor: redactor,
|
|
72
|
-
config_file: options[:config_file],
|
|
73
|
-
destination: options[:destination],
|
|
74
|
-
env: command_env,
|
|
75
|
-
stdout: $stdout,
|
|
76
|
-
stderr: $stderr
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def deployment_mode?
|
|
81
|
-
!options[:destination].to_s.strip.empty? || !options[:config_file].to_s.strip.empty?
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def default_deploy_config?
|
|
85
|
-
File.file?(File.expand_path(KamalBridge::DEFAULT_CONFIG_FILE))
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def remote_command_mode?
|
|
89
|
-
deployment_mode? || default_deploy_config?
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def accessory_name
|
|
93
|
-
@accessory_name ||= bridge.accessory_name(preferred: local_preferences.accessory_name)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def remote_version
|
|
97
|
-
@remote_version ||= bridge.remote_version(accessory_name: accessory_name)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def exec_remote(argv, require_version_match: true)
|
|
101
|
-
ensure_remote_version_match! if require_version_match
|
|
102
|
-
|
|
103
|
-
result = bridge.execute_on_accessory(
|
|
104
|
-
accessory_name: accessory_name,
|
|
105
|
-
command: Shellwords.join(argv),
|
|
106
|
-
stream: true
|
|
107
|
-
)
|
|
108
|
-
print(result.stdout) unless result.streamed
|
|
109
|
-
$stderr.print(result.stderr) if !result.streamed && !result.stderr.empty?
|
|
110
|
-
result
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def ensure_remote_version_match!
|
|
114
|
-
return if remote_version == VERSION
|
|
115
|
-
|
|
116
|
-
raise ConfigurationError, <<~MESSAGE.strip
|
|
117
|
-
local gem version #{VERSION} does not match remote accessory version #{remote_version}.
|
|
118
|
-
Reboot the backup accessory to pick up the latest image:
|
|
119
|
-
#{accessory_reboot_command}
|
|
120
|
-
MESSAGE
|
|
121
|
-
end
|
|
122
|
-
|
|
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]
|
|
127
|
-
Shellwords.join(argv)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def print_remote_version_status
|
|
131
|
-
status = remote_version == VERSION ? "in sync" : "out of sync"
|
|
132
|
-
status_color = status == "in sync" ? :green : :red
|
|
133
|
-
status_output = CommandOutput.new(io: $stdout, env: command_env)
|
|
134
|
-
|
|
135
|
-
puts("local: #{VERSION}")
|
|
136
|
-
puts("remote: #{remote_version}")
|
|
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"
|
|
139
|
-
end
|
|
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
|
-
|
|
161
|
-
def print_prune_result(results)
|
|
162
|
-
output = Array(results).map(&:stdout).join
|
|
163
|
-
|
|
164
|
-
if output.empty?
|
|
165
|
-
puts("Prune completed")
|
|
166
|
-
else
|
|
167
|
-
print(output)
|
|
168
|
-
puts unless output.end_with?("\n")
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def validate_deploy_config
|
|
173
|
-
config = Config.new(
|
|
174
|
-
env: bridge.accessory_environment(accessory_name: accessory_name),
|
|
175
|
-
config_paths: [Config::SHARED_CONFIG_PATH],
|
|
176
|
-
load_project_defaults: false
|
|
177
|
-
)
|
|
178
|
-
config.validate_backup(check_files: false)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def confirm!(message)
|
|
182
|
-
return if options[:yes]
|
|
183
|
-
|
|
184
|
-
unless $stdin.tty?
|
|
185
|
-
raise ConfigurationError, "confirmation required; rerun with --yes"
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
unless yes?("#{message} [y/N]")
|
|
189
|
-
raise ConfigurationError, "aborted"
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def confirm_production_restore!(snapshot)
|
|
194
|
-
return if options[:"confirm-production-restore"]
|
|
195
|
-
|
|
196
|
-
if options[:yes]
|
|
197
|
-
raise ConfigurationError, "--yes does not bypass restore production; use --confirm-production-restore only for deliberate automation"
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
unless $stdin.tty?
|
|
201
|
-
raise ConfigurationError, "production restore confirmation required; rerun interactively or pass --confirm-production-restore only for deliberate automation"
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
app_name = production_restore_confirmation_config.required_app_name
|
|
205
|
-
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")
|
|
208
|
-
confirm!("Restore #{snapshot} into production now? This will overwrite production data.")
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def require_typed_confirmation(prompt, expected)
|
|
212
|
-
answer = ask("#{prompt}:").to_s.strip
|
|
213
|
-
return if answer == expected
|
|
214
|
-
|
|
215
|
-
raise ConfigurationError, "aborted"
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def production_restore_confirmation_config
|
|
219
|
-
if deployment_mode?
|
|
220
|
-
Config.new(
|
|
221
|
-
env: bridge.accessory_environment(accessory_name: accessory_name),
|
|
222
|
-
config_paths: [Config::SHARED_CONFIG_PATH],
|
|
223
|
-
load_project_defaults: false
|
|
224
|
-
)
|
|
225
|
-
else
|
|
226
|
-
direct_app.config
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def prompt_required(label)
|
|
231
|
-
unless $stdin.tty?
|
|
232
|
-
raise ConfigurationError, "#{label.downcase} is required; pass it on the command line"
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
value = ask("#{label}:").to_s.strip
|
|
236
|
-
if value.empty?
|
|
237
|
-
raise ConfigurationError, "#{label.downcase} is required"
|
|
238
|
-
else
|
|
239
|
-
value
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def init_config_root
|
|
244
|
-
config_file = options[:config_file] || KamalBridge::DEFAULT_CONFIG_FILE
|
|
245
|
-
File.dirname(File.expand_path(config_file))
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def shared_config_path
|
|
249
|
-
File.join(init_config_root, "kamal-backup.yml")
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def write_init_file(path, contents)
|
|
253
|
-
if File.exist?(path)
|
|
254
|
-
say "Exists: #{path}", :yellow
|
|
255
|
-
else
|
|
256
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
257
|
-
File.write(path, contents)
|
|
258
|
-
say "Created: #{path}", :green
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def shared_config_template
|
|
263
|
-
<<~YAML
|
|
264
|
-
app: your-app
|
|
265
|
-
accessory: backup
|
|
266
|
-
databases:
|
|
267
|
-
- name: app
|
|
268
|
-
adapter: postgres
|
|
269
|
-
url: postgres://your-app@your-db:5432/your_app_production
|
|
270
|
-
password:
|
|
271
|
-
secret: DATABASE_PASSWORD
|
|
272
|
-
paths:
|
|
273
|
-
- /data/storage
|
|
274
|
-
restic:
|
|
275
|
-
repository: s3:https://s3.example.com/your-app-backups
|
|
276
|
-
password:
|
|
277
|
-
secret: RESTIC_PASSWORD
|
|
278
|
-
init_if_missing: true
|
|
279
|
-
backup:
|
|
280
|
-
schedule: 1d
|
|
281
|
-
YAML
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def deploy_snippet
|
|
285
|
-
<<~YAML
|
|
286
|
-
accessories:
|
|
287
|
-
backup:
|
|
288
|
-
image: ghcr.io/crmne/kamal-backup:#{VERSION}
|
|
289
|
-
host: your-server.example.com
|
|
290
|
-
files:
|
|
291
|
-
- config/kamal-backup.yml:/app/config/kamal-backup.yml:ro
|
|
292
|
-
env:
|
|
293
|
-
secret:
|
|
294
|
-
- DATABASE_PASSWORD
|
|
295
|
-
- RESTIC_PASSWORD
|
|
296
|
-
- AWS_ACCESS_KEY_ID
|
|
297
|
-
- AWS_SECRET_ACCESS_KEY
|
|
298
|
-
volumes:
|
|
299
|
-
- "your_app_storage:/data/storage:ro"
|
|
300
|
-
- "your_app_backup_state:/var/lib/kamal-backup"
|
|
301
|
-
YAML
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
15
|
class CommandBase < Thor
|
|
306
16
|
include Helpers
|
|
307
17
|
|
|
308
|
-
class_option :yes, aliases:
|
|
309
|
-
class_option :config_file, aliases:
|
|
310
|
-
class_option :destination, aliases:
|
|
18
|
+
class_option :yes, aliases: '-y', type: :boolean, default: false, desc: 'Skip confirmation prompt'
|
|
19
|
+
class_option :config_file, aliases: '-c', type: :string, desc: 'Path to Kamal deploy config file'
|
|
20
|
+
class_option :destination, aliases: '-d', type: :string, desc: 'Kamal destination to use'
|
|
311
21
|
remove_command :tree
|
|
312
22
|
end
|
|
313
23
|
|
|
@@ -316,19 +26,20 @@ module KamalBackup
|
|
|
316
26
|
CLI.basename
|
|
317
27
|
end
|
|
318
28
|
|
|
319
|
-
desc
|
|
320
|
-
def local(snapshot =
|
|
29
|
+
desc 'local [SNAPSHOT]', 'Restore the backup into the local database and Active Storage path'
|
|
30
|
+
def local(snapshot = 'latest')
|
|
321
31
|
confirm!("Restore #{snapshot} into the local database and Active Storage path? This will overwrite local data.")
|
|
322
32
|
puts(JSON.pretty_generate(local_restore_app.restore_to_local_machine(snapshot)))
|
|
323
33
|
end
|
|
324
34
|
|
|
325
|
-
method_option :"confirm-production-restore", type: :boolean, default: false,
|
|
326
|
-
|
|
327
|
-
|
|
35
|
+
method_option :"confirm-production-restore", type: :boolean, default: false,
|
|
36
|
+
desc: 'Confirm production restore without interactive prompts'
|
|
37
|
+
desc 'production [SNAPSHOT]', 'Restore the backup into the production database and Active Storage path'
|
|
38
|
+
def production(snapshot = 'latest')
|
|
328
39
|
confirm_production_restore!(snapshot)
|
|
329
40
|
|
|
330
41
|
if deployment_mode?
|
|
331
|
-
exec_remote([
|
|
42
|
+
exec_remote(['kamal-backup', 'restore', 'production', snapshot, '--confirm-production-restore'])
|
|
332
43
|
else
|
|
333
44
|
puts(JSON.pretty_generate(direct_app.restore_to_production(snapshot)))
|
|
334
45
|
end
|
|
@@ -340,28 +51,29 @@ module KamalBackup
|
|
|
340
51
|
CLI.basename
|
|
341
52
|
end
|
|
342
53
|
|
|
343
|
-
method_option :check, type: :string, desc:
|
|
344
|
-
desc
|
|
345
|
-
def local(snapshot =
|
|
54
|
+
method_option :check, type: :string, desc: 'Run a verification command after the restore'
|
|
55
|
+
desc 'local [SNAPSHOT]', 'Run a restore drill on the local machine'
|
|
56
|
+
def local(snapshot = 'latest')
|
|
346
57
|
confirm!("Run a local restore drill for #{snapshot}? This will overwrite local data.")
|
|
347
58
|
result = local_restore_app.drill_on_local_machine(snapshot, check_command: options[:check])
|
|
348
59
|
puts(JSON.pretty_generate(result))
|
|
349
|
-
exit(1)
|
|
60
|
+
exit(1) unless result[:status] == 'ok'
|
|
350
61
|
end
|
|
351
62
|
|
|
352
|
-
method_option :database, type: :string, desc:
|
|
353
|
-
method_option :"sqlite-path", type: :string, desc:
|
|
354
|
-
method_option :files, type: :string, default:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
63
|
+
method_option :database, type: :string, desc: 'Scratch database name for PostgreSQL or MySQL'
|
|
64
|
+
method_option :"sqlite-path", type: :string, desc: 'Scratch SQLite path for production-side drills'
|
|
65
|
+
method_option :files, type: :string, default: '/restore/files',
|
|
66
|
+
desc: 'Scratch Active Storage target for the drill'
|
|
67
|
+
method_option :check, type: :string, desc: 'Run a verification command after the restore'
|
|
68
|
+
desc 'production [SNAPSHOT]', 'Run a restore drill on production infrastructure using scratch targets'
|
|
69
|
+
def production(snapshot = 'latest')
|
|
358
70
|
confirm!("Run a production-side restore drill for #{snapshot}? This will restore into scratch targets on production infrastructure.")
|
|
359
71
|
|
|
360
72
|
if deployment_mode?
|
|
361
|
-
argv = [
|
|
362
|
-
argv.concat([
|
|
363
|
-
argv.concat([
|
|
364
|
-
argv.concat([
|
|
73
|
+
argv = ['kamal-backup', 'drill', 'production', snapshot, '--files', options[:files], '--yes']
|
|
74
|
+
argv.concat(['--database', production_database_name]) if production_database_name
|
|
75
|
+
argv.concat(['--sqlite-path', options[:"sqlite-path"]]) if options[:"sqlite-path"]
|
|
76
|
+
argv.concat(['--check', options[:check]]) if options[:check]
|
|
365
77
|
exec_remote(argv)
|
|
366
78
|
else
|
|
367
79
|
result = direct_app.drill_on_production(
|
|
@@ -372,16 +84,16 @@ module KamalBackup
|
|
|
372
84
|
check_command: options[:check]
|
|
373
85
|
)
|
|
374
86
|
puts(JSON.pretty_generate(result))
|
|
375
|
-
exit(1)
|
|
87
|
+
exit(1) unless result[:status] == 'ok'
|
|
376
88
|
end
|
|
377
89
|
end
|
|
378
90
|
|
|
379
91
|
no_commands do
|
|
380
92
|
def production_database_name
|
|
381
|
-
if local_command_config.database_adapter ==
|
|
93
|
+
if local_command_config.database_adapter == 'sqlite'
|
|
382
94
|
nil
|
|
383
95
|
else
|
|
384
|
-
options[:database] || prompt_required(
|
|
96
|
+
options[:database] || prompt_required('Scratch database name')
|
|
385
97
|
end
|
|
386
98
|
end
|
|
387
99
|
end
|
|
@@ -398,7 +110,7 @@ module KamalBackup
|
|
|
398
110
|
token = tokens.first
|
|
399
111
|
|
|
400
112
|
case token
|
|
401
|
-
when
|
|
113
|
+
when '-d', '--destination', '-c', '--config-file'
|
|
402
114
|
leading << tokens.shift
|
|
403
115
|
leading << tokens.shift if tokens.any?
|
|
404
116
|
when /\A--destination=.+\z/, /\A--config-file=.+\z/
|
|
@@ -416,18 +128,18 @@ module KamalBackup
|
|
|
416
128
|
end
|
|
417
129
|
end
|
|
418
130
|
|
|
419
|
-
package_name
|
|
131
|
+
package_name 'kamal-backup'
|
|
420
132
|
map %w[-v --version] => :version
|
|
421
|
-
class_option :config_file, aliases:
|
|
422
|
-
class_option :destination, aliases:
|
|
133
|
+
class_option :config_file, aliases: '-c', type: :string, desc: 'Path to Kamal deploy config file'
|
|
134
|
+
class_option :destination, aliases: '-d', type: :string, desc: 'Kamal destination to use'
|
|
423
135
|
remove_command :tree
|
|
424
|
-
desc
|
|
425
|
-
subcommand
|
|
426
|
-
desc
|
|
427
|
-
subcommand
|
|
136
|
+
desc 'restore SUBCOMMAND ...ARGS', 'Restore a database and Active Storage backup locally or into production'
|
|
137
|
+
subcommand 'restore', RestoreCLI
|
|
138
|
+
desc 'drill SUBCOMMAND ...ARGS', 'Run a restore drill on the local machine or on production infrastructure'
|
|
139
|
+
subcommand 'drill', DrillCLI
|
|
428
140
|
|
|
429
141
|
def self.basename
|
|
430
|
-
|
|
142
|
+
'kamal-backup'
|
|
431
143
|
end
|
|
432
144
|
|
|
433
145
|
def self.start(argv = ARGV, env: ENV)
|
|
@@ -436,17 +148,11 @@ module KamalBackup
|
|
|
436
148
|
Command.with_output(output) do
|
|
437
149
|
super(normalize_global_options(argv))
|
|
438
150
|
end
|
|
439
|
-
rescue Error => e
|
|
440
|
-
output ||= CommandOutput.new(io: $stderr, env: env)
|
|
441
|
-
output.error("(#{e.class}): #{e.message}", redactor: Redactor.new(env: env))
|
|
442
|
-
exit(1)
|
|
443
151
|
rescue StandardError => e
|
|
444
|
-
output ||= CommandOutput.new(io: $stderr, env: env)
|
|
445
152
|
output.error("(#{e.class}): #{e.message}", redactor: Redactor.new(env: env))
|
|
446
153
|
exit(1)
|
|
447
154
|
rescue Interrupt
|
|
448
|
-
output
|
|
449
|
-
output.error("(Interrupt): interrupted", redactor: Redactor.new(env: env))
|
|
155
|
+
output.error('(Interrupt): interrupted', redactor: Redactor.new(env: env))
|
|
450
156
|
exit(130)
|
|
451
157
|
ensure
|
|
452
158
|
self.command_env = nil
|
|
@@ -454,55 +160,56 @@ module KamalBackup
|
|
|
454
160
|
|
|
455
161
|
include Helpers
|
|
456
162
|
|
|
457
|
-
method_option :force, type: :boolean, default: false,
|
|
458
|
-
|
|
163
|
+
method_option :force, type: :boolean, default: false,
|
|
164
|
+
desc: 'Run a backup even if the configured schedule is not due'
|
|
165
|
+
desc 'backup', 'Run a due database and Active Storage backup'
|
|
459
166
|
def backup
|
|
460
167
|
if remote_command_mode?
|
|
461
|
-
argv = [
|
|
462
|
-
argv <<
|
|
168
|
+
argv = %w[kamal-backup backup]
|
|
169
|
+
argv << '--force' if options[:force]
|
|
463
170
|
exec_remote(argv)
|
|
464
171
|
else
|
|
465
172
|
print_backup_result(direct_app.backup(force: options[:force]))
|
|
466
173
|
end
|
|
467
174
|
end
|
|
468
175
|
|
|
469
|
-
desc
|
|
176
|
+
desc 'list', 'List matching restic snapshots'
|
|
470
177
|
def list
|
|
471
178
|
if remote_command_mode?
|
|
472
|
-
exec_remote([
|
|
179
|
+
exec_remote(%w[kamal-backup list])
|
|
473
180
|
else
|
|
474
181
|
puts(direct_app.snapshots)
|
|
475
182
|
end
|
|
476
183
|
end
|
|
477
184
|
|
|
478
|
-
desc
|
|
185
|
+
desc 'check', 'Run restic check and record the latest result'
|
|
479
186
|
def check
|
|
480
187
|
if remote_command_mode?
|
|
481
|
-
exec_remote([
|
|
188
|
+
exec_remote(%w[kamal-backup check])
|
|
482
189
|
else
|
|
483
190
|
puts(direct_app.check)
|
|
484
191
|
end
|
|
485
192
|
end
|
|
486
193
|
|
|
487
|
-
desc
|
|
194
|
+
desc 'prune', 'Apply the configured restic retention policy and prune unneeded data'
|
|
488
195
|
def prune
|
|
489
196
|
if remote_command_mode?
|
|
490
|
-
exec_remote([
|
|
197
|
+
exec_remote(%w[kamal-backup prune])
|
|
491
198
|
else
|
|
492
199
|
print_prune_result(direct_app.prune)
|
|
493
200
|
end
|
|
494
201
|
end
|
|
495
202
|
|
|
496
|
-
desc
|
|
203
|
+
desc 'evidence', 'Print redacted backup, check, and restore-drill evidence as JSON'
|
|
497
204
|
def evidence
|
|
498
205
|
if remote_command_mode?
|
|
499
|
-
exec_remote([
|
|
206
|
+
exec_remote(%w[kamal-backup evidence])
|
|
500
207
|
else
|
|
501
208
|
puts(direct_app.evidence)
|
|
502
209
|
end
|
|
503
210
|
end
|
|
504
211
|
|
|
505
|
-
desc
|
|
212
|
+
desc 'validate', 'Validate backup configuration without running a backup'
|
|
506
213
|
def validate
|
|
507
214
|
if remote_command_mode?
|
|
508
215
|
validate_deploy_config
|
|
@@ -510,34 +217,34 @@ module KamalBackup
|
|
|
510
217
|
direct_app.validate
|
|
511
218
|
end
|
|
512
219
|
|
|
513
|
-
puts(
|
|
220
|
+
puts('ok')
|
|
514
221
|
end
|
|
515
222
|
|
|
516
|
-
desc
|
|
223
|
+
desc 'init', 'Create config and print the scheduled backup accessory snippet'
|
|
517
224
|
def init
|
|
518
225
|
write_init_file(shared_config_path, shared_config_template)
|
|
519
226
|
|
|
520
227
|
puts
|
|
521
|
-
puts
|
|
228
|
+
puts 'Add this accessory block to your Kamal deploy config:'
|
|
522
229
|
puts
|
|
523
230
|
puts deploy_snippet
|
|
524
231
|
puts
|
|
525
|
-
puts
|
|
526
|
-
puts
|
|
527
|
-
puts
|
|
528
|
-
puts
|
|
232
|
+
puts 'The accessory runs scheduled database and file backups with backup.schedule.'
|
|
233
|
+
puts 'For most Rails apps, restore local and drill local can infer the development database, Active Storage path, and tmp state directory.'
|
|
234
|
+
puts 'Local restore and drill also require the restic binary on your machine.'
|
|
235
|
+
puts 'Create config/kamal-backup.local.yml only if you need to override those local defaults.'
|
|
529
236
|
end
|
|
530
237
|
|
|
531
|
-
desc
|
|
238
|
+
desc 'schedule', 'Run the foreground scheduler loop'
|
|
532
239
|
def schedule
|
|
533
240
|
if deployment_mode?
|
|
534
|
-
exec_remote([
|
|
241
|
+
exec_remote(%w[kamal-backup schedule])
|
|
535
242
|
else
|
|
536
243
|
direct_app.schedule
|
|
537
244
|
end
|
|
538
245
|
end
|
|
539
246
|
|
|
540
|
-
desc
|
|
247
|
+
desc 'version', 'Print the running kamal-backup version'
|
|
541
248
|
def version
|
|
542
249
|
if remote_command_mode?
|
|
543
250
|
print_remote_version_status
|
|
@@ -545,6 +252,5 @@ module KamalBackup
|
|
|
545
252
|
puts(VERSION)
|
|
546
253
|
end
|
|
547
254
|
end
|
|
548
|
-
|
|
549
255
|
end
|
|
550
256
|
end
|