kontena-cli 1.3.0.pre1 → 1.3.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
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