kamal-backup 0.3.0.beta21 → 0.3.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 +1 -0
- data/exe/kamal-backup +7 -6
- data/lib/kamal_backup/app.rb +330 -393
- data/lib/kamal_backup/cli/helpers.rb +298 -0
- data/lib/kamal_backup/cli.rb +73 -367
- data/lib/kamal_backup/command.rb +77 -258
- data/lib/kamal_backup/command_output.rb +189 -0
- data/lib/kamal_backup/config.rb +242 -624
- data/lib/kamal_backup/config_file.rb +376 -0
- data/lib/kamal_backup/databases/base.rb +28 -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 +61 -63
- data/lib/kamal_backup/kamal_bridge.rb +270 -254
- data/lib/kamal_backup/rails_app.rb +94 -104
- data/lib/kamal_backup/redactor.rb +18 -13
- data/lib/kamal_backup/restic.rb +207 -183
- 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/yaml_access.rb +13 -0
- data/lib/kamal_backup.rb +22 -17
- metadata +76 -2
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
require_relative 'databases/base'
|
|
7
|
+
require_relative 'yaml_access'
|
|
4
8
|
|
|
5
9
|
module KamalBackup
|
|
6
10
|
class RailsApp
|
|
7
|
-
|
|
11
|
+
include YamlAccess
|
|
12
|
+
|
|
13
|
+
DEVELOPMENT_ENV = 'development'
|
|
8
14
|
|
|
9
15
|
def initialize(cwd:)
|
|
10
16
|
@cwd = File.expand_path(cwd)
|
|
@@ -17,136 +23,120 @@ module KamalBackup
|
|
|
17
23
|
defaults.merge!(deploy_defaults)
|
|
18
24
|
defaults.merge!(database_defaults)
|
|
19
25
|
|
|
20
|
-
if local_storage_path
|
|
21
|
-
defaults["BACKUP_PATHS"] = local_storage_path
|
|
22
|
-
end
|
|
26
|
+
defaults['BACKUP_PATHS'] = local_storage_path if local_storage_path
|
|
23
27
|
|
|
24
|
-
defaults[
|
|
28
|
+
defaults['KAMAL_BACKUP_STATE_DIR'] = File.join(@cwd, 'tmp', 'kamal-backup')
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
private
|
|
29
|
-
def rails_app?
|
|
30
|
-
File.file?(database_config_path)
|
|
31
|
-
end
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
def rails_app?
|
|
35
|
+
File.file?(database_config_path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deploy_defaults
|
|
39
|
+
service = fetch(parsed_yaml(deploy_config_path), :service)
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
end
|
|
41
|
+
if service
|
|
42
|
+
{ 'APP_NAME' => service.to_s }
|
|
43
|
+
else
|
|
44
|
+
{}
|
|
41
45
|
end
|
|
46
|
+
end
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
def database_defaults
|
|
49
|
+
config = local_database_config
|
|
50
|
+
return {} unless config
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
if (url = fetch(config, :url))
|
|
53
|
+
adapter = adapter_from_url(url)
|
|
49
54
|
|
|
55
|
+
{
|
|
56
|
+
'DATABASE_ADAPTER' => adapter,
|
|
57
|
+
'DATABASE_URL' => url.to_s
|
|
58
|
+
}.compact
|
|
59
|
+
else
|
|
60
|
+
case Databases.normalize_adapter(fetch(config, :adapter))
|
|
61
|
+
when 'postgres'
|
|
50
62
|
{
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
'DATABASE_ADAPTER' => 'postgres',
|
|
64
|
+
'PGHOST' => fetch(config, :host),
|
|
65
|
+
'PGPORT' => fetch(config, :port)&.to_s,
|
|
66
|
+
'PGUSER' => fetch(config, :username),
|
|
67
|
+
'PGDATABASE' => fetch(config, :database)
|
|
53
68
|
}.compact
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
when 'mysql'
|
|
70
|
+
{
|
|
71
|
+
'DATABASE_ADAPTER' => 'mysql',
|
|
72
|
+
'MYSQL_HOST' => fetch(config, :host),
|
|
73
|
+
'MYSQL_PORT' => fetch(config, :port)&.to_s,
|
|
74
|
+
'MYSQL_USER' => fetch(config, :username),
|
|
75
|
+
'MYSQL_DATABASE' => fetch(config, :database)
|
|
76
|
+
}.compact
|
|
77
|
+
when 'sqlite'
|
|
78
|
+
database = fetch(config, :database)
|
|
79
|
+
if database
|
|
65
80
|
{
|
|
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
|
|
81
|
+
'DATABASE_ADAPTER' => 'sqlite',
|
|
82
|
+
'SQLITE_DATABASE_PATH' => File.expand_path(database.to_s, @cwd)
|
|
83
|
+
}
|
|
82
84
|
else
|
|
83
85
|
{}
|
|
84
86
|
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
87
|
else
|
|
99
|
-
|
|
100
|
-
primary if primary.is_a?(Hash)
|
|
88
|
+
{}
|
|
101
89
|
end
|
|
102
90
|
end
|
|
91
|
+
end
|
|
103
92
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
def local_storage_path
|
|
94
|
+
File.join(@cwd, 'storage')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def local_database_config
|
|
98
|
+
environment = fetch(parsed_yaml(database_config_path), DEVELOPMENT_ENV)
|
|
99
|
+
return nil unless environment.is_a?(Hash)
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
if database_entry?(environment)
|
|
102
|
+
environment
|
|
103
|
+
else
|
|
104
|
+
primary = fetch(environment, :primary)
|
|
105
|
+
primary if primary.is_a?(Hash)
|
|
112
106
|
end
|
|
107
|
+
end
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
def database_entry?(config)
|
|
110
|
+
fetch(config, :adapter) || fetch(config, :database) || fetch(config, :url)
|
|
111
|
+
end
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
def adapter_from_url(url)
|
|
114
|
+
Databases.normalize_adapter(URI.parse(url.to_s).scheme)
|
|
115
|
+
rescue URI::InvalidURIError
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
else
|
|
123
|
-
{}
|
|
124
|
-
end
|
|
125
|
-
rescue Psych::SyntaxError => e
|
|
126
|
-
raise ConfigurationError, "invalid YAML in #{path}: #{e.message}"
|
|
127
|
-
end
|
|
119
|
+
def parsed_yaml(path)
|
|
120
|
+
return {} unless File.file?(path)
|
|
128
121
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
end
|
|
122
|
+
rendered = ERB.new(File.read(path), trim_mode: '-').result
|
|
123
|
+
data = YAML.safe_load(rendered, permitted_classes: [], aliases: true)
|
|
132
124
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
when "mysql", "mysql2", "mariadb"
|
|
138
|
-
"mysql"
|
|
139
|
-
when "sqlite", "sqlite3"
|
|
140
|
-
"sqlite"
|
|
141
|
-
end
|
|
125
|
+
if data.is_a?(Hash)
|
|
126
|
+
data
|
|
127
|
+
else
|
|
128
|
+
{}
|
|
142
129
|
end
|
|
130
|
+
rescue Psych::SyntaxError => e
|
|
131
|
+
raise ConfigurationError, "invalid YAML in #{path}: #{e.message}"
|
|
132
|
+
end
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
134
|
+
def database_config_path
|
|
135
|
+
File.join(@cwd, 'config', 'database.yml')
|
|
136
|
+
end
|
|
147
137
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
def deploy_config_path
|
|
139
|
+
File.join(@cwd, 'config', 'deploy.yml')
|
|
140
|
+
end
|
|
151
141
|
end
|
|
152
142
|
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
|