indocker 0.5.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70bf1241f0783dede869906f9f359c2a2137778a3059acaea9ae2fc7db30bb38
4
- data.tar.gz: f4d906fc4ae36b9bf4482c7e5802c03b4a8633c7f839a0ffb71bbfc1571a5e30
3
+ metadata.gz: 0eda49208a82e456326a838e76cab5152673a078e30668832c97c7c1b525a4fa
4
+ data.tar.gz: 5904dabfde8e6110821b104be6fd0809799a5deb3b35f991bed33627323bf1c9
5
5
  SHA512:
6
- metadata.gz: d45a4d6ab954e10b76fbaf2b4ca098c1368fd6b6365756f24bb210757d5f221d0fcfcf5b11b378cc5e31de38d72967158c90d91aaa3c9df53334f8a2596f12f0
7
- data.tar.gz: a0ae86f49931e812d34761ff20f73f704a010b742edb1968dae8e70fc7798edbd75cab519f34632d8f6840ab0b87f5454c70773c50627ac10fc5693e263819cb
6
+ metadata.gz: '020961f0cae0f634adf95f355d2e537bca061ec0380294f0a12ca75d0e72b54f19eb57d357dbe1f1dbef50cfd5d43896ed7f80107fe7ac822e6d3addd2645586'
7
+ data.tar.gz: f9ff2feb5c3288474635a3e7d31354c8065075068872865f51b6542e1c6933c0342e253260c58492cc7eb99e3ae09e1904692a9b1acd5a13751d467ca0edaf5d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- indocker (0.5.4)
4
+ indocker (0.6.1)
5
5
  net-ssh
6
6
 
7
7
  GEM
@@ -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
- thread = Thread.new do
16
+ Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :artifact_sync) do
17
17
  server.synchronize do
18
- session = Indocker::SshSession.new(
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.new(
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
 
@@ -105,8 +105,25 @@ class Indocker::Launchers::ConfigurationDeployer
105
105
 
106
106
  def wait_remote_operations(remote_operations)
107
107
  remote_operations.each do |remote_operation|
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.")
108
+ server = remote_operation.server
109
+ target = "#{server.name} (#{server.user}@#{server.host})"
110
+
111
+ begin
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.")
125
+ end
126
+
110
127
  exit 1
111
128
  end
112
129
  end
@@ -380,13 +397,8 @@ class Indocker::Launchers::ConfigurationDeployer
380
397
  remote_operations += repositories.map do |alias_name, repository|
381
398
  @progress.start_syncing_repository(server, alias_name)
382
399
 
383
- thread = Thread.new do
384
- session = Indocker::SshSession.new(
385
- host: server.host,
386
- user: server.user,
387
- port: server.port,
388
- logger: @logger
389
- )
400
+ Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :repository_pull) do
401
+ session = Indocker::SshSession.for(server, logger: @logger)
390
402
 
391
403
  if repository.is_local?
392
404
  @logger.info("Rsyncing repository :#{alias_name} from #{repository.root_path} to #{server.user}@#{server.host}:#{repository.clone_path}")
@@ -415,8 +427,6 @@ class Indocker::Launchers::ConfigurationDeployer
415
427
 
416
428
  @progress.finish_syncing_repository(server, alias_name)
417
429
  end
418
-
419
- Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :repository_pull)
420
430
  end
421
431
  end
422
432
 
@@ -455,6 +465,7 @@ class Indocker::Launchers::ConfigurationDeployer
455
465
  @logger.info("Updating crontab file #{deploy_user}:#{crontab_filepath}")
456
466
 
457
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
458
469
 
459
470
  Indocker::Shell.command("scp #{ssh_options} #{tmp_crontab_file.path} #{deploy_user}:#{crontab_filepath}", @logger)
460
471
 
@@ -467,13 +478,8 @@ class Indocker::Launchers::ConfigurationDeployer
467
478
  servers.map do |server|
468
479
  @progress.start_syncing_binaries(server)
469
480
 
470
- thread = Thread.new do
471
- session = Indocker::SshSession.new(
472
- host: server.host,
473
- user: server.user,
474
- port: server.port,
475
- logger: @logger
476
- )
481
+ Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :indocker_sync) do
482
+ session = Indocker::SshSession.for(server, logger: @logger)
477
483
 
478
484
  sync_path = Indocker::IndockerHelper.indocker_dir
479
485
  @logger.info("Syncing indocker to #{server.user}@#{server.host}:#{sync_path}")
@@ -488,8 +494,6 @@ class Indocker::Launchers::ConfigurationDeployer
488
494
 
489
495
  @progress.finish_syncing_binaries(server)
490
496
  end
491
-
492
- Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :indocker_sync)
493
497
  end
494
498
  end
495
499
 
@@ -500,17 +504,12 @@ class Indocker::Launchers::ConfigurationDeployer
500
504
  remote_operations += env_files.map do |alias_name, env_file|
501
505
  @progress.start_syncing_env_file(server, alias_name)
502
506
 
503
- thread = Thread.new do
507
+ Indocker::Launchers::DTO::RemoteOperationDTO.spawn(server, :env_file_sync) do
504
508
  if env_file.is_a?(Indocker::EnvFiles::Local)
505
509
  sync_path = File.join(Indocker.deploy_dir, 'env_files', File.basename(env_file.path))
506
510
  @logger.info("Syncing env file :#{alias_name} from #{env_file.path} to #{server.user}@#{server.host}:#{sync_path}")
507
511
 
508
- session = Indocker::SshSession.new(
509
- host: server.host,
510
- user: server.user,
511
- port: server.port,
512
- logger: @logger
513
- )
512
+ session = Indocker::SshSession.for(server, logger: @logger)
514
513
 
515
514
  session.exec!("mkdir -p #{Indocker::EnvFileHelper.folder}")
516
515
 
@@ -529,8 +528,6 @@ class Indocker::Launchers::ConfigurationDeployer
529
528
 
530
529
  @progress.finish_syncing_env_file(server, alias_name)
531
530
  end
532
-
533
- Indocker::Launchers::DTO::RemoteOperationDTO.new(thread, server, :env_file_sync)
534
531
  end
535
532
  end
536
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
@@ -41,7 +41,13 @@ class Indocker::Rsync
41
41
  "-o ConnectTimeout=#{Indocker.ssh_connect_timeout}",
42
42
  "-o ServerAliveInterval=10",
43
43
  "-o ServerAliveCountMax=3",
44
- ].join(' ')
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(' ')
45
51
  command = "rsync --delete-after -a -e '#{ssh_command}' #{from} #{session.user}@#{session.host}:#{to}"
46
52
  Indocker.logger.debug("sync #{from} #{session.user}@#{session.host}:#{to}")
47
53
  Indocker::Shell.command(command, Indocker.logger, raise_on_error: raise_on_error)
@@ -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
- def initialize(name:, host:, user:, port:)
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.new(
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)
@@ -19,24 +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
- @ssh = Net::SSH.start(@host, @user, {
47
+
48
+ options = {
33
49
  port: @port,
34
50
  non_interactive: true,
35
51
  timeout: Indocker.ssh_connect_timeout,
36
52
  keepalive: true,
37
53
  keepalive_interval: 10,
38
54
  keepalive_maxcount: 3,
39
- })
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)
40
63
  end
41
64
  end
42
65
 
@@ -59,6 +82,32 @@ class Indocker::SshSession
59
82
  end
60
83
 
61
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
+
62
111
  def exec_locally!(command)
63
112
  res = Indocker::Shell.command_with_result(command, @logger, skip_logging: false)
64
113
  ExecResult.new(res.stdout, '', res.exit_status, nil)
@@ -1,3 +1,3 @@
1
1
  module Indocker
2
- VERSION = "0.5.4"
2
+ VERSION = "0.6.1"
3
3
  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
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ruslan Gatiyatov