kontena-cli 1.3.0.pre1 → 1.3.0.pre2

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/kontena +2 -1
  4. data/lib/kontena/callback.rb +1 -1
  5. data/lib/kontena/callbacks/auth/01_list_and_select_grid_after_master_auth.rb +1 -2
  6. data/lib/kontena/callbacks/master/01_clear_current_master_after_terminate.rb +2 -3
  7. data/lib/kontena/callbacks/master/deploy/01_show_logo_before_deploy.rb +1 -2
  8. data/lib/kontena/callbacks/master/deploy/05_before_deploy_configuration_wizard.rb +2 -2
  9. data/lib/kontena/callbacks/master/deploy/40_install_ssl_certificate_after_deploy.rb +2 -2
  10. data/lib/kontena/callbacks/master/deploy/50_authenticate_after_deploy.rb +9 -9
  11. data/lib/kontena/callbacks/master/deploy/55_create_initial_grid_after_deploy.rb +2 -2
  12. data/lib/kontena/callbacks/master/deploy/56_set_server_provider_after_deploy.rb +1 -2
  13. data/lib/kontena/callbacks/master/deploy/60_configure_auth_provider_after_deploy.rb +1 -2
  14. data/lib/kontena/callbacks/master/deploy/70_invite_self_after_deploy.rb +2 -3
  15. data/lib/kontena/callbacks/master/deploy/90_proptip_after_deploy.rb +2 -2
  16. data/lib/kontena/cli/apps/common.rb +0 -1
  17. data/lib/kontena/cli/apps/init_command.rb +2 -0
  18. data/lib/kontena/cli/apps/kontena_yml_generator.rb +2 -1
  19. data/lib/kontena/cli/apps/list_command.rb +10 -2
  20. data/lib/kontena/cli/apps/yaml/reader.rb +2 -1
  21. data/lib/kontena/cli/apps/yaml/service_extender.rb +0 -1
  22. data/lib/kontena/cli/cloud/login_command.rb +51 -7
  23. data/lib/kontena/cli/cloud/master/list_command.rb +14 -11
  24. data/lib/kontena/cli/common.rb +36 -83
  25. data/lib/kontena/cli/config.rb +46 -29
  26. data/lib/kontena/cli/containers/list_command.rb +30 -41
  27. data/lib/kontena/cli/etcd/list_command.rb +12 -7
  28. data/lib/kontena/cli/external_registries/list_command.rb +14 -8
  29. data/lib/kontena/cli/grids/list_command.rb +18 -10
  30. data/lib/kontena/cli/grids/trusted_subnets/list_command.rb +7 -5
  31. data/lib/kontena/cli/grids/users/list_command.rb +9 -7
  32. data/lib/kontena/cli/localhost_web_server.rb +3 -3
  33. data/lib/kontena/cli/log_formatters/compact.rb +65 -0
  34. data/lib/kontena/cli/log_formatters/strip_color.rb +13 -0
  35. data/lib/kontena/cli/master/config/import_command.rb +2 -1
  36. data/lib/kontena/cli/master/config/set_command.rb +1 -1
  37. data/lib/kontena/cli/master/list_command.rb +16 -10
  38. data/lib/kontena/cli/master/token/list_command.rb +23 -12
  39. data/lib/kontena/cli/master/user/invite_command.rb +1 -1
  40. data/lib/kontena/cli/master/user/list_command.rb +17 -6
  41. data/lib/kontena/cli/nodes/labels/list_command.rb +3 -0
  42. data/lib/kontena/cli/nodes/list_command.rb +58 -37
  43. data/lib/kontena/cli/nodes/show_command.rb +1 -1
  44. data/lib/kontena/cli/plugins/install_command.rb +2 -2
  45. data/lib/kontena/cli/plugins/list_command.rb +19 -5
  46. data/lib/kontena/cli/plugins/uninstall_command.rb +1 -1
  47. data/lib/kontena/cli/services/containers_command.rb +7 -0
  48. data/lib/kontena/cli/services/envs/list_command.rb +6 -4
  49. data/lib/kontena/cli/services/list_command.rb +47 -36
  50. data/lib/kontena/cli/services/services_helper.rb +9 -16
  51. data/lib/kontena/cli/services/stats_command.rb +2 -1
  52. data/lib/kontena/cli/spinner.rb +3 -5
  53. data/lib/kontena/cli/stacks/common.rb +4 -4
  54. data/lib/kontena/cli/stacks/list_command.rb +42 -33
  55. data/lib/kontena/cli/stacks/registry/search_command.rb +6 -0
  56. data/lib/kontena/cli/stacks/registry/show_command.rb +2 -0
  57. data/lib/kontena/cli/stacks/registry_command.rb +1 -2
  58. data/lib/kontena/cli/stacks/validate_command.rb +1 -0
  59. data/lib/kontena/cli/stacks/yaml/reader.rb +3 -2
  60. data/lib/kontena/cli/stacks/yaml/service_extender.rb +0 -1
  61. data/lib/kontena/cli/stacks/yaml/validations.rb +1 -1
  62. data/lib/kontena/cli/table_generator.rb +125 -0
  63. data/lib/kontena/cli/vault/export_command.rb +7 -4
  64. data/lib/kontena/cli/vault/import_command.rb +3 -0
  65. data/lib/kontena/cli/vault/list_command.rb +23 -10
  66. data/lib/kontena/cli/volumes/create_command.rb +8 -4
  67. data/lib/kontena/cli/volumes/list_command.rb +15 -7
  68. data/lib/kontena/client.rb +44 -33
  69. data/lib/kontena/command.rb +7 -4
  70. data/lib/kontena/debug_instrumentor.rb +10 -9
  71. data/lib/kontena/main_command.rb +1 -3
  72. data/lib/kontena/plugin_manager.rb +15 -7
  73. data/lib/kontena/stacks_cache.rb +7 -7
  74. data/lib/kontena/stacks_client.rb +24 -5
  75. data/lib/kontena/util.rb +43 -15
  76. data/lib/kontena_cli.rb +71 -14
  77. data/spec/kontena/cli/cloud/login_command_spec.rb +42 -0
  78. data/spec/kontena/cli/containers/list_command_spec.rb +1 -2
  79. data/spec/kontena/cli/nodes/list_command_spec.rb +153 -126
  80. data/spec/kontena/cli/registry/create_spec.rb +22 -0
  81. data/spec/kontena/cli/services/stats_command_spec.rb +22 -0
  82. data/spec/kontena/cli/table_generator_spec.rb +118 -0
  83. data/spec/kontena/cli/version_command_spec.rb +2 -2
  84. data/spec/kontena/client_spec.rb +4 -3
  85. data/spec/support/client_helpers.rb +3 -3
  86. data/spec/support/output_helpers.rb +54 -8
  87. metadata +11 -2
@@ -17,7 +17,7 @@ module Kontena::Cli::Nodes
17
17
  puts " agent version: #{node['agent_version']}"
18
18
  puts " docker version: #{node['docker_version']}"
19
19
  puts " connected: #{node['connected'] ? 'yes': 'no'}"
20
- puts " last connect: #{node['updated_at']}"
20
+ puts " last connect: #{node['connected_at']}"
21
21
  puts " last seen: #{node['last_seen_at']}"
22
22
  puts " public ip: #{node['public_ip']}"
23
23
  puts " private ip: #{node['private_ip']}"
@@ -19,7 +19,7 @@ module Kontena::Cli::Plugins
19
19
  Kontena::PluginManager.instance.upgrade_plugin(name, pre: pre?)
20
20
  rescue => ex
21
21
  $stderr.puts pastel.red("#{ex.class.name} : #{ex.message}")
22
- ENV["DEBUG"] && $stderr.puts(ex.backtrace.join("\n "))
22
+ logger.error(ex)
23
23
  spin.fail!
24
24
  end
25
25
  end
@@ -33,7 +33,7 @@ module Kontena::Cli::Plugins
33
33
  Kontena::PluginManager.instance.install_plugin(name, pre: pre?, version: version)
34
34
  rescue => ex
35
35
  $stderr.puts pastel.red("#{ex.class.name} : #{ex.message}")
36
- ENV["DEBUG"] && $stderr.puts(ex.backtrace.join("\n "))
36
+ logger.error(ex)
37
37
  spin.fail!
38
38
  end
39
39
  end
@@ -2,14 +2,28 @@ require_relative 'common'
2
2
 
3
3
  module Kontena::Cli::Plugins
4
4
  class ListCommand < Kontena::Command
5
+ include Kontena::Cli::Common
6
+ include Kontena::Cli::TableGenerator::Helper
5
7
  include Common
6
8
 
7
- def execute
8
- titles = ['NAME', 'VERSION', 'DESCRIPTION']
9
- puts "%-40s %-10s %-40s" % titles
10
- Kontena::PluginManager.instance.plugins.each do |plugin|
11
- puts "%-40s %-10s %-40s" % [short_name(plugin.name), plugin.version, plugin.description]
9
+ banner "List installed plugins"
10
+
11
+ def fields
12
+ quiet? ? [:name] : %i(name version description)
13
+ end
14
+
15
+ def plugins
16
+ Kontena::PluginManager.instance.plugins.map do |plugin|
17
+ {
18
+ name: short_name(plugin.name),
19
+ version: plugin.version,
20
+ description: plugin.description
21
+ }
12
22
  end
13
23
  end
24
+
25
+ def execute
26
+ print_table(plugins)
27
+ end
14
28
  end
15
29
  end
@@ -15,7 +15,7 @@ module Kontena::Cli::Plugins
15
15
  Kontena::PluginManager.instance.uninstall_plugin(name)
16
16
  rescue => ex
17
17
  $stderr.puts pastel.red("#{ex.class.name} : #{ex.message}")
18
- ENV["DEBUG"] && $stderr.puts(ex.backtrace.join("\n "))
18
+ logger.error(ex)
19
19
  spin.fail
20
20
  end
21
21
  end
@@ -7,12 +7,19 @@ module Kontena::Cli::Services
7
7
  include ServicesHelper
8
8
 
9
9
  parameter "NAME", "Service name"
10
+ option ['-q', '--quiet'], :flag, "Output the identifying column only"
10
11
 
11
12
  def execute
12
13
  require_api_url
13
14
  token = require_token
14
15
 
15
16
  result = client(token).get("services/#{parse_service_id(name)}/containers")
17
+
18
+ if quiet?
19
+ puts result['containers'].map { |c| "#{c['node']['name']}/#{c['name']}" }.join("\n")
20
+ exit 0
21
+ end
22
+
16
23
  result['containers'].each do |container|
17
24
  puts "#{container['name']}:"
18
25
  puts " rev: #{container['deploy_rev']}"
@@ -5,15 +5,17 @@ module Kontena::Cli::Services::Envs
5
5
  include Kontena::Cli::Common
6
6
  include Kontena::Cli::GridOptions
7
7
  include Kontena::Cli::Services::ServicesHelper
8
+ include Kontena::Cli::TableGenerator::Helper
8
9
 
9
10
  parameter "NAME", "Service name"
10
11
 
12
+ requires_current_master
13
+ requires_current_master_token
14
+
11
15
  def execute
12
- require_api_url
13
- token = require_token
14
- service = client(token).get("services/#{parse_service_id(name)}")
16
+ service = client.get("services/#{parse_service_id(name)}")
15
17
  service["env"].sort.each do |env|
16
- puts env
18
+ puts quiet? ? env.split('=', 2).first : env
17
19
  end
18
20
  end
19
21
  end
@@ -4,53 +4,64 @@ module Kontena::Cli::Services
4
4
  class ListCommand < Kontena::Command
5
5
  include Kontena::Cli::Common
6
6
  include Kontena::Cli::GridOptions
7
+ include Kontena::Cli::TableGenerator::Helper
7
8
  include ServicesHelper
8
9
 
9
- option ["-q", "--quiet"], :flag, "Show only service names"
10
10
  option '--stack', 'STACK', 'Stack name'
11
11
 
12
- def execute
13
- require_api_url
14
- token = require_token
12
+ requires_current_master
13
+ requires_current_master_token
14
+
15
+ def services
16
+ client.get("grids/#{current_grid}/services#{"?stack=#{stack}" if stack}")['services'].sort_by{|s| s['updated_at'] }.reverse
17
+ end
18
+
19
+ def fields
20
+ quiet? ? ['name'] : {' ' => 'health_icon', name: 'name', instances: 'instances', stateful: 'stateful', state: 'state', "exposed ports" => 'ports' }
21
+ end
15
22
 
16
- grids = client(token).get("grids/#{current_grid}/services?stack=#{stack}")
17
- services = grids['services'].sort_by{|s| s['updated_at'] }.reverse
23
+ def service_port(port)
24
+ "#{port['ip']}:#{port['node_port']}->#{port['container_port']}/#{port['protocol']}"
25
+ end
26
+
27
+ def stack_id(service)
18
28
  if quiet?
19
- services.each do |service|
20
- puts "#{service.dig('stack', 'id')}/#{service['name']}"
21
- end
29
+ service.fetch('stack', {}).fetch('id', 'null')
22
30
  else
23
- titles = ['NAME', 'INSTANCES', 'STATEFUL', 'STATE', 'EXPOSED PORTS']
24
- puts "%-60s %-10s %-8s %-10s %-50s" % titles
25
- services.each do |service|
26
- print_service_row(service)
27
- end
31
+ service.fetch('stack', {}).fetch('name', nil)
28
32
  end
29
33
  end
30
34
 
31
- def print_service_row(service)
32
- stateful = service['stateful'] ? 'yes' : 'no'
33
- running = service['instance_counts']['running']
34
- desired = service['instances']
35
- instances = "#{running} / #{desired}"
36
- ports = service['ports'].map{|p|
37
- "#{p['ip']}:#{p['node_port']}->#{p['container_port']}/#{p['protocol']}"
38
- }.join(", ")
39
- health = health_status(service)
40
- if service.dig('stack', 'name').to_s == 'null'.freeze
41
- name = service['name']
42
- else
43
- name = "#{service.dig('stack', 'name')}/#{service['name']}"
35
+ def service_name(service)
36
+ stack_id = stack_id(service)
37
+ return service['name'] if stack_id == 'null'
38
+ [ stack_id(service), service['name'] ].compact.join('/')
39
+ end
40
+
41
+ def state_color(state)
42
+ case state
43
+ when 'running' then :green
44
+ when 'initialized' then :cyan
45
+ when 'stopped' then :red
46
+ else :blue
47
+ end
48
+ end
49
+
50
+ def execute
51
+ print_table(services) do |row|
52
+ row['name'] = service_name(row)
53
+ next if quiet?
54
+ row['health_icon'] = health_status_icon(health_status(row))
55
+ row['stateful'] = row['stateful'] ? pastel.green('yes') : 'no'
56
+ row['ports'] = row['ports'].map(&method(:service_port)).join(',')
57
+ row['state'] = pastel.send(state_color(row['state']), row['state'])
58
+
59
+ instances = [row['instance_counts']['running'], row['instances']]
60
+ if instances.first < instances.last
61
+ instances[0] = pastel.cyan(instances[0].to_s)
62
+ end
63
+ row['instances'] = instances.join(' / ')
44
64
  end
45
- vars = [
46
- health_status_icon(health),
47
- name,
48
- instances,
49
- stateful,
50
- service['state'],
51
- ports
52
- ]
53
- puts "%s %-58s %-10.10s %-8s %-10s %-50s" % vars
54
65
  end
55
66
  end
56
67
  end
@@ -357,23 +357,16 @@ module Kontena
357
357
  # @param [Array<String>] port_options
358
358
  # @return [Array<Hash>]
359
359
  def parse_ports(port_options)
360
- port_options.map{|p|
361
- port, protocol = p.split('/')
362
- protocol ||= 'tcp'
363
- port_elements = port.split(':')
364
- container_port = port_elements[-1]
365
- node_port = port_elements[-2]
366
- ip = port_elements[-3] || '0.0.0.0'
367
- if node_port.nil? || container_port.nil?
368
- raise ArgumentError.new("Invalid port value #{p}")
369
- end
360
+ port_regex = Regexp.new(/\A(?<ip>\d+\.\d+\.\d+\.\d+)?:?(?<node_port>\d+)\:(?<container_port>\d+)\/?(?<protocol>\w+)?\z/)
361
+ port_options.map do |p|
362
+ match_data = port_regex.match(p.to_s)
363
+ raise ArgumentError, "Invalid port value #{p}" unless match_data
364
+
370
365
  {
371
- ip: ip,
372
- container_port: container_port,
373
- node_port: node_port,
374
- protocol: protocol
375
- }
376
- }
366
+ ip: '0.0.0.0',
367
+ protocol: 'tcp'
368
+ }.merge(match_data.names.map { |name| [name.to_sym, match_data[name]] }.to_h.reject { |_,v| v.nil? })
369
+ end
377
370
  end
378
371
 
379
372
  # @param [Array<String>] link_options
@@ -7,7 +7,8 @@ module Kontena::Cli::Services
7
7
  include ServicesHelper
8
8
 
9
9
  MEM_MAX_LIMITS = [
10
- 1.8446744073709552e+19, 9.223372036854772e+18
10
+ 2**64,
11
+ 2**63 - 4096
11
12
  ]
12
13
 
13
14
  parameter "NAME", "Service name"
@@ -69,7 +69,7 @@ module Kontena
69
69
  end
70
70
  rescue Exception => ex
71
71
  Kernel.puts "* #{msg}.. fail"
72
- ENV["DEBUG"] && $stderr.puts("#{ex.class.name} : #{ex.message}\n#{ex.backtrace.join("\n ")}")
72
+ Kontena.logger.error(ex)
73
73
  raise ex
74
74
  end
75
75
  exit(status) if status
@@ -156,13 +156,11 @@ module Kontena
156
156
  rescue SpinAbort
157
157
  spin_thread.kill
158
158
  Kernel.puts "\r [" + "fail".colorize(:red) + "] #{msg} "
159
- if ENV["DEBUG"]
160
- $stderr.puts "Spin aborted through fail!"
161
- end
159
+ Kontena.logger.debug { "Spin aborted through fail!" }
162
160
  rescue Exception => ex
163
161
  spin_thread.kill
164
162
  Kernel.puts "\r [" + "fail".colorize(:red) + "] #{msg} "
165
- ENV["DEBUG"] && $stderr.puts("#{ex.class.name} : #{ex.message}\n#{ex.backtrace.join("\n ")}")
163
+ Kontena.logger.error(ex)
166
164
  raise ex
167
165
  ensure
168
166
  unless Thread.main['spinner_msgs'].empty?
@@ -3,6 +3,9 @@ require_relative '../services/services_helper'
3
3
  require_relative 'service_generator_v2'
4
4
  require_relative '../../stacks_client'
5
5
 
6
+ require "safe_yaml"
7
+ SafeYAML::OPTIONS[:default_mode] = :safe
8
+
6
9
  module Kontena::Cli::Stacks
7
10
  module Common
8
11
  include Kontena::Cli::Services::ServicesHelper
@@ -153,10 +156,7 @@ module Kontena::Cli::Stacks
153
156
  end
154
157
 
155
158
  def stacks_client
156
- return @stacks_client if @stacks_client
157
- Kontena.run!(%w(cloud login)) unless cloud_auth?
158
- config.reset_instance
159
- @stacks_client = Kontena::StacksClient.new(kontena_account.stacks_url, kontena_account.token)
159
+ @stacks_client ||= Kontena::StacksClient.new(current_account.stacks_url, current_account.token, read_requires_token: current_account.stacks_read_authentication)
160
160
  end
161
161
  end
162
162
  end
@@ -4,6 +4,7 @@ module Kontena::Cli::Stacks
4
4
  class ListCommand < Kontena::Command
5
5
  include Kontena::Cli::Common
6
6
  include Kontena::Cli::GridOptions
7
+ include Kontena::Cli::TableGenerator::Helper
7
8
  include Common
8
9
 
9
10
  banner "Lists all installed stacks on a grid in Kontena Master"
@@ -11,46 +12,54 @@ module Kontena::Cli::Stacks
11
12
  requires_current_master
12
13
  requires_current_master_token
13
14
 
14
- def execute
15
- list_stacks
16
- end
15
+ HEALTH_ICONS = {
16
+ unhealthy: Kontena.pastel.red('⊗').freeze,
17
+ partial: Kontena.pastel.yellow('⊙').freeze,
18
+ healthy: Kontena.pastel.green('⊛').freeze,
19
+ default: Kontena.pastel.dim('⊝').freeze
20
+ }
17
21
 
18
- def list_stacks
19
- response = client.get("grids/#{current_grid}/stacks")
20
-
21
- titles = ['NAME', 'STACK', 'SERVICES', 'STATE', 'EXPOSED PORTS']
22
- puts "%-30s %-40s %-10s %-10s %-50s" % titles
22
+ def stacks
23
+ client.get("grids/#{current_grid}/stacks")['stacks']
24
+ end
23
25
 
24
- response['stacks'].each do |stack|
25
- ports = stack_ports(stack)
26
- health = stack_health(stack)
27
- if health == :unhealthy
28
- icon = ''.freeze
29
- color = :red
30
- elsif health == :partial
31
- icon = ''.freeze
32
- color = :yellow
33
- elsif health == :healthy
34
- icon = '⊛'.freeze
35
- color = :green
36
- else
37
- icon = '⊝'.freeze
38
- color = :dim
39
- end
26
+ def fields
27
+ return ['name'] if quiet?
28
+ {
29
+ ' ' => 'health_icon',
30
+ name: 'name',
31
+ stack: 'stack',
32
+ services: 'services_count',
33
+ state: 'state',
34
+ 'exposed ports' => 'ports'
35
+ }
36
+ end
40
37
 
41
- vars = [
42
- icon.colorize(color),
43
- "#{stack['name']}",
44
- "#{stack['stack']}:#{stack['version']}",
45
- stack['services'].size,
46
- stack['state'],
47
- ports.join(",")
48
- ]
38
+ def execute
39
+ print_table(stacks) do |row|
40
+ next if quiet?
41
+ row['health_icon'] = health_icon(stack_health(row))
42
+ row['stack'] = "#{row['stack']}:#{row['version']}"
43
+ row['services_count'] = row['services'].size
44
+ row['ports'] = stack_ports(row).join(',')
45
+ row['state'] = pastel.send(state_color(row['state']), row['state'])
46
+ end
47
+ end
49
48
 
50
- puts "%s %-28s %-40s %-10s %-10s %-50s" % vars
49
+ def state_color(state)
50
+ case state
51
+ when 'running' then :green
52
+ when 'deploying', 'initialized' then :blue
53
+ when 'stopped' then :red
54
+ when 'partially_running' then :yellow
55
+ else :clear
51
56
  end
52
57
  end
53
58
 
59
+ def health_icon(health)
60
+ HEALTH_ICONS.fetch(health) { HEALTH_ICONS[:default] }
61
+ end
62
+
54
63
  # @param [Hash] stack
55
64
  # @return [Array<String>]
56
65
  def stack_ports(stack)
@@ -9,8 +9,14 @@ module Kontena::Cli::Stacks::Registry
9
9
 
10
10
  parameter "[QUERY]", "Query string"
11
11
 
12
+ option ['-q', '--quiet'], :flag, "Output the identifying column only"
13
+
12
14
  def execute
13
15
  results = stacks_client.search(query.to_s)
16
+ if quiet?
17
+ puts results.map { |s| s['stack'] }.join("\n")
18
+ exit 0
19
+ end
14
20
  exit_with_error 'Nothing found' if results.empty?
15
21
  titles = ['NAME', 'VERSION', 'DESCRIPTION']
16
22
  columns = "%-40s %-10s %-40s"
@@ -14,6 +14,8 @@ module Kontena::Cli::Stacks::Registry
14
14
 
15
15
  def execute
16
16
  require 'semantic'
17
+ require "safe_yaml"
18
+ SafeYAML::OPTIONS[:default_mode] = :safe
17
19
  unless versions?
18
20
  stack = ::YAML.safe_load(stacks_client.show(stack_name, stack_version))
19
21
  puts "#{stack['stack']}:"
@@ -1,9 +1,8 @@
1
1
  module Kontena::Cli::Stacks
2
2
  class RegistryCommand < Kontena::Command
3
-
4
3
  subcommand "push", "Push a stack into the stacks registry", load_subcommand('stacks/registry/push_command')
5
4
  subcommand "pull", "Pull a stack from the stacks registry", load_subcommand('stacks/registry/pull_command')
6
- subcommand "search", "Search for stacks in the stacks registry", load_subcommand('stacks/registry/search_command')
5
+ subcommand ["search"], "Search for stacks in the stacks registry", load_subcommand('stacks/registry/search_command')
7
6
  subcommand "show", "Show info about a stack in the stacks registry", load_subcommand('stacks/registry/show_command')
8
7
  subcommand ["remove", "rm"], "Remove a stack (or version) from the stacks registry", load_subcommand('stacks/registry/remove_command')
9
8
  end
@@ -1,4 +1,5 @@
1
1
  require_relative 'common'
2
+ require 'yaml'
2
3
 
3
4
  module Kontena::Cli::Stacks
4
5
  class ValidateCommand < Kontena::Command