nucleus 0.1.0

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 (224) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +44 -0
  6. data/.travis.yml +21 -0
  7. data/CHANGELOG.md +19 -0
  8. data/CONTRIBUTING.md +13 -0
  9. data/Gemfile +16 -0
  10. data/Guardfile +22 -0
  11. data/LICENSE +21 -0
  12. data/README.md +675 -0
  13. data/Rakefile +137 -0
  14. data/bin/nucleus +91 -0
  15. data/bin/nucleus.bat +1 -0
  16. data/config.ru +18 -0
  17. data/config/adapters/cloud_control.yml +32 -0
  18. data/config/adapters/cloud_foundry_v2.yml +61 -0
  19. data/config/adapters/heroku.yml +13 -0
  20. data/config/adapters/openshift_v2.yml +20 -0
  21. data/config/nucleus_config.rb +47 -0
  22. data/lib/nucleus.rb +13 -0
  23. data/lib/nucleus/adapter_resolver.rb +115 -0
  24. data/lib/nucleus/adapters/base_adapter.rb +109 -0
  25. data/lib/nucleus/adapters/buildpack_translator.rb +79 -0
  26. data/lib/nucleus/adapters/v1/cloud_control/application.rb +108 -0
  27. data/lib/nucleus/adapters/v1/cloud_control/authentication.rb +27 -0
  28. data/lib/nucleus/adapters/v1/cloud_control/buildpacks.rb +23 -0
  29. data/lib/nucleus/adapters/v1/cloud_control/cloud_control.rb +153 -0
  30. data/lib/nucleus/adapters/v1/cloud_control/data.rb +76 -0
  31. data/lib/nucleus/adapters/v1/cloud_control/domains.rb +68 -0
  32. data/lib/nucleus/adapters/v1/cloud_control/lifecycle.rb +27 -0
  33. data/lib/nucleus/adapters/v1/cloud_control/log_poller.rb +71 -0
  34. data/lib/nucleus/adapters/v1/cloud_control/logs.rb +103 -0
  35. data/lib/nucleus/adapters/v1/cloud_control/regions.rb +32 -0
  36. data/lib/nucleus/adapters/v1/cloud_control/scaling.rb +17 -0
  37. data/lib/nucleus/adapters/v1/cloud_control/semantic_errors.rb +31 -0
  38. data/lib/nucleus/adapters/v1/cloud_control/services.rb +162 -0
  39. data/lib/nucleus/adapters/v1/cloud_control/token.rb +17 -0
  40. data/lib/nucleus/adapters/v1/cloud_control/vars.rb +88 -0
  41. data/lib/nucleus/adapters/v1/cloud_foundry_v2/app_states.rb +28 -0
  42. data/lib/nucleus/adapters/v1/cloud_foundry_v2/application.rb +111 -0
  43. data/lib/nucleus/adapters/v1/cloud_foundry_v2/authentication.rb +17 -0
  44. data/lib/nucleus/adapters/v1/cloud_foundry_v2/buildpacks.rb +23 -0
  45. data/lib/nucleus/adapters/v1/cloud_foundry_v2/cloud_foundry_v2.rb +141 -0
  46. data/lib/nucleus/adapters/v1/cloud_foundry_v2/data.rb +97 -0
  47. data/lib/nucleus/adapters/v1/cloud_foundry_v2/domains.rb +149 -0
  48. data/lib/nucleus/adapters/v1/cloud_foundry_v2/lifecycle.rb +41 -0
  49. data/lib/nucleus/adapters/v1/cloud_foundry_v2/logs.rb +303 -0
  50. data/lib/nucleus/adapters/v1/cloud_foundry_v2/regions.rb +33 -0
  51. data/lib/nucleus/adapters/v1/cloud_foundry_v2/scaling.rb +15 -0
  52. data/lib/nucleus/adapters/v1/cloud_foundry_v2/semantic_errors.rb +27 -0
  53. data/lib/nucleus/adapters/v1/cloud_foundry_v2/services.rb +286 -0
  54. data/lib/nucleus/adapters/v1/cloud_foundry_v2/vars.rb +80 -0
  55. data/lib/nucleus/adapters/v1/heroku/app_states.rb +57 -0
  56. data/lib/nucleus/adapters/v1/heroku/application.rb +93 -0
  57. data/lib/nucleus/adapters/v1/heroku/authentication.rb +27 -0
  58. data/lib/nucleus/adapters/v1/heroku/buildpacks.rb +27 -0
  59. data/lib/nucleus/adapters/v1/heroku/data.rb +78 -0
  60. data/lib/nucleus/adapters/v1/heroku/domains.rb +43 -0
  61. data/lib/nucleus/adapters/v1/heroku/heroku.rb +146 -0
  62. data/lib/nucleus/adapters/v1/heroku/lifecycle.rb +51 -0
  63. data/lib/nucleus/adapters/v1/heroku/logs.rb +108 -0
  64. data/lib/nucleus/adapters/v1/heroku/regions.rb +42 -0
  65. data/lib/nucleus/adapters/v1/heroku/scaling.rb +28 -0
  66. data/lib/nucleus/adapters/v1/heroku/semantic_errors.rb +23 -0
  67. data/lib/nucleus/adapters/v1/heroku/services.rb +168 -0
  68. data/lib/nucleus/adapters/v1/heroku/vars.rb +65 -0
  69. data/lib/nucleus/adapters/v1/openshift_v2/app_states.rb +68 -0
  70. data/lib/nucleus/adapters/v1/openshift_v2/application.rb +108 -0
  71. data/lib/nucleus/adapters/v1/openshift_v2/authentication.rb +21 -0
  72. data/lib/nucleus/adapters/v1/openshift_v2/data.rb +96 -0
  73. data/lib/nucleus/adapters/v1/openshift_v2/domains.rb +37 -0
  74. data/lib/nucleus/adapters/v1/openshift_v2/lifecycle.rb +60 -0
  75. data/lib/nucleus/adapters/v1/openshift_v2/logs.rb +106 -0
  76. data/lib/nucleus/adapters/v1/openshift_v2/openshift_v2.rb +125 -0
  77. data/lib/nucleus/adapters/v1/openshift_v2/regions.rb +58 -0
  78. data/lib/nucleus/adapters/v1/openshift_v2/scaling.rb +39 -0
  79. data/lib/nucleus/adapters/v1/openshift_v2/semantic_errors.rb +40 -0
  80. data/lib/nucleus/adapters/v1/openshift_v2/services.rb +173 -0
  81. data/lib/nucleus/adapters/v1/openshift_v2/vars.rb +49 -0
  82. data/lib/nucleus/adapters/v1/stub_adapter.rb +464 -0
  83. data/lib/nucleus/core/adapter_authentication_inductor.rb +62 -0
  84. data/lib/nucleus/core/adapter_extensions/auth/auth_client.rb +44 -0
  85. data/lib/nucleus/core/adapter_extensions/auth/authentication_retry_wrapper.rb +79 -0
  86. data/lib/nucleus/core/adapter_extensions/auth/expiring_token_auth_client.rb +53 -0
  87. data/lib/nucleus/core/adapter_extensions/auth/http_basic_auth_client.rb +37 -0
  88. data/lib/nucleus/core/adapter_extensions/auth/o_auth2_auth_client.rb +95 -0
  89. data/lib/nucleus/core/adapter_extensions/auth/token_auth_client.rb +36 -0
  90. data/lib/nucleus/core/adapter_extensions/http_client.rb +177 -0
  91. data/lib/nucleus/core/adapter_extensions/http_tail_client.rb +26 -0
  92. data/lib/nucleus/core/adapter_extensions/tail_stopper.rb +25 -0
  93. data/lib/nucleus/core/common/errors/ambiguous_adapter_error.rb +7 -0
  94. data/lib/nucleus/core/common/errors/file_existence_error.rb +7 -0
  95. data/lib/nucleus/core/common/errors/startup_error.rb +12 -0
  96. data/lib/nucleus/core/common/exit_codes.rb +25 -0
  97. data/lib/nucleus/core/common/files/application_repo_sanitizer.rb +52 -0
  98. data/lib/nucleus/core/common/files/archive_extractor.rb +112 -0
  99. data/lib/nucleus/core/common/files/archiver.rb +91 -0
  100. data/lib/nucleus/core/common/link_generator.rb +46 -0
  101. data/lib/nucleus/core/common/logging/logging.rb +52 -0
  102. data/lib/nucleus/core/common/logging/multi_logger.rb +59 -0
  103. data/lib/nucleus/core/common/logging/request_log_formatter.rb +48 -0
  104. data/lib/nucleus/core/common/ssh_handler.rb +108 -0
  105. data/lib/nucleus/core/common/stream_callback.rb +27 -0
  106. data/lib/nucleus/core/common/thread_config_accessor.rb +85 -0
  107. data/lib/nucleus/core/common/url_converter.rb +28 -0
  108. data/lib/nucleus/core/enums/application_states.rb +26 -0
  109. data/lib/nucleus/core/enums/logfile_types.rb +28 -0
  110. data/lib/nucleus/core/error_messages.rb +127 -0
  111. data/lib/nucleus/core/errors/adapter_error.rb +13 -0
  112. data/lib/nucleus/core/errors/adapter_missing_implementation_error.rb +12 -0
  113. data/lib/nucleus/core/errors/adapter_request_error.rb +10 -0
  114. data/lib/nucleus/core/errors/adapter_resource_not_found_error.rb +10 -0
  115. data/lib/nucleus/core/errors/endpoint_authentication_error.rb +10 -0
  116. data/lib/nucleus/core/errors/platform_specific_semantic_error.rb +12 -0
  117. data/lib/nucleus/core/errors/platform_timeout_error.rb +10 -0
  118. data/lib/nucleus/core/errors/platform_unavailable_error.rb +10 -0
  119. data/lib/nucleus/core/errors/semantic_adapter_request_error.rb +19 -0
  120. data/lib/nucleus/core/errors/unknown_adapter_call_error.rb +10 -0
  121. data/lib/nucleus/core/file_handling/archive_converter.rb +29 -0
  122. data/lib/nucleus/core/file_handling/file_manager.rb +64 -0
  123. data/lib/nucleus/core/file_handling/git_deployer.rb +133 -0
  124. data/lib/nucleus/core/file_handling/git_repo_analyzer.rb +23 -0
  125. data/lib/nucleus/core/import/adapter_configuration.rb +53 -0
  126. data/lib/nucleus/core/import/vendor_parser.rb +28 -0
  127. data/lib/nucleus/core/import/version_detector.rb +18 -0
  128. data/lib/nucleus/core/models/abstract_model.rb +29 -0
  129. data/lib/nucleus/core/models/endpoint.rb +30 -0
  130. data/lib/nucleus/core/models/provider.rb +26 -0
  131. data/lib/nucleus/core/models/vendor.rb +22 -0
  132. data/lib/nucleus/ext/kernel.rb +5 -0
  133. data/lib/nucleus/ext/regexp.rb +49 -0
  134. data/lib/nucleus/os.rb +15 -0
  135. data/lib/nucleus/root_dir.rb +13 -0
  136. data/lib/nucleus/scripts/finalize.rb +8 -0
  137. data/lib/nucleus/scripts/initialize.rb +9 -0
  138. data/lib/nucleus/scripts/initialize_config_defaults.rb +26 -0
  139. data/lib/nucleus/scripts/load.rb +17 -0
  140. data/lib/nucleus/scripts/load_dependencies.rb +43 -0
  141. data/lib/nucleus/scripts/setup_config.rb +28 -0
  142. data/lib/nucleus/scripts/shutdown.rb +11 -0
  143. data/lib/nucleus/version.rb +3 -0
  144. data/nucleus.gemspec +88 -0
  145. data/public/robots.txt +2 -0
  146. data/public/swagger-ui/css/reset.css +125 -0
  147. data/public/swagger-ui/css/screen.css +1224 -0
  148. data/public/swagger-ui/images/apple-touch-icon-114x114.png +0 -0
  149. data/public/swagger-ui/images/apple-touch-icon-120x120.png +0 -0
  150. data/public/swagger-ui/images/apple-touch-icon-144x144.png +0 -0
  151. data/public/swagger-ui/images/apple-touch-icon-152x152.png +0 -0
  152. data/public/swagger-ui/images/apple-touch-icon-57x57.png +0 -0
  153. data/public/swagger-ui/images/apple-touch-icon-60x60.png +0 -0
  154. data/public/swagger-ui/images/apple-touch-icon-72x72.png +0 -0
  155. data/public/swagger-ui/images/apple-touch-icon-76x76.png +0 -0
  156. data/public/swagger-ui/images/explorer_icons.png +0 -0
  157. data/public/swagger-ui/images/favicon-128.png +0 -0
  158. data/public/swagger-ui/images/favicon-16x16.png +0 -0
  159. data/public/swagger-ui/images/favicon-196x196.png +0 -0
  160. data/public/swagger-ui/images/favicon-32x32.png +0 -0
  161. data/public/swagger-ui/images/favicon-96x96.png +0 -0
  162. data/public/swagger-ui/images/favicon.ico +0 -0
  163. data/public/swagger-ui/images/logo_small.png +0 -0
  164. data/public/swagger-ui/images/mstile-144x144.png +0 -0
  165. data/public/swagger-ui/images/mstile-150x150.png +0 -0
  166. data/public/swagger-ui/images/mstile-310x150.png +0 -0
  167. data/public/swagger-ui/images/mstile-310x310.png +0 -0
  168. data/public/swagger-ui/images/mstile-70x70.png +0 -0
  169. data/public/swagger-ui/images/pet_store_api.png +0 -0
  170. data/public/swagger-ui/images/throbber.gif +0 -0
  171. data/public/swagger-ui/images/wordnik_api.png +0 -0
  172. data/public/swagger-ui/index.html +107 -0
  173. data/public/swagger-ui/lib/backbone-min.js +38 -0
  174. data/public/swagger-ui/lib/handlebars-1.0.0.js +2278 -0
  175. data/public/swagger-ui/lib/highlight.7.3.pack.js +1 -0
  176. data/public/swagger-ui/lib/jquery-1.8.0.min.js +2 -0
  177. data/public/swagger-ui/lib/jquery.ba-bbq.min.js +18 -0
  178. data/public/swagger-ui/lib/jquery.slideto.min.js +1 -0
  179. data/public/swagger-ui/lib/jquery.wiggle.min.js +8 -0
  180. data/public/swagger-ui/lib/shred.bundle.js +2765 -0
  181. data/public/swagger-ui/lib/shred/content.js +193 -0
  182. data/public/swagger-ui/lib/swagger-oauth.js +211 -0
  183. data/public/swagger-ui/lib/swagger.js +1653 -0
  184. data/public/swagger-ui/lib/underscore-min.js +32 -0
  185. data/public/swagger-ui/o2c.html +15 -0
  186. data/public/swagger-ui/redirect.html +14 -0
  187. data/public/swagger-ui/swagger-ui.js +2324 -0
  188. data/public/swagger-ui/swagger-ui.min.js +1 -0
  189. data/schemas/api.adapter.schema.yml +31 -0
  190. data/schemas/api.requirements.schema.yml +17 -0
  191. data/spec/factories/models.rb +61 -0
  192. data/spec/integration/api/auth_spec.rb +58 -0
  193. data/spec/integration/api/endpoints_spec.rb +167 -0
  194. data/spec/integration/api/errors_spec.rb +47 -0
  195. data/spec/integration/api/providers_spec.rb +157 -0
  196. data/spec/integration/api/swagger_schema_spec.rb +64 -0
  197. data/spec/integration/api/vendors_spec.rb +45 -0
  198. data/spec/integration/integration_spec_helper.rb +27 -0
  199. data/spec/integration/test_data_generator.rb +55 -0
  200. data/spec/nucleus_git_key.pem +51 -0
  201. data/spec/spec_helper.rb +98 -0
  202. data/spec/support/shared_example_request_types.rb +99 -0
  203. data/spec/test_suites.rake +31 -0
  204. data/spec/unit/adapters/archive_converter_spec.rb +25 -0
  205. data/spec/unit/adapters/file_manager_spec.rb +93 -0
  206. data/spec/unit/adapters/git_deployer_spec.rb +262 -0
  207. data/spec/unit/adapters/v1/stub_spec.rb +14 -0
  208. data/spec/unit/common/helpers/auth_helper_spec.rb +73 -0
  209. data/spec/unit/common/oauth2_auth_client_spec.rb +108 -0
  210. data/spec/unit/common/regexp_spec.rb +33 -0
  211. data/spec/unit/common/request_log_formatter_spec.rb +108 -0
  212. data/spec/unit/common/thread_config_accessor_spec.rb +97 -0
  213. data/spec/unit/models/endpoint_spec.rb +83 -0
  214. data/spec/unit/models/provider_spec.rb +102 -0
  215. data/spec/unit/models/vendor_spec.rb +100 -0
  216. data/spec/unit/schemas/adapter_schema_spec.rb +16 -0
  217. data/spec/unit/schemas/adapter_validation_spec.rb +56 -0
  218. data/spec/unit/schemas/requirements_schema_spec.rb +16 -0
  219. data/spec/unit/unit_spec_helper.rb +11 -0
  220. data/tasks/compatibility.rake +113 -0
  221. data/tasks/evaluation.rake +162 -0
  222. data/wiki/adapter_tests.md +99 -0
  223. data/wiki/implement_new_adapter.md +155 -0
  224. metadata +836 -0
@@ -0,0 +1,93 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Application
6
+ # @see Stub#applications
7
+ def applications
8
+ response = get('/apps')
9
+ apps = []
10
+ response.body.each do |application|
11
+ apps << to_nucleus_app(application)
12
+ end
13
+ apps
14
+ end
15
+
16
+ # @see Stub#application
17
+ def application(application_id)
18
+ response = get("/apps/#{application_id}")
19
+ to_nucleus_app response.body
20
+ end
21
+
22
+ # @see Stub#delete_application
23
+ def delete_application(application_id)
24
+ # returns the application, but we do not want any output
25
+ delete("/apps/#{application_id}")
26
+ end
27
+
28
+ # @see Stub#create_application
29
+ def create_application(application)
30
+ # updates the application with a valid region identity
31
+ retrieve_region(application)
32
+
33
+ # Heroku does not support autoscale
34
+ fail_with(:no_autoscale) if application[:autoscaled]
35
+ application.delete :autoscaled
36
+
37
+ # can fail if runtime URLs are invalid or names do not exist on this platform
38
+ runtimes = runtimes_to_install(application)
39
+
40
+ # create the actual application
41
+ created_app = post('/apps', body: application).body
42
+
43
+ # when application has been created, process runtime information if applicable
44
+ unless runtimes.nil? || runtimes.empty?
45
+ begin
46
+ install_runtimes(created_app[:id], runtimes)
47
+ rescue
48
+ # if buildpack fails, make sure app gets deleted (!)
49
+ log.debug 'Runtime installation failed, rollback...'
50
+ delete_application(created_app[:id])
51
+ log.debug '...application successfully rolled back'
52
+ raise Errors::AdapterRequestError,
53
+ 'Heroku requires a buildpack URL when the runtime shall be specified manually.'
54
+ end
55
+ end
56
+ to_nucleus_app(created_app)
57
+ end
58
+
59
+ # TODO: make update transactional in case sub-task (app, buildpacks) fails
60
+ # @see Stub#update_application
61
+ def update_application(application_id, application)
62
+ # start updating the buildpacks
63
+ if application.key? :runtimes
64
+ # can fail if runtime URLs are invalid or names do not exist on this platform
65
+ runtimes = runtimes_to_install(application)
66
+ install_runtimes(application_id, runtimes)
67
+ end
68
+
69
+ # now change the app name and all other direct properties, which are in the body
70
+ response = patch("/apps/#{application_id}", body: application)
71
+ to_nucleus_app(response.body)
72
+ end
73
+
74
+ private
75
+
76
+ def to_nucleus_app(heroku_application)
77
+ # load dynos only once
78
+ dynos = dynos(heroku_application[:id])
79
+ # add missing fields to the application representation
80
+ heroku_application[:autoscaled] = false
81
+ heroku_application[:state] = application_state(heroku_application, dynos)
82
+ heroku_application[:instances] = application_instances(heroku_application[:id])
83
+ heroku_application[:active_runtime] = heroku_application.delete(:buildpack_provided_description)
84
+ heroku_application[:runtimes] = installed_buildpacks(heroku_application[:id])
85
+ heroku_application[:release_version] = latest_release(heroku_application[:id], dynos)
86
+ heroku_application[:region] = heroku_application[:region][:name]
87
+ heroku_application
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ # Authentication functionality to support the Heroku API
6
+ module Authentication
7
+ # @see Stub#auth_client
8
+ def auth_client
9
+ log.debug "Authenticate @ #{@endpoint_url}"
10
+ TokenAuthClient.new @check_certificates do |verify_ssl, username, password|
11
+ response = Excon.post("#{@endpoint_url}/login", query: { username: username, password: password },
12
+ ssl_verify_peer: verify_ssl)
13
+ # Heroku returns 404 for invalid credentials, then we do not return an API token
14
+ if response.status == 404
15
+ nil
16
+ else
17
+ # extract the token
18
+ response_parsed = JSON.parse(response.body)
19
+ response_parsed['api_key']
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Buildpacks
6
+ include Nucleus::Adapters::BuildpackTranslator
7
+
8
+ # @see BuildpackTranslator#vendor_specific_runtimes
9
+ def vendor_specific_runtimes
10
+ {
11
+ 'ruby' => 'https://github.com/heroku/heroku-buildpack-ruby',
12
+ 'nodejs' => 'https://github.com/heroku/heroku-buildpack-nodejs',
13
+ 'clojure' => 'https://github.com/heroku/heroku-buildpack-clojure',
14
+ 'python' => 'https://github.com/heroku/heroku-buildpack-python',
15
+ 'java' => 'https://github.com/heroku/heroku-buildpack-java',
16
+ 'gradle' => 'https://github.com/heroku/heroku-buildpack-gradle',
17
+ 'grails' => 'https://github.com/heroku/heroku-buildpack-grails',
18
+ 'scala' => 'https://github.com/heroku/heroku-buildpack-scala',
19
+ 'play' => 'https://github.com/heroku/heroku-buildpack-play',
20
+ 'php' => 'https://github.com/heroku/heroku-buildpack-php'
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +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
@@ -0,0 +1,43 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Domains
6
+ # As of now, there is no update functionality on Heroku as on most other platforms (!)
7
+
8
+ # @see Stub#domains
9
+ def domains(application_id)
10
+ domains = get("/apps/#{application_id}/domains").body
11
+ # exclude web_url by domain, otherwise we would need to fire an additional query and get the application
12
+ domains.delete_if { |domain| domain[:hostname].end_with? ".#{@endpoint_app_domain}" }
13
+ domains.collect { |domain| to_nucleus_domain(domain) }
14
+ end
15
+
16
+ # @see Stub#domain
17
+ def domain(application_id, domain_id)
18
+ domain = get("/apps/#{application_id}/domains/#{domain_id}").body
19
+ to_nucleus_domain(domain)
20
+ end
21
+
22
+ # @see Stub#create_domain
23
+ def create_domain(application_id, domain)
24
+ domain = post("/apps/#{application_id}/domains", body: { hostname: domain[:name] }).body
25
+ to_nucleus_domain(domain)
26
+ end
27
+
28
+ # @see Stub#delete_domain
29
+ def delete_domain(application_id, domain_id)
30
+ delete("/apps/#{application_id}/domains/#{domain_id}")
31
+ end
32
+
33
+ private
34
+
35
+ def to_nucleus_domain(domain)
36
+ domain[:name] = domain.delete :hostname
37
+ domain
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +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
+ latest_version_id
131
+ else
132
+ latest_version = 0
133
+ latest_version_id = nil
134
+ dynos.each do |dyno|
135
+ if dyno[:release][:version] > latest_version
136
+ latest_version = dyno[:release][:version]
137
+ latest_version_id = dyno[:release][:id]
138
+ end
139
+ end
140
+ latest_version_id
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,51 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Lifecycle
6
+ # Lifecycle:
7
+ # A) via maintenance - workers are still active
8
+ # B) via formation - loose scaling information
9
+
10
+ # @see Stub#start
11
+ def start(application_id)
12
+ log.debug "Start @ #{@endpoint_url}"
13
+
14
+ app = application(application_id)
15
+ if app[:state] == Enums::ApplicationStates::DEPLOYED
16
+ # add web dyno if there currently are no dynos (state == deployed)
17
+ scale_web(application_id, 1)
18
+ elsif app[:state] == Enums::ApplicationStates::CREATED
19
+ # fail if there is no deployment
20
+ fail Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be started'
21
+ end
22
+
23
+ scale_worker(application_id, 1)
24
+ update_application(application_id, maintenance: false)
25
+ end
26
+
27
+ # @see Stub#stop
28
+ def stop(application_id)
29
+ log.debug "Stop @ #{@endpoint_url}"
30
+
31
+ # fail if there is no deployment
32
+ app = application(application_id)
33
+ if app[:state] == Enums::ApplicationStates::CREATED
34
+ fail Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be stopped'
35
+ end
36
+
37
+ scale_worker(application_id, 0)
38
+ update_application(application_id, maintenance: true)
39
+ end
40
+
41
+ # @see Stub#restart
42
+ def restart(application_id)
43
+ log.debug "Restart @ #{@endpoint_url}"
44
+ stop(application_id)
45
+ start(application_id)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end