kontena-cli 1.0.6 → 1.1.0.pre1

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.
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