Class: KamalBackup::Config
- Inherits:
-
Object
- Object
- KamalBackup::Config
show all
- Defined in:
- lib/kamal_backup/config.rb
Defined Under Namespace
Classes: ConfigData, DatabaseSource, PathDefinition
Constant Summary
collapse
- DEFAULT_RETENTION =
{
'RESTIC_KEEP_LAST' => '7',
'RESTIC_KEEP_DAILY' => '7',
'RESTIC_KEEP_WEEKLY' => '4',
'RESTIC_KEEP_MONTHLY' => '6',
'RESTIC_KEEP_YEARLY' => '2'
}.freeze
- SUSPICIOUS_BACKUP_PATHS =
%w[/ /var /etc /root /usr /bin /sbin /boot /dev /proc /sys /run].freeze
- SHARED_CONFIG_PATH =
'config/kamal-backup.yml'
- LOCAL_CONFIG_PATH =
'config/kamal-backup.local.yml'
- DEFAULT_CONFIG_PATHS =
[SHARED_CONFIG_PATH, LOCAL_CONFIG_PATH].freeze
- TOP_LEVEL_YAML_KEYS =
%w[app accessory databases paths restore_from restic backup state].freeze
- LEGACY_YAML_KEYS =
%w[
app_name
database_adapter
database_url
sqlite_database_path
backup_paths
local_restore_source_paths
restic_repository
restic_repository_file
restic_password
restic_password_file
restic_password_command
restic_init_if_missing
restic_check_after_backup
restic_check_read_data_subset
restic_forget_after_backup
restic_keep_last
restic_keep_daily
restic_keep_weekly
restic_keep_monthly
restic_keep_yearly
backup_schedule_seconds
backup_start_delay_seconds
state_dir
allow_suspicious_paths
pgpassword
mysql_pwd
].freeze
Instance Attribute Summary collapse
Instance Method Summary
collapse
Constructor Details
#initialize(env: ENV, cwd: Dir.pwd, defaults: {}, config_paths: nil, load_project_defaults: true) ⇒ Config
Returns a new instance of Config.
140
141
142
143
144
145
146
147
148
|
# File 'lib/kamal_backup/config.rb', line 140
def initialize(env: ENV, cwd: Dir.pwd, defaults: {}, config_paths: nil, load_project_defaults: true)
raw_env = env.to_h
base = load_project_defaults ? project_defaults(cwd: cwd) : {}
config_data = load_config_files(raw_env, cwd: cwd, paths: config_paths)
@database_definitions = config_data.database_definitions
@path_definitions = config_data.path_definitions
@restore_from_definitions = config_data.restore_from_definitions
@env = base.merge(defaults.to_h).merge(config_data.env).merge(raw_env)
end
|
Instance Attribute Details
#env ⇒ Object
Returns the value of attribute env.
138
139
140
|
# File 'lib/kamal_backup/config.rb', line 138
def env
@env
end
|
Instance Method Details
#accessory_name ⇒ Object
158
159
160
|
# File 'lib/kamal_backup/config.rb', line 158
def accessory_name
value('KAMAL_BACKUP_ACCESSORY')
end
|
#allow_in_place_file_restore? ⇒ Boolean
198
199
200
|
# File 'lib/kamal_backup/config.rb', line 198
def allow_in_place_file_restore?
truthy?('KAMAL_BACKUP_ALLOW_IN_PLACE_FILE_RESTORE')
end
|
#allow_suspicious_backup_paths? ⇒ Boolean
202
203
204
|
# File 'lib/kamal_backup/config.rb', line 202
def allow_suspicious_backup_paths?
truthy?('KAMAL_BACKUP_ALLOW_SUSPICIOUS_PATHS')
end
|
#app_name ⇒ Object
150
151
152
|
# File 'lib/kamal_backup/config.rb', line 150
def app_name
value('APP_NAME')
end
|
#backup_path_excludes(paths = backup_paths) ⇒ Object
238
239
240
241
242
|
# File 'lib/kamal_backup/config.rb', line 238
def backup_path_excludes(paths = backup_paths)
paths = Array(paths).compact.map(&:to_s).reject(&:empty?)
configured_backup_path_excludes(paths) + sqlite_backup_path_excludes(paths)
end
|
#backup_path_label(path) ⇒ Object
261
262
263
264
|
# File 'lib/kamal_backup/config.rb', line 261
def backup_path_label(path)
label = path.to_s.sub(%r{\A/+}, '').gsub(/[^A-Za-z0-9_.-]+/, '-')
label.empty? ? 'root' : label
end
|
#backup_paths ⇒ Object
230
231
232
233
234
235
236
|
# File 'lib/kamal_backup/config.rb', line 230
def backup_paths
if path_definitions?
@path_definitions.map(&:path)
else
legacy_backup_paths
end
end
|
#backup_schedule_seconds ⇒ Object
206
207
208
|
# File 'lib/kamal_backup/config.rb', line 206
def backup_schedule_seconds
integer('BACKUP_SCHEDULE_SECONDS', 86_400, minimum: 1)
end
|
#backup_start_delay_seconds ⇒ Object
210
211
212
|
# File 'lib/kamal_backup/config.rb', line 210
def backup_start_delay_seconds
integer('BACKUP_START_DELAY_SECONDS', 0, minimum: 0)
end
|
#check_after_backup? ⇒ Boolean
186
187
188
|
# File 'lib/kamal_backup/config.rb', line 186
def check_after_backup?
truthy?('RESTIC_CHECK_AFTER_BACKUP')
end
|
#check_read_data_subset ⇒ Object
194
195
196
|
# File 'lib/kamal_backup/config.rb', line 194
def check_read_data_subset
value('RESTIC_CHECK_READ_DATA_SUBSET')
end
|
#database_adapter ⇒ Object
266
267
268
269
270
271
272
|
# File 'lib/kamal_backup/config.rb', line 266
def database_adapter
if database_definitions?
databases.first&.database_adapter
else
legacy_database_adapter
end
end
|
#database_name ⇒ Object
274
275
276
|
# File 'lib/kamal_backup/config.rb', line 274
def database_name
'app'
end
|
#databases ⇒ Object
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
|
# File 'lib/kamal_backup/config.rb', line 278
def databases
@databases ||= if database_definitions?
@database_definitions.map do |definition|
DatabaseSource.new(
parent: self,
name: definition.fetch(:name),
adapter: definition.fetch(:adapter),
env: definition.fetch(:env),
structured: true,
missing_secrets: definition.fetch(:missing_secrets, [])
)
end
elsif legacy_database_adapter
[
DatabaseSource.new(
parent: self,
name: database_name,
adapter: legacy_database_adapter,
env: {},
structured: false
)
]
else
[]
end
end
|
#falsey?(key) ⇒ Boolean
440
441
442
|
# File 'lib/kamal_backup/config.rb', line 440
def falsey?(key)
%w[0 false no n off].include?(value(key).to_s.downcase)
end
|
#forget_after_backup? ⇒ Boolean
190
191
192
|
# File 'lib/kamal_backup/config.rb', line 190
def forget_after_backup?
!falsey?('RESTIC_FORGET_AFTER_BACKUP')
end
|
#last_backup_path ⇒ Object
222
223
224
|
# File 'lib/kamal_backup/config.rb', line 222
def last_backup_path
File.join(state_dir, 'last_backup.json')
end
|
#last_check_path ⇒ Object
218
219
220
|
# File 'lib/kamal_backup/config.rb', line 218
def last_check_path
File.join(state_dir, 'last_check.json')
end
|
#last_restore_drill_path ⇒ Object
226
227
228
|
# File 'lib/kamal_backup/config.rb', line 226
def last_restore_drill_path
File.join(state_dir, 'last_restore_drill.json')
end
|
#local_restore_path_pairs ⇒ Object
252
253
254
255
256
257
258
259
|
# File 'lib/kamal_backup/config.rb', line 252
def local_restore_path_pairs
source_paths = local_restore_source_paths
target_paths = backup_paths
raise ConfigurationError, 'local restore source paths must contain the same number of paths as file paths' unless source_paths.size == target_paths.size
source_paths.zip(target_paths)
end
|
#local_restore_source_paths ⇒ Object
244
245
246
247
248
249
250
|
# File 'lib/kamal_backup/config.rb', line 244
def local_restore_source_paths
if path_definitions?
@restore_from_definitions || legacy_local_restore_source_paths || backup_paths
else
legacy_local_restore_source_paths || backup_paths
end
end
|
#production_like_target?(target) ⇒ Boolean
418
419
420
421
422
|
# File 'lib/kamal_backup/config.rb', line 418
def production_like_target?(target)
target = target.to_s
source_database_targets.include?(target) || production_named_target?(target.downcase)
end
|
#required_app_name ⇒ Object
154
155
156
|
# File 'lib/kamal_backup/config.rb', line 154
def required_app_name
required_value('APP_NAME')
end
|
#required_value(key) ⇒ Object
432
433
434
|
# File 'lib/kamal_backup/config.rb', line 432
def required_value(key)
value(key) || raise(ConfigurationError, "#{key} is required")
end
|
#restic_init_if_missing? ⇒ Boolean
182
183
184
|
# File 'lib/kamal_backup/config.rb', line 182
def restic_init_if_missing?
truthy?('RESTIC_INIT_IF_MISSING')
end
|
#restic_password ⇒ Object
170
171
172
|
# File 'lib/kamal_backup/config.rb', line 170
def restic_password
value('RESTIC_PASSWORD')
end
|
#restic_password_command ⇒ Object
178
179
180
|
# File 'lib/kamal_backup/config.rb', line 178
def restic_password_command
value('RESTIC_PASSWORD_COMMAND')
end
|
#restic_password_file ⇒ Object
174
175
176
|
# File 'lib/kamal_backup/config.rb', line 174
def restic_password_file
value('RESTIC_PASSWORD_FILE')
end
|
#restic_repository ⇒ Object
162
163
164
|
# File 'lib/kamal_backup/config.rb', line 162
def restic_repository
value('RESTIC_REPOSITORY')
end
|
#restic_repository_file ⇒ Object
166
167
168
|
# File 'lib/kamal_backup/config.rb', line 166
def restic_repository_file
value('RESTIC_REPOSITORY_FILE')
end
|
#retention ⇒ Object
305
306
307
308
309
|
# File 'lib/kamal_backup/config.rb', line 305
def retention
DEFAULT_RETENTION.each_with_object({}) do |(key, default), result|
result[key] = value(key) || default
end
end
|
#retention_args ⇒ Object
311
312
313
314
315
316
317
318
319
320
321
322
323
|
# File 'lib/kamal_backup/config.rb', line 311
def retention_args
retention.each_with_object([]) do |(key, raw), args|
next if raw.to_s.empty?
number = Integer(raw)
next if number <= 0
flag = "--#{key.sub('RESTIC_KEEP_', 'keep-').downcase.tr('_', '-')}"
args.concat([flag, number.to_s])
rescue ArgumentError
raise ConfigurationError, "#{key} must be an integer"
end
end
|
#state_dir ⇒ Object
214
215
216
|
# File 'lib/kamal_backup/config.rb', line 214
def state_dir
value('KAMAL_BACKUP_STATE_DIR') || '/var/lib/kamal-backup'
end
|
#truthy?(key) ⇒ Boolean
436
437
438
|
# File 'lib/kamal_backup/config.rb', line 436
def truthy?(key)
%w[1 true yes y on].include?(value(key).to_s.downcase)
end
|
#validate_backup(check_files: true) ⇒ Object
331
332
333
334
335
|
# File 'lib/kamal_backup/config.rb', line 331
def validate_backup(check_files: true)
validate_restic(check_files: check_files)
validate_database_backup(check_files: check_files)
validate_backup_paths(check_files: check_files)
end
|
#validate_backup_paths(check_files: true) ⇒ Object
375
376
377
378
379
380
381
382
383
384
|
# File 'lib/kamal_backup/config.rb', line 375
def validate_backup_paths(check_files: true)
backup_paths.each do |path|
expanded = File.expand_path(path)
if SUSPICIOUS_BACKUP_PATHS.include?(expanded) && !allow_suspicious_backup_paths?
raise ConfigurationError,
"refusing suspicious backup path #{expanded}; set KAMAL_BACKUP_ALLOW_SUSPICIOUS_PATHS=true to override"
end
raise ConfigurationError, "backup path does not exist: #{path}" if check_files && !File.exist?(path)
end
end
|
#validate_database_backup(check_files: true) ⇒ Object
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
# File 'lib/kamal_backup/config.rb', line 342
def validate_database_backup(check_files: true)
raise ConfigurationError, 'databases must contain at least one database' if databases.empty?
databases.each do |database|
unless database.missing_secrets.empty?
raise ConfigurationError,
"database #{database.database_name} requires missing secret #{database.missing_secrets.join(', ')}"
end
case database.database_adapter
when 'postgres'
unless database.value('DATABASE_URL') || database.value('PGDATABASE')
raise ConfigurationError,
"PostgreSQL database #{database.database_name} requires url or PGDATABASE/libpq environment"
end
when 'mysql'
unless database.value('DATABASE_URL') || database.value('MYSQL_DATABASE') || database.value('MARIADB_DATABASE')
raise ConfigurationError,
"MySQL database #{database.database_name} requires url or MYSQL_DATABASE/MARIADB_DATABASE"
end
when 'sqlite'
path = database.required_value('SQLITE_DATABASE_PATH')
if check_files && !File.file?(path)
raise ConfigurationError,
"SQLite database #{database.database_name} does not exist: #{path}"
end
else
raise ConfigurationError,
"database #{database.database_name} adapter is required and must be postgres, mysql, or sqlite"
end
end
end
|
#validate_database_restore_target(target) ⇒ Object
409
410
411
412
413
414
415
416
|
# File 'lib/kamal_backup/config.rb', line 409
def validate_database_restore_target(target)
raise ConfigurationError, 'restore database target is required' if target.to_s.strip.empty?
return unless production_like_target?(target)
raise ConfigurationError,
"refusing production-looking restore target #{target}; choose a scratch target that does not look like production"
end
|
#validate_file_restore_target(target) ⇒ Object
395
396
397
398
399
400
401
402
403
404
405
406
407
|
# File 'lib/kamal_backup/config.rb', line 395
def validate_file_restore_target(target)
raise ConfigurationError, 'restore target cannot be empty' if target.to_s.strip.empty?
expanded_target = File.expand_path(target)
raise ConfigurationError, 'refusing to restore files to /' if expanded_target == '/'
if in_place_file_restore?(expanded_target) && !allow_in_place_file_restore?
raise ConfigurationError,
"refusing in-place file restore to #{expanded_target}; set KAMAL_BACKUP_ALLOW_IN_PLACE_FILE_RESTORE=true to override"
end
expanded_target
end
|
#validate_local_database_restore_target(target) ⇒ Object
386
387
388
389
390
391
392
393
|
# File 'lib/kamal_backup/config.rb', line 386
def validate_local_database_restore_target(target)
raise ConfigurationError, 'local restore database target is required' if target.to_s.strip.empty?
return unless production_named_target?(target)
raise ConfigurationError,
"refusing production-looking local restore target #{target}; use restore production for production restores"
end
|
#validate_local_machine_restore ⇒ Object
337
338
339
340
|
# File 'lib/kamal_backup/config.rb', line 337
def validate_local_machine_restore
validate_local_machine_environment
validate_local_machine_paths
end
|
#validate_restic(check_files: true) ⇒ Object
325
326
327
328
329
|
# File 'lib/kamal_backup/config.rb', line 325
def validate_restic(check_files: true)
required_app_name
validate_restic_repository(check_files: check_files)
validate_restic_password(check_files: check_files)
end
|
#value(key) ⇒ Object
424
425
426
427
428
429
430
|
# File 'lib/kamal_backup/config.rb', line 424
def value(key)
raw = env[key]
return nil if raw.nil?
stripped = raw.to_s.strip
stripped.empty? ? nil : stripped
end
|