kamal-backup 0.3.0.beta21 → 0.3.0
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/exe/kamal-backup +7 -6
- data/lib/kamal_backup/app.rb +350 -356
- data/lib/kamal_backup/cli.rb +107 -111
- data/lib/kamal_backup/command.rb +165 -161
- data/lib/kamal_backup/config.rb +533 -511
- data/lib/kamal_backup/databases/base.rb +17 -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 +62 -61
- data/lib/kamal_backup/kamal_bridge.rb +254 -250
- data/lib/kamal_backup/rails_app.rb +102 -101
- data/lib/kamal_backup/redactor.rb +18 -13
- data/lib/kamal_backup/restic.rb +195 -167
- 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.rb +19 -17
- metadata +30 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'yaml'
|
|
4
6
|
|
|
5
7
|
module KamalBackup
|
|
6
8
|
class RailsApp
|
|
7
|
-
DEVELOPMENT_ENV =
|
|
9
|
+
DEVELOPMENT_ENV = 'development'
|
|
8
10
|
|
|
9
11
|
def initialize(cwd:)
|
|
10
12
|
@cwd = File.expand_path(cwd)
|
|
@@ -17,136 +19,135 @@ module KamalBackup
|
|
|
17
19
|
defaults.merge!(deploy_defaults)
|
|
18
20
|
defaults.merge!(database_defaults)
|
|
19
21
|
|
|
20
|
-
if local_storage_path
|
|
21
|
-
defaults["BACKUP_PATHS"] = local_storage_path
|
|
22
|
-
end
|
|
22
|
+
defaults['BACKUP_PATHS'] = local_storage_path if local_storage_path
|
|
23
23
|
|
|
24
|
-
defaults[
|
|
24
|
+
defaults['KAMAL_BACKUP_STATE_DIR'] = File.join(@cwd, 'tmp', 'kamal-backup')
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
private
|
|
29
|
-
def rails_app?
|
|
30
|
-
File.file?(database_config_path)
|
|
31
|
-
end
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
def rails_app?
|
|
31
|
+
File.file?(database_config_path)
|
|
32
|
+
end
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
def deploy_defaults
|
|
35
|
+
service = fetch(parsed_yaml(deploy_config_path), :service)
|
|
36
|
+
|
|
37
|
+
if service
|
|
38
|
+
{ 'APP_NAME' => service.to_s }
|
|
39
|
+
else
|
|
40
|
+
{}
|
|
41
41
|
end
|
|
42
|
+
end
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
def database_defaults
|
|
45
|
+
config = local_database_config
|
|
46
|
+
return {} unless config
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if (url = fetch(config, :url))
|
|
49
|
+
adapter = adapter_from_url(url)
|
|
49
50
|
|
|
51
|
+
{
|
|
52
|
+
'DATABASE_ADAPTER' => adapter,
|
|
53
|
+
'DATABASE_URL' => url.to_s
|
|
54
|
+
}.compact
|
|
55
|
+
else
|
|
56
|
+
case normalize_adapter(fetch(config, :adapter))
|
|
57
|
+
when 'postgres'
|
|
50
58
|
{
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
'DATABASE_ADAPTER' => 'postgres',
|
|
60
|
+
'PGHOST' => fetch(config, :host),
|
|
61
|
+
'PGPORT' => fetch(config, :port)&.to_s,
|
|
62
|
+
'PGUSER' => fetch(config, :username),
|
|
63
|
+
'PGDATABASE' => fetch(config, :database)
|
|
53
64
|
}.compact
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
when 'mysql'
|
|
66
|
+
{
|
|
67
|
+
'DATABASE_ADAPTER' => 'mysql',
|
|
68
|
+
'MYSQL_HOST' => fetch(config, :host),
|
|
69
|
+
'MYSQL_PORT' => fetch(config, :port)&.to_s,
|
|
70
|
+
'MYSQL_USER' => fetch(config, :username),
|
|
71
|
+
'MYSQL_DATABASE' => fetch(config, :database)
|
|
72
|
+
}.compact
|
|
73
|
+
when 'sqlite'
|
|
74
|
+
database = fetch(config, :database)
|
|
75
|
+
if database
|
|
65
76
|
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
77
|
+
'DATABASE_ADAPTER' => 'sqlite',
|
|
78
|
+
'SQLITE_DATABASE_PATH' => File.expand_path(database.to_s, @cwd)
|
|
79
|
+
}
|
|
82
80
|
else
|
|
83
81
|
{}
|
|
84
82
|
end
|
|
83
|
+
else
|
|
84
|
+
{}
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
|
+
end
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
def local_storage_path
|
|
90
|
+
File.join(@cwd, 'storage')
|
|
91
|
+
end
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
def local_database_config
|
|
94
|
+
environment = fetch(parsed_yaml(database_config_path), DEVELOPMENT_ENV)
|
|
95
|
+
return nil unless environment.is_a?(Hash)
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
end
|
|
97
|
+
if database_entry?(environment)
|
|
98
|
+
environment
|
|
99
|
+
else
|
|
100
|
+
primary = fetch(environment, :primary)
|
|
101
|
+
primary if primary.is_a?(Hash)
|
|
102
102
|
end
|
|
103
|
+
end
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
def database_entry?(config)
|
|
106
|
+
fetch(config, :adapter) || fetch(config, :database) || fetch(config, :url)
|
|
107
|
+
end
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
def adapter_from_url(url)
|
|
110
|
+
normalize_adapter(URI.parse(url.to_s).scheme)
|
|
111
|
+
rescue URI::InvalidURIError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
def parsed_yaml(path)
|
|
116
|
+
return {} unless File.file?(path)
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
rendered = ERB.new(File.read(path), trim_mode: '-').result
|
|
119
|
+
data = YAML.safe_load(rendered, permitted_classes: [], aliases: true)
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
end
|
|
125
|
-
rescue Psych::SyntaxError => e
|
|
126
|
-
raise ConfigurationError, "invalid YAML in #{path}: #{e.message}"
|
|
121
|
+
if data.is_a?(Hash)
|
|
122
|
+
data
|
|
123
|
+
else
|
|
124
|
+
{}
|
|
127
125
|
end
|
|
126
|
+
rescue Psych::SyntaxError => e
|
|
127
|
+
raise ConfigurationError, "invalid YAML in #{path}: #{e.message}"
|
|
128
|
+
end
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
def fetch(hash, key)
|
|
131
|
+
hash[key] || hash[key.to_s] || hash[key.to_sym]
|
|
132
|
+
end
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
end
|
|
134
|
+
def normalize_adapter(value)
|
|
135
|
+
case value.to_s.downcase
|
|
136
|
+
when 'postgres', 'postgresql'
|
|
137
|
+
'postgres'
|
|
138
|
+
when 'mysql', 'mysql2', 'mariadb'
|
|
139
|
+
'mysql'
|
|
140
|
+
when 'sqlite', 'sqlite3'
|
|
141
|
+
'sqlite'
|
|
142
142
|
end
|
|
143
|
+
end
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
def database_config_path
|
|
146
|
+
File.join(@cwd, 'config', 'database.yml')
|
|
147
|
+
end
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
def deploy_config_path
|
|
150
|
+
File.join(@cwd, 'config', 'deploy.yml')
|
|
151
|
+
end
|
|
151
152
|
end
|
|
152
153
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module KamalBackup
|
|
2
4
|
class Redactor
|
|
3
5
|
SECRET_KEY_PATTERN = /(pass|password|secret|token|key|credential|authorization)/i
|
|
4
6
|
SENSITIVE_KEY_PATTERN = /(?:pass|password|secret|token|key|credential|authorization)|\A(?:user|username|pguser|.*_user|.*_username)\z/i
|
|
5
|
-
REDACTED =
|
|
7
|
+
REDACTED = '[REDACTED]'
|
|
6
8
|
|
|
7
9
|
def initialize(secret_values: [], env: ENV)
|
|
8
10
|
@secret_values = Array(secret_values).compact.map(&:to_s).reject { |value| value.empty? || value.length < 4 }
|
|
@@ -31,22 +33,25 @@ module KamalBackup
|
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
private
|
|
34
|
-
def known_secret_values
|
|
35
|
-
@known_secret_values ||= begin
|
|
36
|
-
env_secrets = @env.each_with_object([]) do |(key, value), values|
|
|
37
|
-
values << value.to_s if key.to_s.match?(SECRET_KEY_PATTERN)
|
|
38
|
-
end
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
def known_secret_values
|
|
38
|
+
@known_secret_values ||= begin
|
|
39
|
+
env_secrets = @env.each_with_object([]) do |(key, value), values|
|
|
40
|
+
values << value.to_s if key.to_s.match?(SECRET_KEY_PATTERN)
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
(@secret_values + env_secrets).compact.uniq.reject { |value| value.empty? || value.length < 4 }
|
|
42
44
|
end
|
|
45
|
+
end
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end.gsub(/([?&](?:password|token|secret|key|access_key_id|secret_access_key)=)[^&\s]+/i) do
|
|
48
|
-
"#{$1}#{REDACTED}"
|
|
49
|
-
end
|
|
47
|
+
def redact_url_credentials(value)
|
|
48
|
+
redacted = value.gsub(%r{(://)([^/\s]+)@}) do
|
|
49
|
+
"#{::Regexp.last_match(1)}#{REDACTED}@"
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
redacted.gsub(/([?&](?:password|token|secret|key|access_key_id|secret_access_key)=)[^&\s]+/i) do
|
|
53
|
+
"#{::Regexp.last_match(1)}#{REDACTED}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
51
56
|
end
|
|
52
57
|
end
|