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
@@ -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)