kontena-cli 1.1.0.rc1 → 1.1.0.rc2

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 (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