nonnative 1.107.0 → 1.108.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 +4 -4
- data/.circleci/config.yml +8 -6
- data/.rubocop.yml +3 -0
- data/AGENTS.md +248 -0
- data/CHANGELOG.md +104 -0
- data/Gemfile.lock +53 -51
- data/README.md +51 -32
- data/lib/nonnative/close_all_socket_pair.rb +14 -0
- data/lib/nonnative/configuration.rb +67 -0
- data/lib/nonnative/configuration_process.rb +14 -0
- data/lib/nonnative/configuration_proxy.rb +28 -0
- data/lib/nonnative/configuration_runner.rb +44 -0
- data/lib/nonnative/configuration_server.rb +12 -0
- data/lib/nonnative/configuration_service.rb +9 -0
- data/lib/nonnative/delay_socket_pair.rb +15 -0
- data/lib/nonnative/error.rb +7 -0
- data/lib/nonnative/fault_injection_proxy.rb +63 -0
- data/lib/nonnative/go_command.rb +34 -0
- data/lib/nonnative/grpc_server.rb +30 -0
- data/lib/nonnative/header.rb +44 -0
- data/lib/nonnative/http_client.rb +45 -0
- data/lib/nonnative/http_proxy_server.rb +62 -1
- data/lib/nonnative/http_server.rb +40 -0
- data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
- data/lib/nonnative/no_proxy.rb +35 -0
- data/lib/nonnative/not_found_error.rb +7 -0
- data/lib/nonnative/observability.rb +44 -0
- data/lib/nonnative/pool.rb +50 -0
- data/lib/nonnative/port.rb +26 -0
- data/lib/nonnative/process.rb +29 -0
- data/lib/nonnative/proxy.rb +24 -0
- data/lib/nonnative/proxy_factory.rb +16 -0
- data/lib/nonnative/runner.rb +30 -0
- data/lib/nonnative/server.rb +28 -0
- data/lib/nonnative/service.rb +16 -0
- data/lib/nonnative/socket_pair.rb +46 -0
- data/lib/nonnative/socket_pair_factory.rb +21 -0
- data/lib/nonnative/start_error.rb +5 -0
- data/lib/nonnative/stop_error.rb +5 -0
- data/lib/nonnative/timeout.rb +20 -0
- data/lib/nonnative/version.rb +4 -1
- data/lib/nonnative.rb +128 -0
- metadata +3 -2
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# gRPC server runner implemented using {GRPC::RpcServer}.
|
|
5
|
+
#
|
|
6
|
+
# This is a convenience server implementation for running a gRPC service in-process under
|
|
7
|
+
# Nonnative's server lifecycle. It binds to the configured proxy `host`/`port` and is started/stopped
|
|
8
|
+
# by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
|
|
9
|
+
#
|
|
10
|
+
# Important note about logging: the `grpc` gem uses a global logger. This implementation sets
|
|
11
|
+
# `GRPC.logger` to write to the configured `service.log`, and whichever gRPC server is initialized
|
|
12
|
+
# first "wins" that global logger.
|
|
13
|
+
#
|
|
14
|
+
# @see Nonnative::Server
|
|
4
15
|
class GRPCServer < Nonnative::Server
|
|
16
|
+
# Creates a gRPC server and registers the provided service handler.
|
|
17
|
+
#
|
|
18
|
+
# @param svc [Object] a gRPC service implementation (typically a `...::Service` subclass instance)
|
|
19
|
+
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
5
20
|
def initialize(svc, service)
|
|
6
21
|
@server = GRPC::RpcServer.new
|
|
7
22
|
server.handle(svc)
|
|
@@ -16,21 +31,36 @@ module Nonnative
|
|
|
16
31
|
|
|
17
32
|
protected
|
|
18
33
|
|
|
34
|
+
# Binds the gRPC server and begins serving requests.
|
|
35
|
+
#
|
|
36
|
+
# The server binds to the proxy host/port so that enabling a proxy results in traffic and readiness
|
|
37
|
+
# checks consistently targeting the proxy endpoint.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
19
40
|
def perform_start
|
|
20
41
|
server.add_http2_port("#{proxy.host}:#{proxy.port}", :this_port_is_insecure)
|
|
21
42
|
server.run
|
|
22
43
|
end
|
|
23
44
|
|
|
45
|
+
# Stops the gRPC server.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
24
48
|
def perform_stop
|
|
25
49
|
server.stop
|
|
26
50
|
end
|
|
27
51
|
|
|
52
|
+
# Waits until the gRPC server reports it is running, or the configured timeout elapses.
|
|
53
|
+
#
|
|
54
|
+
# @return [Object, false] the last evaluated expression from the timeout block, or `false` on timeout
|
|
28
55
|
def wait_start
|
|
29
56
|
timeout.perform do
|
|
30
57
|
super until server.running?
|
|
31
58
|
end
|
|
32
59
|
end
|
|
33
60
|
|
|
61
|
+
# Waits until the gRPC server reports it has stopped, or the configured timeout elapses.
|
|
62
|
+
#
|
|
63
|
+
# @return [Object, false] the last evaluated expression from the timeout block, or `false` on timeout
|
|
34
64
|
def wait_stop
|
|
35
65
|
timeout.perform do
|
|
36
66
|
super until server.stopped?
|
data/lib/nonnative/header.rb
CHANGED
|
@@ -1,20 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Helper utilities for building request headers for HTTP and gRPC clients.
|
|
5
|
+
#
|
|
6
|
+
# This class returns Ruby hashes suitable for passing into client libraries (for example
|
|
7
|
+
# RestClient for HTTP or gRPC call metadata).
|
|
8
|
+
#
|
|
9
|
+
# @example HTTP user-agent (RestClient style)
|
|
10
|
+
# headers = Nonnative::Header.http_user_agent('my-client/1.0')
|
|
11
|
+
# # => { user_agent: "my-client/1.0" }
|
|
12
|
+
#
|
|
13
|
+
# @example gRPC user-agent (metadata)
|
|
14
|
+
# metadata = Nonnative::Header.grpc_user_agent('my-client/1.0')
|
|
15
|
+
# # => { "grpc.primary_user_agent" => "my-client/1.0" }
|
|
16
|
+
#
|
|
17
|
+
# @example Basic auth header
|
|
18
|
+
# headers = Nonnative::Header.auth_basic('user:pass')
|
|
19
|
+
# # => { authorization: "Basic dXNlcjpwYXNz" }
|
|
20
|
+
#
|
|
21
|
+
# @example Bearer auth header
|
|
22
|
+
# headers = Nonnative::Header.auth_bearer('token')
|
|
23
|
+
# # => { authorization: "Bearer token" }
|
|
24
|
+
#
|
|
25
|
+
# @see https://github.com/rest-client/rest-client RestClient
|
|
26
|
+
# @see https://grpc.io/docs/guides/concepts/ gRPC concepts (metadata)
|
|
4
27
|
class Header
|
|
5
28
|
class << self
|
|
29
|
+
# Builds an HTTP `User-Agent` header hash.
|
|
30
|
+
#
|
|
31
|
+
# This uses the key style commonly used by RestClient (`user_agent:`).
|
|
32
|
+
#
|
|
33
|
+
# @param user_agent [String] user agent value
|
|
34
|
+
# @return [Hash{Symbol=>String}] header hash containing the user agent
|
|
6
35
|
def http_user_agent(user_agent)
|
|
7
36
|
{ user_agent: }
|
|
8
37
|
end
|
|
9
38
|
|
|
39
|
+
# Builds gRPC metadata for setting the primary user agent.
|
|
40
|
+
#
|
|
41
|
+
# @param user_agent [String] user agent value
|
|
42
|
+
# @return [Hash{String=>String}] gRPC metadata hash
|
|
10
43
|
def grpc_user_agent(user_agent)
|
|
11
44
|
{ 'grpc.primary_user_agent' => user_agent }
|
|
12
45
|
end
|
|
13
46
|
|
|
47
|
+
# Builds an HTTP Basic Authorization header.
|
|
48
|
+
#
|
|
49
|
+
# The credentials are base64-encoded using strict encoding. The `credentials` string should
|
|
50
|
+
# typically be in the form `"username:password"`.
|
|
51
|
+
#
|
|
52
|
+
# @param credentials [String] basic auth credentials in `"user:pass"` form
|
|
53
|
+
# @return [Hash{Symbol=>String}] header hash containing the Authorization header
|
|
14
54
|
def auth_basic(credentials)
|
|
15
55
|
{ authorization: "Basic #{Base64.strict_encode64(credentials)}" }
|
|
16
56
|
end
|
|
17
57
|
|
|
58
|
+
# Builds an HTTP Bearer Authorization header.
|
|
59
|
+
#
|
|
60
|
+
# @param token [String] bearer token
|
|
61
|
+
# @return [Hash{Symbol=>String}] header hash containing the Authorization header
|
|
18
62
|
def auth_bearer(token)
|
|
19
63
|
{ authorization: "Bearer #{token}" }
|
|
20
64
|
end
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Minimal RestClient-based HTTP client with consistent exception handling.
|
|
5
|
+
#
|
|
6
|
+
# This class is intended to be subclassed by higher-level clients (for example
|
|
7
|
+
# {Nonnative::Observability}). It provides protected helpers for common HTTP verbs and a retry
|
|
8
|
+
# wrapper.
|
|
9
|
+
#
|
|
10
|
+
# Error handling behavior:
|
|
11
|
+
# - Timeouts and broken connections (see {#initialize}) are re-raised so callers can handle them explicitly.
|
|
12
|
+
# - Other `RestClient::Exception` errors return the underlying `response` object.
|
|
13
|
+
#
|
|
14
|
+
# @see Nonnative::Observability
|
|
4
15
|
class HTTPClient
|
|
16
|
+
# @param host [String] base URL used to build request URLs (e.g. `"http://127.0.0.1:8080"`)
|
|
5
17
|
def initialize(host)
|
|
6
18
|
@host = host
|
|
7
19
|
@exceptions = [
|
|
@@ -12,34 +24,67 @@ module Nonnative
|
|
|
12
24
|
|
|
13
25
|
protected
|
|
14
26
|
|
|
27
|
+
# Executes the given block with retries for the configured network exceptions.
|
|
28
|
+
#
|
|
29
|
+
# @param tries [Integer] number of attempts
|
|
30
|
+
# @param wait [Numeric] base interval between retries (seconds)
|
|
31
|
+
# @yield the work to retry
|
|
32
|
+
# @return [Object] the block result
|
|
15
33
|
def with_retry(tries, wait, &)
|
|
16
34
|
Retriable.retriable(tries: tries, base_interval: wait, on: exceptions, &)
|
|
17
35
|
end
|
|
18
36
|
|
|
37
|
+
# Performs a GET request.
|
|
38
|
+
#
|
|
39
|
+
# @param pathname [String] path relative to `host`
|
|
40
|
+
# @param opts [Hash] RestClient request options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
41
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
19
42
|
def get(pathname, opts = {})
|
|
20
43
|
with_exception do
|
|
21
44
|
resource(pathname, opts).get
|
|
22
45
|
end
|
|
23
46
|
end
|
|
24
47
|
|
|
48
|
+
# Performs a POST request.
|
|
49
|
+
#
|
|
50
|
+
# @param pathname [String] path relative to `host`
|
|
51
|
+
# @param payload [Object] request payload
|
|
52
|
+
# @param opts [Hash] RestClient request options
|
|
53
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
25
54
|
def post(pathname, payload, opts = {})
|
|
26
55
|
with_exception do
|
|
27
56
|
resource(pathname, opts).post(payload)
|
|
28
57
|
end
|
|
29
58
|
end
|
|
30
59
|
|
|
60
|
+
# Performs a DELETE request.
|
|
61
|
+
#
|
|
62
|
+
# @param pathname [String] path relative to `host`
|
|
63
|
+
# @param opts [Hash] RestClient request options
|
|
64
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
31
65
|
def delete(pathname, opts = {})
|
|
32
66
|
with_exception do
|
|
33
67
|
resource(pathname, opts).delete
|
|
34
68
|
end
|
|
35
69
|
end
|
|
36
70
|
|
|
71
|
+
# Performs a PUT request.
|
|
72
|
+
#
|
|
73
|
+
# @param pathname [String] path relative to `host`
|
|
74
|
+
# @param payload [Object] request payload
|
|
75
|
+
# @param opts [Hash] RestClient request options
|
|
76
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
37
77
|
def put(pathname, payload, opts = {})
|
|
38
78
|
with_exception do
|
|
39
79
|
resource(pathname, opts).put(payload)
|
|
40
80
|
end
|
|
41
81
|
end
|
|
42
82
|
|
|
83
|
+
# Creates a RestClient resource for a relative path.
|
|
84
|
+
#
|
|
85
|
+
# @param pathname [String] path relative to `host`
|
|
86
|
+
# @param opts [Hash] RestClient request options
|
|
87
|
+
# @return [RestClient::Resource]
|
|
43
88
|
def resource(pathname, opts)
|
|
44
89
|
RestClient::Resource.new(URI.join(host, pathname).to_s, opts)
|
|
45
90
|
end
|
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Sinatra-based HTTP forward proxy server used as an in-process Nonnative server.
|
|
4
|
+
#
|
|
5
|
+
# The proxy receives inbound HTTP requests and forwards them to an upstream host over HTTPS, returning
|
|
6
|
+
# the upstream response status and body.
|
|
7
|
+
#
|
|
8
|
+
# This file defines two classes:
|
|
9
|
+
#
|
|
10
|
+
# - {Nonnative::HTTPProxy}: a Sinatra application that implements the proxying behavior.
|
|
11
|
+
# - {Nonnative::HTTPProxyServer}: a {Nonnative::HTTPServer} wrapper that runs the proxy app under Puma.
|
|
12
|
+
#
|
|
13
|
+
# Notes:
|
|
14
|
+
# - This code is adapted from https://gist.github.com/RaVbaker/d9ead3c92b915f997dab25c7f0c0ab65
|
|
15
|
+
# - Only a subset of request headers are forwarded; certain headers are removed to avoid conflicts.
|
|
16
|
+
#
|
|
17
|
+
# @see Nonnative::HTTPServer
|
|
18
|
+
# @see Nonnative::Server
|
|
4
19
|
module Nonnative
|
|
20
|
+
# Sinatra application implementing a simple forward proxy.
|
|
21
|
+
#
|
|
22
|
+
# The upstream host is configured via Sinatra settings (see {Nonnative::HTTPProxyServer}).
|
|
23
|
+
#
|
|
24
|
+
# Supported HTTP verbs: GET, POST, PUT, PATCH, DELETE.
|
|
5
25
|
class HTTPProxy < Sinatra::Application
|
|
26
|
+
# Extracts request headers from the Rack environment and normalizes them to standard HTTP names.
|
|
27
|
+
#
|
|
28
|
+
# Certain hop-by-hop or proxy-specific headers are removed.
|
|
29
|
+
#
|
|
30
|
+
# @param request [Sinatra::Request] the incoming request
|
|
31
|
+
# @return [Hash{String=>String}] headers to forward to the upstream
|
|
6
32
|
def retrieve_headers(request)
|
|
7
33
|
headers = request.env.map do |header, value|
|
|
8
34
|
[header[5..].split('_').map(&:capitalize).join('-'), value] if header.start_with?('HTTP_')
|
|
@@ -12,10 +38,21 @@ module Nonnative
|
|
|
12
38
|
headers.except('Host', 'Accept-Encoding', 'Version')
|
|
13
39
|
end
|
|
14
40
|
|
|
41
|
+
# Builds the upstream URL for the given request.
|
|
42
|
+
#
|
|
43
|
+
# @param request [Sinatra::Request] the incoming request
|
|
44
|
+
# @param settings [Sinatra::Base] Sinatra settings, expected to include `host`
|
|
45
|
+
# @return [String] HTTPS URL for the upstream request
|
|
15
46
|
def build_url(request, settings)
|
|
16
47
|
URI::HTTPS.build(host: settings.host, path: request.path_info, query: request.query_string).to_s
|
|
17
48
|
end
|
|
18
49
|
|
|
50
|
+
# Executes the upstream request and returns the response.
|
|
51
|
+
#
|
|
52
|
+
# @param verb [String] HTTP verb name (e.g. `"get"`)
|
|
53
|
+
# @param uri [String] upstream URI
|
|
54
|
+
# @param opts [Hash] RestClient options (e.g. headers)
|
|
55
|
+
# @return [RestClient::Response] response for error statuses, otherwise RestClient return value
|
|
19
56
|
def api_response(verb, uri, opts)
|
|
20
57
|
client = RestClient::Resource.new(uri, opts)
|
|
21
58
|
|
|
@@ -36,7 +73,31 @@ module Nonnative
|
|
|
36
73
|
end
|
|
37
74
|
end
|
|
38
75
|
|
|
76
|
+
# Runs {Nonnative::HTTPProxy} as a Puma-based in-process server under Nonnative.
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# Nonnative.configure do |config|
|
|
80
|
+
# config.server do |s|
|
|
81
|
+
# s.name = 'github-proxy'
|
|
82
|
+
# s.klass = Nonnative::Features::HTTPProxyServer
|
|
83
|
+
# s.timeout = 2
|
|
84
|
+
# s.host = '127.0.0.1'
|
|
85
|
+
# s.port = 4567
|
|
86
|
+
# s.log = 'proxy.log'
|
|
87
|
+
# end
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
# # In your server subclass:
|
|
91
|
+
# # class HTTPProxyServer < Nonnative::HTTPProxyServer
|
|
92
|
+
# # def initialize(service)
|
|
93
|
+
# # super('api.github.com', service)
|
|
94
|
+
# # end
|
|
95
|
+
# # end
|
|
96
|
+
#
|
|
97
|
+
# @see Nonnative::HTTPServer
|
|
39
98
|
class HTTPProxyServer < Nonnative::HTTPServer
|
|
99
|
+
# @param host [String] upstream host to proxy to (HTTPS)
|
|
100
|
+
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
40
101
|
def initialize(host, service)
|
|
41
102
|
app = Sinatra.new(Nonnative::HTTPProxy) do
|
|
42
103
|
configure do
|
|
@@ -1,7 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Puma-based HTTP server runner.
|
|
5
|
+
#
|
|
6
|
+
# This is a convenience server implementation for running a Rack/Sinatra application in-process
|
|
7
|
+
# under Nonnative's server lifecycle. It binds to the configured proxy `host`/`port` (so it works
|
|
8
|
+
# consistently with proxy configuration) and uses Puma for HTTP serving.
|
|
9
|
+
#
|
|
10
|
+
# The server is started and stopped by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
|
|
11
|
+
#
|
|
12
|
+
# @example Running a Sinatra app
|
|
13
|
+
# app = Sinatra.new do
|
|
14
|
+
# get('/hello') { 'Hello World!' }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Nonnative.configure do |config|
|
|
18
|
+
# config.server do |s|
|
|
19
|
+
# s.name = 'http'
|
|
20
|
+
# s.klass = ->(service) { Nonnative::HTTPServer.new(app, service) }
|
|
21
|
+
# s.timeout = 2
|
|
22
|
+
# s.host = '127.0.0.1'
|
|
23
|
+
# s.port = 4567
|
|
24
|
+
# s.log = 'http.log'
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# Note: In YAML configuration you typically set `class` to a concrete subclass that calls `super(app, service)`.
|
|
29
|
+
#
|
|
30
|
+
# @see Nonnative::Server
|
|
4
31
|
class HTTPServer < Nonnative::Server
|
|
32
|
+
# Creates a Puma server for the given Rack app and runner configuration.
|
|
33
|
+
#
|
|
34
|
+
# @param app [#call] a Rack-compatible application (e.g. Sinatra/Rack app)
|
|
35
|
+
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
5
36
|
def initialize(app, service)
|
|
6
37
|
log = File.open(service.log, 'a')
|
|
7
38
|
options = {
|
|
@@ -15,11 +46,20 @@ module Nonnative
|
|
|
15
46
|
|
|
16
47
|
protected
|
|
17
48
|
|
|
49
|
+
# Binds the Puma server and begins serving.
|
|
50
|
+
#
|
|
51
|
+
# The listener binds to the proxy host/port so that enabling a proxy results in traffic and
|
|
52
|
+
# readiness checks consistently targeting the proxy endpoint.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
18
55
|
def perform_start
|
|
19
56
|
server.add_tcp_listener proxy.host, proxy.port
|
|
20
57
|
server.run false
|
|
21
58
|
end
|
|
22
59
|
|
|
60
|
+
# Gracefully shuts down the Puma server.
|
|
61
|
+
#
|
|
62
|
+
# @return [void]
|
|
23
63
|
def perform_stop
|
|
24
64
|
server.graceful_shutdown
|
|
25
65
|
end
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Socket-pair variant used by the fault-injection proxy to simulate corrupted/incoherent traffic.
|
|
5
|
+
#
|
|
6
|
+
# When active, data written to the upstream socket is corrupted by shuffling the characters in the
|
|
7
|
+
# payload before forwarding.
|
|
8
|
+
#
|
|
9
|
+
# This behavior is enabled by calling {Nonnative::FaultInjectionProxy#invalid_data}.
|
|
10
|
+
#
|
|
11
|
+
# @see Nonnative::FaultInjectionProxy
|
|
12
|
+
# @see Nonnative::SocketPairFactory
|
|
13
|
+
# @see Nonnative::SocketPair
|
|
4
14
|
class InvalidDataSocketPair < SocketPair
|
|
15
|
+
# Writes corrupted data to the socket by shuffling characters.
|
|
16
|
+
#
|
|
17
|
+
# @param socket [IO] the socket to write to
|
|
18
|
+
# @param data [String] the original payload
|
|
19
|
+
# @return [Integer] number of bytes written
|
|
5
20
|
def write(socket, data)
|
|
6
21
|
Nonnative.logger.info "shuffling socket data '#{socket.inspect}' for 'invalid_data' pair"
|
|
7
22
|
|
data/lib/nonnative/no_proxy.rb
CHANGED
|
@@ -1,23 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# No-op proxy implementation.
|
|
5
|
+
#
|
|
6
|
+
# This is the default proxy when `service.proxy.kind` is `"none"` (or an unknown kind is provided).
|
|
7
|
+
# It does not bind/listen or alter traffic; it simply exposes the underlying runner's configured
|
|
8
|
+
# `host` and `port`.
|
|
9
|
+
#
|
|
10
|
+
# Runners can always call `start`, `stop`, and `reset` safely on this proxy.
|
|
11
|
+
#
|
|
12
|
+
# @see Nonnative.proxy
|
|
13
|
+
# @see Nonnative::Proxy
|
|
4
14
|
class NoProxy < Proxy
|
|
15
|
+
# Starts the proxy.
|
|
16
|
+
#
|
|
17
|
+
# This implementation does nothing.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
5
20
|
def start
|
|
6
21
|
# Do nothing.
|
|
7
22
|
end
|
|
8
23
|
|
|
24
|
+
# Stops the proxy.
|
|
25
|
+
#
|
|
26
|
+
# This implementation does nothing.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
9
29
|
def stop
|
|
10
30
|
# Do nothing.
|
|
11
31
|
end
|
|
12
32
|
|
|
33
|
+
# Resets the proxy state.
|
|
34
|
+
#
|
|
35
|
+
# This implementation does nothing.
|
|
36
|
+
#
|
|
37
|
+
# @return [void]
|
|
13
38
|
def reset
|
|
14
39
|
# Do nothing.
|
|
15
40
|
end
|
|
16
41
|
|
|
42
|
+
# Returns the host clients should connect to.
|
|
43
|
+
#
|
|
44
|
+
# For {NoProxy}, this is the underlying runner configuration host.
|
|
45
|
+
#
|
|
46
|
+
# @return [String]
|
|
17
47
|
def host
|
|
18
48
|
service.host
|
|
19
49
|
end
|
|
20
50
|
|
|
51
|
+
# Returns the port clients should connect to.
|
|
52
|
+
#
|
|
53
|
+
# For {NoProxy}, this is the underlying runner configuration port.
|
|
54
|
+
#
|
|
55
|
+
# @return [Integer]
|
|
21
56
|
def port
|
|
22
57
|
service.port
|
|
23
58
|
end
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Raised when a configured runner cannot be found by name.
|
|
5
|
+
#
|
|
6
|
+
# This is typically raised by lookup helpers such as:
|
|
7
|
+
# - {Nonnative::Configuration#process_by_name}
|
|
8
|
+
# - {Nonnative::Pool#process_by_name}
|
|
9
|
+
# - {Nonnative::Pool#server_by_name}
|
|
10
|
+
# - {Nonnative::Pool#service_by_name}
|
|
4
11
|
class NotFoundError < Nonnative::Error
|
|
5
12
|
end
|
|
6
13
|
end
|
|
@@ -1,25 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# HTTP client for common observability endpoints exposed by the system under test.
|
|
5
|
+
#
|
|
6
|
+
# This client is returned by {Nonnative.observability} and builds endpoint paths from
|
|
7
|
+
# {Nonnative::Configuration#name}.
|
|
8
|
+
#
|
|
9
|
+
# Endpoints:
|
|
10
|
+
# - `/<name>/healthz`
|
|
11
|
+
# - `/<name>/livez`
|
|
12
|
+
# - `/<name>/readyz`
|
|
13
|
+
# - `/<name>/metrics`
|
|
14
|
+
#
|
|
15
|
+
# Requests are performed using {Nonnative::HTTPClient}, so callers may pass RestClient options
|
|
16
|
+
# such as `headers`, `open_timeout`, and `read_timeout`.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Nonnative.configure do |config|
|
|
20
|
+
# config.name = 'my-service'
|
|
21
|
+
# config.url = 'http://127.0.0.1:8080'
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# response = Nonnative.observability.health(read_timeout: 2, open_timeout: 2)
|
|
25
|
+
# response.code # => 200
|
|
26
|
+
#
|
|
27
|
+
# @see Nonnative.observability
|
|
28
|
+
# @see Nonnative::HTTPClient
|
|
4
29
|
class Observability < Nonnative::HTTPClient
|
|
30
|
+
# Calls `/<name>/healthz`.
|
|
31
|
+
#
|
|
32
|
+
# @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
33
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
5
34
|
def health(opts = {})
|
|
6
35
|
get("#{name}/healthz", opts)
|
|
7
36
|
end
|
|
8
37
|
|
|
38
|
+
# Calls `/<name>/livez`.
|
|
39
|
+
#
|
|
40
|
+
# @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
41
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
9
42
|
def liveness(opts = {})
|
|
10
43
|
get("#{name}/livez", opts)
|
|
11
44
|
end
|
|
12
45
|
|
|
46
|
+
# Calls `/<name>/readyz`.
|
|
47
|
+
#
|
|
48
|
+
# @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
49
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
13
50
|
def readiness(opts = {})
|
|
14
51
|
get("#{name}/readyz", opts)
|
|
15
52
|
end
|
|
16
53
|
|
|
54
|
+
# Calls `/<name>/metrics`.
|
|
55
|
+
#
|
|
56
|
+
# @param opts [Hash] RestClient options (e.g. `headers`, `read_timeout`, `open_timeout`)
|
|
57
|
+
# @return [RestClient::Response, String] response for non-2xx errors, otherwise the RestClient result
|
|
17
58
|
def metrics(opts = {})
|
|
18
59
|
get("#{name}/metrics", opts)
|
|
19
60
|
end
|
|
20
61
|
|
|
21
62
|
protected
|
|
22
63
|
|
|
64
|
+
# Returns the configured system name used as the endpoint prefix.
|
|
65
|
+
#
|
|
66
|
+
# @return [String, nil]
|
|
23
67
|
def name
|
|
24
68
|
Nonnative.configuration.name
|
|
25
69
|
end
|
data/lib/nonnative/pool.rb
CHANGED
|
@@ -1,33 +1,83 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Orchestrates lifecycle for configured processes, servers and services.
|
|
5
|
+
#
|
|
6
|
+
# A pool is created when {Nonnative.start} is called and is accessible via {Nonnative.pool}.
|
|
7
|
+
#
|
|
8
|
+
# Lifecycle order is important:
|
|
9
|
+
# - On start: services first, then servers/processes (in parallel port-check threads)
|
|
10
|
+
# - On stop: processes/servers first, then services
|
|
11
|
+
#
|
|
12
|
+
# Readiness and shutdown are determined via TCP port checks ({Nonnative::Port#open?} / {Nonnative::Port#closed?}).
|
|
13
|
+
#
|
|
14
|
+
# @see Nonnative.start
|
|
15
|
+
# @see Nonnative.stop
|
|
16
|
+
# @see Nonnative::Port
|
|
4
17
|
class Pool
|
|
18
|
+
# @param configuration [Nonnative::Configuration] the configuration to run
|
|
5
19
|
def initialize(configuration)
|
|
6
20
|
@configuration = configuration
|
|
7
21
|
end
|
|
8
22
|
|
|
23
|
+
# Starts all configured runners and yields results for each process/server.
|
|
24
|
+
#
|
|
25
|
+
# Services are started first (proxy-only), then servers and processes are started and checked for readiness.
|
|
26
|
+
#
|
|
27
|
+
# @yieldparam name [String, nil] runner name
|
|
28
|
+
# @yieldparam values [Object] runner-specific return value from `start` (e.g. `[pid, running]` for processes)
|
|
29
|
+
# @yieldparam result [Boolean] result of the port readiness check (`true` if ready in time)
|
|
30
|
+
# @return [void]
|
|
9
31
|
def start(&)
|
|
10
32
|
services.each(&:start)
|
|
11
33
|
[servers, processes].each { |t| process(t, :start, :open?, &) }
|
|
12
34
|
end
|
|
13
35
|
|
|
36
|
+
# Stops all configured runners and yields results for each process/server.
|
|
37
|
+
#
|
|
38
|
+
# Processes and servers are stopped first and checked for shutdown, then services are stopped (proxy-only).
|
|
39
|
+
#
|
|
40
|
+
# @yieldparam name [String, nil] runner name
|
|
41
|
+
# @yieldparam id [Object] runner-specific identifier returned by `stop` (e.g. pid or object_id)
|
|
42
|
+
# @yieldparam result [Boolean] result of the port shutdown check (`true` if closed in time)
|
|
43
|
+
# @return [void]
|
|
14
44
|
def stop(&)
|
|
15
45
|
[processes, servers].each { |t| process(t, :stop, :closed?, &) }
|
|
16
46
|
services.each(&:stop)
|
|
17
47
|
end
|
|
18
48
|
|
|
49
|
+
# Finds a running process runner by configured name.
|
|
50
|
+
#
|
|
51
|
+
# @param name [String]
|
|
52
|
+
# @return [Nonnative::Process]
|
|
53
|
+
# @raise [Nonnative::NotFoundError] if no configured process matches the given name
|
|
19
54
|
def process_by_name(name)
|
|
20
55
|
processes[runner_index(configuration.processes, name)].first
|
|
21
56
|
end
|
|
22
57
|
|
|
58
|
+
# Finds a running server runner by configured name.
|
|
59
|
+
#
|
|
60
|
+
# @param name [String]
|
|
61
|
+
# @return [Nonnative::Server]
|
|
62
|
+
# @raise [Nonnative::NotFoundError] if no configured server matches the given name
|
|
23
63
|
def server_by_name(name)
|
|
24
64
|
servers[runner_index(configuration.servers, name)].first
|
|
25
65
|
end
|
|
26
66
|
|
|
67
|
+
# Finds a running service runner by configured name.
|
|
68
|
+
#
|
|
69
|
+
# @param name [String]
|
|
70
|
+
# @return [Nonnative::Service]
|
|
71
|
+
# @raise [Nonnative::NotFoundError] if no configured service matches the given name
|
|
27
72
|
def service_by_name(name)
|
|
28
73
|
services[runner_index(configuration.services, name)]
|
|
29
74
|
end
|
|
30
75
|
|
|
76
|
+
# Resets proxies for all runners in this pool.
|
|
77
|
+
#
|
|
78
|
+
# This is used by the Cucumber `@reset` hook and is safe to call any time after the pool is created.
|
|
79
|
+
#
|
|
80
|
+
# @return [void]
|
|
31
81
|
def reset
|
|
32
82
|
services.each { |s| s.proxy.reset }
|
|
33
83
|
servers.each { |s| s.first.proxy.reset }
|
data/lib/nonnative/port.rb
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Performs TCP port readiness/shutdown checks for a configured runner.
|
|
5
|
+
#
|
|
6
|
+
# Nonnative uses this to decide whether a process/server is ready after start, and whether it has
|
|
7
|
+
# shut down after stop. The checks repeatedly attempt to open a TCP connection to `process.host`
|
|
8
|
+
# and `process.port` until either:
|
|
9
|
+
#
|
|
10
|
+
# - the expected condition is met, or
|
|
11
|
+
# - the configured timeout elapses (in which case the method returns `false`)
|
|
12
|
+
#
|
|
13
|
+
# The `process` argument is a runner configuration object (e.g. {Nonnative::ConfigurationProcess}
|
|
14
|
+
# or {Nonnative::ConfigurationServer}) that responds to `host`, `port`, and `timeout`.
|
|
15
|
+
#
|
|
16
|
+
# @see Nonnative::Pool for how these checks are orchestrated during start/stop
|
|
4
17
|
class Port
|
|
18
|
+
# @param process [#host, #port, #timeout] runner configuration providing connection details
|
|
5
19
|
def initialize(process)
|
|
6
20
|
@process = process
|
|
7
21
|
@timeout = Nonnative::Timeout.new(process.timeout)
|
|
8
22
|
end
|
|
9
23
|
|
|
24
|
+
# Returns whether the configured host/port becomes connectable before the timeout elapses.
|
|
25
|
+
#
|
|
26
|
+
# This method retries on common connection errors until either a connection succeeds
|
|
27
|
+
# (returns `true`) or the timeout elapses (returns `false`).
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean] `true` if the port opened in time; otherwise `false`
|
|
10
30
|
def open?
|
|
11
31
|
Nonnative.logger.info "checking if port '#{process.port}' is open on host '#{process.host}'"
|
|
12
32
|
|
|
@@ -19,6 +39,12 @@ module Nonnative
|
|
|
19
39
|
end
|
|
20
40
|
end
|
|
21
41
|
|
|
42
|
+
# Returns whether the configured host/port becomes non-connectable before the timeout elapses.
|
|
43
|
+
#
|
|
44
|
+
# This method treats a successful connection as “not closed yet” and keeps retrying until it
|
|
45
|
+
# observes connection failure (returns `true`) or the timeout elapses (returns `false`).
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean] `true` if the port closed in time; otherwise `false`
|
|
22
48
|
def closed?
|
|
23
49
|
Nonnative.logger.info "checking if port '#{process.port}' is closed on host '#{process.host}'"
|
|
24
50
|
|