indocker 0.5.3 → 0.6.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/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 +28 -35
- data/lib/indocker/launchers/dto/remote_operation_dto.rb +18 -1
- data/lib/indocker/rsync.rb +20 -1
- data/lib/indocker/server.rb +6 -2
- data/lib/indocker/server_pools/server_connection.rb +1 -6
- data/lib/indocker/ssh_session.rb +59 -3
- 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: 0eda49208a82e456326a838e76cab5152673a078e30668832c97c7c1b525a4fa
|
|
4
|
+
data.tar.gz: 5904dabfde8e6110821b104be6fd0809799a5deb3b35f991bed33627323bf1c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '020961f0cae0f634adf95f355d2e537bca061ec0380294f0a12ca75d0e72b54f19eb57d357dbe1f1dbef50cfd5d43896ed7f80107fe7ac822e6d3addd2645586'
|
|
7
|
+
data.tar.gz: f9ff2feb5c3288474635a3e7d31354c8065075068872865f51b6542e1c6933c0342e253260c58492cc7eb99e3ae09e1904692a9b1acd5a13751d467ca0edaf5d
|
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
|
|
|
@@ -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,25 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
107
105
|
|
|
108
106
|
def wait_remote_operations(remote_operations)
|
|
109
107
|
remote_operations.each do |remote_operation|
|
|
108
|
+
server = remote_operation.server
|
|
109
|
+
target = "#{server.name} (#{server.user}@#{server.host})"
|
|
110
|
+
|
|
110
111
|
begin
|
|
111
|
-
|
|
112
|
-
remote_operation.
|
|
112
|
+
if remote_operation.thread.join(Indocker.remote_operation_timeout).nil?
|
|
113
|
+
@global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{target} did not finish during #{Indocker.remote_operation_timeout} seconds.")
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
# Raised by the operation thread and re-raised here by #join, so the server
|
|
118
|
+
# that failed can be named instead of dumping a bare backtrace.
|
|
119
|
+
reason = e.message.to_s.empty? || e.message == e.class.name ? e.class.to_s : "#{e.class}: #{e.message}"
|
|
120
|
+
|
|
121
|
+
@global_logger.error("Deployment aborted. Remote operation :#{remote_operation.operation} on server #{target} failed: #{reason}")
|
|
122
|
+
|
|
123
|
+
if e.is_a?(Errno::ETIMEDOUT) || (defined?(Net::SSH::ConnectionTimeout) && e.is_a?(Net::SSH::ConnectionTimeout))
|
|
124
|
+
@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.")
|
|
113
125
|
end
|
|
114
|
-
|
|
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.")
|
|
126
|
+
|
|
116
127
|
exit 1
|
|
117
128
|
end
|
|
118
129
|
end
|
|
@@ -386,13 +397,8 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
386
397
|
remote_operations += repositories.map do |alias_name, repository|
|
|
387
398
|
@progress.start_syncing_repository(server, alias_name)
|
|
388
399
|
|
|
389
|
-
|
|
390
|
-
session = Indocker::SshSession.
|
|
391
|
-
host: server.host,
|
|
392
|
-
user: server.user,
|
|
393
|
-
port: server.port,
|
|
394
|
-
logger: @logger
|
|
395
|
-
)
|
|
400
|
+
Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :repository_pull) do
|
|
401
|
+
session = Indocker::SshSession.for(server, logger: @logger)
|
|
396
402
|
|
|
397
403
|
if repository.is_local?
|
|
398
404
|
@logger.info("Rsyncing repository :#{alias_name} from #{repository.root_path} to #{server.user}@#{server.host}:#{repository.clone_path}")
|
|
@@ -421,8 +427,6 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
421
427
|
|
|
422
428
|
@progress.finish_syncing_repository(server, alias_name)
|
|
423
429
|
end
|
|
424
|
-
|
|
425
|
-
Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :repository_pull)
|
|
426
430
|
end
|
|
427
431
|
end
|
|
428
432
|
|
|
@@ -460,24 +464,22 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
460
464
|
|
|
461
465
|
@logger.info("Updating crontab file #{deploy_user}:#{crontab_filepath}")
|
|
462
466
|
|
|
463
|
-
|
|
467
|
+
ssh_options = "-o BatchMode=yes -o ConnectTimeout=#{Indocker.ssh_connect_timeout}"
|
|
468
|
+
ssh_options = "#{ssh_options} -J #{server.jump_host}" if server.jump_host
|
|
469
|
+
|
|
470
|
+
Indocker::Shell.command("scp #{ssh_options} #{tmp_crontab_file.path} #{deploy_user}:#{crontab_filepath}", @logger)
|
|
464
471
|
|
|
465
472
|
tmp_crontab_file.unlink
|
|
466
473
|
|
|
467
|
-
Indocker::Shell.command("ssh #{deploy_user} 'crontab #{crontab_filepath}'", @logger)
|
|
474
|
+
Indocker::Shell.command("ssh #{ssh_options} #{deploy_user} 'crontab #{crontab_filepath}'", @logger)
|
|
468
475
|
end
|
|
469
476
|
|
|
470
477
|
def sync_indocker(servers)
|
|
471
478
|
servers.map do |server|
|
|
472
479
|
@progress.start_syncing_binaries(server)
|
|
473
480
|
|
|
474
|
-
|
|
475
|
-
session = Indocker::SshSession.
|
|
476
|
-
host: server.host,
|
|
477
|
-
user: server.user,
|
|
478
|
-
port: server.port,
|
|
479
|
-
logger: @logger
|
|
480
|
-
)
|
|
481
|
+
Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :indocker_sync) do
|
|
482
|
+
session = Indocker::SshSession.for(server, logger: @logger)
|
|
481
483
|
|
|
482
484
|
sync_path = Indocker::IndockerHelper.indocker_dir
|
|
483
485
|
@logger.info("Syncing indocker to #{server.user}@#{server.host}:#{sync_path}")
|
|
@@ -492,8 +494,6 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
492
494
|
|
|
493
495
|
@progress.finish_syncing_binaries(server)
|
|
494
496
|
end
|
|
495
|
-
|
|
496
|
-
Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :indocker_sync)
|
|
497
497
|
end
|
|
498
498
|
end
|
|
499
499
|
|
|
@@ -504,17 +504,12 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
504
504
|
remote_operations += env_files.map do |alias_name, env_file|
|
|
505
505
|
@progress.start_syncing_env_file(server, alias_name)
|
|
506
506
|
|
|
507
|
-
|
|
507
|
+
Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :env_file_sync) do
|
|
508
508
|
if env_file.is_a?(Indocker::EnvFiles::Local)
|
|
509
509
|
sync_path = File.join(Indocker.deploy_dir, 'env_files', File.basename(env_file.path))
|
|
510
510
|
@logger.info("Syncing env file :#{alias_name} from #{env_file.path} to #{server.user}@#{server.host}:#{sync_path}")
|
|
511
511
|
|
|
512
|
-
session = Indocker::SshSession.
|
|
513
|
-
host: server.host,
|
|
514
|
-
user: server.user,
|
|
515
|
-
port: server.port,
|
|
516
|
-
logger: @logger
|
|
517
|
-
)
|
|
512
|
+
session = Indocker::SshSession.for(server, logger: @logger)
|
|
518
513
|
|
|
519
514
|
session.exec!("mkdir -p #{Indocker::EnvFileHelper.folder}")
|
|
520
515
|
|
|
@@ -533,8 +528,6 @@ class Indocker::Launchers::ConfigurationDeployer
|
|
|
533
528
|
|
|
534
529
|
@progress.finish_syncing_env_file(server, alias_name)
|
|
535
530
|
end
|
|
536
|
-
|
|
537
|
-
Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :env_file_sync)
|
|
538
531
|
end
|
|
539
532
|
end
|
|
540
533
|
|
|
@@ -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
|
@@ -29,7 +29,26 @@ 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
|
+
]
|
|
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.
|
|
48
|
+
ssh_command << "-J #{session.jump_host}" if session.jump_host
|
|
49
|
+
|
|
50
|
+
ssh_command = ssh_command.join(' ')
|
|
51
|
+
command = "rsync --delete-after -a -e '#{ssh_command}' #{from} #{session.user}@#{session.host}:#{to}"
|
|
33
52
|
Indocker.logger.debug("sync #{from} #{session.user}@#{session.host}:#{to}")
|
|
34
53
|
Indocker::Shell.command(command, Indocker.logger, raise_on_error: raise_on_error)
|
|
35
54
|
end
|
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
|
@@ -19,17 +19,47 @@ class Indocker::SshSession
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
attr_reader :host, :user, :port, :logger
|
|
22
|
+
attr_reader :host, :user, :port, :logger, :jump_host
|
|
23
|
+
|
|
24
|
+
# Opens a session to a server as it is defined in the configuration, including its
|
|
25
|
+
# jump host, if any. Every remote operation goes through here, so a server reached
|
|
26
|
+
# through a jump host is reached that way by binaries sync, container deploys and
|
|
27
|
+
# the deployment checker alike.
|
|
28
|
+
def self.for(server, logger:)
|
|
29
|
+
new(
|
|
30
|
+
host: server.host,
|
|
31
|
+
user: server.user,
|
|
32
|
+
port: server.port,
|
|
33
|
+
jump_host: server.jump_host,
|
|
34
|
+
logger: logger
|
|
35
|
+
)
|
|
36
|
+
end
|
|
23
37
|
|
|
24
|
-
def initialize(host:, user:, port:, logger:)
|
|
38
|
+
def initialize(host:, user:, port:, logger:, jump_host: nil)
|
|
25
39
|
@host = host
|
|
26
40
|
@user = user
|
|
27
41
|
@port = port
|
|
42
|
+
@jump_host = jump_host
|
|
28
43
|
@logger = logger
|
|
29
44
|
|
|
30
45
|
if host != LOCALHOST
|
|
31
46
|
require 'net/ssh'
|
|
32
|
-
|
|
47
|
+
|
|
48
|
+
options = {
|
|
49
|
+
port: @port,
|
|
50
|
+
non_interactive: true,
|
|
51
|
+
timeout: Indocker.ssh_connect_timeout,
|
|
52
|
+
keepalive: true,
|
|
53
|
+
keepalive_interval: 10,
|
|
54
|
+
keepalive_maxcount: 3,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if @jump_host
|
|
58
|
+
require 'net/ssh/proxy/command'
|
|
59
|
+
options[:proxy] = Net::SSH::Proxy::Command.new(jump_proxy_command)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@ssh = Net::SSH.start(@host, @user, options)
|
|
33
63
|
end
|
|
34
64
|
end
|
|
35
65
|
|
|
@@ -52,6 +82,32 @@ class Indocker::SshSession
|
|
|
52
82
|
end
|
|
53
83
|
|
|
54
84
|
private
|
|
85
|
+
# Net::SSH::Proxy::Jump builds this same `ssh -W` tunnel, but without BatchMode,
|
|
86
|
+
# connect timeout or -q: a jump host that asks for a password hangs the deploy, and
|
|
87
|
+
# every probe of a server whose sshd is still starting prints "channel 0: open failed"
|
|
88
|
+
# from the ssh client.
|
|
89
|
+
def jump_proxy_command
|
|
90
|
+
host = @jump_host
|
|
91
|
+
port = nil
|
|
92
|
+
|
|
93
|
+
if host =~ /\A(.+):(\d+)\z/
|
|
94
|
+
host = $1
|
|
95
|
+
port = $2
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
command = [
|
|
99
|
+
"ssh -q",
|
|
100
|
+
"-o BatchMode=yes",
|
|
101
|
+
"-o ConnectTimeout=#{Indocker.ssh_connect_timeout}",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
command << "-p #{port}" if port
|
|
105
|
+
command << "-W %h:%p"
|
|
106
|
+
command << host
|
|
107
|
+
|
|
108
|
+
command.join(' ')
|
|
109
|
+
end
|
|
110
|
+
|
|
55
111
|
def exec_locally!(command)
|
|
56
112
|
res = Indocker::Shell.command_with_result(command, @logger, skip_logging: false)
|
|
57
113
|
ExecResult.new(res.stdout, '', res.exit_status, nil)
|
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.
|
|
4
|
+
version: 0.6.1
|
|
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
|