aws_advanced_ruby_driver_wrapper 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/README.md +115 -6
  4. data/THIRD-PARTY-LICENSES +70 -11
  5. data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
  6. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +17 -3
  7. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +1 -1
  9. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +43 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
  11. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -91
  13. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +18 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
  17. data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +1 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
  21. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +115 -7
  22. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
  24. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
  26. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
  27. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
  28. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
  29. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
  30. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +6 -13
  31. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +90 -3
  32. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
  33. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
  34. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
  35. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
  36. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
  40. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
  41. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
  42. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
  43. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
  44. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
  45. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +14 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +41 -23
  49. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
  50. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
  52. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
  53. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +1 -1
  54. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
  55. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
  56. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
  57. data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
  59. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
  60. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
  61. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
  62. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
  63. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
  64. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
  66. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
  67. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
  68. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
  69. data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +9 -5
  71. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_counter.rb +39 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
  79. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
  80. data/lib/aws_advanced_ruby_driver_wrapper.rb +9 -2
  81. metadata +54 -2
@@ -0,0 +1,54 @@
1
+ -- pgcrypto provides hmac(). If it is already installed in a schema other than public,
2
+ -- replace public in public.hmac below with that schema.
3
+ CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;
4
+
5
+ -- Replace 'encrypt' below if encryption_metadata_schema is set to something else.
6
+ -- A trigger runs with the search_path of the session that fires it. Pinning it to pg_catalog
7
+ -- stops a session from putting its own functions or operators, such as hmac() or <>, ahead of
8
+ -- the built-in ones to get a plaintext value past this check.
9
+ CREATE OR REPLACE FUNCTION enforce_encrypted_column() RETURNS trigger
10
+ SET search_path = pg_catalog, pg_temp
11
+ AS $$
12
+ DECLARE
13
+ col_name text := TG_ARGV[0];
14
+ col_value bytea;
15
+ hmac_key bytea;
16
+ unchanged boolean;
17
+ BEGIN
18
+ -- An UPDATE that leaves the encrypted value as it was is not re-checked; see
19
+ -- "Updates after a key rotation".
20
+ IF TG_OP = 'UPDATE' THEN
21
+ EXECUTE format('SELECT ($1).%I IS NOT DISTINCT FROM ($2).%I', col_name, col_name)
22
+ INTO unchanged USING NEW, OLD;
23
+ IF unchanged THEN
24
+ RETURN NEW;
25
+ END IF;
26
+ END IF;
27
+
28
+ EXECUTE format('SELECT ($1).%I', col_name) INTO col_value USING NEW;
29
+ IF col_value IS NULL THEN
30
+ RETURN NEW;
31
+ END IF;
32
+
33
+ -- Read the column's current HMAC key from key_storage.
34
+ SELECT ks.hmac_key INTO hmac_key
35
+ FROM encrypt.encryption_metadata em
36
+ JOIN encrypt.key_storage ks ON em.key_id = ks.id
37
+ WHERE em.table_name = TG_TABLE_NAME AND em.column_name = col_name;
38
+
39
+ IF hmac_key IS NULL THEN
40
+ RAISE EXCEPTION 'No HMAC key is configured for %.%', TG_TABLE_NAME, col_name;
41
+ END IF;
42
+
43
+ IF length(col_value) < 65
44
+ OR substring(col_value from 1 for 32) <> public.hmac(substring(col_value from 33), hmac_key, 'sha256'::text) THEN
45
+ RAISE EXCEPTION 'Column %.% does not carry a valid HMAC tag (plaintext or tampered value)', TG_TABLE_NAME, col_name;
46
+ END IF;
47
+
48
+ RETURN NEW;
49
+ END;
50
+ $$ LANGUAGE plpgsql;
51
+
52
+ -- Records which version of this SQL is installed. Check it with:
53
+ -- SELECT obj_description('enforce_encrypted_column'::regproc, 'pg_proc');
54
+ COMMENT ON FUNCTION enforce_encrypted_column() IS 'aws-advanced-ruby-driver-wrapper kms_encryption trigger v2';
@@ -19,6 +19,7 @@ require_relative '../errors'
19
19
  require_relative '../logging'
20
20
  require_relative '../property_definition'
21
21
  require_relative '../utils/aws_credentials_utils'
22
+ require_relative '../utils/telemetry/telemetry_const'
22
23
 
23
24
  module AwsAdvancedRubyDriverWrapper
24
25
  module Plugins
@@ -27,6 +28,7 @@ module AwsAdvancedRubyDriverWrapper
27
28
 
28
29
  SUBSCRIBED_METHODS = Set['connect', 'internal_connect'].freeze
29
30
  SECRETS_MANAGER_CACHE_NAME = :secrets_manager
31
+ TELEMETRY_FETCH_CREDENTIALS = 'fetch credentials'
30
32
  MIN_EXPIRATION_SEC = 300
31
33
  SYNC_FETCH_TIMEOUT_SEC = 60
32
34
  # The partition segment of an ARN varies across AWS partitions, so match any of them.
@@ -126,6 +128,8 @@ module AwsAdvancedRubyDriverWrapper
126
128
  )
127
129
 
128
130
  @subscribed_methods = SUBSCRIBED_METHODS
131
+ @telemetry_factory = service_container.telemetry_factory
132
+ @fetch_credentials_counter = @telemetry_factory.create_counter('secrets_manager.fetch_credentials.count')
129
133
  end
130
134
 
131
135
  def connect(_host_info, driver_props, _is_initial_connection, pipeline_callable)
@@ -211,7 +215,17 @@ module AwsAdvancedRubyDriverWrapper
211
215
  end
212
216
 
213
217
  # Returns true if credentials were freshly fetched from the service (suppresses login-error retry).
218
+ #
219
+ # Each lookup is counted and recorded as a span, whether it is answered from the cache or from
220
+ # the service.
214
221
  def fetch_secret_and_report_if_fresh?(force:)
222
+ @fetch_credentials_counter.inc
223
+ @telemetry_factory.in_context(TELEMETRY_FETCH_CREDENTIALS, Utils::Telemetry::TraceLevel::NESTED, record_failure: true) do
224
+ load_secret_and_report_if_fresh?(force:)
225
+ end
226
+ end
227
+
228
+ def load_secret_and_report_if_fresh?(force:)
215
229
  credentials = Utils::AwsCredentialsUtils.snapshot(@credentials_provider)
216
230
  cache_key = secret_cache_key(credentials)
217
231
  entry = cache_key && @service_container.storage_service.get(SECRETS_MANAGER_CACHE_NAME, cache_key)
@@ -20,6 +20,7 @@ require_relative '../aws_advanced_ruby_driver_wrapper'
20
20
  require_relative 'utils/connection_config_parser'
21
21
  require_relative 'utils/sql_encoding'
22
22
  require_relative 'services/service_utility'
23
+ require_relative 'utils/telemetry/telemetry_factory'
23
24
  require_relative 'ruby_method'
24
25
  require_relative 'errors'
25
26
 
@@ -45,17 +46,20 @@ module AwsAdvancedRubyDriverWrapper
45
46
  def initialize(*, **)
46
47
  ensure_pg!
47
48
  config = Utils::ConnectionConfigParser.parse(:postgresql, *, **)
48
- @service_container = Services::ServiceUtility.create_standard_container(config)
49
- @service_container.host_service.refresh_host_list
50
- @prepared_on = {}
51
- @prepared_sql = {}
52
- @async_conn = nil
53
- @async_sql = nil
54
- @copy_conn = nil
55
- @copy_sql = nil
56
- @lo_conn = nil
57
- conn_service = @service_container.connection_service
58
- @service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
49
+ telemetry_factory = Utils::Telemetry::TelemetryFactory.new(config.wrapper_props)
50
+ telemetry_factory.in_context("#{self.class.name}.new", Utils::Telemetry::TraceLevel::TOP_LEVEL, record_result: true) do
51
+ @service_container = Services::ServiceUtility.create_standard_container(config, telemetry_factory:)
52
+ @service_container.host_service.refresh_host_list
53
+ @prepared_on = {}
54
+ @prepared_sql = {}
55
+ @async_conn = nil
56
+ @async_sql = nil
57
+ @copy_conn = nil
58
+ @copy_sql = nil
59
+ @lo_conn = nil
60
+ conn_service = @service_container.connection_service
61
+ @service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
62
+ end
59
63
  end
60
64
 
61
65
  # Every canonical pg operation that talks to the server, and everything this class has to know about one: the
@@ -173,6 +177,10 @@ module AwsAdvancedRubyDriverWrapper
173
177
  lotell: :lo_tell, lotruncate: :lo_truncate
174
178
  }.freeze
175
179
 
180
+ # The operations pg yields the result of when given a block, clearing it once the block returns and
181
+ # returning the block's value in its place.
182
+ YIELDS_RESULT = %i[exec async_exec exec_params exec_prepared].freeze
183
+
176
184
  # A statement can also be prepared by sending a +PREPARE+ rather than by calling pg's own
177
185
  # +prepare+, and the +exec_prepared+ that runs it looks no different either way. The two are read
178
186
  # here so that a statement prepared the first way is remembered like one prepared the second, and
@@ -194,24 +202,24 @@ module AwsAdvancedRubyDriverWrapper
194
202
 
195
203
  # Explicitly define critical methods (bypass method_missing to avoid method_missing overhead).
196
204
 
197
- def exec(sql, *params)
198
- execute_operation(:exec, [sql, *params])
205
+ def exec(sql, *params, &)
206
+ execute_operation(:exec, [sql, *params], &)
199
207
  end
200
208
 
201
209
  # pg spells this operation +exec+, +query+, +async_exec+ and +async_query+, all of which run the
202
210
  # same libpq call. +query+ is defined here rather than left to method_missing because it is the
203
211
  # spelling applications use most after +exec+, and it enters the pipeline as +connection.exec+,
204
212
  # since that is the libpq operation being performed.
205
- def query(sql, *params)
206
- execute_operation(:exec, [sql, *params], spelling: :query)
213
+ def query(sql, *params, &)
214
+ execute_operation(:exec, [sql, *params], spelling: :query, &)
207
215
  end
208
216
 
209
- def exec_params(sql, params, result_format = 0, type_map = nil)
210
- execute_operation(:exec_params, [sql, params, result_format, type_map])
217
+ def exec_params(sql, params, result_format = 0, type_map = nil, &)
218
+ execute_operation(:exec_params, [sql, params, result_format, type_map], &)
211
219
  end
212
220
 
213
- def async_exec(sql, *params)
214
- execute_operation(:async_exec, [sql, *params])
221
+ def async_exec(sql, *params, &)
222
+ execute_operation(:async_exec, [sql, *params], &)
215
223
  end
216
224
 
217
225
  def transaction(&)
@@ -250,8 +258,8 @@ module AwsAdvancedRubyDriverWrapper
250
258
  execute_operation(:send_prepare, [stmt_name, sql, param_types])
251
259
  end
252
260
 
253
- def exec_prepared(stmt_name, params = [], result_format = 0, type_map = nil)
254
- execute_operation(:exec_prepared, [stmt_name, params, result_format, type_map])
261
+ def exec_prepared(stmt_name, params = [], result_format = 0, type_map = nil, &)
262
+ execute_operation(:exec_prepared, [stmt_name, params, result_format, type_map], &)
255
263
  end
256
264
 
257
265
  def describe_prepared(stmt_name)
@@ -312,7 +320,7 @@ module AwsAdvancedRubyDriverWrapper
312
320
 
313
321
  def method_missing(method_name, *args, **kwargs, &)
314
322
  conn = current_conn
315
- raise NoMethodError, 'Connection not initialized' if conn.nil?
323
+ raise PG::ConnectionBad, 'connection is not initialized' if conn.nil?
316
324
  raise NoMethodError, "undefined method `#{method_name}' for #{self.class}" unless conn.respond_to?(method_name)
317
325
 
318
326
  operation = operation_for(method_name)
@@ -344,10 +352,14 @@ module AwsAdvancedRubyDriverWrapper
344
352
  # made, since an `after` step may be what forgets it, and it is handed to the result as well, so that a
345
353
  # plugin which has to inspect the statement still sees it when the rows are read.
346
354
  def execute_operation(operation, args = [], kwargs = {}, spelling: operation, &)
355
+ if block_given? && YIELDS_RESULT.include?(operation)
356
+ return yield_result(execute_operation(operation, args, kwargs, spelling: spelling), &)
357
+ end
358
+
347
359
  spec = OPERATIONS[operation] || { method: "connection.#{operation}" }
348
360
  conn = current_conn
349
361
  # Guard against a missing connection. Fail loudly instead.
350
- raise NoMethodError, 'Connection not initialized' if conn.nil?
362
+ raise PG::ConnectionBad, 'connection is not initialized' if conn.nil?
351
363
 
352
364
  # Only forward keyword arguments when there are any.
353
365
  sql = sql_for(spec, args)
@@ -518,6 +530,12 @@ module AwsAdvancedRubyDriverWrapper
518
530
  @network_bound_methods ||= driver_dialect.network_bound_methods
519
531
  end
520
532
 
533
+ def yield_result(result)
534
+ yield result
535
+ ensure
536
+ result.clear
537
+ end
538
+
521
539
  def wrap_pg_result(result, sql = nil)
522
540
  return result unless result.is_a?(PG::Result)
523
541
 
@@ -54,8 +54,41 @@ module AwsAdvancedRubyDriverWrapper
54
54
  type: String)
55
55
  DIALECT = WrapperProperty.new(:wrapper_dialect, 'The database dialect identifier for the database in use.', type: String)
56
56
  AWS_CREDENTIALS_PROVIDER = WrapperProperty.new(:aws_credentials_provider,
57
- 'AWS credentials provider for IAM token generation or Secrets Manager',
57
+ 'AWS credentials provider for the plugins that call AWS services',
58
58
  default_value: nil)
59
+ # -- Telemetry --
60
+ ENABLE_TELEMETRY = WrapperProperty.new(
61
+ :enable_telemetry,
62
+ 'Enables telemetry and observability of the wrapper.',
63
+ default_value: false,
64
+ type: :boolean
65
+ )
66
+ TELEMETRY_SUBMIT_TOP_LEVEL = WrapperProperty.new(
67
+ :telemetry_submit_top_level,
68
+ 'Submit the traces of wrapper calls as top-level traces, rather than only as children of a trace already in progress.',
69
+ default_value: false,
70
+ type: :boolean
71
+ )
72
+ TELEMETRY_TRACES_BACKEND = WrapperProperty.new(
73
+ :telemetry_traces_backend,
74
+ 'Method to export the telemetry traces of the wrapper. Valid values are otlp and none.',
75
+ default_value: nil,
76
+ type: String
77
+ )
78
+ TELEMETRY_METRICS_BACKEND = WrapperProperty.new(
79
+ :telemetry_metrics_backend,
80
+ 'Method to export the telemetry metrics of the wrapper. Valid values are otlp and none.',
81
+ default_value: nil,
82
+ type: String
83
+ )
84
+ TELEMETRY_FAILOVER_ADDITIONAL_TOP_TRACE = WrapperProperty.new(
85
+ :telemetry_failover_additional_top_trace,
86
+ 'Also submit the trace of each failover as a top-level trace of its own. Only a failover that was ' \
87
+ 'traced is submitted: one inside a trace in progress, or inside a wrapper call that was submitted ' \
88
+ 'as a top-level trace.',
89
+ default_value: false,
90
+ type: :boolean
91
+ )
59
92
  # -- Failover --
60
93
  FAILOVER_TIMEOUT_SEC = WrapperProperty.new(
61
94
  :failover_timeout_sec,
@@ -159,6 +192,15 @@ module AwsAdvancedRubyDriverWrapper
159
192
  default_value: 0.1, type: Float, validator: POSITIVE_FLOAT)
160
193
  BG_SWITCHOVER_TIMEOUT_SEC = WrapperProperty.new(:bg_switchover_timeout_sec, 'Blue/Green Deployment switchover timeout (in seconds).',
161
194
  default_value: 180.0, type: Float, validator: POSITIVE_FLOAT)
195
+ BG_DROP_BLUE_CONNECTIONS = WrapperProperty.new(
196
+ :bg_drop_blue_connections,
197
+ 'Whether to invalidate connections that were opened to the old blue environment once a Blue/Green ' \
198
+ 'switchover completes. When enabled, the next use of such a connection closes it: with the failover ' \
199
+ 'plugin enabled it reconnects through Blue/Green routing and raises FailoverSuccessError, and without ' \
200
+ 'failover the driver raises its closed-connection error. Either way the caller gets an explicit error ' \
201
+ 'instead of a stale read from old blue.',
202
+ default_value: true, type: :boolean
203
+ )
162
204
 
163
205
  # -- IAM Authentication --
164
206
  IAM_HOST = WrapperProperty.new(:iam_host, 'Overrides the host used to generate the IAM token', default_value: nil, type: String)
@@ -171,6 +213,26 @@ module AwsAdvancedRubyDriverWrapper
171
213
  IAM_ACCESS_TOKEN_PROPERTY_NAME = WrapperProperty.new(:iam_access_token_property_name, 'Property name used to pass the IAM token',
172
214
  default_value: :password, type: Symbol)
173
215
 
216
+ # -- Federated / Okta Authentication (SAML) --
217
+ IDP_ENDPOINT = WrapperProperty.new(:idp_endpoint, 'The hosting URL of the Identity Provider', default_value: nil, type: String)
218
+ IDP_PORT = WrapperProperty.new(:idp_port, 'The hosting port of the Identity Provider', default_value: 443, type: Integer)
219
+ IDP_USERNAME = WrapperProperty.new(:idp_username, 'The federated user name', default_value: nil, type: String)
220
+ IDP_PASSWORD = WrapperProperty.new(:idp_password, 'The federated user password', default_value: nil, type: String)
221
+ IAM_ROLE_ARN = WrapperProperty.new(:iam_role_arn, 'The ARN of the IAM Role that is to be assumed', default_value: nil, type: String)
222
+ IAM_IDP_ARN = WrapperProperty.new(:iam_idp_arn, 'The ARN of the Identity Provider', default_value: nil, type: String)
223
+ RP_IDENTIFIER = WrapperProperty.new(:rp_identifier, 'The relying party identifier', default_value: 'urn:amazon:webservices',
224
+ type: String)
225
+ DB_USER = WrapperProperty.new(:db_user, 'The database user used to access the database', default_value: nil, type: String)
226
+ SSL_INSECURE = WrapperProperty.new(:ssl_insecure, "Whether the IdP server's TLS certificate chain should not be verified",
227
+ default_value: false, type: :boolean)
228
+ APP_ID = WrapperProperty.new(:app_id, 'The ID of the AWS application configured on Okta', default_value: nil, type: String)
229
+ HTTP_CLIENT_SOCKET_TIMEOUT_SEC = WrapperProperty.new(:http_client_socket_timeout_sec,
230
+ 'Socket timeout in seconds for IdP HTTP requests',
231
+ default_value: 60.0, type: Float, validator: POSITIVE_FLOAT)
232
+ HTTP_CLIENT_CONNECT_TIMEOUT_SEC = WrapperProperty.new(:http_client_connect_timeout_sec,
233
+ 'Connect timeout in seconds for IdP HTTP requests',
234
+ default_value: 60.0, type: Float, validator: POSITIVE_FLOAT)
235
+
174
236
  # -- Initial Connection Strategy --
175
237
  INITIAL_CONNECTION_SUBSTITUTE_HOST = WrapperProperty.new(
176
238
  :initial_connection_substitute_host,
@@ -46,6 +46,9 @@ module AwsAdvancedRubyDriverWrapper
46
46
  CONNECTION_ABANDON_RESULTS = define('connection.abandon_results!', check_bounded_connection: true)
47
47
  CONNECTION_SET_SERVER_OPTION = define('connection.set_server_option', check_bounded_connection: false)
48
48
 
49
+ # -- Connection methods (trilogy-specific) --
50
+ CONNECTION_CHANGE_DB = define('connection.change_db', check_bounded_connection: false)
51
+
49
52
  # -- Connection methods (pg-specific) --
50
53
  CONNECTION_EXEC = define('connection.exec', check_bounded_connection: false)
51
54
  CONNECTION_ASYNC_EXEC = define('connection.async_exec', check_bounded_connection: false)
@@ -61,7 +61,7 @@ module AwsAdvancedRubyDriverWrapper
61
61
  }.freeze
62
62
 
63
63
  # @param connection_service [ConnectionService]
64
- # @param driver_name [Symbol] :mysql2 or :postgresql
64
+ # @param driver_name [Symbol] :mysql2, :trilogy or :postgresql
65
65
  def initialize(connection_service, driver_name)
66
66
  @connection_service = connection_service
67
67
  @dialect_cache = {}
@@ -164,6 +164,21 @@ module AwsAdvancedRubyDriverWrapper
164
164
  @error_handler.read_only_error?(error)
165
165
  end
166
166
 
167
+ # Whether the driver reported the error as a database error (it carries a SQLSTATE, or for
168
+ # trilogy a MySQL error code). Not a strict server-vs-client split; see ErrorHandler#database_error?.
169
+ # @param error [Exception]
170
+ # @return [Boolean]
171
+ def database_error?(error)
172
+ @error_handler.database_error?(error)
173
+ end
174
+
175
+ # Whether the error is a schema/syntax error (SQLSTATE class '42' or its per-driver equivalent).
176
+ # @param error [Exception]
177
+ # @return [Boolean]
178
+ def schema_error?(error)
179
+ @error_handler.schema_error?(error)
180
+ end
181
+
167
182
  private
168
183
 
169
184
  # Resolves the initial database dialect from the initial connection info.
@@ -205,12 +220,10 @@ module AwsAdvancedRubyDriverWrapper
205
220
 
206
221
  rds_type = Utils::RdsUtils.identify_rds_type(host)
207
222
 
208
- @dialect_code = if @driver_dialect == DriverDialects::DriverDialectManager::MYSQL_DIALECT
209
- resolve_mysql_dialect(rds_type)
210
- elsif @driver_dialect == DriverDialects::DriverDialectManager::PG_DIALECT
211
- resolve_pg_dialect(rds_type)
212
- else
213
- DialectCodes::UNKNOWN
223
+ @dialect_code = case @driver_dialect.database_engine
224
+ when :mysql then resolve_mysql_dialect(rds_type)
225
+ when :postgresql then resolve_pg_dialect(rds_type)
226
+ else DialectCodes::UNKNOWN
214
227
  end
215
228
 
216
229
  @db_dialect = dialect_for_code(@dialect_code)
@@ -15,6 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  require 'concurrent/map'
18
+ require_relative '../errors'
18
19
  require_relative '../host/random_host_selector'
19
20
 
20
21
  module AwsAdvancedRubyDriverWrapper
@@ -47,18 +48,45 @@ module AwsAdvancedRubyDriverWrapper
47
48
  # Register a non-default host selector. The selector is shared by every HostService in the process,
48
49
  # so it must be safe to call from multiple threads.
49
50
  #
51
+ # Strategy names are case-insensitive: they are normalized here and in #host_selector, so a
52
+ # selector registered as 'LeastLag' is found when a connection names 'leastlag'.
53
+ #
50
54
  # @param name [String] strategy name
51
55
  # @param selector [#select_host] any object responding to select_host(hosts, role, props)
52
56
  def register_host_selector(name, selector)
53
- raise Errors::AwsError, "Cannot override default host selection strategy: '#{name}'" if DEFAULT_HOST_SELECTORS.key?(name)
57
+ normalized = normalize_strategy(name)
58
+ raise Errors::AwsError, "Cannot override default host selection strategy: '#{name}'" if DEFAULT_HOST_SELECTORS.key?(normalized)
54
59
 
55
- @strategies[name] = selector
60
+ @strategies[normalized] = selector
56
61
  end
57
62
 
58
63
  # @param name [String] strategy name
59
64
  # @return [#select_host, nil] the registered selector, or nil if the name is unknown
60
65
  def host_selector(name)
61
- @strategies[name]
66
+ @strategies[normalize_strategy(name)]
67
+ end
68
+
69
+ # Raises when no selector is registered under the given strategy name. Plugins call this when
70
+ # they are constructed, because they treat a selection failure during connect or failover as
71
+ # "no eligible host", which would otherwise hide a misspelled strategy.
72
+ #
73
+ # @param name [String] strategy name
74
+ def validate_strategy!(name)
75
+ raise unsupported_strategy_error(name) if host_selector(name).nil?
76
+ end
77
+
78
+ def normalize_strategy(name)
79
+ name.to_s.strip.downcase
80
+ end
81
+
82
+ # @param name [String] the strategy name as configured, reported without normalization
83
+ # @return [Errors::AwsError]
84
+ def unsupported_strategy_error(name)
85
+ Errors::AwsError.new(
86
+ "Unsupported host selection strategy: '#{name}'. Registered strategies: #{@strategies.keys.sort.join(', ')}. " \
87
+ 'Register a custom strategy with AwsAdvancedRubyDriverWrapper::Services::HostService.register_host_selector ' \
88
+ 'before opening a connection that uses it.'
89
+ )
62
90
  end
63
91
 
64
92
  # Removes every non-default host selector. For testing only.
@@ -76,9 +104,8 @@ module AwsAdvancedRubyDriverWrapper
76
104
  # @param props [Hash, nil]
77
105
  # @return [Host::HostInfo]
78
106
  def select_host(hosts, role, strategy, props = nil)
79
- normalized = strategy.to_s.strip.downcase
80
- selector = self.class.host_selector(normalized)
81
- raise Errors::AwsError, "Unsupported host selection strategy: '#{strategy}'" if selector.nil?
107
+ selector = self.class.host_selector(strategy)
108
+ raise self.class.unsupported_strategy_error(strategy) if selector.nil?
82
109
 
83
110
  selector.select_host(hosts, role, props)
84
111
  end
@@ -90,8 +117,7 @@ module AwsAdvancedRubyDriverWrapper
90
117
  def hosts
91
118
  rules = @service_container.storage_service.get_if_registered(
92
119
  :custom_endpoint_allowed_blocked,
93
- @service_container.connection_service.initial_host_info&.url,
94
- register_access: false
120
+ @service_container.connection_service.initial_host_info&.url
95
121
  )
96
122
  return @all_hosts if rules.nil?
97
123
 
@@ -144,7 +144,7 @@ module AwsAdvancedRubyDriverWrapper
144
144
  # Processes events from the event publisher.
145
145
  # @param event [Event] the event to process.
146
146
  def process_event(event)
147
- return unless event == Utils::Events::DataAccessEvent
147
+ return unless event.is_a?(Utils::Events::DataAccessEvent)
148
148
 
149
149
  handle_data_access_event(event)
150
150
  end
@@ -19,10 +19,13 @@ require_relative '../errors'
19
19
  require_relative '../property_definition'
20
20
  require_relative '../ruby_method'
21
21
  require_relative '../utils/sql_encoding'
22
+ require_relative '../utils/telemetry/telemetry_const'
22
23
  require_relative '../plugins/default_plugin'
23
24
  require_relative '../plugins/failover_plugin'
24
25
  require_relative '../plugins/gdb/gdb_failover_plugin'
25
26
  require_relative '../plugins/iam_auth_plugin'
27
+ require_relative '../plugins/federated_auth/federated_auth_plugin'
28
+ require_relative '../plugins/federated_auth/okta_auth_plugin'
26
29
  require_relative '../plugins/initial_connection_strategy_plugin'
27
30
  require_relative '../plugins/kms_encryption/kms_encryption_plugin'
28
31
  require_relative '../plugins/secrets_manager_plugin'
@@ -44,6 +47,8 @@ module AwsAdvancedRubyDriverWrapper
44
47
  'failover' => Plugins::FailoverPlugin,
45
48
  'gdb_failover' => Plugins::Gdb::GdbFailoverPlugin,
46
49
  'iam' => Plugins::IamAuthPlugin,
50
+ 'federated_auth' => Plugins::FederatedAuth::FederatedAuthPlugin,
51
+ 'okta' => Plugins::FederatedAuth::OktaAuthPlugin,
47
52
  'initial_connection' => Plugins::InitialConnectionStrategyPlugin,
48
53
  'kms_encryption' => Plugins::KmsEncryptionPlugin,
49
54
  'secrets_manager' => Plugins::SecretsManagerPlugin
@@ -60,9 +65,18 @@ module AwsAdvancedRubyDriverWrapper
60
65
  Plugins::Gdb::GdbFailoverPlugin => 500,
61
66
  Plugins::IamAuthPlugin => 1800,
62
67
  Plugins::SecretsManagerPlugin => 1900,
68
+ Plugins::FederatedAuth::FederatedAuthPlugin => 2000,
69
+ Plugins::FederatedAuth::OktaAuthPlugin => 2010,
63
70
  Plugins::KmsEncryptionPlugin => 2050
64
71
  }
65
72
 
73
+ # Plugins that authenticate by minting an RDS IAM token into the shared IAM token cache.
74
+ IAM_TOKEN_PLUGIN_CLASSES = [
75
+ Plugins::IamAuthPlugin,
76
+ Plugins::FederatedAuth::FederatedAuthPlugin,
77
+ Plugins::FederatedAuth::OktaAuthPlugin
78
+ ].freeze
79
+
66
80
  class << self
67
81
  def register_plugin(plugin_code, plugin_class, weight: WEIGHT_RELATIVE_TO_PRIOR_PLUGIN)
68
82
  @plugin_classes[plugin_code] = plugin_class
@@ -73,30 +87,35 @@ module AwsAdvancedRubyDriverWrapper
73
87
  end
74
88
 
75
89
  def initialize(service_container)
90
+ @telemetry_factory = service_container.telemetry_factory
76
91
  @plugins = load_plugins(service_container)
77
92
  @pipeline_cache = {}
78
93
  end
79
94
 
80
95
  def connect(host_info, driver_props, is_initial_connection, plugin_to_skip: nil)
81
- execute_with_subscribed_plugins(
82
- 'connect',
83
- lambda do |plugin, next_plugin_callable|
84
- plugin.connect(host_info, driver_props, is_initial_connection, next_plugin_callable)
85
- end,
86
- NOOP_CALLABLE,
87
- plugin_to_skip:
88
- )
96
+ @telemetry_factory.in_context('connect', Utils::Telemetry::TraceLevel::NESTED) do
97
+ execute_with_subscribed_plugins(
98
+ 'connect',
99
+ lambda do |plugin, next_plugin_callable|
100
+ plugin.connect(host_info, driver_props, is_initial_connection, next_plugin_callable)
101
+ end,
102
+ NOOP_CALLABLE,
103
+ plugin_to_skip:
104
+ )
105
+ end
89
106
  end
90
107
 
91
108
  def internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, plugin_to_skip: nil)
92
- execute_with_subscribed_plugins(
93
- 'internal_connect',
94
- lambda do |plugin, next_plugin_callable|
95
- plugin.internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, next_plugin_callable)
96
- end,
97
- NOOP_CALLABLE,
98
- plugin_to_skip:
99
- )
109
+ @telemetry_factory.in_context('internal_connect', Utils::Telemetry::TraceLevel::NESTED) do
110
+ execute_with_subscribed_plugins(
111
+ 'internal_connect',
112
+ lambda do |plugin, next_plugin_callable|
113
+ plugin.internal_connect(host_info, driver_props, wrapper_props, is_initial_connection, next_plugin_callable)
114
+ end,
115
+ NOOP_CALLABLE,
116
+ plugin_to_skip:
117
+ )
118
+ end
100
119
  end
101
120
 
102
121
  # The context of the call currently being executed, for plugins that need to know more about
@@ -140,15 +159,18 @@ module AwsAdvancedRubyDriverWrapper
140
159
  Thread.current[CURRENT_CALL_CONTEXT_KEY] = context
141
160
 
142
161
  begin
143
- execute_with_subscribed_plugins(
144
- method_name,
145
- lambda do |plugin, next_plugin_callable|
146
- # Read from the context rather than from args and block, so that a plugin which
147
- # replaced either is honoured by the plugins after it and by the target method.
148
- plugin.execute(method_name, next_plugin_callable, *context.args, **kwargs, &context.block)
149
- end,
150
- target_callable
151
- )
162
+ @telemetry_factory.in_context(method_name, Utils::Telemetry::TraceLevel::TOP_LEVEL, record_result: true) do |telemetry_context|
163
+ telemetry_context.set_attribute(Utils::Telemetry::TelemetryConst::RUBY_CALL_ANNOTATION, method_name)
164
+ execute_with_subscribed_plugins(
165
+ method_name,
166
+ lambda do |plugin, next_plugin_callable|
167
+ # Read from the context rather than from args and block, so that a plugin which
168
+ # replaced either is honoured by the plugins after it and by the target method.
169
+ plugin.execute(method_name, next_plugin_callable, *context.args, **kwargs, &context.block)
170
+ end,
171
+ target_callable
172
+ )
173
+ end
152
174
  ensure
153
175
  Thread.current[CURRENT_CALL_CONTEXT_KEY] = previous_context
154
176
  end
@@ -164,6 +186,12 @@ module AwsAdvancedRubyDriverWrapper
164
186
  @plugins.any?(plugin_class)
165
187
  end
166
188
 
189
+ # Whether any plugin that mints RDS IAM tokens into the shared cache is active.
190
+ # @see IAM_TOKEN_PLUGIN_CLASSES
191
+ def iam_token_plugin_in_use?
192
+ IAM_TOKEN_PLUGIN_CLASSES.any? { |plugin_class| plugin_in_use?(plugin_class) }
193
+ end
194
+
167
195
  private
168
196
 
169
197
  def load_plugins(service_container)
@@ -185,7 +213,7 @@ module AwsAdvancedRubyDriverWrapper
185
213
  end
186
214
 
187
215
  def ensure_single_auth_plugin(plugin_code_list)
188
- auth_plugins_used = plugin_code_list & %w[iam secrets_manager].freeze
216
+ auth_plugins_used = plugin_code_list & %w[iam secrets_manager federated_auth okta].freeze
189
217
 
190
218
  return unless auth_plugins_used.length > 1
191
219
 
@@ -245,29 +273,52 @@ module AwsAdvancedRubyDriverWrapper
245
273
 
246
274
  # Builds the plugin pipeline function chain. The pipeline allows plugins to perform logic both before and after
247
275
  # the target driver function is called.
276
+ #
277
+ # When traces are recorded, each plugin's part of the call is recorded as a span named after the
278
+ # plugin. That is decided here, once per pipeline, so a pipeline built without traces does no
279
+ # tracing work at all.
248
280
  def make_pipeline(target_method_name)
249
281
  subscribed_plugins = @plugins.select do |p|
250
282
  p.subscribed_methods.include?('*') || p.subscribed_methods.include?(target_method_name)
251
283
  end
252
284
  raise Errors::AwsError, 'Plugin pipeline is nil' if subscribed_plugins.empty?
253
285
 
286
+ traced = @telemetry_factory.traces_in_use?
254
287
  base_plugin = subscribed_plugins.last
288
+ base_plugin_name = plugin_name(base_plugin) if traced
255
289
  base = lambda do |plugin_callable, target_driver_callable, *, **|
256
- plugin_callable.call(base_plugin, target_driver_callable)
290
+ if traced
291
+ call_traced(plugin_callable, base_plugin, base_plugin_name, target_driver_callable)
292
+ else
293
+ plugin_callable.call(base_plugin, target_driver_callable)
294
+ end
257
295
  end
258
296
 
259
297
  subscribed_plugins[0...-1].reverse.reduce(base) do |next_plugin_callable, plugin|
298
+ name = plugin_name(plugin) if traced
260
299
  lambda do |plugin_callable, target_driver_callable, method_name, plugin_to_skip|
261
300
  if plugin_to_skip == plugin
262
301
  next_plugin_callable.call(plugin_callable, target_driver_callable, method_name, plugin_to_skip)
263
302
  else
264
- plugin_callable.call(plugin, lambda do
303
+ call_next = lambda do
265
304
  next_plugin_callable.call(plugin_callable, target_driver_callable, method_name, plugin_to_skip)
266
- end)
305
+ end
306
+ traced ? call_traced(plugin_callable, plugin, name, call_next) : plugin_callable.call(plugin, call_next)
267
307
  end
268
308
  end
269
309
  end
270
310
  end
311
+
312
+ # A plugin's span is not marked as failed when the call raises: the error passes out through every
313
+ # plugin's span, and is recorded on the call's top-level span instead.
314
+ def call_traced(plugin_callable, plugin, name, next_plugin_callable)
315
+ @telemetry_factory.in_context(name, Utils::Telemetry::TraceLevel::NESTED) { plugin_callable.call(plugin, next_plugin_callable) }
316
+ end
317
+
318
+ # The plugin's code (such as +failover+), or the class name of a plugin that has none.
319
+ def plugin_name(plugin)
320
+ self.class.plugin_classes.key(plugin.class) || plugin.class.name&.split('::')&.last || plugin.class.to_s
321
+ end
271
322
  end
272
323
  end
273
324
  end