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