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,33 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ module Regions
6
+ # @see Stub#regions
7
+ def regions
8
+ [default_region]
9
+ end
10
+
11
+ # @see Stub#region
12
+ def region(region_name)
13
+ unless region_name.casecmp('default') == 0
14
+ fail Errors::AdapterResourceNotFoundError, "Region '#{region_name}' does not exist at the endpoint"
15
+ end
16
+ default_region
17
+ end
18
+
19
+ private
20
+
21
+ def default_region
22
+ {
23
+ id: 'default',
24
+ description: 'Default region, Cloud Foundry does not support multi regions yet.',
25
+ created_at: Time.at(0).to_datetime,
26
+ updated_at: Time.at(0).to_datetime
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ module Scaling
6
+ # @see Stub#scale
7
+ def scale(application_name_or_id, instances)
8
+ # update the number of instances on the application
9
+ update_application(application_name_or_id, instances: instances)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ # Semantic error messages that are specific for Cloud Foundry V2
6
+ module SemanticErrors
7
+ # Get all Cloud Foundry V2 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
+ only_one_runtime: { code: 422_200_1, message: 'Cloud Foundry V2 only allows 1 runtime per application' },
13
+ build_in_progrss: { code: 422_200_2, message: 'Application build is still in progress' },
14
+ no_space_assigned: { code: 422_200_3, message: 'User is not assigned to any space' },
15
+ service_not_bindable: { code: 422_200_4, message: "Can't add service '%s' to the application: "\
16
+ 'The service does not allow to be bound to applications.' },
17
+ service_not_active: { code: 422_200_5, message: "Can't add service '%s' to the application: "\
18
+ 'The service does is not active, thus does not allow to create a new instance to bind to the app.' },
19
+ service_not_updateable: { code: 422_200_6, message: "Can't change service '%s' to the new plan: "\
20
+ 'The service does not allow to update the plan.' }
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,286 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ # Cloud Foundry, operations for the application's addons
6
+ module Services
7
+ # @see Stub#services
8
+ def services
9
+ get('/v2/services?inline-relations-depth=1').body[:resources].collect do |service|
10
+ # show only services that are both, active and bindable
11
+ next unless service[:entity][:active] && service[:entity][:bindable]
12
+ to_nucleus_service(service)
13
+ end.compact
14
+ end
15
+
16
+ # @see Stub#service
17
+ def service(service_id_or_name)
18
+ service_guid = service_guid(service_id_or_name)
19
+ to_nucleus_service(get("/v2/services/#{service_guid}?inline-relations-depth=1").body)
20
+ end
21
+
22
+ # @see Stub#service_plans
23
+ def service_plans(service_id_or_name)
24
+ service_guid = service_guid(service_id_or_name)
25
+ load_plans(service_guid).collect { |plan| to_nucleus_plan(plan) }
26
+ end
27
+
28
+ # @see Stub#service_plan
29
+ def service_plan(service_id_or_name, plan_id)
30
+ service_guid = service_guid(service_id_or_name)
31
+ plan_guid = plan_guid(service_guid, plan_id, Errors::AdapterResourceNotFoundError)
32
+ to_nucleus_plan(get("/v2/service_plans/#{plan_guid}").body)
33
+ end
34
+
35
+ # @see Stub#installed_services
36
+ def installed_services(application_name_or_id)
37
+ app_guid = app_guid(application_name_or_id)
38
+ get("/v2/apps/#{app_guid}/service_bindings?inline-relations-depth=1").body[:resources].collect do |binding|
39
+ to_nucleus_installed_service(binding)
40
+ end
41
+ end
42
+
43
+ # @see Stub#installed_service
44
+ def installed_service(application_name_or_id, service_id_or_name)
45
+ app_guid = app_guid(application_name_or_id)
46
+ service_guid = service_guid(service_id_or_name)
47
+ cf_binding = binding(app_guid, service_guid)
48
+ # make sure there is a binding
49
+ fail Errors::AdapterResourceNotFoundError,
50
+ "No such service '#{service_id_or_name}' for application '#{application_name_or_id}'" unless cf_binding
51
+ to_nucleus_installed_service(cf_binding)
52
+ end
53
+
54
+ # @see Stub#add_service
55
+ def add_service(application_name_or_id, service_entity, plan_entity)
56
+ app_guid = app_guid(application_name_or_id)
57
+ service_guid = service_guid(service_entity[:id], Errors::SemanticAdapterRequestError)
58
+ cf_service = load_allowed_service(service_entity, service_guid)
59
+
60
+ # get the plan, throws 422 if the plan could not be found
61
+ plan_guid = plan_guid(service_guid, plan_entity[:id])
62
+
63
+ # create new service instance
64
+ instance_request_body = { space_guid: user_space_guid, service_plan_guid: plan_guid,
65
+ name: "#{cf_service[:entity][:label]}-#{application_name_or_id}-nucleus" }
66
+ cf_instance = post('/v2/service_instances', body: instance_request_body).body
67
+
68
+ # bind the created service instance to the application
69
+ binding_request_body = { service_instance_guid: cf_instance[:metadata][:guid], app_guid: app_guid }
70
+ cf_binding = post('/v2/service_bindings', body: binding_request_body).body
71
+
72
+ # created service presentation
73
+ to_nucleus_installed_service(cf_binding, cf_service, cf_instance)
74
+ end
75
+
76
+ # @see Stub#change_service
77
+ def change_service(application_name_or_id, service_id, plan_entity)
78
+ app_guid = app_guid(application_name_or_id)
79
+ service_guid = service_guid(service_id)
80
+ cf_service = get("/v2/services/#{service_guid}").body
81
+ fail_with(:service_not_updateable, [service_id]) unless cf_service[:entity][:plan_updateable]
82
+
83
+ cf_binding = binding(app_guid, service_guid)
84
+
85
+ # get the plan, throws 422 if the plan could not be found
86
+ plan_guid = plan_guid(service_guid, plan_entity[:id])
87
+ cf_instance = put("/v2/service_instances/#{cf_binding[:entity][:service_instance_guid]}",
88
+ body: { service_plan_guid: plan_guid }).body
89
+ to_nucleus_installed_service(cf_binding, cf_service, cf_instance)
90
+ end
91
+
92
+ # @see Stub#remove_service
93
+ def remove_service(application_name_or_id, service_id)
94
+ app_guid = app_guid(application_name_or_id)
95
+ service_guid = service_guid(service_id)
96
+ # sadly we can't resolve the binding and instance from the service_id with ease
97
+ # we therefore setup a chain to resolve the binding and instance from the active pla
98
+ binding = binding(app_guid, service_guid)
99
+
100
+ # now remove the binding from the application
101
+ delete("/v2/apps/#{app_guid}/service_bindings/#{binding[:metadata][:guid]}", expects: [201])
102
+ # and finally delete the service instance
103
+ delete("/v2/service_instances/#{binding[:entity][:service_instance_guid]}")
104
+ end
105
+
106
+ private
107
+
108
+ def load_allowed_service(service_entity, service_guid)
109
+ begin
110
+ cf_service = get("/v2/services/#{service_guid}").body
111
+ rescue Errors::AdapterResourceNotFoundError
112
+ # convert to semantic error with the service being a body, not a path entity
113
+ raise Errors::SemanticAdapterRequestError,
114
+ "Invalid service: Could not find service with the ID '#{service_entity[:id]}'"
115
+ end
116
+
117
+ # must be active and bindable?
118
+ # currently we focus only on bindable services
119
+ fail_with(:service_not_bindable, [service_entity[:id]]) unless cf_service[:entity][:bindable]
120
+ # service must be active, otherwise we can't create the instance
121
+ fail_with(:service_not_active, [service_entity[:id]]) unless cf_service[:entity][:active]
122
+ # service seems to be valid, return
123
+ cf_service
124
+ end
125
+
126
+ def remove_all_services(app_guid)
127
+ get("/v2/apps/#{app_guid}/service_bindings").body[:resources].collect do |binding|
128
+ # remove the binding from the application
129
+ delete("/v2/apps/#{app_guid}/service_bindings/#{binding[:metadata][:guid]}", expects: [201])
130
+ # and delete the service instance to prevent orphans
131
+ delete("/v2/service_instances/#{binding[:entity][:service_instance_guid]}")
132
+ end
133
+ end
134
+
135
+ def service_guid(service_id_or_name, error_class = Errors::AdapterResourceNotFoundError)
136
+ return service_id_or_name if guid?(service_id_or_name)
137
+ # list all available services
138
+ services = get('/v2/services').body[:resources]
139
+ # find a match and use the service's guid
140
+ service_match = services.find { |service| service[:entity][:label] == service_id_or_name }
141
+ fail error_class,
142
+ "Invalid service: Could not find service with name '#{service_id_or_name}'" unless service_match
143
+ service_match[:metadata][:guid]
144
+ end
145
+
146
+ def binding(app_guid, service_id)
147
+ service_plans = get("/v2/services/#{service_id}/service_plans").body[:resources]
148
+ service_plan_ids = service_plans.collect { |plan| plan[:metadata][:guid] }
149
+ app_bindings = get("/v2/apps/#{app_guid}/service_bindings?inline-relations-depth=1").body[:resources]
150
+ # the plan must be bound to the app via an instance
151
+ app_bindings.find do |binding|
152
+ service_plan_ids.include?(binding[:entity][:service_instance][:entity][:service_plan_guid])
153
+ end
154
+ end
155
+
156
+ def plan_guid(service_id, plan_name_or_id, error_class = Errors::SemanticAdapterRequestError)
157
+ return plan_name_or_id if guid?(plan_name_or_id)
158
+ # list all plans for the service
159
+ plans = get("/v2/services/#{service_id}/service_plans").body[:resources]
160
+ # find a match and use the plan's guid
161
+ plan_match = plans.find { |plan| plan[:entity][:name] == plan_name_or_id }
162
+ fail error_class,
163
+ "Invalid plan: No such plan '#{plan_name_or_id}' for service '#{service_id}'" unless plan_match
164
+ plan_match[:metadata][:guid]
165
+ end
166
+
167
+ def load_plans(service_id)
168
+ get("/v2/services/#{service_id}/service_plans").body[:resources]
169
+ end
170
+
171
+ # Memoize this detection.
172
+ # The information is not critical, but takes some time to evaluate.
173
+ # Values are not expected to change often.
174
+ def free_plan?(service_id, plans = nil)
175
+ @free_plans ||= {}
176
+ return @free_plans[service_id] if @free_plans.key?(service_id)
177
+ plans = load_plans(service_id) unless plans
178
+ @free_plans[service_id] = plans.any? { |plan| plan[:entity][:free] }
179
+ @free_plans[service_id]
180
+ end
181
+
182
+ def to_nucleus_plan(cf_plan)
183
+ plan = cf_plan[:entity]
184
+ plan[:id] = cf_plan[:metadata][:guid]
185
+ plan[:created_at] = cf_plan[:metadata][:created_at]
186
+ plan[:updated_at] = cf_plan[:metadata][:updated_at]
187
+ plan[:costs] = []
188
+ plan
189
+
190
+ # TODO: determine prices for CF services
191
+ # we know how IBM handles the costs, but can't determine the country
192
+ # we know how Pivotal IO handles the costs
193
+ # but what do the others???
194
+
195
+ # extra = Oj.load(plan[:extra])
196
+ # if plan[:free]
197
+ # plan[:costs] = { period: '', per_instance: false, price: { amount: 0.00, currency: nil} }
198
+ # elsif endpoint_url.include?('pivotal.io')
199
+ # # show prices for Pivotal Web Services
200
+ # # see for an explanation: http://docs.pivotal.io/pivotalcf/services/catalog-metadata.html
201
+ # plan[:costs] = extra[:costs].collect do |cost|
202
+ # prices = cost[:amount].collect { |currency, amount| { currency: currency, amount: amount } }
203
+ # { per_instance: false, period: cost[:unit], price: prices }
204
+ # end
205
+ # elsif endpoint_url.include?('bluemix.net')
206
+ # # show prices for IBM Bluemix
207
+ # else
208
+ # # fallback, unknown CF system
209
+ # end
210
+ end
211
+
212
+ def to_nucleus_service(cf_service)
213
+ service = cf_service[:entity]
214
+ service = apply_metadata(service, cf_service)
215
+ service[:name] = service.delete(:label)
216
+ service[:release] = service.delete(:version)
217
+ if cf_service[:entity].key?(:service_plans)
218
+ # use preloaded plans if available
219
+ service[:free_plan] = free_plan?(service[:id], cf_service[:entity][:service_plans])
220
+ else
221
+ service[:free_plan] = free_plan?(service[:id])
222
+ end
223
+ # CF does not have service dependencies
224
+ service[:required_services] = service.delete(:requires)
225
+ # description and documentation_url should already be set
226
+ service
227
+ end
228
+
229
+ # Show the installed service. Therefore we need:<br>
230
+ # <ul>
231
+ # <li>the classic service for the basic information (version, name, ...)</li>
232
+ # <li>the binding for the properties and metadata (id, timestamps, ...)</li>
233
+ # <li>the bound instance for the active plan and web_url</li>
234
+ # </ul>
235
+ def to_nucleus_installed_service(cf_binding, cf_service = nil, cf_instance = nil)
236
+ # load if not provided
237
+ cf_instance = load_instance(cf_binding) unless cf_instance
238
+ cf_service = load_service(cf_instance) unless cf_service
239
+ # load if not provided
240
+ unless cf_service
241
+ cf_service = get("/v2/service_plans/#{cf_instance[:entity][:service_plan_guid]}"\
242
+ '?inline-relations-depth=1').body[:entity][:service]
243
+ end
244
+
245
+ # active_plan, web_url, properties
246
+ service = to_nucleus_service(cf_service)
247
+ # use the metadata of the binding, is more future proof than instance metadata
248
+ apply_metadata(service, cf_binding)
249
+ service[:active_plan] = cf_instance[:entity][:service_plan_guid]
250
+ service[:web_url] = cf_instance[:entity][:dashboard_url]
251
+ service[:properties] = binding_properties(cf_binding)
252
+ service
253
+ end
254
+
255
+ def load_instance(cf_binding)
256
+ if cf_binding[:entity].key?(:service_instance)
257
+ # use if nested property is available
258
+ cf_binding[:entity][:service_instance]
259
+ else
260
+ get("/v2/service_instances/#{cf_binding[:entity][:service_instance_guid]}").body
261
+ end
262
+ end
263
+
264
+ def load_service(cf_instance)
265
+ get("/v2/service_plans/#{cf_instance[:entity][:service_plan_guid]}"\
266
+ '?inline-relations-depth=1').body[:entity][:service]
267
+ end
268
+
269
+ def binding_properties(binding)
270
+ # in the credentials there are information such as: hostname, username, password, license keys, ...
271
+ binding[:entity][:credentials].collect do |key, value|
272
+ { key: key, value: value, description: nil }
273
+ end
274
+ end
275
+
276
+ def apply_metadata(apply_to, cf_object)
277
+ apply_to[:id] = cf_object[:metadata][:guid]
278
+ apply_to[:created_at] = cf_object[:metadata][:created_at]
279
+ apply_to[:updated_at] = cf_object[:metadata][:updated_at]
280
+ apply_to
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,80 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ module Vars
6
+ # @see Stub#env_vars
7
+ def env_vars(application_name_or_id)
8
+ app_guid = app_guid(application_name_or_id)
9
+ app_vars = get("/v2/apps/#{app_guid}/env").body[:environment_json]
10
+ formatted_vars = []
11
+ app_vars.each do |key, value|
12
+ formatted_vars.push(id: key, key: key, value: value)
13
+ end
14
+ formatted_vars
15
+ end
16
+
17
+ # @see Stub#env_var
18
+ def env_var(application_name_or_id, env_var_key)
19
+ app_guid = app_guid(application_name_or_id)
20
+ all_vars = get("/v2/apps/#{app_guid}/env").body[:environment_json]
21
+ fail Errors::AdapterResourceNotFoundError,
22
+ "Env. var key '#{env_var_key}' does not exist" unless env_var?(app_guid, env_var_key, all_vars)
23
+ { id: env_var_key, key: env_var_key, value: all_vars[env_var_key.to_sym] }
24
+ end
25
+
26
+ # @see Stub#create_env_var
27
+ def create_env_var(application_name_or_id, env_var)
28
+ app_guid = app_guid(application_name_or_id)
29
+ fail Errors::SemanticAdapterRequestError,
30
+ "Env. var key '#{env_var[:key]}' already taken" if env_var?(app_guid, env_var[:key])
31
+ set_var(app_guid, env_var[:key].to_sym, env_var[:value])
32
+ end
33
+
34
+ # @see Stub#update_env_var
35
+ def update_env_var(application_name_or_id, env_var_key, env_var)
36
+ app_guid = app_guid(application_name_or_id)
37
+ fail Errors::AdapterResourceNotFoundError,
38
+ "Env. var key '#{env_var_key}' does not exist" unless env_var?(app_guid, env_var_key)
39
+ set_var(app_guid, env_var_key.to_sym, env_var[:value])
40
+ end
41
+
42
+ # @see Stub#delete_env_var
43
+ def delete_env_var(application_name_or_id, env_var_key)
44
+ app_guid = app_guid(application_name_or_id)
45
+ fail Errors::AdapterResourceNotFoundError,
46
+ "Env. var key '#{env_var_key}' does not exist" unless env_var?(app_guid, env_var_key)
47
+ request_body = get("/v2/apps/#{app_guid}/env").body[:environment_json]
48
+ request_body.delete(env_var_key.to_sym)
49
+ put("/v2/apps/#{app_guid}", body: { environment_json: request_body }).body[:entity][:environment_json]
50
+ end
51
+
52
+ private
53
+
54
+ # Set the variable value (create or update) and include all already existing variables
55
+ # to protect them from being deleted.
56
+ # @param [String] app_guid GUID of the CF application
57
+ # @param [Symbol] key variable key name
58
+ # @param [String, Integer, Float, Double] value value to apply to the variable
59
+ # @return [Hash] Nucleus representation of the modified variable
60
+ def set_var(app_guid, key, value)
61
+ request_body = get("/v2/apps/#{app_guid}/env").body[:environment_json]
62
+ request_body[key] = value
63
+ vars = put("/v2/apps/#{app_guid}", body: { environment_json: request_body }).body[:entity]
64
+ { id: key, key: key, value: vars[:environment_json][key] }
65
+ end
66
+
67
+ # Checks if a variable with the env_var_key already exists.
68
+ # @param [String] app_guid GUID of the CF application
69
+ # @param [Symbol] env_var_key key name that shall be checked for existence
70
+ # @param [Hash] all_vars collection of currently existing variables for the application
71
+ # @return [Boolean] true if there is a variable with the env_var_key, otherwise false
72
+ def env_var?(app_guid, env_var_key, all_vars = nil)
73
+ all_vars = get("/v2/apps/#{app_guid}/env").body[:environment_json] if all_vars.nil?
74
+ all_vars.key? env_var_key.to_sym
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,57 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class Heroku < Stub
5
+ # AppStates for Heroku, or the logic to determine the current application state
6
+ module AppStates
7
+ private
8
+
9
+ def application_state(app, retrieved_dynos = nil)
10
+ # 1: created, both repo and slug are nil
11
+ return Enums::ApplicationStates::CREATED unless repo_or_slug_content?(app)
12
+
13
+ # all subsequent states require dynos to be determined
14
+ dynos = retrieved_dynos ? retrieved_dynos : dynos(app[:id])
15
+
16
+ # 2: deployed if no dynos assigned
17
+ return Enums::ApplicationStates::DEPLOYED if dynos.empty?
18
+
19
+ # 3: stopped if maintenance
20
+ return Enums::ApplicationStates::STOPPED if app[:maintenance] || dynos_not_running?(dynos)
21
+
22
+ # 4: running if no maintenance (checked above) and at least ony dyno is up
23
+ return Enums::ApplicationStates::RUNNING if dyno_states(dynos).include?('up')
24
+
25
+ # 5: idle if all dynos are idling
26
+ return Enums::ApplicationStates::IDLE if dynos_idle?(dynos)
27
+
28
+ # arriving here the above states do not catch all states of the Heroku app, which should not happen ;-)
29
+ log.debug("Faild to determine state for: #{app}, #{dynos}")
30
+ fail Errors::UnknownAdapterCallError, 'Could not determine app state. Please verify the Heroku adapter'
31
+ end
32
+
33
+ def repo_or_slug_content?(app)
34
+ return true if !app[:repo_size].nil? && app[:repo_size].to_i > 0
35
+ return true if !app[:slug_size].nil? && app[:slug_size].to_i > 0
36
+ false
37
+ end
38
+
39
+ def dyno_states(dynos)
40
+ dynos.collect { |dyno| dyno[:state] }.compact.uniq
41
+ end
42
+
43
+ def dynos_idle?(dynos)
44
+ dyno_states = dyno_states(dynos)
45
+ dyno_states.length == 1 && dyno_states[0] == 'idle'
46
+ end
47
+
48
+ def dynos_not_running?(dynos)
49
+ dynos.empty? || dyno_states(dynos).reject do |state|
50
+ %w(crashed down starting).include?(state)
51
+ end.empty?
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end