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