kontena-cli 1.3.0.pre2 → 1.3.0.rc1

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: 3cecf22e1d7197b410494d6fbbc265d052c2cb36
4
- data.tar.gz: 84f981033ed64b0ba6a85344544d1981a723fd21
3
+ metadata.gz: d1aa18c17b13a8b1887ca08c0a4a97f01d99b6df
4
+ data.tar.gz: 396d0b859359808ee9bcc11b0f92c9a6f9c3119b
5
5
  SHA512:
6
- metadata.gz: 3dabb9b18fe5a4f0c62bb31686311a6fa65c46cc953fb803a0a53f6dad0ea4a7f36e777c995a0b01957e114eb288517b9ecfd41f1648a2555956ed2751f45cc1
7
- data.tar.gz: 8203264603f9bb1c79fefd5ce372a85481531eea3e70d44b0aaad9ac57a27a8f641bc13739cc3684bb9a0ca3a2e5cf974d46bf3c9bcb17bb57e39dd9d277f53d
6
+ metadata.gz: 6c8e6b7eb2767919326db9290c952457303c7c390622fc6d762d9453df15d9d2e5deb9993e7731aff7d7b310cb325dd178da401051c8088d90472595c159d2e1
7
+ data.tar.gz: 02a1e6756340210bd7de9c552b9f67464165db55476c55defe14a907c9da0ca55a7d05921bf4417b752e1e4712f23aab8dfb5589c1afc724c7e7029831096cb6
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.0.pre2
1
+ 1.3.0.rc1
@@ -34,4 +34,5 @@ Gem::Specification.new do |spec|
34
34
  spec.add_runtime_dependency "safe_yaml", "~> 1.0"
35
35
  spec.add_runtime_dependency "liquid", "~> 4.0.0"
36
36
  spec.add_runtime_dependency "tty-table", "~> 0.8.0"
37
+ spec.add_runtime_dependency "websocket-client-simple", "~> 0.3.0"
37
38
  end
@@ -248,14 +248,44 @@ module Kontena
248
248
  def any_key_to_continue_with_timeout(timeout=9)
249
249
  return nil if running_silent?
250
250
  return nil unless $stdout.tty?
251
- prompt.keypress("Press any key to continue or ctrl-c to cancel (Automatically continuing in :countdown seconds) ...", timeout: timeout)
251
+ start_time = Time.now.to_i
252
+ end_time = start_time + timeout
253
+ Thread.main['any_key.timed_out'] = false
254
+ msg = "Press any key to continue or ctrl-c to cancel.. (Automatically continuing in ? seconds)"
255
+
256
+ reader_thread = Thread.new do
257
+ Thread.main['any_key.char'] = $stdin.getch
258
+ end
259
+
260
+ countdown_thread = Thread.new do
261
+ time_left = timeout
262
+ while time_left > 0 && Thread.main['any_key.char'].nil?
263
+ print "\r#{pastel.bright_white("#{msg.sub("?", time_left.to_s)}")} "
264
+ time_left = end_time - Time.now.to_i
265
+ sleep 0.1
266
+ end
267
+ print "\r#{' ' * msg.length} \r"
268
+ reader_thread.kill if reader_thread.alive?
269
+ end
270
+
271
+ countdown_thread.join
272
+
273
+ if Thread.main['any_key.char'] == "\u0003"
274
+ error "Canceled"
275
+ end
252
276
  end
253
277
 
254
278
  def any_key_to_continue(timeout = nil)
255
279
  return nil if running_silent?
256
280
  return nil unless $stdout.tty?
257
281
  return any_key_to_continue_with_timeout(timeout) if timeout
258
- prompt.keypress("Press any key to continue or ctrl-c to cancel..")
282
+ msg = "Press any key to continue or ctrl-c to cancel.. "
283
+ print pastel.bright_cyan("#{msg}")
284
+ char = $stdin.getch
285
+ print "\r#{' ' * msg.length}\r"
286
+ if char == "\u0003"
287
+ error "Canceled"
288
+ end
259
289
  end
260
290
 
261
291
  def display_account_login_info
@@ -1,21 +1,40 @@
1
+ require_relative '../helpers/exec_helper'
2
+
1
3
  module Kontena::Cli::Containers
2
4
  class ExecCommand < Kontena::Command
3
5
  include Kontena::Cli::Common
4
6
  include Kontena::Cli::GridOptions
7
+ include Kontena::Cli::Helpers::ExecHelper
5
8
 
6
9
  parameter "CONTAINER_ID", "Container id"
7
10
  parameter "CMD ...", "Command"
8
11
 
12
+ option ["--shell"], :flag, "Execute as a shell command"
13
+ option ["--interactive"], :flag, "Keep stdin open"
14
+
9
15
  def execute
16
+ require 'websocket-client-simple'
17
+
10
18
  require_api_url
11
19
  token = require_token
20
+ cmd = JSON.dump({cmd: cmd_list})
21
+ base = self
22
+ ws = connect(ws_url("#{current_grid}/#{container_id}"), token)
23
+ ws.on :message do |msg|
24
+ base.handle_message(msg)
25
+ end
26
+ ws.on :open do
27
+ ws.send(cmd)
28
+ end
29
+ ws.on :close do |e|
30
+ exit 1
31
+ end
12
32
 
13
- payload = {cmd: ["sh", "-c", Shellwords.join(cmd_list)]}
14
- result = client(token).post("containers/#{current_grid}/#{container_id}/exec", payload)
15
-
16
- puts result[0].join(" ") unless result[0].size == 0
17
- $stderr.puts result[1].join(" ") unless result[1].size == 0
18
- exit result[2]
33
+ if interactive?
34
+ stream_stdin_to_ws(ws).join
35
+ else
36
+ sleep
37
+ end
19
38
  end
20
39
  end
21
40
  end
@@ -0,0 +1,61 @@
1
+ module Kontena::Cli::Helpers
2
+ module ExecHelper
3
+
4
+ # @param [WebSocket::Client::Simple] ws
5
+ # @return [Thread]
6
+ def stream_stdin_to_ws(ws)
7
+ Thread.new {
8
+ STDIN.raw {
9
+ while char = STDIN.readpartial(1024)
10
+ ws.send(JSON.dump({ stdin: char }))
11
+ end
12
+ }
13
+ }
14
+ end
15
+
16
+ # @param [Websocket::Frame::Incoming] msg
17
+ def handle_message(msg)
18
+ data = parse_message(msg)
19
+ if data.is_a?(Hash)
20
+ if data.has_key?('exit')
21
+ exit data['exit'].to_i
22
+ elsif data.has_key?('stream')
23
+ if data['stream'] == 'stdout'
24
+ $stdout << data['chunk']
25
+ else
26
+ $stderr << data['chunk']
27
+ end
28
+ end
29
+ end
30
+ rescue => exc
31
+ $stderr << "#{exc.class.name}: #{exc.message}"
32
+ end
33
+
34
+ # @param [Websocket::Frame::Incoming] msg
35
+ def parse_message(msg)
36
+ JSON.parse(msg.data)
37
+ rescue JSON::ParserError
38
+ nil
39
+ end
40
+
41
+ # @param [String] container_id
42
+ # @return [String]
43
+ def ws_url(container_id)
44
+ url = require_current_master.url
45
+ url << '/' unless url.end_with?('/')
46
+ "#{url.sub('http', 'ws')}v1/containers/#{container_id}/exec"
47
+ end
48
+
49
+ # @param [String] url
50
+ # @param [String] token
51
+ # @return [WebSocket::Client::Simple]
52
+ def connect(url, token)
53
+ WebSocket::Client::Simple.connect(url, {
54
+ headers: {
55
+ 'Authorization' => "Bearer #{token.access_token}",
56
+ 'Accept' => 'application/json'
57
+ }
58
+ })
59
+ end
60
+ end
61
+ end
@@ -12,16 +12,14 @@ module Kontena::Cli::Master::Config
12
12
 
13
13
  option ['-p', '--pair'], :flag, "Print key=value instead of only value"
14
14
 
15
- option '--return', :flag, "Return the value", hidden: true
15
+ def response
16
+ client.get("config/#{key}")
17
+ end
16
18
 
17
19
  def execute
18
- if self.pair?
19
- puts client.get("config/#{self.key}").inspect
20
- elsif self.return?
21
- return client.get("config/#{self.key}")[self.key]
22
- else
23
- puts client.get("config/#{self.key}")[self.key]
24
- end
20
+ value = response[key]
21
+ print(key + '=') if pair?
22
+ puts value
25
23
  end
26
24
  end
27
25
  end
@@ -15,25 +15,50 @@ module Kontena::Cli::Master
15
15
  URI.parse(current_master.url).host
16
16
  end
17
17
 
18
- def master_provider
19
- Kontena.run!(%w(master config get --return server.provider))
18
+ def master_provider_vagrant?
19
+ require 'kontena/cli/master/config/get_command'
20
+ cmd = Kontena::Cli::Master::Config::GetCommand.new([])
21
+ cmd.parse(['server.provider'])
22
+ cmd.response['server.provider'] == 'vagrant'
23
+ rescue => ex
24
+ false
20
25
  end
21
26
 
22
- def execute
23
- if master_provider == 'vagrant'
24
- unless Kontena::PluginManager.instance.plugins.find { |plugin| plugin.name == 'kontena-plugin-vagrant' }
25
- exit_with_error 'You need to install vagrant plugin to ssh into this node. Use kontena plugin install vagrant'
27
+ def vagrant_plugin_installed?
28
+ Kontena::PluginManager.instance.plugins.any? { |plugin| plugin.name == 'kontena-plugin-vagrant' }
29
+ end
30
+
31
+ def master_is_vagrant?
32
+ if master_provider_vagrant?
33
+ unless vagrant_plugin_installed?
34
+ exit_with_error 'You need to install vagrant plugin to ssh into this master. Use: kontena plugin install vagrant'
26
35
  end
27
- cmd = ['vagrant', 'master', 'ssh']
28
- cmd += commands_list
29
- Kontena.run!(cmd)
36
+ logger.debug { "Master config server.provider is vagrant" }
37
+ true
38
+ elsif vagrant_plugin_installed? && current_master.url.include?('192.168.66.')
39
+ logger.debug { "Vagrant plugin installed and current_master url looks like vagrant" }
40
+ true
30
41
  else
31
- cmd = ['ssh']
32
- cmd << "#{user}@#{master_host}"
33
- cmd += ["-i", identity_file] if identity_file
34
- cmd += commands_list
35
- exec(*cmd)
42
+ logger.debug { "Assuming non-vagrant master host" }
43
+ false
36
44
  end
37
45
  end
46
+
47
+ def run_ssh
48
+ cmd = ['ssh']
49
+ cmd << "#{user}@#{master_host}"
50
+ cmd += ["-i", identity_file] if identity_file
51
+ cmd += commands_list
52
+ logger.debug { "Executing #{cmd.inspect}" }
53
+ exec(*cmd)
54
+ end
55
+
56
+ def run_vagrant_ssh
57
+ Kontena.run!(['vagrant', 'master', 'ssh'] + commands_list)
58
+ end
59
+
60
+ def execute
61
+ master_is_vagrant? ? run_vagrant_ssh : run_ssh
62
+ end
38
63
  end
39
64
  end
@@ -1,9 +1,12 @@
1
+ require 'shellwords'
1
2
  require_relative 'services_helper'
3
+ require_relative '../helpers/exec_helper'
2
4
 
3
5
  module Kontena::Cli::Services
4
6
  class ExecCommand < Kontena::Command
5
7
  include Kontena::Cli::Common
6
8
  include Kontena::Cli::GridOptions
9
+ include Kontena::Cli::Helpers::ExecHelper
7
10
  include ServicesHelper
8
11
 
9
12
  parameter "NAME", "Service name"
@@ -12,6 +15,7 @@ module Kontena::Cli::Services
12
15
  option ["-i", "--instance"], "INSTANCE", "Exec on given numbered instance, default first running" do |value| Integer(value) end
13
16
  option ["-a", "--all"], :flag, "Exec on all running instances"
14
17
  option ["--shell"], :flag, "Execute as a shell command"
18
+ option ["--interactive"], :flag, "Keep stdin open"
15
19
  option ["--skip"], :flag, "Skip failed instances when executing --all"
16
20
  option ["--silent"], :flag, "Do not show exec status"
17
21
  option ["--verbose"], :flag, "Show exec status"
@@ -19,42 +23,16 @@ module Kontena::Cli::Services
19
23
  requires_current_master
20
24
  requires_current_grid
21
25
 
22
- # Exits if exec returns with non-zero
23
- def exec_container(container)
24
- if shell?
25
- cmd = ['sh', '-c', cmd_list.join(' ')]
26
- else
27
- cmd = cmd_list
28
- end
29
-
30
- stdout = stderr = exit_status = nil
31
-
32
- if !silent? && (verbose? || all?)
33
- spinner "Executing command on #{container['name']}" do
34
- stdout, stderr, exit_status = client.post("containers/#{container['id']}/exec", {cmd: cmd})
35
-
36
- raise Kontena::Cli::SpinAbort if exit_status != 0
37
- end
38
- else
39
- stdout, stderr, exit_status = client.post("containers/#{container['id']}/exec", {cmd: cmd})
40
- end
41
-
42
- stdout.each do |chunk| $stdout.write chunk end
43
- stderr.each do |chunk| $stderr.write chunk end
26
+ def execute
27
+ require 'websocket-client-simple'
44
28
 
45
- exit exit_status if exit_status != 0 && !skip?
29
+ exit_with_error "--interactive cannot be used with --all" if all? && interactive?
46
30
 
47
- return exit_status == 0
48
- end
49
-
50
- def execute
51
31
  service_containers = client.get("services/#{parse_service_id(name)}/containers")['containers']
52
32
  service_containers.sort_by! { |container| container['instance_number'] }
53
33
  running_containers = service_containers.select{|container| container['status'] == 'running' }
54
-
55
- if running_containers.empty?
56
- exit_with_error "Service #{name} does not have any running containers"
57
- end
34
+
35
+ exit_with_error "Service #{name} does not have any running containers" if running_containers.empty?
58
36
 
59
37
  if all?
60
38
  ret = true
@@ -69,16 +47,98 @@ module Kontena::Cli::Services
69
47
  end
70
48
  return ret
71
49
  elsif instance
72
- if !(container = service_containers.find{|container| container['instance_number'] == instance})
50
+ if !(container = service_containers.find{|c| c['instance_number'] == instance})
73
51
  exit_with_error "Service #{name} does not have container instance #{instance}"
74
52
  elsif container['status'] != 'running'
75
53
  exit_with_error "Service #{name} container #{container['name']} is not running, it is #{container['status']}"
76
- else
54
+ elsif interactive?
55
+ interactive_exec(container)
56
+ else
77
57
  exec_container(container)
78
58
  end
79
59
  else
80
- exec_container(running_containers.first)
60
+ if interactive?
61
+ interactive_exec(running_containers.first)
62
+ else
63
+ exec_container(running_containers.first)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Exits if exec returns with non-zero
69
+ # @param [Hash] container
70
+ def exec_container(container)
71
+ exit_status = nil
72
+ if !silent? && (verbose? || all?)
73
+ spinner "Executing command on #{container['name']}" do
74
+ exit_status = normal_exec(container)
75
+
76
+ raise Kontena::Cli::SpinAbort if exit_status != 0
77
+ end
78
+ else
79
+ exit_status = normal_exec(container)
80
+ end
81
+
82
+ exit exit_status if exit_status != 0 && !skip?
83
+
84
+ return exit_status == 0
85
+ end
86
+
87
+ # @param [Hash] container
88
+ # @return [Boolean]
89
+ def normal_exec(container)
90
+ base = self
91
+ cmd = JSON.dump({ cmd: cmd_list })
92
+ exit_status = nil
93
+ token = require_token
94
+ url = ws_url(container['id'])
95
+ url << '?shell=true' if shell?
96
+ ws = connect(url, token)
97
+ ws.on :message do |msg|
98
+ data = base.parse_message(msg)
99
+ if data
100
+ if data['exit']
101
+ exit_status = data['exit'].to_i
102
+ elsif data['stream'] == 'stdout'
103
+ $stdout << data['chunk']
104
+ else
105
+ $stderr << data['chunk']
106
+ end
107
+ end
108
+ end
109
+ ws.on :open do
110
+ ws.send(cmd)
111
+ end
112
+ ws.on :close do |e|
113
+ exit_status = 1
114
+ end
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
+ require 'io/console'
124
+
125
+ token = require_token
126
+ cmd = JSON.dump({ cmd: cmd_list })
127
+ base = self
128
+ url = ws_url(container['id']) << '?interactive=true'
129
+ url << '&shell=true' if shell?
130
+ ws = connect(url, token)
131
+ ws.on :message do |msg|
132
+ base.handle_message(msg)
133
+ end
134
+ ws.on :open do
135
+ ws.send(cmd)
136
+ end
137
+ ws.on :close do |e|
138
+ exit 1
81
139
  end
140
+
141
+ stream_stdin_to_ws(ws).join
82
142
  end
83
143
  end
84
- end
144
+ end
@@ -1,8 +1,11 @@
1
1
  module Kontena::Cli::Stacks
2
2
  module YAML
3
3
  class Opto::Resolvers::ServiceInstances < ::Opto::Resolver
4
+ include Kontena::Cli::Common
5
+
4
6
  def resolve
5
7
  return nil unless current_master && current_grid
8
+ require 'kontena/cli/stacks/show_command'
6
9
  read_command = Kontena::Cli::Stacks::ShowCommand.new([self.stack])
7
10
  stack = read_command.fetch_stack(self.stack)
8
11
  service = stack['services'].find { |s| s['name'] == hint }
@@ -34,7 +34,7 @@ module Kontena
34
34
  else
35
35
  command = cmdline
36
36
  end
37
- logger.debug { "Running Kontena.run(#{command.inspect}" }
37
+ logger.debug { "Running Kontena.run(#{command.inspect})" }
38
38
  result = Kontena::MainCommand.new(File.basename(__FILE__)).run(command)
39
39
  logger.debug { "Command completed, result: #{result.inspect} status: 0" }
40
40
  result
@@ -1,23 +1,53 @@
1
+ require 'websocket-client-simple'
1
2
  require 'kontena/cli/services/exec_command'
2
3
 
3
4
  describe Kontena::Cli::Services::ExecCommand do
4
5
  include ClientHelpers
5
6
  include OutputHelpers
6
7
 
7
- let :exec_ok do
8
- [
9
- ["ok\n"],
10
- [],
11
- 0, # exit
12
- ]
8
+ let(:ws_client_class) do
9
+ Class.new do
10
+
11
+ Event = Struct.new(:data)
12
+
13
+ def initialize
14
+ @callbacks = {}
15
+ end
16
+
17
+ def on(callback, &block)
18
+ @callbacks[callback] = block
19
+ if callback == :open
20
+ Thread.new {
21
+ sleep 0.01
22
+ @callbacks[:open].call
23
+ }
24
+ end
25
+ end
26
+
27
+ def receive_message(msg)
28
+ @callbacks[:message].call(Event.new(JSON.dump(msg)))
29
+ rescue => exc
30
+ STDERR.puts exc.message
31
+ end
32
+ end
33
+ end
34
+
35
+ let(:ws_client) do
36
+ ws_client_class.new
37
+ end
38
+
39
+ let(:master_url) do
40
+ subject.require_current_master.url.gsub('http', 'ws')
41
+ end
42
+
43
+ def respond_ok(ws_client)
44
+ ws_client.receive_message({'stream' => 'stdout', 'chunk' => "ok\n"})
45
+ ws_client.receive_message({'exit' => 0})
13
46
  end
14
47
 
15
- let :exec_fail do
16
- [
17
- [],
18
- ["error\n"],
19
- 1, # exit
20
- ]
48
+ def respond_error(ws_client)
49
+ ws_client.receive_message({'stream' => 'stderr', 'chunk' => "error\n"})
50
+ ws_client.receive_message({'exit' => 1})
21
51
  end
22
52
 
23
53
  context "For a service with one running instance" do
@@ -37,11 +67,15 @@ describe Kontena::Cli::Services::ExecCommand do
37
67
  end
38
68
 
39
69
  it "Executes on the running container by default" do
40
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_ok)
41
-
42
- expect{subject.run(['test-service', 'test'])}.to return_and_output true, [
43
- 'ok',
44
- ]
70
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-1/exec", anything).and_return(ws_client)
71
+ expect(ws_client).to receive(:send) do |foo|
72
+ ws_client.receive_message({'stream' => 'stdout', 'chunk' => "ok\n"})
73
+ ws_client.receive_message({'exit' => 0})
74
+ end
75
+
76
+ expect {
77
+ subject.run(['test-service', 'test'])
78
+ }.to output("ok\n").to_stdout
45
79
  end
46
80
  end
47
81
 
@@ -71,30 +105,46 @@ describe Kontena::Cli::Services::ExecCommand do
71
105
 
72
106
  it "Executes on the first running container by default" do
73
107
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return(service_containers)
74
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_ok)
75
-
76
- expect{subject.run(['test-service', 'test'])}.to output_lines ["ok"]
108
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-1/exec", anything).and_return(ws_client)
109
+ expect(ws_client).to receive(:send) do
110
+ respond_ok(ws_client)
111
+ end
112
+ expect {
113
+ subject.run(['test-service', 'test'])
114
+ }.to output("ok\n").to_stdout
77
115
  end
78
116
 
79
117
  it "Executes on the first running container, even if they are ordered differently" do
80
118
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return({'containers' => service_containers['containers'].reverse })
81
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_ok)
82
-
83
- expect{subject.run(['test-service', 'test'])}.to output_lines ["ok"]
119
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-1/exec", anything).and_return(ws_client)
120
+ expect(ws_client).to receive(:send) do
121
+ respond_ok(ws_client)
122
+ end
123
+ expect {
124
+ subject.run(['test-service', 'test'])
125
+ }.to output("ok\n").to_stdout
84
126
  end
85
127
 
86
128
  it "Executes on the first running container if given" do
87
129
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return(service_containers)
88
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_ok)
89
-
90
- expect{subject.run(['--instance=1', 'test-service', 'test'])}.to output_lines ["ok"]
130
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-1/exec", anything).and_return(ws_client)
131
+ expect(ws_client).to receive(:send) do
132
+ respond_ok(ws_client)
133
+ end
134
+ expect {
135
+ subject.run(['test-service', 'test'])
136
+ }.to output("ok\n").to_stdout
91
137
  end
92
138
 
93
139
  it "Executes on the second running container if given" do
94
140
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return(service_containers)
95
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-2/exec', { cmd: ['test'] }).and_return(exec_ok)
96
-
97
- expect{subject.run(['--instance=2', 'test-service', 'test'])}.to output_lines ["ok"]
141
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-2/exec", anything).and_return(ws_client)
142
+ expect(ws_client).to receive(:send) do
143
+ respond_ok(ws_client)
144
+ end
145
+ expect {
146
+ subject.run(['--instance', '2', 'test-service', 'test'])
147
+ }.to output("ok\n").to_stdout
98
148
  end
99
149
 
100
150
  it "Errors on a nonexistant container if given" do
@@ -105,35 +155,71 @@ describe Kontena::Cli::Services::ExecCommand do
105
155
 
106
156
  it "Executes on each running container" do
107
157
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return(service_containers)
108
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_ok)
109
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-2/exec', { cmd: ['test'] }).and_return(exec_ok)
110
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-3/exec', { cmd: ['test'] }).and_return(exec_ok)
111
158
 
112
- expect{subject.run(['--silent', '--all', 'test-service', 'test'])}.to output_lines ["ok", "ok", "ok"]
159
+ 3.times do |i|
160
+ ws_client = ws_client_class.new
161
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-#{i + 1}/exec", anything).and_return(ws_client)
162
+ expect(ws_client).to receive(:send) do
163
+ ws_client.receive_message({'stream' => 'stdout', 'chunk' => "test#{i + 1}\n"})
164
+ ws_client.receive_message({'exit' => 0})
165
+ end
166
+ end
167
+
168
+ expect {
169
+ subject.run(['--silent', '--all', 'test-service', 'test'])
170
+ }.to output("test1\ntest2\ntest3\n").to_stdout
113
171
  end
114
172
 
115
173
  it "Stops if the first container fails" do
116
174
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return(service_containers)
117
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_fail)
118
-
119
- expect{subject.run(['--silent', '--all', 'test-service', 'test'])}.to exit_with_error.and output("error\n").to_stderr
175
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-1/exec", anything).and_return(ws_client)
176
+ expect(ws_client).to receive(:send) do
177
+ respond_error(ws_client)
178
+ end
179
+ expect {
180
+ subject.run(['--silent', '--all', 'test-service', 'test'])
181
+ }.to output("error\n").to_stderr
120
182
  end
121
183
 
122
184
  it "Stops if the second container fails" do
123
185
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return(service_containers)
124
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_ok)
125
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-2/exec', { cmd: ['test'] }).and_return(exec_fail)
126
-
127
- expect{subject.run(['--silent', '--all', 'test-service', 'test'])}.to exit_with_error.and output("ok\n").to_stdout.and output("error\n").to_stderr
186
+ i = 1
187
+ [:ok, :err].each do |status|
188
+ ws_client = ws_client_class.new
189
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-#{i}/exec", anything).and_return(ws_client)
190
+ expect(ws_client).to receive(:send) do
191
+ if status == :ok
192
+ respond_ok(ws_client)
193
+ else
194
+ respond_error(ws_client)
195
+ end
196
+ end
197
+ i += 1
198
+ end
199
+ expect {
200
+ subject.run(['--silent', '--all', 'test-service', 'test'])
201
+ }.to output("ok\n").to_stdout.and output("error\n").to_stderr
128
202
  end
129
203
 
130
204
  it "Keeps going if the second container fails when using --skip" do
131
205
  expect(client).to receive(:get).with('services/test-grid/null/test-service/containers').and_return(service_containers)
132
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-1/exec', { cmd: ['test'] }).and_return(exec_ok)
133
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-2/exec', { cmd: ['test'] }).and_return(exec_fail)
134
- expect(client).to receive(:post).with('containers/test-grid/host/test-service.container-3/exec', { cmd: ['test'] }).and_return(exec_ok)
135
206
 
136
- expect{subject.run(['--silent', '--all', '--skip', 'test-service', 'test'])}.to exit_with_error.and output("ok\nok\n").to_stdout.and output("error\n").to_stderr
207
+ i = 1
208
+ [:ok, :err, :ok].each do |status|
209
+ ws_client = ws_client_class.new
210
+ expect(WebSocket::Client::Simple).to receive(:connect).with("#{master_url}v1/containers/test-grid/host/test-service.container-#{i}/exec", anything).and_return(ws_client)
211
+ expect(ws_client).to receive(:send) do
212
+ if status == :ok
213
+ respond_ok(ws_client)
214
+ else
215
+ respond_error(ws_client)
216
+ end
217
+ end
218
+ i += 1
219
+ end
220
+ expect {
221
+ subject.run(['--silent', '--all', '--skip', 'test-service', 'test'])
222
+ }.to output("ok\nok\n").to_stdout.and output("error\n").to_stderr
137
223
  end
138
224
  end
139
225
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kontena-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0.pre2
4
+ version: 1.3.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kontena, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-26 00:00:00.000000000 Z
11
+ date: 2017-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -206,6 +206,20 @@ dependencies:
206
206
  - - "~>"
207
207
  - !ruby/object:Gem::Version
208
208
  version: 0.8.0
209
+ - !ruby/object:Gem::Dependency
210
+ name: websocket-client-simple
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: 0.3.0
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: 0.3.0
209
223
  description: Command-line client for the Kontena container and microservices platform
210
224
  email:
211
225
  - info@kontena.io
@@ -339,6 +353,7 @@ files:
339
353
  - lib/kontena/cli/grids/users/add_command.rb
340
354
  - lib/kontena/cli/grids/users/list_command.rb
341
355
  - lib/kontena/cli/grids/users/remove_command.rb
356
+ - lib/kontena/cli/helpers/exec_helper.rb
342
357
  - lib/kontena/cli/helpers/health_helper.rb
343
358
  - lib/kontena/cli/helpers/log_helper.rb
344
359
  - lib/kontena/cli/localhost_web_server.rb