kamal-backup 0.3.0 → 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/lib/kamal_backup/app.rb +145 -202
- data/lib/kamal_backup/cli/helpers.rb +298 -0
- data/lib/kamal_backup/cli.rb +4 -294
- data/lib/kamal_backup/command.rb +0 -185
- data/lib/kamal_backup/command_output.rb +189 -0
- data/lib/kamal_backup/config.rb +77 -481
- data/lib/kamal_backup/config_file.rb +376 -0
- data/lib/kamal_backup/databases/base.rb +11 -0
- data/lib/kamal_backup/databases/postgres.rb +3 -3
- data/lib/kamal_backup/evidence.rb +0 -3
- data/lib/kamal_backup/kamal_bridge.rb +39 -27
- data/lib/kamal_backup/rails_app.rb +6 -17
- data/lib/kamal_backup/restic.rb +41 -45
- data/lib/kamal_backup/version.rb +1 -1
- data/lib/kamal_backup/yaml_access.rb +13 -0
- data/lib/kamal_backup.rb +3 -0
- metadata +48 -2
data/lib/kamal_backup/command.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'pty'
|
|
5
|
-
require 'securerandom'
|
|
6
5
|
require 'shellwords'
|
|
7
6
|
require_relative 'errors'
|
|
8
7
|
|
|
@@ -30,190 +29,6 @@ module KamalBackup
|
|
|
30
29
|
|
|
31
30
|
CommandResult = Struct.new(:stdout, :stderr, :status, :streamed, keyword_init: true)
|
|
32
31
|
|
|
33
|
-
class CommandOutput
|
|
34
|
-
LEVELS = {
|
|
35
|
-
'DEBUG' => 0,
|
|
36
|
-
'INFO' => 1,
|
|
37
|
-
'WARN' => 2,
|
|
38
|
-
'ERROR' => 3,
|
|
39
|
-
'FATAL' => 4
|
|
40
|
-
}.freeze
|
|
41
|
-
LEVEL_COLORS = {
|
|
42
|
-
'DEBUG' => :black,
|
|
43
|
-
'INFO' => :blue,
|
|
44
|
-
'WARN' => :yellow,
|
|
45
|
-
'ERROR' => :red,
|
|
46
|
-
'FATAL' => :red
|
|
47
|
-
}.freeze
|
|
48
|
-
COLOR_CODES = {
|
|
49
|
-
black: 30,
|
|
50
|
-
red: 31,
|
|
51
|
-
green: 32,
|
|
52
|
-
yellow: 33,
|
|
53
|
-
blue: 34,
|
|
54
|
-
magenta: 35,
|
|
55
|
-
cyan: 36,
|
|
56
|
-
white: 37,
|
|
57
|
-
light_black: 90,
|
|
58
|
-
light_red: 91,
|
|
59
|
-
light_green: 92,
|
|
60
|
-
light_yellow: 93,
|
|
61
|
-
light_blue: 94,
|
|
62
|
-
light_magenta: 95,
|
|
63
|
-
light_cyan: 96,
|
|
64
|
-
light_white: 97
|
|
65
|
-
}.freeze
|
|
66
|
-
|
|
67
|
-
def initialize(io: $stdout, env: ENV, verbosity: :info)
|
|
68
|
-
@io = io
|
|
69
|
-
@env = env
|
|
70
|
-
@verbosity = LEVELS.fetch(verbosity.to_s.upcase)
|
|
71
|
-
@mutex = Mutex.new
|
|
72
|
-
@buffers = {}
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def info(message, redactor:)
|
|
76
|
-
write_message('INFO', redactor.redact_string(message))
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def error(message, redactor:)
|
|
80
|
-
write_message('ERROR', colorize(redactor.redact_string(message), :red, :bold))
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def decorate(value, color, mode = nil)
|
|
84
|
-
colorize(value, color, mode)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def command_start(spec, redactor:)
|
|
88
|
-
id = SecureRandom.hex(4)
|
|
89
|
-
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
90
|
-
display = spec.display(redactor)
|
|
91
|
-
|
|
92
|
-
write_message('INFO', "Running #{colorize(display, :yellow, :bold)} #{target_for(spec)}", id)
|
|
93
|
-
write_message('DEBUG', "Command: #{colorize(display, :blue)}", id)
|
|
94
|
-
|
|
95
|
-
{ id: id, started_at: started_at, redactor: redactor }
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def command_output(context, stream, data, redactor:)
|
|
99
|
-
raw = data.to_s
|
|
100
|
-
return if raw.empty?
|
|
101
|
-
return unless log_level?('DEBUG')
|
|
102
|
-
|
|
103
|
-
synchronize do
|
|
104
|
-
key = [context.fetch(:id), stream]
|
|
105
|
-
@buffers[key] = "#{@buffers[key]}#{raw}"
|
|
106
|
-
flush_complete_output_lines(context, key, redactor: redactor)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def command_exit(context, status)
|
|
111
|
-
runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - context.fetch(:started_at)
|
|
112
|
-
result = status.to_i.zero? ? 'successful' : 'failed'
|
|
113
|
-
result_color = status.to_i.zero? ? :green : :red
|
|
114
|
-
|
|
115
|
-
synchronize do
|
|
116
|
-
flush_output_buffers(context)
|
|
117
|
-
message = "Finished in #{format('%.3f seconds', runtime)} with exit status #{status} " \
|
|
118
|
-
"(#{colorize(result, result_color, :bold)})."
|
|
119
|
-
write_message_unlocked('INFO', message, context.fetch(:id))
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
def write_message(level, message, id = nil)
|
|
126
|
-
return unless log_level?(level)
|
|
127
|
-
|
|
128
|
-
synchronize { write_message_unlocked(level, message, id) }
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def write_message_unlocked(level, message, id = nil)
|
|
132
|
-
@io.puts(format_message(level, message, id)) if log_level?(level)
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def synchronize(&block)
|
|
136
|
-
@mutex.synchronize(&block)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def flush_complete_output_lines(context, key, redactor:)
|
|
140
|
-
buffer = @buffers.fetch(key)
|
|
141
|
-
output = +''
|
|
142
|
-
|
|
143
|
-
while (index = buffer.index("\n"))
|
|
144
|
-
output << buffer.slice!(0..index)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
@buffers[key] = buffer
|
|
148
|
-
write_output(context, output, redactor: redactor, stream: key.last) unless output.empty?
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def flush_output_buffers(context)
|
|
152
|
-
id = context.fetch(:id)
|
|
153
|
-
keys = @buffers.keys.select { |key_id, _stream| key_id == id }
|
|
154
|
-
|
|
155
|
-
keys.each do |key|
|
|
156
|
-
output = @buffers.delete(key)
|
|
157
|
-
next if output.to_s.empty?
|
|
158
|
-
|
|
159
|
-
write_output(context, output, redactor: context.fetch(:redactor), stream: key.last)
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def write_output(context, output, redactor:, stream: nil)
|
|
164
|
-
color = stream == :stderr ? :red : :green
|
|
165
|
-
|
|
166
|
-
redactor.redact_string(output).each_line do |line|
|
|
167
|
-
write_message_unlocked('DEBUG', colorize("\t#{line}".chomp, color), context.fetch(:id))
|
|
168
|
-
end
|
|
169
|
-
@io.flush if @io.respond_to?(:flush)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def format_message(level, message, id = nil)
|
|
173
|
-
message = "[#{colorize(id, :green)}] #{message}" if id
|
|
174
|
-
"#{colorize(level.rjust(6), LEVEL_COLORS.fetch(level))} #{message}"
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def local_target
|
|
178
|
-
if (remote_host = @env['KAMAL_HOST'].to_s) && !remote_host.empty?
|
|
179
|
-
return "on #{colorize(remote_host, :blue)}"
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
user = @env['USER'].to_s.empty? ? @env['USERNAME'].to_s : @env['USER'].to_s
|
|
183
|
-
|
|
184
|
-
if user.empty?
|
|
185
|
-
"on #{colorize('localhost', :blue)}"
|
|
186
|
-
else
|
|
187
|
-
"as #{colorize(user, :blue)}@#{colorize('localhost', :blue)}"
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def target_for(spec)
|
|
192
|
-
if spec.host
|
|
193
|
-
"on #{colorize(spec.host, :blue)}"
|
|
194
|
-
else
|
|
195
|
-
local_target
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def log_level?(level)
|
|
200
|
-
LEVELS.fetch(level) >= @verbosity
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def colorize(value, color, mode = nil)
|
|
204
|
-
string = value.to_s
|
|
205
|
-
return string unless colorize?
|
|
206
|
-
return string unless COLOR_CODES.key?(color)
|
|
207
|
-
|
|
208
|
-
prefix = mode == :bold ? "\e[1;" : "\e[0;"
|
|
209
|
-
"#{prefix}#{COLOR_CODES.fetch(color)};49m#{string}\e[0m"
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def colorize?
|
|
213
|
-
@env['SSHKIT_COLOR'] || (@io.respond_to?(:tty?) && @io.tty?)
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
32
|
class Command
|
|
218
33
|
class << self
|
|
219
34
|
attr_accessor :output
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module KamalBackup
|
|
6
|
+
class CommandOutput
|
|
7
|
+
LEVELS = {
|
|
8
|
+
'DEBUG' => 0,
|
|
9
|
+
'INFO' => 1,
|
|
10
|
+
'WARN' => 2,
|
|
11
|
+
'ERROR' => 3,
|
|
12
|
+
'FATAL' => 4
|
|
13
|
+
}.freeze
|
|
14
|
+
LEVEL_COLORS = {
|
|
15
|
+
'DEBUG' => :black,
|
|
16
|
+
'INFO' => :blue,
|
|
17
|
+
'WARN' => :yellow,
|
|
18
|
+
'ERROR' => :red,
|
|
19
|
+
'FATAL' => :red
|
|
20
|
+
}.freeze
|
|
21
|
+
COLOR_CODES = {
|
|
22
|
+
black: 30,
|
|
23
|
+
red: 31,
|
|
24
|
+
green: 32,
|
|
25
|
+
yellow: 33,
|
|
26
|
+
blue: 34,
|
|
27
|
+
magenta: 35,
|
|
28
|
+
cyan: 36,
|
|
29
|
+
white: 37,
|
|
30
|
+
light_black: 90,
|
|
31
|
+
light_red: 91,
|
|
32
|
+
light_green: 92,
|
|
33
|
+
light_yellow: 93,
|
|
34
|
+
light_blue: 94,
|
|
35
|
+
light_magenta: 95,
|
|
36
|
+
light_cyan: 96,
|
|
37
|
+
light_white: 97
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
def initialize(io: $stdout, env: ENV, verbosity: :info)
|
|
41
|
+
@io = io
|
|
42
|
+
@env = env
|
|
43
|
+
@verbosity = LEVELS.fetch(verbosity.to_s.upcase)
|
|
44
|
+
@mutex = Mutex.new
|
|
45
|
+
@buffers = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def info(message, redactor:)
|
|
49
|
+
write_message('INFO', redactor.redact_string(message))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def error(message, redactor:)
|
|
53
|
+
write_message('ERROR', colorize(redactor.redact_string(message), :red, :bold))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def decorate(value, color, mode = nil)
|
|
57
|
+
colorize(value, color, mode)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def command_start(spec, redactor:)
|
|
61
|
+
id = SecureRandom.hex(4)
|
|
62
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
63
|
+
display = spec.display(redactor)
|
|
64
|
+
|
|
65
|
+
write_message('INFO', "Running #{colorize(display, :yellow, :bold)} #{target_for(spec)}", id)
|
|
66
|
+
write_message('DEBUG', "Command: #{colorize(display, :blue)}", id)
|
|
67
|
+
|
|
68
|
+
{ id: id, started_at: started_at, redactor: redactor }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def command_output(context, stream, data, redactor:)
|
|
72
|
+
raw = data.to_s
|
|
73
|
+
return if raw.empty?
|
|
74
|
+
return unless log_level?('DEBUG')
|
|
75
|
+
|
|
76
|
+
synchronize do
|
|
77
|
+
key = [context.fetch(:id), stream]
|
|
78
|
+
@buffers[key] = "#{@buffers[key]}#{raw}"
|
|
79
|
+
flush_complete_output_lines(context, key, redactor: redactor)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def command_exit(context, status)
|
|
84
|
+
runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - context.fetch(:started_at)
|
|
85
|
+
result = status.to_i.zero? ? 'successful' : 'failed'
|
|
86
|
+
result_color = status.to_i.zero? ? :green : :red
|
|
87
|
+
|
|
88
|
+
synchronize do
|
|
89
|
+
flush_output_buffers(context)
|
|
90
|
+
message = "Finished in #{format('%.3f seconds', runtime)} with exit status #{status} " \
|
|
91
|
+
"(#{colorize(result, result_color, :bold)})."
|
|
92
|
+
write_message_unlocked('INFO', message, context.fetch(:id))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def write_message(level, message, id = nil)
|
|
99
|
+
return unless log_level?(level)
|
|
100
|
+
|
|
101
|
+
synchronize { write_message_unlocked(level, message, id) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def write_message_unlocked(level, message, id = nil)
|
|
105
|
+
@io.puts(format_message(level, message, id)) if log_level?(level)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def synchronize(&block)
|
|
109
|
+
@mutex.synchronize(&block)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def flush_complete_output_lines(context, key, redactor:)
|
|
113
|
+
buffer = @buffers.fetch(key)
|
|
114
|
+
output = +''
|
|
115
|
+
|
|
116
|
+
while (index = buffer.index("\n"))
|
|
117
|
+
output << buffer.slice!(0..index)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
@buffers[key] = buffer
|
|
121
|
+
write_output(context, output, redactor: redactor, stream: key.last) unless output.empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def flush_output_buffers(context)
|
|
125
|
+
id = context.fetch(:id)
|
|
126
|
+
keys = @buffers.keys.select { |key_id, _stream| key_id == id }
|
|
127
|
+
|
|
128
|
+
keys.each do |key|
|
|
129
|
+
output = @buffers.delete(key)
|
|
130
|
+
next if output.to_s.empty?
|
|
131
|
+
|
|
132
|
+
write_output(context, output, redactor: context.fetch(:redactor), stream: key.last)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def write_output(context, output, redactor:, stream: nil)
|
|
137
|
+
color = stream == :stderr ? :red : :green
|
|
138
|
+
|
|
139
|
+
redactor.redact_string(output).each_line do |line|
|
|
140
|
+
write_message_unlocked('DEBUG', colorize("\t#{line}".chomp, color), context.fetch(:id))
|
|
141
|
+
end
|
|
142
|
+
@io.flush if @io.respond_to?(:flush)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def format_message(level, message, id = nil)
|
|
146
|
+
message = "[#{colorize(id, :green)}] #{message}" if id
|
|
147
|
+
"#{colorize(level.rjust(6), LEVEL_COLORS.fetch(level))} #{message}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def local_target
|
|
151
|
+
if (remote_host = @env['KAMAL_HOST'].to_s) && !remote_host.empty?
|
|
152
|
+
return "on #{colorize(remote_host, :blue)}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
user = @env['USER'].to_s.empty? ? @env['USERNAME'].to_s : @env['USER'].to_s
|
|
156
|
+
|
|
157
|
+
if user.empty?
|
|
158
|
+
"on #{colorize('localhost', :blue)}"
|
|
159
|
+
else
|
|
160
|
+
"as #{colorize(user, :blue)}@#{colorize('localhost', :blue)}"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def target_for(spec)
|
|
165
|
+
if spec.host
|
|
166
|
+
"on #{colorize(spec.host, :blue)}"
|
|
167
|
+
else
|
|
168
|
+
local_target
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def log_level?(level)
|
|
173
|
+
LEVELS.fetch(level) >= @verbosity
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def colorize(value, color, mode = nil)
|
|
177
|
+
string = value.to_s
|
|
178
|
+
return string unless colorize?
|
|
179
|
+
return string unless COLOR_CODES.key?(color)
|
|
180
|
+
|
|
181
|
+
prefix = mode == :bold ? "\e[1;" : "\e[0;"
|
|
182
|
+
"#{prefix}#{COLOR_CODES.fetch(color)};49m#{string}\e[0m"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def colorize?
|
|
186
|
+
@env['SSHKIT_COLOR'] || (@io.respond_to?(:tty?) && @io.tty?)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|