kontena-cli 0.16.3 → 0.17.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +1 -0
  3. data/.gitignore +3 -1
  4. data/VERSION +1 -1
  5. data/lib/kontena/callbacks/master/deploy/40_install_ssl_certificate_after_deploy.rb +32 -0
  6. data/lib/kontena/cli/apps/deploy_command.rb +2 -2
  7. data/lib/kontena/cli/apps/scale_command.rb +2 -2
  8. data/lib/kontena/cli/apps/show_command.rb +3 -2
  9. data/lib/kontena/cli/apps/yaml/validations.rb +10 -6
  10. data/lib/kontena/cli/apps/yaml/validator.rb +1 -0
  11. data/lib/kontena/cli/apps/yaml/validator_v2.rb +1 -0
  12. data/lib/kontena/cli/cloud/login_command.rb +66 -64
  13. data/lib/kontena/cli/common.rb +0 -10
  14. data/lib/kontena/cli/grids/logs_command.rb +0 -1
  15. data/lib/kontena/cli/localhost_web_server.rb +11 -3
  16. data/lib/kontena/cli/master/login_command.rb +213 -163
  17. data/lib/kontena/cli/nodes/label_command.rb +2 -0
  18. data/lib/kontena/cli/nodes/labels/add_command.rb +7 -8
  19. data/lib/kontena/cli/nodes/labels/list_command.rb +17 -0
  20. data/lib/kontena/cli/nodes/labels/remove_command.rb +7 -12
  21. data/lib/kontena/cli/nodes/show_command.rb +1 -0
  22. data/lib/kontena/cli/plugins/common.rb +8 -0
  23. data/lib/kontena/cli/plugins/install_command.rb +21 -2
  24. data/lib/kontena/cli/plugins/list_command.rb +4 -2
  25. data/lib/kontena/cli/plugins/search_command.rb +4 -2
  26. data/lib/kontena/cli/registry/create_command.rb +19 -12
  27. data/lib/kontena/cli/registry/remove_command.rb +4 -4
  28. data/lib/kontena/cli/registry_command.rb +0 -1
  29. data/lib/kontena/cli/services/create_command.rb +6 -6
  30. data/lib/kontena/cli/services/deploy_command.rb +8 -4
  31. data/lib/kontena/cli/services/list_command.rb +34 -21
  32. data/lib/kontena/cli/services/logs_command.rb +1 -1
  33. data/lib/kontena/cli/services/scale_command.rb +3 -3
  34. data/lib/kontena/cli/services/services_helper.rb +18 -14
  35. data/lib/kontena/cli/services/show_command.rb +1 -0
  36. data/lib/kontena/cli/services/update_command.rb +6 -6
  37. data/lib/kontena/cli/stack_command.rb +12 -6
  38. data/lib/kontena/cli/stacks/build_command.rb +110 -0
  39. data/lib/kontena/cli/stacks/common.rb +85 -20
  40. data/lib/kontena/cli/stacks/deploy_command.rb +30 -7
  41. data/lib/kontena/cli/stacks/install_command.rb +30 -0
  42. data/lib/kontena/cli/stacks/list_command.rb +74 -14
  43. data/lib/kontena/cli/stacks/logs_command.rb +31 -0
  44. data/lib/kontena/cli/stacks/monitor_command.rb +91 -0
  45. data/lib/kontena/cli/stacks/remove_command.rb +24 -7
  46. data/lib/kontena/cli/stacks/service_generator.rb +115 -0
  47. data/lib/kontena/cli/stacks/service_generator_v2.rb +27 -0
  48. data/lib/kontena/cli/stacks/show_command.rb +65 -13
  49. data/lib/kontena/cli/stacks/upgrade_command.rb +28 -0
  50. data/lib/kontena/cli/stacks/yaml/custom_validators/affinities_validator.rb +19 -0
  51. data/lib/kontena/cli/stacks/yaml/custom_validators/build_validator.rb +22 -0
  52. data/lib/kontena/cli/stacks/yaml/custom_validators/extends_validator.rb +21 -0
  53. data/lib/kontena/cli/stacks/yaml/custom_validators/hooks_validator.rb +54 -0
  54. data/lib/kontena/cli/stacks/yaml/custom_validators/secrets_validator.rb +22 -0
  55. data/lib/kontena/cli/stacks/yaml/reader.rb +219 -0
  56. data/lib/kontena/cli/stacks/yaml/service_extender.rb +78 -0
  57. data/lib/kontena/cli/stacks/yaml/validations.rb +71 -0
  58. data/lib/kontena/cli/stacks/yaml/validator_v3.rb +52 -0
  59. data/lib/kontena/cli/version_command.rb +5 -1
  60. data/lib/kontena/cli/vpn/create_command.rb +20 -17
  61. data/lib/kontena/cli/vpn/remove_command.rb +4 -3
  62. data/lib/kontena/client.rb +21 -20
  63. data/lib/kontena/machine/cert_helper.rb +4 -0
  64. data/lib/kontena/machine/cloud_config/cloudinit.yml +1 -1
  65. data/lib/kontena/main_command.rb +1 -1
  66. data/spec/fixtures/kontena-build.yml +2 -2
  67. data/spec/fixtures/kontena-invalid.yml +1 -1
  68. data/spec/fixtures/kontena-not-hash-service-config.yml +1 -1
  69. data/spec/fixtures/kontena-with-env-file.yml +2 -2
  70. data/spec/fixtures/kontena_build_v3.yml +23 -0
  71. data/spec/fixtures/kontena_v3.yml +20 -0
  72. data/spec/fixtures/stack-internal-extend.yml +11 -0
  73. data/spec/fixtures/stack-with-env-file.yml +21 -0
  74. data/spec/fixtures/stack-with-variables.yml +22 -0
  75. data/spec/kontena/cli/app/scale_spec.rb +3 -1
  76. data/spec/kontena/cli/cloud/login_command_spec.rb +283 -0
  77. data/spec/kontena/cli/master/login_command_spec.rb +324 -145
  78. data/spec/kontena/cli/services/link_command_spec.rb +1 -1
  79. data/spec/kontena/cli/services/secrets/link_command_spec.rb +4 -4
  80. data/spec/kontena/cli/services/secrets/unlink_command_spec.rb +2 -2
  81. data/spec/kontena/cli/services/services_helper_spec.rb +15 -11
  82. data/spec/kontena/cli/services/unlink_command_spec.rb +1 -1
  83. data/spec/kontena/cli/stacks/deploy_command_spec.rb +26 -0
  84. data/spec/kontena/cli/stacks/install_command_spec.rb +54 -0
  85. data/spec/kontena/cli/stacks/list_command_spec.rb +27 -0
  86. data/spec/kontena/cli/stacks/remove_command_spec.rb +45 -0
  87. data/spec/kontena/cli/stacks/service_generator_spec.rb +385 -0
  88. data/spec/kontena/cli/stacks/service_generator_v2_spec.rb +74 -0
  89. data/spec/kontena/cli/stacks/show_command_spec.rb +26 -0
  90. data/spec/kontena/cli/stacks/upgrade_command_spec.rb +50 -0
  91. data/spec/kontena/cli/stacks/yaml/reader_spec.rb +370 -0
  92. data/spec/kontena/cli/stacks/yaml/service_extender_spec.rb +128 -0
  93. data/spec/kontena/cli/stacks/yaml/validator_v3_spec.rb +302 -0
  94. data/spec/spec_helper.rb +6 -4
  95. data/spec/support/client_helpers.rb +1 -0
  96. metadata +57 -7
  97. data/lib/kontena/cli/registry/delete_command.rb +0 -18
  98. data/lib/kontena/cli/stacks/create_command.rb +0 -27
  99. 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: 35bc91fa20a773394c0648eb255b1227016a2196
4
- data.tar.gz: c5f6e11b8dbc89950b8377ea9adc45c7eba17bcf
3
+ metadata.gz: 1ad6422b172a48accf054239e4dab6b61fa03f55
4
+ data.tar.gz: 536fafed2231e6fcece9120b06402aa88cab080b
5
5
  SHA512:
6
- metadata.gz: 3380de50851f421c6f72329182f0b4cc077a7f3ed5835f129d9282d87acd9dc11cfc280eacac69666b68f7657d1a42afe07a8063bdee8af27679638c9c935f70
7
- data.tar.gz: db89b1df29886e1eaa067cdc4b18abf36fd0eec9d0cbb0492c2a82c783c63a83097b1551efe8d9017901505ede538c2672202d1583d23e64ea193cde22bbd726
6
+ metadata.gz: 889ecc5b3aef9a47aab715c3bfc4cb1a5200589b2c8fab55ceb00c7ac5d95f2d2fb8ceb06638c5f3de870920de2102c3722f9c120d20b3c494c1c875a57942db
7
+ data.tar.gz: 2140338ef86e138a7fdf5de202b7d8246e4402ade84327a493aea66307552daeca81c38a27b7b866804ef81cc219d95236713c65ff4736b83452c9fd624f034a
data/.dockerignore CHANGED
@@ -3,3 +3,4 @@
3
3
  vendor
4
4
  omnibus
5
5
  examples
6
+ coverage
data/.gitignore CHANGED
@@ -12,4 +12,6 @@
12
12
  *.o
13
13
  *.a
14
14
  mkmf.log
15
- .idea
15
+ .idea
16
+ *.gem
17
+ /vendor/
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.16.3
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, service['id'])
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, parse_service_id(prefixed_name(service)))
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(require_token, prefixed_name(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
- HashValidator.append_validator(AffinitiesValidator.new)
11
- HashValidator.append_validator(BuildValidator.new)
12
- HashValidator.append_validator(ExtendsValidator.new)
13
- HashValidator.append_validator(SecretsValidator.new)
14
- HashValidator.append_validator(HooksValidator.new)
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
@@ -12,6 +12,7 @@ module Kontena::Cli::Apps
12
12
  @schema['net'] = optional(%w(host bridge))
13
13
  @schema['log_driver'] = optional('string')
14
14
  @schema['log_opts'] = optional({})
15
+ Validations::CustomValidators.load
15
16
  end
16
17
 
17
18
  ##
@@ -16,6 +16,7 @@ module Kontena::Cli::Apps
16
16
  'driver' => optional('string'),
17
17
  'options' => optional(-> (value) { value.is_a?(Hash) })
18
18
  })
19
+ Validations::CustomValidators.load
19
20
  end
20
21
 
21
22
  ##
@@ -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
- require_relative '../localhost_web_server'
13
- require 'launchy'
13
+ if self.code && self.force?
14
+ exit_with_error "Can't use --code and --force together"
15
+ end
14
16
 
15
- if self.code
16
- kontena_account.token ||= Kontena::Cli::Config::Token.new(access_token: self.token, parent_type: :account, parent_name: kontena_account.name)
17
- client = Kontena::Client.new(kontena_account.token_endpoint, kontena_account.token)
18
- response = nil
19
- begin
20
- vspinner "Exchanging authorization code to access token" do
21
- response = client.exchange_code(self.code)
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 = false
44
- vspinner "Verifying current access token" do
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
- config.write
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
- vspinner "Waiting for browser authorization response" do
89
+ spinner "Waiting for browser authorization response" do
89
90
  server_thread.join
90
91
  end
91
92
  browser_thread.join
92
93
 
93
- response = Thread.main['response']
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
- client = Kontena::Client.new(uri.to_s, kontena_account.token)
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
- config.write
121
- display_logo
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
- puts "Authentication failed".colorize(:red)
128
- exit 1
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
@@ -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 = Hash[*URI.decode_www_form(uri.query).flatten(1)].reject{|_,v| v.to_s == ''}
86
- params.map{|k,v| v = (v =~ /\A\d+\z$/ ? v.to_i : v); [k,v]}
87
- params = Hash[*params.flatten(1)]
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
- # rewrites self.url
24
- use_current_master_if_available || use_master_by_name
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
- # find server by url or create a new one
27
- server = find_server_or_create_new(url)
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
- # set server token from self.token or create a new one
30
- set_server_token(server)
32
+ server = select_a_server(self.name, self.url)
31
33
 
32
- # set server token by exchanging code if --code given
33
- use_authorization_code(server, self.code) if self.code
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
- client = Kontena::Client.new(server.url, server.token)
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
- # Unless an invitation code was supplied, check auth and exit
38
- # if it works already.
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
- config.write
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
- # no local browser? tell user to launch an external one
49
- if self.remote?
50
- config.current_server = server.name
51
- config.write
52
- display_remote_message_and_exit(get_authorization_url)
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
- def use_current_master_if_available
75
- return nil if self.url
76
- if config.current_master
77
- self.url = config.current_master.url
78
- true
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
- exit_with_error "Current master is not set and URL was not provided."
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 use_master_by_name
85
- return if self.url =~ /^(?:http|https):\/\//
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 find_server_or_create_new(url)
96
- existing_server = config.find_server_by(url: url, name: self.name)
97
- if existing_server
98
- config.current_server = existing_server.name
99
- existing_server
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
- new_server = Kontena::Cli::Config::Server.new(url: url, name: self.name)
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 set_server_token(server)
109
- if self.token
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
- client = Kontena::Client.new(server.url, server.token)
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
- true
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
- if server && server.token && server.token.access_token
149
- # See if the existing or supplied authentication works without reauthenticating
150
- auth_ok = false
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
- def build_auth_url_path(port = nil)
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 self.remote?
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
- auth_url_params[:redirect_uri] = "http://localhost:#{port}/cb"
132
+ raise ArgumentError, "Local port not defined and not performing remote login"
167
133
  end
168
- auth_url_params[:invite_code] = self.join if self.join
169
- auth_url_params[:expires_in] = self.expires_in if self.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
- def get_authorization_url(web_server_port = nil)
174
- authorization_url = nil
175
-
176
- http_client = Kontena::Client.new(self.url)
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
- http_client.request(
147
+ response = client.request(
180
148
  http_method: :get,
181
- path: build_auth_url_path(web_server_port),
149
+ path: authentication_path(auth_params),
182
150
  expects: [501, 400, 302, 403],
183
151
  auth: false
184
152
  )
185
153
 
186
- case http_client.last_response.status
187
- when 302
188
- authorization_url = http_client.last_response.headers['Location']
189
- when 501
190
- exit_with_error "Authentication provider not configured"
191
- when 403
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 display_remote_message_and_exit(url)
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 response_from_web_flow
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
- uri = URI.parse(get_authorization_url(web_server.port))
222
- puts "Opening browser to #{uri.scheme}://#{uri.host}"
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
- vspinner "Waiting for browser authorization response" do
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 in_to_at(expires_in)
251
- if expires_in.to_i > 0
252
- Time.now.utc.to_i + expires_in.to_i
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
- nil
229
+ server.name = next_default_name
255
230
  end
256
231
  end
257
232
 
258
- def update_server_token(server, response)
259
- server.token = Kontena::Cli::Config::Token.new
260
- server.token.access_token = response['access_token']
261
- server.token.refresh_token = response['refresh_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 update_server_name(server, response)
268
- return unless server.name.nil?
269
-
270
- if self.name
271
- server.name = self.name
272
- elsif response['server'] && response['server']['name']
273
- server.name = response['server']['name']
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.name = "kontena-master"
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