indocker 0.5.1 → 0.5.4
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/Gemfile.lock +1 -1
- data/lib/indocker/container_deployer.rb +57 -52
- data/lib/indocker/launchers/configuration_deployer.rb +20 -31
- data/lib/indocker/rsync.rb +14 -1
- data/lib/indocker/ssh_session.rb +8 -1
- data/lib/indocker/version.rb +1 -1
- data/lib/indocker.rb +16 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70bf1241f0783dede869906f9f359c2a2137778a3059acaea9ae2fc7db30bb38
|
|
4
|
+
data.tar.gz: f4d906fc4ae36b9bf4482c7e5802c03b4a8633c7f839a0ffb71bbfc1571a5e30
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d45a4d6ab954e10b76fbaf2b4ca098c1368fd6b6365756f24bb210757d5f221d0fcfcf5b11b378cc5e31de38d72967158c90d91aaa3c9df53334f8a2596f12f0
|
|
7
|
+
data.tar.gz: a0ae86f49931e812d34761ff20f73f704a010b742edb1968dae8e70fc7798edbd75cab519f34632d8f6840ab0b87f5454c70773c50627ac10fc5693e263819cb
|
data/Gemfile.lock
CHANGED
|
@@ -20,29 +20,29 @@ class Indocker::ContainerDeployer
|
|
|
20
20
|
def deploy(container, force_restart, skip_force_restart, progress)
|
|
21
21
|
return if @deployed_containers[container]
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
exit_codes =
|
|
24
|
+
if deploy_servers_in_parallel?(container)
|
|
25
|
+
# Stateless containers: deploy to all servers at once.
|
|
26
|
+
threads = container.servers.map do |server|
|
|
27
|
+
Thread.new do
|
|
28
|
+
deploy_to_server(container, server, force_restart, skip_force_restart, progress)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
threads.map(&:value)
|
|
33
|
+
else
|
|
34
|
+
# Rolling deploy: one server at a time. Containers with rolling-restart
|
|
35
|
+
# hooks (e.g. draining/enabling an nginx upstream) sit behind a load
|
|
36
|
+
# balancer, so all of their servers must not restart simultaneously —
|
|
37
|
+
# otherwise there is a moment with no live backend (502s). Deploying
|
|
38
|
+
# server by server keeps the rest of the fleet serving.
|
|
39
|
+
container.servers.map do |server|
|
|
37
40
|
deploy_to_server(container, server, force_restart, skip_force_restart, progress)
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if results.any? { |exit_code| exit_code != 0 }
|
|
44
|
-
exit 1
|
|
45
|
-
end
|
|
44
|
+
if exit_codes.any? { |exit_code| exit_code != 0 }
|
|
45
|
+
exit 1
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
@deployed_containers[container] = true
|
|
@@ -56,52 +56,57 @@ class Indocker::ContainerDeployer
|
|
|
56
56
|
|
|
57
57
|
private
|
|
58
58
|
|
|
59
|
+
# Whether the container may be deployed to all of its servers concurrently.
|
|
60
|
+
# An explicit :parallel_deploy start option wins; otherwise a container is
|
|
61
|
+
# deployed in parallel only when it has no rolling-restart hooks (containers
|
|
62
|
+
# with before/after start hooks are drained at a load balancer and must roll
|
|
63
|
+
# across servers one at a time).
|
|
64
|
+
def deploy_servers_in_parallel?(container)
|
|
65
|
+
option = container.get_start_option(:parallel_deploy)
|
|
66
|
+
return option unless option.nil?
|
|
67
|
+
|
|
68
|
+
container.before_start_proc.nil? && container.after_start_proc.nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
59
71
|
# Runs a single container deployment on a single server. Returns the remote
|
|
60
|
-
# exit code (0 on success).
|
|
61
|
-
#
|
|
62
|
-
#
|
|
72
|
+
# exit code (0 on success).
|
|
73
|
+
#
|
|
74
|
+
# Per-server exclusion: only one container deploys on a given server at a time,
|
|
75
|
+
# so several container types don't spike CPU/memory by starting at once on the
|
|
76
|
+
# same host. Each invocation holds only its own server's lock, so concurrent
|
|
77
|
+
# deployments can't deadlock.
|
|
63
78
|
def deploy_to_server(container, server, force_restart, skip_force_restart, progress)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
server_lock(server).synchronize do
|
|
80
|
+
progress.start_deploying_container(container, server)
|
|
81
|
+
|
|
82
|
+
deploy_server = @server_pool.create_connection!(server)
|
|
83
|
+
@logger.info("Deploying container: #{container.name.to_s.green} to #{server.user}@#{server.host}")
|
|
84
|
+
|
|
85
|
+
result = deploy_server
|
|
86
|
+
.run_container_remotely(
|
|
87
|
+
configuration_name: Indocker.configuration_name,
|
|
88
|
+
container_name: container.name,
|
|
89
|
+
force_restart: force_restart && !skip_force_restart.include?(container.name)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if result.exit_code == 0
|
|
93
|
+
@logger.info("Container deployment to #{server.user}@#{server.host} finished: #{container.name.to_s.green}")
|
|
94
|
+
progress.finish_deploying_container(container, server)
|
|
95
|
+
end
|
|
68
96
|
|
|
69
|
-
|
|
70
|
-
.run_container_remotely(
|
|
71
|
-
configuration_name: Indocker.configuration_name,
|
|
72
|
-
container_name: container.name,
|
|
73
|
-
force_restart: force_restart && !skip_force_restart.include?(container.name)
|
|
74
|
-
)
|
|
97
|
+
deploy_server.close_session
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
@logger.info("Container deployment to #{server.user}@#{server.host} finished: #{container.name.to_s.green}")
|
|
78
|
-
progress.finish_deploying_container(container, server)
|
|
99
|
+
result.exit_code
|
|
79
100
|
end
|
|
80
|
-
|
|
81
|
-
deploy_server.close_session
|
|
82
|
-
|
|
83
|
-
result.exit_code
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def server_lock_key(server)
|
|
87
|
-
[server.host, server.port, server.user]
|
|
88
101
|
end
|
|
89
102
|
|
|
90
103
|
# Returns a process-wide singleton Mutex for the given server, so the same lock
|
|
91
104
|
# is shared across every container targeting that server.
|
|
92
105
|
def server_lock(server)
|
|
93
|
-
key =
|
|
106
|
+
key = [server.host, server.port, server.user]
|
|
94
107
|
|
|
95
108
|
@server_locks_mutex.synchronize do
|
|
96
109
|
@server_locks[key] ||= Mutex.new
|
|
97
110
|
end
|
|
98
111
|
end
|
|
99
|
-
|
|
100
|
-
def with_locks(locks, &block)
|
|
101
|
-
if locks.empty?
|
|
102
|
-
block.call
|
|
103
|
-
else
|
|
104
|
-
locks.first.synchronize { with_locks(locks.drop(1), &block) }
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
112
|
end
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
require 'timeout'
|
|
2
1
|
require 'benchmark'
|
|
3
2
|
require 'tempfile'
|
|
4
3
|
|
|
5
4
|
class Indocker::Launchers::ConfigurationDeployer
|
|
6
|
-
REMOTE_OPERATION_TIMEOUT = 60
|
|
7
5
|
|
|
8
6
|
def initialize(logger:, global_logger:)
|
|
9
7
|
Thread.abort_on_exception = true # abort all threads if exception occurs
|
|
@@ -107,12 +105,8 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
107
105
|
|
|
108
106
|
def wait_remote_operations(remote_operations)
|
|
109
107
|
remote_operations.each do |remote_operation|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
remote_operation.thread.join
|
|
113
|
-
end
|
|
114
|
-
rescue Timeout::Error
|
|
115
|
-
@global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{remote_operation.server.user}@#{remote_operation.server.host} did not finish during #{REMOTE_OPERATION_TIMEOUT} seconds.")
|
|
108
|
+
if remote_operation.thread.join(Indocker.remote_operation_timeout).nil?
|
|
109
|
+
@global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{remote_operation.server.user}@#{remote_operation.server.host} did not finish during #{Indocker.remote_operation_timeout} seconds.")
|
|
116
110
|
exit 1
|
|
117
111
|
end
|
|
118
112
|
end
|
|
@@ -302,34 +296,28 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
302
296
|
# container to the deploy stage — so the next image is already building while
|
|
303
297
|
# the previous container deploys.
|
|
304
298
|
#
|
|
305
|
-
# The consumer
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
299
|
+
# The consumer deploys containers strictly one at a time, in the order they
|
|
300
|
+
# were built (which is dependency order, so depends_on is respected). Only a
|
|
301
|
+
# single container is ever restarting across the whole fleet at any moment —
|
|
302
|
+
# the invariant the original sequential deploy guaranteed. This matters for
|
|
303
|
+
# zero-downtime web apps: deploying several containers concurrently can flap an
|
|
304
|
+
# app's health (its health check depends on backends that would be restarting
|
|
305
|
+
# in parallel), leaving nginx with a live-but-unhealthy upstream and serving
|
|
306
|
+
# 502s during the deploy.
|
|
307
|
+
#
|
|
308
|
+
# Only image builds run ahead of deploys (below) — that is where the pipeline
|
|
309
|
+
# speedup comes from. A single container still fans out to its own servers in
|
|
310
|
+
# parallel (see ContainerDeployer), so multi-server containers stay fast.
|
|
309
311
|
def pipeline_build_and_deploy(configuration, deployer, build_server_pool, containers,
|
|
310
312
|
skip_build, skip_deploy, force_restart, skip_force_restart)
|
|
311
313
|
|
|
312
314
|
ordered_containers = build_deploy_order(containers)
|
|
313
315
|
|
|
314
|
-
deploy_queue
|
|
315
|
-
deploy_threads = {}
|
|
316
|
+
deploy_queue = Queue.new
|
|
316
317
|
|
|
317
318
|
consumer = Thread.new do
|
|
318
319
|
while (container = deploy_queue.pop) != :done
|
|
319
|
-
|
|
320
|
-
# dependents, so their deploy threads already exist here.
|
|
321
|
-
dependency_threads = container
|
|
322
|
-
.dependent_containers
|
|
323
|
-
.map { |dependency| deploy_threads[dependency] }
|
|
324
|
-
.compact
|
|
325
|
-
|
|
326
|
-
# Pass container/dependency_threads as thread arguments so each thread
|
|
327
|
-
# captures its own value — the shared loop variables get reassigned on
|
|
328
|
-
# the next iteration (eventually to :done) before these threads run.
|
|
329
|
-
deploy_threads[container] = Thread.new(container, dependency_threads) do |container, dependency_threads|
|
|
330
|
-
dependency_threads.each(&:join)
|
|
331
|
-
deploy_container(deployer, container, force_restart, skip_force_restart)
|
|
332
|
-
end
|
|
320
|
+
deploy_container(deployer, container, force_restart, skip_force_restart)
|
|
333
321
|
end
|
|
334
322
|
end
|
|
335
323
|
|
|
@@ -351,7 +339,6 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
351
339
|
|
|
352
340
|
deploy_queue.push(:done)
|
|
353
341
|
consumer.join
|
|
354
|
-
deploy_threads.values.each(&:join)
|
|
355
342
|
end
|
|
356
343
|
|
|
357
344
|
# Returns the containers to deploy in dependency order (each container's
|
|
@@ -467,11 +454,13 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
467
454
|
|
|
468
455
|
@logger.info("Updating crontab file #{deploy_user}:#{crontab_filepath}")
|
|
469
456
|
|
|
470
|
-
|
|
457
|
+
ssh_options = "-o BatchMode=yes -o ConnectTimeout=#{Indocker.ssh_connect_timeout}"
|
|
458
|
+
|
|
459
|
+
Indocker::Shell.command("scp #{ssh_options} #{tmp_crontab_file.path} #{deploy_user}:#{crontab_filepath}", @logger)
|
|
471
460
|
|
|
472
461
|
tmp_crontab_file.unlink
|
|
473
462
|
|
|
474
|
-
Indocker::Shell.command("ssh #{deploy_user} 'crontab #{crontab_filepath}'", @logger)
|
|
463
|
+
Indocker::Shell.command("ssh #{ssh_options} #{deploy_user} 'crontab #{crontab_filepath}'", @logger)
|
|
475
464
|
end
|
|
476
465
|
|
|
477
466
|
def sync_indocker(servers)
|
data/lib/indocker/rsync.rb
CHANGED
|
@@ -29,7 +29,20 @@ class Indocker::Rsync
|
|
|
29
29
|
sync_local_cp(from, to, raise_on_error: raise_on_error)
|
|
30
30
|
end
|
|
31
31
|
else
|
|
32
|
-
|
|
32
|
+
# BatchMode/ConnectTimeout: an unreachable server must fail in seconds with
|
|
33
|
+
# a clear rsync error instead of hanging silently (or prompting for a password)
|
|
34
|
+
# until the deployment-wide remote operation timeout kills the whole deploy.
|
|
35
|
+
# ServerAlive*: a path that accepts the connection and then stops delivering
|
|
36
|
+
# packets (seen when a cloud NAT address half-works) would otherwise hang
|
|
37
|
+
# just as long — keepalives tear such a session down in ~30s.
|
|
38
|
+
ssh_command = [
|
|
39
|
+
"ssh -p #{session.port}",
|
|
40
|
+
"-o BatchMode=yes",
|
|
41
|
+
"-o ConnectTimeout=#{Indocker.ssh_connect_timeout}",
|
|
42
|
+
"-o ServerAliveInterval=10",
|
|
43
|
+
"-o ServerAliveCountMax=3",
|
|
44
|
+
].join(' ')
|
|
45
|
+
command = "rsync --delete-after -a -e '#{ssh_command}' #{from} #{session.user}@#{session.host}:#{to}"
|
|
33
46
|
Indocker.logger.debug("sync #{from} #{session.user}@#{session.host}:#{to}")
|
|
34
47
|
Indocker::Shell.command(command, Indocker.logger, raise_on_error: raise_on_error)
|
|
35
48
|
end
|
data/lib/indocker/ssh_session.rb
CHANGED
|
@@ -29,7 +29,14 @@ class Indocker::SshSession
|
|
|
29
29
|
|
|
30
30
|
if host != LOCALHOST
|
|
31
31
|
require 'net/ssh'
|
|
32
|
-
@ssh = Net::SSH.start(@host, @user, {
|
|
32
|
+
@ssh = Net::SSH.start(@host, @user, {
|
|
33
|
+
port: @port,
|
|
34
|
+
non_interactive: true,
|
|
35
|
+
timeout: Indocker.ssh_connect_timeout,
|
|
36
|
+
keepalive: true,
|
|
37
|
+
keepalive_interval: 10,
|
|
38
|
+
keepalive_maxcount: 3,
|
|
39
|
+
})
|
|
33
40
|
end
|
|
34
41
|
end
|
|
35
42
|
|
data/lib/indocker/version.rb
CHANGED
data/lib/indocker.rb
CHANGED
|
@@ -135,6 +135,22 @@ module Indocker
|
|
|
135
135
|
@redeploy_crontab_path = val
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
def set_remote_operation_timeout(seconds)
|
|
139
|
+
@remote_operation_timeout = seconds
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def remote_operation_timeout
|
|
143
|
+
@remote_operation_timeout || 300
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def set_ssh_connect_timeout(seconds)
|
|
147
|
+
@ssh_connect_timeout = seconds
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def ssh_connect_timeout
|
|
151
|
+
@ssh_connect_timeout || 15
|
|
152
|
+
end
|
|
153
|
+
|
|
138
154
|
def redeploy_crontab_path
|
|
139
155
|
@redeploy_crontab_path
|
|
140
156
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: indocker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ruslan Gatiyatov
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-08-18 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: net-ssh
|