nucleus 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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