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.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/VERSION +1 -1
- data/bin/kontena +4 -1
- data/kontena-cli.gemspec +1 -1
- data/lib/kontena/callback.rb +1 -1
- data/lib/kontena/callbacks/master/01_clear_current_master_after_terminate.rb +1 -1
- data/lib/kontena/callbacks/master/deploy/50_authenticate_after_deploy.rb +5 -5
- data/lib/kontena/callbacks/master/deploy/55_create_initial_grid_after_deploy.rb +1 -1
- data/lib/kontena/callbacks/master/deploy/56_set_server_provider_after_deploy.rb +25 -0
- data/lib/kontena/callbacks/master/deploy/70_invite_self_after_deploy.rb +1 -1
- data/lib/kontena/cli/common.rb +3 -3
- data/lib/kontena/cli/config.rb +1 -1
- data/lib/kontena/cli/grid_command.rb +2 -0
- data/lib/kontena/cli/grids/common.rb +12 -0
- data/lib/kontena/cli/grids/health_command.rb +69 -0
- data/lib/kontena/cli/helpers/health_helper.rb +53 -0
- data/lib/kontena/cli/localhost_web_server.rb +3 -3
- data/lib/kontena/cli/master/users/invite_command.rb +1 -1
- data/lib/kontena/cli/node_command.rb +2 -0
- data/lib/kontena/cli/nodes/health_command.rb +32 -0
- data/lib/kontena/cli/nodes/list_command.rb +40 -26
- data/lib/kontena/cli/nodes/show_command.rb +0 -1
- data/lib/kontena/cli/plugins/install_command.rb +28 -30
- data/lib/kontena/cli/plugins/search_command.rb +6 -14
- data/lib/kontena/cli/plugins/uninstall_command.rb +7 -11
- data/lib/kontena/cli/services/stats_command.rb +4 -2
- data/lib/kontena/cli/spinner.rb +20 -4
- data/lib/kontena/cli/stacks/show_command.rb +5 -1
- data/lib/kontena/cli/stacks/yaml/opto/service_instances_resolver.rb +22 -0
- data/lib/kontena/cli/stacks/yaml/opto/vault_setter.rb +1 -1
- data/lib/kontena/cli/stacks/yaml/reader.rb +1 -0
- data/lib/kontena/cli/vault/export_command.rb +22 -0
- data/lib/kontena/cli/vault/import_command.rb +80 -0
- data/lib/kontena/cli/vault/list_command.rb +4 -0
- data/lib/kontena/cli/vault/read_command.rb +8 -3
- data/lib/kontena/cli/vault/remove_command.rb +2 -1
- data/lib/kontena/cli/vault/update_command.rb +5 -7
- data/lib/kontena/cli/vault_command.rb +5 -1
- data/lib/kontena/client.rb +25 -2
- data/lib/kontena/command.rb +1 -1
- data/lib/kontena/debug_instrumentor.rb +70 -0
- data/lib/kontena/light_prompt.rb +103 -0
- data/lib/kontena/plugin_manager.rb +167 -6
- data/lib/kontena/stacks_cache.rb +1 -1
- data/lib/kontena_cli.rb +23 -6
- data/spec/kontena/cli/grids/health_command_spec.rb +390 -0
- data/spec/kontena/cli/nodes/health_command_spec.rb +206 -0
- data/spec/kontena/cli/nodes/list_command_spec.rb +205 -0
- data/spec/kontena/cli/vault/export_spec.rb +32 -0
- data/spec/kontena/cli/vault/import_spec.rb +69 -0
- data/spec/kontena/client_spec.rb +39 -0
- data/spec/kontena/plugin_manager_spec.rb +7 -7
- data/spec/spec_helper.rb +1 -0
- data/spec/support/output_helpers.rb +51 -0
- metadata +27 -6
@@ -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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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 =
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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' % ['
|
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
|
-
|
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
|
##
|
data/lib/kontena/cli/spinner.rb
CHANGED
@@ -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 =
|
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
|
@@ -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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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
|
-
|
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:
|
19
|
+
value: value,
|
22
20
|
upsert: upsert?
|
23
21
|
}
|
24
|
-
|
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
|