kontena-cli 0.16.3 → 0.17.0.pre1
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.
- checksums.yaml +4 -4
- data/.dockerignore +1 -0
- data/.gitignore +3 -1
- data/VERSION +1 -1
- data/lib/kontena/callbacks/master/deploy/40_install_ssl_certificate_after_deploy.rb +32 -0
- data/lib/kontena/cli/apps/deploy_command.rb +2 -2
- data/lib/kontena/cli/apps/scale_command.rb +2 -2
- data/lib/kontena/cli/apps/show_command.rb +3 -2
- data/lib/kontena/cli/apps/yaml/validations.rb +10 -6
- data/lib/kontena/cli/apps/yaml/validator.rb +1 -0
- data/lib/kontena/cli/apps/yaml/validator_v2.rb +1 -0
- data/lib/kontena/cli/cloud/login_command.rb +66 -64
- data/lib/kontena/cli/common.rb +0 -10
- data/lib/kontena/cli/grids/logs_command.rb +0 -1
- data/lib/kontena/cli/localhost_web_server.rb +11 -3
- data/lib/kontena/cli/master/login_command.rb +213 -163
- data/lib/kontena/cli/nodes/label_command.rb +2 -0
- data/lib/kontena/cli/nodes/labels/add_command.rb +7 -8
- data/lib/kontena/cli/nodes/labels/list_command.rb +17 -0
- data/lib/kontena/cli/nodes/labels/remove_command.rb +7 -12
- data/lib/kontena/cli/nodes/show_command.rb +1 -0
- data/lib/kontena/cli/plugins/common.rb +8 -0
- data/lib/kontena/cli/plugins/install_command.rb +21 -2
- data/lib/kontena/cli/plugins/list_command.rb +4 -2
- data/lib/kontena/cli/plugins/search_command.rb +4 -2
- data/lib/kontena/cli/registry/create_command.rb +19 -12
- data/lib/kontena/cli/registry/remove_command.rb +4 -4
- data/lib/kontena/cli/registry_command.rb +0 -1
- data/lib/kontena/cli/services/create_command.rb +6 -6
- data/lib/kontena/cli/services/deploy_command.rb +8 -4
- data/lib/kontena/cli/services/list_command.rb +34 -21
- data/lib/kontena/cli/services/logs_command.rb +1 -1
- data/lib/kontena/cli/services/scale_command.rb +3 -3
- data/lib/kontena/cli/services/services_helper.rb +18 -14
- data/lib/kontena/cli/services/show_command.rb +1 -0
- data/lib/kontena/cli/services/update_command.rb +6 -6
- data/lib/kontena/cli/stack_command.rb +12 -6
- data/lib/kontena/cli/stacks/build_command.rb +110 -0
- data/lib/kontena/cli/stacks/common.rb +85 -20
- data/lib/kontena/cli/stacks/deploy_command.rb +30 -7
- data/lib/kontena/cli/stacks/install_command.rb +30 -0
- data/lib/kontena/cli/stacks/list_command.rb +74 -14
- data/lib/kontena/cli/stacks/logs_command.rb +31 -0
- data/lib/kontena/cli/stacks/monitor_command.rb +91 -0
- data/lib/kontena/cli/stacks/remove_command.rb +24 -7
- data/lib/kontena/cli/stacks/service_generator.rb +115 -0
- data/lib/kontena/cli/stacks/service_generator_v2.rb +27 -0
- data/lib/kontena/cli/stacks/show_command.rb +65 -13
- data/lib/kontena/cli/stacks/upgrade_command.rb +28 -0
- data/lib/kontena/cli/stacks/yaml/custom_validators/affinities_validator.rb +19 -0
- data/lib/kontena/cli/stacks/yaml/custom_validators/build_validator.rb +22 -0
- data/lib/kontena/cli/stacks/yaml/custom_validators/extends_validator.rb +21 -0
- data/lib/kontena/cli/stacks/yaml/custom_validators/hooks_validator.rb +54 -0
- data/lib/kontena/cli/stacks/yaml/custom_validators/secrets_validator.rb +22 -0
- data/lib/kontena/cli/stacks/yaml/reader.rb +219 -0
- data/lib/kontena/cli/stacks/yaml/service_extender.rb +78 -0
- data/lib/kontena/cli/stacks/yaml/validations.rb +71 -0
- data/lib/kontena/cli/stacks/yaml/validator_v3.rb +52 -0
- data/lib/kontena/cli/version_command.rb +5 -1
- data/lib/kontena/cli/vpn/create_command.rb +20 -17
- data/lib/kontena/cli/vpn/remove_command.rb +4 -3
- data/lib/kontena/client.rb +21 -20
- data/lib/kontena/machine/cert_helper.rb +4 -0
- data/lib/kontena/machine/cloud_config/cloudinit.yml +1 -1
- data/lib/kontena/main_command.rb +1 -1
- data/spec/fixtures/kontena-build.yml +2 -2
- data/spec/fixtures/kontena-invalid.yml +1 -1
- data/spec/fixtures/kontena-not-hash-service-config.yml +1 -1
- data/spec/fixtures/kontena-with-env-file.yml +2 -2
- data/spec/fixtures/kontena_build_v3.yml +23 -0
- data/spec/fixtures/kontena_v3.yml +20 -0
- data/spec/fixtures/stack-internal-extend.yml +11 -0
- data/spec/fixtures/stack-with-env-file.yml +21 -0
- data/spec/fixtures/stack-with-variables.yml +22 -0
- data/spec/kontena/cli/app/scale_spec.rb +3 -1
- data/spec/kontena/cli/cloud/login_command_spec.rb +283 -0
- data/spec/kontena/cli/master/login_command_spec.rb +324 -145
- data/spec/kontena/cli/services/link_command_spec.rb +1 -1
- data/spec/kontena/cli/services/secrets/link_command_spec.rb +4 -4
- data/spec/kontena/cli/services/secrets/unlink_command_spec.rb +2 -2
- data/spec/kontena/cli/services/services_helper_spec.rb +15 -11
- data/spec/kontena/cli/services/unlink_command_spec.rb +1 -1
- data/spec/kontena/cli/stacks/deploy_command_spec.rb +26 -0
- data/spec/kontena/cli/stacks/install_command_spec.rb +54 -0
- data/spec/kontena/cli/stacks/list_command_spec.rb +27 -0
- data/spec/kontena/cli/stacks/remove_command_spec.rb +45 -0
- data/spec/kontena/cli/stacks/service_generator_spec.rb +385 -0
- data/spec/kontena/cli/stacks/service_generator_v2_spec.rb +74 -0
- data/spec/kontena/cli/stacks/show_command_spec.rb +26 -0
- data/spec/kontena/cli/stacks/upgrade_command_spec.rb +50 -0
- data/spec/kontena/cli/stacks/yaml/reader_spec.rb +370 -0
- data/spec/kontena/cli/stacks/yaml/service_extender_spec.rb +128 -0
- data/spec/kontena/cli/stacks/yaml/validator_v3_spec.rb +302 -0
- data/spec/spec_helper.rb +6 -4
- data/spec/support/client_helpers.rb +1 -0
- metadata +57 -7
- data/lib/kontena/cli/registry/delete_command.rb +0 -18
- data/lib/kontena/cli/stacks/create_command.rb +0 -27
- data/lib/kontena/cli/stacks/update_command.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ad6422b172a48accf054239e4dab6b61fa03f55
|
|
4
|
+
data.tar.gz: 536fafed2231e6fcece9120b06402aa88cab080b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 889ecc5b3aef9a47aab715c3bfc4cb1a5200589b2c8fab55ceb00c7ac5d95f2d2fb8ceb06638c5f3de870920de2102c3722f9c120d20b3c494c1c875a57942db
|
|
7
|
+
data.tar.gz: 2140338ef86e138a7fdf5de202b7d8246e4402ade84327a493aea66307552daeca81c38a27b7b866804ef81cc219d95236713c65ff4736b83452c9fd624f034a
|
data/.dockerignore
CHANGED
data/.gitignore
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.17.0.pre1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Kontena
|
|
2
|
+
module Callbacks
|
|
3
|
+
class InstallSslCertificateAfterDeploy < Kontena::Callback
|
|
4
|
+
|
|
5
|
+
include Kontena::Cli::Common
|
|
6
|
+
|
|
7
|
+
matches_commands 'master create'
|
|
8
|
+
|
|
9
|
+
def after
|
|
10
|
+
return unless command.exit_code == 0
|
|
11
|
+
return unless command.result.kind_of?(Hash)
|
|
12
|
+
return unless command.result.has_key?(:ssl_certificate)
|
|
13
|
+
return unless command.result.has_key?(:public_ip)
|
|
14
|
+
|
|
15
|
+
cert_dir = File.join(Dir.home, '.kontena/certs')
|
|
16
|
+
unless File.directory?(cert_dir)
|
|
17
|
+
require 'fileutils'
|
|
18
|
+
FileUtils.mkdir_p(cert_dir)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
cert_file = File.join(cert_dir, "#{command.result[:public_ip]}.pem")
|
|
22
|
+
|
|
23
|
+
spinner "Installing SSL certificate to #{cert_file}" do
|
|
24
|
+
File.unlink(cert_file) if File.exist?(cert_file)
|
|
25
|
+
File.write(cert_file, command.result[:ssl_certificate])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
@@ -51,9 +51,9 @@ module Kontena::Cli::Apps
|
|
|
51
51
|
warning " --force-deploy will deprecate in the future, use --force"
|
|
52
52
|
end
|
|
53
53
|
spinner "Deploying #{unprefixed_name(name).colorize(:cyan)} " do
|
|
54
|
-
deploy_service(token, name, options)
|
|
54
|
+
deployment = deploy_service(token, name, options)
|
|
55
55
|
unless async?
|
|
56
|
-
wait_for_deploy_to_finish(token,
|
|
56
|
+
wait_for_deploy_to_finish(token, deployment)
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
end
|
|
@@ -21,8 +21,8 @@ module Kontena::Cli::Apps
|
|
|
21
21
|
options = yml_service[service]
|
|
22
22
|
exit_with_error("Service has already instances defined in #{filename}. Please update #{filename} and deploy service instead") if options['container_count']
|
|
23
23
|
spinner "Scaling #{service.colorize(:cyan)} " do
|
|
24
|
-
scale_service(require_token, prefixed_name(service), instances)
|
|
25
|
-
wait_for_deploy_to_finish(token,
|
|
24
|
+
deployment = scale_service(require_token, prefixed_name(service), instances)
|
|
25
|
+
wait_for_deploy_to_finish(token, deployment)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
else
|
|
@@ -15,8 +15,9 @@ module Kontena::Cli::Apps
|
|
|
15
15
|
|
|
16
16
|
def execute
|
|
17
17
|
require_config_file(filename)
|
|
18
|
-
|
|
19
|
-
show_service(
|
|
18
|
+
token = require_token
|
|
19
|
+
show_service(token, prefixed_name(service))
|
|
20
|
+
show_service_instances(token, prefixed_name(service))
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
end
|
|
@@ -6,12 +6,16 @@ module Kontena::Cli::Apps::YAML
|
|
|
6
6
|
require_relative 'custom_validators/extends_validator'
|
|
7
7
|
require_relative 'custom_validators/hooks_validator'
|
|
8
8
|
require_relative 'custom_validators/secrets_validator'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
|
|
10
|
+
def self.load
|
|
11
|
+
return if @loaded
|
|
12
|
+
HashValidator.append_validator(AffinitiesValidator.new)
|
|
13
|
+
HashValidator.append_validator(BuildValidator.new)
|
|
14
|
+
HashValidator.append_validator(ExtendsValidator.new)
|
|
15
|
+
HashValidator.append_validator(SecretsValidator.new)
|
|
16
|
+
HashValidator.append_validator(HooksValidator.new)
|
|
17
|
+
@loaded = true
|
|
18
|
+
end
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def common_validations
|
|
@@ -4,55 +4,56 @@ module Kontena::Cli::Cloud
|
|
|
4
4
|
class LoginCommand < Kontena::Command
|
|
5
5
|
include Kontena::Cli::Common
|
|
6
6
|
|
|
7
|
-
option ['-t', '--token'], '[TOKEN]', 'Use a pre-generated access token'
|
|
7
|
+
option ['-t', '--token'], '[TOKEN]', 'Use a pre-generated access token', environment_variable: 'KONTENA_ACCOUNT_TOKEN'
|
|
8
8
|
option ['-c', '--code'], '[CODE]', 'Use an authorization code'
|
|
9
9
|
option ['-v', '--verbose'], :flag, 'Increase output verbosity'
|
|
10
|
+
option ['-f', '--force'], :flag, 'Force reauthentication'
|
|
10
11
|
|
|
11
12
|
def execute
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
if self.code && self.force?
|
|
14
|
+
exit_with_error "Can't use --code and --force together"
|
|
15
|
+
end
|
|
14
16
|
|
|
15
|
-
if self.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
raise Kontena::Errors::StandardError.new(400, 'Code exchange failed') unless response
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
if response && response.kind_of?(Hash) && response['access_token']
|
|
26
|
-
kontena_account.token.access_token = response['access_token']
|
|
27
|
-
kontena_account.token.refresh_token = response['refresh_token']
|
|
28
|
-
kontena_account.token.expires_at = response['expires_in'].to_i > 0 ? Time.now.utc.to_i + response['expires_in'].to_i : nil
|
|
29
|
-
logger.debug "Code exchanged succesfully"
|
|
30
|
-
else
|
|
31
|
-
puts "Code exchange failed".colorize(:red)
|
|
32
|
-
exit 1
|
|
33
|
-
end
|
|
34
|
-
elsif self.token.nil?
|
|
35
|
-
token = kontena_account.token ||= Kontena::Cli::Config::Token.new(parent_type: :account, parent_name: ENV['KONTENA_ACCOUNT'] || 'kontena')
|
|
36
|
-
elsif self.token
|
|
37
|
-
kontena_account.token = Kontena::Cli::Config::Token.new(access_token: self.token, parent_type: :account, parent_name: ENV['KONTENA_ACCOUNT'] || 'kontena')
|
|
17
|
+
if self.token
|
|
18
|
+
exit_with_error "Can't use --token and --force together" if self.force?
|
|
19
|
+
exit_with_error "Can't use --token and --code together" if self.code
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if !kontena_account.token || !kontena_account.token.access_token || self.token || self.force?
|
|
23
|
+
kontena_account.token = Kontena::Cli::Config::Token.new(access_token: self.token, parent_type: :account, parent_name: kontena_account.name)
|
|
38
24
|
end
|
|
39
25
|
|
|
26
|
+
use_authorization_code(self.code) if self.code
|
|
27
|
+
|
|
40
28
|
client = Kontena::Client.new(kontena_account.userinfo_endpoint, kontena_account.token, prefix: '')
|
|
41
29
|
|
|
42
30
|
if kontena_account.token.access_token
|
|
43
|
-
auth_ok =
|
|
44
|
-
|
|
45
|
-
auth_ok = client.authentication_ok?(kontena_account.userinfo_endpoint)
|
|
31
|
+
auth_ok = vspinner "Verifying current access token" do
|
|
32
|
+
client.authentication_ok?(kontena_account.userinfo_endpoint)
|
|
46
33
|
end
|
|
47
|
-
|
|
48
34
|
if auth_ok
|
|
49
|
-
|
|
50
|
-
display_logo
|
|
51
|
-
display_login_info(only: :account)
|
|
52
|
-
exit 0
|
|
35
|
+
finish and return
|
|
53
36
|
end
|
|
54
37
|
end
|
|
55
38
|
|
|
39
|
+
web_flow
|
|
40
|
+
finish
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def finish
|
|
44
|
+
update_userinfo unless kontena_account.username
|
|
45
|
+
config.current_account = kontena_account.name
|
|
46
|
+
config.write
|
|
47
|
+
config.reset_instance
|
|
48
|
+
reset_cloud_client
|
|
49
|
+
display_logo
|
|
50
|
+
display_login_info(only: :account)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def web_flow
|
|
54
|
+
require_relative '../localhost_web_server'
|
|
55
|
+
require 'launchy'
|
|
56
|
+
|
|
56
57
|
uri = URI.parse(kontena_account.authorization_endpoint)
|
|
57
58
|
uri.host ||= kontena_account.url
|
|
58
59
|
|
|
@@ -85,47 +86,48 @@ module Kontena::Cli::Cloud
|
|
|
85
86
|
server_thread = Thread.new { Thread.main['response'] = web_server.serve_one }
|
|
86
87
|
browser_thread = Thread.new { Launchy.open(uri.to_s) }
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
spinner "Waiting for browser authorization response" do
|
|
89
90
|
server_thread.join
|
|
90
91
|
end
|
|
91
92
|
browser_thread.join
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# If the master responds with a code, then exchange it to a token
|
|
96
|
-
if response && response.kind_of?(Hash) && response['code']
|
|
97
|
-
logger.debug 'Account responded with code, exchanging to token'
|
|
98
|
-
response = client.exchange_code(response['code'])
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
if response && response.kind_of?(Hash) && response['access_token']
|
|
102
|
-
kontena_account.token = Kontena::Cli::Config::Token.new
|
|
103
|
-
kontena_account.token.access_token = response['access_token']
|
|
104
|
-
kontena_account.token.refresh_token = response['refresh_token']
|
|
105
|
-
kontena_account.token.expires_at = response['expires_in'].to_i > 0 ? Time.now.utc.to_i + response['expires_in'].to_i : nil
|
|
106
|
-
else
|
|
107
|
-
puts "Authentication failed".colorize(:red)
|
|
108
|
-
exit 1
|
|
109
|
-
end
|
|
94
|
+
update_token(Thread.main['response'])
|
|
95
|
+
end
|
|
110
96
|
|
|
97
|
+
def update_userinfo
|
|
111
98
|
uri = URI.parse(kontena_account.userinfo_endpoint)
|
|
112
99
|
path = uri.path
|
|
113
100
|
uri.path = '/'
|
|
114
101
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
response = client.get(path) rescue nil
|
|
118
|
-
if response && response.kind_of?(Hash)
|
|
102
|
+
response = Kontena::Client.new(uri.to_s, kontena_account.token).get(path)
|
|
103
|
+
if response.kind_of?(Hash) && response['data'] && response['data']['attributes']
|
|
119
104
|
kontena_account.username = response['data']['attributes']['username']
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
display_login_info(only: :account)
|
|
123
|
-
config.reset_instance
|
|
124
|
-
reset_cloud_client
|
|
125
|
-
exit 0
|
|
105
|
+
elsif response && response['error']
|
|
106
|
+
exit_with_error response['error']
|
|
126
107
|
else
|
|
127
|
-
|
|
128
|
-
|
|
108
|
+
exit_with_error "Userinfo request failed"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def use_authorization_code(code)
|
|
113
|
+
response = vspinner "Exchanging authorization code to access token" do
|
|
114
|
+
Kontena::Client.new(kontena_account.token_endpoint, kontena_account.token).exchange_code(code)
|
|
115
|
+
end
|
|
116
|
+
update_token(response)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def update_token(response)
|
|
120
|
+
if !response.kind_of?(Hash)
|
|
121
|
+
raise TypeError, "Invalid authentication response, expected Hash, got #{response.class}"
|
|
122
|
+
elsif response['error']
|
|
123
|
+
exit_with_error "Authentication failed: #{response['error']}"
|
|
124
|
+
elsif response['code']
|
|
125
|
+
use_authorization_code(response['code'])
|
|
126
|
+
else
|
|
127
|
+
kontena_account.token.access_token = response['access_token']
|
|
128
|
+
kontena_account.token.refresh_token = response['refresh_token']
|
|
129
|
+
kontena_account.token.expires_at = response['expires_at']
|
|
130
|
+
true
|
|
129
131
|
end
|
|
130
132
|
end
|
|
131
133
|
end
|
data/lib/kontena/cli/common.rb
CHANGED
|
@@ -197,16 +197,6 @@ module Kontena
|
|
|
197
197
|
config.require_current_master.url
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
-
def ensure_custom_ssl_ca(url)
|
|
201
|
-
return if Excon.defaults[:ssl_ca_file]
|
|
202
|
-
|
|
203
|
-
uri = URI::parse(url)
|
|
204
|
-
cert_file = File.join(Dir.home, "/.kontena/certs/#{uri.host}.pem")
|
|
205
|
-
if File.exist?(cert_file)
|
|
206
|
-
Excon.defaults[:ssl_ca_file] = cert_file
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
200
|
def current_grid=(grid)
|
|
211
201
|
config.current_grid=(grid)
|
|
212
202
|
end
|
|
@@ -4,7 +4,6 @@ module Kontena::Cli::Grids
|
|
|
4
4
|
class LogsCommand < Kontena::Command
|
|
5
5
|
include Kontena::Cli::Common
|
|
6
6
|
include Kontena::Cli::Helpers::LogHelper
|
|
7
|
-
|
|
8
7
|
option "--node", "NODE", "Filter by node name", multivalued: true
|
|
9
8
|
option "--service", "SERVICE", "Filter by service name", multivalued: true
|
|
10
9
|
option ["-c", "--container"], "CONTAINER", "Filter by container", multivalued: true
|
|
@@ -82,9 +82,17 @@ module Kontena
|
|
|
82
82
|
server.close
|
|
83
83
|
uri = URI.parse("http://localhost#{get_request}")
|
|
84
84
|
ENV["DEBUG"] && puts(" * Parsing params: \"#{uri.query}\"")
|
|
85
|
-
params =
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
params = {}
|
|
86
|
+
URI.decode_www_form(uri.query).each do |key, value|
|
|
87
|
+
if value.to_s == ''
|
|
88
|
+
next
|
|
89
|
+
elsif value.to_s =~ /\A\d+\z/
|
|
90
|
+
params[key] = value.to_i
|
|
91
|
+
else
|
|
92
|
+
params[key] = value
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
params
|
|
88
96
|
else
|
|
89
97
|
# Unless it's a query to /cb, send an error message and keep listening,
|
|
90
98
|
# it might have been something funny like fetching favicon.ico
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
#TODO: Something wrong with picking up wrong server in config using the url,
|
|
2
|
-
#maybe remove that part altogether.
|
|
3
1
|
require 'uri'
|
|
4
2
|
|
|
5
3
|
module Kontena::Cli::Master
|
|
@@ -8,196 +6,165 @@ module Kontena::Cli::Master
|
|
|
8
6
|
|
|
9
7
|
parameter "[URL]", "Kontena Master URL or name"
|
|
10
8
|
option ['-j', '--join'], '[INVITE_CODE]', "Join master using an invitation code"
|
|
11
|
-
option ['-t', '--token'], '[TOKEN]', 'Use a pre-generated access token'
|
|
12
|
-
option ['-n', '--name'], '[NAME]', 'Set server name'
|
|
9
|
+
option ['-t', '--token'], '[TOKEN]', 'Use a pre-generated access token', environment_variable: 'KONTENA_TOKEN'
|
|
10
|
+
option ['-n', '--name'], '[NAME]', 'Set server name', environment_variable: 'KONTENA_MASTER'
|
|
13
11
|
option ['-c', '--code'], '[CODE]', 'Use authorization code generated during master install'
|
|
14
12
|
option ['-r', '--remote'], :flag, 'Do not try to open a browser'
|
|
15
13
|
option ['-e', '--expires-in'], '[SECONDS]', 'Request token with expiration of X seconds. Use 0 to never expire', default: 7200
|
|
16
14
|
option ['-v', '--verbose'], :flag, 'Increase output verbosity'
|
|
17
15
|
option ['-f', '--force'], :flag, 'Force reauthentication'
|
|
18
16
|
option ['-s', '--silent'], :flag, 'Reduce output verbosity'
|
|
17
|
+
option ['--grid'], '[GRID]', 'Set grid'
|
|
19
18
|
|
|
20
19
|
option ['--no-login-info'], :flag, "Don't show login info", hidden: true
|
|
21
20
|
|
|
22
21
|
def execute
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
if self.code
|
|
23
|
+
exit_with_error "Can't use --token and --code together" if self.token
|
|
24
|
+
exit_with_error "Can't use --join and --code together" if self.join
|
|
25
|
+
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
if self.force?
|
|
28
|
+
exit_with_error "Can't use --code and --force together" if self.code
|
|
29
|
+
exit_with_error "Can't use --token and --force together" if self.token
|
|
30
|
+
end
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
set_server_token(server)
|
|
32
|
+
server = select_a_server(self.name, self.url)
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
if self.token
|
|
35
|
+
# If a --token was given create a token with access_token set to --token value
|
|
36
|
+
server.token = Kontena::Cli::Config::Token.new(access_token: self.token, parent_type: :master, parent_name: server.name)
|
|
37
|
+
elsif server.token.nil? || self.force?
|
|
38
|
+
# Force reauth or no existing token, create a token with no access_token
|
|
39
|
+
server.token = Kontena::Cli::Config::Token.new(parent_type: :master, parent_name: server.name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if self.grid
|
|
43
|
+
self.skip_grid_auto_select = true if self.respond_to?(:skip_grid_auto_select?)
|
|
44
|
+
server.grid = self.grid
|
|
45
|
+
end
|
|
34
46
|
|
|
35
|
-
|
|
47
|
+
# set server token by exchanging code if --code given
|
|
48
|
+
if self.code
|
|
49
|
+
use_authorization_code(server, self.code)
|
|
50
|
+
exit 0
|
|
51
|
+
end
|
|
36
52
|
|
|
37
|
-
#
|
|
38
|
-
# if
|
|
53
|
+
# unless an invitation code was supplied, check auth and exit
|
|
54
|
+
# if existing auth works already.
|
|
39
55
|
unless self.join || self.force?
|
|
40
56
|
if auth_works?(server)
|
|
41
|
-
|
|
42
|
-
config.reset_instance
|
|
57
|
+
update_server_to_config(server)
|
|
43
58
|
display_login_info(only: :master) unless self.no_login_info?
|
|
44
59
|
exit 0
|
|
45
60
|
end
|
|
46
61
|
end
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# local web flow
|
|
56
|
-
response = response_from_web_flow
|
|
57
|
-
|
|
58
|
-
# If the master responds with a code, then exchange it to a token
|
|
59
|
-
if response['code']
|
|
60
|
-
use_authorization_code(server, response['code'])
|
|
61
|
-
elsif response['access_token']
|
|
62
|
-
update_server_token(server, response)
|
|
63
|
-
update_server_name(server, response)
|
|
64
|
-
config.current_server = server.name
|
|
65
|
-
end
|
|
66
|
-
config.write
|
|
67
|
-
display_login_info(only: :master) unless (running_silent? || self.no_login_info?)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def master_account
|
|
71
|
-
@master_account ||= config.find_account('master')
|
|
72
|
-
end
|
|
63
|
+
auth_params = {
|
|
64
|
+
remote: self.remote?,
|
|
65
|
+
invite_code: self.join,
|
|
66
|
+
expires_in: self.expires_in
|
|
67
|
+
}
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
if self.remote?
|
|
70
|
+
# no local browser? tell user to launch an external one
|
|
71
|
+
display_remote_message(server, auth_params)
|
|
72
|
+
update_server_to_config(server)
|
|
73
|
+
exit 1
|
|
79
74
|
else
|
|
80
|
-
|
|
75
|
+
# local web flow
|
|
76
|
+
web_flow(server, auth_params)
|
|
77
|
+
display_login_info(only: :master) unless (running_silent? || self.no_login_info?)
|
|
81
78
|
end
|
|
82
79
|
end
|
|
83
80
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
server = config.find_server(self.url)
|
|
87
|
-
if server && server.url
|
|
88
|
-
self.url = server.url
|
|
89
|
-
true
|
|
90
|
-
else
|
|
91
|
-
exit_with_error "Server '#{self.url}' not found in configuration."
|
|
92
|
-
end
|
|
81
|
+
def next_default_name
|
|
82
|
+
next_name('kontena-master')
|
|
93
83
|
end
|
|
94
84
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
def next_name(base)
|
|
86
|
+
if config.find_server(base)
|
|
87
|
+
new_name = base.dup
|
|
88
|
+
unless new_name =~ /\-\d+$/
|
|
89
|
+
new_name += "-2"
|
|
90
|
+
end
|
|
91
|
+
new_name.succ! until config.find_server(new_name).nil?
|
|
92
|
+
new_name
|
|
100
93
|
else
|
|
101
|
-
|
|
102
|
-
config.servers << new_server
|
|
103
|
-
config.current_server = new_server.name
|
|
104
|
-
new_server
|
|
94
|
+
base
|
|
105
95
|
end
|
|
106
96
|
end
|
|
107
97
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
# Use supplied token
|
|
111
|
-
server.token = Kontena::Cli::Config::Token.new(access_token: self.token, parent_type: :master, parent_name: server.name)
|
|
112
|
-
elsif server.token.nil? || self.force?
|
|
113
|
-
# Create new empty token if the server does not have one yet
|
|
114
|
-
server.token = Kontena::Cli::Config::Token.new(parent_type: :master, parent_name: server.name)
|
|
115
|
-
end
|
|
98
|
+
def master_account
|
|
99
|
+
@master_account ||= config.find_account('master')
|
|
116
100
|
end
|
|
117
101
|
|
|
118
102
|
def use_authorization_code(server, code)
|
|
119
|
-
vspinner "Exchanging authorization code for an access token from Kontena Master" do
|
|
120
|
-
|
|
121
|
-
begin
|
|
122
|
-
response = client.exchange_code(code)
|
|
123
|
-
rescue StandardError => ex
|
|
124
|
-
ENV["DEBUG"] && puts("#{ex}\n#{ex.backtrace.join(" \n")}")
|
|
125
|
-
exit_with_error "Code exchange failed: #{ex}"
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
if response['server'] && response['server']['name']
|
|
129
|
-
server.name ||= response['server']['name']
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
if response['user']
|
|
133
|
-
server.username = response['user']['name'] || response['user']['email']
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
server.token = Kontena::Cli::Config::Token.new(
|
|
137
|
-
access_token: response['access_token'],
|
|
138
|
-
refresh_token: response['refresh_token'],
|
|
139
|
-
expires_at: in_to_at(response['expires_in']),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
config.current_server = server.name
|
|
103
|
+
response = vspinner "Exchanging authorization code for an access token from Kontena Master" do
|
|
104
|
+
Kontena::Client.new(server.url, server.token).exchange_code(code)
|
|
143
105
|
end
|
|
144
|
-
|
|
106
|
+
update_server(server, response)
|
|
107
|
+
update_server_to_config(server)
|
|
145
108
|
end
|
|
146
109
|
|
|
110
|
+
# Check if the existing (or --token) authentication works without reauthenticating
|
|
147
111
|
def auth_works?(server)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
vspinner "Testing if authentication works using current access token" do
|
|
152
|
-
auth_ok = Kontena::Client.new(server.url, server.token).authentication_ok?(master_account.userinfo_endpoint)
|
|
153
|
-
config.current_master = server.name
|
|
154
|
-
end
|
|
155
|
-
auth_ok
|
|
156
|
-
else
|
|
157
|
-
false
|
|
112
|
+
return false unless (server && server.token && server.token.access_token)
|
|
113
|
+
vspinner "Testing if authentication works using current access token" do
|
|
114
|
+
Kontena::Client.new(server.url, server.token).authentication_ok?(master_account.userinfo_endpoint)
|
|
158
115
|
end
|
|
159
116
|
end
|
|
160
117
|
|
|
161
|
-
|
|
118
|
+
# Build a path for master authentication
|
|
119
|
+
#
|
|
120
|
+
# @param local_port [Fixnum] tcp port where localhost webserver is listening
|
|
121
|
+
# @param invite_code [String] an invitation code generated when user was invited
|
|
122
|
+
# @param expires_in [Fixnum] expiration time for the requested access token
|
|
123
|
+
# @param remote [Boolean] true when performing a login where the code is displayed on the web page
|
|
124
|
+
# @return [String]
|
|
125
|
+
def authentication_path(local_port: nil, invite_code: nil, expires_in: nil, remote: false)
|
|
162
126
|
auth_url_params = {}
|
|
163
|
-
if
|
|
127
|
+
if remote
|
|
164
128
|
auth_url_params[:redirect_uri] = "/code"
|
|
129
|
+
elsif local_port
|
|
130
|
+
auth_url_params[:redirect_uri] = "http://localhost:#{local_port}/cb"
|
|
165
131
|
else
|
|
166
|
-
|
|
132
|
+
raise ArgumentError, "Local port not defined and not performing remote login"
|
|
167
133
|
end
|
|
168
|
-
auth_url_params[:invite_code] =
|
|
169
|
-
auth_url_params[:expires_in] =
|
|
134
|
+
auth_url_params[:invite_code] = invite_code if invite_code
|
|
135
|
+
auth_url_params[:expires_in] = expires_in if expires_in
|
|
170
136
|
"/authenticate?#{URI.encode_www_form(auth_url_params)}"
|
|
171
137
|
end
|
|
172
138
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
139
|
+
# Request a redirect to the authentication url from master
|
|
140
|
+
#
|
|
141
|
+
# @param master_url [String] master root url
|
|
142
|
+
# @param auth_params [Hash] auth parameters (keyword arguments of #authentication_path)
|
|
143
|
+
# @return [String] url to begin authentication web flow
|
|
144
|
+
def authentication_url_from_master(master_url, auth_params)
|
|
145
|
+
client = Kontena::Client.new(master_url)
|
|
178
146
|
vspinner "Sending authentication request to receive an authorization URL" do
|
|
179
|
-
|
|
147
|
+
response = client.request(
|
|
180
148
|
http_method: :get,
|
|
181
|
-
path:
|
|
149
|
+
path: authentication_path(auth_params),
|
|
182
150
|
expects: [501, 400, 302, 403],
|
|
183
151
|
auth: false
|
|
184
152
|
)
|
|
185
153
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
exit_with_error "Invalid invitation code"
|
|
154
|
+
if client.last_response.status == 302
|
|
155
|
+
client.last_response.headers['Location']
|
|
156
|
+
elsif response.kind_of?(Hash)
|
|
157
|
+
exit_with_error [response['error'], response['error_description']].compact.join(' : ')
|
|
158
|
+
elsif response.kind_of?(String) && response.length > 1
|
|
159
|
+
exit_with_error response
|
|
193
160
|
else
|
|
194
|
-
exit_with_error "Invalid response to authentication request"
|
|
161
|
+
exit_with_error "Invalid response to authentication request : HTTP#{client.last_response.status} #{client.last_response.body if ENV["DEBUG"]}"
|
|
195
162
|
end
|
|
196
163
|
end
|
|
197
|
-
authorization_url
|
|
198
164
|
end
|
|
199
165
|
|
|
200
|
-
def
|
|
166
|
+
def display_remote_message(server, auth_params)
|
|
167
|
+
url = authentication_url_from_master(server.url, auth_params.merge(remote: true))
|
|
201
168
|
if running_silent?
|
|
202
169
|
sputs url
|
|
203
170
|
else
|
|
@@ -205,21 +172,21 @@ module Kontena::Cli::Master
|
|
|
205
172
|
puts "#{url}"
|
|
206
173
|
puts
|
|
207
174
|
puts "Then complete the authentication by using:"
|
|
208
|
-
puts "kontena master login --code <CODE FROM BROWSER>"
|
|
209
|
-
# Using exit code 1 because the operation isn't complete,
|
|
210
|
-
# you can't do something like:
|
|
211
|
-
# kontena master login --remote && echo "yes"
|
|
175
|
+
puts "kontena master login --code <CODE FROM BROWSER> #{server.url}"
|
|
212
176
|
end
|
|
213
|
-
exit 1
|
|
214
177
|
end
|
|
215
178
|
|
|
216
|
-
def
|
|
179
|
+
def web_flow(server, auth_params)
|
|
217
180
|
require_relative '../localhost_web_server'
|
|
218
181
|
require 'launchy'
|
|
219
182
|
|
|
183
|
+
|
|
220
184
|
web_server = Kontena::LocalhostWebServer.new
|
|
221
|
-
|
|
222
|
-
|
|
185
|
+
|
|
186
|
+
url = authentication_url_from_master(server.url, auth_params.merge(local_port: web_server.port))
|
|
187
|
+
uri = URI.parse(url)
|
|
188
|
+
|
|
189
|
+
puts "Opening a browser to #{uri.scheme}://#{uri.host}"
|
|
223
190
|
puts
|
|
224
191
|
puts "If you are running this command over an ssh connection or it's"
|
|
225
192
|
puts "otherwise not possible to open a browser from this terminal"
|
|
@@ -239,44 +206,127 @@ module Kontena::Cli::Master
|
|
|
239
206
|
server_thread = Thread.new { Thread.main['response'] = web_server.serve_one }
|
|
240
207
|
browser_thread = Thread.new { Launchy.open(uri.to_s) }
|
|
241
208
|
|
|
242
|
-
|
|
209
|
+
spinner "Waiting for browser authorization response" do
|
|
243
210
|
server_thread.join
|
|
244
211
|
end
|
|
245
212
|
browser_thread.join
|
|
246
213
|
|
|
247
|
-
Thread.main['response']
|
|
214
|
+
update_server(server, Thread.main['response'])
|
|
215
|
+
update_server_to_config(server)
|
|
248
216
|
end
|
|
249
217
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
218
|
+
def update_server(server, response)
|
|
219
|
+
update_server_token(server, response)
|
|
220
|
+
update_server_name(server, response)
|
|
221
|
+
update_server_username(server, response)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def update_server_name(server, response)
|
|
225
|
+
return nil unless server.name.nil?
|
|
226
|
+
if response.kind_of?(Hash) && response['server'] && response['server']['name']
|
|
227
|
+
server.name = next_name(response['server']['name'])
|
|
253
228
|
else
|
|
254
|
-
|
|
229
|
+
server.name = next_default_name
|
|
255
230
|
end
|
|
256
231
|
end
|
|
257
232
|
|
|
258
|
-
def
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
server.token.
|
|
262
|
-
server.token.expires_at = in_to_at(response['expires_in'])
|
|
263
|
-
server.token.username = response.fetch('user', {}).fetch('name', nil) || response.fetch('user', {}).fetch('email', nil)
|
|
233
|
+
def update_server_username(server, response)
|
|
234
|
+
return nil unless response.kind_of?(Hash)
|
|
235
|
+
return nil unless response['user']
|
|
236
|
+
server.token.username = response['user']['name'] || response['user']['email']
|
|
264
237
|
server.username = server.token.username
|
|
265
238
|
end
|
|
266
239
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
server
|
|
272
|
-
elsif response['
|
|
273
|
-
|
|
274
|
-
elsif config.find_server('kontena-master')
|
|
275
|
-
new_name = "kontena-master-2"
|
|
276
|
-
new_name.succ! until config.find_server(new_name).nil?
|
|
277
|
-
server.name = new_name
|
|
240
|
+
def update_server_token(server, response)
|
|
241
|
+
if !response.kind_of?(Hash)
|
|
242
|
+
raise TypeError, "Response type mismatch - expected Hash, got #{response.class}"
|
|
243
|
+
elsif response['code']
|
|
244
|
+
use_authorization_code(server, response['code'])
|
|
245
|
+
elsif response['error']
|
|
246
|
+
exit_with_error "Authentication failed: #{response['error']} #{response['error_description']}"
|
|
278
247
|
else
|
|
279
|
-
server.
|
|
248
|
+
server.token = Kontena::Cli::Config::Token.new
|
|
249
|
+
server.token.access_token = response['access_token']
|
|
250
|
+
server.token.refresh_token = response['refresh_token']
|
|
251
|
+
server.token.expires_at = response['expires_at']
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def update_server_to_config(server)
|
|
256
|
+
server.name ||= next_default_name
|
|
257
|
+
config.servers << server unless config.servers.include?(server)
|
|
258
|
+
config.current_server = server.name
|
|
259
|
+
config.write
|
|
260
|
+
config.reset_instance
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Figure out or create a server based on url or name.
|
|
264
|
+
#
|
|
265
|
+
# No name or url provided: try to use current_master
|
|
266
|
+
# A name provided with --name but no url defined: try to find a server by name from config
|
|
267
|
+
# An URL starting with 'http' provided: try to find a server by url from config
|
|
268
|
+
# An URL not starting with 'http' provided: try to find a server by name
|
|
269
|
+
# An URL and a name provided
|
|
270
|
+
# - If a server is found by name: use entry and update URL to the provided url
|
|
271
|
+
# - Else create a new entry with the url and name
|
|
272
|
+
#
|
|
273
|
+
# @param name [String] master name
|
|
274
|
+
# @param url [String] master url or name
|
|
275
|
+
# @return [Kontena::Cli::Config::Server]
|
|
276
|
+
def select_a_server(name, url)
|
|
277
|
+
# no url, no name, try to use current master
|
|
278
|
+
if url.nil? && name.nil?
|
|
279
|
+
if config.current_master
|
|
280
|
+
return config.current_master
|
|
281
|
+
else
|
|
282
|
+
exit_with_error 'URL not specified and current master not selected'
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
if name && url
|
|
287
|
+
exact_match = config.find_server_by(url: url, name: name)
|
|
288
|
+
return exact_match if exact_match # found an exact match, going to use that one.
|
|
289
|
+
|
|
290
|
+
name_match = config.find_server(name)
|
|
291
|
+
|
|
292
|
+
if name_match
|
|
293
|
+
#found a server with the provided name, set the provided url to it and return
|
|
294
|
+
name_match.url = url
|
|
295
|
+
return name_match
|
|
296
|
+
else
|
|
297
|
+
# nothing found, create new.
|
|
298
|
+
return Kontena::Cli::Config::Server.new(name: name, url: url)
|
|
299
|
+
end
|
|
300
|
+
elsif name
|
|
301
|
+
# only --name provided, try to find a server with that name
|
|
302
|
+
name_match = config.find_server(name)
|
|
303
|
+
|
|
304
|
+
if name_match && name_match.url
|
|
305
|
+
return name_match
|
|
306
|
+
else
|
|
307
|
+
exit_with_error "Master #{name} was found from config, but it does not have an URL and no URL was provided on command line"
|
|
308
|
+
end
|
|
309
|
+
elsif url
|
|
310
|
+
# only url provided
|
|
311
|
+
if url =~ /^https?:\/\//
|
|
312
|
+
# url is actually an url
|
|
313
|
+
url_match = config.find_server_by(url: url)
|
|
314
|
+
if url_match
|
|
315
|
+
return url_match
|
|
316
|
+
else
|
|
317
|
+
return Kontena::Cli::Config::Server.new(url: url, name: nil)
|
|
318
|
+
end
|
|
319
|
+
else
|
|
320
|
+
name_match = config.find_server(url)
|
|
321
|
+
if name_match
|
|
322
|
+
unless name_match.url
|
|
323
|
+
exit_with_error "Master #{url} was found from config, but it does not have an URL and no URL was provided on command line"
|
|
324
|
+
end
|
|
325
|
+
return name_match
|
|
326
|
+
else
|
|
327
|
+
exit_with_error "Can't find a master with name #{name} from configuration"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
280
330
|
end
|
|
281
331
|
end
|
|
282
332
|
|