kontena-cli 1.4.0.pre1 → 1.4.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69a344212c91c304c80efd5073e2b3d3e9003b66
4
- data.tar.gz: 04ba668d77961f031b6bdd1bad7cbeacab5dfac9
3
+ metadata.gz: c2f4001bbbebedb3f6e7840825f441c24d125457
4
+ data.tar.gz: 7fa42d09f45e5c62336691c9896ac0928f31635e
5
5
  SHA512:
6
- metadata.gz: 5fb80753ebfd53b27841cedaa60b2de9af188b6448691b7cc5e9f02410e644d44115b8f873295c7cf2eef823fa929911069d7032ebe5334ffbadd46b52d18b35
7
- data.tar.gz: 32129fc7dd6d6ba258238a47799d085d639d5ae0168ee64a90de78756c8984dbfc88885324ce06f5cb4ffe8a76286cb97f119479c3c41e7e4430b6fa6241befc
6
+ metadata.gz: f3d420649dce177b328eb57202875eff8d4929447ee77cd05091aaeda928327678c74f9bda74404b0fa17a95963b68eef549e51d9c871313e36af09ee56c0681
7
+ data.tar.gz: 4e259827e5ed4771a82e1d30267f49fb97e75ba00ec7ad68037f7b4e3f543b8a78084aa350826210c78ff134a89eee876ed97e85736170de2b79fc4200eeae57
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.0.pre1
1
+ 1.4.0.pre2
data/kontena-cli.gemspec CHANGED
@@ -33,5 +33,5 @@ Gem::Specification.new do |spec|
33
33
  spec.add_runtime_dependency "semantic", "~> 1.5"
34
34
  spec.add_runtime_dependency "liquid", "~> 4.0.0"
35
35
  spec.add_runtime_dependency "tty-table", "~> 0.8.0"
36
- spec.add_runtime_dependency "websocket-driver-kontena", "0.6.5"
36
+ spec.add_runtime_dependency "kontena-websocket-client", "~> 0.1.0"
37
37
  end
@@ -170,7 +170,10 @@ module Kontena
170
170
  return unless server.token
171
171
  return unless server.token.refresh_token
172
172
  return if server.token.expired?
173
- client = Kontena::Client.new(server.url, server.token)
173
+ client = Kontena::Client.new(server.url, server.token,
174
+ ssl_cert_path: server.ssl_cert_path,
175
+ ssl_subject_cn: server.ssl_subject_cn,
176
+ )
174
177
  logger.debug "Trying to invalidate refresh token on #{server.name}"
175
178
  client.refresh_token
176
179
  rescue => ex
@@ -205,7 +208,9 @@ module Kontena
205
208
 
206
209
  @client = Kontena::Client.new(
207
210
  api_url || require_current_master.url,
208
- token || require_current_master.token
211
+ token || require_current_master.token,
212
+ ssl_cert_path: require_current_master.ssl_cert_path,
213
+ ssl_subject_cn: require_current_master.ssl_subject_cn,
209
214
  )
210
215
  end
211
216
 
@@ -507,6 +507,39 @@ module Kontena
507
507
  super
508
508
  @table[:account] ||= 'master'
509
509
  end
510
+
511
+ def uri
512
+ @uri ||= URI.parse(self.url)
513
+ end
514
+
515
+ # @return [String, nil] path to ~/.kontena/certs/*.pem
516
+ def ssl_cert_path
517
+ path = File.join(Dir.home, '.kontena', 'certs', "#{self.uri.host}.pem")
518
+
519
+ if File.exist?(path) && File.readable?(path)
520
+ return path
521
+ else
522
+ return nil
523
+ end
524
+ end
525
+
526
+ # @return [OpenSSL::X509::Certificate, nil]
527
+ def ssl_cert
528
+ if path = self.ssl_cert_path
529
+ return OpenSSL::X509::Certificate.new(File.read(path))
530
+ else
531
+ return nil
532
+ end
533
+ end
534
+
535
+ # @return [String, nil] ssl cert subject CN=
536
+ def ssl_subject_cn
537
+ if cert = self.ssl_cert
538
+ return cert.subject.to_a.select{|name, data, type| name == 'CN' }.map{|name, data, type| data }.first
539
+ else
540
+ nil
541
+ end
542
+ end
510
543
  end
511
544
 
512
545
  class Token < OpenStruct
@@ -9,42 +9,21 @@ module Kontena::Cli::Containers
9
9
  parameter "CONTAINER_ID", "Container id"
10
10
  parameter "CMD ...", "Command"
11
11
 
12
- option ["--shell"], :flag, "Execute as a shell command"
13
- option ["-i", "--interactive"], :flag, "Keep stdin open"
14
- option ["-t", "--tty"], :flag, "Allocate a pseudo-TTY"
12
+ option ["--shell"], :flag, "Execute as a shell command", default: false
13
+ option ["-i", "--interactive"], :flag, "Keep stdin open", default: false
14
+ option ["-t", "--tty"], :flag, "Allocate a pseudo-TTY", default: false
15
+
16
+ requires_current_master
17
+ requires_current_grid
15
18
 
16
19
  def execute
17
- exit_with_error "the input device is not a TTY" if tty? && !STDIN.tty?
20
+ exit_status = container_exec("#{current_grid}/#{self.container_id}", self.cmd_list,
21
+ interactive: interactive?,
22
+ shell: shell?,
23
+ tty: tty?,
24
+ )
18
25
 
19
- require_api_url
20
- token = require_token
21
- cmd = JSON.dump({cmd: cmd_list})
22
- queue = Queue.new
23
- stdin_reader = nil
24
- url = ws_url("#{current_grid}/#{container_id}", interactive: interactive?, shell: shell?, tty: tty?)
25
- ws = connect(url, token)
26
- ws.on :message do |msg|
27
- data = parse_message(msg)
28
- queue << data if data.is_a?(Hash)
29
- end
30
- ws.on :open do
31
- ws.text(cmd)
32
- stdin_reader = self.stream_stdin_to_ws(ws, tty: self.tty?) if self.interactive?
33
- end
34
- ws.on :close do |e|
35
- if e.reason.include?('code: 404')
36
- queue << {'exit' => 1, 'message' => 'Not found'}
37
- else
38
- queue << {'exit' => 1}
39
- end
40
- end
41
- ws.connect
42
- while msg = queue.pop
43
- self.handle_message(msg)
44
- end
45
- rescue SystemExit
46
- stdin_reader.kill if stdin_reader
47
- raise
26
+ exit exit_status unless exit_status.zero?
48
27
  end
49
28
  end
50
29
  end
@@ -1,87 +1,190 @@
1
- require_relative '../../websocket/client'
1
+ require 'io/console'
2
+ require 'kontena-websocket-client'
2
3
 
3
4
  module Kontena::Cli::Helpers
4
5
  module ExecHelper
5
6
 
6
- # @param [WebSocket::Client::Simple] ws
7
- # @return [Thread]
8
- def stream_stdin_to_ws(ws, tty: nil)
9
- require 'io/console'
10
- Thread.new {
11
- if tty
12
- STDIN.raw {
13
- while char = STDIN.readpartial(1024)
14
- ws.text(JSON.dump({ stdin: char }))
15
- end
16
- }
17
- else
18
- while char = STDIN.gets
19
- ws.text(JSON.dump({ stdin: char }))
7
+ websocket_log_level = if ENV["DEBUG"] == 'websocket'
8
+ Logger::DEBUG
9
+ elsif ENV["DEBUG"]
10
+ Logger::INFO
11
+ else
12
+ Logger::WARN
13
+ end
14
+
15
+ Kontena::Websocket::Logging.initialize_logger(STDERR, websocket_log_level)
16
+
17
+ WEBSOCKET_CLIENT_OPTIONS = {
18
+ connect_timeout: ENV["EXCON_CONNECT_TIMEOUT"] ? ENV["EXCON_CONNECT_TIMEOUT"].to_f : 5.0,
19
+ open_timeout: ENV["EXCON_CONNECT_TIMEOUT"] ? ENV["EXCON_CONNECT_TIMEOUT"].to_f : 5.0,
20
+ ping_interval: ENV["EXCON_READ_TIMEOUT"] ? ENV["EXCON_READ_TIMEOUT"].to_f : 30.0,
21
+ ping_timeout: ENV["EXCON_CONNECT_TIMEOUT"] ? ENV["EXCON_CONNECT_TIMEOUT"].to_f : 5.0,
22
+ close_timeout: ENV["EXCON_CONNECT_TIMEOUT"] ? ENV["EXCON_CONNECT_TIMEOUT"].to_f : 5.0,
23
+ write_timeout: ENV["EXCON_WRITE_TIMEOUT"] ? ENV["EXCON_WRITE_TIMEOUT"].to_f : 5.0,
24
+ }
25
+
26
+ # @param ws [Kontena::Websocket::Client]
27
+ # @param tty [Boolean] read stdin in raw mode, sending tty escapes for remote pty
28
+ # @raise [ArgumentError] not a tty
29
+ # @yield [data]
30
+ # @yieldparam data [String] data from stdin
31
+ # @raise [ArgumentError] not a tty
32
+ # @return EOF on stdin (!tty)
33
+ def read_stdin(tty: nil)
34
+ if tty
35
+ raise ArgumentError, "the input device is not a TTY" unless STDIN.tty?
36
+
37
+ STDIN.raw { |io|
38
+ # we do not expect EOF on a TTY, ^D sends a tty escape to close the pty instead
39
+ loop do
40
+ # raises EOFError, SyscallError or IOError
41
+ yield io.readpartial(1024)
20
42
  end
21
- ws.text(JSON.dump({ stdin: nil }))
43
+ }
44
+ else
45
+ # line-buffered
46
+ while line = STDIN.gets
47
+ yield line
22
48
  end
23
- }
49
+ end
24
50
  end
25
51
 
26
- # @param [Hash] msg
27
- def handle_message(msg)
28
- if msg.has_key?('exit')
29
- if msg['message']
30
- exit_with_error(msg['message'])
31
- else
32
- exit msg['exit'].to_i
33
- end
34
- elsif msg.has_key?('stream')
35
- if msg['stream'] == 'stdout'
36
- $stdout << msg['chunk']
37
- else
38
- $stderr << msg['chunk']
52
+ # @return [String]
53
+ def websocket_url(path, query = nil)
54
+ url = URI.parse(require_current_master.url)
55
+ url.scheme = url.scheme.sub('http', 'ws')
56
+ url.path = '/v1/' + path
57
+ url.query = (query && !query.empty?) ? URI.encode_www_form(query) : nil
58
+ url.to_s
59
+ end
60
+
61
+ # @param ws [Kontena::Websocket::Client]
62
+ # @return [Integer] exit code
63
+ def websocket_exec_read(ws)
64
+ ws.read do |msg|
65
+ msg = JSON.parse(msg)
66
+
67
+ logger.debug "websocket exec read: #{msg.inspect}"
68
+
69
+ if msg.has_key?('exit')
70
+ # breaks the read loop
71
+ return msg['exit'].to_i
72
+ elsif msg.has_key?('stream')
73
+ if msg['stream'] == 'stdout'
74
+ $stdout << msg['chunk']
75
+ else
76
+ $stderr << msg['chunk']
77
+ end
39
78
  end
40
79
  end
41
80
  end
42
81
 
43
- # @param [Websocket::Frame::Incoming] msg
44
- def parse_message(msg)
45
- JSON.parse(msg.data)
46
- rescue JSON::ParserError
47
- nil
82
+ # @param ws [Kontena::Websocket::Client]
83
+ # @param msg [Hash]
84
+ def websocket_exec_write(ws, msg)
85
+ logger.debug "websocket exec write: #{msg.inspect}"
86
+
87
+ ws.send(JSON.dump(msg))
88
+ end
89
+
90
+ # Start thread to read from stdin, and write to websocket.
91
+ # Closes websocket on stdin read errors.
92
+ #
93
+ # @param ws [Kontena::Websocket::Client]
94
+ # @param tty [Boolean]
95
+ # @return [Thread]
96
+ def websocket_exec_write_thread(ws, tty: nil)
97
+ Thread.new do
98
+ begin
99
+ read_stdin(tty: tty) do |stdin|
100
+ websocket_exec_write(ws, 'stdin' => stdin)
101
+ end
102
+ websocket_exec_write(ws, 'stdin' => nil) # EOF
103
+ rescue => exc
104
+ logger.error exc
105
+ ws.close(1001, "stdin read #{exc.class}: #{exc}")
106
+ end
107
+ end
48
108
  end
49
109
 
50
- # @param container_id [String] The container id
110
+ # Connect to server websocket, send from stdin, and write out messages
111
+ #
112
+ # @param paths [String]
113
+ # @param options [Hash] @see Kontena::Websocket::Client
114
+ # @param cmd [Array<String>] command to execute
51
115
  # @param interactive [Boolean] Interactive TTY on/off
52
116
  # @param shell [Boolean] Shell on/of
53
117
  # @param tty [Boolean] TTY on/of
54
- # @return [String]
55
- def ws_url(container_id, interactive: false, shell: false, tty: false)
56
- require 'uri' unless Object.const_defined?(:URI)
57
- extend Kontena::Cli::Common unless self.respond_to?(:require_current_master)
118
+ # @return [Integer] exit code
119
+ def websocket_exec(path, cmd, interactive: false, shell: false, tty: false)
120
+ exit_status = nil
121
+ write_thread = nil
58
122
 
59
- url = URI.parse(require_current_master.url)
60
- url.scheme = url.scheme.sub('http', 'ws')
61
- url.path = "/v1/containers/#{container_id}/exec"
62
- if shell || interactive || tty
63
- query = {}
64
- query.merge!(interactive: true) if interactive
65
- query.merge!(shell: true) if shell
66
- query.merge!(tty: true) if tty
67
- url.query = URI.encode_www_form(query)
68
- end
69
- url.to_s
70
- end
123
+ query = {}
124
+ query[:interactive] = interactive if interactive
125
+ query[:shell] = shell if shell
126
+ query[:tty] = tty if tty
71
127
 
72
- # @param [String] url
73
- # @param [String] token
74
- # @return [WebSocket::Client::Simple]
75
- def connect(url, token)
76
- options = {
77
- headers: {
128
+ server = require_current_master
129
+ url = websocket_url(path, query)
130
+ token = require_token
131
+ options = WEBSOCKET_CLIENT_OPTIONS.dup
132
+ options[:headers] = {
78
133
  'Authorization' => "Bearer #{token.access_token}"
79
- }
80
134
  }
81
- if ENV['SSL_IGNORE_ERRORS'].to_s == 'true'
82
- options[:verify_mode] = ::OpenSSL::SSL::VERIFY_NONE
135
+ options[:ssl_params] = {
136
+ verify_mode: ENV['SSL_IGNORE_ERRORS'].to_s == 'true' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER,
137
+ ca_file: server.ssl_cert_path,
138
+ }
139
+ options[:ssl_hostname] = server.ssl_subject_cn
140
+
141
+ logger.debug { "websocket exec connect... #{url}" }
142
+
143
+ # we do not expect CloseError, because the server will send an 'exit' message first,
144
+ # and we return before seeing the close frame
145
+ # TODO: handle HTTP 404 errors
146
+ Kontena::Websocket::Client.connect(url, **options) do |ws|
147
+ logger.debug { "websocket exec open" }
148
+
149
+ # first frame contains exec command
150
+ websocket_exec_write(ws, 'cmd' => cmd)
151
+
152
+ if interactive
153
+ # start new thread to write from stdin to websocket
154
+ write_thread = websocket_exec_write_thread(ws, tty: tty)
155
+ end
156
+
157
+ # blocks reading from websocket, returns with exec exit code
158
+ exit_status = websocket_exec_read(ws)
159
+
160
+ fail ws.close_reason unless exit_status
161
+ end
162
+
163
+ rescue Kontena::Websocket::Error => exc
164
+ exit_with_error(exc)
165
+
166
+ rescue => exc
167
+ logger.error { "websocket exec error: #{exc}" }
168
+ raise
169
+
170
+ else
171
+ logger.debug { "websocket exec exit: #{exit_status}"}
172
+ return exit_status
173
+
174
+ ensure
175
+ if write_thread
176
+ write_thread.kill
177
+ write_thread.join
83
178
  end
84
- Kontena::Websocket::Client.new(url, options)
179
+ end
180
+
181
+ # Execute command on container using websocket API.
182
+ #
183
+ # @param id [String] Container ID (grid/host/name)
184
+ # @param cmd [Array<String>] command to execute
185
+ # @return [Integer] exit code
186
+ def container_exec(id, cmd, **exec_options)
187
+ websocket_exec("containers/#{id}/exec", cmd, **exec_options)
85
188
  end
86
189
  end
87
190
  end
@@ -10,14 +10,23 @@ module Kontena::Cli::Services
10
10
  include Kontena::Cli::Helpers::ExecHelper
11
11
  include ServicesHelper
12
12
 
13
+ class ExecExit < StandardError
14
+ attr_reader :exit_status
15
+
16
+ def initialize(exit_status, message = nil)
17
+ super(message)
18
+ @exit_status = exit_status
19
+ end
20
+ end
21
+
13
22
  parameter "NAME", "Service name"
14
23
  parameter "CMD ...", "Command"
15
24
 
16
25
  option ["--instance"], "INSTANCE", "Exec on given numbered instance, default first running" do |value| Integer(value) end
17
26
  option ["-a", "--all"], :flag, "Exec on all running instances"
18
- option ["--shell"], :flag, "Execute as a shell command"
19
- option ["-i", "--interactive"], :flag, "Keep stdin open"
20
- option ["-t", "--tty"], :flag, "Allocate a pseudo-TTY"
27
+ option ["--shell"], :flag, "Execute as a shell command", default: false
28
+ option ["-i", "--interactive"], :flag, "Keep stdin open", default: false
29
+ option ["-t", "--tty"], :flag, "Allocate a pseudo-TTY", default: false
21
30
  option ["--skip"], :flag, "Skip failed instances when executing --all"
22
31
  option ["--silent"], :flag, "Do not show exec status"
23
32
  option ["--verbose"], :flag, "Show exec status"
@@ -39,7 +48,7 @@ module Kontena::Cli::Services
39
48
  ret = true
40
49
  service_containers.each do |container|
41
50
  if container['status'] == 'running'
42
- if !exec_container(container)
51
+ if !execute_container(container)
43
52
  ret = false
44
53
  end
45
54
  else
@@ -52,105 +61,45 @@ module Kontena::Cli::Services
52
61
  exit_with_error "Service #{name} does not have container instance #{instance}"
53
62
  elsif container['status'] != 'running'
54
63
  exit_with_error "Service #{name} container #{container['name']} is not running, it is #{container['status']}"
55
- elsif interactive?
56
- interactive_exec(container)
57
64
  else
58
- exec_container(container)
65
+ execute_container(container)
59
66
  end
60
67
  else
61
- if interactive?
62
- interactive_exec(running_containers.first)
63
- else
64
- exec_container(running_containers.first)
65
- end
68
+ execute_container(running_containers.first)
66
69
  end
67
70
  end
68
71
 
69
- # Exits if exec returns with non-zero
70
- # @param [Hash] container
71
- def exec_container(container)
72
- exit_status = nil
73
- if !silent? && (verbose? || all?)
74
- spinner "Executing command on #{container['name']}" do
75
- exit_status = normal_exec(container)
76
-
77
- raise Kontena::Cli::SpinAbort if exit_status != 0
78
- end
72
+ # Run block with spinner by default if --all, or when using --verbose.
73
+ # Do not use spinner if --silent.
74
+ def maybe_spinner(msg, &block)
75
+ if (all? || verbose?) && !silent?
76
+ spinner(msg, &block)
79
77
  else
80
- exit_status = normal_exec(container)
78
+ yield
81
79
  end
82
-
83
- exit exit_status if exit_status != 0 && !skip?
84
-
85
- return exit_status == 0
86
80
  end
87
81
 
88
82
  # @param [Hash] container
89
- # @return [Boolean]
90
- def normal_exec(container)
91
- base = self
92
- cmd = JSON.dump({ cmd: cmd_list })
93
- exit_status = nil
94
- token = require_token
95
- ws = connect(url(container['id']), token)
96
- ws.on :message do |msg|
97
- data = base.parse_message(msg)
98
- if data
99
- if data['exit']
100
- exit_status = data['exit'].to_i
101
- elsif data['stream'] == 'stdout'
102
- $stdout << data['chunk']
103
- else
104
- $stderr << data['chunk']
105
- end
106
- end
107
- end
108
- ws.on :open do
109
- ws.text(cmd)
83
+ # @raise [SystemExit] if exec exits with non-zero status, and not --skip
84
+ # @return [true] exit exit status zero
85
+ # @return [false] exit exit status non-zero and --skip
86
+ def execute_container(container)
87
+ maybe_spinner "Executing command on #{container['name']}" do
88
+ exit_status = container_exec(container['id'], self.cmd_list,
89
+ interactive: interactive?,
90
+ shell: shell?,
91
+ tty: tty?,
92
+ )
93
+ raise ExecExit.new(exit_status) unless exit_status.zero?
110
94
  end
111
- ws.on :close do |e|
112
- exit_status = 1 if exit_status.nil? && e.code != 1000
113
- end
114
- ws.connect
115
-
116
- sleep 0.01 until !exit_status.nil?
117
-
118
- exit_status
119
- end
120
-
121
- # @param [Hash] container
122
- def interactive_exec(container)
123
- token = require_token
124
- cmd = JSON.dump({ cmd: cmd_list })
125
- queue = Queue.new
126
- stdin_stream = nil
127
- ws = connect(url(container['id']), token)
128
- ws.on :message do |msg|
129
- data = self.parse_message(msg)
130
- queue << data if data.is_a?(Hash)
131
- end
132
- ws.on :open do
133
- ws.text(cmd)
134
- stdin_stream = self.stream_stdin_to_ws(ws, tty: self.tty?)
135
- end
136
- ws.on :close do |e|
137
- if e.code != 1000
138
- queue << {'exit' => 1}
139
- else
140
- queue << {'exit' => 0}
141
- end
142
- end
143
- ws.connect
144
- while msg = queue.pop
145
- self.handle_message(msg)
95
+ rescue ExecExit => exc
96
+ if skip?
97
+ return false
98
+ else
99
+ exit exc.exit_status
146
100
  end
147
- rescue SystemExit
148
- stdin_stream.kill if stdin_stream
149
- raise
150
- end
151
-
152
- def url(container_id)
153
- ws_url(container_id, shell: shell?, interactive: interactive?, tty: tty?)
101
+ else
102
+ return true
154
103
  end
155
104
  end
156
105
  end