kdeploy 1.3.5 → 1.4.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/CHANGELOG.md +80 -0
- data/CONTRIBUTING.md +35 -0
- data/LICENSE +21 -0
- data/README.md +17 -7
- data/README_EN.md +18 -7
- data/SECURITY.md +37 -0
- data/lib/kdeploy/cli/cli.rb +64 -9
- data/lib/kdeploy/cli/help_formatter.rb +6 -0
- data/lib/kdeploy/config/configuration.rb +7 -2
- data/lib/kdeploy/dsl/dsl.rb +211 -110
- data/lib/kdeploy/dsl/initializer.rb +20 -0
- data/lib/kdeploy/executor/command_executor.rb +77 -1
- data/lib/kdeploy/executor/executor.rb +91 -17
- data/lib/kdeploy/output/output_formatter.rb +12 -3
- data/lib/kdeploy/runner/runner.rb +74 -22
- data/lib/kdeploy/template/template.rb +2 -1
- data/lib/kdeploy/version.rb +1 -1
- metadata +9 -3
data/lib/kdeploy/dsl/dsl.rb
CHANGED
|
@@ -22,12 +22,132 @@ module Kdeploy
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Per-invocation collector so parallel hosts do not race on DSL ivars.
|
|
26
|
+
class TaskContext
|
|
27
|
+
def initialize(dsl, commands, on_failure_commands, vars)
|
|
28
|
+
@dsl = dsl
|
|
29
|
+
@commands = commands
|
|
30
|
+
@on_failure_commands = on_failure_commands
|
|
31
|
+
@vars = vars.transform_keys(&:to_sym)
|
|
32
|
+
@failure_mode = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def on_failure(&block)
|
|
36
|
+
raise ArgumentError, 'on_failure requires a block' unless block
|
|
37
|
+
|
|
38
|
+
previous = @failure_mode
|
|
39
|
+
@failure_mode = true
|
|
40
|
+
instance_eval(&block)
|
|
41
|
+
ensure
|
|
42
|
+
@failure_mode = previous
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run(command, sudo: nil)
|
|
46
|
+
buf << { type: :run, command: interpolate(command), sudo: sudo }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def upload(source, destination)
|
|
50
|
+
buf << { type: :upload, source: interpolate(source), destination: interpolate(destination) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def upload_template(source, destination, variables = {})
|
|
54
|
+
buf << {
|
|
55
|
+
type: :upload_template,
|
|
56
|
+
source: interpolate(source),
|
|
57
|
+
destination: interpolate(destination),
|
|
58
|
+
variables: interpolate_hash(variables)
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def sync(source, destination, ignore: [], delete: false, exclude: [], fast: nil, parallel: nil)
|
|
63
|
+
buf << {
|
|
64
|
+
type: :sync,
|
|
65
|
+
source: interpolate(source),
|
|
66
|
+
destination: interpolate(destination),
|
|
67
|
+
ignore: Array(ignore).map { |p| interpolate(p.to_s) },
|
|
68
|
+
exclude: Array(exclude).map { |p| interpolate(p.to_s) },
|
|
69
|
+
delete: delete,
|
|
70
|
+
fast: fast,
|
|
71
|
+
parallel: parallel
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def package(name, version: nil, platform: :apt)
|
|
76
|
+
cmd = @dsl.build_package_command(interpolate(name.to_s), version && interpolate(version.to_s), platform)
|
|
77
|
+
buf << { type: :run, command: cmd, sudo: true }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def service(name, action: :start)
|
|
81
|
+
Array(action).each do |a|
|
|
82
|
+
cmd = "systemctl #{a} #{Shellwords.escape(interpolate(name.to_s))}"
|
|
83
|
+
buf << { type: :run, command: cmd, sudo: true }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def template(destination, source: nil, variables: nil, &block)
|
|
88
|
+
if block
|
|
89
|
+
opts = TemplateOptions.new
|
|
90
|
+
opts.instance_eval(&block)
|
|
91
|
+
src = opts.source || raise(ArgumentError, 'template requires source')
|
|
92
|
+
vars = opts.variables || {}
|
|
93
|
+
else
|
|
94
|
+
raise ArgumentError, 'template requires source' unless source
|
|
95
|
+
|
|
96
|
+
src = source
|
|
97
|
+
vars = variables || {}
|
|
98
|
+
end
|
|
99
|
+
upload_template(src, destination, vars)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def file(destination, source:)
|
|
103
|
+
upload(source, destination)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def directory(path, mode: nil)
|
|
107
|
+
cmd = "mkdir -p #{Shellwords.escape(interpolate(path.to_s))}"
|
|
108
|
+
buf << { type: :run, command: cmd, sudo: true }
|
|
109
|
+
return unless mode
|
|
110
|
+
|
|
111
|
+
buf << { type: :run, command: "chmod #{mode} #{Shellwords.escape(interpolate(path.to_s))}", sudo: true }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def wait_port(port, timeout: 30, interval: 1)
|
|
115
|
+
buf << {
|
|
116
|
+
type: :wait_port,
|
|
117
|
+
port: port.to_i,
|
|
118
|
+
timeout: timeout.to_f,
|
|
119
|
+
interval: interval.to_f
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def wait_http(url, timeout: 30, interval: 1, code: 200)
|
|
124
|
+
buf << {
|
|
125
|
+
type: :wait_http,
|
|
126
|
+
url: interpolate(url),
|
|
127
|
+
timeout: timeout.to_f,
|
|
128
|
+
interval: interval.to_f,
|
|
129
|
+
code: code.to_i
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def buf
|
|
136
|
+
@failure_mode ? @on_failure_commands : @commands
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def interpolate(value)
|
|
140
|
+
@dsl.interpolate_string(value, @vars)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def interpolate_hash(hash)
|
|
144
|
+
hash.to_h.transform_values { |v| v.is_a?(String) ? interpolate(v) : v }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
25
148
|
# Domain-specific language for defining hosts, roles, and tasks
|
|
26
149
|
module DSL
|
|
27
150
|
def self.included(base)
|
|
28
|
-
# Support `include Kdeploy::DSL` by promoting the DSL methods to class methods.
|
|
29
|
-
# This keeps tests and external integrations simpler while preserving the
|
|
30
|
-
# primary usage pattern (CLI uses `extend DSL`).
|
|
31
151
|
base.extend(self)
|
|
32
152
|
end
|
|
33
153
|
|
|
@@ -35,10 +155,9 @@ module Kdeploy
|
|
|
35
155
|
base.instance_variable_set(:@kdeploy_hosts, {})
|
|
36
156
|
base.instance_variable_set(:@kdeploy_tasks, {})
|
|
37
157
|
base.instance_variable_set(:@kdeploy_roles, {})
|
|
158
|
+
base.instance_variable_set(:@kdeploy_vars, {})
|
|
38
159
|
end
|
|
39
160
|
|
|
40
|
-
# Stable read accessors for tests/integrations.
|
|
41
|
-
# Keep these as aliases so internal storage can evolve without breaking callers.
|
|
42
161
|
def hosts
|
|
43
162
|
kdeploy_hosts
|
|
44
163
|
end
|
|
@@ -63,6 +182,44 @@ module Kdeploy
|
|
|
63
182
|
@kdeploy_roles ||= {}
|
|
64
183
|
end
|
|
65
184
|
|
|
185
|
+
def kdeploy_vars
|
|
186
|
+
@kdeploy_vars ||= {}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def clear_inventory!
|
|
190
|
+
kdeploy_hosts.clear
|
|
191
|
+
kdeploy_roles.clear
|
|
192
|
+
kdeploy_tasks.clear
|
|
193
|
+
kdeploy_vars.clear
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def vars(hash = nil, **kwargs)
|
|
197
|
+
merged = {}
|
|
198
|
+
merged.merge!(hash) if hash.is_a?(Hash)
|
|
199
|
+
merged.merge!(kwargs)
|
|
200
|
+
merged.each { |k, v| kdeploy_vars[k.to_sym] = v }
|
|
201
|
+
kdeploy_vars
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def merge_extra_vars!(extra)
|
|
205
|
+
return if extra.nil? || extra.empty?
|
|
206
|
+
|
|
207
|
+
extra.each { |k, v| kdeploy_vars[k.to_sym] = v }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def interpolate_string(value, local_vars = {})
|
|
211
|
+
return value unless value.is_a?(String)
|
|
212
|
+
return value unless value.include?('%{')
|
|
213
|
+
|
|
214
|
+
binding_vars = kdeploy_vars.merge(local_vars.transform_keys(&:to_sym))
|
|
215
|
+
value.gsub(/%\{(\w+)\}/) do
|
|
216
|
+
key = Regexp.last_match(1).to_sym
|
|
217
|
+
raise ArgumentError, "Unknown variable %{#{key}}" unless binding_vars.key?(key)
|
|
218
|
+
|
|
219
|
+
binding_vars[key].to_s
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
66
223
|
def host(name, **options)
|
|
67
224
|
kdeploy_hosts[name] = options.merge(name: name)
|
|
68
225
|
end
|
|
@@ -71,46 +228,37 @@ module Kdeploy
|
|
|
71
228
|
kdeploy_roles[name] = hosts
|
|
72
229
|
end
|
|
73
230
|
|
|
74
|
-
def task(name, on: nil, roles: nil, &block)
|
|
231
|
+
def task(name, on: nil, roles: nil, tags: nil, &block)
|
|
75
232
|
kdeploy_tasks[name] = {
|
|
76
233
|
hosts: normalize_hosts_option(on),
|
|
77
234
|
roles: normalize_roles_option(roles),
|
|
235
|
+
tags: normalize_tags_option(tags),
|
|
78
236
|
block: create_task_block(block)
|
|
79
237
|
}
|
|
80
238
|
end
|
|
81
239
|
|
|
82
|
-
|
|
83
|
-
def assign_task(task_name, on: nil, roles: nil)
|
|
240
|
+
def assign_task(task_name, on: nil, roles: nil, tags: nil)
|
|
84
241
|
task = kdeploy_tasks[task_name.to_sym]
|
|
85
242
|
raise ArgumentError, "Task #{task_name} not found" unless task
|
|
86
243
|
|
|
87
244
|
task[:hosts] = normalize_hosts_option(on) if on
|
|
88
245
|
task[:roles] = normalize_roles_option(roles) if roles
|
|
246
|
+
task[:tags] = normalize_tags_option(tags) if tags
|
|
89
247
|
end
|
|
90
248
|
|
|
91
|
-
# Include task file and automatically assign all tasks to specified roles or hosts
|
|
92
249
|
def include_tasks(file_path, roles: nil, on: nil)
|
|
93
|
-
# Resolve relative paths based on the caller's file location
|
|
94
250
|
unless File.absolute_path?(file_path)
|
|
95
251
|
caller_file = caller_locations(1, 1).first.path
|
|
96
252
|
base_dir = File.dirname(File.expand_path(caller_file))
|
|
97
253
|
file_path = File.expand_path(file_path, base_dir)
|
|
98
254
|
end
|
|
99
255
|
|
|
100
|
-
# Store tasks before loading
|
|
101
256
|
tasks_before = kdeploy_tasks.keys
|
|
102
|
-
|
|
103
|
-
# Load the task file
|
|
104
257
|
module_eval(File.read(file_path), file_path)
|
|
258
|
+
new_tasks = kdeploy_tasks.keys - tasks_before
|
|
105
259
|
|
|
106
|
-
# Get newly added tasks
|
|
107
|
-
tasks_after = kdeploy_tasks.keys
|
|
108
|
-
new_tasks = tasks_after - tasks_before
|
|
109
|
-
|
|
110
|
-
# Assign roles/hosts to all new tasks (only if task doesn't already have hosts/roles)
|
|
111
260
|
new_tasks.each do |task_name|
|
|
112
261
|
task = kdeploy_tasks[task_name]
|
|
113
|
-
# Only assign if task doesn't already have hosts or roles defined
|
|
114
262
|
next if task[:hosts] || task[:roles]
|
|
115
263
|
|
|
116
264
|
assign_task(task_name, roles: roles, on: on) if roles || on
|
|
@@ -119,7 +267,6 @@ module Kdeploy
|
|
|
119
267
|
|
|
120
268
|
def normalize_hosts_option(on)
|
|
121
269
|
return on if on.is_a?(Array)
|
|
122
|
-
|
|
123
270
|
return [on] if on
|
|
124
271
|
|
|
125
272
|
nil
|
|
@@ -127,106 +274,40 @@ module Kdeploy
|
|
|
127
274
|
|
|
128
275
|
def normalize_roles_option(roles)
|
|
129
276
|
return roles if roles.is_a?(Array)
|
|
130
|
-
|
|
131
277
|
return [roles] if roles
|
|
132
278
|
|
|
133
279
|
nil
|
|
134
280
|
end
|
|
135
281
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
@kdeploy_commands = []
|
|
139
|
-
instance_eval(&block)
|
|
140
|
-
@kdeploy_commands
|
|
141
|
-
}
|
|
142
|
-
end
|
|
282
|
+
def normalize_tags_option(tags)
|
|
283
|
+
return nil if tags.nil?
|
|
143
284
|
|
|
144
|
-
|
|
145
|
-
@kdeploy_commands ||= []
|
|
146
|
-
@kdeploy_commands << { type: :run, command: command, sudo: sudo }
|
|
285
|
+
Array(tags).map { |t| t.to_s.to_sym }
|
|
147
286
|
end
|
|
148
287
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
type: :upload_template,
|
|
158
|
-
source: source,
|
|
159
|
-
destination: destination,
|
|
160
|
-
variables: variables
|
|
161
|
-
}
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def sync(source, destination, ignore: [], delete: false, exclude: [], fast: nil, parallel: nil)
|
|
165
|
-
@kdeploy_commands ||= []
|
|
166
|
-
@kdeploy_commands << {
|
|
167
|
-
type: :sync,
|
|
168
|
-
source: source,
|
|
169
|
-
destination: destination,
|
|
170
|
-
ignore: Array(ignore),
|
|
171
|
-
exclude: Array(exclude),
|
|
172
|
-
delete: delete,
|
|
173
|
-
fast: fast,
|
|
174
|
-
parallel: parallel
|
|
288
|
+
def create_task_block(block)
|
|
289
|
+
dsl = self
|
|
290
|
+
lambda {
|
|
291
|
+
commands = []
|
|
292
|
+
on_failure = []
|
|
293
|
+
ctx = TaskContext.new(dsl, commands, on_failure, dsl.kdeploy_vars.dup)
|
|
294
|
+
ctx.instance_eval(&block)
|
|
295
|
+
{ commands: commands, on_failure: on_failure }
|
|
175
296
|
}
|
|
176
297
|
end
|
|
177
298
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Manage a systemd service. action supports :start, :stop, :restart, :reload, :enable, :disable.
|
|
190
|
-
def service(name, action: :start)
|
|
191
|
-
@kdeploy_commands ||= []
|
|
192
|
-
actions = Array(action)
|
|
193
|
-
actions.each do |a|
|
|
194
|
-
cmd = "systemctl #{a} #{Shellwords.escape(name.to_s)}"
|
|
195
|
-
@kdeploy_commands << { type: :run, command: cmd, sudo: true }
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Deploy an ERB template to a remote path. Supports block and keyword args.
|
|
200
|
-
def template(destination, source: nil, variables: nil, &block)
|
|
201
|
-
@kdeploy_commands ||= []
|
|
202
|
-
if block
|
|
203
|
-
opts = TemplateOptions.new
|
|
204
|
-
opts.instance_eval(&block)
|
|
205
|
-
src = opts.source || raise(ArgumentError, 'template requires source')
|
|
206
|
-
vars = opts.variables || {}
|
|
299
|
+
def self.normalize_plan(raw)
|
|
300
|
+
case raw
|
|
301
|
+
when Hash
|
|
302
|
+
{
|
|
303
|
+
commands: Array(raw[:commands] || raw['commands']),
|
|
304
|
+
on_failure: Array(raw[:on_failure] || raw['on_failure'])
|
|
305
|
+
}
|
|
306
|
+
when Array
|
|
307
|
+
{ commands: raw, on_failure: [] }
|
|
207
308
|
else
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
src = source
|
|
211
|
-
vars = variables || {}
|
|
309
|
+
{ commands: Array(raw), on_failure: [] }
|
|
212
310
|
end
|
|
213
|
-
upload_template(src, destination, vars)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# Upload a local file to a remote path.
|
|
217
|
-
def file(destination, source:)
|
|
218
|
-
@kdeploy_commands ||= []
|
|
219
|
-
upload(source, destination)
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Ensure a remote directory exists. Supports mode option.
|
|
223
|
-
def directory(path, mode: nil)
|
|
224
|
-
@kdeploy_commands ||= []
|
|
225
|
-
cmd = "mkdir -p #{Shellwords.escape(path.to_s)}"
|
|
226
|
-
@kdeploy_commands << { type: :run, command: cmd, sudo: true }
|
|
227
|
-
return unless mode
|
|
228
|
-
|
|
229
|
-
@kdeploy_commands << { type: :run, command: "chmod #{mode} #{Shellwords.escape(path.to_s)}", sudo: true }
|
|
230
311
|
end
|
|
231
312
|
|
|
232
313
|
def inventory(&block)
|
|
@@ -243,23 +324,43 @@ module Kdeploy
|
|
|
243
324
|
hosts.to_a
|
|
244
325
|
end
|
|
245
326
|
|
|
327
|
+
def tasks_matching_tags(tag_list)
|
|
328
|
+
wanted = Array(tag_list).map { |t| t.to_s.to_sym }
|
|
329
|
+
return kdeploy_tasks.keys if wanted.empty?
|
|
330
|
+
|
|
331
|
+
kdeploy_tasks.select do |_name, task|
|
|
332
|
+
task[:tags]&.any? { |t| wanted.include?(t) }
|
|
333
|
+
end.keys
|
|
334
|
+
end
|
|
335
|
+
|
|
246
336
|
def task_empty?(task)
|
|
247
337
|
!task || (!task[:hosts] && !task[:roles])
|
|
248
338
|
end
|
|
249
339
|
|
|
250
340
|
def add_explicit_hosts(task, hosts)
|
|
251
341
|
task[:hosts]&.each do |host|
|
|
252
|
-
|
|
342
|
+
if kdeploy_hosts.key?(host)
|
|
343
|
+
hosts.add(host)
|
|
344
|
+
else
|
|
345
|
+
warn "Warning: task host not in inventory (skipped): #{host}"
|
|
346
|
+
end
|
|
253
347
|
end
|
|
254
348
|
end
|
|
255
349
|
|
|
256
350
|
def add_role_hosts(task, hosts)
|
|
257
351
|
task[:roles]&.each do |role|
|
|
258
352
|
role_hosts = kdeploy_roles[role]
|
|
259
|
-
|
|
353
|
+
unless role_hosts
|
|
354
|
+
warn "Warning: task role not defined (skipped): #{role}"
|
|
355
|
+
next
|
|
356
|
+
end
|
|
260
357
|
|
|
261
358
|
role_hosts.each do |host|
|
|
262
|
-
|
|
359
|
+
if kdeploy_hosts.key?(host)
|
|
360
|
+
hosts.add(host)
|
|
361
|
+
else
|
|
362
|
+
warn "Warning: role #{role} host not in inventory (skipped): #{host}"
|
|
363
|
+
end
|
|
263
364
|
end
|
|
264
365
|
end
|
|
265
366
|
end
|
|
@@ -13,6 +13,7 @@ module Kdeploy
|
|
|
13
13
|
create_directory_structure
|
|
14
14
|
create_deploy_file
|
|
15
15
|
create_config_files
|
|
16
|
+
create_kdeploy_yml
|
|
16
17
|
create_readme
|
|
17
18
|
show_success_message
|
|
18
19
|
end
|
|
@@ -165,6 +166,20 @@ module Kdeploy
|
|
|
165
166
|
CONF
|
|
166
167
|
end
|
|
167
168
|
|
|
169
|
+
def create_kdeploy_yml
|
|
170
|
+
path = File.join(@target_dir, '.kdeploy.yml')
|
|
171
|
+
return if File.exist?(path)
|
|
172
|
+
|
|
173
|
+
File.write(path, <<~YAML)
|
|
174
|
+
# Kdeploy project config (see SECURITY.md in the kdeploy repo)
|
|
175
|
+
parallel: 10
|
|
176
|
+
ssh_timeout: 30
|
|
177
|
+
# Library default is accept_new (safer). Use never only for ephemeral labs.
|
|
178
|
+
verify_host_key: accept_new
|
|
179
|
+
retries: 0
|
|
180
|
+
YAML
|
|
181
|
+
end
|
|
182
|
+
|
|
168
183
|
def create_readme
|
|
169
184
|
File.write(File.join(@target_dir, 'README.md'), <<~MD)
|
|
170
185
|
# Deployment Project
|
|
@@ -215,6 +230,10 @@ module Kdeploy
|
|
|
215
230
|
|
|
216
231
|
See the main documentation for more details on sudo usage.
|
|
217
232
|
|
|
233
|
+
## Security
|
|
234
|
+
|
|
235
|
+
Default `verify_host_key` is `accept_new`. Use `always` for strict known_hosts, or `never` only for ephemeral labs.
|
|
236
|
+
|
|
218
237
|
## 🚀 Usage
|
|
219
238
|
|
|
220
239
|
### Task Execution
|
|
@@ -283,6 +302,7 @@ module Kdeploy
|
|
|
283
302
|
#{pastel.dim(" #{File.join(@target_dir, 'config/nginx.conf.erb')}")}
|
|
284
303
|
#{pastel.dim(" #{File.join(@target_dir, 'config/app.conf')}")}
|
|
285
304
|
#{pastel.dim(" #{File.join(@target_dir, 'README.md')}")}
|
|
305
|
+
#{pastel.dim(" #{File.join(@target_dir, '.kdeploy.yml')}")}
|
|
286
306
|
|
|
287
307
|
#{pastel.bright_white('Try running:')}
|
|
288
308
|
#{pastel.bright_cyan(" kdeploy execute #{File.join(@target_dir, 'deploy.rb')} deploy_web --dry-run")}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'timeout'
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'net/http'
|
|
4
7
|
|
|
5
8
|
module Kdeploy
|
|
6
9
|
# Executes a single command and records execution time
|
|
@@ -83,6 +86,32 @@ module Kdeploy
|
|
|
83
86
|
build_sync_result(source, destination, result, duration)
|
|
84
87
|
end
|
|
85
88
|
|
|
89
|
+
def execute_wait_port(command, _host_name)
|
|
90
|
+
port = command[:port]
|
|
91
|
+
timeout = command[:timeout] || 30
|
|
92
|
+
interval = command[:interval] || 1
|
|
93
|
+
ip = @executor.host_ip
|
|
94
|
+
_result, duration = measure_time do
|
|
95
|
+
with_timeout { wait_until(timeout, interval, "port #{ip}:#{port}") { port_open?(ip, port) } }
|
|
96
|
+
end
|
|
97
|
+
{ command: "wait_port: #{ip}:#{port}", duration: duration, type: :wait_port }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# -- runtime %{ip}/%{host} placeholders
|
|
101
|
+
def execute_wait_http(command, host_name)
|
|
102
|
+
url = command[:url].to_s.gsub('%{ip}', @executor.host_ip.to_s).gsub('%{host}', host_name.to_s)
|
|
103
|
+
# rubocop:enable Style/FormatStringToken
|
|
104
|
+
timeout = command[:timeout] || 30
|
|
105
|
+
interval = command[:interval] || 1
|
|
106
|
+
expect_code = command[:code] || 200
|
|
107
|
+
_result, duration = measure_time do
|
|
108
|
+
with_timeout do
|
|
109
|
+
wait_until(timeout, interval, "http #{url}") { http_ok?(url, expect_code) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
{ command: "wait_http: #{url} (#{expect_code})", duration: duration, type: :wait_http }
|
|
113
|
+
end
|
|
114
|
+
|
|
86
115
|
private
|
|
87
116
|
|
|
88
117
|
def build_sync_result(source, destination, result, duration)
|
|
@@ -112,7 +141,7 @@ module Kdeploy
|
|
|
112
141
|
attempts += 1
|
|
113
142
|
yield
|
|
114
143
|
rescue SSHError, SCPError, TemplateError => e
|
|
115
|
-
raise
|
|
144
|
+
raise unless retryable_error?(e, exit_codes)
|
|
116
145
|
raise if attempts > (max_retries + 1)
|
|
117
146
|
|
|
118
147
|
sleep(@retry_delay) if @retry_delay.positive?
|
|
@@ -120,14 +149,61 @@ module Kdeploy
|
|
|
120
149
|
end
|
|
121
150
|
end
|
|
122
151
|
|
|
152
|
+
def retryable_error?(error, exit_codes)
|
|
153
|
+
return true unless error.is_a?(SSHError)
|
|
154
|
+
return retry_on_exit_status?(error.exit_status, exit_codes) if error.exit_status
|
|
155
|
+
|
|
156
|
+
!authentication_error?(error)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def authentication_error?(error)
|
|
160
|
+
original = error.respond_to?(:original_error) ? error.original_error : nil
|
|
161
|
+
return true if original && original.class.name.include?('AuthenticationFailed')
|
|
162
|
+
|
|
163
|
+
error.message.match?(/authentication failed/i)
|
|
164
|
+
end
|
|
165
|
+
|
|
123
166
|
def with_timeout(&block)
|
|
124
167
|
return yield unless @step_timeout&.positive?
|
|
125
168
|
|
|
126
169
|
Timeout.timeout(@step_timeout, &block)
|
|
127
170
|
rescue Timeout::Error
|
|
171
|
+
@executor.abort! if @executor.respond_to?(:abort!)
|
|
128
172
|
raise StepTimeoutError, "exceeded #{@step_timeout}s"
|
|
129
173
|
end
|
|
130
174
|
|
|
175
|
+
def wait_until(timeout, interval, label)
|
|
176
|
+
deadline = Time.now + timeout.to_f
|
|
177
|
+
loop do
|
|
178
|
+
return true if yield
|
|
179
|
+
raise StepTimeoutError, "#{label} not ready within #{timeout}s" if Time.now >= deadline
|
|
180
|
+
|
|
181
|
+
sleep([interval.to_f, 0.1].max)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def port_open?(ip, port)
|
|
186
|
+
Socket.tcp(ip, port, connect_timeout: 2) { true }
|
|
187
|
+
rescue StandardError
|
|
188
|
+
false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def http_ok?(url, expect_code)
|
|
192
|
+
uri = URI.parse(url)
|
|
193
|
+
response = Net::HTTP.start(
|
|
194
|
+
uri.host,
|
|
195
|
+
uri.port,
|
|
196
|
+
use_ssl: uri.scheme == 'https',
|
|
197
|
+
open_timeout: 2,
|
|
198
|
+
read_timeout: 2
|
|
199
|
+
) do |http|
|
|
200
|
+
http.request(Net::HTTP::Get.new(uri.request_uri))
|
|
201
|
+
end
|
|
202
|
+
response.code.to_i == expect_code.to_i
|
|
203
|
+
rescue StandardError
|
|
204
|
+
false
|
|
205
|
+
end
|
|
206
|
+
|
|
131
207
|
def retries_for(step_type)
|
|
132
208
|
return @retries unless @retry_policy.is_a?(Hash) && step_type
|
|
133
209
|
|