kontena-cli 1.1.0.rc1 → 1.1.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/kontena/cli/apps/deploy_command.rb +1 -5
  4. data/lib/kontena/cli/cloud/login_command.rb +9 -1
  5. data/lib/kontena/cli/common.rb +2 -2
  6. data/lib/kontena/cli/etcd/health_command.rb +58 -0
  7. data/lib/kontena/cli/etcd_command.rb +2 -0
  8. data/lib/kontena/cli/external_registry_command.rb +0 -2
  9. data/lib/kontena/cli/master/login_command.rb +5 -7
  10. data/lib/kontena/cli/service_command.rb +2 -2
  11. data/lib/kontena/cli/services/deploy_command.rb +1 -5
  12. data/lib/kontena/cli/services/exec_command.rb +84 -0
  13. data/lib/kontena/cli/services/services_helper.rb +4 -1
  14. data/lib/kontena/cli/stacks/common.rb +6 -17
  15. data/lib/kontena/cli/stacks/install_command.rb +2 -10
  16. data/lib/kontena/cli/stacks/show_command.rb +30 -4
  17. data/lib/kontena/cli/stacks/upgrade_command.rb +20 -7
  18. data/lib/kontena/cli/stacks/validate_command.rb +1 -9
  19. data/lib/kontena/cli/stacks/yaml/opto/service_link_resolver.rb +45 -0
  20. data/lib/kontena/cli/stacks/yaml/opto/vault_cert_prompt_resolver.rb +15 -0
  21. data/lib/kontena/cli/stacks/yaml/opto/vault_resolver.rb +1 -0
  22. data/lib/kontena/cli/stacks/yaml/reader.rb +36 -26
  23. data/lib/kontena/command.rb +5 -0
  24. data/lib/kontena/main_command.rb +5 -4
  25. data/lib/kontena_cli.rb +4 -0
  26. data/spec/fixtures/stack-with-prompted-variables.yml +5 -1
  27. data/spec/fixtures/stack-with-variables.yml +5 -1
  28. data/spec/kontena/cli/cloud/login_command_spec.rb +1 -0
  29. data/spec/kontena/cli/etcd/health_command_spec.rb +87 -0
  30. data/spec/kontena/cli/master/login_command_spec.rb +8 -17
  31. data/spec/kontena/cli/services/exec_command_spec.rb +137 -0
  32. data/spec/kontena/cli/stacks/install_command_spec.rb +5 -5
  33. data/spec/kontena/cli/stacks/upgrade_command_spec.rb +39 -32
  34. data/spec/kontena/cli/stacks/yaml/reader_spec.rb +22 -0
  35. data/spec/support/client_helpers.rb +6 -2
  36. data/spec/support/output_helpers.rb +23 -0
  37. metadata +11 -7
  38. data/lib/kontena/cli/external_registries/delete_command.rb +0 -15
  39. data/lib/kontena/cli/login_command.rb +0 -12
  40. data/lib/kontena/cli/register_command.rb +0 -9
  41. data/lib/kontena/cli/services/delete_command.rb +0 -19
@@ -13,22 +13,35 @@ module Kontena::Cli::Stacks
13
13
  include Common::StackFileOrNameParam
14
14
  include Common::StackValuesFromOption
15
15
 
16
- option '--deploy', :flag, 'Deploy after upgrade'
16
+ option '--[no-]deploy', :flag, 'Trigger deploy after upgrade', default: true
17
17
 
18
18
  requires_current_master
19
19
  requires_current_master_token
20
20
 
21
21
  def execute
22
- require_config_file(filename)
23
- stack = stack_from_yaml(filename, name: name, values: values, from_registry: from_registry)
24
- spinner "Upgrading stack #{pastel.cyan(name)} " do
25
- update_stack(stack)
22
+ master_data = spinner "Reading stack #{pastel.cyan(name)} metadata from Kontena Master" do |spin|
23
+ read_stack || spin.fail!
26
24
  end
27
- Kontena.run("stack deploy #{name}") if deploy?
25
+
26
+ stack = stack_from_yaml(filename, name: name, values: values, defaults: master_data['variables'])
27
+
28
+ spinner "Upgrading stack #{pastel.cyan(name)}" do |spin|
29
+ update_stack(stack) || spin.fail!
30
+ end
31
+
32
+ Kontena.run(['stack', 'deploy', name]) if deploy?
28
33
  end
29
34
 
30
35
  def update_stack(stack)
31
- client.put("stacks/#{current_grid}/#{name}", stack)
36
+ client.put(stack_url, stack)
37
+ end
38
+
39
+ def stack_url
40
+ @stack_url ||= "stacks/#{current_grid}/#{name}"
41
+ end
42
+
43
+ def read_stack
44
+ client.get(stack_url)
32
45
  end
33
46
  end
34
47
  end
@@ -19,15 +19,7 @@ module Kontena::Cli::Stacks
19
19
  requires_current_master_token
20
20
 
21
21
  def execute
22
-
23
- if !File.exist?(filename) && filename =~ /\A[a-zA-Z0-9\_\.\-]+\/[a-zA-Z0-9\_\.\-]+(?::.*)?\z/
24
- from_registry = true
25
- else
26
- from_registry = false
27
- require_config_file(filename)
28
- end
29
-
30
- reader = reader_from_yaml(filename, from_registry: from_registry, name: name, values: values)
22
+ reader = reader_from_yaml(filename, name: name, values: values)
31
23
  outcome = reader.execute
32
24
  hint_on_validation_notifications(outcome[:notifications]) if outcome[:notifications].size > 0
33
25
  abort_on_validation_errors(outcome[:errors]) if outcome[:errors].size > 0
@@ -0,0 +1,45 @@
1
+ module Kontena::Cli::Stacks
2
+ module YAML
3
+ class Opto::Resolvers::ServiceLink < Opto::Resolver
4
+ include Kontena::Cli::Common
5
+
6
+ def resolve
7
+ message = hint['prompt']
8
+ name_filter = hint['name']
9
+ image_filter = hint['image']
10
+ raise "prompt missing" unless message
11
+
12
+ services = client.get("grids/#{current_grid}/services")['services']
13
+ services = filter_by_image(services, image_filter) if image_filter
14
+ services = filter_by_name(services, name_filter) if name_filter
15
+ prompt.select(message) do |menu|
16
+ menu.choice "<none>", nil unless option.required?
17
+ services.each do |s|
18
+ if s.dig('stack', 'name') == 'null'
19
+ name = s['name']
20
+ else
21
+ name = "#{s.dig('stack', 'name')}/#{s['name']}"
22
+ end
23
+ menu.choice name, "#{s.dig('stack', 'name')}/#{s['name']}"
24
+ end
25
+ end
26
+ end
27
+
28
+ def filter_by_image(services, image)
29
+ services.select { |s|
30
+ s['image'].include?(image)
31
+ }
32
+ end
33
+
34
+ def filter_by_name(services, name)
35
+ services.select { |s|
36
+ s['name'].include?(name)
37
+ }
38
+ end
39
+
40
+ def stack
41
+ ENV['STACK']
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ module Kontena::Cli::Stacks
2
+ module YAML
3
+ class Opto::Resolvers::VaultCertPrompt < Opto::Resolver
4
+ include Kontena::Cli::Common
5
+
6
+ def resolve
7
+ message = hint || 'Select SSL certs'
8
+ secrets = client.get("grids/#{current_grid}/secrets")['secrets'].select{ |s|
9
+ s['name'].match(/(ssl|cert)/i)
10
+ }
11
+ prompt.multi_select(hint, secrets.map{ |s| s['name'] })
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,6 +2,7 @@ module Kontena::Cli::Stacks
2
2
  module YAML
3
3
  class Opto::Resolvers::Vault < Opto::Resolver
4
4
  def resolve
5
+ raise RuntimeError, "Missing or empty vault secret name" if hint.to_s.empty?
5
6
  require 'shellwords'
6
7
  Kontena.run("vault read --return #{hint.shellescape}", returning: :result)
7
8
  end
@@ -6,9 +6,9 @@ module Kontena::Cli::Stacks
6
6
  include Kontena::Util
7
7
  include Kontena::Cli::Common
8
8
 
9
- attr_reader :file, :raw_content, :errors, :notifications
9
+ attr_reader :file, :raw_content, :errors, :notifications, :defaults, :values
10
10
 
11
- def initialize(file, skip_validation: false, skip_variables: false, from_registry: false, variables: nil, values: nil)
11
+ def initialize(file, skip_validation: false, skip_variables: false, variables: nil, values: nil, defaults: nil)
12
12
  require 'yaml'
13
13
  require_relative 'service_extender'
14
14
  require_relative 'validator_v3'
@@ -17,15 +17,20 @@ module Kontena::Cli::Stacks
17
17
  require_relative 'opto/vault_resolver'
18
18
  require_relative 'opto/prompt_resolver'
19
19
  require_relative 'opto/service_instances_resolver'
20
+ require_relative 'opto/vault_cert_prompt_resolver'
21
+ require_relative 'opto/service_link_resolver'
20
22
  require 'liquid'
21
23
 
22
24
  @file = file
23
- @from_registry = from_registry
24
25
 
25
26
  if from_registry?
26
27
  require 'shellwords'
27
28
  @raw_content = Kontena::StacksCache.pull(file)
28
29
  @registry = Kontena::StacksCache.registry_url
30
+ elsif from_url?
31
+ require 'open-uri'
32
+ stream = open(file)
33
+ @raw_content = stream.read
29
34
  else
30
35
  @raw_content = File.read(File.expand_path(file))
31
36
  end
@@ -36,6 +41,7 @@ module Kontena::Cli::Stacks
36
41
  @skip_variables = skip_variables
37
42
  @variables = variables
38
43
  @values = values
44
+ @defaults = defaults
39
45
  end
40
46
 
41
47
  def internals_interpolated_yaml
@@ -89,8 +95,15 @@ module Kontena::Cli::Stacks
89
95
  to: :env
90
96
  }
91
97
  )
92
- if @values
93
- @values.each do |key, val|
98
+ if defaults
99
+ defaults.each do |key, val|
100
+ var = variables.option(key)
101
+ var.default = val if var
102
+ end
103
+ end
104
+
105
+ if values
106
+ values.each do |key, val|
94
107
  var = @variables.option(key)
95
108
  var.set(val) if var
96
109
  end
@@ -105,7 +118,7 @@ module Kontena::Cli::Stacks
105
118
  validate unless skip_validation?
106
119
 
107
120
  result = {}
108
- Dir.chdir(from_registry? ? Dir.pwd : File.dirname(File.expand_path(file))) do
121
+ Dir.chdir(from_file? ? File.dirname(File.expand_path(file)) : Dir.pwd) do
109
122
  result[:stack] = raw_yaml['stack']
110
123
  result[:version] = self.stack_version
111
124
  result[:name] = self.stack_name
@@ -119,7 +132,6 @@ module Kontena::Cli::Stacks
119
132
  k == 'GRID' || k == 'STACK' || variables.option(k).to.has_key?(:vault) || variables.option(k).from.has_key?(:vault)
120
133
  end
121
134
  end
122
- result[:vault_keys] = extract_vault_keys(result[:services])
123
135
  end
124
136
  result
125
137
  end
@@ -160,7 +172,15 @@ module Kontena::Cli::Stacks
160
172
  end
161
173
 
162
174
  def from_registry?
163
- !!@from_registry
175
+ file =~ /\A[a-zA-Z0-9\_\.\-]+\/[a-zA-Z0-9\_\.\-]+(?::.*)?\z/ && !File.exist?(file)
176
+ end
177
+
178
+ def from_url?
179
+ file =~ /\A(?:http|https|ftp):\/\//
180
+ end
181
+
182
+ def from_file?
183
+ !from_registry? && !from_url?
164
184
  end
165
185
 
166
186
  # @return [Kontena::Cli::Stacks::YAML::ValidatorV3]
@@ -229,8 +249,8 @@ module Kontena::Cli::Stacks
229
249
  @services ||= fully_interpolated_yaml['services']
230
250
  end
231
251
 
232
- def from_external_file(filename, service_name, from_registry: false)
233
- outcome = Reader.new(filename, skip_validation: skip_validation?, skip_variables: true, from_registry: from_registry, variables: variables).execute(service_name)
252
+ def from_external_file(filename, service_name)
253
+ outcome = Reader.new(filename, skip_validation: skip_validation?, skip_variables: true, variables: variables, defaults: defaults, values: values).execute(service_name)
234
254
  errors.concat outcome[:errors] unless errors.any? { |item| item.has_key?(filename) }
235
255
  notifications.concat outcome[:notifications] unless notifications.any? { |item| item.has_key?(filename) }
236
256
  outcome[:services]
@@ -252,8 +272,11 @@ module Kontena::Cli::Stacks
252
272
  if use_opto
253
273
  opt = variables.option(var)
254
274
  if opt.nil?
255
- raise RuntimeError, "Undeclared variable '#{var}' in #{file}:#{line_num} -- #{row}" if raise_on_unknown
256
- val = nil
275
+ if variables.find { |opt| opt.to[:env][var] }
276
+ val = env[var]
277
+ else
278
+ raise RuntimeError, "Undeclared variable '#{var}' in #{file}:#{line_num} -- #{row}" if raise_on_unknown
279
+ end
257
280
  else
258
281
  val = opt.value
259
282
  end
@@ -315,7 +338,7 @@ module Kontena::Cli::Stacks
315
338
  if filename
316
339
  parent_config = from_external_file(filename, extended_service)
317
340
  elsif stackname
318
- parent_config = from_external_file(stackname, extended_service, from_registry: true)
341
+ parent_config = from_external_file(stackname, extended_service)
319
342
  else
320
343
  raise ("Service '#{extended_service}' not found in #{file}") unless services.has_key?(extended_service)
321
344
  parent_config = process_config(services[extended_service])
@@ -384,19 +407,6 @@ module Kontena::Cli::Stacks
384
407
  end
385
408
  end
386
409
 
387
- # Goes through an array of service hashes and extracts vault secret key names
388
- # @param [Hash] services_array
389
- # @return [Array] keys
390
- def extract_vault_keys(services)
391
- keys = []
392
- services.each do |_, data|
393
- Array(services['secrets']).each do |secret|
394
- keys << secret['secret']
395
- end
396
- end
397
- keys.uniq.compact
398
- end
399
-
400
410
  # Takes a stack name such as user/foo:1.0.0 and breaks it into components
401
411
  # @param [String] stack_name
402
412
  # @return [Hash] a hash with :user, :stack and :version
@@ -2,6 +2,10 @@ require 'clamp'
2
2
 
3
3
  class Kontena::Command < Clamp::Command
4
4
 
5
+ option ['-D', '--debug'], :flag, "Enable debug", environment_variable: 'DEBUG' do
6
+ ENV['DEBUG'] = 'true'
7
+ end
8
+
5
9
  attr_accessor :arguments
6
10
  attr_reader :result
7
11
  attr_reader :exit_code
@@ -194,6 +198,7 @@ class Kontena::Command < Clamp::Command
194
198
  exit(@exit_code) if @exit_code.to_i > 0
195
199
  @result
196
200
  end
201
+
197
202
  end
198
203
 
199
204
  require_relative 'callback'
@@ -6,7 +6,6 @@ require_relative 'callback'
6
6
  require_relative 'cli/bytes_helper'
7
7
  require_relative 'cli/grid_options'
8
8
  require_relative 'cli/app_command'
9
- require_relative 'cli/login_command'
10
9
  require_relative 'cli/logout_command'
11
10
  require_relative 'cli/whoami_command'
12
11
  require_relative 'cli/container_command'
@@ -25,12 +24,16 @@ require_relative 'cli/version_command'
25
24
  require_relative 'cli/stack_command'
26
25
  require_relative 'cli/certificate_command'
27
26
  require_relative 'cli/cloud_command'
28
- require_relative 'cli/register_command'
29
27
 
30
28
  class Kontena::MainCommand < Kontena::Command
31
29
  include Kontena::Util
32
30
  include Kontena::Cli::Common
33
31
 
32
+ option ['-v', '--version'], :flag, "Output Kontena CLI version #{Kontena::Cli::VERSION}" do
33
+ puts ['kontena-cli', Kontena::Cli::VERSION, '[ruby' + RUBY_VERSION + '+' + RUBY_PLATFORM + ']'].join(' ')
34
+ exit 0
35
+ end
36
+
34
37
  subcommand "cloud", "Kontena Cloud specific commands", Kontena::Cli::CloudCommand
35
38
  subcommand "logout", "Logout from Kontena Masters or Kontena Cloud accounts", Kontena::Cli::LogoutCommand
36
39
  subcommand "grid", "Grid specific commands", Kontena::Cli::GridCommand
@@ -49,8 +52,6 @@ class Kontena::MainCommand < Kontena::Command
49
52
  subcommand "whoami", "Shows current logged in user", Kontena::Cli::WhoamiCommand
50
53
  subcommand "plugin", "Plugin related commands", Kontena::Cli::PluginCommand
51
54
  subcommand "version", "Show version", Kontena::Cli::VersionCommand
52
- subcommand "login", "[DEPRECATED] Login to Kontena Master", Kontena::Cli::LoginCommand
53
- subcommand "register", "[DEPRECATED] Register a Kontena Cloud account", Kontena::Cli::RegisterCommand
54
55
 
55
56
  def execute
56
57
  end
data/lib/kontena_cli.rb CHANGED
@@ -36,6 +36,10 @@ module Kontena
36
36
  ENV['OS'] == 'Windows_NT' && RUBY_PLATFORM !~ /cygwin/
37
37
  end
38
38
 
39
+ def self.browserless?
40
+ !!(RUBY_PLATFORM =~ /linux|(?:free|net|open)bsd|solaris|aix|hpux/ && ENV['DISPLAY'].to_s.empty?)
41
+ end
42
+
39
43
  def self.simple_terminal?
40
44
  ENV['KONTENA_SIMPLE_TERM'] || !$stdout.tty?
41
45
  end
@@ -31,8 +31,12 @@ variables:
31
31
  env: test_var
32
32
  TEST_ENV_VAR: # the default from/to is to set/read env of the option name
33
33
  type: :string
34
- TAG:
34
+ tag:
35
35
  type: string
36
+ from:
37
+ env: TAG
38
+ to:
39
+ env: TAG
36
40
  MYSQL_IMAGE:
37
41
  type: string
38
42
  empty_is_nil: false
@@ -23,8 +23,12 @@ variables:
23
23
  env: test_var
24
24
  TEST_ENV_VAR: # the default from/to is to set/read env of the option name
25
25
  type: string
26
- TAG:
26
+ tag:
27
27
  type: string
28
+ from:
29
+ env: TAG
30
+ to:
31
+ env: TAG
28
32
  MYSQL_IMAGE:
29
33
  type: string
30
34
  empty_is_nil: false
@@ -17,6 +17,7 @@ describe Kontena::Cli::Cloud::LoginCommand do
17
17
  before(:each) do
18
18
  allow(subject).to receive(:config).and_return(config)
19
19
  allow(Kontena::Client).to receive(:new).and_return(client)
20
+ allow(Kontena).to receive(:browserless?).and_return(false)
20
21
  end
21
22
 
22
23
  it 'should give error if trying to use --code and --force' do
@@ -0,0 +1,87 @@
1
+ describe Kontena::Cli::Etcd::HealthCommand do
2
+ include ClientHelpers
3
+ include OutputHelpers
4
+
5
+ before do
6
+ allow(subject).to receive(:health_icon) {|health| health.inspect }
7
+ end
8
+
9
+ describe '#show_node_health' do
10
+ context "For an offline node" do
11
+ let :node_health do
12
+ {
13
+ "connected" => false,
14
+ "name" => "node-1",
15
+ 'etcd_health' => {
16
+ 'health' => nil,
17
+ 'error' => nil,
18
+ },
19
+ }
20
+ end
21
+
22
+ it "shows offline and returns false" do
23
+ expect{subject.show_node_health(node_health)}.to return_and_output false, [
24
+ ":offline Node node-1 is offline",
25
+ ]
26
+ end
27
+ end
28
+
29
+ context "For a node with health errors" do
30
+ let :node_health do
31
+ {
32
+ "name" => "node-1",
33
+ "connected" => true,
34
+ 'etcd_health' => {
35
+ 'health' => nil,
36
+ 'error' => "timeout",
37
+ },
38
+ }
39
+ end
40
+
41
+ it "shows errored and returns false" do
42
+ expect{subject.show_node_health(node_health)}.to return_and_output false, [
43
+ ":error Node node-1 is unhealthy: timeout",
44
+ ]
45
+ end
46
+ end
47
+
48
+ context "For a node that returns health=false" do
49
+ let :node_health do
50
+ {
51
+ "name" => "node-1",
52
+ "connected" => true,
53
+ 'etcd_health' => {
54
+ 'health' => false,
55
+ 'error' => nil,
56
+ },
57
+ }
58
+ end
59
+
60
+ it "shows unhealthy and returns false" do
61
+ expect{subject.show_node_health(node_health)}.to return_and_output false, [
62
+ ":error Node node-1 is unhealthy",
63
+ ]
64
+ end
65
+ end
66
+
67
+ context "For a healthy node" do
68
+ let :node_health do
69
+ {
70
+ "name" => "node-1",
71
+ "connected" => true,
72
+ 'etcd_health' => {
73
+ 'health' => true,
74
+ 'error' => nil,
75
+ },
76
+ }
77
+ end
78
+
79
+
80
+ it "shows healthy and returns true" do
81
+ expect{subject.show_node_health(node_health)}.to return_and_output true, [
82
+ ":ok Node node-1 is healthy",
83
+ ]
84
+ end
85
+ end
86
+ end
87
+ end