kamal-backup 0.1.0.pre.8 → 0.1.0.pre.10

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: 8ed4e21bfee7ac0e789c3a0706626062055c92701128b5999a3828f5f3190057
4
- data.tar.gz: 5983618fb0fb16db51cfa82f4ff28398c00568d7a5082fd494a7063c944b111d
3
+ metadata.gz: 5b0a9aca508a2247ae1f44ae5cb3eab3366dcb2f47c80681c67890e0ad60a2fd
4
+ data.tar.gz: 16208b98365aeccabd346cd5ba150797ecff0c98e86a8592b71c8c882af67c42
5
5
  SHA512:
6
- metadata.gz: 227a34dc536cd9d5ca13cb2853fcac13c03c5defbe87e406374c1ebc7b94e7cc094693903aab1e7b6addb2b2a11913bae419767cb0e242f56d86818171bacb79
7
- data.tar.gz: 16788fa7108df55c2afa70f900511ce6574c931be8459774649ebcb533c415873e4424d8172e0df987804f6509f4aea7fb398676cd3741ff0c59fddf6d7e9a74
6
+ metadata.gz: dd46bd886c0f59413ad0762aa4a252d9e3a6ed721bc54c53e2a81ba88b80a9da40272b52c19792817ffabd2a92903db00eab0aa2bdd0d63917745955e5c7ffc2
7
+ data.tar.gz: '057386894dadb84c3afbd73b6949ab749873df06c8470f32e32a02036b8facf195067cd8652d13dd920110aa95e14462c275586a1de7c92e8b32823dedb1cf77'
data/README.md CHANGED
@@ -40,7 +40,7 @@ group :development do
40
40
  end
41
41
  ```
42
42
 
43
- Install it and generate the local config stubs:
43
+ Install it and generate the shared config stub:
44
44
 
45
45
  ```sh
46
46
  bundle install
@@ -50,7 +50,14 @@ bundle exec kamal-backup init
50
50
  That creates:
51
51
 
52
52
  - `config/kamal-backup.yml`
53
- - `config/kamal-backup.local.yml`
53
+
54
+ For most Rails apps, that is enough. `restore local` and `drill local` can infer:
55
+
56
+ - the development database target from `config/database.yml`
57
+ - the local files target from `storage`
58
+ - the local drill state directory from `tmp/kamal-backup`
59
+
60
+ Only create `config/kamal-backup.local.yml` if you need to override those local defaults.
54
61
 
55
62
  Then add the backup accessory to `config/deploy.yml`:
56
63
 
@@ -174,11 +181,17 @@ That shared `run:<timestamp>` tag lets you match the database backup and file ba
174
181
  - `RESTIC_REPOSITORY`
175
182
  - `LOCAL_RESTORE_SOURCE_PATHS` from the accessory `BACKUP_PATHS`
176
183
 
177
- You still provide the local targets yourself in `config/kamal-backup.local.yml` or env:
184
+ For a normal Rails app, the local targets come from Rails conventions:
185
+
186
+ - the development database in `config/database.yml`
187
+ - `storage` as the local files target
188
+ - `tmp/kamal-backup` as the local drill state directory
189
+
190
+ You still provide the local secrets yourself in env:
178
191
 
179
- - `DATABASE_URL` or `SQLITE_DATABASE_PATH`
180
- - `BACKUP_PATHS`
181
- - local secrets such as `RESTIC_PASSWORD` and DB passwords
192
+ - `RESTIC_PASSWORD`
193
+ - `POSTGRES_PASSWORD` or `MYSQL_PWD` when needed
194
+ - `RESTIC_REPOSITORY` when it is not visible through `kamal config`
182
195
 
183
196
  Example:
184
197
 
@@ -271,12 +284,12 @@ DATABASE_ADAPTER=sqlite
271
284
  SQLITE_DATABASE_PATH=/data/db/production.sqlite3
272
285
  ```
273
286
 
274
- Local config files:
287
+ Optional local config files:
275
288
 
276
289
  - `config/kamal-backup.yml`
277
290
  - `config/kamal-backup.local.yml`
278
291
 
279
- Keep secrets such as `RESTIC_PASSWORD`, cloud credentials, and local DB passwords in environment variables, not in YAML files.
292
+ `config/kamal-backup.local.yml` is only for nonstandard local targets. Keep secrets such as `RESTIC_PASSWORD`, cloud credentials, and local DB passwords in environment variables, not in YAML files.
280
293
 
281
294
  ## Docs
282
295
 
data/exe/kamal-backup CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
3
+ root = File.expand_path("..", __dir__)
4
+ gemfile = File.join(root, "Gemfile")
5
+ libdir = File.join(root, "lib")
4
6
 
5
- require "bundler/setup"
7
+ $LOAD_PATH.unshift(libdir) if File.directory?(libdir) && !$LOAD_PATH.include?(libdir)
6
8
 
7
- $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
9
+ if File.exist?(gemfile)
10
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
11
+ require "bundler/setup"
12
+ end
8
13
 
9
14
  require "kamal_backup"
10
15
 
@@ -97,10 +97,6 @@ module KamalBackup
97
97
  File.join(init_config_root, "kamal-backup.yml")
98
98
  end
99
99
 
100
- def local_config_path
101
- File.join(init_config_root, "kamal-backup.local.yml")
102
- end
103
-
104
100
  def write_init_file(path, contents)
105
101
  if File.exist?(path)
106
102
  say "Exists: #{path}", :yellow
@@ -119,19 +115,6 @@ module KamalBackup
119
115
  YAML
120
116
  end
121
117
 
122
- def local_config_template
123
- <<~YAML
124
- # Local machine targets for restore local and drill local.
125
- # With -d/-c, APP_NAME, DATABASE_ADAPTER, RESTIC_REPOSITORY, and
126
- # LOCAL_RESTORE_SOURCE_PATHS can come from your Kamal accessory config.
127
- # Keep secrets such as RESTIC_PASSWORD and cloud credentials in env.
128
- database_url: postgres://localhost/your_app_development
129
- backup_paths:
130
- - storage
131
- state_dir: tmp/kamal-backup
132
- YAML
133
- end
134
-
135
118
  def deploy_snippet
136
119
  <<~YAML
137
120
  accessories:
@@ -339,12 +322,14 @@ module KamalBackup
339
322
  desc "init", "Create kamal-backup config stubs for local restore and drill commands"
340
323
  def init
341
324
  write_init_file(shared_config_path, shared_config_template)
342
- write_init_file(local_config_path, local_config_template)
343
325
 
344
326
  puts
345
327
  puts "Add this accessory block to your Kamal deploy config:"
346
328
  puts
347
329
  puts deploy_snippet
330
+ puts
331
+ puts "For most Rails apps, restore local and drill local can infer the development database, storage path, and tmp state directory."
332
+ puts "Create config/kamal-backup.local.yml only if you need to override those local defaults."
348
333
  end
349
334
 
350
335
  desc "schedule", "Run the foreground scheduler loop"
@@ -1,6 +1,7 @@
1
1
  require "uri"
2
2
  require "yaml"
3
3
  require_relative "errors"
4
+ require_relative "rails_app"
4
5
 
5
6
  module KamalBackup
6
7
  class Config
@@ -46,7 +47,7 @@ module KamalBackup
46
47
 
47
48
  def initialize(env: ENV, cwd: Dir.pwd, defaults: {})
48
49
  raw_env = env.to_h
49
- @env = defaults.to_h.merge(load_config_files(raw_env, cwd: cwd)).merge(raw_env)
50
+ @env = project_defaults(cwd: cwd).merge(defaults.to_h).merge(load_config_files(raw_env, cwd: cwd)).merge(raw_env)
50
51
  end
51
52
 
52
53
  def app_name
@@ -283,6 +284,10 @@ module KamalBackup
283
284
  end
284
285
 
285
286
  private
287
+ def project_defaults(cwd:)
288
+ RailsApp.new(cwd: cwd).defaults
289
+ end
290
+
286
291
  def load_config_files(raw_env, cwd:)
287
292
  config_paths(raw_env, cwd: cwd).each_with_object({}) do |path, merged|
288
293
  next unless File.file?(path)
@@ -0,0 +1,152 @@
1
+ require "erb"
2
+ require "uri"
3
+ require "yaml"
4
+
5
+ module KamalBackup
6
+ class RailsApp
7
+ DEVELOPMENT_ENV = "development"
8
+
9
+ def initialize(cwd:)
10
+ @cwd = File.expand_path(cwd)
11
+ end
12
+
13
+ def defaults
14
+ return {} unless rails_app?
15
+
16
+ {}.tap do |defaults|
17
+ defaults.merge!(deploy_defaults)
18
+ defaults.merge!(database_defaults)
19
+
20
+ if local_storage_path
21
+ defaults["BACKUP_PATHS"] = local_storage_path
22
+ end
23
+
24
+ defaults["KAMAL_BACKUP_STATE_DIR"] = File.join(@cwd, "tmp", "kamal-backup")
25
+ end
26
+ end
27
+
28
+ private
29
+ def rails_app?
30
+ File.file?(database_config_path)
31
+ end
32
+
33
+ def deploy_defaults
34
+ service = fetch(parsed_yaml(deploy_config_path), :service)
35
+
36
+ if service
37
+ { "APP_NAME" => service.to_s }
38
+ else
39
+ {}
40
+ end
41
+ end
42
+
43
+ def database_defaults
44
+ config = local_database_config
45
+ return {} unless config
46
+
47
+ if url = fetch(config, :url)
48
+ adapter = adapter_from_url(url)
49
+
50
+ {
51
+ "DATABASE_ADAPTER" => adapter,
52
+ "DATABASE_URL" => url.to_s
53
+ }.compact
54
+ else
55
+ case normalize_adapter(fetch(config, :adapter))
56
+ when "postgres"
57
+ {
58
+ "DATABASE_ADAPTER" => "postgres",
59
+ "PGHOST" => fetch(config, :host),
60
+ "PGPORT" => fetch(config, :port)&.to_s,
61
+ "PGUSER" => fetch(config, :username),
62
+ "PGDATABASE" => fetch(config, :database)
63
+ }.compact
64
+ when "mysql"
65
+ {
66
+ "DATABASE_ADAPTER" => "mysql",
67
+ "MYSQL_HOST" => fetch(config, :host),
68
+ "MYSQL_PORT" => fetch(config, :port)&.to_s,
69
+ "MYSQL_USER" => fetch(config, :username),
70
+ "MYSQL_DATABASE" => fetch(config, :database)
71
+ }.compact
72
+ when "sqlite"
73
+ database = fetch(config, :database)
74
+ if database
75
+ {
76
+ "DATABASE_ADAPTER" => "sqlite",
77
+ "SQLITE_DATABASE_PATH" => File.expand_path(database.to_s, @cwd)
78
+ }
79
+ else
80
+ {}
81
+ end
82
+ else
83
+ {}
84
+ end
85
+ end
86
+ end
87
+
88
+ def local_storage_path
89
+ File.join(@cwd, "storage")
90
+ end
91
+
92
+ def local_database_config
93
+ environment = fetch(parsed_yaml(database_config_path), DEVELOPMENT_ENV)
94
+ return nil unless environment.is_a?(Hash)
95
+
96
+ if database_entry?(environment)
97
+ environment
98
+ else
99
+ primary = fetch(environment, :primary)
100
+ primary if primary.is_a?(Hash)
101
+ end
102
+ end
103
+
104
+ def database_entry?(config)
105
+ fetch(config, :adapter) || fetch(config, :database) || fetch(config, :url)
106
+ end
107
+
108
+ def adapter_from_url(url)
109
+ normalize_adapter(URI.parse(url.to_s).scheme)
110
+ rescue URI::InvalidURIError
111
+ nil
112
+ end
113
+
114
+ def parsed_yaml(path)
115
+ return {} unless File.file?(path)
116
+
117
+ rendered = ERB.new(File.read(path), trim_mode: "-").result
118
+ data = YAML.safe_load(rendered, permitted_classes: [], aliases: true)
119
+
120
+ if data.is_a?(Hash)
121
+ data
122
+ else
123
+ {}
124
+ end
125
+ rescue Psych::SyntaxError => e
126
+ raise ConfigurationError, "invalid YAML in #{path}: #{e.message}"
127
+ end
128
+
129
+ def fetch(hash, key)
130
+ hash[key] || hash[key.to_s] || hash[key.to_sym]
131
+ end
132
+
133
+ def normalize_adapter(value)
134
+ case value.to_s.downcase
135
+ when "postgres", "postgresql"
136
+ "postgres"
137
+ when "mysql", "mysql2", "mariadb"
138
+ "mysql"
139
+ when "sqlite", "sqlite3"
140
+ "sqlite"
141
+ end
142
+ end
143
+
144
+ def database_config_path
145
+ File.join(@cwd, "config", "database.yml")
146
+ end
147
+
148
+ def deploy_config_path
149
+ File.join(@cwd, "config", "deploy.yml")
150
+ end
151
+ end
152
+ end
@@ -1,3 +1,3 @@
1
1
  module KamalBackup
2
- VERSION = "0.1.0.pre.8"
2
+ VERSION = "0.1.0.pre.10"
3
3
  end
data/lib/kamal_backup.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "kamal_backup/errors"
4
4
  require_relative "kamal_backup/command"
5
5
  require_relative "kamal_backup/redactor"
6
6
  require_relative "kamal_backup/config"
7
+ require_relative "kamal_backup/rails_app"
7
8
  require_relative "kamal_backup/kamal_bridge"
8
9
  require_relative "kamal_backup/restic"
9
10
  require_relative "kamal_backup/evidence"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kamal-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.8
4
+ version: 0.1.0.pre.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - crmne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-23 00:00:00.000000000 Z
11
+ date: 2026-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -61,6 +61,7 @@ files:
61
61
  - lib/kamal_backup/errors.rb
62
62
  - lib/kamal_backup/evidence.rb
63
63
  - lib/kamal_backup/kamal_bridge.rb
64
+ - lib/kamal_backup/rails_app.rb
64
65
  - lib/kamal_backup/redactor.rb
65
66
  - lib/kamal_backup/restic.rb
66
67
  - lib/kamal_backup/scheduler.rb