nucleus 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +44 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +13 -0
- data/Gemfile +16 -0
- data/Guardfile +22 -0
- data/LICENSE +21 -0
- data/README.md +675 -0
- data/Rakefile +137 -0
- data/bin/nucleus +91 -0
- data/bin/nucleus.bat +1 -0
- data/config.ru +18 -0
- data/config/adapters/cloud_control.yml +32 -0
- data/config/adapters/cloud_foundry_v2.yml +61 -0
- data/config/adapters/heroku.yml +13 -0
- data/config/adapters/openshift_v2.yml +20 -0
- data/config/nucleus_config.rb +47 -0
- data/lib/nucleus.rb +13 -0
- data/lib/nucleus/adapter_resolver.rb +115 -0
- data/lib/nucleus/adapters/base_adapter.rb +109 -0
- data/lib/nucleus/adapters/buildpack_translator.rb +79 -0
- data/lib/nucleus/adapters/v1/cloud_control/application.rb +108 -0
- data/lib/nucleus/adapters/v1/cloud_control/authentication.rb +27 -0
- data/lib/nucleus/adapters/v1/cloud_control/buildpacks.rb +23 -0
- data/lib/nucleus/adapters/v1/cloud_control/cloud_control.rb +153 -0
- data/lib/nucleus/adapters/v1/cloud_control/data.rb +76 -0
- data/lib/nucleus/adapters/v1/cloud_control/domains.rb +68 -0
- data/lib/nucleus/adapters/v1/cloud_control/lifecycle.rb +27 -0
- data/lib/nucleus/adapters/v1/cloud_control/log_poller.rb +71 -0
- data/lib/nucleus/adapters/v1/cloud_control/logs.rb +103 -0
- data/lib/nucleus/adapters/v1/cloud_control/regions.rb +32 -0
- data/lib/nucleus/adapters/v1/cloud_control/scaling.rb +17 -0
- data/lib/nucleus/adapters/v1/cloud_control/semantic_errors.rb +31 -0
- data/lib/nucleus/adapters/v1/cloud_control/services.rb +162 -0
- data/lib/nucleus/adapters/v1/cloud_control/token.rb +17 -0
- data/lib/nucleus/adapters/v1/cloud_control/vars.rb +88 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/app_states.rb +28 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/application.rb +111 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/authentication.rb +17 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/buildpacks.rb +23 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/cloud_foundry_v2.rb +141 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/data.rb +97 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/domains.rb +149 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/lifecycle.rb +41 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/logs.rb +303 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/regions.rb +33 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/scaling.rb +15 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/semantic_errors.rb +27 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/services.rb +286 -0
- data/lib/nucleus/adapters/v1/cloud_foundry_v2/vars.rb +80 -0
- data/lib/nucleus/adapters/v1/heroku/app_states.rb +57 -0
- data/lib/nucleus/adapters/v1/heroku/application.rb +93 -0
- data/lib/nucleus/adapters/v1/heroku/authentication.rb +27 -0
- data/lib/nucleus/adapters/v1/heroku/buildpacks.rb +27 -0
- data/lib/nucleus/adapters/v1/heroku/data.rb +78 -0
- data/lib/nucleus/adapters/v1/heroku/domains.rb +43 -0
- data/lib/nucleus/adapters/v1/heroku/heroku.rb +146 -0
- data/lib/nucleus/adapters/v1/heroku/lifecycle.rb +51 -0
- data/lib/nucleus/adapters/v1/heroku/logs.rb +108 -0
- data/lib/nucleus/adapters/v1/heroku/regions.rb +42 -0
- data/lib/nucleus/adapters/v1/heroku/scaling.rb +28 -0
- data/lib/nucleus/adapters/v1/heroku/semantic_errors.rb +23 -0
- data/lib/nucleus/adapters/v1/heroku/services.rb +168 -0
- data/lib/nucleus/adapters/v1/heroku/vars.rb +65 -0
- data/lib/nucleus/adapters/v1/openshift_v2/app_states.rb +68 -0
- data/lib/nucleus/adapters/v1/openshift_v2/application.rb +108 -0
- data/lib/nucleus/adapters/v1/openshift_v2/authentication.rb +21 -0
- data/lib/nucleus/adapters/v1/openshift_v2/data.rb +96 -0
- data/lib/nucleus/adapters/v1/openshift_v2/domains.rb +37 -0
- data/lib/nucleus/adapters/v1/openshift_v2/lifecycle.rb +60 -0
- data/lib/nucleus/adapters/v1/openshift_v2/logs.rb +106 -0
- data/lib/nucleus/adapters/v1/openshift_v2/openshift_v2.rb +125 -0
- data/lib/nucleus/adapters/v1/openshift_v2/regions.rb +58 -0
- data/lib/nucleus/adapters/v1/openshift_v2/scaling.rb +39 -0
- data/lib/nucleus/adapters/v1/openshift_v2/semantic_errors.rb +40 -0
- data/lib/nucleus/adapters/v1/openshift_v2/services.rb +173 -0
- data/lib/nucleus/adapters/v1/openshift_v2/vars.rb +49 -0
- data/lib/nucleus/adapters/v1/stub_adapter.rb +464 -0
- data/lib/nucleus/core/adapter_authentication_inductor.rb +62 -0
- data/lib/nucleus/core/adapter_extensions/auth/auth_client.rb +44 -0
- data/lib/nucleus/core/adapter_extensions/auth/authentication_retry_wrapper.rb +79 -0
- data/lib/nucleus/core/adapter_extensions/auth/expiring_token_auth_client.rb +53 -0
- data/lib/nucleus/core/adapter_extensions/auth/http_basic_auth_client.rb +37 -0
- data/lib/nucleus/core/adapter_extensions/auth/o_auth2_auth_client.rb +95 -0
- data/lib/nucleus/core/adapter_extensions/auth/token_auth_client.rb +36 -0
- data/lib/nucleus/core/adapter_extensions/http_client.rb +177 -0
- data/lib/nucleus/core/adapter_extensions/http_tail_client.rb +26 -0
- data/lib/nucleus/core/adapter_extensions/tail_stopper.rb +25 -0
- data/lib/nucleus/core/common/errors/ambiguous_adapter_error.rb +7 -0
- data/lib/nucleus/core/common/errors/file_existence_error.rb +7 -0
- data/lib/nucleus/core/common/errors/startup_error.rb +12 -0
- data/lib/nucleus/core/common/exit_codes.rb +25 -0
- data/lib/nucleus/core/common/files/application_repo_sanitizer.rb +52 -0
- data/lib/nucleus/core/common/files/archive_extractor.rb +112 -0
- data/lib/nucleus/core/common/files/archiver.rb +91 -0
- data/lib/nucleus/core/common/link_generator.rb +46 -0
- data/lib/nucleus/core/common/logging/logging.rb +52 -0
- data/lib/nucleus/core/common/logging/multi_logger.rb +59 -0
- data/lib/nucleus/core/common/logging/request_log_formatter.rb +48 -0
- data/lib/nucleus/core/common/ssh_handler.rb +108 -0
- data/lib/nucleus/core/common/stream_callback.rb +27 -0
- data/lib/nucleus/core/common/thread_config_accessor.rb +85 -0
- data/lib/nucleus/core/common/url_converter.rb +28 -0
- data/lib/nucleus/core/enums/application_states.rb +26 -0
- data/lib/nucleus/core/enums/logfile_types.rb +28 -0
- data/lib/nucleus/core/error_messages.rb +127 -0
- data/lib/nucleus/core/errors/adapter_error.rb +13 -0
- data/lib/nucleus/core/errors/adapter_missing_implementation_error.rb +12 -0
- data/lib/nucleus/core/errors/adapter_request_error.rb +10 -0
- data/lib/nucleus/core/errors/adapter_resource_not_found_error.rb +10 -0
- data/lib/nucleus/core/errors/endpoint_authentication_error.rb +10 -0
- data/lib/nucleus/core/errors/platform_specific_semantic_error.rb +12 -0
- data/lib/nucleus/core/errors/platform_timeout_error.rb +10 -0
- data/lib/nucleus/core/errors/platform_unavailable_error.rb +10 -0
- data/lib/nucleus/core/errors/semantic_adapter_request_error.rb +19 -0
- data/lib/nucleus/core/errors/unknown_adapter_call_error.rb +10 -0
- data/lib/nucleus/core/file_handling/archive_converter.rb +29 -0
- data/lib/nucleus/core/file_handling/file_manager.rb +64 -0
- data/lib/nucleus/core/file_handling/git_deployer.rb +133 -0
- data/lib/nucleus/core/file_handling/git_repo_analyzer.rb +23 -0
- data/lib/nucleus/core/import/adapter_configuration.rb +53 -0
- data/lib/nucleus/core/import/vendor_parser.rb +28 -0
- data/lib/nucleus/core/import/version_detector.rb +18 -0
- data/lib/nucleus/core/models/abstract_model.rb +29 -0
- data/lib/nucleus/core/models/endpoint.rb +30 -0
- data/lib/nucleus/core/models/provider.rb +26 -0
- data/lib/nucleus/core/models/vendor.rb +22 -0
- data/lib/nucleus/ext/kernel.rb +5 -0
- data/lib/nucleus/ext/regexp.rb +49 -0
- data/lib/nucleus/os.rb +15 -0
- data/lib/nucleus/root_dir.rb +13 -0
- data/lib/nucleus/scripts/finalize.rb +8 -0
- data/lib/nucleus/scripts/initialize.rb +9 -0
- data/lib/nucleus/scripts/initialize_config_defaults.rb +26 -0
- data/lib/nucleus/scripts/load.rb +17 -0
- data/lib/nucleus/scripts/load_dependencies.rb +43 -0
- data/lib/nucleus/scripts/setup_config.rb +28 -0
- data/lib/nucleus/scripts/shutdown.rb +11 -0
- data/lib/nucleus/version.rb +3 -0
- data/nucleus.gemspec +88 -0
- data/public/robots.txt +2 -0
- data/public/swagger-ui/css/reset.css +125 -0
- data/public/swagger-ui/css/screen.css +1224 -0
- data/public/swagger-ui/images/apple-touch-icon-114x114.png +0 -0
- data/public/swagger-ui/images/apple-touch-icon-120x120.png +0 -0
- data/public/swagger-ui/images/apple-touch-icon-144x144.png +0 -0
- data/public/swagger-ui/images/apple-touch-icon-152x152.png +0 -0
- data/public/swagger-ui/images/apple-touch-icon-57x57.png +0 -0
- data/public/swagger-ui/images/apple-touch-icon-60x60.png +0 -0
- data/public/swagger-ui/images/apple-touch-icon-72x72.png +0 -0
- data/public/swagger-ui/images/apple-touch-icon-76x76.png +0 -0
- data/public/swagger-ui/images/explorer_icons.png +0 -0
- data/public/swagger-ui/images/favicon-128.png +0 -0
- data/public/swagger-ui/images/favicon-16x16.png +0 -0
- data/public/swagger-ui/images/favicon-196x196.png +0 -0
- data/public/swagger-ui/images/favicon-32x32.png +0 -0
- data/public/swagger-ui/images/favicon-96x96.png +0 -0
- data/public/swagger-ui/images/favicon.ico +0 -0
- data/public/swagger-ui/images/logo_small.png +0 -0
- data/public/swagger-ui/images/mstile-144x144.png +0 -0
- data/public/swagger-ui/images/mstile-150x150.png +0 -0
- data/public/swagger-ui/images/mstile-310x150.png +0 -0
- data/public/swagger-ui/images/mstile-310x310.png +0 -0
- data/public/swagger-ui/images/mstile-70x70.png +0 -0
- data/public/swagger-ui/images/pet_store_api.png +0 -0
- data/public/swagger-ui/images/throbber.gif +0 -0
- data/public/swagger-ui/images/wordnik_api.png +0 -0
- data/public/swagger-ui/index.html +107 -0
- data/public/swagger-ui/lib/backbone-min.js +38 -0
- data/public/swagger-ui/lib/handlebars-1.0.0.js +2278 -0
- data/public/swagger-ui/lib/highlight.7.3.pack.js +1 -0
- data/public/swagger-ui/lib/jquery-1.8.0.min.js +2 -0
- data/public/swagger-ui/lib/jquery.ba-bbq.min.js +18 -0
- data/public/swagger-ui/lib/jquery.slideto.min.js +1 -0
- data/public/swagger-ui/lib/jquery.wiggle.min.js +8 -0
- data/public/swagger-ui/lib/shred.bundle.js +2765 -0
- data/public/swagger-ui/lib/shred/content.js +193 -0
- data/public/swagger-ui/lib/swagger-oauth.js +211 -0
- data/public/swagger-ui/lib/swagger.js +1653 -0
- data/public/swagger-ui/lib/underscore-min.js +32 -0
- data/public/swagger-ui/o2c.html +15 -0
- data/public/swagger-ui/redirect.html +14 -0
- data/public/swagger-ui/swagger-ui.js +2324 -0
- data/public/swagger-ui/swagger-ui.min.js +1 -0
- data/schemas/api.adapter.schema.yml +31 -0
- data/schemas/api.requirements.schema.yml +17 -0
- data/spec/factories/models.rb +61 -0
- data/spec/integration/api/auth_spec.rb +58 -0
- data/spec/integration/api/endpoints_spec.rb +167 -0
- data/spec/integration/api/errors_spec.rb +47 -0
- data/spec/integration/api/providers_spec.rb +157 -0
- data/spec/integration/api/swagger_schema_spec.rb +64 -0
- data/spec/integration/api/vendors_spec.rb +45 -0
- data/spec/integration/integration_spec_helper.rb +27 -0
- data/spec/integration/test_data_generator.rb +55 -0
- data/spec/nucleus_git_key.pem +51 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/shared_example_request_types.rb +99 -0
- data/spec/test_suites.rake +31 -0
- data/spec/unit/adapters/archive_converter_spec.rb +25 -0
- data/spec/unit/adapters/file_manager_spec.rb +93 -0
- data/spec/unit/adapters/git_deployer_spec.rb +262 -0
- data/spec/unit/adapters/v1/stub_spec.rb +14 -0
- data/spec/unit/common/helpers/auth_helper_spec.rb +73 -0
- data/spec/unit/common/oauth2_auth_client_spec.rb +108 -0
- data/spec/unit/common/regexp_spec.rb +33 -0
- data/spec/unit/common/request_log_formatter_spec.rb +108 -0
- data/spec/unit/common/thread_config_accessor_spec.rb +97 -0
- data/spec/unit/models/endpoint_spec.rb +83 -0
- data/spec/unit/models/provider_spec.rb +102 -0
- data/spec/unit/models/vendor_spec.rb +100 -0
- data/spec/unit/schemas/adapter_schema_spec.rb +16 -0
- data/spec/unit/schemas/adapter_validation_spec.rb +56 -0
- data/spec/unit/schemas/requirements_schema_spec.rb +16 -0
- data/spec/unit/unit_spec_helper.rb +11 -0
- data/tasks/compatibility.rake +113 -0
- data/tasks/evaluation.rake +162 -0
- data/wiki/adapter_tests.md +99 -0
- data/wiki/implement_new_adapter.md +155 -0
- 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
|