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,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