nucleus 0.2.0 → 0.3.1

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +9 -0
  4. data/README.md +43 -72
  5. data/lib/nucleus/adapter_resolver.rb +3 -3
  6. data/lib/nucleus/adapters/base_adapter.rb +109 -109
  7. data/lib/nucleus/adapters/v1/cloud_foundry_v2/application.rb +111 -111
  8. data/lib/nucleus/adapters/v1/cloud_foundry_v2/cloud_foundry_v2.rb +141 -141
  9. data/lib/nucleus/adapters/v1/cloud_foundry_v2/data.rb +97 -97
  10. data/lib/nucleus/adapters/v1/cloud_foundry_v2/domains.rb +5 -5
  11. data/lib/nucleus/adapters/v1/cloud_foundry_v2/lifecycle.rb +41 -41
  12. data/lib/nucleus/adapters/v1/cloud_foundry_v2/logs.rb +6 -6
  13. data/lib/nucleus/adapters/v1/cloud_foundry_v2/regions.rb +33 -33
  14. data/lib/nucleus/adapters/v1/cloud_foundry_v2/services.rb +6 -6
  15. data/lib/nucleus/adapters/v1/cloud_foundry_v2/vars.rb +80 -80
  16. data/lib/nucleus/adapters/v1/heroku/app_states.rb +57 -57
  17. data/lib/nucleus/adapters/v1/heroku/data.rb +78 -78
  18. data/lib/nucleus/adapters/v1/heroku/heroku.rb +146 -146
  19. data/lib/nucleus/adapters/v1/heroku/lifecycle.rb +51 -51
  20. data/lib/nucleus/adapters/v1/heroku/logs.rb +2 -2
  21. data/lib/nucleus/adapters/v1/heroku/regions.rb +42 -42
  22. data/lib/nucleus/adapters/v1/heroku/services.rb +168 -168
  23. data/lib/nucleus/adapters/v1/heroku/vars.rb +65 -65
  24. data/lib/nucleus/adapters/v1/openshift_v2/app_states.rb +68 -68
  25. data/lib/nucleus/adapters/v1/openshift_v2/application.rb +1 -1
  26. data/lib/nucleus/adapters/v1/openshift_v2/data.rb +96 -96
  27. data/lib/nucleus/adapters/v1/openshift_v2/lifecycle.rb +60 -60
  28. data/lib/nucleus/adapters/v1/openshift_v2/logs.rb +106 -106
  29. data/lib/nucleus/adapters/v1/openshift_v2/openshift_v2.rb +125 -125
  30. data/lib/nucleus/adapters/v1/openshift_v2/regions.rb +58 -58
  31. data/lib/nucleus/adapters/v1/openshift_v2/services.rb +173 -173
  32. data/lib/nucleus/adapters/v1/openshift_v2/vars.rb +49 -49
  33. data/lib/nucleus/adapters/v1/stub_adapter.rb +464 -464
  34. data/lib/nucleus/core/adapter_extensions/auth/auth_client.rb +44 -44
  35. data/lib/nucleus/core/adapter_extensions/auth/expiring_token_auth_client.rb +53 -53
  36. data/lib/nucleus/core/adapter_extensions/auth/http_basic_auth_client.rb +3 -3
  37. data/lib/nucleus/core/adapter_extensions/auth/o_auth2_auth_client.rb +95 -95
  38. data/lib/nucleus/core/adapter_extensions/auth/token_auth_client.rb +36 -36
  39. data/lib/nucleus/core/adapter_extensions/http_client.rb +5 -5
  40. data/lib/nucleus/core/common/files/archive_extractor.rb +1 -1
  41. data/lib/nucleus/core/common/files/archiver.rb +2 -2
  42. data/lib/nucleus/core/file_handling/file_manager.rb +64 -64
  43. data/lib/nucleus/core/file_handling/git_deployer.rb +133 -133
  44. data/lib/nucleus/core/import/adapter_configuration.rb +53 -53
  45. data/lib/nucleus/scripts/initialize_config_defaults.rb +26 -26
  46. data/lib/nucleus/version.rb +1 -1
  47. data/nucleus.gemspec +2 -2
  48. data/spec/integration/api/auth_spec.rb +3 -3
  49. data/spec/spec_helper.rb +98 -98
  50. data/spec/test_suites.rake +1 -1
  51. data/spec/unit/adapters/git_deployer_spec.rb +262 -262
  52. data/spec/unit/common/helpers/auth_helper_spec.rb +1 -1
  53. data/tasks/evaluation.rake +1 -1
  54. data/wiki/adapter_tests.md +0 -7
  55. data/wiki/implement_new_adapter.md +1 -1
  56. metadata +4 -20
  57. data/config/adapters/cloud_control.yml +0 -32
  58. data/lib/nucleus/adapters/v1/cloud_control/application.rb +0 -108
  59. data/lib/nucleus/adapters/v1/cloud_control/authentication.rb +0 -27
  60. data/lib/nucleus/adapters/v1/cloud_control/buildpacks.rb +0 -23
  61. data/lib/nucleus/adapters/v1/cloud_control/cloud_control.rb +0 -153
  62. data/lib/nucleus/adapters/v1/cloud_control/data.rb +0 -76
  63. data/lib/nucleus/adapters/v1/cloud_control/domains.rb +0 -68
  64. data/lib/nucleus/adapters/v1/cloud_control/lifecycle.rb +0 -27
  65. data/lib/nucleus/adapters/v1/cloud_control/log_poller.rb +0 -71
  66. data/lib/nucleus/adapters/v1/cloud_control/logs.rb +0 -103
  67. data/lib/nucleus/adapters/v1/cloud_control/regions.rb +0 -32
  68. data/lib/nucleus/adapters/v1/cloud_control/scaling.rb +0 -17
  69. data/lib/nucleus/adapters/v1/cloud_control/semantic_errors.rb +0 -31
  70. data/lib/nucleus/adapters/v1/cloud_control/services.rb +0 -162
  71. data/lib/nucleus/adapters/v1/cloud_control/token.rb +0 -17
  72. data/lib/nucleus/adapters/v1/cloud_control/vars.rb +0 -88
@@ -1,57 +1,57 @@
1
- module Nucleus
2
- module Adapters
3
- module V1
4
- class Heroku < Stub
5
- # AppStates for Heroku, or the logic to determine the current application state
6
- module AppStates
7
- private
8
-
9
- def application_state(app, retrieved_dynos = nil)
10
- # 1: created, both repo and slug are nil
11
- return Enums::ApplicationStates::CREATED unless repo_or_slug_content?(app)
12
-
13
- # all subsequent states require dynos to be determined
14
- dynos = retrieved_dynos ? retrieved_dynos : dynos(app[:id])
15
-
16
- # 2: deployed if no dynos assigned
17
- return Enums::ApplicationStates::DEPLOYED if dynos.empty?
18
-
19
- # 3: stopped if maintenance
20
- return Enums::ApplicationStates::STOPPED if app[:maintenance] || dynos_not_running?(dynos)
21
-
22
- # 4: running if no maintenance (checked above) and at least ony dyno is up
23
- return Enums::ApplicationStates::RUNNING if dyno_states(dynos).include?('up')
24
-
25
- # 5: idle if all dynos are idling
26
- return Enums::ApplicationStates::IDLE if dynos_idle?(dynos)
27
-
28
- # arriving here the above states do not catch all states of the Heroku app, which should not happen ;-)
29
- log.debug("Faild to determine state for: #{app}, #{dynos}")
30
- fail Errors::UnknownAdapterCallError, 'Could not determine app state. Please verify the Heroku adapter'
31
- end
32
-
33
- def repo_or_slug_content?(app)
34
- return true if !app[:repo_size].nil? && app[:repo_size].to_i > 0
35
- return true if !app[:slug_size].nil? && app[:slug_size].to_i > 0
36
- false
37
- end
38
-
39
- def dyno_states(dynos)
40
- dynos.collect { |dyno| dyno[:state] }.compact.uniq
41
- end
42
-
43
- def dynos_idle?(dynos)
44
- dyno_states = dyno_states(dynos)
45
- dyno_states.length == 1 && dyno_states[0] == 'idle'
46
- end
47
-
48
- def dynos_not_running?(dynos)
49
- dynos.empty? || dyno_states(dynos).reject do |state|
50
- %w(crashed down starting).include?(state)
51
- end.empty?
52
- end
53
- end
54
- end
55
- end
56
- end
57
- end
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ # AppStates for Heroku, or the logic to determine the current application state
6
+ module AppStates
7
+ private
8
+
9
+ def application_state(app, retrieved_dynos = nil)
10
+ # 1: created, both repo and slug are nil
11
+ return Enums::ApplicationStates::CREATED unless repo_or_slug_content?(app)
12
+
13
+ # all subsequent states require dynos to be determined
14
+ dynos = retrieved_dynos ? retrieved_dynos : dynos(app[:id])
15
+
16
+ # 2: deployed if no dynos assigned
17
+ return Enums::ApplicationStates::DEPLOYED if dynos.empty?
18
+
19
+ # 3: stopped if maintenance
20
+ return Enums::ApplicationStates::STOPPED if app[:maintenance] || dynos_not_running?(dynos)
21
+
22
+ # 4: running if no maintenance (checked above) and at least ony dyno is up
23
+ return Enums::ApplicationStates::RUNNING if dyno_states(dynos).include?('up')
24
+
25
+ # 5: idle if all dynos are idling
26
+ return Enums::ApplicationStates::IDLE if dynos_idle?(dynos)
27
+
28
+ # arriving here the above states do not catch all states of the Heroku app, which should not happen ;-)
29
+ log.debug("Faild to determine state for: #{app}, #{dynos}")
30
+ raise Errors::UnknownAdapterCallError, 'Could not determine app state. Please verify the Heroku adapter'
31
+ end
32
+
33
+ def repo_or_slug_content?(app)
34
+ return true if !app[:repo_size].nil? && app[:repo_size].to_i > 0
35
+ return true if !app[:slug_size].nil? && app[:slug_size].to_i > 0
36
+ false
37
+ end
38
+
39
+ def dyno_states(dynos)
40
+ dynos.collect { |dyno| dyno[:state] }.compact.uniq
41
+ end
42
+
43
+ def dynos_idle?(dynos)
44
+ dyno_states = dyno_states(dynos)
45
+ dyno_states.length == 1 && dyno_states[0] == 'idle'
46
+ end
47
+
48
+ def dynos_not_running?(dynos)
49
+ dynos.empty? || dyno_states(dynos).reject do |state|
50
+ %w(crashed down starting).include?(state)
51
+ end.empty?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,78 +1,78 @@
1
- module Nucleus
2
- module Adapters
3
- module V1
4
- class Heroku < Stub
5
- module Data
6
- # @see Stub#deploy
7
- def deploy(application_id, file, file_compression_format)
8
- app = get("/apps/#{application_id}").body
9
- account = get('/account').body
10
- repo_name = "nucleus.app.repo.heroku.deploy.#{application_id}.#{SecureRandom.uuid}"
11
- # clone, extract, push and finally delete cloned repository (sync)
12
- with_ssh_key do
13
- GitDeployer.new(repo_name, app[:git_url], account[:email]).deploy(file, file_compression_format)
14
- end
15
-
16
- return unless application_state(app) == Enums::ApplicationStates::CREATED
17
- # instantly remove all initially added dynos to keep the 'deployed' state on first deployment
18
- log.debug 'state before deployment was \'created\', scale web to 0'
19
- scale_web(application_id, 0)
20
- end
21
-
22
- # @see Stub#download
23
- def download(application_id, compression_format)
24
- # Only possible with git, not with HTTP builds
25
- app = get("/apps/#{application_id}").body
26
- if application_state(app) == Enums::ApplicationStates::CREATED
27
- fail Errors::SemanticAdapterRequestError, 'Application must be deployed before data can be downloaded'
28
- end
29
- # compress files to archive but exclude the .git repo
30
- repo_name = "nucleus.app.repo.heroku.download.#{application_id}.#{SecureRandom.uuid}"
31
- with_ssh_key do
32
- GitDeployer.new(repo_name, app[:git_url], nil).download(compression_format, true)
33
- end
34
- end
35
-
36
- # @see Stub#rebuild
37
- def rebuild(application_id)
38
- app = get("/apps/#{application_id}").body
39
- if application_state(app) == Enums::ApplicationStates::CREATED
40
- fail Errors::SemanticAdapterRequestError, 'Application must be deployed before data can be rebuild'
41
- end
42
-
43
- account = get('/account').body
44
- repo_name = "nucleus.app.repo.heroku.rebuild.#{application_id}.#{SecureRandom.uuid}"
45
-
46
- with_ssh_key do
47
- GitDeployer.new(repo_name, app[:git_url], account[:email]).trigger_build
48
- end
49
-
50
- # return with updated application
51
- application(application_id)
52
- end
53
-
54
- private
55
-
56
- def with_ssh_key
57
- # load ssh key into cloud control
58
- matches = nucleus_config.ssh.handler.public_key.match(/(.*)\s{1}(.*)\s{1}(.*)/)
59
- key_id = register_key(matches[1], matches[2])
60
- return yield
61
- ensure
62
- # unload ssh key, allow 404 if the key couldn't be registered at first
63
- delete("/account/keys/#{key_id}") if key_id
64
- end
65
-
66
- def register_key(type, key)
67
- # skip if the key is already registered
68
- installed_keys = get('/account/keys').body
69
- return nil if installed_keys.any? { |installed_key| installed_key[:public_key].include?(key) }
70
-
71
- key_name = "nucleus-#{SecureRandom.uuid}"
72
- post('/account/keys', body: { public_key: [type, key, key_name].join(' ') }).body[:id]
73
- end
74
- end
75
- end
76
- end
77
- end
78
- end
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Data
6
+ # @see Stub#deploy
7
+ def deploy(application_id, file, file_compression_format)
8
+ app = get("/apps/#{application_id}").body
9
+ account = get('/account').body
10
+ repo_name = "nucleus.app.repo.heroku.deploy.#{application_id}.#{SecureRandom.uuid}"
11
+ # clone, extract, push and finally delete cloned repository (sync)
12
+ with_ssh_key do
13
+ GitDeployer.new(repo_name, app[:git_url], account[:email]).deploy(file, file_compression_format)
14
+ end
15
+
16
+ return unless application_state(app) == Enums::ApplicationStates::CREATED
17
+ # instantly remove all initially added dynos to keep the 'deployed' state on first deployment
18
+ log.debug 'state before deployment was \'created\', scale web to 0'
19
+ scale_web(application_id, 0)
20
+ end
21
+
22
+ # @see Stub#download
23
+ def download(application_id, compression_format)
24
+ # Only possible with git, not with HTTP builds
25
+ app = get("/apps/#{application_id}").body
26
+ if application_state(app) == Enums::ApplicationStates::CREATED
27
+ raise Errors::SemanticAdapterRequestError, 'Application must be deployed before data can be downloaded'
28
+ end
29
+ # compress files to archive but exclude the .git repo
30
+ repo_name = "nucleus.app.repo.heroku.download.#{application_id}.#{SecureRandom.uuid}"
31
+ with_ssh_key do
32
+ GitDeployer.new(repo_name, app[:git_url], nil).download(compression_format, true)
33
+ end
34
+ end
35
+
36
+ # @see Stub#rebuild
37
+ def rebuild(application_id)
38
+ app = get("/apps/#{application_id}").body
39
+ if application_state(app) == Enums::ApplicationStates::CREATED
40
+ raise Errors::SemanticAdapterRequestError, 'Application must be deployed before data can be rebuild'
41
+ end
42
+
43
+ account = get('/account').body
44
+ repo_name = "nucleus.app.repo.heroku.rebuild.#{application_id}.#{SecureRandom.uuid}"
45
+
46
+ with_ssh_key do
47
+ GitDeployer.new(repo_name, app[:git_url], account[:email]).trigger_build
48
+ end
49
+
50
+ # return with updated application
51
+ application(application_id)
52
+ end
53
+
54
+ private
55
+
56
+ def with_ssh_key
57
+ # load ssh key into cloud control
58
+ matches = nucleus_config.ssh.handler.public_key.match(/(.*)\s{1}(.*)\s{1}(.*)/)
59
+ key_id = register_key(matches[1], matches[2])
60
+ return yield
61
+ ensure
62
+ # unload ssh key, allow 404 if the key couldn't be registered at first
63
+ delete("/account/keys/#{key_id}") if key_id
64
+ end
65
+
66
+ def register_key(type, key)
67
+ # skip if the key is already registered
68
+ installed_keys = get('/account/keys').body
69
+ return nil if installed_keys.any? { |installed_key| installed_key[:public_key].include?(key) }
70
+
71
+ key_name = "nucleus-#{SecureRandom.uuid}"
72
+ post('/account/keys', body: { public_key: [type, key, key_name].join(' ') }).body[:id]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,146 +1,146 @@
1
- module Nucleus
2
- module Adapters
3
- module V1
4
- # The {Heroku} adapter is designed to support the Heroku platform API.<br>
5
- # <br>
6
- # The Nucleus API is fully supported, there are no known issues.
7
- # @see https://devcenter.heroku.com/articles/platform-api-reference Heroku Platform API
8
- class Heroku < Stub
9
- include Nucleus::Logging
10
- include Nucleus::Adapters::V1::Heroku::Authentication
11
- include Nucleus::Adapters::V1::Heroku::Application
12
- include Nucleus::Adapters::V1::Heroku::AppStates
13
- include Nucleus::Adapters::V1::Heroku::Buildpacks
14
- include Nucleus::Adapters::V1::Heroku::Data
15
- include Nucleus::Adapters::V1::Heroku::Domains
16
- include Nucleus::Adapters::V1::Heroku::Logs
17
- include Nucleus::Adapters::V1::Heroku::Lifecycle
18
- include Nucleus::Adapters::V1::Heroku::Regions
19
- include Nucleus::Adapters::V1::Heroku::Scaling
20
- include Nucleus::Adapters::V1::Heroku::Services
21
- include Nucleus::Adapters::V1::Heroku::SemanticErrors
22
- include Nucleus::Adapters::V1::Heroku::Vars
23
-
24
- def initialize(endpoint_url, endpoint_app_domain = nil, check_certificates = true)
25
- super(endpoint_url, endpoint_app_domain, check_certificates)
26
- end
27
-
28
- def handle_error(error_response)
29
- handle_422(error_response)
30
- if error_response.status == 404 && error_response.body[:id] == 'not_found'
31
- fail Errors::AdapterResourceNotFoundError, error_response.body[:message]
32
- elsif error_response.status == 503
33
- fail Errors::PlatformUnavailableError, 'The Heroku API is currently not responding'
34
- end
35
- # error still unhandled, will result in a 500, server error
36
- log.warn "Heroku error still unhandled: #{error_response}"
37
- end
38
-
39
- def handle_422(error_response)
40
- return unless error_response.status == 422
41
- if error_response.body[:id] == 'invalid_params'
42
- fail Errors::SemanticAdapterRequestError, error_response.body[:message]
43
- elsif error_response.body[:id] == 'verification_required'
44
- fail_with(:need_verification, [error_response.body[:message]])
45
- end
46
- end
47
-
48
- private
49
-
50
- def install_runtimes(application_id, runtimes)
51
- runtime_instructions = runtimes.collect { |buildpack_url| { buildpack: buildpack_url } }
52
- log.debug "Install runtimes: #{runtime_instructions}"
53
- buildpack_instructions = { updates: runtime_instructions }
54
- put("/apps/#{application_id}/buildpack-installations", body: buildpack_instructions)
55
- end
56
-
57
- def runtimes_to_install(application)
58
- return [] unless application[:runtimes]
59
- runtimes_to_install = []
60
- application[:runtimes].each do |runtime_identifier|
61
- # we do not need to install native buildpacks
62
- # TODO: 2 options for heroku runtime handling
63
- # a) skip native, fails when native required and not in list
64
- # b) (current) use native, fails when others (additional) are in the list
65
- # next if native_runtime?(runtime_identifier)
66
- runtime_is_url = runtime_identifier =~ /\A#{URI.regexp}\z/
67
- runtime_url = find_runtime(runtime_identifier)
68
- runtime_is_valid = runtime_url || runtime_is_url
69
- fail_with(:invalid_runtime, [runtime_identifier]) unless runtime_is_valid
70
- # if runtime identifier is valid, we need to install the runtime
71
- runtimes_to_install.push(runtime_is_url ? runtime_identifier : runtime_url)
72
- end
73
- # heroku does not know the 'runtimes' property and would crash if present
74
- application.delete :runtimes
75
- runtimes_to_install
76
- end
77
-
78
- def heroku_api
79
- ::Heroku::API.new(headers: headers)
80
- end
81
-
82
- def headers
83
- super.merge(
84
- 'Accept' => 'application/vnd.heroku+json; version=3',
85
- 'Content-Type' => 'application/json'
86
- )
87
- end
88
-
89
- def installed_buildpacks(application_id)
90
- buildpacks = get("/apps/#{application_id}/buildpack-installations").body
91
- return [] if buildpacks.empty?
92
- buildpacks.collect do |buildpack|
93
- buildpack[:buildpack][:url]
94
- end
95
- end
96
-
97
- def application_instances(application_id)
98
- formations = get("/apps/#{application_id}/formation").body
99
- web_formation = formations.find { |formation| formation[:type] == 'web' }
100
- return web_formation[:quantity] unless web_formation.nil?
101
- # if no web formation was detected, there is no instance available
102
- 0
103
- end
104
-
105
- def dynos(application_id)
106
- get("/apps/#{application_id}/dynos").body
107
- end
108
-
109
- def web_dynos(application_id, retrieved_dynos = nil)
110
- all_dynos = retrieved_dynos ? retrieved_dynos : dynos(application_id)
111
- all_dynos.find_all do |dyno|
112
- dyno[:type] == 'web'
113
- end.compact
114
- end
115
-
116
- def latest_release(application_id, retrieved_dynos = nil)
117
- dynos = web_dynos(application_id, retrieved_dynos)
118
- if dynos.nil? || dynos.empty?
119
- log.debug 'no dynos for build detection, fallback to latest release version'
120
- # this approach might be wrong if the app is rolled-back to a previous release
121
- # However, if no dyno is active, this is the only option to identify the current release
122
- latest_version = 0
123
- latest_version_id = nil
124
- get("/apps/#{application_id}/releases").body.each do |release|
125
- if release[:version] > latest_version
126
- latest_version = release[:version]
127
- latest_version_id = release[:id]
128
- end
129
- end
130
- else
131
- latest_version = 0
132
- latest_version_id = nil
133
- dynos.each do |dyno|
134
- if dyno[:release][:version] > latest_version
135
- latest_version = dyno[:release][:version]
136
- latest_version_id = dyno[:release][:id]
137
- end
138
- end
139
- end
140
-
141
- latest_version_id
142
- end
143
- end
144
- end
145
- end
146
- end
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ # The {Heroku} adapter is designed to support the Heroku platform API.<br>
5
+ # <br>
6
+ # The Nucleus API is fully supported, there are no known issues.
7
+ # @see https://devcenter.heroku.com/articles/platform-api-reference Heroku Platform API
8
+ class Heroku < Stub
9
+ include Nucleus::Logging
10
+ include Nucleus::Adapters::V1::Heroku::Authentication
11
+ include Nucleus::Adapters::V1::Heroku::Application
12
+ include Nucleus::Adapters::V1::Heroku::AppStates
13
+ include Nucleus::Adapters::V1::Heroku::Buildpacks
14
+ include Nucleus::Adapters::V1::Heroku::Data
15
+ include Nucleus::Adapters::V1::Heroku::Domains
16
+ include Nucleus::Adapters::V1::Heroku::Logs
17
+ include Nucleus::Adapters::V1::Heroku::Lifecycle
18
+ include Nucleus::Adapters::V1::Heroku::Regions
19
+ include Nucleus::Adapters::V1::Heroku::Scaling
20
+ include Nucleus::Adapters::V1::Heroku::Services
21
+ include Nucleus::Adapters::V1::Heroku::SemanticErrors
22
+ include Nucleus::Adapters::V1::Heroku::Vars
23
+
24
+ def initialize(endpoint_url, endpoint_app_domain = nil, check_certificates = true)
25
+ super(endpoint_url, endpoint_app_domain, check_certificates)
26
+ end
27
+
28
+ def handle_error(error_response)
29
+ handle_422(error_response)
30
+ if error_response.status == 404 && error_response.body[:id] == 'not_found'
31
+ raise Errors::AdapterResourceNotFoundError, error_response.body[:message]
32
+ elsif error_response.status == 503
33
+ raise Errors::PlatformUnavailableError, 'The Heroku API is currently not responding'
34
+ end
35
+ # error still unhandled, will result in a 500, server error
36
+ log.warn "Heroku error still unhandled: #{error_response}"
37
+ end
38
+
39
+ def handle_422(error_response)
40
+ return unless error_response.status == 422
41
+ if error_response.body[:id] == 'invalid_params'
42
+ raise Errors::SemanticAdapterRequestError, error_response.body[:message]
43
+ elsif error_response.body[:id] == 'verification_required'
44
+ fail_with(:need_verification, [error_response.body[:message]])
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def install_runtimes(application_id, runtimes)
51
+ runtime_instructions = runtimes.collect { |buildpack_url| { buildpack: buildpack_url } }
52
+ log.debug "Install runtimes: #{runtime_instructions}"
53
+ buildpack_instructions = { updates: runtime_instructions }
54
+ put("/apps/#{application_id}/buildpack-installations", body: buildpack_instructions)
55
+ end
56
+
57
+ def runtimes_to_install(application)
58
+ return [] unless application[:runtimes]
59
+ runtimes_to_install = []
60
+ application[:runtimes].each do |runtime_identifier|
61
+ # we do not need to install native buildpacks
62
+ # TODO: 2 options for heroku runtime handling
63
+ # a) skip native, fails when native required and not in list
64
+ # b) (current) use native, fails when others (additional) are in the list
65
+ # next if native_runtime?(runtime_identifier)
66
+ runtime_is_url = runtime_identifier =~ /\A#{URI.regexp}\z/
67
+ runtime_url = find_runtime(runtime_identifier)
68
+ runtime_is_valid = runtime_url || runtime_is_url
69
+ fail_with(:invalid_runtime, [runtime_identifier]) unless runtime_is_valid
70
+ # if runtime identifier is valid, we need to install the runtime
71
+ runtimes_to_install.push(runtime_is_url ? runtime_identifier : runtime_url)
72
+ end
73
+ # heroku does not know the 'runtimes' property and would crash if present
74
+ application.delete :runtimes
75
+ runtimes_to_install
76
+ end
77
+
78
+ def heroku_api
79
+ ::Heroku::API.new(headers: headers)
80
+ end
81
+
82
+ def headers
83
+ super.merge(
84
+ 'Accept' => 'application/vnd.heroku+json; version=3',
85
+ 'Content-Type' => 'application/json'
86
+ )
87
+ end
88
+
89
+ def installed_buildpacks(application_id)
90
+ buildpacks = get("/apps/#{application_id}/buildpack-installations").body
91
+ return [] if buildpacks.empty?
92
+ buildpacks.collect do |buildpack|
93
+ buildpack[:buildpack][:url]
94
+ end
95
+ end
96
+
97
+ def application_instances(application_id)
98
+ formations = get("/apps/#{application_id}/formation").body
99
+ web_formation = formations.find { |formation| formation[:type] == 'web' }
100
+ return web_formation[:quantity] unless web_formation.nil?
101
+ # if no web formation was detected, there is no instance available
102
+ 0
103
+ end
104
+
105
+ def dynos(application_id)
106
+ get("/apps/#{application_id}/dynos").body
107
+ end
108
+
109
+ def web_dynos(application_id, retrieved_dynos = nil)
110
+ all_dynos = retrieved_dynos ? retrieved_dynos : dynos(application_id)
111
+ all_dynos.find_all do |dyno|
112
+ dyno[:type] == 'web'
113
+ end.compact
114
+ end
115
+
116
+ def latest_release(application_id, retrieved_dynos = nil)
117
+ dynos = web_dynos(application_id, retrieved_dynos)
118
+ if dynos.nil? || dynos.empty?
119
+ log.debug 'no dynos for build detection, fallback to latest release version'
120
+ # this approach might be wrong if the app is rolled-back to a previous release
121
+ # However, if no dyno is active, this is the only option to identify the current release
122
+ latest_version = 0
123
+ latest_version_id = nil
124
+ get("/apps/#{application_id}/releases").body.each do |release|
125
+ if release[:version] > latest_version
126
+ latest_version = release[:version]
127
+ latest_version_id = release[:id]
128
+ end
129
+ end
130
+ else
131
+ latest_version = 0
132
+ latest_version_id = nil
133
+ dynos.each do |dyno|
134
+ if dyno[:release][:version] > latest_version
135
+ latest_version = dyno[:release][:version]
136
+ latest_version_id = dyno[:release][:id]
137
+ end
138
+ end
139
+ end
140
+
141
+ latest_version_id
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end