Class: KamalBackup::Config
- Inherits:
-
Object
- Object
- KamalBackup::Config
show all
- Defined in:
- lib/kamal_backup/config.rb
Defined Under Namespace
Classes: ConfigData, DatabaseSource
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.
136
137
138
139
140
141
142
143
144
|
# File 'lib/kamal_backup/config.rb', line 136
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.
134
135
136
|
# File 'lib/kamal_backup/config.rb', line 134
def env
@env
end
|
Instance Method Details
#accessory_name ⇒ Object
154
155
156
|
# File 'lib/kamal_backup/config.rb', line 154
def accessory_name
value("KAMAL_BACKUP_ACCESSORY")
end
|
#allow_in_place_file_restore? ⇒ Boolean
194
195
196
|
# File 'lib/kamal_backup/config.rb', line 194
def allow_in_place_file_restore?
truthy?("KAMAL_BACKUP_ALLOW_IN_PLACE_FILE_RESTORE")
end
|
#allow_suspicious_backup_paths? ⇒ Boolean
198
199
200
|
# File 'lib/kamal_backup/config.rb', line 198
def allow_suspicious_backup_paths?
truthy?("KAMAL_BACKUP_ALLOW_SUSPICIOUS_PATHS")
end
|
#app_name ⇒ Object
146
147
148
|
# File 'lib/kamal_backup/config.rb', line 146
def app_name
value("APP_NAME")
end
|
#backup_path_label(path) ⇒ Object
249
250
251
252
|
# File 'lib/kamal_backup/config.rb', line 249
def backup_path_label(path)
label = path.to_s.sub(%r{\A/+}, "").gsub(%r{[^A-Za-z0-9_.-]+}, "-")
label.empty? ? "root" : label
end
|
#backup_paths ⇒ Object
222
223
224
225
226
227
228
|
# File 'lib/kamal_backup/config.rb', line 222
def backup_paths
if path_definitions?
@path_definitions
else
legacy_backup_paths
end
end
|
#backup_schedule_seconds ⇒ Object
202
203
204
|
# File 'lib/kamal_backup/config.rb', line 202
def backup_schedule_seconds
integer("BACKUP_SCHEDULE_SECONDS", 86_400, minimum: 1)
end
|
#backup_start_delay_seconds ⇒ Object
206
207
208
|
# File 'lib/kamal_backup/config.rb', line 206
def backup_start_delay_seconds
integer("BACKUP_START_DELAY_SECONDS", 0, minimum: 0)
end
|
#check_after_backup? ⇒ Boolean
182
183
184
|
# File 'lib/kamal_backup/config.rb', line 182
def check_after_backup?
truthy?("RESTIC_CHECK_AFTER_BACKUP")
end
|
#check_read_data_subset ⇒ Object
190
191
192
|
# File 'lib/kamal_backup/config.rb', line 190
def check_read_data_subset
value("RESTIC_CHECK_READ_DATA_SUBSET")
end
|
#database_adapter ⇒ Object
254
255
256
257
258
259
260
|
# File 'lib/kamal_backup/config.rb', line 254
def database_adapter
if database_definitions?
databases.first&.database_adapter
else
legacy_database_adapter
end
end
|
#database_name ⇒ Object
262
263
264
|
# File 'lib/kamal_backup/config.rb', line 262
def database_name
"app"
end
|
#databases ⇒ Object
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
|
# File 'lib/kamal_backup/config.rb', line 266
def databases
@databases ||= begin
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
end
|
#falsey?(key) ⇒ Boolean
423
424
425
|
# File 'lib/kamal_backup/config.rb', line 423
def falsey?(key)
%w[0 false no n off].include?(value(key).to_s.downcase)
end
|
#forget_after_backup? ⇒ Boolean
186
187
188
|
# File 'lib/kamal_backup/config.rb', line 186
def forget_after_backup?
!falsey?("RESTIC_FORGET_AFTER_BACKUP")
end
|
#last_check_path ⇒ Object
214
215
216
|
# File 'lib/kamal_backup/config.rb', line 214
def last_check_path
File.join(state_dir, "last_check.json")
end
|
#last_restore_drill_path ⇒ Object
218
219
220
|
# File 'lib/kamal_backup/config.rb', line 218
def last_restore_drill_path
File.join(state_dir, "last_restore_drill.json")
end
|
#local_restore_path_pairs ⇒ Object
238
239
240
241
242
243
244
245
246
247
|
# File 'lib/kamal_backup/config.rb', line 238
def local_restore_path_pairs
source_paths = local_restore_source_paths
target_paths = backup_paths
if source_paths.size == target_paths.size
source_paths.zip(target_paths)
else
raise ConfigurationError, "local restore source paths must contain the same number of paths as file paths"
end
end
|
#local_restore_source_paths ⇒ Object
230
231
232
233
234
235
236
|
# File 'lib/kamal_backup/config.rb', line 230
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
397
398
399
400
401
402
403
404
405
|
# File 'lib/kamal_backup/config.rb', line 397
def production_like_target?(target)
target = target.to_s
if source_database_targets.include?(target)
true
else
production_named_target?(target.downcase)
end
end
|
#required_app_name ⇒ Object
150
151
152
|
# File 'lib/kamal_backup/config.rb', line 150
def required_app_name
required_value("APP_NAME")
end
|
#required_value(key) ⇒ Object
415
416
417
|
# File 'lib/kamal_backup/config.rb', line 415
def required_value(key)
value(key) || raise(ConfigurationError, "#{key} is required")
end
|
#restic_init_if_missing? ⇒ Boolean
178
179
180
|
# File 'lib/kamal_backup/config.rb', line 178
def restic_init_if_missing?
truthy?("RESTIC_INIT_IF_MISSING")
end
|
#restic_password ⇒ Object
166
167
168
|
# File 'lib/kamal_backup/config.rb', line 166
def restic_password
value("RESTIC_PASSWORD")
end
|
#restic_password_command ⇒ Object
174
175
176
|
# File 'lib/kamal_backup/config.rb', line 174
def restic_password_command
value("RESTIC_PASSWORD_COMMAND")
end
|
#restic_password_file ⇒ Object
170
171
172
|
# File 'lib/kamal_backup/config.rb', line 170
def restic_password_file
value("RESTIC_PASSWORD_FILE")
end
|
#restic_repository ⇒ Object
158
159
160
|
# File 'lib/kamal_backup/config.rb', line 158
def restic_repository
value("RESTIC_REPOSITORY")
end
|
#restic_repository_file ⇒ Object
162
163
164
|
# File 'lib/kamal_backup/config.rb', line 162
def restic_repository_file
value("RESTIC_REPOSITORY_FILE")
end
|
#retention ⇒ Object
295
296
297
298
299
|
# File 'lib/kamal_backup/config.rb', line 295
def retention
DEFAULT_RETENTION.each_with_object({}) do |(key, default), result|
result[key] = value(key) || default
end
end
|
#retention_args ⇒ Object
301
302
303
304
305
306
307
308
309
310
311
312
313
|
# File 'lib/kamal_backup/config.rb', line 301
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
210
211
212
|
# File 'lib/kamal_backup/config.rb', line 210
def state_dir
value("KAMAL_BACKUP_STATE_DIR") || "/var/lib/kamal-backup"
end
|
#truthy?(key) ⇒ Boolean
419
420
421
|
# File 'lib/kamal_backup/config.rb', line 419
def truthy?(key)
%w[1 true yes y on].include?(value(key).to_s.downcase)
end
|
#validate_backup(check_files: true) ⇒ Object
321
322
323
324
325
|
# File 'lib/kamal_backup/config.rb', line 321
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
358
359
360
361
362
363
364
365
366
|
# File 'lib/kamal_backup/config.rb', line 358
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
|
# File 'lib/kamal_backup/config.rb', line 332
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")
raise ConfigurationError, "SQLite database #{database.database_name} does not exist: #{path}" if check_files && !File.file?(path)
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
389
390
391
392
393
394
395
|
# File 'lib/kamal_backup/config.rb', line 389
def validate_database_restore_target(target)
raise ConfigurationError, "restore database target is required" if target.to_s.strip.empty?
if production_like_target?(target)
raise ConfigurationError, "refusing production-looking restore target #{target}; choose a scratch target that does not look like production"
end
end
|
#validate_file_restore_target(target) ⇒ Object
376
377
378
379
380
381
382
383
384
385
386
387
|
# File 'lib/kamal_backup/config.rb', line 376
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
368
369
370
371
372
373
374
|
# File 'lib/kamal_backup/config.rb', line 368
def validate_local_database_restore_target(target)
raise ConfigurationError, "local restore database target is required" if target.to_s.strip.empty?
if production_named_target?(target)
raise ConfigurationError, "refusing production-looking local restore target #{target}; use restore production for production restores"
end
end
|
#validate_local_machine_restore ⇒ Object
327
328
329
330
|
# File 'lib/kamal_backup/config.rb', line 327
def validate_local_machine_restore
validate_local_machine_environment
validate_local_machine_paths
end
|
#validate_restic(check_files: true) ⇒ Object
315
316
317
318
319
|
# File 'lib/kamal_backup/config.rb', line 315
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
407
408
409
410
411
412
413
|
# File 'lib/kamal_backup/config.rb', line 407
def value(key)
raw = env[key]
return nil if raw.nil?
stripped = raw.to_s.strip
stripped.empty? ? nil : stripped
end
|