nucleus 0.1.0

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