indocker 0.5.4 → 0.6.2
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/artifacts/services/synchronizer.rb +2 -9
- data/lib/indocker/deployment_checker.rb +1 -6
- data/lib/indocker/launchers/configuration_deployer.rb +38 -29
- data/lib/indocker/launchers/dto/remote_operation_dto.rb +18 -1
- data/lib/indocker/rsync.rb +12 -1
- data/lib/indocker/server.rb +6 -2
- data/lib/indocker/server_pools/server_connection.rb +1 -6
- data/lib/indocker/ssh_session.rb +75 -4
- data/lib/indocker/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10915e6da5c1e8d2899b510a223d8761a0542880776b61e8078d38d23e261b28
|
|
4
|
+
data.tar.gz: dd047c45c09dd0f26b95ae668f00266c2f45c46ff1c4aecff14ef0155d9e5a59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b879a49d9defa1cbe4dc56685e0467ff070c9d0310a6fd5531766c941e7b51028aa1ace5817dec1638dec944ccba8afbbceffa062095a658fdb248c615095253
|
|
7
|
+
data.tar.gz: 70814bb24a5a66da13c36a6d1350403686b6a5672f184aa691179cfb40850b74e66b059fc82f249fc052ebe942c2ed26326570ce041270d561f25ca7bb67021c
|
data/Gemfile.lock
CHANGED
|
@@ -13,14 +13,9 @@ class Indocker::Artifacts::Services::Synchronizer
|
|
|
13
13
|
remote_operations += servers.map do |server|
|
|
14
14
|
@progress.start_syncing_artifact(server, artifact)
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :artifact_sync) do
|
|
17
17
|
server.synchronize do
|
|
18
|
-
session = Indocker::SshSession.
|
|
19
|
-
host: server.host,
|
|
20
|
-
user: server.user,
|
|
21
|
-
port: server.port,
|
|
22
|
-
logger: @logger
|
|
23
|
-
)
|
|
18
|
+
session = Indocker::SshSession.for(server, logger: @logger)
|
|
24
19
|
|
|
25
20
|
if artifact.is_git?
|
|
26
21
|
@logger.info("Pulling git artifact #{artifact.name.to_s.green} for #{server.user}@#{server.host}")
|
|
@@ -49,8 +44,6 @@ class Indocker::Artifacts::Services::Synchronizer
|
|
|
49
44
|
@progress.finish_syncing_artifact(server, artifact)
|
|
50
45
|
end
|
|
51
46
|
end
|
|
52
|
-
|
|
53
|
-
Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :artifact_sync)
|
|
54
47
|
end
|
|
55
48
|
end
|
|
56
49
|
|
|
@@ -58,12 +58,7 @@ class Indocker::DeploymentChecker
|
|
|
58
58
|
total_missing_containers = []
|
|
59
59
|
|
|
60
60
|
server_list.each do |server|
|
|
61
|
-
session = Indocker::SshSession.
|
|
62
|
-
host: server.host,
|
|
63
|
-
user: server.user,
|
|
64
|
-
port: server.port,
|
|
65
|
-
logger: @debug_logger
|
|
66
|
-
)
|
|
61
|
+
session = Indocker::SshSession.for(server, logger: @debug_logger)
|
|
67
62
|
|
|
68
63
|
result = session.exec!('docker ps --filter="status=running" --format="{{.Names}}"')
|
|
69
64
|
|
|
@@ -46,6 +46,9 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
46
46
|
deployer = Indocker::ContainerDeployer.new(configuration: configuration, logger: @logger)
|
|
47
47
|
|
|
48
48
|
@global_logger.info("Establishing ssh sessions to all servers...")
|
|
49
|
+
|
|
50
|
+
warm_jump_hosts(deploy_servers + build_servers)
|
|
51
|
+
|
|
49
52
|
build_server_pool.create_sessions!
|
|
50
53
|
|
|
51
54
|
build_servers = configuration
|
|
@@ -103,10 +106,36 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
103
106
|
|
|
104
107
|
private
|
|
105
108
|
|
|
109
|
+
# Opens the shared connection to each jump host before anything runs in parallel, so the
|
|
110
|
+
# fan-out reuses it instead of racing to create one connection per operation.
|
|
111
|
+
def warm_jump_hosts(servers)
|
|
112
|
+
servers.map(&:jump_host).compact.uniq.each do |jump_host|
|
|
113
|
+
@logger.info("Establishing shared connection to jump host #{jump_host}")
|
|
114
|
+
Indocker::SshSession.warm_jump_host(jump_host, logger: @logger)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
106
118
|
def wait_remote_operations(remote_operations)
|
|
107
119
|
remote_operations.each do |remote_operation|
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
server = remote_operation.server
|
|
121
|
+
target = "#{server.name} (#{server.user}@#{server.host})"
|
|
122
|
+
|
|
123
|
+
begin
|
|
124
|
+
if remote_operation.thread.join(Indocker.remote_operation_timeout).nil?
|
|
125
|
+
@global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{target} did not finish during #{Indocker.remote_operation_timeout} seconds.")
|
|
126
|
+
exit 1
|
|
127
|
+
end
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
# Raised by the operation thread and re-raised here by #join, so the server
|
|
130
|
+
# that failed can be named instead of dumping a bare backtrace.
|
|
131
|
+
reason = e.message.to_s.empty? || e.message == e.class.name ? e.class.to_s : "#{e.class}: #{e.message}"
|
|
132
|
+
|
|
133
|
+
@global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{target} failed: #{reason}")
|
|
134
|
+
|
|
135
|
+
if e.is_a?(Errno::ETIMEDOUT) || (defined?(Net::SSH::ConnectionTimeout) && e.is_a?(Net::SSH::ConnectionTimeout))
|
|
136
|
+
@global_logger.error("Server #{server.host} did not accept a connection within #{Indocker.ssh_connect_timeout} seconds. It is powered off, or its address does not accept TCP.")
|
|
137
|
+
end
|
|
138
|
+
|
|
110
139
|
exit 1
|
|
111
140
|
end
|
|
112
141
|
end
|
|
@@ -380,13 +409,8 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
380
409
|
remote_operations += repositories.map do |alias_name, repository|
|
|
381
410
|
@progress.start_syncing_repository(server, alias_name)
|
|
382
411
|
|
|
383
|
-
|
|
384
|
-
session = Indocker::SshSession.
|
|
385
|
-
host: server.host,
|
|
386
|
-
user: server.user,
|
|
387
|
-
port: server.port,
|
|
388
|
-
logger: @logger
|
|
389
|
-
)
|
|
412
|
+
Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :repository_pull) do
|
|
413
|
+
session = Indocker::SshSession.for(server, logger: @logger)
|
|
390
414
|
|
|
391
415
|
if repository.is_local?
|
|
392
416
|
@logger.info("Rsyncing repository :#{alias_name} from #{repository.root_path} to #{server.user}@#{server.host}:#{repository.clone_path}")
|
|
@@ -415,8 +439,6 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
415
439
|
|
|
416
440
|
@progress.finish_syncing_repository(server, alias_name)
|
|
417
441
|
end
|
|
418
|
-
|
|
419
|
-
Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :repository_pull)
|
|
420
442
|
end
|
|
421
443
|
end
|
|
422
444
|
|
|
@@ -455,6 +477,7 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
455
477
|
@logger.info("Updating crontab file #{deploy_user}:#{crontab_filepath}")
|
|
456
478
|
|
|
457
479
|
ssh_options = "-o BatchMode=yes -o ConnectTimeout=#{Indocker.ssh_connect_timeout}"
|
|
480
|
+
ssh_options = "#{ssh_options} -J #{server.jump_host}" if server.jump_host
|
|
458
481
|
|
|
459
482
|
Indocker::Shell.command("scp #{ssh_options} #{tmp_crontab_file.path} #{deploy_user}:#{crontab_filepath}", @logger)
|
|
460
483
|
|
|
@@ -467,13 +490,8 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
467
490
|
servers.map do |server|
|
|
468
491
|
@progress.start_syncing_binaries(server)
|
|
469
492
|
|
|
470
|
-
|
|
471
|
-
session = Indocker::SshSession.
|
|
472
|
-
host: server.host,
|
|
473
|
-
user: server.user,
|
|
474
|
-
port: server.port,
|
|
475
|
-
logger: @logger
|
|
476
|
-
)
|
|
493
|
+
Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :indocker_sync) do
|
|
494
|
+
session = Indocker::SshSession.for(server, logger: @logger)
|
|
477
495
|
|
|
478
496
|
sync_path = Indocker::IndockerHelper.indocker_dir
|
|
479
497
|
@logger.info("Syncing indocker to #{server.user}@#{server.host}:#{sync_path}")
|
|
@@ -488,8 +506,6 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
488
506
|
|
|
489
507
|
@progress.finish_syncing_binaries(server)
|
|
490
508
|
end
|
|
491
|
-
|
|
492
|
-
Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :indocker_sync)
|
|
493
509
|
end
|
|
494
510
|
end
|
|
495
511
|
|
|
@@ -500,17 +516,12 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
500
516
|
remote_operations += env_files.map do |alias_name, env_file|
|
|
501
517
|
@progress.start_syncing_env_file(server, alias_name)
|
|
502
518
|
|
|
503
|
-
|
|
519
|
+
Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :env_file_sync) do
|
|
504
520
|
if env_file.is_a?(Indocker::EnvFiles::Local)
|
|
505
521
|
sync_path = File.join(Indocker.deploy_dir, 'env_files', File.basename(env_file.path))
|
|
506
522
|
@logger.info("Syncing env file :#{alias_name} from #{env_file.path} to #{server.user}@#{server.host}:#{sync_path}")
|
|
507
523
|
|
|
508
|
-
session = Indocker::SshSession.
|
|
509
|
-
host: server.host,
|
|
510
|
-
user: server.user,
|
|
511
|
-
port: server.port,
|
|
512
|
-
logger: @logger
|
|
513
|
-
)
|
|
524
|
+
session = Indocker::SshSession.for(server, logger: @logger)
|
|
514
525
|
|
|
515
526
|
session.exec!("mkdir -p #{Indocker::EnvFileHelper.folder}")
|
|
516
527
|
|
|
@@ -529,8 +540,6 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
529
540
|
|
|
530
541
|
@progress.finish_syncing_env_file(server, alias_name)
|
|
531
542
|
end
|
|
532
|
-
|
|
533
|
-
Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :env_file_sync)
|
|
534
543
|
end
|
|
535
544
|
end
|
|
536
545
|
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
class Indocker::Launchers::DTO::RemoteOperationDTO
|
|
2
2
|
attr_reader :thread, :server, :operation, :message
|
|
3
3
|
|
|
4
|
+
# Runs a remote operation in a thread that keeps its failure to itself, so that
|
|
5
|
+
# wait_remote_operations can re-raise it on join and report which server failed.
|
|
6
|
+
# Left to the global abort_on_exception/report_on_exception defaults, a failed ssh
|
|
7
|
+
# dumps a raw backtrace and tears the deploy down from inside the thread, naming
|
|
8
|
+
# no server at all. The flags are set inside the thread so that they are in place
|
|
9
|
+
# before the operation gets a chance to fail.
|
|
10
|
+
def self.spawn(server, operation, message = nil, &block)
|
|
11
|
+
thread = Thread.new do
|
|
12
|
+
Thread.current.abort_on_exception = false
|
|
13
|
+
Thread.current.report_on_exception = false
|
|
14
|
+
|
|
15
|
+
block.call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
new(thread, server, operation, message)
|
|
19
|
+
end
|
|
20
|
+
|
|
4
21
|
def initialize(thread, server, operation, message = nil)
|
|
5
22
|
@thread = thread
|
|
6
23
|
@server = server
|
|
7
24
|
@operation = operation
|
|
8
25
|
@message = message
|
|
9
26
|
end
|
|
10
|
-
end
|
|
27
|
+
end
|
data/lib/indocker/rsync.rb
CHANGED
|
@@ -41,7 +41,18 @@ class Indocker::Rsync
|
|
|
41
41
|
"-o ConnectTimeout=#{Indocker.ssh_connect_timeout}",
|
|
42
42
|
"-o ServerAliveInterval=10",
|
|
43
43
|
"-o ServerAliveCountMax=3",
|
|
44
|
-
]
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Tunnel through the jump host the server is defined with, so that a server
|
|
47
|
+
# reachable only over its internal address syncs like any other one. ProxyCommand
|
|
48
|
+
# rather than -J: it is the same tunnel every other remote operation uses, including
|
|
49
|
+
# the shared connection that keeps a parallel sync from exhausting the jump host's
|
|
50
|
+
# MaxStartups.
|
|
51
|
+
if session.jump_host
|
|
52
|
+
ssh_command << %Q{-o ProxyCommand="#{Indocker::SshSession.jump_proxy_command(session.jump_host)}"}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ssh_command = ssh_command.join(' ')
|
|
45
56
|
command = "rsync --delete-after -a -e '#{ssh_command}' #{from} #{session.user}@#{session.host}:#{to}"
|
|
46
57
|
Indocker.logger.debug("sync #{from} #{session.user}@#{session.host}:#{to}")
|
|
47
58
|
Indocker::Shell.command(command, Indocker.logger, raise_on_error: raise_on_error)
|
data/lib/indocker/server.rb
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
class Indocker::Server
|
|
2
2
|
include Indocker::Concerns::Inspectable
|
|
3
3
|
|
|
4
|
-
attr_reader :name, :host, :user, :port
|
|
4
|
+
attr_reader :name, :host, :user, :port, :jump_host
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# jump_host ("user@host") makes every connection to this server tunnel through
|
|
7
|
+
# another one. Servers whose public address is unreachable can still be deployed
|
|
8
|
+
# to over their internal address that way.
|
|
9
|
+
def initialize(name:, host:, user:, port:, jump_host: nil)
|
|
7
10
|
@name = name
|
|
8
11
|
@host = host
|
|
9
12
|
@user = user
|
|
10
13
|
@port = port
|
|
14
|
+
@jump_host = jump_host
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def ==(value)
|
|
@@ -10,12 +10,7 @@ class Indocker::ServerPools::ServerConnection
|
|
|
10
10
|
def create_session!
|
|
11
11
|
return unless @server
|
|
12
12
|
|
|
13
|
-
@session = Indocker::SshSession.
|
|
14
|
-
host: @server.host,
|
|
15
|
-
user: @server.user,
|
|
16
|
-
port: @server.port,
|
|
17
|
-
logger: @logger
|
|
18
|
-
)
|
|
13
|
+
@session = Indocker::SshSession.for(@server, logger: @logger)
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
def exec!(command)
|
data/lib/indocker/ssh_session.rb
CHANGED
|
@@ -2,6 +2,7 @@ require_relative 'shell'
|
|
|
2
2
|
|
|
3
3
|
class Indocker::SshSession
|
|
4
4
|
LOCALHOST = 'localhost'.freeze
|
|
5
|
+
JUMP_CONTROL_PERSIST = '600'.freeze
|
|
5
6
|
|
|
6
7
|
class ExecResult
|
|
7
8
|
attr_reader :stdout_data, :stderr_data, :exit_code, :exit_signal
|
|
@@ -19,24 +20,94 @@ class Indocker::SshSession
|
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
attr_reader :host, :user, :port, :logger
|
|
23
|
+
attr_reader :host, :user, :port, :logger, :jump_host
|
|
24
|
+
|
|
25
|
+
# Builds the tunnel to a jump host, shared by Net::SSH sessions and by rsync.
|
|
26
|
+
#
|
|
27
|
+
# Net::SSH::Proxy::Jump would build a bare `ssh -W`: a jump host that asks for a password
|
|
28
|
+
# hangs the deploy, and probing a server whose sshd is still starting prints
|
|
29
|
+
# "channel 0: open failed" from the ssh client on every retry.
|
|
30
|
+
#
|
|
31
|
+
# Multiplexing matters even more. A deploy syncs and deploys to every server in parallel,
|
|
32
|
+
# and each of those opens its own tunnel; sshd drops everything above MaxStartups (10 by
|
|
33
|
+
# default), which surfaces as "connection closed by remote host" on random servers. Sharing
|
|
34
|
+
# one connection to the jump host keeps the whole fan-out down to a single tunnel.
|
|
35
|
+
def self.jump_proxy_command(jump_host)
|
|
36
|
+
require 'digest'
|
|
37
|
+
|
|
38
|
+
host = jump_host
|
|
39
|
+
port = nil
|
|
40
|
+
|
|
41
|
+
if host =~ /\A(.+):(\d+)\z/
|
|
42
|
+
host = $1
|
|
43
|
+
port = $2
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
control_path = "/tmp/indocker-jump-#{Digest::MD5.hexdigest(jump_host)[0, 10]}"
|
|
47
|
+
|
|
48
|
+
command = [
|
|
49
|
+
"ssh -q",
|
|
50
|
+
"-o BatchMode=yes",
|
|
51
|
+
"-o ConnectTimeout=#{Indocker.ssh_connect_timeout}",
|
|
52
|
+
"-o ControlMaster=auto",
|
|
53
|
+
"-o ControlPath=#{control_path}",
|
|
54
|
+
"-o ControlPersist=#{JUMP_CONTROL_PERSIST}",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
command << "-p #{port}" if port
|
|
58
|
+
command << "-W %h:%p"
|
|
59
|
+
command << host
|
|
23
60
|
|
|
24
|
-
|
|
61
|
+
command.join(' ')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Opens the shared connection to a jump host up front. Without it the first wave of
|
|
65
|
+
# parallel operations races to create the master connection and opens a burst of them.
|
|
66
|
+
def self.warm_jump_host(jump_host, logger:)
|
|
67
|
+
command = jump_proxy_command(jump_host).sub("-W %h:%p ", "")
|
|
68
|
+
|
|
69
|
+
Indocker::Shell.command("#{command} true", logger)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Opens a session to a server as it is defined in the configuration, including its
|
|
73
|
+
# jump host, if any. Every remote operation goes through here, so a server reached
|
|
74
|
+
# through a jump host is reached that way by binaries sync, container deploys and
|
|
75
|
+
# the deployment checker alike.
|
|
76
|
+
def self.for(server, logger:)
|
|
77
|
+
new(
|
|
78
|
+
host: server.host,
|
|
79
|
+
user: server.user,
|
|
80
|
+
port: server.port,
|
|
81
|
+
jump_host: server.jump_host,
|
|
82
|
+
logger: logger
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def initialize(host:, user:, port:, logger:, jump_host: nil)
|
|
25
87
|
@host = host
|
|
26
88
|
@user = user
|
|
27
89
|
@port = port
|
|
90
|
+
@jump_host = jump_host
|
|
28
91
|
@logger = logger
|
|
29
92
|
|
|
30
93
|
if host != LOCALHOST
|
|
31
94
|
require 'net/ssh'
|
|
32
|
-
|
|
95
|
+
|
|
96
|
+
options = {
|
|
33
97
|
port: @port,
|
|
34
98
|
non_interactive: true,
|
|
35
99
|
timeout: Indocker.ssh_connect_timeout,
|
|
36
100
|
keepalive: true,
|
|
37
101
|
keepalive_interval: 10,
|
|
38
102
|
keepalive_maxcount: 3,
|
|
39
|
-
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if @jump_host
|
|
106
|
+
require 'net/ssh/proxy/command'
|
|
107
|
+
options[:proxy] = Net::SSH::Proxy::Command.new(self.class.jump_proxy_command(@jump_host))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@ssh = Net::SSH.start(@host, @user, options)
|
|
40
111
|
end
|
|
41
112
|
end
|
|
42
113
|
|
data/lib/indocker/version.rb
CHANGED