kontena-cli 0.15.5 → 0.16.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +0 -3
  3. data/Gemfile +3 -0
  4. data/LOGO +8 -0
  5. data/VERSION +1 -1
  6. data/kontena-cli.gemspec +2 -3
  7. data/lib/kontena/callback.rb +57 -0
  8. data/lib/kontena/callbacks/.gitkeep +0 -0
  9. data/lib/kontena/callbacks/auth/01_list_and_select_grid_after_master_auth.rb +27 -0
  10. data/lib/kontena/callbacks/master/01_clear_current_master_after_terminate.rb +20 -0
  11. data/lib/kontena/callbacks/master/deploy/01_show_logo_before_deploy.rb +15 -0
  12. data/lib/kontena/callbacks/master/deploy/05_before_deploy_configuration_wizard.rb +124 -0
  13. data/lib/kontena/callbacks/master/deploy/50_authenticate_after_deploy.rb +53 -0
  14. data/lib/kontena/callbacks/master/deploy/55_create_initial_grid_after_deploy.rb +32 -0
  15. data/lib/kontena/callbacks/master/deploy/60_configure_auth_provider_after_deploy.rb +49 -0
  16. data/lib/kontena/callbacks/master/deploy/90_suggest_inviting_yourself_after_deploy.rb +24 -0
  17. data/lib/kontena/cli/app_command.rb +2 -1
  18. data/lib/kontena/cli/apps/build_command.rb +1 -1
  19. data/lib/kontena/cli/apps/common.rb +6 -1
  20. data/lib/kontena/cli/apps/config_command.rb +1 -1
  21. data/lib/kontena/cli/apps/deploy_command.rb +1 -1
  22. data/lib/kontena/cli/apps/init_command.rb +3 -5
  23. data/lib/kontena/cli/apps/list_command.rb +1 -1
  24. data/lib/kontena/cli/apps/logs_command.rb +1 -1
  25. data/lib/kontena/cli/apps/monitor_command.rb +1 -1
  26. data/lib/kontena/cli/apps/remove_command.rb +2 -3
  27. data/lib/kontena/cli/apps/restart_command.rb +1 -1
  28. data/lib/kontena/cli/apps/scale_command.rb +1 -1
  29. data/lib/kontena/cli/apps/show_command.rb +1 -1
  30. data/lib/kontena/cli/apps/start_command.rb +1 -1
  31. data/lib/kontena/cli/apps/stop_command.rb +1 -1
  32. data/lib/kontena/cli/apps/yaml/reader.rb +3 -13
  33. data/lib/kontena/cli/apps/yaml/validator.rb +0 -4
  34. data/lib/kontena/cli/apps/yaml/validator_v2.rb +1 -5
  35. data/lib/kontena/cli/certificate/authorize_command.rb +1 -1
  36. data/lib/kontena/cli/certificate/get_command.rb +1 -1
  37. data/lib/kontena/cli/certificate/register_command.rb +1 -1
  38. data/lib/kontena/cli/certificate_command.rb +1 -1
  39. data/lib/kontena/cli/cloud/login_command.rb +128 -0
  40. data/lib/kontena/cli/cloud/master/add_command.rb +54 -0
  41. data/lib/kontena/cli/cloud/master/delete_command.rb +20 -0
  42. data/lib/kontena/cli/cloud/master/list_command.rb +29 -0
  43. data/lib/kontena/cli/cloud/master/show_command.rb +23 -0
  44. data/lib/kontena/cli/cloud/master/update_command.rb +58 -0
  45. data/lib/kontena/cli/cloud/master_command.rb +21 -0
  46. data/lib/kontena/cli/cloud_command.rb +10 -0
  47. data/lib/kontena/cli/common.rb +230 -88
  48. data/lib/kontena/cli/config.rb +537 -0
  49. data/lib/kontena/cli/container_command.rb +1 -1
  50. data/lib/kontena/cli/containers/exec_command.rb +1 -1
  51. data/lib/kontena/cli/containers/inspect_command.rb +1 -1
  52. data/lib/kontena/cli/etcd/get_command.rb +1 -1
  53. data/lib/kontena/cli/etcd/list_command.rb +1 -1
  54. data/lib/kontena/cli/etcd/mkdir_command.rb +1 -1
  55. data/lib/kontena/cli/etcd/remove_command.rb +1 -1
  56. data/lib/kontena/cli/etcd/set_command.rb +1 -1
  57. data/lib/kontena/cli/etcd_command.rb +1 -1
  58. data/lib/kontena/cli/external_registries/add_command.rb +1 -1
  59. data/lib/kontena/cli/external_registries/delete_command.rb +1 -1
  60. data/lib/kontena/cli/external_registries/list_command.rb +1 -1
  61. data/lib/kontena/cli/external_registries/remove_command.rb +1 -1
  62. data/lib/kontena/cli/external_registry_command.rb +1 -1
  63. data/lib/kontena/cli/grid_command.rb +1 -1
  64. data/lib/kontena/cli/grids/audit_log_command.rb +6 -5
  65. data/lib/kontena/cli/grids/cloud_config_command.rb +1 -1
  66. data/lib/kontena/cli/grids/common.rb +1 -1
  67. data/lib/kontena/cli/grids/create_command.rb +8 -4
  68. data/lib/kontena/cli/grids/current_command.rb +1 -1
  69. data/lib/kontena/cli/grids/env_command.rb +1 -1
  70. data/lib/kontena/cli/grids/list_command.rb +35 -10
  71. data/lib/kontena/cli/grids/logs_command.rb +1 -1
  72. data/lib/kontena/cli/grids/remove_command.rb +2 -2
  73. data/lib/kontena/cli/grids/show_command.rb +1 -1
  74. data/lib/kontena/cli/grids/trusted_subnet_command.rb +1 -1
  75. data/lib/kontena/cli/grids/trusted_subnets/add_command.rb +1 -1
  76. data/lib/kontena/cli/grids/trusted_subnets/list_command.rb +1 -1
  77. data/lib/kontena/cli/grids/trusted_subnets/remove_command.rb +1 -1
  78. data/lib/kontena/cli/grids/update_command.rb +1 -1
  79. data/lib/kontena/cli/grids/use_command.rb +11 -6
  80. data/lib/kontena/cli/grids/user_command.rb +1 -1
  81. data/lib/kontena/cli/grids/users/add_command.rb +1 -1
  82. data/lib/kontena/cli/grids/users/list_command.rb +1 -1
  83. data/lib/kontena/cli/grids/users/remove_command.rb +1 -1
  84. data/lib/kontena/cli/localhost_web_server.rb +93 -0
  85. data/lib/kontena/cli/login_command.rb +5 -118
  86. data/lib/kontena/cli/logout_command.rb +33 -2
  87. data/lib/kontena/cli/master/audit_log_command.rb +19 -0
  88. data/lib/kontena/cli/master/config/export_command.rb +47 -0
  89. data/lib/kontena/cli/master/config/get_command.rb +24 -0
  90. data/lib/kontena/cli/master/config/import_command.rb +69 -0
  91. data/lib/kontena/cli/master/config/set_command.rb +19 -0
  92. data/lib/kontena/cli/master/config/unset_command.rb +20 -0
  93. data/lib/kontena/cli/master/config_command.rb +24 -0
  94. data/lib/kontena/cli/master/create_command.rb +76 -0
  95. data/lib/kontena/cli/master/current_command.rb +10 -2
  96. data/lib/kontena/cli/master/join_command.rb +20 -0
  97. data/lib/kontena/cli/master/list_command.rb +4 -4
  98. data/lib/kontena/cli/master/login_command.rb +274 -0
  99. data/lib/kontena/cli/master/use_command.rb +8 -19
  100. data/lib/kontena/cli/master/users/invite_command.rb +33 -6
  101. data/lib/kontena/cli/master/users/list_command.rb +2 -2
  102. data/lib/kontena/cli/master/users/remove_command.rb +1 -1
  103. data/lib/kontena/cli/master/users/role_command.rb +1 -1
  104. data/lib/kontena/cli/master/users/roles/add_command.rb +18 -16
  105. data/lib/kontena/cli/master/users/roles/remove_command.rb +1 -1
  106. data/lib/kontena/cli/master/users_command.rb +1 -1
  107. data/lib/kontena/cli/master_command.rb +21 -1
  108. data/lib/kontena/cli/node_command.rb +1 -1
  109. data/lib/kontena/cli/nodes/label_command.rb +1 -1
  110. data/lib/kontena/cli/nodes/labels/add_command.rb +1 -1
  111. data/lib/kontena/cli/nodes/labels/remove_command.rb +1 -1
  112. data/lib/kontena/cli/nodes/list_command.rb +1 -1
  113. data/lib/kontena/cli/nodes/remove_command.rb +1 -1
  114. data/lib/kontena/cli/nodes/show_command.rb +1 -1
  115. data/lib/kontena/cli/nodes/ssh_command.rb +1 -1
  116. data/lib/kontena/cli/nodes/update_command.rb +1 -1
  117. data/lib/kontena/cli/plugin_command.rb +1 -1
  118. data/lib/kontena/cli/plugins/install_command.rb +2 -2
  119. data/lib/kontena/cli/plugins/list_command.rb +2 -2
  120. data/lib/kontena/cli/plugins/search_command.rb +1 -1
  121. data/lib/kontena/cli/plugins/uninstall_command.rb +2 -2
  122. data/lib/kontena/cli/registry/create_command.rb +2 -4
  123. data/lib/kontena/cli/registry/delete_command.rb +1 -1
  124. data/lib/kontena/cli/registry/remove_command.rb +1 -1
  125. data/lib/kontena/cli/registry_command.rb +1 -1
  126. data/lib/kontena/cli/service_command.rb +1 -1
  127. data/lib/kontena/cli/services/container_command.rb +1 -1
  128. data/lib/kontena/cli/services/containers_command.rb +1 -1
  129. data/lib/kontena/cli/services/create_command.rb +1 -1
  130. data/lib/kontena/cli/services/delete_command.rb +1 -1
  131. data/lib/kontena/cli/services/deploy_command.rb +1 -1
  132. data/lib/kontena/cli/services/env_command.rb +1 -1
  133. data/lib/kontena/cli/services/envs/add_command.rb +1 -1
  134. data/lib/kontena/cli/services/envs/list_command.rb +1 -1
  135. data/lib/kontena/cli/services/envs/remove_command.rb +1 -1
  136. data/lib/kontena/cli/services/link_command.rb +1 -1
  137. data/lib/kontena/cli/services/list_command.rb +1 -1
  138. data/lib/kontena/cli/services/logs_command.rb +1 -1
  139. data/lib/kontena/cli/services/monitor_command.rb +1 -1
  140. data/lib/kontena/cli/services/remove_command.rb +1 -1
  141. data/lib/kontena/cli/services/restart_command.rb +1 -1
  142. data/lib/kontena/cli/services/scale_command.rb +1 -1
  143. data/lib/kontena/cli/services/secret_command.rb +1 -1
  144. data/lib/kontena/cli/services/secrets/link_command.rb +1 -1
  145. data/lib/kontena/cli/services/secrets/unlink_command.rb +1 -1
  146. data/lib/kontena/cli/services/services_helper.rb +6 -3
  147. data/lib/kontena/cli/services/show_command.rb +1 -1
  148. data/lib/kontena/cli/services/start_command.rb +1 -1
  149. data/lib/kontena/cli/services/stats_command.rb +1 -1
  150. data/lib/kontena/cli/services/stop_command.rb +1 -1
  151. data/lib/kontena/cli/services/unlink_command.rb +1 -1
  152. data/lib/kontena/cli/services/update_command.rb +1 -1
  153. data/lib/kontena/cli/spinner.rb +122 -0
  154. data/lib/kontena/cli/stack_command.rb +1 -1
  155. data/lib/kontena/cli/stacks/create_command.rb +1 -1
  156. data/lib/kontena/cli/stacks/deploy_command.rb +1 -1
  157. data/lib/kontena/cli/stacks/list_command.rb +1 -1
  158. data/lib/kontena/cli/stacks/remove_command.rb +1 -1
  159. data/lib/kontena/cli/stacks/show_command.rb +1 -1
  160. data/lib/kontena/cli/stacks/update_command.rb +1 -1
  161. data/lib/kontena/cli/vault/list_command.rb +1 -1
  162. data/lib/kontena/cli/vault/read_command.rb +1 -1
  163. data/lib/kontena/cli/vault/remove_command.rb +1 -1
  164. data/lib/kontena/cli/vault/update_command.rb +1 -1
  165. data/lib/kontena/cli/vault/write_command.rb +1 -1
  166. data/lib/kontena/cli/vault_command.rb +1 -1
  167. data/lib/kontena/cli/version.rb +1 -1
  168. data/lib/kontena/cli/version_command.rb +1 -1
  169. data/lib/kontena/cli/vpn/config_command.rb +1 -1
  170. data/lib/kontena/cli/vpn/create_command.rb +2 -4
  171. data/lib/kontena/cli/vpn/delete_command.rb +1 -1
  172. data/lib/kontena/cli/vpn/remove_command.rb +1 -1
  173. data/lib/kontena/cli/vpn_command.rb +1 -1
  174. data/lib/kontena/cli/whoami_command.rb +16 -13
  175. data/lib/kontena/client.rb +410 -90
  176. data/lib/kontena/command.rb +172 -0
  177. data/lib/kontena/main_command.rb +7 -8
  178. data/lib/kontena/presets/github_auth_provider.yml +11 -0
  179. data/lib/kontena/presets/kontena_auth_provider.yml +11 -0
  180. data/lib/kontena_cli.rb +51 -1
  181. data/spec/kontena/cli/app/deploy_command_spec.rb +14 -44
  182. data/spec/kontena/cli/app/scale_spec.rb +1 -1
  183. data/spec/kontena/cli/app/yaml/reader_spec.rb +0 -48
  184. data/spec/kontena/cli/common_spec.rb +63 -59
  185. data/spec/kontena/cli/grids/use_command_spec.rb +43 -0
  186. data/spec/kontena/cli/master/current_command_spec.rb +3 -24
  187. data/spec/kontena/cli/master/use_command_spec.rb +2 -27
  188. data/spec/kontena/cli/master/users/invite_command_spec.rb +4 -18
  189. data/spec/kontena/cli/master/users/roles/add_command_spec.rb +2 -16
  190. data/spec/kontena/cli/master/users/roles/remove_command_spec.rb +2 -13
  191. data/spec/kontena/cli/services/restart_command_spec.rb +1 -1
  192. data/spec/kontena/cli/services/update_command_spec.rb +5 -5
  193. data/spec/kontena/client_spec.rb +104 -35
  194. data/spec/kontena/config_spec.rb +65 -0
  195. data/spec/spec_helper.rb +25 -3
  196. data/spec/support/client_helpers.rb +10 -3
  197. data/spec/support/requirements_helper.rb +32 -0
  198. metadata +61 -48
  199. data/lib/kontena/cli/register_command.rb +0 -23
  200. data/lib/kontena/cli/user/forgot_password_command.rb +0 -16
  201. data/lib/kontena/cli/user/reset_password_command.rb +0 -23
  202. data/lib/kontena/cli/user/verify_command.rb +0 -20
  203. data/lib/kontena/cli/user_command.rb +0 -13
  204. data/spec/fixtures/kontena-malformed-yaml.yml +0 -6
  205. data/spec/fixtures/kontena-not-hash-service-config.yml +0 -3
  206. data/spec/kontena/cli/login_command_spec.rb +0 -32
  207. data/spec/kontena/cli/register_command_spec.rb +0 -57
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vault
2
- class ListCommand < Clamp::Command
2
+ class ListCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vault
2
- class ReadCommand < Clamp::Command
2
+ class ReadCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vault
2
- class RemoveCommand < Clamp::Command
2
+ class RemoveCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vault
2
- class UpdateCommand < Clamp::Command
2
+ class UpdateCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
 
5
5
  parameter 'NAME', 'Secret name'
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vault
2
- class WriteCommand < Clamp::Command
2
+ class WriteCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
 
@@ -4,7 +4,7 @@ require_relative 'vault/read_command'
4
4
  require_relative 'vault/remove_command'
5
5
  require_relative 'vault/update_command'
6
6
 
7
- class Kontena::Cli::VaultCommand < Clamp::Command
7
+ class Kontena::Cli::VaultCommand < Kontena::Command
8
8
 
9
9
  subcommand ["list", "ls"], "List secrets", Kontena::Cli::Vault::ListCommand
10
10
  subcommand "write", "Write a secret", Kontena::Cli::Vault::WriteCommand
@@ -1,5 +1,5 @@
1
1
  module Kontena
2
2
  module Cli
3
- VERSION = File.read(File.realpath(File.join(__dir__, '../../../VERSION'))).strip
3
+ VERSION = File.read(File.realpath(File.join(__dir__, '../../../VERSION'))).strip unless const_defined?(:VERSION)
4
4
  end
5
5
  end
@@ -1,6 +1,6 @@
1
1
  require_relative 'version'
2
2
 
3
- class Kontena::Cli::VersionCommand < Clamp::Command
3
+ class Kontena::Cli::VersionCommand < Kontena::Command
4
4
  include Kontena::Cli::Common
5
5
 
6
6
  def execute
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vpn
2
- class ConfigCommand < Clamp::Command
2
+ class ConfigCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
 
@@ -1,7 +1,5 @@
1
- require 'shell-spinner'
2
-
3
1
  module Kontena::Cli::Vpn
4
- class CreateCommand < Clamp::Command
2
+ class CreateCommand < Kontena::Command
5
3
  include Kontena::Cli::Common
6
4
  include Kontena::Cli::GridOptions
7
5
 
@@ -36,7 +34,7 @@ module Kontena::Cli::Vpn
36
34
  }
37
35
  client(token).post("grids/#{current_grid}/services", data)
38
36
  client(token).post("services/#{current_grid}/vpn/deploy", {})
39
- ShellSpinner "Deploying vpn service " do
37
+ spinner "Deploying vpn service " do
40
38
  sleep 1 until client(token).get("services/#{current_grid}/vpn")['state'] != 'deploying'
41
39
  end
42
40
  puts "OpenVPN service is now started (udp://#{vpn_ip}:1194)."
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vpn
2
- class DeleteCommand < Clamp::Command
2
+ class DeleteCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
  include Kontena::Cli::GridOptions
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Kontena::Cli::Vpn
2
- class RemoveCommand < Clamp::Command
2
+ class RemoveCommand < Kontena::Command
3
3
  include Kontena::Cli::Common
4
4
 
5
5
  option "--force", :flag, "Force remove", default: false, attribute_name: :forced
@@ -3,7 +3,7 @@ require_relative 'vpn/config_command'
3
3
  require_relative 'vpn/remove_command'
4
4
  require_relative 'vpn/delete_command'
5
5
 
6
- class Kontena::Cli::VpnCommand < Clamp::Command
6
+ class Kontena::Cli::VpnCommand < Kontena::Command
7
7
 
8
8
  subcommand "create", "Create VPN service", Kontena::Cli::Vpn::CreateCommand
9
9
  subcommand "config", "Show/Export VPN config", Kontena::Cli::Vpn::ConfigCommand
@@ -1,7 +1,8 @@
1
- class Kontena::Cli::WhoamiCommand < Clamp::Command
1
+ class Kontena::Cli::WhoamiCommand < Kontena::Command
2
2
  include Kontena::Cli::Common
3
3
 
4
4
  option '--bash-completion-path', :flag, 'Show bash completion path', hidden: true
5
+ option '--token', :flag, 'Show current master token', hidden: true
5
6
 
6
7
  def execute
7
8
  if bash_completion_path?
@@ -9,26 +10,28 @@ class Kontena::Cli::WhoamiCommand < Clamp::Command
9
10
  exit 0
10
11
  end
11
12
 
13
+ if self.token?
14
+ if config.current_master && config.current_master.token
15
+ puts config.current_master.token.access_token
16
+ exit 0
17
+ else
18
+ exit 1
19
+ end
20
+ end
21
+
12
22
  require_api_url
13
23
  puts "Master: #{ENV['KONTENA_URL'] || self.current_master['name']}"
14
24
  puts "URL: #{ENV['KONTENA_URL'] || api_url}"
15
25
  puts "Grid: #{ENV['KONTENA_GRID'] || current_grid}"
16
26
  unless ENV['KONTENA_URL']
17
- if current_master['email']
18
- puts "User: #{current_master['email']}"
27
+ if current_master['username']
28
+ puts "User: #{current_master['username']}"
19
29
  else # In case local storage doesn't have the user email yet
20
30
  token = require_token
21
- user = client(token).get('user')
31
+ user = client.get('user')
22
32
  puts "User: #{user['email']}"
23
- master = {
24
- 'name' => current_master['name'],
25
- 'url' => current_master['url'],
26
- 'token' => current_master['token'],
27
- 'email' => user['email'],
28
- 'grid' => current_master['grid']
29
- }
30
-
31
- self.add_master(current_master['name'], master)
33
+ current_master['username'] = user['email']
34
+ config.write
32
35
  end
33
36
  end
34
37
  end
@@ -1,63 +1,216 @@
1
1
  require 'json'
2
2
  require 'excon'
3
+ require 'uri'
4
+ require 'base64'
5
+ require 'socket'
6
+ require 'openssl'
7
+ require 'uri'
3
8
  require_relative 'errors'
4
9
  require_relative 'cli/version'
10
+ begin
11
+ require_relative 'cli/config'
12
+ rescue LoadError
13
+ end
5
14
 
6
15
  module Kontena
7
16
  class Client
8
17
 
9
- attr_accessor :default_headers, :path_prefix
18
+ CLIENT_ID = ENV['KONTENA_CLIENT_ID'] || '15faec8a7a9b4f1e8b7daebb1307f1d8'.freeze
19
+ CLIENT_SECRET = ENV['KONTENA_CLIENT_SECRET'] || 'fb8942ae00da4c7b8d5a1898effc742f'.freeze
20
+
21
+ CONTENT_URLENCODED = 'application/x-www-form-urlencoded'.freeze
22
+ CONTENT_JSON = 'application/json'.freeze
23
+ JSON_REGEX = /application\/(.+?\+)?json/.freeze
24
+ CONTENT_TYPE = 'Content-Type'.freeze
25
+ ACCEPT = 'Accept'.freeze
26
+ AUTHORIZATION = 'Authorization'.freeze
27
+
28
+ attr_accessor :default_headers
29
+ attr_accessor :path_prefix
10
30
  attr_reader :http_client
31
+ attr_reader :last_response
32
+ attr_reader :options
33
+ attr_reader :token
34
+ attr_reader :logger
35
+ attr_reader :api_url
36
+ attr_reader :host
11
37
 
12
38
  # Initialize api client
13
39
  #
14
40
  # @param [String] api_url
15
- # @param [Hash] default_headers
16
- def initialize(api_url, default_headers = {})
41
+ # @param [Kontena::Cli::Config::Token,Hash] access_token
42
+ # @param [Hash] options
43
+ def initialize(api_url, token = nil, options = {})
44
+ @api_url, @token, @options = api_url, token, options
45
+ uri = URI.parse(@api_url)
46
+ @host = uri.host
47
+
48
+ @logger = Logger.new(STDOUT)
49
+ @logger.level = ENV["DEBUG"].nil? ? Logger::INFO : Logger::DEBUG
50
+ @logger.progname = 'CLIENT'
51
+
52
+ @options[:default_headers] ||= {}
17
53
  Excon.defaults[:ssl_verify_peer] = false if ignore_ssl_errors?
18
- @http_client = Excon.new(api_url)
54
+
55
+ @http_client = Excon.new(api_url, omit_default_port: true)
56
+
19
57
  @default_headers = {
20
- 'Accept' => 'application/json',
21
- 'Content-Type' => 'application/json',
58
+ ACCEPT => CONTENT_JSON,
59
+ CONTENT_TYPE => CONTENT_JSON,
22
60
  'User-Agent' => "kontena-cli/#{Kontena::Cli::VERSION}"
23
- }.merge(default_headers)
61
+ }.merge(options[:default_headers])
62
+
63
+ if token
64
+ if token.kind_of?(String)
65
+ @token = { 'access_token' => token }
66
+ else
67
+ @token = token
68
+ end
69
+ @default_headers.merge!('Authorization' => "Bearer #{@token['access_token']}")
70
+ end
71
+
24
72
  @api_url = api_url
25
- @path_prefix = '/v1/'
73
+ @path_prefix = options[:prefix] || '/v1/'
26
74
  end
27
75
 
28
- # Get request
76
+ # Generates a header hash for HTTP basic authentication.
77
+ # Defaults to using client_id and client_secret as user/pass
29
78
  #
30
- # @param [String] path
31
- # @param [Hash,NilClass] params
32
- # @param [Hash] headers
33
- # @return [Hash]
34
- def get(path, params = nil, headers = {})
35
- response = http_client.get(
36
- path: request_uri(path),
37
- query: params,
38
- headers: request_headers(headers)
39
- )
40
- if response.status == 200
41
- parse_response(response)
79
+ # @param [String] username
80
+ # @param [String] password
81
+ # @return [Hash] auth_header_hash
82
+ def basic_auth_header(user = nil, pass = nil)
83
+ user ||= client_id
84
+ pass ||= client_secret
85
+ {
86
+ AUTHORIZATION =>
87
+ "Basic #{Base64.encode64([user, pass].join(':')).gsub(/[\r\n]/, '')}"
88
+ }
89
+ end
90
+
91
+ # Generates a bearer token authentication header hash if a token object is
92
+ # available. Otherwise returns an empty hash.
93
+ #
94
+ # @return [Hash] authentication_header
95
+ def bearer_authorization_header
96
+ if token && token['access_token']
97
+ {AUTHORIZATION => "Bearer #{token['access_token']}"}
42
98
  else
43
- handle_error_response(response)
99
+ {}
44
100
  end
45
101
  end
46
102
 
103
+ # OAuth2 client_id from ENV KONTENA_CLIENT_ID or client CLIENT_ID constant
104
+ #
105
+ # @return [String]
106
+ def client_id
107
+ ENV['KONTENA_CLIENT_ID'] || CLIENT_ID
108
+ end
109
+
110
+ # OAuth2 client_secret from ENV KONTENA_CLIENT_SECRET or client CLIENT_SECRET constant
111
+ #
112
+ # @return [String]
113
+ def client_secret
114
+ ENV['KONTENA_CLIENT_SECRET'] || CLIENT_SECRET
115
+ end
116
+
117
+ # Requests path supplied as argument and returns true if the request was a success.
118
+ # For checking if the current authentication is valid.
119
+ #
120
+ # @param [String] token_verify_path a path that requires authentication
121
+ # @return [Boolean]
122
+ def authentication_ok?(token_verify_path)
123
+ return false unless token
124
+ return false unless token['access_token']
125
+ return false unless token_verify_path
126
+
127
+ uri = URI.parse(token_verify_path)
128
+ host_options = {}
129
+ host_options[:host] = uri.host if uri.host
130
+ host_options[:port] = uri.port if uri.port
131
+
132
+ if uri.host
133
+ client = Kontena::Client.new("#{uri.scheme}://#{uri.host}:#{uri.port}", token)
134
+ else
135
+ client = self
136
+ end
137
+
138
+ final_path = uri.path.gsub(/\:access\_token/, token['access_token'])
139
+ logger.debug "Requesting user info from #{final_path} - #{host_options}"
140
+ client.request(path: final_path)
141
+ true
142
+ rescue
143
+ false
144
+ end
145
+
146
+ # Calls the code exchange endpoint in token's config to exchange an authorization_code
147
+ # to a access_token
148
+ def exchange_code(code)
149
+ return nil unless token_account
150
+ return nil unless token_account['token_endpoint']
151
+ uri = URI.parse(token_account['token_endpoint'])
152
+ host_options = {}
153
+ host_options[:host] = uri.host if uri.host
154
+ host_options[:port] = uri.port if uri.port
155
+
156
+ if uri.host
157
+ client = Kontena::Client.new("#{uri.scheme}://#{uri.host}:#{uri.port}")
158
+ else
159
+ client = self
160
+ end
161
+
162
+ client.request(
163
+ {
164
+ http_method: token_account['token_method'].downcase.to_sym,
165
+ path: uri.path,
166
+ headers: { CONTENT_TYPE => token_account['token_post_content_type'] },
167
+ body: {
168
+ 'grant_type' => 'authorization_code',
169
+ 'code' => code,
170
+ 'client_id' => Kontena::Client::CLIENT_ID,
171
+ 'client_secret' => Kontena::Client::CLIENT_SECRET
172
+ },
173
+ expects: [200,201],
174
+ auth: false
175
+ }
176
+ )
177
+ rescue
178
+ logger.debug "Code exchange exception: #{$!} #{$!.message}\n#{$!.backtrace}"
179
+ nil
180
+ end
181
+
182
+ # Return server version from a Kontena master by requesting '/'
183
+ #
184
+ # @return [String] version_string
185
+ def server_version
186
+ request(auth: false, expects: 200)['version']
187
+ rescue
188
+ logger.debug "Server version exception: #{$!} #{$!.message}"
189
+ nil
190
+ end
191
+
192
+ # OAuth2 client_id from ENV KONTENA_CLIENT_ID or client CLIENT_ID constant
193
+ #
194
+ # @return [String]
195
+ def client_id
196
+ ENV['KONTENA_CLIENT_ID'] || CLIENT_ID
197
+ end
198
+
199
+ # OAuth2 client_secret from ENV KONTENA_CLIENT_SECRET or client CLIENT_SECRET constant
200
+ #
201
+ # @return [String]
202
+ def client_secret
203
+ ENV['KONTENA_CLIENT_SECRET'] || CLIENT_SECRET
204
+ end
205
+
47
206
  # Get request
48
207
  #
49
208
  # @param [String] path
50
- # @param [Lambda] response_block
51
209
  # @param [Hash,NilClass] params
52
210
  # @param [Hash] headers
53
- def get_stream(path, response_block, params = nil, headers = {})
54
- http_client.get(
55
- read_timeout: 360,
56
- path: request_uri(path),
57
- query: params,
58
- headers: request_headers(headers),
59
- response_block: response_block
60
- )
211
+ # @return [Hash]
212
+ def get(path, params = nil, headers = {}, auth = true)
213
+ request(path: path, query: params, headers: headers, auth: auth)
61
214
  end
62
215
 
63
216
  # Post request
@@ -67,21 +220,8 @@ module Kontena
67
220
  # @param [Hash] params
68
221
  # @param [Hash] headers
69
222
  # @return [Hash]
70
- def post(path, obj, params = {}, headers = {})
71
- request_headers = request_headers(headers)
72
- request_options = {
73
- path: request_uri(path),
74
- headers: request_headers,
75
- body: encode_body(obj, request_headers['Content-Type']),
76
- query: params
77
- }
78
-
79
- response = http_client.post(request_options)
80
- if [200, 201].include?(response.status)
81
- parse_response(response)
82
- else
83
- handle_error_response(response)
84
- end
223
+ def post(path, obj, params = {}, headers = {}, auth = true)
224
+ request(http_method: :post, path: path, body: obj, query: params, headers: headers, auth: auth)
85
225
  end
86
226
 
87
227
  # Put request
@@ -91,21 +231,19 @@ module Kontena
91
231
  # @param [Hash] params
92
232
  # @param [Hash] headers
93
233
  # @return [Hash]
94
- def put(path, obj, params = {}, headers = {})
95
- request_headers = request_headers(headers)
96
- request_options = {
97
- path: request_uri(path),
98
- headers: request_headers,
99
- body: encode_body(obj, request_headers['Content-Type']),
100
- query: params
101
- }
234
+ def put(path, obj, params = {}, headers = {}, auth = true)
235
+ request(http_method: :put, path: path, body: obj, query: params, headers: headers, auth: auth)
236
+ end
102
237
 
103
- response = http_client.put(request_options)
104
- if [200, 201].include?(response.status)
105
- parse_response(response)
106
- else
107
- handle_error_response(response)
108
- end
238
+ # Patch request
239
+ #
240
+ # @param [String] path
241
+ # @param [Object] obj
242
+ # @param [Hash] params
243
+ # @param [Hash] headers
244
+ # @return [Hash]
245
+ def patch(path, obj, params = {}, headers = {}, auth = true)
246
+ request(http_method: :patch, path: path, body: obj, query: params, headers: headers, auth: auth)
109
247
  end
110
248
 
111
249
  # Delete request
@@ -115,25 +253,188 @@ module Kontena
115
253
  # @param [Hash] params
116
254
  # @param [Hash] headers
117
255
  # @return [Hash]
118
- def delete(path, body = nil, params = {}, headers = {})
119
- request_headers = request_headers(headers)
256
+ def delete(path, body = nil, params = {}, headers = {}, auth = true)
257
+ request(http_method: :delete, path: path, body: body, query: params, headers: headers, auth: auth)
258
+ end
259
+
260
+ # Get stream request
261
+ #
262
+ # @param [String] path
263
+ # @param [Lambda] response_block
264
+ # @param [Hash,NilClass] params
265
+ # @param [Hash] headers
266
+ def get_stream(path, response_block, params = nil, headers = {}, auth = true)
267
+ request(path: path, query: params, headers: headers, response_block: response_block, auth: auth)
268
+ end
269
+
270
+ def token_expired?
271
+ return false unless token
272
+ if token.respond_to?(:expired?)
273
+ token.expired?
274
+ elsif token['expires_at'].to_i > 0
275
+ token['expires_at'].to_i < Time.now.utc.to_i
276
+ else
277
+ false
278
+ end
279
+ end
280
+
281
+ # Perform a HTTP request. Will try to refresh the access token and retry if it's
282
+ # expired or if the server responds with HTTP 401.
283
+ #
284
+ # Automatically parses a JSON response into a hash.
285
+ #
286
+ # After the request has been performed, the response can be inspected using
287
+ # client.last_response.
288
+ #
289
+ # @param http_method [Symbol] :get, :post, etc
290
+ # @param path [String] if it starts with / then prefix won't be used.
291
+ # @param body [Hash, String] will be encoded using #encode_body
292
+ # @param query [Hash] url query parameters
293
+ # @param headers [Hash] extra headers for request.
294
+ # @param response_block [Proc] for streaming requests, must respond to #call
295
+ # @param expects [Array] raises unless response status code matches this list.
296
+ # @param auth [Boolean] use token authentication default = true
297
+ # @return [Hash, String] response parsed response object
298
+ def request(http_method: :get, path:'/', body: nil, query: {}, headers: {}, response_block: nil, expects: [200, 201], host: nil, port: nil, auth: true)
299
+
300
+ retried ||= false
301
+
302
+ if auth && token_expired?
303
+ raise Excon::Errors::Unauthorized, "Token expired or not valid, you need to login again, use: kontena #{token_is_for_master? ? "master auth" : "auth"}"
304
+ end
305
+
306
+ request_headers = request_headers(headers, auth)
307
+
308
+ body_content = body.nil? ? '' : encode_body(body, request_headers[CONTENT_TYPE])
309
+
310
+ request_headers.merge!('Content-Length' => body_content.bytesize)
311
+
312
+ host_options = {}
313
+ host_options[:host] = host if host
314
+ host_options[:port] = port if port
315
+
120
316
  request_options = {
121
- path: request_uri(path),
317
+ method: http_method,
318
+ expects: Array(expects),
319
+ path: path.start_with?('/') ? path : request_uri(path),
122
320
  headers: request_headers,
123
- body: encode_body(body, request_headers['Content-Type']),
124
- query: params
321
+ body: body_content,
322
+ query: query
323
+ }.merge(host_options)
324
+
325
+ request_options.merge!(response_block: response_block) if response_block
326
+
327
+ # Store the response into client.last_response
328
+ @last_response = http_client.request(request_options)
329
+
330
+ parse_response
331
+ rescue Excon::Errors::Unauthorized
332
+ if token && token_is_for_master?
333
+ logger.debug 'Server reports access token expired'
334
+
335
+ if retried || !token || !token['refresh_token']
336
+ raise Kontena::Errors::StandardError.new(401, 'The access token has expired and needs to be refreshed')
337
+ end
338
+
339
+ retried = true
340
+ retry if refresh_token
341
+ end
342
+ raise Kontena::Errors::StandardError.new(401, 'Unauthorized')
343
+ rescue Excon::Errors::NotFound
344
+ raise Kontena::Errors::StandardError.new(404, 'Not found')
345
+ rescue Excon::Errors::Forbidden
346
+ raise Kontena::Errors::StandardError.new(403, 'Access denied')
347
+ rescue
348
+ logger.debug "Request exception: #{$!} - #{$!.message}\n#{$!.backtrace.join("\n")}"
349
+ handle_error_response
350
+ end
351
+
352
+ # Build a token refresh request param hash
353
+ #
354
+ # @return [Hash]
355
+ def refresh_request_params
356
+ {
357
+ refresh_token: token['refresh_token'],
358
+ grant_type: 'refresh_token',
359
+ client_id: client_id,
360
+ client_secret: client_secret
125
361
  }
126
- response = http_client.delete(request_options)
127
- if response.status == 200
128
- parse_response(response)
362
+ end
363
+
364
+ # Accessor to token's account settings
365
+ def token_account
366
+ return {} unless token
367
+ if token.respond_to?(:account)
368
+ token.account
369
+ elsif token.kind_of?(Hash) && token['account'].kind_of?(String)
370
+ config.find_account(token['account'])
129
371
  else
130
- handle_error_response(response)
372
+ {}
131
373
  end
374
+ rescue
375
+ logger.debug "Access token refresh exception: #{$!} - #{$!.message} #{$!.backtrace}"
376
+ false
377
+ end
378
+
379
+ # Perform refresh token request to auth provider.
380
+ # Updates the client's Token object and writes changes to
381
+ # configuration.
382
+ #
383
+ # @param [Boolean] use_basic_auth? When true, use basic auth authentication header
384
+ # @return [Boolean] success?
385
+ def refresh_token
386
+ logger.debug "Performing token refresh"
387
+ return false if token.nil?
388
+ return false if token['refresh_token'].nil?
389
+ uri = URI.parse(token_account['token_endpoint'])
390
+ endpoint_data = { path: uri.path }
391
+ endpoint_data[:host] = uri.host if uri.host
392
+ endpoint_data[:port] = uri.port if uri.port
393
+
394
+ logger.debug "Token refresh endpoint: #{endpoint_data.inspect}"
395
+
396
+ return false unless endpoint_data[:path]
397
+
398
+ response = request(
399
+ {
400
+ http_method: token_account['token_method'].downcase.to_sym,
401
+ body: refresh_request_params,
402
+ headers: {
403
+ CONTENT_TYPE => token_account['token_post_content_type']
404
+ }.merge(
405
+ token_account['code_requires_basic_auth'] ? basic_auth_header : {}
406
+ ),
407
+ expects: [200, 201, 400, 401, 403],
408
+ auth: false
409
+ }.merge(endpoint_data)
410
+ )
411
+
412
+ if response && response['access_token']
413
+ logger.debug "Got response to refresh request"
414
+ token['access_token'] = response['access_token']
415
+ token['refresh_token'] = response['refresh_token']
416
+ token['expires_at'] = in_to_at(response['expires_in'])
417
+ token.config.write if token.respond_to?(:config)
418
+ true
419
+ else
420
+ logger.debug "Got null or bad response to refresh request: #{last_response.inspect}"
421
+ false
422
+ end
423
+ rescue
424
+ logger.debug "Access token refresh exception: #{$!} - #{$!.message} #{$!.backtrace}"
425
+ false
132
426
  end
133
427
 
134
428
  private
135
429
 
136
- ##
430
+ # Returns true if the token object belongs to a master
431
+ #
432
+ # @return [Boolean]
433
+ def token_is_for_master?
434
+ token_account['name'] == 'master'
435
+ end
436
+
437
+
137
438
  # Get full request uri
138
439
  #
139
440
  # @param [String] path
@@ -142,51 +443,61 @@ module Kontena
142
443
  "#{path_prefix}#{path}"
143
444
  end
144
445
 
446
+
145
447
  ##
146
- # Get request headers
448
+ # Build request headers. Removes empty headers.
449
+ # @example
450
+ # request_headers('Authorization' => nil)
147
451
  #
148
452
  # @param [Hash] headers
149
453
  # @return [Hash]
150
- def request_headers(headers = {})
151
- @default_headers.merge(headers)
454
+ def request_headers(headers = {}, auth = true)
455
+ headers = default_headers.merge(headers)
456
+ headers.merge!(bearer_authorization_header) if auth
457
+ headers.reject{|_,v| v.nil? || (v.respond_to?(:empty?) && v.empty?)}
152
458
  end
153
459
 
154
460
  ##
155
- # Encode body based on content type
461
+ # Encode body based on content type.
156
462
  #
157
463
  # @param [Object] body
158
464
  # @param [String] content_type
465
+ # @return [String] encoded_content
159
466
  def encode_body(body, content_type)
160
- if content_type == 'application/json'
467
+ if content_type =~ JSON_REGEX # vnd.api+json should pass as json
161
468
  dump_json(body)
469
+ elsif content_type == CONTENT_URLENCODED && body.kind_of?(Hash)
470
+ URI.encode_www_form(body)
162
471
  else
163
472
  body
164
473
  end
165
474
  end
166
475
 
167
476
  ##
168
- # Parse response
477
+ # Parse response. If the respons is JSON, returns a Hash representation.
478
+ # Otherwise returns the raw body.
169
479
  #
170
480
  # @param [HTTP::Message]
171
- # @return [Object]
172
- def parse_response(response)
173
- if response.headers['Content-Type'].include?('application/json')
174
- parse_json(response.body)
481
+ # @return [Hash,String]
482
+ def parse_response
483
+ if last_response.headers[CONTENT_TYPE] =~ JSON_REGEX
484
+ parse_json(last_response.body)
175
485
  else
176
- response.body
486
+ last_response.body
177
487
  end
178
488
  end
179
489
 
180
- ##
181
490
  # Parse json
182
491
  #
183
492
  # @param [String] json
184
493
  # @return [Hash,Object,NilClass]
185
494
  def parse_json(json)
186
- JSON.parse(json) rescue nil
495
+ JSON.parse(json)
496
+ rescue
497
+ logger.debug "JSON parse exception: #{$!} : #{$!.message}"
498
+ nil
187
499
  end
188
500
 
189
- ##
190
501
  # Dump json
191
502
  #
192
503
  # @param [Object] obj
@@ -197,16 +508,25 @@ module Kontena
197
508
 
198
509
  # @return [Boolean]
199
510
  def ignore_ssl_errors?
200
- ENV['SSL_IGNORE_ERRORS'] == 'true'
511
+ ENV['SSL_IGNORE_ERRORS'] == 'true' || options[:ignore_ssl_errors]
201
512
  end
202
513
 
203
514
  # @param [Excon::Response] response
204
- def handle_error_response(response)
205
- message = response.body
206
- if response.status == 404 && message == ''
207
- message = 'Not found'
515
+ def handle_error_response
516
+ raise $!, $!.message unless last_response
517
+ raise Kontena::Errors::StandardError.new(last_response.status, last_response.body)
518
+ end
519
+
520
+ # Convert expires_in into expires_at
521
+ #
522
+ # @param [Fixnum] seconds_till_expiration
523
+ # @return [Fixnum] expires_at_unix_timestamp
524
+ def in_to_at(expires_in)
525
+ if expires_in.to_i < 1
526
+ 0
527
+ else
528
+ Time.now.utc.to_i + expires_in.to_i
208
529
  end
209
- raise Kontena::Errors::StandardError.new(response.status, message)
210
530
  end
211
531
  end
212
532
  end