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,149 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ # Application domain / route functionality to support the Cloud Foundry API.<br>
6
+ module Domains
7
+ # @see Stub#domains
8
+ def domains(domain_id)
9
+ app_guid = app_guid(domain_id)
10
+ assigned_routes = get("/v2/apps/#{app_guid}/routes").body
11
+ domains = []
12
+ assigned_routes[:resources].each do |assigned_route|
13
+ nucleus_domain = route_to_nucleus_domain(assigned_route)
14
+ domains.push(nucleus_domain) unless nucleus_domain[:name] == app_web_url(app_guid)
15
+ end
16
+ domains
17
+ end
18
+
19
+ # @see Stub#domain
20
+ def domain(application_name_or_id, domain_id)
21
+ app_guid = app_guid(application_name_or_id)
22
+ assigned_routes = get("/v2/apps/#{app_guid}/routes").body
23
+ assigned_routes[:resources].each do |assigned_route|
24
+ return route_to_nucleus_domain(assigned_route) if assigned_route[:metadata][:guid] == domain_id
25
+ end
26
+ end
27
+
28
+ # @see Stub#create_domain
29
+ def create_domain(application_name_or_id, domain)
30
+ domains(application_name_or_id).each do |existing_domain|
31
+ if existing_domain[:name] == domain[:name]
32
+ fail Errors::SemanticAdapterRequestError,
33
+ "Domain '#{domain[:name]}' is already assigned to the application"
34
+ end
35
+ end
36
+
37
+ app_guid = app_guid(application_name_or_id)
38
+ # extract the hostname and the domain name from the FQDN
39
+ /(?<domain_host>([-\w]+\.)*)(?<domain_name>([-\w]+\.[-\w]+))/ =~ domain[:name]
40
+ domain_host.chomp!('.') unless domain_host.nil?
41
+
42
+ # finally build the response
43
+ route_to_nucleus_domain(create_cf_domain(app_guid, domain_name, domain_host))
44
+ end
45
+
46
+ # @see Stub#delete_domain
47
+ def delete_domain(application_name_or_id, route_id)
48
+ app_guid = app_guid(application_name_or_id)
49
+ # remove route from the app
50
+ delete_response = delete("/v2/apps/#{app_guid}/routes/#{route_id}", expects: [201, 400])
51
+ if delete_response.status == 400
52
+ cf_error = delete_response.body[:code]
53
+ if cf_error == 1002
54
+ fail Errors::AdapterResourceNotFoundError, 'Domain not found. '\
55
+ 'CF context specific: Route does not exist or is not assigned with this application'
56
+ else
57
+ # delete failed with 400, but not due to invalid domain
58
+ fail Errors::AdapterRequestError,
59
+ "#{delete_response.body[:description]} (#{cf_error} - #{delete_response.body[:error_code]})"
60
+ end
61
+ end
62
+
63
+ # check route usage
64
+ route_in_apps = get("/v2/routes/#{route_id}/apps").body
65
+ return unless route_in_apps[:total_results] == 0
66
+
67
+ # route is no longer needed, delete
68
+ delete("/v2/routes/#{route_id}")
69
+ end
70
+
71
+ private
72
+
73
+ def domain?(application_name_or_id, domain_name)
74
+ app_guid = app_guid(application_name_or_id)
75
+ domain_without_protocol = %r{([a-zA-Z]+://)?([-\.\w]*)}.match(domain_name)[2]
76
+ assigned_routes = get("/v2/apps/#{app_guid}/routes").body
77
+ assigned_routes[:resources].each do |route|
78
+ route_domain = get(route[:entity][:domain_url]).body
79
+ return true if "#{route[:entity][:host]}.#{route_domain[:entity][:name]}" == domain_without_protocol
80
+ end
81
+ false
82
+ end
83
+
84
+ def route_to_nucleus_domain(route_resource)
85
+ route_entity = route_resource[:entity]
86
+ route_metadata = route_resource[:metadata]
87
+ assigned_domain = get(route_entity[:domain_url]).body
88
+ domain = { id: route_metadata[:guid], created_at: route_metadata[:created_at] }
89
+ if route_metadata[:updated_at].to_s == ''
90
+ domain[:updated_at] = route_metadata[:created_at]
91
+ else
92
+ domain[:updated_at] = route_metadata[:updated_at]
93
+ end
94
+
95
+ if route_entity[:host].to_s == ''
96
+ domain[:name] = assigned_domain[:entity][:name]
97
+ else
98
+ domain[:name] = "#{route_entity[:host]}.#{assigned_domain[:entity][:name]}"
99
+ end
100
+ domain
101
+ end
102
+
103
+ def cf_domain(domain_name)
104
+ %w(private shared).each do |domain_type|
105
+ response = get("/v2/#{domain_type}_domains").body
106
+ response[:resources].each do |domain|
107
+ return domain if domain[:entity][:name] == domain_name
108
+ end
109
+ end
110
+ nil
111
+ end
112
+
113
+ def cf_route(domain_guid, domain_host)
114
+ # There is no way to check if a root domain (empty hostname) is already taken.
115
+ # Therefore we must iterate through all routes and find matches...
116
+ all_routes = get('/v2/routes').body[:resources]
117
+ all_routes.each do |route|
118
+ return route if route[:entity][:domain_guid] == domain_guid && route[:entity][:host] == domain_host
119
+ end
120
+ nil
121
+ end
122
+
123
+ def create_cf_domain(app_guid, domain_name, domain_host)
124
+ created_domain = cf_domain(domain_name)
125
+ unless created_domain
126
+ # domain does not exist, create!
127
+ domain_request_body = { name: domain_name, owning_organization_guid: default_organization_guid }
128
+ created_domain = post('/v2/private_domains', body: domain_request_body).body
129
+ end
130
+
131
+ created_route = cf_route(created_domain[:metadata][:guid], domain_host)
132
+ unless created_route
133
+ # route does not exist, create!
134
+ route_request_body = { domain_guid: created_domain[:metadata][:guid],
135
+ host: domain_host, space_guid: user_space_guid }
136
+ created_route = post('/v2/routes', body: route_request_body).body
137
+ end
138
+
139
+ # assign the route to the application
140
+ put("/v2/apps/#{app_guid}/routes/#{created_route[:metadata][:guid]}").body
141
+
142
+ # return the actual route, not the association response
143
+ created_route
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,41 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ module Lifecycle
6
+ # @see Stub#start
7
+ def start(application_name_or_id)
8
+ app_guid = app_guid(application_name_or_id)
9
+ # fail if there is no deployment
10
+ unless deployed?(app_guid)
11
+ fail Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be started'
12
+ end
13
+
14
+ # start by name or id
15
+ start_response = put("/v2/apps/#{app_guid}", body: { state: 'STARTED' })
16
+ to_nucleus_app(start_response.body)
17
+ end
18
+
19
+ # @see Stub#stop
20
+ def stop(application_name_or_id)
21
+ app_guid = app_guid(application_name_or_id)
22
+ # fail if there is no deployment
23
+ unless deployed?(app_guid)
24
+ fail Errors::SemanticAdapterRequestError, 'Application must be deployed before it can be stopped'
25
+ end
26
+
27
+ # stop by name or id
28
+ stop_response = put("/v2/apps/#{app_guid}", body: { state: 'STOPPED' })
29
+ to_nucleus_app(stop_response.body)
30
+ end
31
+
32
+ # @see Stub#restart
33
+ def restart(application_name_or_id)
34
+ stop(application_name_or_id)
35
+ start(application_name_or_id)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,303 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module V1
4
+ class CloudFoundryV2 < Stub
5
+ module Logs
6
+ LOGGREGATOR_TYPES = [Enums::ApplicationLogfileType::API,
7
+ Enums::ApplicationLogfileType::APPLICATION,
8
+ Enums::ApplicationLogfileType::REQUEST,
9
+ Enums::ApplicationLogfileType::SYSTEM]
10
+ # Carriage return (newline in Mac OS) + line feed (newline in Unix) == CRLF (newline in Windows)
11
+ CRLF = "\r\n"
12
+ WSP = "\s"
13
+
14
+ # @see Stub#logs
15
+ def logs(application_name_or_id)
16
+ app_guid = app_guid(application_name_or_id)
17
+ # retrieve app for timestamps only :/
18
+ app_created = get("/v2/apps/#{app_guid}").body[:metadata][:created_at]
19
+ logs = []
20
+
21
+ begin
22
+ log_files_list = download_file(app_guid, 'logs')
23
+ # parse raw response to array
24
+ log_files_list.split(CRLF).each do |logfile_line|
25
+ filename = logfile_line.rpartition(' ').first.strip
26
+ if filename == 'staging_task.log'
27
+ filename = 'build'
28
+ log_type = Enums::ApplicationLogfileType::BUILD
29
+ else
30
+ log_type = Enums::ApplicationLogfileType::OTHER
31
+ end
32
+ # TODO: right now, we always assume the log has recently been updated
33
+ logs.push(id: filename, name: filename, type: log_type, created_at: app_created,
34
+ updated_at: Time.now.utc.iso8601)
35
+ end
36
+ rescue Errors::AdapterError
37
+ log.debug('no logs directory found for cf application')
38
+ end
39
+
40
+ # add the default logtypes, available according to:
41
+ # http://docs.cloudfoundry.org/devguide/deploy-apps/streaming-logs.html#format
42
+ LOGGREGATOR_TYPES.each do |type|
43
+ logs.push(id: type, name: type, type: type, created_at: app_created, updated_at: Time.now.utc.iso8601)
44
+ end
45
+ # TODO: 'all' is probably not perfect, since the build log wont be included
46
+ logs.push(id: 'all', name: 'all', type: Enums::ApplicationLogfileType::OTHER,
47
+ created_at: app_created, updated_at: Time.now.utc.iso8601)
48
+ logs
49
+ end
50
+
51
+ # @see Stub#log?
52
+ def log?(application_name_or_id, log_id)
53
+ app_guid = app_guid(application_name_or_id)
54
+ # test file existence
55
+ log_id = 'staging_task.log' if log_id.to_sym == Enums::ApplicationLogfileType::BUILD
56
+ # checks also if application is even valid
57
+ response = get("/v2/apps/#{app_guid}/instances/0/files/logs/#{log_id}",
58
+ follow_redirects: false, expects: [200, 302, 400])
59
+ return true if response == 200 || log_stream?(log_id)
60
+ return false if response == 400
61
+ # if 302 (only remaining option), followup...
62
+
63
+ # download log file
64
+ download_file(app_guid, "logs/#{log_id}")
65
+ # no error, file exists
66
+ true
67
+ rescue Errors::AdapterResourceNotFoundError, Errors::UnknownAdapterCallError,
68
+ Excon::Errors::NotFound, Excon::Errors::BadRequest
69
+ false
70
+ end
71
+
72
+ # @see Stub#tail
73
+ def tail(application_name_or_id, log_id, stream)
74
+ app_guid = app_guid(application_name_or_id)
75
+ return tail_stream(app_guid, log_id, stream) if log_stream?(log_id)
76
+ tail_file(app_guid, log_id, stream)
77
+ end
78
+
79
+ # @see Stub#log_entries
80
+ def log_entries(application_name_or_id, log_id)
81
+ app_guid = app_guid(application_name_or_id)
82
+ # first check if this log is a file or must be fetched from the loggregator
83
+ if log_stream?(log_id)
84
+ # fetch recent data from loggregator and return an array of log entries
85
+ recent_decoded = recent_log_messages(app_guid, loggregator_filter(log_id))
86
+ recent_decoded.collect { |log_msg| construct_log_entry(log_msg) }
87
+ elsif log_id.to_sym == Enums::ApplicationLogfileType::BUILD
88
+ # handle special staging log
89
+ build_log_entries(app_guid)
90
+ else
91
+ download_logfile_entries(app_guid, log_id)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def build_log_entries(app_guid)
98
+ log_id = 'staging_task.log'
99
+ download_logfile_entries(app_guid, log_id)
100
+ rescue Errors::AdapterResourceNotFoundError
101
+ # if there was no build yet, return no entries instead of the 404 error
102
+ []
103
+ end
104
+
105
+ def loggregator_filter(log_id)
106
+ case log_id.to_sym
107
+ when Enums::ApplicationLogfileType::API
108
+ filter = ['API']
109
+ when Enums::ApplicationLogfileType::APPLICATION
110
+ filter = ['APP']
111
+ when Enums::ApplicationLogfileType::REQUEST
112
+ filter = ['RTR']
113
+ when Enums::ApplicationLogfileType::SYSTEM
114
+ filter = %w(STG LGR DEA)
115
+ when :all
116
+ # no filter, show all
117
+ filter = nil
118
+ else
119
+ # invalid log requests --> 404
120
+ fail Errors::AdapterResourceNotFoundError,
121
+ "Invalid log file '#{log_id}', not available for application '#{app_guid}'"
122
+ end
123
+ filter
124
+ end
125
+
126
+ def construct_log_entry(decoded_message)
127
+ # 2015-03-22T15:28:55.83+0100 [RTR/0] OUT message...
128
+ "#{Time.at(decoded_message.timestamp / 1_000_000_000.0).iso8601} "\
129
+ "[#{decoded_message.source_name}/#{decoded_message.source_id}] "\
130
+ "#{decoded_message.message_type == 1 ? 'OUT' : 'ERR'} #{decoded_message.message}"
131
+ end
132
+
133
+ def download_logfile_entries(app_guid, log_id, headers_to_use = nil)
134
+ # download log file
135
+ logfile_contents = download_file(app_guid, "logs/#{log_id}", headers_to_use)
136
+ # split file into entries by line breaks and return an array of log entries
137
+ logfile_contents.split("\n")
138
+ end
139
+
140
+ def download_file(app_guid, file_path, headers_to_use = nil)
141
+ expected_statuses = [200, 302, 400, 404]
142
+ # Hack, do not create fresh headers (which would fail) when in a deferred action
143
+ headers_to_use = headers unless headers_to_use
144
+
145
+ # log list consists of 2 parts, loggregator and files
146
+ log_files = get("/v2/apps/#{app_guid}/instances/0/files/#{file_path}",
147
+ follow_redirects: false, expects: expected_statuses, headers: headers_to_use)
148
+ if log_files.status == 400 || log_files.status == 404
149
+ fail Errors::AdapterResourceNotFoundError,
150
+ "Invalid log file: '#{file_path}' not available for application '#{app_guid}'"
151
+ end
152
+ return log_files.body if log_files.status == 200
153
+
154
+ # status must be 302, follow to the Location
155
+ download_location = log_files.headers[:Location]
156
+ # if IBM f*cked with the download URL, fix the address
157
+ download_location.gsub!(/objectstorage.service.networklayer.com/, 'objectstorage.softlayer.net')
158
+ Excon.defaults[:ssl_verify_peer] = false unless @check_certificates
159
+
160
+ connection_params = { ssl_verify_peer: @check_certificates }
161
+ connection = Excon.new(download_location, connection_params)
162
+ downloaded_logfile_response = connection.request(method: :get, expects: expected_statuses)
163
+
164
+ if downloaded_logfile_response.status == 404
165
+ fail Errors::AdapterResourceNotFoundError,
166
+ "Invalid log file: '#{file_path}' not available for application '#{app_guid}'"
167
+ end
168
+ downloaded_logfile_response.body
169
+ end
170
+
171
+ def recent_log_messages(app_guid, filter = nil)
172
+ loggregator_recent_uri = "https://#{loggregator_endpoint}:443/recent?app=#{app_guid}"
173
+ # current log state before tailing, multipart message of protobuf objects
174
+ current_log_response = get(loggregator_recent_uri)
175
+ current_log_boundary = /boundary=(\w+)/.match(current_log_response.headers['Content-Type'])[1]
176
+ current_log = current_log_response.body
177
+
178
+ boundary_regexp = /--#{Regexp.quote(current_log_boundary)}(--)?#{CRLF}/
179
+ parts = current_log.split(boundary_regexp).collect do |chunk|
180
+ header_part = chunk.split(/#{CRLF}#{WSP}*#{CRLF}/m, 2)[0]
181
+ if header_part
182
+ headers = header_part.split(/\r\n/).map { |kv| kv }
183
+ headers.length > 1 ? headers[1] : nil
184
+ end
185
+ end.compact
186
+ # decode log messages
187
+ decoded_messages = parts.collect do |proto_message|
188
+ Message.decode(proto_message)
189
+ end.compact
190
+ return decoded_messages unless filter
191
+ # return filtered messages
192
+ decoded_messages.find_all do |msg|
193
+ filter.include?(msg.source_name)
194
+ end
195
+ end
196
+
197
+ def log_stream?(log_id)
198
+ LOGGREGATOR_TYPES.include?(log_id.to_sym) || log_id.to_sym == :all
199
+ end
200
+
201
+ def loggregator_endpoint
202
+ @endpoint_url.gsub(%r{^(\w*://)?(api)([-\.\w]+)$}i, 'loggregator\3')
203
+ end
204
+
205
+ def tail_file(app_guid, log_id, stream)
206
+ log.debug 'Tailing CF log file'
207
+ log_id = 'staging_task.log' if log_id.to_sym == Enums::ApplicationLogfileType::BUILD
208
+
209
+ # cache headers as they are bound to a request and could be lost with the next tick
210
+ headers_to_use = headers
211
+ latest_pushed_line = -1
212
+
213
+ # update every 3 seconds
214
+ @tail_file_timer = EM.add_periodic_timer(3) do
215
+ log.debug('Poll updated file tail...')
216
+ begin
217
+ latest_pushed_line = push_file_tail(app_guid, log_id, stream, latest_pushed_line, headers_to_use)
218
+ rescue Errors::AdapterResourceNotFoundError
219
+ log.debug('Logfile not found, finished tailing')
220
+ # file lost, close stream
221
+ @tail_file_timer.cancel if @tail_file_timer
222
+ stream.close
223
+ end
224
+ end
225
+ # listener to stop polling
226
+ StopListener.new(@tail_file_timer, :cancel)
227
+ end
228
+
229
+ def push_file_tail(app_guid, log_id, stream, pushed_line_idx, headers_to_use)
230
+ log.debug('Fetching file for tail response...')
231
+ entries = download_logfile_entries(app_guid, log_id, headers_to_use)
232
+ # file was shortened, close stream since we do not know where to continue
233
+ if entries.length < pushed_line_idx
234
+ log.debug('File was modified and shortened, stop tailing the file...')
235
+ stream.close
236
+ else
237
+ entries.each_with_index do |entry, index|
238
+ next if index <= pushed_line_idx
239
+ pushed_line_idx = index
240
+ stream.send_message(entry)
241
+ end
242
+ pushed_line_idx
243
+ end
244
+ end
245
+
246
+ def tail_stream(app_guid, log_id, stream)
247
+ filter = loggregator_filter(log_id)
248
+
249
+ # push current state
250
+ recent_log_messages(app_guid, filter).each { |entry| stream.send_message(construct_log_entry(entry)) }
251
+
252
+ # Now register websocket to receive the latest updates
253
+ ws = Faye::WebSocket::Client.new("wss://#{loggregator_endpoint}:443/tail/?app=#{app_guid}",
254
+ nil, headers: headers.slice('Authorization'))
255
+
256
+ ws.on :message do |event|
257
+ log.debug "CF loggregator message received: #{event}"
258
+ begin
259
+ msg = Message.decode(event.data.pack('C*'))
260
+ # notify stream to print new log line if msg type matches the applied filter
261
+ stream.send_message(construct_log_entry(msg)) if filter.nil? || filter.include?(msg.source_name)
262
+ rescue StandardError => e
263
+ log.error "Cloud Foundry log message de-serialization failed: #{e}"
264
+ end
265
+ end
266
+
267
+ ws.on :close do |event|
268
+ log.debug "Closing CF loggregator websocket: code=#{event.code}, reason=#{event.reason}"
269
+ ws = nil
270
+ # notify stream that no more update are to arrive and stream shall be closed
271
+ stream.close
272
+ end
273
+ # return listener to stop websocket
274
+ TailStopper.new(ws, :close)
275
+ end
276
+
277
+ # Message class definition, matching the Protocol Buffer definition of the Cloud Foundry loggregator.
278
+ # see also: https://github.com/cloudfoundry/loggregatorlib/blob/master/logmessage/log_message.proto
279
+ class Message < ::Protobuf::Message
280
+ class MessageType < ::Protobuf::Enum
281
+ define :OUT, 1
282
+ define :ERR, 2
283
+ end
284
+
285
+ required :bytes, :message, 1
286
+ required Logs::Message::MessageType, :message_type, 2
287
+ required :sint64, :timestamp, 3
288
+ required :string, :app_id, 4
289
+ optional :string, :source_id, 6
290
+ repeated :string, :drain_urls, 7
291
+ optional :string, :source_name, 8
292
+ end
293
+
294
+ class Envelope < ::Protobuf::Message
295
+ required :string, :routing_key, 1
296
+ required :bytes, :signature, 2
297
+ required Logs::Message, :log_message, 3
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end