nucleus 0.1.0 → 0.2.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +18 -4
  4. data/README.md +28 -40
  5. data/Rakefile +137 -137
  6. data/config/nucleus_config.rb +0 -4
  7. data/lib/nucleus/adapter_resolver.rb +115 -115
  8. data/lib/nucleus/adapters/buildpack_translator.rb +79 -79
  9. data/lib/nucleus/adapters/v1/cloud_control/application.rb +108 -108
  10. data/lib/nucleus/adapters/v1/cloud_control/authentication.rb +27 -27
  11. data/lib/nucleus/adapters/v1/cloud_control/cloud_control.rb +153 -153
  12. data/lib/nucleus/adapters/v1/cloud_control/domains.rb +68 -68
  13. data/lib/nucleus/adapters/v1/cloud_control/logs.rb +103 -103
  14. data/lib/nucleus/adapters/v1/cloud_control/vars.rb +88 -88
  15. data/lib/nucleus/adapters/v1/cloud_foundry_v2/domains.rb +149 -149
  16. data/lib/nucleus/adapters/v1/cloud_foundry_v2/logs.rb +303 -303
  17. data/lib/nucleus/adapters/v1/cloud_foundry_v2/services.rb +286 -286
  18. data/lib/nucleus/adapters/v1/heroku/heroku.rb +2 -2
  19. data/lib/nucleus/adapters/v1/heroku/logs.rb +108 -108
  20. data/lib/nucleus/core/adapter_authentication_inductor.rb +0 -2
  21. data/lib/nucleus/core/adapter_extensions/auth/http_basic_auth_client.rb +37 -37
  22. data/lib/nucleus/core/adapter_extensions/http_client.rb +177 -177
  23. data/lib/nucleus/core/common/files/archive_extractor.rb +112 -112
  24. data/lib/nucleus/core/common/files/archiver.rb +91 -91
  25. data/lib/nucleus/core/common/logging/request_log_formatter.rb +48 -48
  26. data/lib/nucleus/core/error_messages.rb +127 -127
  27. data/lib/nucleus/core/models/abstract_model.rb +29 -29
  28. data/lib/nucleus/scripts/load_dependencies.rb +0 -1
  29. data/lib/nucleus/scripts/setup_config.rb +28 -28
  30. data/lib/nucleus/version.rb +3 -3
  31. data/nucleus.gemspec +10 -12
  32. data/spec/factories/models.rb +63 -61
  33. data/spec/integration/api/auth_spec.rb +58 -58
  34. data/spec/test_suites.rake +31 -31
  35. data/spec/unit/common/helpers/auth_helper_spec.rb +73 -73
  36. data/spec/unit/common/oauth2_auth_client_spec.rb +1 -1
  37. data/tasks/compatibility.rake +113 -113
  38. data/tasks/evaluation.rake +162 -162
  39. metadata +16 -30
@@ -127,7 +127,6 @@ module Nucleus
127
127
  latest_version_id = release[:id]
128
128
  end
129
129
  end
130
- latest_version_id
131
130
  else
132
131
  latest_version = 0
133
132
  latest_version_id = nil
@@ -137,8 +136,9 @@ module Nucleus
137
136
  latest_version_id = dyno[:release][:id]
138
137
  end
139
138
  end
140
- latest_version_id
141
139
  end
140
+
141
+ latest_version_id
142
142
  end
143
143
  end
144
144
  end
@@ -1,108 +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
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".freeze
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
@@ -15,8 +15,6 @@ module Nucleus
15
15
  end
16
16
  end
17
17
 
18
- private
19
-
20
18
  # Patch the actual method that is defined in an API version stub.
21
19
  # The method shall than be able to update the authentication token if the initial authentication expired.<br>
22
20
  # Only major authentication issues, e.g. if the credentials are repeatedly rejected,
@@ -1,37 +1,37 @@
1
- module Nucleus
2
- module Adapters
3
- # Implementation of the AuthClient that works with the HTTP basic authentication.
4
- class HttpBasicAuthClient < AuthClient
5
- # Create a new instance of an {HttpBasicAuthClient}.
6
- # @param [Boolean] check_certificates true if SSL certificates are to be validated,
7
- # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
8
- # @yield [verify_ssl, username, password] Auth credentials verification block,
9
- # must check if the combination of username and password is accepted by the endpoint.
10
- # @yieldparam [Hash<String,String>] headers headers for an HTTP request,
11
- # including the authentication header to be tested
12
- # @yieldreturn [Boolean] true if the authentication was verified to be ok,
13
- # false if an error occurred, e.g. with bad credentials
14
- def initialize(check_certificates = true, &verification)
15
- @verification = verification
16
- super(check_certificates)
17
- end
18
-
19
- # @see AuthClient#authenticate
20
- def authenticate(username, password)
21
- packed_credentials = ["#{username}:#{password}"].pack('m*').gsub(/\n/, '')
22
- valid = @verification.call(verify_ssl, 'Authorization' => "Basic #{packed_credentials}")
23
- fail Errors::EndpointAuthenticationError, 'Authentication failed, credentials seem to be invalid' unless valid
24
- # verification passed, credentials are valid
25
- @packed_credentials = packed_credentials
26
- self
27
- end
28
-
29
- # @see AuthClient#auth_header
30
- def auth_header
31
- fail Errors::EndpointAuthenticationError,
32
- 'Authentication client was not authenticated yet' unless @packed_credentials
33
- { 'Authorization' => "Basic #{@packed_credentials}" }
34
- end
35
- end
36
- end
37
- end
1
+ module Nucleus
2
+ module Adapters
3
+ # Implementation of the AuthClient that works with the HTTP basic authentication.
4
+ class HttpBasicAuthClient < AuthClient
5
+ # Create a new instance of an {HttpBasicAuthClient}.
6
+ # @param [Boolean] check_certificates true if SSL certificates are to be validated,
7
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
8
+ # @yield [verify_ssl, username, password] Auth credentials verification block,
9
+ # must check if the combination of username and password is accepted by the endpoint.
10
+ # @yieldparam [Hash<String,String>] headers headers for an HTTP request,
11
+ # including the authentication header to be tested
12
+ # @yieldreturn [Boolean] true if the authentication was verified to be ok,
13
+ # false if an error occurred, e.g. with bad credentials
14
+ def initialize(check_certificates = true, &verification)
15
+ @verification = verification
16
+ super(check_certificates)
17
+ end
18
+
19
+ # @see AuthClient#authenticate
20
+ def authenticate(username, password)
21
+ packed_credentials = ["#{username}:#{password}"].pack('m*').delete("\n")
22
+ valid = @verification.call(verify_ssl, 'Authorization' => "Basic #{packed_credentials}")
23
+ fail Errors::EndpointAuthenticationError, 'Authentication failed, credentials seem to be invalid' unless valid
24
+ # verification passed, credentials are valid
25
+ @packed_credentials = packed_credentials
26
+ self
27
+ end
28
+
29
+ # @see AuthClient#auth_header
30
+ def auth_header
31
+ fail Errors::EndpointAuthenticationError,
32
+ 'Authentication client was not authenticated yet' unless @packed_credentials
33
+ { 'Authorization' => "Basic #{@packed_credentials}" }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,177 +1,177 @@
1
- module Nucleus
2
- module Adapters
3
- module HttpClient
4
- # Executes a HEAD request to the given URL.
5
- #
6
- # @param [String] path path to add to the endpoint URL
7
- # @param [Hash] params options to call the post request with
8
- # @option params [Array<int>] :expects ([200]) http status code that is expected
9
- # @option params [Hash] :headers request headers to use with the request
10
- # @option params [Boolean] :native_call if true the request is a native API call and shall return the
11
- # unprocessed response
12
- # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
13
- def head(path, params = {})
14
- execute_request(:head, [200], path, params, params.delete(:native_call) { false })
15
- end
16
-
17
- # Executes a GET request to the given URL.
18
- #
19
- # @param [String] path path to add to the endpoint URL
20
- # @param [Hash] params options to call the post request with
21
- # @option params [Array<int>] :expects ([200]) http status code that is expected
22
- # @option params [Hash] :headers request headers to use with the request
23
- # @option params [Boolean] :native_call if true the request is a native API call and shall return the
24
- # unprocessed response
25
- # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
26
- def get(path, params = {})
27
- execute_request(:get, [200], path, params, params.delete(:native_call) { false })
28
- end
29
-
30
- # Executes a POST request to the given URL.
31
- #
32
- # @param [String] path path to add to the endpoint URL
33
- # @param [Hash] params options to call the post request with
34
- # @option params [Array<int>] :expects ([200,201]) http status code that is expected
35
- # @option params [Hash] :body request body, will be converted to json format
36
- # @option params [Hash] :headers request headers to use with the request
37
- # @option params [Boolean] :native_call if true the request is a native API call and shall return the
38
- # unprocessed response
39
- # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
40
- def post(path, params = {})
41
- execute_request(:post, [200, 201], path, params, params.delete(:native_call) { false })
42
- end
43
-
44
- # Executes a PATCH request to the given URL.
45
- #
46
- # @param [String] path path to add to the endpoint URL
47
- # @param [Hash] params options to call the post request with
48
- # @option params [Array<int>] :expects ([200,201]) http status code that is expected
49
- # @option params [Hash] :body request body, will be converted to json format
50
- # @option params [Hash] :headers request headers to use with the request
51
- # @option params [Boolean] :native_call if true the request is a native API call and shall return the
52
- # unprocessed response
53
- # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
54
- def patch(path, params = {})
55
- execute_request(:patch, [200, 201], path, params, params.delete(:native_call) { false })
56
- end
57
-
58
- # Executes a PUT request to the given URL.
59
- #
60
- # @param [String] path path to add to the endpoint URL
61
- # @param [Hash] params options to call the post request with
62
- # @option params [Array<int>] :expects ([200,201]) http status code that is expected
63
- # @option params [Hash] :body request body, will be converted to json format
64
- # @option params [Hash] :headers request headers to use with the request
65
- # @option params [Boolean] :native_call if true the request is a native API call and shall return the
66
- # unprocessed response
67
- # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
68
- def put(path, params = {})
69
- execute_request(:put, [200, 201], path, params, params.delete(:native_call) { false })
70
- end
71
-
72
- # Executes a DELETE request to the given URL.
73
- #
74
- # @param [String] path path to add to the endpoint URL
75
- # @param [Hash] params options to call the post request with
76
- # @option params [Array<int>] :expects ([200,204]) http status code that is expected
77
- # @option params [Hash] :headers request headers to use with the request
78
- # @option params [Boolean] :native_call if true the request is a native API call and shall return the
79
- # unprocessed response
80
- # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
81
- def delete(path, params = {})
82
- execute_request(:delete, [200, 204], path, params, params.delete(:native_call) { false })
83
- end
84
-
85
- private
86
-
87
- def execute_request(method, default_expect, path, params, native_call = false)
88
- params[:expects] = default_expect unless params.key? :expects
89
- params[:method] = method
90
-
91
- url = Regexp::PERFECT_URL_PATTERN =~ path ? path : to_url(path)
92
- response = Excon.new(url, excon_connection_params(params)).request(add_common_request_params(params))
93
- # we never want the JSON string, but always the hash representation
94
- response.body = hash_of(response.body)
95
- response
96
- rescue Excon::Errors::HTTPStatusError => e
97
- handle_execute_request_error(e, url, native_call)
98
- end
99
-
100
- def handle_execute_request_error(e, url, native_call)
101
- log.debug 'ERROR, Excon could not execute the request.'
102
- # transform json response to Hash object
103
- e.response.body = hash_of(e.response.body)
104
-
105
- # if this is a native API call, do not further process the error
106
- return e.response if native_call
107
-
108
- # fail with adapter specific error handling
109
- handle_error(e.response) if respond_to?(:handle_error)
110
- fallback_error_handling(e, url)
111
- end
112
-
113
- def fallback_error_handling(e, url)
114
- error_status = e.response.status
115
- # arriving here, error could not be processed --> use fallback errors
116
- if e.is_a? Excon::Errors::ServerError
117
- fail Errors::UnknownAdapterCallError, e.message
118
- elsif error_status == 404
119
- log.error("Resource not found (404) at '#{url}', indicating an adapter issue")
120
- fail Errors::UnknownAdapterCallError, 'Resource not found, probably the adapter must be updated'
121
- elsif error_status == 401
122
- fail Errors::EndpointAuthenticationError,
123
- 'Auth. failed, probably cache is outdated or permissions were revoked?'
124
- else
125
- log.error("Fallback error handling (#{error_status}) at '#{url}', indicating an adapter issue")
126
- fail Errors::UnknownAdapterCallError, e.message
127
- end
128
- end
129
-
130
- def to_url(path)
131
- # insert missing slash, prevent double slashes
132
- return "#{@endpoint_url}/#{path}" unless @endpoint_url.end_with?('/') || path.start_with?('/')
133
- "#{@endpoint_url}#{path}"
134
- end
135
-
136
- def excon_connection_params(params)
137
- middleware = Excon.defaults[:middlewares].dup
138
-
139
- if params[:follow_redirects] == false
140
- middleware = [Excon::Middleware::ResponseParser, Excon::Middleware::Decompress].push(*middleware).uniq
141
- else
142
- middleware = [Excon::Middleware::ResponseParser, Excon::Middleware::RedirectFollower,
143
- Excon::Middleware::Decompress].push(*middleware).uniq
144
- end
145
- { middlewares: middleware, ssl_verify_peer: @check_certificates }
146
- end
147
-
148
- def hash_of(message_body)
149
- return {} if message_body.nil? || message_body.empty?
150
- begin
151
- return Oj.load(message_body, symbol_keys: true)
152
- rescue Oj::Error
153
- # parsing failed, content probably is no valid JSON content
154
- message_body
155
- end
156
- end
157
-
158
- def add_common_request_params(params)
159
- common_params = { connection_timeout: 610, write_timeout: 600, read_timeout: 600 }
160
- # allow to follow redirects in the APIs
161
- allowed_status_codes = params.key?(:expects) ? [*params[:expects]] : []
162
- unless params[:follow_redirects] == false
163
- allowed_status_codes.push(*[301, 302, 303, 307, 308])
164
- end
165
-
166
- params[:expects] = allowed_status_codes.uniq
167
- # use default or customized headers
168
- params[:headers] = headers unless params[:headers]
169
- # specify encoding if not done yet: use only gzip since deflate does cause issues with VCR cassettes in tests
170
- params[:headers]['Accept-Encoding'] = 'gzip' unless params[:headers].key? 'Accept-Encoding'
171
- params[:body] = params[:body].to_json if params.key? :body
172
- # merge and return
173
- common_params.merge params
174
- end
175
- end
176
- end
177
- end
1
+ module Nucleus
2
+ module Adapters
3
+ module HttpClient
4
+ # Executes a HEAD request to the given URL.
5
+ #
6
+ # @param [String] path path to add to the endpoint URL
7
+ # @param [Hash] params options to call the post request with
8
+ # @option params [Array<int>] :expects ([200]) http status code that is expected
9
+ # @option params [Hash] :headers request headers to use with the request
10
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
11
+ # unprocessed response
12
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
13
+ def head(path, params = {})
14
+ execute_request(:head, [200], path, params, params.delete(:native_call) { false })
15
+ end
16
+
17
+ # Executes a GET request to the given URL.
18
+ #
19
+ # @param [String] path path to add to the endpoint URL
20
+ # @param [Hash] params options to call the post request with
21
+ # @option params [Array<int>] :expects ([200]) http status code that is expected
22
+ # @option params [Hash] :headers request headers to use with the request
23
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
24
+ # unprocessed response
25
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
26
+ def get(path, params = {})
27
+ execute_request(:get, [200], path, params, params.delete(:native_call) { false })
28
+ end
29
+
30
+ # Executes a POST request to the given URL.
31
+ #
32
+ # @param [String] path path to add to the endpoint URL
33
+ # @param [Hash] params options to call the post request with
34
+ # @option params [Array<int>] :expects ([200,201]) http status code that is expected
35
+ # @option params [Hash] :body request body, will be converted to json format
36
+ # @option params [Hash] :headers request headers to use with the request
37
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
38
+ # unprocessed response
39
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
40
+ def post(path, params = {})
41
+ execute_request(:post, [200, 201], path, params, params.delete(:native_call) { false })
42
+ end
43
+
44
+ # Executes a PATCH request to the given URL.
45
+ #
46
+ # @param [String] path path to add to the endpoint URL
47
+ # @param [Hash] params options to call the post request with
48
+ # @option params [Array<int>] :expects ([200,201]) http status code that is expected
49
+ # @option params [Hash] :body request body, will be converted to json format
50
+ # @option params [Hash] :headers request headers to use with the request
51
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
52
+ # unprocessed response
53
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
54
+ def patch(path, params = {})
55
+ execute_request(:patch, [200, 201], path, params, params.delete(:native_call) { false })
56
+ end
57
+
58
+ # Executes a PUT request to the given URL.
59
+ #
60
+ # @param [String] path path to add to the endpoint URL
61
+ # @param [Hash] params options to call the post request with
62
+ # @option params [Array<int>] :expects ([200,201]) http status code that is expected
63
+ # @option params [Hash] :body request body, will be converted to json format
64
+ # @option params [Hash] :headers request headers to use with the request
65
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
66
+ # unprocessed response
67
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
68
+ def put(path, params = {})
69
+ execute_request(:put, [200, 201], path, params, params.delete(:native_call) { false })
70
+ end
71
+
72
+ # Executes a DELETE request to the given URL.
73
+ #
74
+ # @param [String] path path to add to the endpoint URL
75
+ # @param [Hash] params options to call the post request with
76
+ # @option params [Array<int>] :expects ([200,204]) http status code that is expected
77
+ # @option params [Hash] :headers request headers to use with the request
78
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
79
+ # unprocessed response
80
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
81
+ def delete(path, params = {})
82
+ execute_request(:delete, [200, 204], path, params, params.delete(:native_call) { false })
83
+ end
84
+
85
+ private
86
+
87
+ def execute_request(method, default_expect, path, params, native_call = false)
88
+ params[:expects] = default_expect unless params.key? :expects
89
+ params[:method] = method
90
+
91
+ url = Regexp::PERFECT_URL_PATTERN =~ path ? path : to_url(path)
92
+ response = Excon.new(url, excon_connection_params(params)).request(add_common_request_params(params))
93
+ # we never want the JSON string, but always the hash representation
94
+ response.body = hash_of(response.body)
95
+ response
96
+ rescue Excon::Errors::HTTPStatusError => e
97
+ handle_execute_request_error(e, url, native_call)
98
+ end
99
+
100
+ def handle_execute_request_error(e, url, native_call)
101
+ log.debug 'ERROR, Excon could not execute the request.'
102
+ # transform json response to Hash object
103
+ e.response.body = hash_of(e.response.body)
104
+
105
+ # if this is a native API call, do not further process the error
106
+ return e.response if native_call
107
+
108
+ # fail with adapter specific error handling
109
+ handle_error(e.response) if respond_to?(:handle_error)
110
+ fallback_error_handling(e, url)
111
+ end
112
+
113
+ def fallback_error_handling(e, url)
114
+ error_status = e.response.status
115
+ # arriving here, error could not be processed --> use fallback errors
116
+ if e.is_a? Excon::Errors::ServerError
117
+ fail Errors::UnknownAdapterCallError, e.message
118
+ elsif error_status == 404
119
+ log.error("Resource not found (404) at '#{url}', indicating an adapter issue")
120
+ fail Errors::UnknownAdapterCallError, 'Resource not found, probably the adapter must be updated'
121
+ elsif error_status == 401
122
+ fail Errors::EndpointAuthenticationError,
123
+ 'Auth. failed, probably cache is outdated or permissions were revoked?'
124
+ else
125
+ log.error("Fallback error handling (#{error_status}) at '#{url}', indicating an adapter issue")
126
+ fail Errors::UnknownAdapterCallError, e.message
127
+ end
128
+ end
129
+
130
+ def to_url(path)
131
+ # insert missing slash, prevent double slashes
132
+ return "#{@endpoint_url}/#{path}" unless @endpoint_url.end_with?('/') || path.start_with?('/')
133
+ "#{@endpoint_url}#{path}"
134
+ end
135
+
136
+ def excon_connection_params(params)
137
+ middleware = Excon.defaults[:middlewares].dup
138
+
139
+ middleware = if params[:follow_redirects] == false
140
+ [Excon::Middleware::ResponseParser, Excon::Middleware::Decompress].push(*middleware).uniq
141
+ else
142
+ [Excon::Middleware::ResponseParser, Excon::Middleware::RedirectFollower,
143
+ Excon::Middleware::Decompress].push(*middleware).uniq
144
+ end
145
+ { middlewares: middleware, ssl_verify_peer: @check_certificates }
146
+ end
147
+
148
+ def hash_of(message_body)
149
+ return {} if message_body.nil? || message_body.empty?
150
+ begin
151
+ return Oj.load(message_body, symbol_keys: true)
152
+ rescue Oj::Error
153
+ # parsing failed, content probably is no valid JSON content
154
+ message_body
155
+ end
156
+ end
157
+
158
+ def add_common_request_params(params)
159
+ common_params = { connection_timeout: 610, write_timeout: 600, read_timeout: 600 }
160
+ # allow to follow redirects in the APIs
161
+ allowed_status_codes = params.key?(:expects) ? [*params[:expects]] : []
162
+ unless params[:follow_redirects] == false
163
+ allowed_status_codes.push(*[301, 302, 303, 307, 308])
164
+ end
165
+
166
+ params[:expects] = allowed_status_codes.uniq
167
+ # use default or customized headers
168
+ params[:headers] = headers unless params[:headers]
169
+ # specify encoding if not done yet: use only gzip since deflate does cause issues with VCR cassettes in tests
170
+ params[:headers]['Accept-Encoding'] = 'gzip' unless params[:headers].key? 'Accept-Encoding'
171
+ params[:body] = params[:body].to_json if params.key? :body
172
+ # merge and return
173
+ common_params.merge params
174
+ end
175
+ end
176
+ end
177
+ end