aws_advanced_ruby_driver_wrapper 1.0.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 (150) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +175 -0
  4. data/NOTICE +1 -0
  5. data/README.md +168 -0
  6. data/THIRD-PARTY-LICENSES +473 -0
  7. data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
  26. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
  27. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
  29. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
  31. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
  32. data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
  33. data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
  34. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
  35. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
  36. data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
  40. data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
  41. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
  45. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
  53. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
  54. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
  55. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
  57. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
  59. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
  60. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
  61. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
  62. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
  64. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
  66. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
  67. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
  68. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
  69. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
  80. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
  85. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
  86. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
  87. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
  88. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
  89. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
  90. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
  91. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
  92. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
  93. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
  94. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
  95. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
  96. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
  97. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
  98. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
  99. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
  100. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
  101. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
  102. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
  103. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
  104. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
  105. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
  106. data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
  107. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
  108. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
  109. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
  110. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
  111. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
  112. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
  113. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
  114. data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
  115. data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
  116. data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
  117. data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
  118. data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
  119. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
  120. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
  121. data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
  122. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
  123. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
  124. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
  125. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
  126. data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
  127. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
  128. data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
  129. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
  130. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
  131. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
  132. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
  133. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
  134. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
  135. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
  136. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
  137. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
  138. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
  139. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
  140. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
  141. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
  142. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
  143. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
  144. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
  145. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
  146. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
  147. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
  148. data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
  149. data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
  150. metadata +227 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module AwsAdvancedRubyDriverWrapper
18
+ module Utils
19
+ module Events
20
+ # Event indicating data was accessed. Batched delivery (not immediate).
21
+ DataAccessEvent = Data.define(:data_type, :key) do
22
+ def immediate_delivery? = false
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module AwsAdvancedRubyDriverWrapper
18
+ module Utils
19
+ module Events
20
+ # Event signaling that monitors for a cluster should be reset. Immediate delivery.
21
+ MonitorResetEvent = Data.define(:cluster_id, :endpoints) do
22
+ def immediate_delivery? = true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative '../host/host_role'
18
+ require_relative 'rds_url_type'
19
+ require_relative 'rds_utils'
20
+ require_relative 'aurora_topology_utils'
21
+
22
+ module AwsAdvancedRubyDriverWrapper
23
+ module Utils
24
+ # Topology utilities specific to Global Aurora database clusters.
25
+ # Global Aurora clusters span multiple AWS regions, so topology queries return a region column
26
+ # and instance templates are keyed by region.
27
+ class GlobalAuroraTopologyUtils < AuroraTopologyUtils
28
+ # Query the database for topology information across global Aurora cluster regions.
29
+ #
30
+ # @param conn [Object] the connection to use to query the database.
31
+ # @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the HostInfo used to initially connect.
32
+ # @param instance_templates_by_region [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>]
33
+ # a map of AWS region to instance template HostInfo for constructing hosts.
34
+ # @return [Array<AwsAdvancedRubyDriverWrapper::Host::HostInfo>, nil] a list of HostInfo objects or nil.
35
+ def query_global_topology(conn, initial_host_info, instance_templates_by_region)
36
+ results = @dialect.execute(conn, @dialect.topology_query)
37
+
38
+ if results.fields.empty?
39
+ # We expect at least 4 columns. Note that the server may return 0 columns if failover has occurred.
40
+ logger.debug('The topology query returned a result with 0 columns. ' \
41
+ 'This may occur if the topology query is executed when the server is failing over.')
42
+ return nil
43
+ end
44
+
45
+ verify_writer(build_global_hosts(results, initial_host_info, instance_templates_by_region))
46
+ rescue StandardError => e
47
+ raise "Invalid topology query: #{e.message}"
48
+ end
49
+
50
+ # Retrieves the AWS region for a given instance ID.
51
+ #
52
+ # @param instance_id [String] the database instance identifier.
53
+ # @param conn [Object] the database connection.
54
+ # @return [String, nil] the AWS region, or nil if it cannot be determined.
55
+ def query_region(instance_id, conn)
56
+ results = @dialect.execute_with_params(conn, @dialect.region_by_instance_id_query, [instance_id])
57
+ return nil if results.nil? || results.none?
58
+
59
+ row = results.first
60
+ region = row.values.first
61
+ region.nil? || region.to_s.empty? ? nil : region.to_s
62
+ end
63
+
64
+ # Parses a comma-separated instance templates string into a region-to-HostInfo map.
65
+ # Each comma-separated entry in the string can be one of the following formats:
66
+ # - "host_pattern"
67
+ # - "host_pattern:port"
68
+ # - "[region]host_pattern"
69
+ # - "[region]host_pattern:port"
70
+ #
71
+ # @param instance_templates_string [String] comma-separated list of instance template entries.
72
+ # @param host_validator [Proc] proc to validate each host pattern.
73
+ # @return [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>] map of region to instance template.
74
+ # @raise [AwsError] if the host or region in any of the template strings could not be parsed.
75
+ def parse_instance_templates(instance_templates_string, host_validator)
76
+ templates = {}
77
+
78
+ instance_templates_string.split(',').each do |entry|
79
+ entry = entry.strip
80
+ region, host_pattern, port = extract_region_host_and_port(entry)
81
+ raise Errors::AwsError, "Unable to parse region from '#{entry}'" if region.nil? || region.empty?
82
+ raise Errors::AwsError, "Unable to parse host from '#{entry}'" if host_pattern.nil? || host_pattern.empty?
83
+
84
+ # The region may be supplied explicitly in square brackets (e.g. "[us-east-1]..."), validate it.
85
+ unless RdsUtils.valid_region?(region)
86
+ raise Errors::AwsError, "Unknown or misspelled AWS region '#{region}' in instance template '#{entry}'"
87
+ end
88
+
89
+ host_validator.call(host_pattern)
90
+ url_type = RdsUtils.identify_rds_type(host_pattern)
91
+ # assign HostRole of READER if using the reader cluster URL, otherwise assume a HostRole of WRITER
92
+ role = url_type == RdsUrlType::RDS_READER_CLUSTER ? Host::HostRole::READER : Host::HostRole::WRITER
93
+ templates[region.downcase] = Host::HostInfo.new(id: '?', host: host_pattern, port:, role:)
94
+ end
95
+
96
+ logger.debug("Detected global database patterns: #{templates}")
97
+ templates
98
+ end
99
+
100
+ private
101
+
102
+ # Process global Aurora topology query results into a list of HostInfo objects.
103
+ # Each row in 'results' includes a region column used to select the appropriate instance template.
104
+ #
105
+ # @param results [Object] the query result set (enumerable of row hashes).
106
+ # @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
107
+ # @param instance_templates_by_region [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>]
108
+ # map of region to instance template.
109
+ # @return [Array<AwsAdvancedRubyDriverWrapper::Host::HostInfo>, nil] the parsed hosts or nil on failure.
110
+ def build_global_hosts(results, initial_host_info, instance_templates_by_region)
111
+ hosts_map = {}
112
+
113
+ results.each do |row|
114
+ host = build_host_from_row(row, initial_host_info, instance_templates_by_region)
115
+ hosts_map[host.host] = host
116
+ rescue StandardError => e
117
+ logger.debug("Error processing topology query results: #{e.message}")
118
+ return nil
119
+ end
120
+
121
+ hosts_map.values
122
+ end
123
+
124
+ # Builds a HostInfo from a single global Aurora topology query result row.
125
+ #
126
+ # Expected columns: host_id, is_writer, instance_lag, aws_region.
127
+ #
128
+ # @param row [Hash] a single row from the topology query result.
129
+ # @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
130
+ # @param instance_templates_by_region [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>]
131
+ # map of region to instance template.
132
+ # @return [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the constructed host info.
133
+ # @raise [AwsError] if no template is found for the row's region.
134
+ def build_host_from_row(row, initial_host_info, instance_templates_by_region)
135
+ instance_id = row_value(row, 'instance_id')
136
+ is_writer = to_boolean(row_value(row, 'is_writer'))
137
+ lag = to_float(row_value(row, 'instance_lag'))
138
+ aws_region = row_value(row, 'aws_region').to_s.downcase
139
+
140
+ weight = (lag.round * 100)
141
+
142
+ instance_template = instance_templates_by_region[aws_region]
143
+ raise Errors::AwsError, "Cannot find instance template for region '#{aws_region}'" if instance_template.nil?
144
+
145
+ build_host(instance_id, instance_id, is_writer, weight, Time.now, initial_host_info, instance_template)
146
+ end
147
+
148
+ # Extracts the region, host pattern, and port from an instance template string.
149
+ # The region may be prefixed in square brackets or inferred from the host pattern, for example:
150
+ # - "[us-west-1]?.custom-host:5432"
151
+ # - "?.xyz.us-west-1.rds.amazonaws.com:5432"
152
+ #
153
+ # @param entry [String] a single instance template entry.
154
+ # @return [Array(String, String, Integer)] region, host_pattern, and port.
155
+ def extract_region_host_and_port(entry)
156
+ if entry.start_with?('[')
157
+ closing = entry.index(']')
158
+ raise ArgumentError, "Invalid instance template format: '#{entry}'" if closing.nil?
159
+
160
+ region = entry[1...closing]
161
+ host_pattern, port = parse_host_and_port(entry[(closing + 1)..])
162
+ else
163
+ host_pattern, port = parse_host_and_port(entry)
164
+ region = RdsUtils.rds_region(host_pattern)
165
+ end
166
+
167
+ [region, host_pattern, port]
168
+ end
169
+
170
+ # Parses "host_pattern" or "host_pattern:port" and returns [host_pattern, port].
171
+ def parse_host_and_port(value)
172
+ # Split from the right to handle host patterns that might not contain a colon.
173
+ # Only treat the last segment as a port if it's purely numeric.
174
+ last_colon = value.rindex(':')
175
+ if last_colon && value[(last_colon + 1)..].match?(/\A\d+\z/)
176
+ host_pattern = value[0...last_colon]
177
+ port = value[(last_colon + 1)..].to_i
178
+ [host_pattern, port]
179
+ else
180
+ [value, Host::HostInfo::NO_PORT]
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module AwsAdvancedRubyDriverWrapper
18
+ module Utils
19
+ module HostListUtils
20
+ def self.writer(hosts) = hosts&.find { |host_info| host_info.role == HostRole::WRITER }
21
+
22
+ def self.contains_url?(hosts, url) = hosts&.any? { |host_info| host_info.url == url } || false
23
+
24
+ def self.to_host_urls_s(hosts) = hosts&.map(&:url)&.join(',') || ''
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'uri'
18
+ require_relative 'rds_utils'
19
+ require_relative 'rds_url_type'
20
+ require_relative '../property_definition'
21
+ require_relative '../logging'
22
+
23
+ module AwsAdvancedRubyDriverWrapper
24
+ module Utils
25
+ module IamAuthUtils
26
+ module_function
27
+
28
+ TokenEntry = Data.define(:token, :expires_at) do
29
+ # Redact the token so it is never exposed if an instance is logged,
30
+ # interpolated, or rendered in a backtrace.
31
+ def inspect
32
+ "#<data TokenEntry token=#{AwsAdvancedRubyDriverWrapper::REDACTED.inspect}, " \
33
+ "expires_at=#{expires_at.inspect}>"
34
+ end
35
+ alias_method :to_s, :inspect
36
+
37
+ # `pp` / PrettyPrint does not call #inspect; route them through the
38
+ # redacted representation so `pp entry` cannot leak the token.
39
+ def pretty_print(pp)
40
+ pp.text(inspect)
41
+ end
42
+ end
43
+
44
+ EXPIRY_BUFFER_SEC = 60
45
+
46
+ def parse_token_expiry(token)
47
+ URI.decode_www_form(URI.parse("https://#{token}").query)
48
+ .filter_map { |(key, value)| Integer(value, 10) if key.downcase == 'x-amz-expires' }
49
+ .first
50
+ rescue StandardError
51
+ nil
52
+ end
53
+
54
+ def build_token_entry(token, fallback_expiry_sec)
55
+ expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) +
56
+ (parse_token_expiry(token) || fallback_expiry_sec) -
57
+ EXPIRY_BUFFER_SEC
58
+ TokenEntry.new(token:, expires_at:)
59
+ end
60
+
61
+ def valid_entry?(entry)
62
+ entry.is_a?(TokenEntry) && Process.clock_gettime(Process::CLOCK_MONOTONIC) < entry.expires_at
63
+ end
64
+
65
+ def region_for(host:, props:, rds_type:, credentials_provider:, rds_client_func: nil)
66
+ explicit = PropertyDefinition::IAM_REGION.get(props)
67
+ return explicit if explicit && !explicit.empty?
68
+
69
+ return RdsUtils.rds_region(host) unless rds_type == RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER
70
+
71
+ region_from_global_cluster(host, credentials_provider, rds_client: rds_client_func&.call)
72
+ end
73
+
74
+ def region_from_global_cluster(host, credentials_provider, rds_client: nil)
75
+ cluster_id = RdsUtils.rds_cluster_id(host)
76
+ client = rds_client || Aws::RDS::Client.new(credentials: credentials_provider)
77
+ arn = client.describe_global_clusters(global_cluster_identifier: cluster_id)
78
+ .global_clusters
79
+ .flat_map(&:global_cluster_members)
80
+ .find(&:is_writer)
81
+ &.db_cluster_arn
82
+ # The partition segment of an ARN varies across AWS partitions, so match any of them.
83
+ arn&.match(/\Aarn:aws(?:-[a-z]+)*:rds:(?<region>[^:]+)/)&.[](:region)
84
+ end
85
+
86
+ def generate_token(region:, hostname:, port:, user:, credentials_provider:)
87
+ Aws::RDS::AuthTokenGenerator
88
+ .new(credentials: credentials_provider)
89
+ .auth_token(region:, endpoint: "#{hostname}:#{port}", user_name: user)
90
+ end
91
+
92
+ def resolve_host(iam_host, host_info)
93
+ return iam_host if iam_host && !iam_host.empty?
94
+
95
+ host_info.host
96
+ end
97
+
98
+ def resolve_port(iam_default_port, host_info, dialect_default_port)
99
+ iam_port = iam_default_port.to_i
100
+ return iam_port if iam_port.positive?
101
+
102
+ return host_info.port.to_i if host_info.port_specified?
103
+
104
+ dialect_default_port
105
+ end
106
+
107
+ def cache_key(region, host, port, user, credentials_identity)
108
+ "#{region}:#{host}:#{port}:#{user}:#{credentials_identity}"
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'topology_utils'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module Utils
21
+ # Topology utilities specific to Multi-AZ database clusters.
22
+ # In Multi-AZ clusters, the writer is identified via a separate query rather than
23
+ # from a column in the topology results.
24
+ class MultiAzTopologyUtils
25
+ include TopologyUtils
26
+
27
+ def initialize(dialect:)
28
+ raise ArgumentError, 'dialect cannot be nil' if dialect.nil?
29
+
30
+ @dialect = dialect
31
+ end
32
+
33
+ # Evaluate whether the given connection is to a writer instance.
34
+ # In Multi-AZ, when connected to a writer, the writer ID query returns no rows.
35
+ #
36
+ # @param conn [Object] the connection to evaluate.
37
+ # @return [Boolean] true if the connection is to a writer instance, false otherwise.
38
+ def writer_instance?(conn)
39
+ results = @dialect.execute(conn, @dialect.writer_id_query)
40
+ # When connected to a writer, the result is empty; otherwise it contains a single row.
41
+ results.nil? || results.none?
42
+ end
43
+
44
+ # Process Multi-AZ topology query results into a list of HostInfo objects.
45
+ # First determines the writer ID, then parses each row using endpoint and id columns.
46
+ #
47
+ # @param conn [Object] the database connection.
48
+ # @param results [Object] the query result set (enumerable of row hashes).
49
+ # @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
50
+ # @param instance_template [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the template for building hosts.
51
+ # @return [Array<AwsAdvancedRubyDriverWrapper::Host::HostInfo>, nil] the parsed hosts or nil on failure.
52
+ def build_hosts(conn, results, initial_host_info, instance_template)
53
+ hosts_map = {}
54
+ writer_id = query_writer_id(conn)
55
+
56
+ results.each do |row|
57
+ host = build_host_from_row(row, initial_host_info, instance_template, writer_id)
58
+
59
+ # Ensure newer records replace the older ones if there are duplicate keys.
60
+ existing = hosts_map[host.host]
61
+ if existing.nil? || (existing.last_update_time && host.last_update_time &&
62
+ existing.last_update_time < host.last_update_time)
63
+ hosts_map[host.host] = host
64
+ end
65
+ rescue StandardError => e
66
+ logger.debug("Error processing topology query results: #{e.message}")
67
+ return nil
68
+ end
69
+
70
+ hosts_map.values
71
+ end
72
+
73
+ private
74
+
75
+ # Determines the writer instance ID by querying the database.
76
+ # The writer ID query returns the writer ID when connected to a reader.
77
+ # When connected to a writer, the query returns no rows, so we fall back to
78
+ # getting the host ID of the current connection.
79
+ #
80
+ # @param conn [Object] the database connection.
81
+ # @return [String, nil] the writer instance ID, or nil if it cannot be determined.
82
+ def query_writer_id(conn)
83
+ results = @dialect.execute(conn, @dialect.writer_id_query)
84
+
85
+ if results&.any?
86
+ row = results.first
87
+ writer_id_column = @dialect.respond_to?(:writer_id_column_name) ? @dialect.writer_id_column_name : 'writer_id'
88
+ writer_id = row_value(row, writer_id_column)
89
+ return writer_id unless writer_id.nil? || writer_id.to_s.empty?
90
+ end
91
+
92
+ # The writer ID is only returned when connected to a reader.
93
+ # If the query does not return a value, we are connected to the writer.
94
+ instance_id, = @dialect.instance_identity(conn)
95
+ instance_id
96
+ rescue StandardError
97
+ nil
98
+ end
99
+
100
+ # Builds a HostInfo from a single Multi-AZ topology query result row.
101
+ # Expected columns: endpoint.
102
+ #
103
+ # @param row [Hash] a single row from the topology query result.
104
+ # @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
105
+ # @param instance_template [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the template for building hosts.
106
+ # @param writer_id [String, nil] the writer instance ID.
107
+ # @return [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the constructed host info.
108
+ def build_host_from_row(row, initial_host_info, instance_template, writer_id)
109
+ instance_id = row_value(row, 'instance_id')
110
+ endpoint = row_value(row, 'endpoint')
111
+ # Extract instance name from the endpoint (everything before the first dot).
112
+ instance_name = endpoint&.split('.')&.first
113
+ build_host(instance_id, instance_name, instance_id == writer_id, 0, Time.now, initial_host_info, instance_template)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module AwsAdvancedRubyDriverWrapper
18
+ module Utils
19
+ module Parser
20
+ module EncryptionAnnotationParser
21
+ # Both placeholder styles are accepted, so that the same annotation works for mysql2's
22
+ # positional +?+ and pg's numbered +$1+.
23
+ ANNOTATION_PATTERN = %r{/\*@encrypt:([\w.]+)\*/\s*(?:\?|\$(\d+))}
24
+ STRIP_PATTERN = %r{/\*@encrypt:[\w.]+\*/\s*}
25
+ # The quote characters a literal can be wrapped in. A backtick quotes an identifier rather than
26
+ # a value, but is included so a +?+ inside a backtick-quoted name is not miscounted either.
27
+ QUOTES = ["'", '"', '`'].freeze
28
+
29
+ module_function
30
+
31
+ # Returns a 1-based map of parameter index => "table.column" for each
32
+ # /*@encrypt:table.column*/ ? or /*@encrypt:table.column*/ $n placeholder.
33
+ # @param sql [String]
34
+ # @return [Hash{Integer => String}]
35
+ def parse_annotations(sql)
36
+ return {} unless sql.is_a?(String) && !sql.empty?
37
+
38
+ question_marks = unquoted_question_mark_positions(sql)
39
+
40
+ sql.to_enum(:scan, ANNOTATION_PATTERN)
41
+ .each_with_object({}) do |_, result|
42
+ match = Regexp.last_match
43
+ next unless match
44
+
45
+ # A numbered placeholder states its own position; a question mark is located by
46
+ # counting the question marks that precede it.
47
+ if match[2]
48
+ result[match[2].to_i] = match[1]
49
+ else
50
+ param_index = question_marks.index(match.end(0) - 1)
51
+ result[param_index + 1] = match[1] if param_index
52
+ end
53
+ end
54
+ end
55
+
56
+ def strip_annotations(sql)
57
+ return sql unless sql.is_a?(String) && !sql.empty?
58
+
59
+ sql.gsub(STRIP_PATTERN, '')
60
+ end
61
+
62
+ def annotations?(sql)
63
+ return false unless sql.is_a?(String) && !sql.empty?
64
+
65
+ ANNOTATION_PATTERN.match?(sql)
66
+ end
67
+
68
+ # The character positions of every +?+ that is a bind placeholder, skipping any inside a quoted
69
+ # literal so a literal question mark does not shift the count of the placeholder an annotation
70
+ # points at. A doubled quote reads as one closing and one opening quote, and a backslash escapes
71
+ # the next character except inside a backtick, which matches how the drivers read a literal.
72
+ def unquoted_question_mark_positions(sql)
73
+ positions = []
74
+ quote = nil
75
+ index = 0
76
+
77
+ while index < sql.length
78
+ char = sql[index]
79
+ if quote
80
+ if char == '\\' && quote != '`'
81
+ index += 2
82
+ next
83
+ end
84
+
85
+ quote = nil if char == quote
86
+ elsif QUOTES.include?(char)
87
+ quote = char
88
+ elsif char == '?'
89
+ positions << index
90
+ end
91
+ index += 1
92
+ end
93
+
94
+ positions
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end