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
@@ -1,8 +1,10 @@
1
- require_relative 'vault/write_command'
1
+ require_relative 'vault/export_command'
2
+ require_relative 'vault/import_command'
2
3
  require_relative 'vault/list_command'
3
4
  require_relative 'vault/read_command'
4
5
  require_relative 'vault/remove_command'
5
6
  require_relative 'vault/update_command'
7
+ require_relative 'vault/write_command'
6
8
 
7
9
  class Kontena::Cli::VaultCommand < Kontena::Command
8
10
 
@@ -11,6 +13,8 @@ class Kontena::Cli::VaultCommand < Kontena::Command
11
13
  subcommand "read", "Read secret", Kontena::Cli::Vault::ReadCommand
12
14
  subcommand "update", "Update secret", Kontena::Cli::Vault::UpdateCommand
13
15
  subcommand ["remove", "rm"], "Remove secret", Kontena::Cli::Vault::RemoveCommand
16
+ subcommand "export", "Export secrets to STDOUT", Kontena::Cli::Vault::ExportCommand
17
+ subcommand "import", "Import secrets from a file or STDIN", Kontena::Cli::Vault::ImportCommand
14
18
 
15
19
  def execute
16
20
  end
@@ -23,6 +23,7 @@ module Kontena
23
23
  CONTENT_JSON = 'application/json'.freeze
24
24
  JSON_REGEX = /application\/(.+?\+)?json/.freeze
25
25
  CONTENT_TYPE = 'Content-Type'.freeze
26
+ X_KONTENA_VERSION = 'X-Kontena-Version'.freeze
26
27
  ACCEPT = 'Accept'.freeze
27
28
  AUTHORIZATION = 'Authorization'.freeze
28
29
 
@@ -46,7 +47,7 @@ module Kontena
46
47
  uri = URI.parse(@api_url)
47
48
  @host = uri.host
48
49
 
49
- @logger = Logger.new(STDOUT)
50
+ @logger = Logger.new(ENV["DEBUG"] ? STDERR : STDOUT)
50
51
  @logger.level = ENV["DEBUG"].nil? ? Logger::INFO : Logger::DEBUG
51
52
  @logger.progname = 'CLIENT'
52
53
 
@@ -59,6 +60,10 @@ module Kontena
59
60
  write_timeout: ENV["EXCON_WRITE_TIMEOUT"] ? ENV["EXCON_WRITE_TIMEOUT"].to_i : 5,
60
61
  ssl_verify_peer: ignore_ssl_errors? ? false : true
61
62
  }
63
+ if ENV["DEBUG"]
64
+ require_relative 'debug_instrumentor'
65
+ excon_opts[:instrumentor] = Kontena::DebugInstrumentor
66
+ end
62
67
 
63
68
  cert_file = File.join(Dir.home, "/.kontena/certs/#{uri.host}.pem")
64
69
  if File.exist?(cert_file) && File.readable?(cert_file)
@@ -135,7 +140,7 @@ module Kontena
135
140
  request(path: final_path)
136
141
  true
137
142
  rescue
138
- ENV["DEBUG"] && puts("Authentication verification exception: #{$!} #{$!.message} #{$!.backtrace}")
143
+ logger.debug "Authentication verification exception: #{$!} #{$!.message} #{$!.backtrace}"
139
144
  false
140
145
  end
141
146
 
@@ -473,6 +478,8 @@ module Kontena
473
478
  # @param [Excon::Response]
474
479
  # @return [Hash,String]
475
480
  def parse_response(response)
481
+ check_version_and_warn(response.headers[X_KONTENA_VERSION])
482
+
476
483
  if response.headers[CONTENT_TYPE] =~ JSON_REGEX
477
484
  parse_json(response.body)
478
485
  else
@@ -480,6 +487,22 @@ module Kontena
480
487
  end
481
488
  end
482
489
 
490
+ def check_version_and_warn(server_version)
491
+ return nil if $VERSION_WARNING_ADDED
492
+ return nil unless server_version.to_s =~ /^\d+\.\d+\.\d+/
493
+
494
+ unless server_version[/^(\d+\.\d+)/, 1] == Kontena::Cli::VERSION[/^(\d+\.\d+)/, 1] # Just compare x.y
495
+ add_version_warning(server_version)
496
+ $VERSION_WARNING_ADDED = true
497
+ end
498
+ end
499
+
500
+ def add_version_warning(server_version)
501
+ at_exit do
502
+ warn Kontena.pastel.yellow("Warning: Server version is #{server_version}. You are using CLI version #{Kontena::Cli::VERSION}.")
503
+ end
504
+ end
505
+
483
506
  # Parse json
484
507
  #
485
508
  # @param [String] json
@@ -169,7 +169,7 @@ class Kontena::Command < Clamp::Command
169
169
  end
170
170
 
171
171
  def run(arguments)
172
- ENV["DEBUG"] && puts("Running #{self} -- callback matcher = '#{self.class.callback_matcher.nil? ? "nil" : self.class.callback_matcher.map(&:to_s).join(' ')}'")
172
+ ENV["DEBUG"] && STDERR.puts("Running #{self} -- callback matcher = '#{self.class.callback_matcher.nil? ? "nil" : self.class.callback_matcher.map(&:to_s).join(' ')}'")
173
173
  @arguments = arguments
174
174
 
175
175
  run_callbacks :before_parse unless help_requested?
@@ -0,0 +1,70 @@
1
+ require 'uri'
2
+ module Kontena
3
+ class DebugInstrumentor
4
+ def self.instrument(name, params = {}, &block)
5
+ result = []
6
+ params = params.dup
7
+
8
+ direction = name.split('.').last.capitalize
9
+
10
+ if direction == 'Request'
11
+ uri = URI.parse("#{params[:scheme]}://#{params[:host]}:#{params[:port]}")
12
+ uri.path = params[:path]
13
+ uri.query = URI.encode_www_form(params[:query]) if params[:query] && !params[:query].empty?
14
+ str = "#{params[:method].to_s.upcase} #{uri}"
15
+ str << " (ssl_verify: #{params[:ssl_verify_peer]}) " if params[:scheme] == 'https'
16
+ result << str
17
+ end
18
+
19
+ if params[:headers]
20
+ str = "Headers: {"
21
+ heads = []
22
+ heads << "Accept: #{params[:headers]['Accept']}" if params[:headers]['Accept']
23
+ heads << "Content-Type: #{params[:headers]['Content-Type']}" if params[:headers]['Content-Type']
24
+ heads << "Authorization: #{params[:headers]['Authorization'].split(' ', 2).first}" if params[:headers]['Authorization']
25
+ str << heads.join(', ')
26
+ str << "} "
27
+ result << str
28
+ end
29
+
30
+ if params[:status]
31
+ str = "Status: "
32
+ if params[:status] < 299
33
+ str << Kontena.pastel.green(params[:status])
34
+ else
35
+ str << Kontena.pastel.red(params[:status])
36
+ end
37
+ result << str
38
+ end
39
+
40
+ if params[:body] && !params[:body].empty?
41
+ str = "Body: "
42
+ if ENV["DEBUG"] == "api"
43
+ str << "\n"
44
+ str << params[:body]
45
+ else
46
+ body = params[:body].inspect.strip
47
+ str << body[0,80]
48
+ if body.length > 80
49
+ str << "...\""
50
+ end
51
+ end
52
+ result << str
53
+ end
54
+
55
+ if $stderr.tty?
56
+ if direction == 'Request'
57
+ $stderr.puts(Kontena.pastel.blue("[API Client #{direction}]: #{result.join(" | ")}"))
58
+ else
59
+ $stderr.puts(Kontena.pastel.magenta("[API Client #{direction}]: #{result.join(" | ")}"))
60
+ end
61
+ else
62
+ $stderr.puts("[API Client #{direction}]: #{result.join(" | ")}")
63
+ end
64
+
65
+ if block_given?
66
+ yield
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,103 @@
1
+ require 'tty-prompt'
2
+ require 'pastel'
3
+
4
+ module Kontena
5
+ class LightPrompt
6
+
7
+ attr_reader :prompt
8
+
9
+ extend Forwardable
10
+
11
+ class Menu
12
+ attr_reader :choices, :calls
13
+
14
+ def initialize
15
+ @choices = []
16
+ @calls = {}
17
+ end
18
+
19
+ def choice(text, label)
20
+ choices << [text, label]
21
+ end
22
+
23
+ def add_quit_choice
24
+ choice('(done)', :done)
25
+ end
26
+
27
+ def remove_choice(value)
28
+ choices.reject! { |c| c.last == value }
29
+ end
30
+
31
+ def remove_choices(values)
32
+ values.each { |v| remove_choice(v) }
33
+ end
34
+
35
+ def method_missing(meth, *args)
36
+ calls[meth] = args
37
+ end
38
+
39
+ def respond_to_missing?(meth, privates = false)
40
+ prompt.respond_to?(meth, privates)
41
+ end
42
+ end
43
+
44
+ def initialize(options={})
45
+ @prompt = TTY::Prompt.new(options)
46
+ end
47
+
48
+ def select(*args, &block)
49
+ choice_collector = Menu.new
50
+ yield choice_collector
51
+
52
+ prompt.enum_select(*args) do |menu|
53
+ choice_collector.calls.each do |meth, args|
54
+ if menu.respond_to?(meth)
55
+ menu.send(meth, *args)
56
+ end
57
+ end
58
+ choice_collector.choices.each do |choice|
59
+ menu.choice choice.first, choice.last
60
+ end
61
+ end
62
+ end
63
+
64
+ def multi_select(*args, &block)
65
+ choice_collector = Menu.new
66
+ yield choice_collector
67
+ choice_collector.add_quit_choice
68
+
69
+ selections = []
70
+
71
+ loop do
72
+ choice_collector.remove_choices(selections)
73
+
74
+ answer = prompt.enum_select(*args) do |menu|
75
+ choice_collector.calls.each do |meth, args|
76
+ if menu.respond_to?(meth)
77
+ menu.send(meth, *args)
78
+ end
79
+ end
80
+ choice_collector.choices.each do |choice|
81
+ menu.choice choice.first, choice.last
82
+ end
83
+ end
84
+
85
+ break if answer == :done
86
+ selections << answer
87
+ end
88
+
89
+ selections
90
+ end
91
+
92
+
93
+ def_delegators :prompt, :ask, :yes?, :error
94
+
95
+ def method_missing(meth, *args)
96
+ prompt.send(meth, *args)
97
+ end
98
+
99
+ def respond_to_missing?(meth, privates = false)
100
+ prompt.respond_to?(meth, privates)
101
+ end
102
+ end
103
+ end
@@ -2,27 +2,174 @@ require 'singleton'
2
2
 
3
3
  module Kontena
4
4
  class PluginManager
5
+
5
6
  include Singleton
6
7
 
7
8
  CLI_GEM = 'kontena-cli'.freeze
8
9
  MIN_CLI_VERSION = '0.15.99'.freeze
9
10
 
10
- attr_reader :plugins
11
+ # Initialize plugin manager
12
+ def init
13
+ ENV["GEM_HOME"] = install_dir
14
+ Gem.paths = ENV
15
+ plugins
16
+ use_dummy_ui unless ENV["DEBUG"]
17
+ true
18
+ end
19
+
20
+ # Install a plugin
21
+ # @param plugin_name [String]
22
+ # @param pre [Boolean] install a prerelease version if available
23
+ # @param version [String] install a specific version
24
+ def install_plugin(plugin_name, pre: false, version: nil)
25
+ require 'rubygems/dependency_installer'
26
+ require 'rubygems/requirement'
27
+
28
+ cmd = Gem::DependencyInstaller.new(
29
+ document: false,
30
+ force: true,
31
+ prerelease: pre,
32
+ minimal_deps: true
33
+ )
34
+ plugin_version = version.nil? ? Gem::Requirement.default : Gem::Requirement.new(version)
35
+ without_safe { cmd.install(prefix(plugin_name), plugin_version) }
36
+ cleanup_plugin(plugin_name)
37
+ cmd.installed_gems
38
+ end
39
+
40
+ # Uninstall a plugin
41
+ # @param plugin_name [String]
42
+ def uninstall_plugin(plugin_name)
43
+ installed = installed(plugin_name)
44
+ raise "Plugin #{plugin_name} not installed" unless installed
45
+
46
+ require 'rubygems/uninstaller'
47
+ cmd = Gem::Uninstaller.new(
48
+ installed.name,
49
+ all: true,
50
+ executables: true,
51
+ force: true,
52
+ install_dir: installed.base_dir
53
+ )
54
+ cmd.uninstall
55
+ end
56
+
57
+ # Search rubygems for kontena plugins
58
+ # @param pattern [String] optional search pattern
59
+ def search_plugins(pattern = nil)
60
+ client = Excon.new('https://rubygems.org')
61
+ response = client.get(
62
+ path: "/api/v1/search.json?query=#{prefix(pattern)}",
63
+ headers: {
64
+ 'Content-Type' => 'application/json',
65
+ 'Accept' => 'application/json'
66
+ }
67
+ )
68
+
69
+ JSON.parse(response.body) rescue nil
70
+ end
71
+
72
+ # Retrieve plugin versions from rubygems
73
+ # @param plugin_name [String]
74
+ def gem_versions(plugin_name)
75
+ client = Excon.new('https://rubygems.org')
76
+ response = client.get(
77
+ path: "/api/v1/versions/#{prefix(plugin_name)}.json",
78
+ headers: {
79
+ 'Content-Type' => 'application/json',
80
+ 'Accept' => 'application/json'
81
+ }
82
+ )
83
+ versions = JSON.parse(response.body)
84
+ versions.map { |version| Gem::Version.new(version["number"]) }.sort.reverse
85
+ end
86
+
87
+ # Get the latest version number from rubygems
88
+ # @param plugin_name [String]
89
+ # @param pre [Boolean] include prerelease versions
90
+ def latest_version(plugin_name, pre: false)
91
+ return gem_versions(plugin_name).first if pre
92
+ gem_versions(plugin_name).find { |version| !version.prerelease? }
93
+ end
94
+
95
+ # Find a plugin by name from installed plugins
96
+ # @param plugin_name [String]
97
+ def installed(plugin_name)
98
+ search = prefix(plugin_name)
99
+ plugins.find {|plugin| plugin.name == search }
100
+ end
101
+
102
+ # Upgrade an installed plugin
103
+ # @param plugin_name [String]
104
+ # @param pre [Boolean] upgrade to a prerelease version if available. Will happen always when the installed version is a prerelease version.
105
+ def upgrade_plugin(plugin_name, pre: false)
106
+ installed = installed(plugin_name)
107
+ if installed.version.prerelease?
108
+ pre = true
109
+ end
110
+
111
+ if installed
112
+ latest = latest_version(plugin_name, pre: pre)
113
+ if latest > installed.version
114
+ install_plugin(plugin_name, version: latest.to_s)
115
+ end
116
+ else
117
+ raise "Plugin #{plugin_name} not installed"
118
+ end
119
+ end
120
+
121
+ # Runs gem cleanup, removes remains from previous versions
122
+ # @param plugin_name [String]
123
+ def cleanup_plugin(plugin_name)
124
+ require 'rubygems/commands/cleanup_command'
125
+ cmd = Gem::Commands::CleanupCommand.new
126
+ options = ['--norc']
127
+ options += ['-q', '--no-verbose'] unless ENV["DEBUG"]
128
+ cmd.handle_options options
129
+ without_safe { cmd.execute }
130
+ rescue Gem::SystemExitException => e
131
+ return true if e.exit_code == 0
132
+ raise
133
+ end
11
134
 
12
- def initialize
13
- @plugins = []
135
+ # Gem installation directory
136
+ # @return [String]
137
+ def install_dir
138
+ return @install_dir if @install_dir
139
+ install_dir = File.join(Dir.home, '.kontena', 'gems', RUBY_VERSION)
140
+ unless File.directory?(install_dir)
141
+ require 'fileutils'
142
+ FileUtils.mkdir_p(install_dir, mode: 0700)
143
+ end
144
+ @install_dir = install_dir
14
145
  end
15
146
 
147
+
16
148
  # @return [Array<Gem::Specification>]
149
+ def plugins
150
+ @plugins ||= load_plugins
151
+ end
152
+
153
+ private
154
+
155
+ # Execute block without SafeYAML. Gem does security internally.
156
+ def without_safe(&block)
157
+ SafeYAML::OPTIONS[:default_mode] = :unsafe if Object.const_defined?(:SafeYAML)
158
+ yield
159
+ ensure
160
+ SafeYAML::OPTIONS[:default_mode] = :safe if Object.const_defined?(:SafeYAML)
161
+ end
162
+
17
163
  def load_plugins
164
+ plugins = []
18
165
  Gem::Specification.to_a.each do |spec|
19
166
  spec.require_paths.to_a.each do |require_path|
20
167
  plugin = File.join(spec.gem_dir, require_path, 'kontena_cli_plugin.rb')
21
- if File.exist?(plugin) && !@plugins.find{ |p| p.name == spec.name }
168
+ if File.exist?(plugin) && !plugins.find{ |p| p.name == spec.name }
22
169
  begin
23
170
  if spec_has_valid_dependency?(spec)
24
171
  load(plugin)
25
- @plugins << spec
172
+ plugins << spec
26
173
  else
27
174
  plugin_name = spec.name.sub('kontena-plugin-', '')
28
175
  STDERR.puts " [#{Kontena.pastel.red('error')}] Plugin #{Kontena.pastel.cyan(plugin_name)} (#{spec.version}) is not compatible with the current cli version."
@@ -39,11 +186,25 @@ module Kontena
39
186
  end
40
187
  end
41
188
  end
42
- @plugins
189
+ plugins
43
190
  rescue => exc
44
191
  STDERR.puts exc.message
45
192
  end
46
193
 
194
+ def prefix(plugin_name)
195
+ return plugin_name if plugin_name.to_s.start_with?('kontena-plugin-')
196
+ "kontena-plugin-#{plugin_name}"
197
+ end
198
+
199
+ def dummy_ui
200
+ Gem::StreamUI.new(StringIO.new, StringIO.new, StringIO.new, false)
201
+ end
202
+
203
+ def use_dummy_ui
204
+ require 'rubygems/user_interaction'
205
+ Gem::DefaultUserInteraction.ui = dummy_ui
206
+ end
207
+
47
208
  # @param [Gem::Specification] spec
48
209
  # @return [Boolean]
49
210
  def spec_has_valid_dependency?(spec)