kamal-backup 0.1.2 → 0.2.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 +64 -247
- data/lib/kamal_backup/app.rb +5 -0
- data/lib/kamal_backup/cli.rb +70 -22
- data/lib/kamal_backup/config.rb +62 -18
- data/lib/kamal_backup/kamal_bridge.rb +36 -4
- data/lib/kamal_backup/restic.rb +19 -5
- data/lib/kamal_backup/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c69dd196192b66424004ae3769d75602862227ad958e056288e1cb49b8242a8a
|
|
4
|
+
data.tar.gz: fa70cfffdf1d95212700244fcdfbb5e7b72b8f4eaf27f2898177c16c305fe9d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1764bd5d9a81b9d705838a27f857a679efebe7b2c3ff64be0f4faffc440678e1958124f0cb8ca81c24fa5e30380324d20d42447ae20e5b33f7d946eab7dcec4
|
|
7
|
+
data.tar.gz: f929aecfd15c9b2ed34560292217a97e4ceff31fd4fad1b9ce0e2589af862cd725a45a275c3755ddc744bb8f0e8703c187aa96537664431b93cc4a2841ae4627
|
data/README.md
CHANGED
|
@@ -1,82 +1,66 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="docs/assets/images/logo.svg" alt="kamal-backup" height="96">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<h1>kamal-backup</h1>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- file-backed Active Storage and other mounted app files
|
|
7
|
+
<strong>The easiest way to run scheduled backups for a Rails app deployed with Kamal.</strong>
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
[](https://rubygems.org/gems/kamal-backup)
|
|
10
|
+
[](https://github.com/crmne/kamal-backup/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/crmne/kamal-backup/pkgs/container/kamal-backup)
|
|
12
|
+
[](https://kamal-backup.dev)
|
|
13
|
+
[](LICENSE)
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
- `restore production`: restore back into live production
|
|
15
|
+
</div>
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
---
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
- `drill production`: restore into scratch targets on production infrastructure, run checks, and record evidence
|
|
19
|
+
Backups for Rails apps deployed with Kamal should not become a separate ops project.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
`kamal-backup` is one Kamal accessory that runs encrypted backups for your Rails database and file-backed Active Storage files on a schedule. It also gives you restore drills and redacted evidence for security reviews like CASA.
|
|
22
|
+
|
|
23
|
+
If you already deploy with Kamal, backups should feel like adding one more accessory.
|
|
21
24
|
|
|
22
25
|
## Why Rails teams use it
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
Most self-hosted Rails apps need the same things:
|
|
25
28
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
29
|
+
- scheduled backups for PostgreSQL, MySQL/MariaDB, or SQLite
|
|
30
|
+
- file-backed Active Storage backups from mounted volumes
|
|
31
|
+
- a fast way to restore production data locally
|
|
32
|
+
- restore drills that do not touch the live production database
|
|
33
|
+
- evidence that says more than "the backup job is green"
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
`kamal-backup` packages that workflow into a small Ruby gem, a production accessory image, and a restic repository.
|
|
32
36
|
|
|
33
|
-
## Quick
|
|
37
|
+
## Quick start
|
|
34
38
|
|
|
35
|
-
Add the gem
|
|
39
|
+
Add the gem:
|
|
36
40
|
|
|
37
|
-
```
|
|
41
|
+
```rb
|
|
38
42
|
group :development do
|
|
39
43
|
gem "kamal-backup"
|
|
40
44
|
end
|
|
41
45
|
```
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
Generate the config file and accessory snippet:
|
|
44
48
|
|
|
45
49
|
```sh
|
|
46
50
|
bundle install
|
|
47
51
|
bundle exec kamal-backup init
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- `config/kamal-backup.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.
|
|
61
|
-
|
|
62
|
-
Local restore and drill also require the `restic` binary on your machine. The backup accessory image already includes `restic` for production-side commands.
|
|
63
|
-
|
|
64
|
-
Then add the backup accessory to `config/deploy.yml`:
|
|
54
|
+
Add the accessory to `config/deploy.yml`:
|
|
65
55
|
|
|
66
56
|
```yaml
|
|
67
57
|
accessories:
|
|
68
58
|
backup:
|
|
69
59
|
image: ghcr.io/crmne/kamal-backup:latest
|
|
70
60
|
host: chatwithwork.com
|
|
61
|
+
files:
|
|
62
|
+
- config/kamal-backup.yml:/app/config/kamal-backup.yml:ro
|
|
71
63
|
env:
|
|
72
|
-
clear:
|
|
73
|
-
APP_NAME: chatwithwork
|
|
74
|
-
DATABASE_ADAPTER: postgres
|
|
75
|
-
DATABASE_URL: postgres://chatwithwork@chatwithwork-db:5432/chatwithwork_production
|
|
76
|
-
BACKUP_PATHS: /data/storage
|
|
77
|
-
RESTIC_REPOSITORY: s3:https://s3.example.com/chatwithwork-backups
|
|
78
|
-
RESTIC_INIT_IF_MISSING: "true"
|
|
79
|
-
BACKUP_SCHEDULE_SECONDS: "86400"
|
|
80
64
|
secret:
|
|
81
65
|
- PGPASSWORD
|
|
82
66
|
- RESTIC_PASSWORD
|
|
@@ -84,227 +68,60 @@ accessories:
|
|
|
84
68
|
- AWS_SECRET_ACCESS_KEY
|
|
85
69
|
volumes:
|
|
86
70
|
- "chatwithwork_storage:/data/storage:ro"
|
|
71
|
+
- "chatwithwork_backup_state:/var/lib/kamal-backup"
|
|
87
72
|
```
|
|
88
73
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
```sh
|
|
92
|
-
bin/kamal accessory boot backup
|
|
93
|
-
bin/kamal accessory logs backup
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Run the first backup from your app checkout with the local gem and Kamal-style destination selection:
|
|
74
|
+
Put the backup settings in `config/kamal-backup.yml`:
|
|
97
75
|
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
```yaml
|
|
77
|
+
accessory: backup
|
|
78
|
+
app_name: chatwithwork
|
|
79
|
+
database_adapter: postgres
|
|
80
|
+
database_url: postgres://chatwithwork@chatwithwork-db:5432/chatwithwork_production
|
|
81
|
+
backup_paths:
|
|
82
|
+
- /data/storage
|
|
83
|
+
restic_repository: s3:https://s3.example.com/chatwithwork-backups
|
|
84
|
+
restic_init_if_missing: true
|
|
85
|
+
backup_schedule_seconds: 86400
|
|
102
86
|
```
|
|
103
87
|
|
|
104
|
-
|
|
88
|
+
Boot it. The container runs `kamal-backup schedule` by default:
|
|
105
89
|
|
|
106
90
|
```sh
|
|
107
|
-
bundle exec kamal-backup
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Examples live in:
|
|
111
|
-
|
|
112
|
-
- [examples/kamal-accessory.yml](examples/kamal-accessory.yml)
|
|
113
|
-
- [examples/kamal-backup.yml.example](examples/kamal-backup.yml.example)
|
|
114
|
-
- [examples/kamal-backup.local.yml.example](examples/kamal-backup.local.yml.example)
|
|
115
|
-
|
|
116
|
-
## What Restic Does Here
|
|
117
|
-
|
|
118
|
-
`kamal-backup` uses restic as the backup engine and repository format.
|
|
119
|
-
|
|
120
|
-
In the normal Kamal setup, you do not install restic on the Rails app host. The backup accessory image already includes it. You only point the accessory at a restic repository, usually:
|
|
121
|
-
|
|
122
|
-
- S3-compatible object storage
|
|
123
|
-
- a restic REST server
|
|
124
|
-
- a filesystem path for local development
|
|
125
|
-
|
|
126
|
-
If you choose a `rest:` repository, `kamal-backup` does not install or operate that server for you. It is a separate service.
|
|
127
|
-
|
|
128
|
-
## Commands
|
|
129
|
-
|
|
130
|
-
The operator-facing command surface is:
|
|
131
|
-
|
|
132
|
-
```sh
|
|
133
|
-
kamal-backup init
|
|
134
|
-
kamal-backup backup
|
|
135
|
-
kamal-backup restore local [snapshot-or-latest]
|
|
136
|
-
kamal-backup restore production [snapshot-or-latest]
|
|
137
|
-
kamal-backup drill local [snapshot-or-latest]
|
|
138
|
-
kamal-backup drill production [snapshot-or-latest]
|
|
139
|
-
kamal-backup list
|
|
140
|
-
kamal-backup check
|
|
141
|
-
kamal-backup evidence
|
|
142
|
-
kamal-backup schedule
|
|
143
|
-
kamal-backup version
|
|
91
|
+
bundle exec kamal-backup validate
|
|
92
|
+
bin/kamal accessory boot backup
|
|
93
|
+
bin/kamal accessory logs backup
|
|
144
94
|
```
|
|
145
95
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Remote-backed commands require the local gem version and the backup accessory version to match. If they drift, `kamal-backup` fails fast and tells you to reboot the accessory so it pulls the current `latest` image. `version` is the diagnostic exception: from an app checkout with `config/deploy.yml`, it shows both versions and the sync status even without `-d`.
|
|
149
|
-
|
|
150
|
-
Common examples:
|
|
96
|
+
Run the first backup and print evidence:
|
|
151
97
|
|
|
152
98
|
```sh
|
|
153
99
|
bundle exec kamal-backup -d production backup
|
|
154
|
-
bundle exec kamal-backup -d production
|
|
100
|
+
bundle exec kamal-backup -d production list
|
|
155
101
|
bundle exec kamal-backup -d production evidence
|
|
156
|
-
bundle exec kamal-backup -d production restore production latest
|
|
157
|
-
bundle exec kamal-backup -d production drill production latest --database app_restore_20260423 --files /restore/files
|
|
158
|
-
bundle exec kamal-backup -d production version
|
|
159
|
-
bundle exec kamal-backup restore local latest
|
|
160
|
-
bundle exec kamal-backup drill local latest --check "bin/rails runner 'puts User.count'"
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Use `kamal-backup help`, `kamal-backup help restore`, or `kamal-backup help drill` for task-specific usage.
|
|
164
|
-
|
|
165
|
-
## How a Backup Run Works
|
|
166
|
-
|
|
167
|
-
When `kamal-backup backup` runs, it does five things:
|
|
168
|
-
|
|
169
|
-
1. Validates the app name, restic repository, database settings, and `BACKUP_PATHS`.
|
|
170
|
-
2. Creates a database backup with the database-native export tool.
|
|
171
|
-
3. Streams that database backup into restic with tags such as `type:database`, `adapter:<adapter>`, and `run:<timestamp>`.
|
|
172
|
-
4. Runs one `restic backup` for all configured `BACKUP_PATHS`, tagged as `type:files` with the same `run:<timestamp>`.
|
|
173
|
-
5. Optionally runs `restic forget --prune` and `restic check`.
|
|
174
|
-
|
|
175
|
-
That shared `run:<timestamp>` tag lets you match the database backup and file backup from the same run.
|
|
176
|
-
|
|
177
|
-
## Restore
|
|
178
|
-
|
|
179
|
-
`restore` means "put data back."
|
|
180
|
-
|
|
181
|
-
`restore local` runs on your machine. With `-d` or `-c`, it asks Kamal for the backup accessory config and uses that as the source of truth for:
|
|
182
|
-
|
|
183
|
-
- `APP_NAME`
|
|
184
|
-
- `DATABASE_ADAPTER`
|
|
185
|
-
- `RESTIC_REPOSITORY`
|
|
186
|
-
- `LOCAL_RESTORE_SOURCE_PATHS` from the accessory `BACKUP_PATHS`
|
|
187
|
-
|
|
188
|
-
For a normal Rails app, the local targets come from Rails conventions:
|
|
189
|
-
|
|
190
|
-
- the development database in `config/database.yml`
|
|
191
|
-
- `storage` as the local files target
|
|
192
|
-
- `tmp/kamal-backup` as the local drill state directory
|
|
193
|
-
|
|
194
|
-
You still provide the local secrets yourself in env:
|
|
195
|
-
|
|
196
|
-
- `RESTIC_PASSWORD`
|
|
197
|
-
- `POSTGRES_PASSWORD` or `MYSQL_PWD` when needed
|
|
198
|
-
- `RESTIC_REPOSITORY` when it is not visible through `kamal config`
|
|
199
|
-
|
|
200
|
-
And you need `restic` installed locally and available on `PATH`.
|
|
201
|
-
|
|
202
|
-
Example:
|
|
203
|
-
|
|
204
|
-
```sh
|
|
205
|
-
bundle exec kamal-backup -d production restore local latest
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
`restore production` is the emergency path back into the live production database and live production file paths:
|
|
209
|
-
|
|
210
|
-
```sh
|
|
211
|
-
bundle exec kamal-backup -d production restore production latest
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
It prompts locally, then shells out through Kamal to the backup accessory.
|
|
215
|
-
|
|
216
|
-
## Restore Drills
|
|
217
|
-
|
|
218
|
-
`drill` means "restore, check, and record the result."
|
|
219
|
-
|
|
220
|
-
`drill local` is often the fastest proof for a small app:
|
|
221
|
-
|
|
222
|
-
```sh
|
|
223
|
-
bundle exec kamal-backup -d production drill local latest --check "bin/rails runner 'puts User.count'"
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
Like `restore local`, this runs on your machine and requires a local `restic` install.
|
|
227
|
-
|
|
228
|
-
`drill production` restores into scratch targets on production infrastructure. It does not touch the live production database:
|
|
229
|
-
|
|
230
|
-
```sh
|
|
231
|
-
bundle exec kamal-backup -d production drill production latest \
|
|
232
|
-
--database app_restore_20260423 \
|
|
233
|
-
--files /restore/files \
|
|
234
|
-
--check "test -d /restore/files/data/storage"
|
|
235
102
|
```
|
|
236
103
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
## Evidence for CASA and Similar Reviews
|
|
240
|
-
|
|
241
|
-
`evidence` is the JSON summary you can attach to an ops record or security review.
|
|
104
|
+
## What you get
|
|
242
105
|
|
|
243
|
-
|
|
106
|
+
- **Scheduled backups:** the accessory runs continuously and backs up on `backup_schedule_seconds`.
|
|
107
|
+
- **Database and Active Storage coverage:** database dumps plus file-backed Active Storage files from mounted volumes.
|
|
108
|
+
- **Restic underneath:** encrypted, deduplicated snapshots in S3-compatible storage, a restic REST server, or a filesystem repository.
|
|
109
|
+
- **Local restores:** pull production backups into your local Rails app when you need to inspect real data.
|
|
110
|
+
- **Restore drills:** restore into scratch production-side targets and record the result.
|
|
111
|
+
- **Security review evidence:** `kamal-backup evidence` prints redacted JSON with latest snapshots, checks, drills, retention, and tool versions.
|
|
244
112
|
|
|
245
|
-
|
|
246
|
-
- latest `restic check` result
|
|
247
|
-
- latest restore drill result
|
|
248
|
-
- retention settings
|
|
249
|
-
- tool versions
|
|
250
|
-
|
|
251
|
-
For many reviews, the useful sequence is:
|
|
252
|
-
|
|
253
|
-
1. scheduled backups
|
|
254
|
-
2. repository checks
|
|
255
|
-
3. a real restore drill
|
|
256
|
-
4. `kamal-backup evidence`
|
|
257
|
-
|
|
258
|
-
That reads much better to a reviewer than "the backup job is green."
|
|
259
|
-
|
|
260
|
-
## Configuration Highlights
|
|
261
|
-
|
|
262
|
-
Core accessory environment:
|
|
263
|
-
|
|
264
|
-
```sh
|
|
265
|
-
APP_NAME=chatwithwork
|
|
266
|
-
DATABASE_ADAPTER=postgres
|
|
267
|
-
RESTIC_REPOSITORY=s3:https://s3.example.com/chatwithwork-backups
|
|
268
|
-
RESTIC_PASSWORD=change-me
|
|
269
|
-
BACKUP_PATHS=/data/storage
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
PostgreSQL:
|
|
273
|
-
|
|
274
|
-
```sh
|
|
275
|
-
DATABASE_ADAPTER=postgres
|
|
276
|
-
DATABASE_URL=postgres://app@app-db:5432/app_production
|
|
277
|
-
PGPASSWORD=change-me
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
MySQL/MariaDB:
|
|
281
|
-
|
|
282
|
-
```sh
|
|
283
|
-
DATABASE_ADAPTER=mysql
|
|
284
|
-
DATABASE_URL=mysql2://app@app-mysql:3306/app_production
|
|
285
|
-
MYSQL_PWD=change-me
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
SQLite:
|
|
289
|
-
|
|
290
|
-
```sh
|
|
291
|
-
DATABASE_ADAPTER=sqlite
|
|
292
|
-
SQLITE_DATABASE_PATH=/data/db/production.sqlite3
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Optional local config files:
|
|
113
|
+
## Docs
|
|
296
114
|
|
|
297
|
-
|
|
298
|
-
- `config/kamal-backup.local.yml`
|
|
115
|
+
Read the full documentation at **[kamal-backup.dev](https://kamal-backup.dev)**.
|
|
299
116
|
|
|
300
|
-
|
|
117
|
+
Start here:
|
|
301
118
|
|
|
302
|
-
|
|
119
|
+
- [Getting Started](https://kamal-backup.dev/getting-started/)
|
|
120
|
+
- [How Backups Work](https://kamal-backup.dev/how-backups-work/)
|
|
121
|
+
- [Configuration](https://kamal-backup.dev/configuration/)
|
|
122
|
+
- [Restore Drills](https://kamal-backup.dev/restore-drills/)
|
|
123
|
+
- [Commands](https://kamal-backup.dev/commands/)
|
|
303
124
|
|
|
304
|
-
|
|
125
|
+
## License
|
|
305
126
|
|
|
306
|
-
|
|
307
|
-
- [`docs/_guide/configuration.md`](docs/_guide/configuration.md)
|
|
308
|
-
- [`docs/_guide/restore.md`](docs/_guide/restore.md)
|
|
309
|
-
- [`docs/_guide/restore-drills.md`](docs/_guide/restore-drills.md)
|
|
310
|
-
- [`docs/_reference/commands.md`](docs/_reference/commands.md)
|
|
127
|
+
MIT
|
data/lib/kamal_backup/app.rb
CHANGED
data/lib/kamal_backup/cli.rb
CHANGED
|
@@ -33,8 +33,30 @@ module KamalBackup
|
|
|
33
33
|
|
|
34
34
|
def local_command_config
|
|
35
35
|
@local_command_config ||= begin
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
if deployment_mode?
|
|
37
|
+
Config.new(
|
|
38
|
+
env: command_env,
|
|
39
|
+
defaults: production_source_defaults,
|
|
40
|
+
config_paths: [Config::LOCAL_CONFIG_PATH]
|
|
41
|
+
)
|
|
42
|
+
else
|
|
43
|
+
Config.new(env: command_env)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def production_source_defaults
|
|
49
|
+
shared_config_source_defaults.merge(bridge.local_restore_defaults(accessory_name: accessory_name))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def shared_config_source_defaults
|
|
53
|
+
config = Config.new(env: {}, config_paths: [Config::SHARED_CONFIG_PATH], load_project_defaults: false)
|
|
54
|
+
|
|
55
|
+
{}.tap do |defaults|
|
|
56
|
+
defaults["APP_NAME"] = config.app_name if config.app_name
|
|
57
|
+
defaults["DATABASE_ADAPTER"] = config.database_adapter if config.database_adapter
|
|
58
|
+
defaults["RESTIC_REPOSITORY"] = config.restic_repository if config.restic_repository
|
|
59
|
+
defaults["LOCAL_RESTORE_SOURCE_PATHS"] = config.backup_paths.join("\n") if config.backup_paths.any?
|
|
38
60
|
end
|
|
39
61
|
end
|
|
40
62
|
|
|
@@ -58,6 +80,10 @@ module KamalBackup
|
|
|
58
80
|
deployment_mode? || default_deploy_config?
|
|
59
81
|
end
|
|
60
82
|
|
|
83
|
+
def validate_deploy_mode?
|
|
84
|
+
deployment_mode? || default_deploy_config?
|
|
85
|
+
end
|
|
86
|
+
|
|
61
87
|
def accessory_name
|
|
62
88
|
@accessory_name ||= bridge.accessory_name(preferred: local_preferences.accessory_name)
|
|
63
89
|
end
|
|
@@ -103,6 +129,15 @@ module KamalBackup
|
|
|
103
129
|
puts("fix: #{accessory_reboot_command}") if status == "out of sync"
|
|
104
130
|
end
|
|
105
131
|
|
|
132
|
+
def validate_deploy_config
|
|
133
|
+
config = Config.new(
|
|
134
|
+
env: bridge.accessory_environment(accessory_name: accessory_name),
|
|
135
|
+
config_paths: [Config::SHARED_CONFIG_PATH],
|
|
136
|
+
load_project_defaults: false
|
|
137
|
+
)
|
|
138
|
+
config.validate_backup(check_files: false)
|
|
139
|
+
end
|
|
140
|
+
|
|
106
141
|
def confirm!(message)
|
|
107
142
|
return if options[:yes]
|
|
108
143
|
|
|
@@ -149,9 +184,15 @@ module KamalBackup
|
|
|
149
184
|
|
|
150
185
|
def shared_config_template
|
|
151
186
|
<<~YAML
|
|
152
|
-
# Shared defaults for kamal-backup in this app.
|
|
153
|
-
# Set this when your accessory is not named "backup".
|
|
154
187
|
accessory: backup
|
|
188
|
+
app_name: your-app
|
|
189
|
+
database_adapter: postgres
|
|
190
|
+
database_url: postgres://your-app@your-db:5432/your_app_production
|
|
191
|
+
backup_paths:
|
|
192
|
+
- /data/storage
|
|
193
|
+
restic_repository: s3:https://s3.example.com/your-app-backups
|
|
194
|
+
restic_init_if_missing: true
|
|
195
|
+
backup_schedule_seconds: 86400
|
|
155
196
|
YAML
|
|
156
197
|
end
|
|
157
198
|
|
|
@@ -161,15 +202,9 @@ module KamalBackup
|
|
|
161
202
|
backup:
|
|
162
203
|
image: ghcr.io/crmne/kamal-backup:#{VERSION}
|
|
163
204
|
host: your-server.example.com
|
|
205
|
+
files:
|
|
206
|
+
- config/kamal-backup.yml:/app/config/kamal-backup.yml:ro
|
|
164
207
|
env:
|
|
165
|
-
clear:
|
|
166
|
-
APP_NAME: your-app
|
|
167
|
-
DATABASE_ADAPTER: postgres
|
|
168
|
-
DATABASE_URL: postgres://your-app@your-db:5432/your_app_production
|
|
169
|
-
BACKUP_PATHS: /data/storage
|
|
170
|
-
RESTIC_REPOSITORY: s3:https://s3.example.com/your-app-backups
|
|
171
|
-
RESTIC_INIT_IF_MISSING: "true"
|
|
172
|
-
BACKUP_SCHEDULE_SECONDS: "86400"
|
|
173
208
|
secret:
|
|
174
209
|
- PGPASSWORD
|
|
175
210
|
- RESTIC_PASSWORD
|
|
@@ -177,6 +212,7 @@ module KamalBackup
|
|
|
177
212
|
- AWS_SECRET_ACCESS_KEY
|
|
178
213
|
volumes:
|
|
179
214
|
- "your_app_storage:/data/storage:ro"
|
|
215
|
+
- "your_app_backup_state:/var/lib/kamal-backup"
|
|
180
216
|
YAML
|
|
181
217
|
end
|
|
182
218
|
end
|
|
@@ -195,15 +231,15 @@ module KamalBackup
|
|
|
195
231
|
CLI.basename
|
|
196
232
|
end
|
|
197
233
|
|
|
198
|
-
desc "local [SNAPSHOT]", "Restore the backup into the local database and
|
|
234
|
+
desc "local [SNAPSHOT]", "Restore the backup into the local database and Active Storage path"
|
|
199
235
|
def local(snapshot = "latest")
|
|
200
|
-
confirm!("Restore #{snapshot} into the local database and
|
|
236
|
+
confirm!("Restore #{snapshot} into the local database and Active Storage path? This will overwrite local data.")
|
|
201
237
|
puts(JSON.pretty_generate(local_app.restore_to_local_machine(snapshot)))
|
|
202
238
|
end
|
|
203
239
|
|
|
204
|
-
desc "production [SNAPSHOT]", "Restore the backup into the production database and
|
|
240
|
+
desc "production [SNAPSHOT]", "Restore the backup into the production database and Active Storage path"
|
|
205
241
|
def production(snapshot = "latest")
|
|
206
|
-
confirm!("Restore #{snapshot} into the production database and
|
|
242
|
+
confirm!("Restore #{snapshot} into the production database and Active Storage path? This will overwrite production data.")
|
|
207
243
|
|
|
208
244
|
if deployment_mode?
|
|
209
245
|
exec_remote(["kamal-backup", "restore", "production", snapshot, "--yes"])
|
|
@@ -229,7 +265,7 @@ module KamalBackup
|
|
|
229
265
|
|
|
230
266
|
method_option :database, type: :string, desc: "Scratch database name for PostgreSQL or MySQL"
|
|
231
267
|
method_option :"sqlite-path", type: :string, desc: "Scratch SQLite path for production-side drills"
|
|
232
|
-
method_option :files, type: :string, default: "/restore/files", desc: "Scratch
|
|
268
|
+
method_option :files, type: :string, default: "/restore/files", desc: "Scratch Active Storage target for the drill"
|
|
233
269
|
method_option :check, type: :string, desc: "Run a verification command after the restore"
|
|
234
270
|
desc "production [SNAPSHOT]", "Run a restore drill on production infrastructure using scratch targets"
|
|
235
271
|
def production(snapshot = "latest")
|
|
@@ -299,7 +335,7 @@ module KamalBackup
|
|
|
299
335
|
class_option :config_file, aliases: "-c", type: :string, desc: "Path to Kamal deploy config file"
|
|
300
336
|
class_option :destination, aliases: "-d", type: :string, desc: "Kamal destination to use"
|
|
301
337
|
remove_command :tree
|
|
302
|
-
desc "restore SUBCOMMAND ...ARGS", "Restore a
|
|
338
|
+
desc "restore SUBCOMMAND ...ARGS", "Restore a database and Active Storage backup locally or into production"
|
|
303
339
|
subcommand "restore", RestoreCLI
|
|
304
340
|
desc "drill SUBCOMMAND ...ARGS", "Run a restore drill on the local machine or on production infrastructure"
|
|
305
341
|
subcommand "drill", DrillCLI
|
|
@@ -323,7 +359,7 @@ module KamalBackup
|
|
|
323
359
|
|
|
324
360
|
include Helpers
|
|
325
361
|
|
|
326
|
-
desc "backup", "Run one backup immediately"
|
|
362
|
+
desc "backup", "Run one database and Active Storage backup immediately"
|
|
327
363
|
def backup
|
|
328
364
|
if deployment_mode?
|
|
329
365
|
exec_remote(["kamal-backup", "backup"])
|
|
@@ -350,7 +386,7 @@ module KamalBackup
|
|
|
350
386
|
end
|
|
351
387
|
end
|
|
352
388
|
|
|
353
|
-
desc "evidence", "Print redacted
|
|
389
|
+
desc "evidence", "Print redacted backup, check, and restore-drill evidence as JSON"
|
|
354
390
|
def evidence
|
|
355
391
|
if deployment_mode?
|
|
356
392
|
exec_remote(["kamal-backup", "evidence"])
|
|
@@ -359,7 +395,18 @@ module KamalBackup
|
|
|
359
395
|
end
|
|
360
396
|
end
|
|
361
397
|
|
|
362
|
-
desc "
|
|
398
|
+
desc "validate", "Validate backup configuration without running a backup"
|
|
399
|
+
def validate
|
|
400
|
+
if validate_deploy_mode?
|
|
401
|
+
validate_deploy_config
|
|
402
|
+
else
|
|
403
|
+
direct_app.validate
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
puts("ok")
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
desc "init", "Create config and print the scheduled backup accessory snippet"
|
|
363
410
|
def init
|
|
364
411
|
write_init_file(shared_config_path, shared_config_template)
|
|
365
412
|
|
|
@@ -368,7 +415,8 @@ module KamalBackup
|
|
|
368
415
|
puts
|
|
369
416
|
puts deploy_snippet
|
|
370
417
|
puts
|
|
371
|
-
puts "
|
|
418
|
+
puts "The accessory runs scheduled database and Active Storage backups with backup_schedule_seconds."
|
|
419
|
+
puts "For most Rails apps, restore local and drill local can infer the development database, Active Storage path, and tmp state directory."
|
|
372
420
|
puts "Local restore and drill also require the restic binary on your machine."
|
|
373
421
|
puts "Create config/kamal-backup.local.yml only if you need to override those local defaults."
|
|
374
422
|
end
|
data/lib/kamal_backup/config.rb
CHANGED
|
@@ -14,7 +14,9 @@ module KamalBackup
|
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
16
|
SUSPICIOUS_BACKUP_PATHS = %w[/ /var /etc /root /usr /bin /sbin /boot /dev /proc /sys /run].freeze
|
|
17
|
-
|
|
17
|
+
SHARED_CONFIG_PATH = "config/kamal-backup.yml"
|
|
18
|
+
LOCAL_CONFIG_PATH = "config/kamal-backup.local.yml"
|
|
19
|
+
DEFAULT_CONFIG_PATHS = [SHARED_CONFIG_PATH, LOCAL_CONFIG_PATH].freeze
|
|
18
20
|
YAML_KEY_ALIASES = {
|
|
19
21
|
"app_name" => "APP_NAME",
|
|
20
22
|
"database_adapter" => "DATABASE_ADAPTER",
|
|
@@ -24,7 +26,10 @@ module KamalBackup
|
|
|
24
26
|
"local_restore_source_paths" => "LOCAL_RESTORE_SOURCE_PATHS",
|
|
25
27
|
"accessory" => "KAMAL_BACKUP_ACCESSORY",
|
|
26
28
|
"restic_repository" => "RESTIC_REPOSITORY",
|
|
29
|
+
"restic_repository_file" => "RESTIC_REPOSITORY_FILE",
|
|
27
30
|
"restic_password" => "RESTIC_PASSWORD",
|
|
31
|
+
"restic_password_file" => "RESTIC_PASSWORD_FILE",
|
|
32
|
+
"restic_password_command" => "RESTIC_PASSWORD_COMMAND",
|
|
28
33
|
"restic_init_if_missing" => "RESTIC_INIT_IF_MISSING",
|
|
29
34
|
"restic_check_after_backup" => "RESTIC_CHECK_AFTER_BACKUP",
|
|
30
35
|
"restic_check_read_data_subset" => "RESTIC_CHECK_READ_DATA_SUBSET",
|
|
@@ -45,9 +50,10 @@ module KamalBackup
|
|
|
45
50
|
|
|
46
51
|
attr_reader :env
|
|
47
52
|
|
|
48
|
-
def initialize(env: ENV, cwd: Dir.pwd, defaults: {})
|
|
53
|
+
def initialize(env: ENV, cwd: Dir.pwd, defaults: {}, config_paths: nil, load_project_defaults: true)
|
|
49
54
|
raw_env = env.to_h
|
|
50
|
-
|
|
55
|
+
base = load_project_defaults ? project_defaults(cwd: cwd) : {}
|
|
56
|
+
@env = base.merge(defaults.to_h).merge(load_config_files(raw_env, cwd: cwd, paths: config_paths)).merge(raw_env)
|
|
51
57
|
end
|
|
52
58
|
|
|
53
59
|
def app_name
|
|
@@ -66,10 +72,22 @@ module KamalBackup
|
|
|
66
72
|
value("RESTIC_REPOSITORY")
|
|
67
73
|
end
|
|
68
74
|
|
|
75
|
+
def restic_repository_file
|
|
76
|
+
value("RESTIC_REPOSITORY_FILE")
|
|
77
|
+
end
|
|
78
|
+
|
|
69
79
|
def restic_password
|
|
70
80
|
value("RESTIC_PASSWORD")
|
|
71
81
|
end
|
|
72
82
|
|
|
83
|
+
def restic_password_file
|
|
84
|
+
value("RESTIC_PASSWORD_FILE")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def restic_password_command
|
|
88
|
+
value("RESTIC_PASSWORD_COMMAND")
|
|
89
|
+
end
|
|
90
|
+
|
|
73
91
|
def restic_init_if_missing?
|
|
74
92
|
truthy?("RESTIC_INIT_IF_MISSING")
|
|
75
93
|
end
|
|
@@ -176,16 +194,16 @@ module KamalBackup
|
|
|
176
194
|
end
|
|
177
195
|
end
|
|
178
196
|
|
|
179
|
-
def validate_restic
|
|
197
|
+
def validate_restic(check_files: true)
|
|
180
198
|
required_app_name
|
|
181
|
-
|
|
182
|
-
|
|
199
|
+
validate_restic_repository(check_files: check_files)
|
|
200
|
+
validate_restic_password(check_files: check_files)
|
|
183
201
|
end
|
|
184
202
|
|
|
185
|
-
def validate_backup
|
|
186
|
-
validate_restic
|
|
187
|
-
validate_database_backup
|
|
188
|
-
validate_backup_paths
|
|
203
|
+
def validate_backup(check_files: true)
|
|
204
|
+
validate_restic(check_files: check_files)
|
|
205
|
+
validate_database_backup(check_files: check_files)
|
|
206
|
+
validate_backup_paths(check_files: check_files)
|
|
189
207
|
end
|
|
190
208
|
|
|
191
209
|
def validate_local_machine_restore
|
|
@@ -193,7 +211,7 @@ module KamalBackup
|
|
|
193
211
|
validate_local_machine_paths
|
|
194
212
|
end
|
|
195
213
|
|
|
196
|
-
def validate_database_backup
|
|
214
|
+
def validate_database_backup(check_files: true)
|
|
197
215
|
case database_adapter
|
|
198
216
|
when "postgres"
|
|
199
217
|
unless value("DATABASE_URL") || value("PGDATABASE")
|
|
@@ -205,13 +223,13 @@ module KamalBackup
|
|
|
205
223
|
end
|
|
206
224
|
when "sqlite"
|
|
207
225
|
path = required_value("SQLITE_DATABASE_PATH")
|
|
208
|
-
raise ConfigurationError, "SQLITE_DATABASE_PATH does not exist: #{path}"
|
|
226
|
+
raise ConfigurationError, "SQLITE_DATABASE_PATH does not exist: #{path}" if check_files && !File.file?(path)
|
|
209
227
|
else
|
|
210
228
|
raise ConfigurationError, "DATABASE_ADAPTER is required or must be detectable from DATABASE_URL/SQLITE_DATABASE_PATH"
|
|
211
229
|
end
|
|
212
230
|
end
|
|
213
231
|
|
|
214
|
-
def validate_backup_paths
|
|
232
|
+
def validate_backup_paths(check_files: true)
|
|
215
233
|
paths = backup_paths
|
|
216
234
|
raise ConfigurationError, "BACKUP_PATHS must contain at least one path" if paths.empty?
|
|
217
235
|
|
|
@@ -220,7 +238,7 @@ module KamalBackup
|
|
|
220
238
|
if SUSPICIOUS_BACKUP_PATHS.include?(expanded) && !allow_suspicious_backup_paths?
|
|
221
239
|
raise ConfigurationError, "refusing suspicious backup path #{expanded}; set KAMAL_BACKUP_ALLOW_SUSPICIOUS_PATHS=true to override"
|
|
222
240
|
end
|
|
223
|
-
raise ConfigurationError, "backup path does not exist: #{path}"
|
|
241
|
+
raise ConfigurationError, "backup path does not exist: #{path}" if check_files && !File.exist?(path)
|
|
224
242
|
end
|
|
225
243
|
end
|
|
226
244
|
|
|
@@ -288,22 +306,48 @@ module KamalBackup
|
|
|
288
306
|
RailsApp.new(cwd: cwd).defaults
|
|
289
307
|
end
|
|
290
308
|
|
|
291
|
-
def load_config_files(raw_env, cwd:)
|
|
292
|
-
config_paths(raw_env, cwd: cwd).each_with_object({}) do |path, merged|
|
|
309
|
+
def load_config_files(raw_env, cwd:, paths:)
|
|
310
|
+
config_paths(raw_env, cwd: cwd, paths: paths).each_with_object({}) do |path, merged|
|
|
293
311
|
next unless File.file?(path)
|
|
294
312
|
|
|
295
313
|
merged.merge!(normalize_config_file(path))
|
|
296
314
|
end
|
|
297
315
|
end
|
|
298
316
|
|
|
299
|
-
def config_paths(raw_env, cwd:)
|
|
300
|
-
if
|
|
317
|
+
def config_paths(raw_env, cwd:, paths:)
|
|
318
|
+
if paths
|
|
319
|
+
Array(paths).map { |path| File.expand_path(path, cwd) }
|
|
320
|
+
elsif explicit = raw_env["KAMAL_BACKUP_CONFIG"]
|
|
301
321
|
[File.expand_path(explicit, cwd)]
|
|
302
322
|
else
|
|
303
323
|
DEFAULT_CONFIG_PATHS.map { |relative| File.expand_path(relative, cwd) }
|
|
304
324
|
end
|
|
305
325
|
end
|
|
306
326
|
|
|
327
|
+
def validate_restic_repository(check_files:)
|
|
328
|
+
return if restic_repository
|
|
329
|
+
|
|
330
|
+
if path = restic_repository_file
|
|
331
|
+
raise ConfigurationError, "RESTIC_REPOSITORY_FILE does not exist: #{path}" if check_files && !File.file?(path)
|
|
332
|
+
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
raise ConfigurationError, "RESTIC_REPOSITORY or RESTIC_REPOSITORY_FILE is required"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def validate_restic_password(check_files:)
|
|
340
|
+
return if restic_password || restic_password_command
|
|
341
|
+
|
|
342
|
+
if path = restic_password_file
|
|
343
|
+
raise ConfigurationError, "RESTIC_PASSWORD_FILE does not exist: #{path}" if check_files && !File.file?(path)
|
|
344
|
+
|
|
345
|
+
return
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
raise ConfigurationError, "RESTIC_PASSWORD, RESTIC_PASSWORD_FILE, or RESTIC_PASSWORD_COMMAND is required"
|
|
349
|
+
end
|
|
350
|
+
|
|
307
351
|
def normalize_config_file(path)
|
|
308
352
|
data = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
|
|
309
353
|
return {} if data.nil?
|
|
@@ -4,6 +4,7 @@ require_relative "command"
|
|
|
4
4
|
module KamalBackup
|
|
5
5
|
class KamalBridge
|
|
6
6
|
DEFAULT_CONFIG_FILE = "config/deploy.yml"
|
|
7
|
+
VERSION_LINE_PATTERN = /\A\d+(?:\.\d+)+(?:[-.][A-Za-z0-9]+)*\z/
|
|
7
8
|
|
|
8
9
|
def initialize(redactor:, config_file: nil, destination: nil)
|
|
9
10
|
@redactor = redactor
|
|
@@ -42,13 +43,17 @@ module KamalBackup
|
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
def accessory_environment(accessory_name:)
|
|
47
|
+
accessory_secret_placeholders(accessory_name).merge(accessory_clear_env(accessory_name))
|
|
48
|
+
end
|
|
49
|
+
|
|
45
50
|
def execute_on_accessory(accessory_name:, command:)
|
|
46
51
|
capture_kamal(kamal_exec_argv(accessory_name, command))
|
|
47
52
|
end
|
|
48
53
|
|
|
49
54
|
def remote_version(accessory_name:)
|
|
50
55
|
result = execute_on_accessory(accessory_name: accessory_name, command: "kamal-backup version")
|
|
51
|
-
version = result.stdout
|
|
56
|
+
version = parse_version_line(result.stdout)
|
|
52
57
|
|
|
53
58
|
if version.empty?
|
|
54
59
|
raise ConfigurationError, "could not determine remote kamal-backup version from accessory #{accessory_name}"
|
|
@@ -71,13 +76,23 @@ module KamalBackup
|
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
def accessory_clear_env(accessory_name)
|
|
74
|
-
|
|
79
|
+
normalize_env(fetch(accessory_env(accessory_name), :clear) || {})
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def accessory_secret_placeholders(accessory_name)
|
|
83
|
+
normalize_secret_env(fetch(accessory_env(accessory_name), :secret))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def accessory_env(accessory_name)
|
|
87
|
+
fetch(accessory(accessory_name), :env) || {}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def accessory(accessory_name)
|
|
91
|
+
accessories.fetch(accessory_name) do
|
|
75
92
|
accessories.fetch(accessory_name.to_sym) do
|
|
76
93
|
raise ConfigurationError, "accessory #{accessory_name.inspect} is not defined in #{config_file || DEFAULT_CONFIG_FILE}"
|
|
77
94
|
end
|
|
78
95
|
end
|
|
79
|
-
|
|
80
|
-
normalize_env(fetch(fetch(accessory, :env) || {}, :clear) || {})
|
|
81
96
|
end
|
|
82
97
|
|
|
83
98
|
def normalize_env(values)
|
|
@@ -86,6 +101,19 @@ module KamalBackup
|
|
|
86
101
|
end
|
|
87
102
|
end
|
|
88
103
|
|
|
104
|
+
def normalize_secret_env(values)
|
|
105
|
+
case values
|
|
106
|
+
when Hash
|
|
107
|
+
values.each_with_object({}) { |(key, _value), env| env[key.to_s] = "configured" }
|
|
108
|
+
when Array
|
|
109
|
+
values.each_with_object({}) { |key, env| env[key.to_s] = "configured" }
|
|
110
|
+
when String, Symbol
|
|
111
|
+
{ values.to_s => "configured" }
|
|
112
|
+
else
|
|
113
|
+
{}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
89
117
|
def fetch(hash, key)
|
|
90
118
|
hash[key] || hash[key.to_s] || hash[key.to_sym]
|
|
91
119
|
end
|
|
@@ -127,5 +155,9 @@ module KamalBackup
|
|
|
127
155
|
Command.capture(spec, redactor: @redactor)
|
|
128
156
|
end
|
|
129
157
|
end
|
|
158
|
+
|
|
159
|
+
def parse_version_line(output)
|
|
160
|
+
output.to_s.lines.map(&:strip).reverse.find { |line| line.match?(VERSION_LINE_PATTERN) }.to_s
|
|
161
|
+
end
|
|
130
162
|
end
|
|
131
163
|
end
|
data/lib/kamal_backup/restic.rb
CHANGED
|
@@ -6,6 +6,8 @@ require_relative "command"
|
|
|
6
6
|
|
|
7
7
|
module KamalBackup
|
|
8
8
|
class Restic
|
|
9
|
+
RESTIC_ENV_PATTERN = /\A(?:RESTIC_|AWS_|B2_|AZURE_|GOOGLE_|RCLONE_|OS_|ST_|HP_|HTTP_|HTTPS_|NO_PROXY)/i
|
|
10
|
+
|
|
9
11
|
attr_reader :config, :redactor
|
|
10
12
|
|
|
11
13
|
def initialize(config, redactor:)
|
|
@@ -25,13 +27,19 @@ module KamalBackup
|
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def backup_stream(command, filename:, tags:)
|
|
28
|
-
restic_command = CommandSpec.new(
|
|
30
|
+
restic_command = CommandSpec.new(
|
|
31
|
+
argv: ["restic", "backup", "--stdin", "--stdin-filename", filename] + tag_args(common_tags + tags),
|
|
32
|
+
env: restic_env
|
|
33
|
+
)
|
|
29
34
|
log("backing up stream as #{filename}")
|
|
30
35
|
pipe_commands(command, restic_command, producer_label: "dump", consumer_label: "restic backup")
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def backup_file(path, filename:, tags:)
|
|
34
|
-
command = CommandSpec.new(
|
|
39
|
+
command = CommandSpec.new(
|
|
40
|
+
argv: ["restic", "backup", "--stdin", "--stdin-filename", filename] + tag_args(common_tags + tags),
|
|
41
|
+
env: restic_env
|
|
42
|
+
)
|
|
35
43
|
log("backing up file content as #{filename}")
|
|
36
44
|
|
|
37
45
|
File.open(path, "rb") do |file|
|
|
@@ -126,12 +134,12 @@ module KamalBackup
|
|
|
126
134
|
end
|
|
127
135
|
|
|
128
136
|
def pipe_dump_to_command(snapshot, filename, command)
|
|
129
|
-
restic_command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename])
|
|
137
|
+
restic_command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename], env: restic_env)
|
|
130
138
|
pipe_commands(restic_command, command, producer_label: "restic dump", consumer_label: command.argv.first)
|
|
131
139
|
end
|
|
132
140
|
|
|
133
141
|
def write_dump_to_path(snapshot, filename, target_path)
|
|
134
|
-
command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename])
|
|
142
|
+
command = CommandSpec.new(argv: ["restic", "dump", snapshot, filename], env: restic_env)
|
|
135
143
|
target_path = File.expand_path(target_path)
|
|
136
144
|
FileUtils.mkdir_p(File.dirname(target_path))
|
|
137
145
|
temp_path = "#{target_path}.kamal-backup-#{$$}.tmp"
|
|
@@ -160,7 +168,7 @@ module KamalBackup
|
|
|
160
168
|
end
|
|
161
169
|
|
|
162
170
|
def run(args)
|
|
163
|
-
Command.capture(CommandSpec.new(argv: ["restic"] + args), redactor: redactor)
|
|
171
|
+
Command.capture(CommandSpec.new(argv: ["restic"] + args, env: restic_env), redactor: redactor)
|
|
164
172
|
end
|
|
165
173
|
|
|
166
174
|
def common_tags
|
|
@@ -172,6 +180,12 @@ module KamalBackup
|
|
|
172
180
|
tags.compact.each_with_object([]) { |tag, args| args.concat(["--tag", tag]) }
|
|
173
181
|
end
|
|
174
182
|
|
|
183
|
+
def restic_env
|
|
184
|
+
config.env.each_with_object({}) do |(key, value), env|
|
|
185
|
+
env[key] = value if key.to_s.match?(RESTIC_ENV_PATTERN)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
175
189
|
def pipe_commands(producer, consumer, producer_label:, consumer_label:)
|
|
176
190
|
Open3.popen3(producer.env, *producer.argv) do |producer_stdin, producer_stdout, producer_stderr, producer_wait|
|
|
177
191
|
producer_stdin.close
|
data/lib/kamal_backup/version.rb
CHANGED
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
|
|
4
|
+
version: 0.2.1
|
|
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-
|
|
11
|
+
date: 2026-04-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|
|
@@ -38,8 +38,8 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '0'
|
|
41
|
-
description: Back up PostgreSQL, MySQL, SQLite, and
|
|
42
|
-
then run
|
|
41
|
+
description: Back up PostgreSQL, MySQL, SQLite, and file-backed Active Storage files
|
|
42
|
+
into restic on a schedule, then run restore drills and produce review evidence.
|
|
43
43
|
email:
|
|
44
44
|
executables:
|
|
45
45
|
- kamal-backup
|
|
@@ -92,5 +92,6 @@ requirements: []
|
|
|
92
92
|
rubygems_version: 3.5.22
|
|
93
93
|
signing_key:
|
|
94
94
|
specification_version: 4
|
|
95
|
-
summary:
|
|
95
|
+
summary: Scheduled backups, restore drills, and evidence for Rails apps deployed with
|
|
96
|
+
Kamal
|
|
96
97
|
test_files: []
|