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,108 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Logs
6
+ # Carriage return (newline in Mac OS) + line feed (newline in Unix) == CRLF (newline in Windows)
7
+ CRLF = "\r\n"
8
+
9
+ # @see Stub#logs
10
+ def logs(application_id)
11
+ # fails with 404 if application is not available and serves for timestamps
12
+ app = get("/apps/#{application_id}").body
13
+
14
+ available_log_files = []
15
+ available_log_types.keys.each do |type|
16
+ # TODO: right now, we always assume the log has recently been updated
17
+ available_log_files.push(id: type, name: type, type: type,
18
+ created_at: app[:created_at], updated_at: Time.now.utc.iso8601)
19
+ end
20
+ available_log_files
21
+ end
22
+
23
+ # @see Stub#log?
24
+ def log?(application_id, log_id)
25
+ # fails with 404 if application is not available
26
+ get("/apps/#{application_id}")
27
+
28
+ return true if log_id.to_sym == :all
29
+ return true if log_id.to_sym == :build
30
+ available_log_types.key? log_id.to_sym
31
+ end
32
+
33
+ # @see Stub#log_entries
34
+ def log_entries(application_id, log_id)
35
+ unless log?(application_id, log_id)
36
+ fail Errors::AdapterResourceNotFoundError,
37
+ "Invalid log file '#{log_id}', not available for application '#{application_id}'"
38
+ end
39
+
40
+ return build_log_entries(application_id) if log_id.to_sym == Enums::ApplicationLogfileType::BUILD
41
+
42
+ request_body = request_body(log_id.to_sym).merge(tail: false)
43
+ log = post("/apps/#{application_id}/log-sessions", body: request_body).body
44
+ logfile = get(log[:logplex_url], headers: {}).body
45
+ # process to entries
46
+ entries = []
47
+ # skip empty logs, which are detected as Hash by the http client
48
+ logfile.split(CRLF).each { |logfile_line| entries.push logfile_line } unless logfile == {}
49
+ entries
50
+ end
51
+
52
+ # @see Stub#tail
53
+ def tail(application_id, log_id, stream)
54
+ # Currently no tailing for build log possible
55
+ if log_id == Enums::ApplicationLogfileType::BUILD
56
+ entries = build_log_entries(application_id)
57
+ entries.each { |entry| stream.send_message(entry) }
58
+ stream.close
59
+ else
60
+ request_body = request_body(log_id.to_sym).merge(tail: true)
61
+ log = post("/apps/#{application_id}/log-sessions", body: request_body).body
62
+ tail_http_response(log[:logplex_url], stream)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def available_log_types
69
+ log_types = {}
70
+ log_types[Enums::ApplicationLogfileType::API] = { source: 'heroku', dyno: 'api' }
71
+ log_types[Enums::ApplicationLogfileType::APPLICATION] = { source: 'app' }
72
+ log_types[Enums::ApplicationLogfileType::REQUEST] = { source: 'heroku', dyno: 'router' }
73
+ # TODO: filter only for web and worker dynos (must be merged manually :/)
74
+ log_types[Enums::ApplicationLogfileType::SYSTEM] = { source: 'heroku' }
75
+ log_types
76
+ end
77
+
78
+ def request_body(log_id)
79
+ return {} if log_id == :all
80
+ available_log_types[log_id]
81
+ end
82
+
83
+ def build_log_entries(application_id)
84
+ build_list = get("/apps/#{application_id}/builds").body
85
+ # limitation: show only the last 3 builds
86
+ entries = []
87
+ build_list.last(3).each do |build|
88
+ entries.push(*build_result_entries(application_id, build[:id]))
89
+ end
90
+ entries
91
+ end
92
+
93
+ def build_result_entries(application_id, build_id)
94
+ build_result = get("/apps/#{application_id}/builds/#{build_id}/result").body
95
+ entries = []
96
+ build_result[:lines].each do |line_entry|
97
+ # skip all blank lines
98
+ next if line_entry[:line].strip.empty?
99
+ # push and remove all trailing newline characters
100
+ entries.push line_entry[:line].chomp('')
101
+ end
102
+ entries
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,42 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Regions
6
+ # @see Stub#regions
7
+ def regions
8
+ response = get('/regions').body
9
+ response.each do |region|
10
+ region[:id] = region.delete(:name).upcase
11
+ end
12
+ response
13
+ end
14
+
15
+ # @see Stub#region
16
+ def region(region_name)
17
+ found_region = native_region(region_name)
18
+ fail Errors::AdapterResourceNotFoundError,
19
+ "Region '#{region_name}' does not exist at the endpoint" if found_region.nil?
20
+ found_region[:id] = found_region.delete(:name).upcase
21
+ found_region
22
+ end
23
+
24
+ private
25
+
26
+ def retrieve_region(application)
27
+ return unless application.key?(:region)
28
+ found_region = native_region(application[:region])
29
+ fail Errors::SemanticAdapterRequestError,
30
+ "Region '#{application[:region]}' does not exist at the endpoint" if found_region.nil?
31
+ application[:region] = found_region[:id]
32
+ end
33
+
34
+ def native_region(region_name)
35
+ response = get('/regions').body
36
+ response.find { |region| region[:name].casecmp(region_name) == 0 }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Scaling
6
+ # @see Stub#scale
7
+ def scale(application_id, instances)
8
+ scale_web(application_id, instances)
9
+ # return the updated application object
10
+ application(application_id)
11
+ end
12
+
13
+ private
14
+
15
+ def scale_web(application_id, instances)
16
+ patch("/apps/#{application_id}/formation", body: { updates: [{ process: 'web', quantity: instances }] })
17
+ end
18
+
19
+ def scale_worker(application_id, instances)
20
+ patch("/apps/#{application_id}/formation", body: { updates: [{ process: 'worker', quantity: instances }] },
21
+ # raises 404 if no worker is defined in the Procfile
22
+ expects: [404])
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ # Semantic error messages that are specific for Heroku
6
+ module SemanticErrors
7
+ # Get all Heroku specific semantic error definitions.
8
+ # @return [Hash<Symbol,Hash<Symbol,String>>] the error message definitions, including the error +code+,
9
+ # e.g. +422_200_1+ and the +message+ that shall be formatted when used.
10
+ def semantic_error_messages
11
+ {
12
+ need_verification: { code: 422_100_1,
13
+ message: 'Heroku requires a billing account to allow this action: %s' },
14
+ no_autoscale: { code: 422_100_2, message: 'Can\'t use \'autoscale\' on Heroku' },
15
+ invalid_runtime: { code: 422_100_3,
16
+ message: 'Invalid runtime: %s is neither a known runtime, nor a buildpack URL' }
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,168 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Services
6
+ # @see Stub#services
7
+ def services
8
+ get('/addon-services').body.collect { |service| to_nucleus_service(service) }
9
+ end
10
+
11
+ # @see Stub#service
12
+ def service(service_id)
13
+ to_nucleus_service(get("/addon-services/#{service_id}").body)
14
+ end
15
+
16
+ # @see Stub#service_plans
17
+ def service_plans(service_id)
18
+ load_plans(service_id).collect { |plan| to_nucleus_plan(plan) }.sort_by do |plan|
19
+ # only compare the first cost, covers most cases and sorting for all costs would be far too complex
20
+ plan[:costs][0][:price].first[:amount].to_f
21
+ end
22
+ end
23
+
24
+ # @see Stub#service_plan
25
+ def service_plan(service_id, plan_id)
26
+ to_nucleus_plan(get("/addon-services/#{service_id}/plans/#{plan_id}").body)
27
+ end
28
+
29
+ # @see Stub#installed_services
30
+ def installed_services(application_id)
31
+ get("/apps/#{application_id}/addons").body.collect { |service| to_nucleus_installed_service(service) }
32
+ end
33
+
34
+ # @see Stub#installed_service
35
+ def installed_service(application_id, service_id)
36
+ assigned_service = raw_installed_service(application_id, service_id)
37
+ to_nucleus_installed_service(assigned_service)
38
+ end
39
+
40
+ # @see Stub#add_service
41
+ def add_service(application_id, service_entity, plan_entity)
42
+ begin
43
+ # make sure plan belongs to this service, throws 404 if no such plan
44
+ # the service plan itself requires the name, e.g. 'sandbox' or the UUID
45
+ service_plan(service_entity[:id], plan_entity[:id])
46
+ rescue Errors::AdapterResourceNotFoundError => e
47
+ # convert to 422
48
+ raise Errors::SemanticAdapterRequestError, e.message
49
+ end
50
+ # the plan to choose requires the UUID of the plan OR the combination of both names
51
+ plan_id = service_plan_identifier(service_entity[:id], plan_entity[:id])
52
+ created = post("/apps/#{application_id}/addons", body: { plan: plan_id }).body
53
+ to_nucleus_installed_service(created)
54
+ end
55
+
56
+ # @see Stub#change_service
57
+ def change_service(application_id, service_id, plan_entity)
58
+ # make sure service is bound to the application
59
+ assignment_id = raw_installed_service(application_id, service_id)[:id]
60
+ begin
61
+ # make sure plan belongs to this service, throws 404 if no such plan
62
+ # the service plan itself requires the name, e.g. 'sandbox' or the UUID
63
+ service_plan(service_id, plan_entity[:id])
64
+ rescue Errors::AdapterResourceNotFoundError => e
65
+ # convert to 422
66
+ raise Errors::SemanticAdapterRequestError, e.message
67
+ end
68
+ # the plan to choose requires the UUID of the plan OR the combination of both names
69
+ plan_id = service_plan_identifier(service_id, plan_id)
70
+ updated = patch("/apps/#{application_id}/addons/#{assignment_id}", body: { plan: plan_id }).body
71
+ to_nucleus_installed_service(updated)
72
+ end
73
+
74
+ # @see Stub#remove_service
75
+ def remove_service(application_id, service_id)
76
+ # make sure service is bound to the application
77
+ assignment_id = raw_installed_service(application_id, service_id)[:id]
78
+ delete("/apps/#{application_id}/addons/#{assignment_id}")
79
+ end
80
+
81
+ private
82
+
83
+ def service_plan_identifier(service_id, plan_id)
84
+ # process plan id_or_name to build the unique identifier
85
+ # a) is a UUID
86
+ return plan_id if Regexp::UUID_PATTERN =~ plan_id
87
+ # b) is valid identifier, contains ':'
88
+ return plan_id if /^[-\w]+:[-\w]+$/i =~ plan_id
89
+ # c) fetch id for name
90
+ return "#{service_id}:#{plan_id}" unless Regexp::UUID_PATTERN =~ service_id
91
+ # arriving here, service_id is UUID but plan_id is the name --> DOH!
92
+ # we return the plan_id and the request will presumably fail
93
+ plan_id
94
+ end
95
+
96
+ def raw_installed_service(application_id, service_id)
97
+ # here we probably receive the ID of the service, not the service assignment ID itself
98
+ installed = get("/apps/#{application_id}/addons/#{service_id}", expects: [200, 404])
99
+ if installed.status == 404
100
+ assignment_id = service_assignment_id(application_id, service_id)
101
+ fail Errors::AdapterResourceNotFoundError,
102
+ "Service #{service_id} is not assigned to application #{application_id}" unless assignment_id
103
+ return get("/apps/#{application_id}/addons/#{assignment_id}").body
104
+ end
105
+ installed.body
106
+ end
107
+
108
+ def service_assignment_id(application_id, service_id)
109
+ all_services = get("/apps/#{application_id}/addons").body
110
+ match = all_services.find do |addon|
111
+ addon[:addon_service][:id] == service_id || addon[:addon_service][:name] == service_id
112
+ end
113
+ return match[:id] if match
114
+ nil
115
+ end
116
+
117
+ def to_nucleus_service(service)
118
+ service[:description] = service.delete(:human_name)
119
+ service[:release] = service.delete(:state)
120
+ service[:required_services] = []
121
+ service[:free_plan] = free_plan?(service[:id])
122
+ service[:documentation_url] = "https://addons.heroku.com/#{service[:name]}"
123
+ service
124
+ end
125
+
126
+ def to_nucleus_installed_service(installed_service)
127
+ service = service(installed_service[:addon_service][:id])
128
+ # get all variables and reject all that do not belong to the addon
129
+ unless installed_service[:config_vars].nil? && installed_service[:config_vars].empty?
130
+ vars = get("/apps/#{installed_service[:app][:id]}/config-vars").body
131
+ # ignore all vars that do not belong to the service
132
+ vars = vars.delete_if { |k| !installed_service[:config_vars].include?(k.to_s) }
133
+ # format to desired format
134
+ vars = vars.collect { |k, v| { key: k, value: v, description: nil } }
135
+ end
136
+ service[:properties] = vars ? vars : []
137
+ service[:active_plan] = installed_service[:plan][:id]
138
+ service[:web_url] = installed_service[:web_url]
139
+ service
140
+ end
141
+
142
+ def to_nucleus_plan(plan)
143
+ # TODO: extract payment period to enum
144
+ plan[:costs] = [{ price: [amount: plan[:price][:cents] / 100.0, currency: 'USD'],
145
+ period: plan[:price][:unit], per_instance: false }]
146
+ plan[:free] = plan[:price][:cents] == 0
147
+ plan
148
+ end
149
+
150
+ def load_plans(service_id)
151
+ get("/addon-services/#{service_id}/plans").body
152
+ end
153
+
154
+ # Memoize this detection.
155
+ # The information is not critical, but takes some time to evaluate.
156
+ # Values are not expected to change often.
157
+ def free_plan?(service_id, plans = nil)
158
+ @free_plans ||= {}
159
+ return @free_plans[service_id] if @free_plans.key?(service_id)
160
+ plans = load_plans(service_id) unless plans
161
+ @free_plans[service_id] = plans.any? { |plan| plan[:price][:cents] == 0 }
162
+ @free_plans[service_id]
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,65 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ module Vars
6
+ # @see Stub#env_vars
7
+ def env_vars(application_id)
8
+ all_vars = get("/apps/#{application_id}/config-vars").body
9
+ formatted_vars = []
10
+ all_vars.each do |key, value|
11
+ formatted_vars.push(id: key, key: key, value: value)
12
+ end
13
+ formatted_vars
14
+ end
15
+
16
+ # @see Stub#env_var
17
+ def env_var(application_id, env_var_key)
18
+ all_vars = get("/apps/#{application_id}/config-vars").body
19
+ fail Errors::AdapterResourceNotFoundError,
20
+ "Env. var key '#{env_var_key}' does not exist" unless env_var?(application_id, env_var_key, all_vars)
21
+
22
+ { id: env_var_key, key: env_var_key, value: all_vars[env_var_key.to_sym] }
23
+ end
24
+
25
+ # @see Stub#create_env_var
26
+ def create_env_var(application_id, env_var)
27
+ fail Errors::SemanticAdapterRequestError,
28
+ "Env. var key '#{env_var[:key]}' already taken" if env_var?(application_id, env_var[:key])
29
+
30
+ request_body = { env_var[:key] => env_var[:value] }
31
+ all_vars = patch("/apps/#{application_id}/config-vars", body: request_body).body
32
+ { id: env_var[:key], key: env_var[:key], value: all_vars[env_var[:key].to_sym] }
33
+ end
34
+
35
+ # @see Stub#update_env_var
36
+ def update_env_var(application_id, env_var_key, env_var)
37
+ fail Errors::AdapterResourceNotFoundError,
38
+ "Env. var key '#{env_var_key}' does not exist" unless env_var?(application_id, env_var_key)
39
+
40
+ request_body = { env_var_key => env_var[:value] }
41
+ updated_vars = patch("/apps/#{application_id}/config-vars", body: request_body).body
42
+ { id: env_var_key, key: env_var_key, value: updated_vars[env_var_key.to_sym] }
43
+ end
44
+
45
+ # @see Stub#delete_env_var
46
+ def delete_env_var(application_id, env_var_key)
47
+ fail Errors::AdapterResourceNotFoundError,
48
+ "Env. var key '#{env_var_key}' does not exist" unless env_var?(application_id, env_var_key)
49
+
50
+ # vars can be deleted by setting them to null / nil
51
+ request_body = { env_var_key => nil }
52
+ patch("/apps/#{application_id}/config-vars", body: request_body).body
53
+ end
54
+
55
+ private
56
+
57
+ def env_var?(application_id, env_var_key, all_vars = nil)
58
+ all_vars = get("/apps/#{application_id}/config-vars").body if all_vars.nil?
59
+ all_vars.key? env_var_key.to_sym
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,68 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class OpenshiftV2 < Stub
5
+ module AppStates
6
+ # Determine the current state of the application in the Nucleus lifecycle.
7
+ # @return [Symbol] application state according to {Nucleus::Enums::ApplicationStates}
8
+ def application_state(app, gear_groups = nil, deployments = nil)
9
+ deployments = load_deployments(app[:id]) unless deployments
10
+ gear_groups = load_gears(app[:id]) unless gear_groups
11
+
12
+ return :created if state_created?(app, gear_groups, deployments)
13
+ return :deployed if state_deployed?(app, gear_groups, deployments)
14
+ return :running if gear_groups[0][:gears].any? { |gear| gear[:state] == 'started' }
15
+ return :stopped if gear_groups[0][:gears].all? { |gear| gear[:state] == 'stopped' }
16
+ return :idle if gear_groups[0][:gears].all? { |gear| gear[:state] == 'idle' }
17
+
18
+ log.debug("Failed to determine state for: #{app}")
19
+ fail Errors::UnknownAdapterCallError,
20
+ 'Could not determine app state. Please verify the Openshift V2 adapter'
21
+ end
22
+
23
+ private
24
+
25
+ def state_created?(app, gear_groups, deployments)
26
+ # this state exists, but only within the first seconds before the original deployment is applied
27
+ return true if gear_groups[0][:gears].all? { |gear| gear[:state] == 'new' }
28
+
29
+ if app[:keep_deployments].to_i > 1
30
+ if deployments.length == 1
31
+ original_os_deployment = original_deployment(app, deployments)
32
+ currently_activated = active_deployment(app, deployments)
33
+ # if the current deployment still is the default, the state must be :created
34
+ return true if original_os_deployment && original_os_deployment[:id] == currently_activated[:id]
35
+ end
36
+ # if there is more than 1 deployment, state can't be :created
37
+ else
38
+ # app was not created with nucleus or has recently been modified :/
39
+ diff = Time.parse(deployments[0][:created_at]).to_i - Time.parse(app[:creation_time]).to_i
40
+ # we can analyse if the deployment was created within 15 seconds after the application,
41
+ # then there can't possibly be an actual code deployment
42
+ return true if diff.abs < 15
43
+ end
44
+ # does not seem to be in state :created
45
+ false
46
+ end
47
+
48
+ def state_deployed?(app, gear_groups, deployments)
49
+ # Gears must all be stopped
50
+ return false unless gear_groups[0][:gears].all? { |gear| gear[:state] == 'stopped' }
51
+
52
+ deployments = load_deployments(app[:id]) unless deployments
53
+
54
+ # If there still is the initial deployment, then the state can be deployed.
55
+ original_os_deployment = original_deployment(app, deployments) unless original_os_deployment
56
+ return false unless original_os_deployment
57
+
58
+ activations = deployments.inject(0) { |a, e| a + e[:activations].length }
59
+ # deduct the activations of the original deployment
60
+ activations -= original_os_deployment[:activations].length
61
+ return false if activations > 1
62
+ true
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end