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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/kontena/cli/apps/deploy_command.rb +1 -5
- data/lib/kontena/cli/cloud/login_command.rb +9 -1
- data/lib/kontena/cli/common.rb +2 -2
- data/lib/kontena/cli/etcd/health_command.rb +58 -0
- data/lib/kontena/cli/etcd_command.rb +2 -0
- data/lib/kontena/cli/external_registry_command.rb +0 -2
- data/lib/kontena/cli/master/login_command.rb +5 -7
- data/lib/kontena/cli/service_command.rb +2 -2
- data/lib/kontena/cli/services/deploy_command.rb +1 -5
- data/lib/kontena/cli/services/exec_command.rb +84 -0
- data/lib/kontena/cli/services/services_helper.rb +4 -1
- data/lib/kontena/cli/stacks/common.rb +6 -17
- data/lib/kontena/cli/stacks/install_command.rb +2 -10
- data/lib/kontena/cli/stacks/show_command.rb +30 -4
- data/lib/kontena/cli/stacks/upgrade_command.rb +20 -7
- data/lib/kontena/cli/stacks/validate_command.rb +1 -9
- data/lib/kontena/cli/stacks/yaml/opto/service_link_resolver.rb +45 -0
- data/lib/kontena/cli/stacks/yaml/opto/vault_cert_prompt_resolver.rb +15 -0
- data/lib/kontena/cli/stacks/yaml/opto/vault_resolver.rb +1 -0
- data/lib/kontena/cli/stacks/yaml/reader.rb +36 -26
- data/lib/kontena/command.rb +5 -0
- data/lib/kontena/main_command.rb +5 -4
- data/lib/kontena_cli.rb +4 -0
- data/spec/fixtures/stack-with-prompted-variables.yml +5 -1
- data/spec/fixtures/stack-with-variables.yml +5 -1
- data/spec/kontena/cli/cloud/login_command_spec.rb +1 -0
- data/spec/kontena/cli/etcd/health_command_spec.rb +87 -0
- data/spec/kontena/cli/master/login_command_spec.rb +8 -17
- data/spec/kontena/cli/services/exec_command_spec.rb +137 -0
- data/spec/kontena/cli/stacks/install_command_spec.rb +5 -5
- data/spec/kontena/cli/stacks/upgrade_command_spec.rb +39 -32
- data/spec/kontena/cli/stacks/yaml/reader_spec.rb +22 -0
- data/spec/support/client_helpers.rb +6 -2
- data/spec/support/output_helpers.rb +23 -0
- metadata +11 -7
- data/lib/kontena/cli/external_registries/delete_command.rb +0 -15
- data/lib/kontena/cli/login_command.rb +0 -12
- data/lib/kontena/cli/register_command.rb +0 -9
- 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, '
|
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
|
-
|
23
|
-
|
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
|
-
|
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(
|
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,
|
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
|
93
|
-
|
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(
|
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
|
-
|
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
|
233
|
-
outcome = Reader.new(filename, skip_validation: skip_validation?, skip_variables: true,
|
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
|
-
|
256
|
-
|
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
|
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
|
data/lib/kontena/command.rb
CHANGED
@@ -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'
|
data/lib/kontena/main_command.rb
CHANGED
@@ -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
|
-
|
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
|
@@ -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
|