kontena-cli 1.0.6 → 1.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/VERSION +1 -1
  4. data/bin/kontena +4 -1
  5. data/kontena-cli.gemspec +1 -1
  6. data/lib/kontena/callback.rb +1 -1
  7. data/lib/kontena/callbacks/master/01_clear_current_master_after_terminate.rb +1 -1
  8. data/lib/kontena/callbacks/master/deploy/50_authenticate_after_deploy.rb +5 -5
  9. data/lib/kontena/callbacks/master/deploy/55_create_initial_grid_after_deploy.rb +1 -1
  10. data/lib/kontena/callbacks/master/deploy/56_set_server_provider_after_deploy.rb +25 -0
  11. data/lib/kontena/callbacks/master/deploy/70_invite_self_after_deploy.rb +1 -1
  12. data/lib/kontena/cli/common.rb +3 -3
  13. data/lib/kontena/cli/config.rb +1 -1
  14. data/lib/kontena/cli/grid_command.rb +2 -0
  15. data/lib/kontena/cli/grids/common.rb +12 -0
  16. data/lib/kontena/cli/grids/health_command.rb +69 -0
  17. data/lib/kontena/cli/helpers/health_helper.rb +53 -0
  18. data/lib/kontena/cli/localhost_web_server.rb +3 -3
  19. data/lib/kontena/cli/master/users/invite_command.rb +1 -1
  20. data/lib/kontena/cli/node_command.rb +2 -0
  21. data/lib/kontena/cli/nodes/health_command.rb +32 -0
  22. data/lib/kontena/cli/nodes/list_command.rb +40 -26
  23. data/lib/kontena/cli/nodes/show_command.rb +0 -1
  24. data/lib/kontena/cli/plugins/install_command.rb +28 -30
  25. data/lib/kontena/cli/plugins/search_command.rb +6 -14
  26. data/lib/kontena/cli/plugins/uninstall_command.rb +7 -11
  27. data/lib/kontena/cli/services/stats_command.rb +4 -2
  28. data/lib/kontena/cli/spinner.rb +20 -4
  29. data/lib/kontena/cli/stacks/show_command.rb +5 -1
  30. data/lib/kontena/cli/stacks/yaml/opto/service_instances_resolver.rb +22 -0
  31. data/lib/kontena/cli/stacks/yaml/opto/vault_setter.rb +1 -1
  32. data/lib/kontena/cli/stacks/yaml/reader.rb +1 -0
  33. data/lib/kontena/cli/vault/export_command.rb +22 -0
  34. data/lib/kontena/cli/vault/import_command.rb +80 -0
  35. data/lib/kontena/cli/vault/list_command.rb +4 -0
  36. data/lib/kontena/cli/vault/read_command.rb +8 -3
  37. data/lib/kontena/cli/vault/remove_command.rb +2 -1
  38. data/lib/kontena/cli/vault/update_command.rb +5 -7
  39. data/lib/kontena/cli/vault_command.rb +5 -1
  40. data/lib/kontena/client.rb +25 -2
  41. data/lib/kontena/command.rb +1 -1
  42. data/lib/kontena/debug_instrumentor.rb +70 -0
  43. data/lib/kontena/light_prompt.rb +103 -0
  44. data/lib/kontena/plugin_manager.rb +167 -6
  45. data/lib/kontena/stacks_cache.rb +1 -1
  46. data/lib/kontena_cli.rb +23 -6
  47. data/spec/kontena/cli/grids/health_command_spec.rb +390 -0
  48. data/spec/kontena/cli/nodes/health_command_spec.rb +206 -0
  49. data/spec/kontena/cli/nodes/list_command_spec.rb +205 -0
  50. data/spec/kontena/cli/vault/export_spec.rb +32 -0
  51. data/spec/kontena/cli/vault/import_spec.rb +69 -0
  52. data/spec/kontena/client_spec.rb +39 -0
  53. data/spec/kontena/plugin_manager_spec.rb +7 -7
  54. data/spec/spec_helper.rb +1 -0
  55. data/spec/support/output_helpers.rb +51 -0
  56. metadata +27 -6
@@ -48,6 +48,5 @@ module Kontena::Cli::Nodes
48
48
  end
49
49
  end
50
50
  end
51
-
52
51
  end
53
52
  end
@@ -11,38 +11,36 @@ module Kontena::Cli::Plugins
11
11
  option '--pre', :flag, 'Allow pre-release of a plugin to be installed', default: false
12
12
 
13
13
  def execute
14
- require 'shellwords'
15
- install_plugin(name)
16
- end
17
-
18
- def install_plugin(name)
19
- plugin = "kontena-plugin-#{name}"
20
- uninstall_previous(plugin) if plugin_exists?(plugin)
21
- install_options = ['--no-ri', '--no-doc']
22
- install_options << "--version #{version}" if version
23
- install_options << "--pre" if pre?
24
- install_options << plugin
25
- install_command = "#{gem_bin} install #{install_options.shelljoin}"
26
- ENV["DEBUG"] && STDERR.puts("Running #{install_command}")
27
- spinner "Installing plugin #{name.colorize(:cyan)}" do
28
- stdout, stderr, status = Open3.capture3(install_command)
29
- raise(RuntimeError, stderr) unless status.success?
14
+ installed_version = Kontena::PluginManager.instance.installed(name)
15
+
16
+ if installed_version
17
+ installed = spinner "Upgrading plugin #{name.colorize(:cyan)}" do |spin|
18
+ begin
19
+ Kontena::PluginManager.instance.upgrade_plugin(name, pre: pre?)
20
+ rescue => ex
21
+ puts Kontena.pastel.red(ex.message)
22
+ ENV["DEBUG"] && puts(ex.backtrace.join("\n "))
23
+ spin.fail!
24
+ end
25
+ end
26
+ else
27
+ installed = spinner "Installing plugin #{name.colorize(:cyan)}" do |spin|
28
+ begin
29
+ Kontena::PluginManager.instance.install_plugin(name, pre: pre?, version: version)
30
+ rescue => ex
31
+ puts Kontena.pastel.red(ex.message)
32
+ ENV["DEBUG"] && puts(ex.backtrace.join("\n "))
33
+ spin.fail!
34
+ end
35
+ end
30
36
  end
31
- end
32
-
33
- def plugin_exists?(name)
34
- Kontena::PluginManager.instance.plugins.any? { |p| p.name == name}
35
- end
36
-
37
- def gem_bin
38
- @gem_bin ||= which('gem')
39
- end
40
37
 
41
- def uninstall_previous(name)
42
- uninstall_command = "#{gem_bin} uninstall -q #{name.shellescape}"
43
- spinner "Uninstalling previous version of plugin" do
44
- stdout, stderr, status = Open3.capture3(uninstall_command)
45
- raise(RuntimeError, stderr) unless status.success?
38
+ Array(installed).each do |gem|
39
+ if gem.name.start_with?('kontena-plugin-')
40
+ puts Kontena.pastel.green("Installed plugin #{gem.name.sub('kontena-plugin-', '')} version #{gem.version}")
41
+ else
42
+ puts Kontena.pastel.cyan("Installed dependency #{gem.name} version #{gem.version}")
43
+ end
46
44
  end
47
45
  end
48
46
  end
@@ -5,27 +5,19 @@ module Kontena::Cli::Plugins
5
5
  include Common
6
6
 
7
7
  parameter '[NAME]', 'Search text'
8
+ option '--pre', :flag, 'Include pre-release versions'
8
9
 
9
10
  def execute
10
- results = fetch_plugins(name)
11
+ results = Kontena::PluginManager.instance.search_plugins(name)
11
12
  exit_with_error("Cannot access plugin server") unless results
12
13
  puts "%-50s %-10s %-60s" % ['NAME', 'VERSION', 'DESCRIPTION']
13
14
  results.each do |item|
15
+ if pre?
16
+ latest = Kontena::PluginManager.instance.latest_version(item['name'], pre: true)
17
+ item['version'] = latest.version.to_s
18
+ end
14
19
  puts "%-50s %-10s %-60s" % [short_name(item['name']), item['version'], item['info']]
15
20
  end
16
21
  end
17
-
18
- def fetch_plugins(name)
19
- client = Excon.new('https://rubygems.org')
20
- response = client.get(
21
- path: "/api/v1/search.json?query=kontena-plugin-#{name}",
22
- headers: {
23
- 'Content-Type' => 'application/json',
24
- 'Accept' => 'application/json'
25
- }
26
- )
27
-
28
- JSON.parse(response.body) rescue nil
29
- end
30
22
  end
31
23
  end
@@ -9,18 +9,14 @@ module Kontena::Cli::Plugins
9
9
  option "--force", :flag, "Force remove", default: false, attribute_name: :forced
10
10
 
11
11
  def execute
12
- require 'shellwords'
13
12
  confirm unless forced?
14
- uninstall_plugin(name)
15
- end
16
-
17
- def uninstall_plugin(name)
18
- plugin = "kontena-plugin-#{name}"
19
- gem_bin = which('gem')
20
- uninstall_command = "#{gem_bin} uninstall -q #{plugin.shellescape}"
21
- stderr = spinner "Uninstalling plugin #{name.colorize(:cyan)}" do |spin|
22
- stdout, stderr, status = Open3.capture3(uninstall_command)
23
- raise(RuntimeError, stderr) unless status.success?
13
+ spinner "Uninstalling plugin #{name.colorize(:cyan)}" do |spin|
14
+ begin
15
+ Kontena::PluginManager.instance.uninstall_plugin(name)
16
+ rescue => ex
17
+ puts Kontena.pastel.red(ex.message)
18
+ spin.fail
19
+ end
24
20
  end
25
21
  end
26
22
  end
@@ -39,7 +39,7 @@ module Kontena::Cli::Services
39
39
  end
40
40
 
41
41
  def render_header
42
- puts '%-30.30s %-15s %-20s %-15s %-15s' % ['CONTAINER', 'CPU %', 'MEM USAGE/LIMIT', 'MEM %', 'NET I/O']
42
+ puts '%-30.30s %-15s %-20s %-15s %-15s' % ['INSTANCE', 'CPU %', 'MEM USAGE/LIMIT', 'MEM %', 'NET I/O']
43
43
  end
44
44
 
45
45
  def render_stat_row(stat)
@@ -55,7 +55,9 @@ module Kontena::Cli::Services
55
55
  cpu = stat['cpu'].nil? ? 'N/A' : stat['cpu']['usage']
56
56
  network_in = stat['network'].nil? ? 'N/A' : filesize_to_human(stat['network']['rx_bytes'])
57
57
  network_out = stat['network'].nil? ? 'N/A' : filesize_to_human(stat['network']['tx_bytes'])
58
- puts '%-30.30s %-15s %-20s %-15s %-15s' % [ stat['container_id'], "#{cpu}%", "#{memory} / #{memory_limit}", "#{memory_pct}", "#{network_in}/#{network_out}"]
58
+ prefix = self.name.split('/')[0]
59
+ instance_name = stat['container_id'].gsub("#{prefix}-", "")
60
+ puts '%-30.30s %-15s %-20s %-15s %-15s' % [ instance_name, "#{cpu}%", "#{memory} / #{memory_limit}", "#{memory_pct}", "#{network_in}/#{network_out}"]
59
61
  end
60
62
 
61
63
  ##
@@ -10,6 +10,14 @@ module Kontena
10
10
  @result = :done
11
11
  end
12
12
 
13
+ def set_title(message)
14
+ if $stdout.tty?
15
+ thread['update_msg'] = message
16
+ else
17
+ Kernel.puts "- #{message}"
18
+ end
19
+ end
20
+
13
21
  def warn?
14
22
  @result == :warn
15
23
  end
@@ -32,7 +40,7 @@ module Kontena
32
40
  @result = :warn
33
41
  end
34
42
  end
35
-
43
+
36
44
  class Spinner
37
45
  CHARS = ['\\', '|', '/', '-']
38
46
  CHARS_LENGTH = CHARS.length
@@ -99,7 +107,15 @@ module Kontena
99
107
  end
100
108
  Kernel.print "\r#{message}#{CHARS[curr_index]}"
101
109
  end
102
-
110
+
111
+ if Thread.current['update_msg']
112
+ msg = Thread.current['update_msg']
113
+ Thread.current['update_msg'] = nil
114
+ Thread.current['msg'] = msg
115
+ message = " * #{msg} .. "
116
+ Kernel.print "\r#{message}#{CHARS[curr_index]}"
117
+ end
118
+
103
119
  break if Thread.current['abort']
104
120
 
105
121
  if Thread.main['spinner_msgs']
@@ -143,13 +159,13 @@ module Kontena
143
159
  spin_thread.kill
144
160
  Kernel.puts "\r [" + "fail".colorize(:red) + "] #{msg} "
145
161
  if ENV["DEBUG"]
146
- puts "Spin aborted through fail!"
162
+ STDERR.puts "Spin aborted through fail!"
147
163
  end
148
164
  rescue Exception => ex
149
165
  spin_thread.kill
150
166
  Kernel.puts "\r [" + "fail".colorize(:red) + "] #{msg} "
151
167
  if ENV["DEBUG"]
152
- puts "#{ex} #{ex.message}\n#{ex.backtrace.join("\n")}"
168
+ STDERR.puts "#{ex} #{ex.message}\n#{ex.backtrace.join("\n")}"
153
169
  end
154
170
  raise ex
155
171
  ensure
@@ -17,8 +17,12 @@ module Kontena::Cli::Stacks
17
17
  show_stack(name)
18
18
  end
19
19
 
20
+ def fetch_stack(name)
21
+ client.get("stacks/#{current_grid}/#{name}")
22
+ end
23
+
20
24
  def show_stack(name)
21
- stack = client.get("stacks/#{current_grid}/#{name}")
25
+ stack = fetch_stack(name)
22
26
 
23
27
  puts "#{stack['name']}:"
24
28
  puts " state: #{stack['state']}"
@@ -0,0 +1,22 @@
1
+ module Kontena::Cli::Stacks
2
+ module YAML
3
+ class Opto::Resolvers::ServiceInstances < Opto::Resolver
4
+ def resolve
5
+ read_command = Kontena::Cli::Stacks::ShowCommand.new([self.stack])
6
+ stack = read_command.fetch_stack(self.stack)
7
+ service = stack['services'].find { |s| s['name'] == hint }
8
+ if service
9
+ service['instances']
10
+ else
11
+ nil
12
+ end
13
+ rescue Kontena::Errors::StandardError
14
+ nil
15
+ end
16
+
17
+ def stack
18
+ ENV['STACK']
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,7 +3,7 @@ module Kontena::Cli::Stacks
3
3
  class Opto::Setters::Vault < Opto::Setter
4
4
  def set(value)
5
5
  require 'shellwords'
6
- ENV["DEBUG"] && puts("Setting to vault: #{hint}")
6
+ ENV["DEBUG"] && STDERR.puts("Setting to vault: #{hint}")
7
7
  Kontena.run("vault write --silent #{hint} #{value.to_s.shellescape}")
8
8
  end
9
9
  end
@@ -16,6 +16,7 @@ module Kontena::Cli::Stacks
16
16
  require_relative 'opto/vault_setter'
17
17
  require_relative 'opto/vault_resolver'
18
18
  require_relative 'opto/prompt_resolver'
19
+ require_relative 'opto/service_instances_resolver'
19
20
 
20
21
  @file = file
21
22
  @from_registry = from_registry
@@ -0,0 +1,22 @@
1
+ module Kontena::Cli::Vault
2
+ class ExportCommand < Kontena::Command
3
+ include Kontena::Cli::Common
4
+ include Kontena::Cli::GridOptions
5
+
6
+ banner "Exports secrets from Vault to STDOUT as YAML or JSON."
7
+
8
+ requires_current_master
9
+
10
+ option '--json', :flag, "Output JSON"
11
+
12
+ def execute
13
+ require 'shellwords'
14
+ meth = json? ? :to_json : :to_yaml
15
+ puts Hash[
16
+ *Kontena.run('vault ls --return', returning: :result).sort.flat_map do |secret|
17
+ [secret, Kontena.run("vault read --return #{secret.shellescape}", returning: :result)]
18
+ end
19
+ ].send(meth)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,80 @@
1
+ module Kontena::Cli::Vault
2
+ class ImportCommand < Kontena::Command
3
+ include Kontena::Cli::Common
4
+ include Kontena::Cli::GridOptions
5
+
6
+ banner "Imports secrets to Vault from a YAML file. Secrets with a null value will be deleted from Vault."
7
+
8
+ option "--force", :flag, "Force import", default: false, attribute_name: :forced
9
+ option '--json', :flag, "Input JSON instead of YAML"
10
+ option '--skip-null', :flag, "Do not remove keys with null values"
11
+ option '--empty-is-null', :flag, "Treat empty values as null"
12
+
13
+ parameter '[PATH]', "Input from file in PATH, default: STDIN"
14
+
15
+ requires_current_master
16
+
17
+ UPDATE_CMD = 'vault update --upsert --silent %{key} %{value}'
18
+ DELETE_CMD = 'vault rm --silent --force %{key}'
19
+
20
+
21
+ def parsed_input
22
+ json? ? JSON.load(input) : YAML.safe_load(input)
23
+ end
24
+
25
+ def input
26
+ path ? File.read(path) : STDIN.read
27
+ end
28
+
29
+ def execute
30
+ require_current_grid
31
+
32
+ updates = []
33
+ deletes = []
34
+
35
+ parsed_input.map do |k,v|
36
+ case v
37
+ when String, Numeric, TrueClass, FalseClass
38
+ if empty_is_null? && v.to_s.empty?
39
+ deletes << k.to_s
40
+ else
41
+ updates << [k.to_s, v.to_s]
42
+ end
43
+ when NilClass
44
+ deletes << k.to_s
45
+ else
46
+ exit_with_error "Invalid value type #{v.class} for #{k}."
47
+ end
48
+ end
49
+
50
+ if updates.empty? && deletes.empty?
51
+ exit_with_error "No secrets loaded"
52
+ end
53
+
54
+ unless forced?
55
+ puts "About to.."
56
+ puts " * #{Kontena.pastel.yellow("IMPORT")} #{updates.size} secret#{"s" if updates.size > 1}" unless updates.empty?
57
+ puts " * #{Kontena.pastel.red("DELETE")} #{deletes.size} secret#{"s" if deletes.size > 1}" unless deletes.empty?
58
+ confirm
59
+ end
60
+
61
+ unless updates.empty?
62
+ spinner "Updating #{updates.size} secrets" do |spin|
63
+ updates.each do |pair|
64
+ result = Kontena.run(UPDATE_CMD % { key: pair.first.shellescape, value: pair.last.shellescape })
65
+ spin.fail! unless result.zero?
66
+ end
67
+ end
68
+ end
69
+
70
+ unless deletes.empty? || skip_null?
71
+ spinner "Deleting #{deletes.size} secrets" do |spin|
72
+ deletes.map(&:shellescape).each do |del|
73
+ result = Kontena.run(DELETE_CMD % { key: del })
74
+ spin.fail! unless result.zero?
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -3,6 +3,8 @@ module Kontena::Cli::Vault
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
 
6
+ option '--return', :flag, "Return the keys", hidden: true
7
+
6
8
  def execute
7
9
  require_api_url
8
10
  require_current_grid
@@ -10,6 +12,8 @@ module Kontena::Cli::Vault
10
12
  token = require_token
11
13
  result = client(token).get("grids/#{current_grid}/secrets")
12
14
 
15
+ return result['secrets'].map { |s| s['name'] } if return?
16
+
13
17
  column_width_paddings = '%-54s %-25.25s %-25.25s'
14
18
  puts column_width_paddings % ['NAME', 'CREATED AT', 'UPDATED AT']
15
19
  result['secrets'].sort_by { |s| s['name'] }.each do |secret|
@@ -5,6 +5,7 @@ module Kontena::Cli::Vault
5
5
 
6
6
  parameter "NAME", "Secret name"
7
7
 
8
+ option '--value', :flag, 'Just output the value'
8
9
  option '--return', :flag, 'Return the value', hidden: true
9
10
 
10
11
  def execute
@@ -14,9 +15,13 @@ module Kontena::Cli::Vault
14
15
  token = require_token
15
16
  result = client(token).get("secrets/#{current_grid}/#{name}")
16
17
  return result['value'] if self.return?
17
- puts "#{result['name']}:"
18
- puts " created_at: #{result['created_at']}"
19
- puts " value: #{result['value']}"
18
+ if self.value?
19
+ puts result['value']
20
+ else
21
+ puts "#{result['name']}:"
22
+ puts " created_at: #{result['created_at']}"
23
+ puts " value: #{result['value']}"
24
+ end
20
25
  end
21
26
  end
22
27
  end
@@ -5,6 +5,7 @@ module Kontena::Cli::Vault
5
5
 
6
6
  parameter "NAME", "Secret name"
7
7
  option "--force", :flag, "Force remove", default: false, attribute_name: :forced
8
+ option "--silent", :flag, "Reduce output verbosity"
8
9
 
9
10
  def execute
10
11
  require_api_url
@@ -12,7 +13,7 @@ module Kontena::Cli::Vault
12
13
  confirm_command(name) unless forced?
13
14
 
14
15
  token = require_token
15
- spinner "Removing #{name.colorize(:cyan)} from the vault " do
16
+ vspinner "Removing #{name.colorize(:cyan)} from the vault " do
16
17
  client(token).delete("secrets/#{current_grid}/#{name}")
17
18
  end
18
19
  end
@@ -4,24 +4,22 @@ module Kontena::Cli::Vault
4
4
 
5
5
  parameter 'NAME', 'Secret name'
6
6
  parameter '[VALUE]', 'Secret value'
7
+
7
8
  option ['-u', '--upsert'], :flag, 'Create secret unless already exists', default: false
9
+ option '--silent', :flag, "Reduce output verbosity"
8
10
 
9
11
  def execute
10
12
  require_api_url
11
13
  require_current_grid
12
14
 
13
15
  token = require_token
14
- secret = value
15
- if secret.to_s == ''
16
- secret = STDIN.read
17
- end
18
- exit_with_error('No value provided') if secret.to_s == ''
16
+ value ||= STDIN.read.chomp
19
17
  data = {
20
18
  name: name,
21
- value: secret,
19
+ value: value,
22
20
  upsert: upsert?
23
21
  }
24
- spinner "Updating #{name.colorize(:cyan)} value in the vault " do
22
+ vspinner "Updating #{name.colorize(:cyan)} value in the vault " do
25
23
  client(token).put("secrets/#{current_grid}/#{name}", data)
26
24
  end
27
25
  end