nucleus 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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